diff --git a/docs/superpowers/reports/p1r-api-coverage.json b/docs/superpowers/reports/p1r-api-coverage.json index 05bd7f2c..4991927d 100644 --- a/docs/superpowers/reports/p1r-api-coverage.json +++ b/docs/superpowers/reports/p1r-api-coverage.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-05-25T13:33:07+00:00", + "generatedAt": "2026-05-25T13:45:55+00:00", "sourceContracts": [ "docs/api-contracts/account/openapi.yaml", "docs/api-contracts/ai/openapi.yaml", @@ -12,11 +12,11 @@ "summary": { "totalOperations": 233, "completedOperations": 100, - "incompleteOperations": 1, - "needsVerificationOperations": 132, + "incompleteOperations": 0, + "needsVerificationOperations": 133, "catchAllOperations": 0, "genericPersistenceOperations": 0, - "ssePlaceholderOperations": 1, + "ssePlaceholderOperations": 0, "missingOperations": 0, "blockedOperations": 0 }, @@ -9667,13 +9667,18 @@ "requiresCommandId": false, "externalDependencies": [], "targetStage": "P1R-7 End-to-End Acceptance", - "implementationStatus": "sse_placeholder", - "completionStatus": "incomplete", + "implementationStatus": "dedicated", + "completionStatus": "needs_verification", "controllerFiles": [ - "muse-cloud/muse-module-content/muse-module-content-server/src/main/java/cn/iocoder/muse/module/content/controller/app/AppMuseEventsController.java" + "muse-cloud/muse-module-events/muse-module-events-server/src/main/java/cn/iocoder/muse/module/events/controller/app/AppMuseEventsController.java" ], - "serviceFiles": [], - "notes": "SSE endpoint returns placeholder events and needs P1R real task stream implementation." + "serviceFiles": [ + "muse-cloud/muse-module-events/muse-module-events-server/src/main/java/cn/iocoder/muse/module/events/application/publish/EventsPublishService.java", + "muse-cloud/muse-module-events/muse-module-events-server/src/main/java/cn/iocoder/muse/module/events/application/publish/EventsPublishServiceImpl.java", + "muse-cloud/muse-module-events/muse-module-events-server/src/main/java/cn/iocoder/muse/module/events/application/stream/EventsStreamService.java", + "muse-cloud/muse-module-events/muse-module-events-server/src/main/java/cn/iocoder/muse/module/events/application/stream/EventsStreamServiceImpl.java" + ], + "notes": "Dedicated entry exists; later P1R stage must verify DTO, state machine, persistence, and real external closure." }, { "domain": "knowledge", diff --git a/docs/superpowers/reports/p1r-api-coverage.md b/docs/superpowers/reports/p1r-api-coverage.md index d0b94d90..fbd01c36 100644 --- a/docs/superpowers/reports/p1r-api-coverage.md +++ b/docs/superpowers/reports/p1r-api-coverage.md @@ -1,13 +1,13 @@ # P1R API Coverage Report -- Generated at: `2026-05-25T13:33:07+00:00` +- Generated at: `2026-05-25T13:45:55+00:00` - Total operations: `233` - Completed: `100` -- Needs verification: `132` -- Incomplete: `1` +- Needs verification: `133` +- Incomplete: `0` - Catch-all: `0` - Generic persistence: `0` -- SSE placeholder: `1` +- SSE placeholder: `0` - Missing: `0` ## Operations @@ -139,7 +139,7 @@ | content | app | POST | `/app-api/muse/works/{workId}/planning/style-checks` | `createStyleCheck` | dedicated | needs_verification | P1R-1 Content Real API | | content | app | GET | `/app-api/muse/works/{workId}/planning/style-checks/{jobId}` | `getStyleCheckResult` | dedicated | needs_verification | P1R-1 Content Real API | | content | app | PUT | `/app-api/muse/works/{workId}/planning/{sectionKey}` | `savePlanningItem` | dedicated | needs_verification | P1R-1 Content Real API | -| events | app | GET | `/app-api/muse/events` | `streamEvents` | sse_placeholder | incomplete | P1R-7 End-to-End Acceptance | +| events | app | GET | `/app-api/muse/events` | `streamEvents` | dedicated | needs_verification | P1R-7 End-to-End Acceptance | | knowledge | admin | GET | `/admin-api/muse/knowledge/drafts` | `listKnowledgeDraftsGovernance` | dedicated | completed | P1R-5 Knowledge Real API | | knowledge | admin | GET | `/admin-api/muse/knowledge/global-kbs` | `listGlobalKnowledgeBases` | dedicated | completed | P1R-5 Knowledge Real API | | knowledge | admin | POST | `/admin-api/muse/knowledge/global-kbs` | `createGlobalKnowledgeBase` | dedicated | completed | P1R-5 Knowledge Real API | @@ -250,4 +250,3 @@ ## Incomplete Or Blocked -- `streamEvents` `GET /app-api/muse/events`: sse_placeholder - SSE endpoint returns placeholder events and needs P1R real task stream implementation. diff --git a/muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rEventsRealApiGateTest.java b/muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rEventsRealApiGateTest.java new file mode 100644 index 00000000..78a7fe8b --- /dev/null +++ b/muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rEventsRealApiGateTest.java @@ -0,0 +1,156 @@ +package cn.iocoder.muse.server.framework.api; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +/** + * P1R-7a Task 8 Events 真实 API 覆盖门禁。 + * + *

该测试只证明 Events streamEvents 已退出 SSE placeholder, + * 并进入 dedicated / needs_verification;它不代表 P1R-7a 已获准标记为 completed。

+ */ +class P1rEventsRealApiGateTest { + + /** P1R-7a 当前只允许 Events 域拥有 streamEvents 一个 operation。 */ + private static final int EXPECTED_EVENTS_OPERATION_COUNT = 1; + + /** P1R-6 Market 合同固定 operation 数量,P1R-7a 不能顺手改变 Market 覆盖状态。 */ + private static final int EXPECTED_MARKET_OPERATION_COUNT = 32; + + /** Events 真实入口门禁必须拒绝的占位或缺失状态。 */ + private static final Set NON_REAL_IMPLEMENTATION_STATUSES = Set.of( + "sse_placeholder", + "generic_persistence", + "catch_all", + "missing" + ); + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void should_keep_events_inventory_limited_to_stream_events() throws IOException { + JsonNode eventsOperations = readEventsOperations(); + + assertEquals(EXPECTED_EVENTS_OPERATION_COUNT, eventsOperations.size(), + "P1R-7a Events 域只能包含 streamEvents 一个 operation"); + + JsonNode streamEvents = eventsOperations.get(0); + assertEquals("streamEvents", streamEvents.path("operationId").asText(), + "P1R-7a Events 唯一 operation 必须是 streamEvents"); + assertEquals("GET", streamEvents.path("method").asText(), + "streamEvents 必须保持 GET 方法"); + assertEquals("/app-api/muse/events", streamEvents.path("path").asText(), + "streamEvents 必须保持既有合同路径"); + } + + @Test + void should_promote_stream_events_to_dedicated_needs_verification_only() throws IOException { + JsonNode streamEvents = readSingleEventsOperation(); + + assertEquals("dedicated", streamEvents.path("implementationStatus").asText(), + "streamEvents 必须进入 dedicated,不能继续使用 SSE placeholder"); + assertEquals("needs_verification", streamEvents.path("completionStatus").asText(), + "streamEvents 在 P1R-7a 只能保持 needs_verification,不能自动提升 completed"); + } + + @Test + void should_not_allow_events_to_keep_non_real_entrypoints() throws IOException { + for (JsonNode operation : readEventsOperations()) { + String operationId = operation.path("operationId").asText(""); + String implementationStatus = operation.path("implementationStatus").asText(); + + assertFalse(NON_REAL_IMPLEMENTATION_STATUSES.contains(implementationStatus), + operationId + " 不能继续使用 Events 非真实入口: " + implementationStatus); + } + } + + @Test + void should_keep_p1r7a_summary_at_needs_verification_boundary() throws IOException { + JsonNode summary = readReport().path("summary"); + + assertEquals(100, summary.path("completedOperations").asInt(), + "completedOperations 必须仍只来自已批准域,P1R-7a 不能自动提升 Events completed"); + assertEquals(133, summary.path("needsVerificationOperations").asInt(), + "needsVerificationOperations 必须包含 Events dedicated 后的待验收 operation"); + assertEquals(0, summary.path("incompleteOperations").asInt(), + "Events 退出 placeholder 后不应再留下 incomplete operation"); + assertEquals(0, summary.path("genericPersistenceOperations").asInt(), + "P1R-7a 后不能引入 generic_persistence operation"); + assertEquals(0, summary.path("ssePlaceholderOperations").asInt(), + "P1R-7a 后 summary.ssePlaceholderOperations 必须为 0"); + } + + @Test + void should_keep_all_market_operations_dedicated_and_needs_verification() throws IOException { + JsonNode marketOperations = readMarketOperations(); + + assertEquals(EXPECTED_MARKET_OPERATION_COUNT, marketOperations.size(), + "P1R-7a 不能改变 Market 32 个 operation 的覆盖清单"); + + for (JsonNode operation : marketOperations) { + String operationId = operation.path("operationId").asText(""); + assertEquals("dedicated", operation.path("implementationStatus").asText(), + "Market/" + operationId + " 必须继续保持 dedicated"); + assertEquals("needs_verification", operation.path("completionStatus").asText(), + "Market/" + operationId + " 必须继续保持 needs_verification,不能提前 completed"); + } + } + + private JsonNode readSingleEventsOperation() throws IOException { + JsonNode eventsOperations = readEventsOperations(); + assertEquals(EXPECTED_EVENTS_OPERATION_COUNT, eventsOperations.size(), + "P1R-7a Events 域只能包含 streamEvents 一个 operation"); + return eventsOperations.get(0); + } + + private JsonNode readEventsOperations() throws IOException { + return readOperationsByDomain("events"); + } + + private JsonNode readMarketOperations() throws IOException { + return readOperationsByDomain("market"); + } + + private JsonNode readOperationsByDomain(String domain) throws IOException { + ArrayNode matchingOperations = objectMapper.createArrayNode(); + for (JsonNode operation : readOperations()) { + if (domain.equals(operation.path("domain").asText())) { + matchingOperations.add(operation); + } + } + return matchingOperations; + } + + private JsonNode readOperations() throws IOException { + return readReport().path("operations"); + } + + private JsonNode readReport() throws IOException { + return objectMapper.readTree(findReportPath().toFile()); + } + + /** + * 从当前 Maven 执行目录逐级向上查找仓库根目录,避免 surefire 在不同模块目录执行时路径失效。 + */ + private static Path findReportPath() { + Path current = Path.of("").toAbsolutePath(); + for (Path candidate = current; candidate != null; candidate = candidate.getParent()) { + Path reportPath = candidate.resolve("docs/superpowers/reports/p1r-api-coverage.json"); + if (Files.exists(reportPath)) { + return reportPath; + } + } + return current.resolve("docs/superpowers/reports/p1r-api-coverage.json"); + } + +}