test(p1r): 收口 Events SSE 门禁

This commit is contained in:
zizi 2026-06-05 22:21:26 +08:00
parent f0ab1b4fce
commit fa29753972
3 changed files with 175 additions and 15 deletions

View File

@ -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",

View File

@ -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.

View File

@ -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 覆盖门禁
*
* <p>该测试只证明 Events streamEvents 已退出 SSE placeholder
* 并进入 dedicated / needs_verification它不代表 P1R-7a 已获准标记为 completed</p>
*/
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<String> 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("<missing-operationId>");
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("<missing-operationId>");
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");
}
}