test(p1r): 收口 Market 第一批 completed approval 门禁
只推进 Market 4 个 operation-level completed approval,保持 market domain-level allowlist 关闭,并保留剩余 28 个 Market operation 为 needs_verification。
This commit is contained in:
parent
f18116a1f5
commit
00327749a2
@ -145,3 +145,21 @@ P1R Account Completed Approval 执行版 fresh review 已双 PASS:James execut
|
||||
P1R Account 第一批 operation-level completed approval implementation 当前状态:用户已批准执行版四项前置条件,本轮只把 10 个 Account operation 加入 operation-level completed approval,未把 `account` 加入 domain-level allowlist,未修改 OpenAPI、Account 业务实现或数据库迁移。10 个 completed operation 为 `getCurrentUser`、`getProfile`、`updateProfile`、`adminListAccountUsers`、`adminGetUserEntitlements`、`getAppEntitlements`、`adminGetBalanceSnapshots`、`getAppBalanceSnapshots`、`adminCreateQuotaAdjustment`、`adminListQuotaAdjustments`;`getAppUsage`、`adminListUsageRecords` 和 New-API / FileService / attribution / Market projection / security event 相关 23 个 Account operation 继续 `dedicated/needs_verification`。scanner 仍保持 `APPROVED_COMPLETED_DOMAINS={"ai","knowledge"}`,只在 `APPROVED_COMPLETED_OPERATIONS` 追加上述 10 个 `account:*` key。TDD RED 已验证旧 report 下 `P1rApiCoverageReportTest,P1rAccountRealApiGateTest,P1rEventsRealApiGateTest,P1rAiRealApiGateTest,P1rKnowledgeRealApiGateTest,P1rMarketRealApiGateTest` 为 37 tests / 10 failures,失败点为 expected completed=127 but was 117、Account expected 10 completed but was 0;GREEN 后 scanner 输出 summary `233/127/106/0/0/0`,Account 为 `completed=10`、`needs_verification=23`。验证 evidence:`python3 muse-cloud/scripts/p1r-audit-api-coverage.py --check` 通过;Account focused verification 为 Member 114/114 pass 与 muse-server 19/19 pass;P1R mixed gates `P1rApiCoverageReportTest,P1rAccountRealApiGateTest,P1rEventsRealApiGateTest,P1rAiRealApiGateTest,P1rKnowledgeRealApiGateTest,P1rMarketRealApiGateTest,P1rContentRealApiGateTest,P1rMetaRealApiGateTest` 为 46/46 pass;`P1rAccountEventsPublishFlywayMigrationIT` 使用真实 PostgreSQL `_test` 库 `muse_p1r_account_completed_approval_test` 为 4/4 pass,输出 `flyway_success=true`、`migrations_executed=20`、`successful_migration_count=20`、`target_schema_version=20`、`flyway_latest=20:extend account events publish outbox`;增强证据 `AccountEventPublishOutboxServiceTest,AccountEventPublishWorkerTest` 为 19/19 pass;XML 防空跑覆盖 25 个目标 XML,合计 192 tests,failures/errors/skipped 均为 0。新增 memory:`docs/memorys/2026-06-11-P1RAccount状态推进.md`。当前尚未提交、未 push;下一步必须 fresh implementation spec/correctness review + fresh implementation quality/data-integrity/testing review,双 PASS 前不得提交、push 或宣称 Account 33/33、Market、Content 或总 P1R completed。
|
||||
|
||||
P1R Account 第一批 operation-level completed approval fresh implementation review 已双 PASS:Nash spec/correctness review PASS、Copernicus quality/data-integrity/testing review PASS,均无 P0/P1/P2/P3 blocker。Nash 已验证正确 worktree、scanner 未加入 `account` domain allowlist、只追加批准的 10 个 `account:*` operation key、report 为 `233/127/106/0/0/0` 且 Account `10 completed / 23 needs_verification`、usage/New-API/FileService/attribution/Market projection/security event 仍为 `dedicated/needs_verification`、Account/Coverage gate 绑定具体 key、非目标 Market/Content/Meta/AI/Knowledge/Events 口径正确、OpenAPI 与业务实现无 diff、memory/.agent 未过度宣称。Copernicus 已验证 XML 防空跑 25 个目标 XML 合计 192 tests 且 failures/errors/skipped 均为 0,XML 时间集中在本轮验证窗口,Flyway IT 只连接 `_test` 库 `muse_p1r_account_completed_approval_test` 且 URL/user 脱敏、密码来自环境变量、clean/migrate V1-V20 target V20 并验证 V20 outbox 表/索引/约束/trigger,allowed diff 只落在 scanner/report/P1R gates/spec/memory 文档。该双 PASS 只代表本轮 Account 第一批 10 个 operation-level completed approval 具备收口条件;不代表 Account 33/33、Market、Content 或总 P1R completed。提交或 push 仍需用户明确指令。
|
||||
|
||||
P1R Market Completed Approval 已启动审阅版:`docs/agent-specs/2026-06-11-P1RMarketCompletedApproval审阅版.md`。本审阅版只冻结 Market 32 个 `dedicated/needs_verification` operation 的 completed approval 证据标准和推荐路线,不修改 OpenAPI、scanner、coverage report 或 Market 业务实现,也不推进 Market/Content/Account remaining/总 P1R completed。只读盘点事实:当前 coverage summary 为 `233/127/106/0/0/0`,Market 32 个 operation 全部仍为 `dedicated/needs_verification`;Account 当前为 `completed=10`、`needs_verification=23`。并行子代理只读复核结论:第一候选是 Marketplace 浏览/收藏 6 个 operation,但 `favoriteAsset` / `unfavoriteAsset` 存在实现要求 `X-Command-Id` 而 OpenAPI/report `requiresCommandId=false` 的合同不一致;如不批准 OpenAPI 合同修正,第一批应降级为纯读候选。purchase/install/handoff/governanceImpact 缺目标 owner 消费闭环与 Account 读侧 E2E;publish/review/governance 可作为后续强候选但缺 fresh completed 级验证;appeal 申诉审计、治理 outbox、Account projection 不应混成 completed 闭环。下一步必须对该 Market 审阅版做 fresh spec/scope review + fresh quality/feasibility review;双 PASS 前不得写执行版、实现、提交、push 或修改 scanner/report/gate。
|
||||
|
||||
P1R Market Completed Approval 审阅版首轮 review:Popper spec/scope review PASS,无阻塞项;Ptolemy quality/feasibility/testing review FAIL,P1 有效:后续执行版原要求只点名 `P1rMarketRealApiGateTest` / `P1rApiCoverageReportTest` 与泛化 mixed gates,不足以覆盖 `P1rEventsRealApiGateTest`、`P1rAiRealApiGateTest`、`P1rKnowledgeRealApiGateTest` 等 legacy gate 中硬编码的 `completed=127`、`needsVerification=106` 或 Market 全 `needs_verification` 断言。审阅版已修订:执行版必须枚举所有读取全局 summary 或 Market 状态的 P1R mixed gate,至少包括 `P1rApiCoverageReportTest`、`P1rMarketRealApiGateTest`、`P1rEventsRealApiGateTest`、`P1rAiRealApiGateTest`、`P1rKnowledgeRealApiGateTest`;需要同步断言的文件必须列入单独用户批准项、allowed diff 和 required XML 防空跑清单;目标值公式化为 `completed=127+N`、`needsVerification=106-N`、Market `completed=N / needs_verification=32-N`,其中 `N` 只能来自用户批准的 Market operation。由于审阅版已在 review 后修订,Popper PASS 失效,下一步必须重新派发 fresh spec/scope review + fresh quality/feasibility review。
|
||||
|
||||
P1R Market Completed Approval 审阅版第二轮 fresh review 已双 PASS:Godel spec/scope review PASS、Jason quality/feasibility/testing review PASS,均无 P0/P1/P2/P3 阻塞项。Godel 已验证文档仍停留在审阅版边界,未推进 Market 32/32、Market domain、Account remaining、Content 或总 P1R completed;浏览/收藏 6 个 operation 只作为候选;`favoriteAsset` / `unfavoriteAsset` 的 `X-Command-Id` 合同缺口仍是执行前硬门槛;legacy mixed gate 修订覆盖首轮 P1。Jason 已验证 OpenAPI 当前确实缺 favorite/unfavorite `X-Command-Id` header,而 controller/service 会强制校验;scanner 支持识别 required `X-Command-Id` header,后续修 OpenAPI 是可落地路径;`listMarketplaceRecommendations` 确实为 fallback 排序,已要求用户确认;handoff/install/events outbox/Account projection 等暂不推荐切片的缺口与代码事实一致。非阻塞建议:执行版需列明 Account remaining 23 / Content 防误推进的具体 gate 类名,写清第一批 `N` 的分支目标值,补 missing/blank `X-Command-Id` HTTP 失败断言,并把 recommendations fallback 写成明确的用户可批准业务口径。该双 PASS 只代表 Market 审阅版可进入执行版编写;不代表 Market completed,也不允许未获批准前修改 OpenAPI、scanner、report、gate 或业务实现。
|
||||
|
||||
P1R Market Completed Approval 执行版初稿记录:该初稿曾把 Marketplace 浏览/推荐/收藏候选合并进第一批,但已在首轮 execution quality/feasibility review 后判定证据不足并被下一段修订记录取代。当前有效执行版必须以后续 4-operation 修订版本为准;初稿不能作为实现、审批、提交或推进 Market completed 的依据。
|
||||
|
||||
P1R Market Completed Approval 执行版首轮 review:Meitner execution spec/scope review PASS,Mill execution quality/feasibility review FAIL。Mill 的 5 个 P1 已只读验证为有效:`listMarketplaceRecommendations` 仍存在 OpenAPI 个性化推荐语义与 fallback 排序实现不闭合;MockMvc + mock service/mapper + Flyway 组合不足以证明 favorite/unfavorite HTTP 到真实 DB 闭环;V8 favorite 表、唯一约束、command 约束和 trigger 在 `P1rMarketFlywayMigrationIT` 中不是 mandatory evidence;XML 防空跑只读旧 surefire XML 存在假绿风险;`listMarketplaceAssets` 缺 pageNo/pageSize 1..100 边界证据且 `PAGE_SIZE_NONE=-1` 可能触发全量查询。执行版已修订:第一批从 6 个 operation 收紧为 4 个 `getMarketplaceAsset`、`listMarketplaceCategories`、`favoriteAsset`、`unfavoriteAsset`;目标 summary 改为 `233/131/102/0/0/0`,Market 改为 `completed=4`、`needsVerification=28`;`listMarketplaceAssets` 与 `listMarketplaceRecommendations` 明确保留 `dedicated/needs_verification`;新增 mandatory `P1rMarketDiscoveryFavoriteCompletedApprovalIT` HTTP + 真实 PostgreSQL `_test` gate,沿用 `p1r.flyway.url/user` system property 与 `P1R_FLYWAY_PASSWORD` 环境变量,禁止通过 JVM property 传密码;Flyway favorite schema/约束/trigger assertion 改为 mandatory 且 XML 最低 tests 提到 4;GREEN 阶段 Maven test 验证命令要求删除目标 XML、记录 exported `RUN_START_EPOCH` 并校验 XML mtime/tests/failures/errors/skipped,TDD RED 只要求确认失败发生在本次运行窗口且失败点来自旧 report mismatch。由于执行版已在 review 后修订,Meitner PASS 不能计入最终 gate;下一步必须重新派发 fresh execution spec/scope review + fresh execution quality/feasibility review。未双 PASS 前不得实现、提交、push 或推进 Market completed。
|
||||
|
||||
P1R Market Completed Approval 修订执行版 fresh review 当前状态:Parfit execution spec/scope review PASS,无阻塞项;Harvey execution quality/feasibility review FAIL,反馈已按 `superpowers:receiving-code-review` 只读验证并修订。有效修订包括:Market HTTP+DB `_test` 建库命令改为 `PGPASSWORD=... psql -h/-p/-U/-d`,不再把密码嵌入 URI 或 JVM system property;`P1rMarketDiscoveryFavoriteCompletedApprovalIT` 最低 XML tests 从 6 提到 8,mandatory 覆盖 `DELETE /favorite` 缺 `X-Command-Id`、missing/invisible asset 对 favorite/unfavorite 不写 command/favorite fact;rollback 策略按“撤回 OpenAPI commandId header”和“保留 OpenAPI commandId 合同”两条分支分别恢复 `requiresCommandId=false/true`。由于执行版再次修订,Parfit PASS 不能计入最终 gate;下一步必须重新派发 fresh execution spec/scope review + fresh execution quality/feasibility review。未双 PASS 前不得实现、提交、push 或推进 Market completed。
|
||||
|
||||
P1R Market Completed Approval 修订执行版最终 fresh review 已双 PASS:Banach execution spec/scope review PASS、Avicenna execution quality/feasibility/testing review PASS,均无 P0/P1/P2/P3 blocker。Banach 已验证第一批严格限定 4 个 operation:`getMarketplaceAsset`、`listMarketplaceCategories`、`favoriteAsset`、`unfavoriteAsset`;不做 Market domain-level allowlist;目标值仅为获批后目标 `233/131/102/0/0/0` 与 Market `4 completed / 28 needs_verification`;`listMarketplaceAssets` 与 `listMarketplaceRecommendations` 继续 `needs_verification`;OpenAPI `X-Command-Id` 修正是单独用户批准项;allowed/prohibited diff 与非目标域边界自洽。Avicenna 已验证 Harvey 三项 blocker 已关闭:建库命令使用 `PGPASSWORD` 且 Maven 不通过 URI/JVM property 暴露密码,HTTP+DB `_test` 最低 8 tests 覆盖 detail/categories/favorite/unfavorite happy/error/replay/inactive,Flyway favorite assertion、XML 防空跑、rollback 双分支和目标 summary/Market 4/28 均已落到执行步骤。当前仍未实现、未修改 OpenAPI、scanner、coverage report、Market 业务实现或 gate test;下一步进入用户批准点。未获得用户明确批准四项前,不得实施本执行版、提交、push 或推进 Market completed。
|
||||
|
||||
P1R Market 第一批 operation-level completed approval implementation 当前状态:用户已批准执行版四项前置条件,本轮只把 4 个 Market operation 加入 operation-level completed approval,未把 `market` 加入 domain-level allowlist,未修改 Market 业务实现或数据库迁移。4 个 completed operation 为 `getMarketplaceAsset`、`listMarketplaceCategories`、`favoriteAsset`、`unfavoriteAsset`;`listMarketplaceAssets`、`listMarketplaceRecommendations` 以及 purchase/install/handoff/governance/publish/review/appeal/projection 相关 28 个 Market operation 继续 `dedicated/needs_verification`。OpenAPI 仅在 `docs/api-contracts/market/openapi.yaml` 为 `favoriteAsset` / `unfavoriteAsset` 增加 required `X-Command-Id` header,修正已有实现强校验与合同不一致;scanner 仍保持 `APPROVED_COMPLETED_DOMAINS={"ai","knowledge"}`,只追加上述 4 个 `market:*` operation key。TDD RED 已验证旧 report 下 focused P1R gates 因 expected completed=131 but was 127、Market expected 4 completed but was 0 等断言失败;GREEN 后 scanner 输出 summary `233/131/102/0/0/0`,Market 为 `completed=4`、`needs_verification=28`。验证 evidence:`python3 muse-cloud/scripts/p1r-audit-api-coverage.py --check` 通过;Market focused verification 为 Market module 26/26 pass 与 muse-server 25/25 pass;新增 `P1rMarketDiscoveryFavoriteCompletedApprovalIT` 使用 MockMvc HTTP 入口与真实 PostgreSQL `_test` 库 `muse_p1r_market_discovery_favorite_completed_approval_test` 为 9/9 pass,覆盖 detail/categories/favorite/unfavorite happy/error/replay/inactive/no-write;增强 `P1rMarketFlywayMigrationIT` 使用真实 PostgreSQL `_test` 库 `muse_p1r_market_completed_approval_test` 为 4/4 pass,输出 V1-V15 clean migrate 并验证 favorite 表、索引、约束、trigger、duplicate tenant/user/asset 拒绝、duplicate tenant/command_id 拒绝和 inactive update trigger 可执行;P1R mixed gates `P1rApiCoverageReportTest,P1rMarketRealApiGateTest,P1rEventsRealApiGateTest,P1rAiRealApiGateTest,P1rKnowledgeRealApiGateTest` 为 33/33 pass;XML 防空跑覆盖 13 个目标 XML,全部 failures/errors/skipped=0;`git diff --check` 通过;OpenAPI diff 仅限 Market favorite/unfavorite `X-Command-Id` header。fresh correctness review 指出新增 Discovery IT 脱敏 `p1r.flyway.url/user` 后未恢复会污染同 JVM 后续 Flyway IT;该问题已修复为 Discovery IT 与 Flyway IT 均保存原始 system property 并在 `@AfterAll` 恢复,组合验证使用真实 PostgreSQL `_test` 库 `muse_p1r_market_combined_completed_approval_test`,普通顺序与 `-Dsurefire.runOrder=reversealphabetical` 强制反向顺序均通过,均为 13/13 pass。fresh data/testing review 提到“真实 HTTP”若理解为 socket 级 live server则当前证据不足;按执行版原文,本轮要求是 WebApplicationContext/MockMvc + real mapper + `_test` DB,不是 live socket server,memory 已将措辞收窄为 MockMvc HTTP 入口证据;其提出的 status CHECK / FK 属于新增迁移范围,当前执行版明确禁止改迁移,作为后续 Market schema hardening 风险保留,不作为本轮 approved diff;Dirac 复审提出 raw Surefire XML 会记录原始 host/user 但未泄露 password,当前作为 P3 artifact 边界记录:raw XML 不应直接外发,外发前需清洗 host/user 或后续改造 URL/user 传递方式。新增 memory:`docs/memorys/2026-06-11-P1RMarket状态推进.md`。当前尚未提交、未 push;下一步必须重新派发 fresh implementation spec/correctness review + fresh implementation quality/data-integrity/testing review,双 PASS 前不得提交、push 或宣称 Market 32/32、Content、Account remaining 或总 P1R completed。
|
||||
|
||||
P1R Market 第一批 operation-level completed approval fresh implementation review 已双 PASS:Raman spec/correctness review PASS、Hubble data-integrity/testing review PASS,均无 P0/P1/P2/P3 阻塞项。Raman 已验证正确 worktree、scanner 未加入 `market` domain allowlist、只追加 4 个 `market:*` operation key、coverage summary 为 `233/131/102/0/0/0`、Market 为 `4 completed / 28 needs_verification`、`listMarketplaceAssets` 与 `listMarketplaceRecommendations` 继续 `dedicated/needs_verification`、OpenAPI diff 仅限 favorite/unfavorite required `X-Command-Id`、Market main/java 与 SQL diff 为空、文档未宣称 Market 32/32 或总 P1R completed。Hubble 已验证 Discovery IT 使用 WebApplicationContext + MockMvc + real mapper/service + PostgreSQL `_test`,Flyway IT 覆盖 favorite unique/index/trigger/duplicate/no-write 证据,双向组合顺序 13/13 pass 关闭同 JVM system property 污染风险,13 个目标 XML 合计 85 tests 且 failures/errors/skipped 均为 0。非阻塞注意事项:favorite `status` CHECK 与 FK 属于后续 schema hardening;raw Surefire XML 暴露 DB host/user 但未泄露 password,外发前需清洗。该双 PASS 只代表本轮 Market 第一批 4 个 operation-level completed approval 具备收口条件;不代表 Market 32/32、Content、Account remaining 或总 P1R completed。当前尚未提交、未 push;提交或 push 仍需用户明确指令。
|
||||
|
||||
311
docs/agent-specs/2026-06-11-P1RMarketCompletedApproval审阅版.md
Normal file
311
docs/agent-specs/2026-06-11-P1RMarketCompletedApproval审阅版.md
Normal file
@ -0,0 +1,311 @@
|
||||
# P1R Market Completed Approval 审阅版
|
||||
|
||||
日期:2026-06-11
|
||||
|
||||
## 结论
|
||||
|
||||
不建议把 Market 32 个 operation 一次性整域推进 `completed`。
|
||||
|
||||
推荐把 Market Completed Approval 拆成 operation-level 或证据切片推进。第一候选是 Marketplace 浏览/收藏切片,但它还不能直接进入实现:`favoriteAsset` / `unfavoriteAsset` 的实现要求 `X-Command-Id`,而当前 OpenAPI 与 coverage report 仍显示这两个 operation `requiresCommandId=false`。这是合同不一致,必须在执行版中单独审批修正或把首批降级为纯读 operation。
|
||||
|
||||
本审阅版只冻结 Market completed approval 的范围判断、证据缺口、推荐审批粒度和后续执行版要求。不修改 OpenAPI,不修改 scanner,不修改 coverage report,不修改业务实现,也不把 Market 或其它 domain 推进 `completed`。
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Start["当前 coverage<br/>Market 32 dedicated / needs_verification"] --> Review["Market completed approval 审阅版<br/>冻结证据标准"]
|
||||
Review --> Split{"是否整域 32/32 completed?"}
|
||||
Split -->|否,推荐| Slice["按证据切片 operation-level approval"]
|
||||
Split -->|是,不推荐| Domain["domain-level approval<br/>需购买/安装/handoff/发布/治理/申诉全闭环"]
|
||||
Slice --> Candidate["第一候选<br/>Marketplace 浏览/收藏"]
|
||||
Candidate --> Contract{"favorite/unfavorite 合同一致?"}
|
||||
Contract -->|否| ReadOnly["降级为纯读候选<br/>或先审批 OpenAPI 合同修正"]
|
||||
Contract -->|是| Exec["执行版<br/>列出 operation、allowed diff、fresh 验证"]
|
||||
Exec --> FreshReview["fresh spec/scope review<br/>fresh quality/feasibility review"]
|
||||
FreshReview --> UserApproval{"用户明确批准<br/>Market 状态推进?"}
|
||||
UserApproval -->|否| Stay["保持 Market 32 needs_verification"]
|
||||
UserApproval -->|是| Change["最小修改 scanner/report/gates<br/>运行 focused + P1R + _test + XML 防空跑"]
|
||||
```
|
||||
|
||||
## 当前事实状态
|
||||
|
||||
工作区:
|
||||
|
||||
```text
|
||||
/Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0
|
||||
```
|
||||
|
||||
当前分支与远端同步,HEAD 为:
|
||||
|
||||
```text
|
||||
f18116a test(p1r): 收口 Account 第一批 completed approval 门禁
|
||||
```
|
||||
|
||||
当前 coverage summary:
|
||||
|
||||
```text
|
||||
total=233
|
||||
completed=127
|
||||
needsVerification=106
|
||||
incomplete=0
|
||||
genericPersistence=0
|
||||
ssePlaceholder=0
|
||||
```
|
||||
|
||||
当前按 domain 聚合:
|
||||
|
||||
```text
|
||||
account total=33 completed=10 needsVerification=23
|
||||
ai total=41 completed=41 needsVerification=0
|
||||
content total=51 completed=0 needsVerification=51
|
||||
events total=1 completed=1 needsVerification=0
|
||||
knowledge total=59 completed=59 needsVerification=0
|
||||
market total=32 completed=0 needsVerification=32
|
||||
meta total=16 completed=16 needsVerification=0
|
||||
```
|
||||
|
||||
Market 32 个 operation 当前全部为 `dedicated / needs_verification`:
|
||||
|
||||
| operationId | Method | Path | 当前状态 |
|
||||
|---|---|---|---|
|
||||
| `adminListAppeals` | `GET` | `/admin-api/muse/market/appeals` | `dedicated / needs_verification` |
|
||||
| `adminGetAppeal` | `GET` | `/admin-api/muse/market/appeals/{appealId}` | `dedicated / needs_verification` |
|
||||
| `adminResolveAppeal` | `POST` | `/admin-api/muse/market/appeals/{appealId}/resolve` | `dedicated / needs_verification` |
|
||||
| `adminListMarketAssets` | `GET` | `/admin-api/muse/market/assets` | `dedicated / needs_verification` |
|
||||
| `adminGetMarketAsset` | `GET` | `/admin-api/muse/market/assets/{assetId}` | `dedicated / needs_verification` |
|
||||
| `adminDelistAsset` | `POST` | `/admin-api/muse/market/assets/{assetId}/delist` | `dedicated / needs_verification` |
|
||||
| `adminPreviewGovernanceImpact` | `POST` | `/admin-api/muse/market/assets/{assetId}/governance-impact` | `dedicated / needs_verification` |
|
||||
| `adminRecallAsset` | `POST` | `/admin-api/muse/market/assets/{assetId}/recall` | `dedicated / needs_verification` |
|
||||
| `adminListPublishRequests` | `GET` | `/admin-api/muse/market/publish-requests` | `dedicated / needs_verification` |
|
||||
| `adminApprovePublishRequest` | `POST` | `/admin-api/muse/market/publish-requests/{requestId}/approve` | `dedicated / needs_verification` |
|
||||
| `adminRejectPublishRequest` | `POST` | `/admin-api/muse/market/publish-requests/{requestId}/reject` | `dedicated / needs_verification` |
|
||||
| `submitAppeal` | `POST` | `/app-api/muse/marketplace/appeals` | `dedicated / needs_verification` |
|
||||
| `supplementAppeal` | `POST` | `/app-api/muse/marketplace/appeals/{appealId}/supplements` | `dedicated / needs_verification` |
|
||||
| `withdrawAppeal` | `POST` | `/app-api/muse/marketplace/appeals/{appealId}/withdraw` | `dedicated / needs_verification` |
|
||||
| `listMarketplaceAssets` | `GET` | `/app-api/muse/marketplace/assets` | `dedicated / needs_verification` |
|
||||
| `getMarketplaceAsset` | `GET` | `/app-api/muse/marketplace/assets/{assetId}` | `dedicated / needs_verification` |
|
||||
| `createBindPrecheck` | `POST` | `/app-api/muse/marketplace/assets/{assetId}/bind-precheck` | `dedicated / needs_verification` |
|
||||
| `unfavoriteAsset` | `DELETE` | `/app-api/muse/marketplace/assets/{assetId}/favorite` | `dedicated / needs_verification` |
|
||||
| `favoriteAsset` | `POST` | `/app-api/muse/marketplace/assets/{assetId}/favorite` | `dedicated / needs_verification` |
|
||||
| `getGovernanceImpact` | `GET` | `/app-api/muse/marketplace/assets/{assetId}/governance-impact` | `dedicated / needs_verification` |
|
||||
| `installMarketplaceAsset` | `POST` | `/app-api/muse/marketplace/assets/{assetId}/install` | `dedicated / needs_verification` |
|
||||
| `purchaseAsset` | `POST` | `/app-api/muse/marketplace/assets/{assetId}/purchase` | `dedicated / needs_verification` |
|
||||
| `listMarketplaceCategories` | `GET` | `/app-api/muse/marketplace/categories` | `dedicated / needs_verification` |
|
||||
| `createMarketplaceHandoff` | `POST` | `/app-api/muse/marketplace/handoffs` | `dedicated / needs_verification` |
|
||||
| `getHandoffStatus` | `GET` | `/app-api/muse/marketplace/handoffs/{handoffToken}` | `dedicated / needs_verification` |
|
||||
| `cancelHandoff` | `POST` | `/app-api/muse/marketplace/handoffs/{handoffToken}/cancel` | `dedicated / needs_verification` |
|
||||
| `listMyPublishRecords` | `GET` | `/app-api/muse/marketplace/my-publish-records` | `dedicated / needs_verification` |
|
||||
| `savePublishDraft` | `POST` | `/app-api/muse/marketplace/publish-drafts` | `dedicated / needs_verification` |
|
||||
| `runPublishCheck` | `POST` | `/app-api/muse/marketplace/publish-drafts/{draftId}/checks` | `dedicated / needs_verification` |
|
||||
| `submitPublishRequest` | `POST` | `/app-api/muse/marketplace/publish-requests` | `dedicated / needs_verification` |
|
||||
| `withdrawPublishRequest` | `POST` | `/app-api/muse/marketplace/publish-requests/{requestId}/withdraw` | `dedicated / needs_verification` |
|
||||
| `listMarketplaceRecommendations` | `GET` | `/app-api/muse/marketplace/recommendations` | `dedicated / needs_verification` |
|
||||
|
||||
## 已验证实现证据
|
||||
|
||||
### Market owner 与入口
|
||||
|
||||
Market owner 当前位于 `muse-module-market`,负责市场来源侧事实、授权、安装、handoff、发布、治理、申诉、浏览和收藏。
|
||||
|
||||
现有 dedicated Controller 覆盖 Market 32 个 operation:
|
||||
|
||||
- `AppMuseMarketplaceAssetController`
|
||||
- `AppMuseMarketLicenseController`
|
||||
- `AppMuseMarketHandoffController`
|
||||
- `AppMuseMarketPublishController`
|
||||
- `AppMuseMarketAppealController`
|
||||
- `AdminMuseMarketAssetController`
|
||||
- `AdminMuseMarketReviewController`
|
||||
- `AdminMuseMarketAppealController`
|
||||
|
||||
`P1rMarketRouteOwnershipTest` 已证明 Market 路由由 dedicated controller 持有,不再走 `MuseContractPersistenceService` 旧合同兜底。
|
||||
|
||||
### 数据库与迁移证据
|
||||
|
||||
V15 Market Real API schema 已存在:
|
||||
|
||||
- `muse-cloud/sql/muse/V15__extend_market_real_api_schema.sql`
|
||||
|
||||
历史留痕记录 V15 已在真实 PostgreSQL `_test` 库通过 Flyway:
|
||||
|
||||
```text
|
||||
flyway_success=true
|
||||
migrations_executed=15
|
||||
successful_migration_count=15
|
||||
target_schema_version=15
|
||||
flyway_latest=15:extend market real api schema
|
||||
schema_version=15
|
||||
```
|
||||
|
||||
历史 Market focused tests 记录为 191/191 pass,P1R Market gates 记录为 24/24 pass。但这些证据只证明 P1R-6 已把 Market 32 个 operation 收口到 `dedicated / needs_verification`,不能作为本轮 completed approval 的 fresh runtime 证据。
|
||||
|
||||
### 子代理只读盘点结论
|
||||
|
||||
本轮按 Market 业务切片并行只读复核,结论如下:
|
||||
|
||||
- Marketplace 浏览/收藏 6 个 operation 是第一批 operation-level completed 候选,但仍缺合同一致性和真实 HTTP + DB 闭环。
|
||||
- purchase / install / handoff / bind-precheck / governanceImpact 7 个 operation 不适合作为第一批;缺目标 owner 消费 handoff token、回写 completed、Account 读侧 E2E 和跨 owner governance 影响闭环。
|
||||
- publish / review / governance 13 个 operation 的实现和测试证据较强,可作为后续候选切片;但当前仍缺审阅版/执行版冻结、fresh completed 级验证和 Market 整域覆盖,不应直接推进 completed。
|
||||
- appeal / events / projection 6 个申诉相关 operation 不适合作为第一批;申诉事件是 `muse_market_appeal_event` 审计事实,不是统一 Events publish outbox;`restore` 治理 action 当前不进入 MarketEventPublishOutbox;MarketAccountProjectionProvider 不覆盖申诉。
|
||||
|
||||
## 推荐第一候选
|
||||
|
||||
推荐第一批候选限定为 Marketplace 浏览/收藏切片:
|
||||
|
||||
```text
|
||||
listMarketplaceAssets
|
||||
getMarketplaceAsset
|
||||
listMarketplaceCategories
|
||||
listMarketplaceRecommendations
|
||||
favoriteAsset
|
||||
unfavoriteAsset
|
||||
```
|
||||
|
||||
推荐理由:
|
||||
|
||||
- Controller / Service / Mapper 链路相对短,主要停留在 Market 本域。
|
||||
- 浏览读侧依赖资产、版本、收藏状态和可见性,不需要真实支付、安装、handoff 或目标 owner 消费。
|
||||
- 收藏写侧已有 command replay、owner 登录校验、可见资产校验、`muse_market_favorite` active/inactive 审计事实。
|
||||
- 风险面小于 purchase/install/handoff、publish/review/governance、appeal/projection 切片。
|
||||
|
||||
但该候选不能直接批准。执行版必须先解决以下硬条件:
|
||||
|
||||
1. `favoriteAsset` / `unfavoriteAsset` 的 `X-Command-Id` 合同不一致:实现和 controller test 要求 header,OpenAPI 与 coverage report 仍显示 `requiresCommandId=false`。
|
||||
2. `listMarketplaceRecommendations` 当前是可解释 fallback 排序,不是独立推荐系统读模型;执行版必须明确 MVP completed 口径接受 fallback,或把该 operation 留在 `needs_verification`。
|
||||
3. 缺少真实 Spring context + 测试数据库的 HTTP/DB 闭环:上架资产种子、列表/详情/分类/推荐、收藏/取消、DB 状态、回放、冲突和不可见资产拒绝。
|
||||
4. 列表/推荐当前存在逐项取 currentVersion / favorite 的 N+1 查询形态;执行版必须给出分页上限、SQL/mapper 验证或明确的 MVP 接受边界。
|
||||
5. 当前 `P1rMarketRealApiGateTest` 明确要求 Market 32 个 operation 保持 `dedicated / needs_verification`;未获用户批准前不能修改 gate。
|
||||
|
||||
如果不允许在 Market completed approval 中修改 OpenAPI 合同,第一批应降级为纯读候选:
|
||||
|
||||
```text
|
||||
listMarketplaceAssets
|
||||
getMarketplaceAsset
|
||||
listMarketplaceCategories
|
||||
```
|
||||
|
||||
`listMarketplaceRecommendations` 是否纳入纯读候选,取决于用户是否接受 fallback recommendation 作为当前 completed 口径。
|
||||
|
||||
## 暂不推荐的切片
|
||||
|
||||
### purchase / install / handoff / governanceImpact
|
||||
|
||||
以下 operation 暂不推荐第一批 completed:
|
||||
|
||||
- `purchaseAsset`
|
||||
- `installMarketplaceAsset`
|
||||
- `createBindPrecheck`
|
||||
- `createMarketplaceHandoff`
|
||||
- `getHandoffStatus`
|
||||
- `cancelHandoff`
|
||||
- `getGovernanceImpact`
|
||||
|
||||
主要缺口:
|
||||
|
||||
- `purchaseAsset` 只证明本地授权、purchase fact 和 Account projection 写入,不证明真实支付或 Account 读侧 E2E。
|
||||
- `installMarketplaceAsset` 明确不写目标 owner canonical facts,`targetFactsWritten=false`。
|
||||
- handoff token 只落 hash,缺真实目标 owner 消费 token、创建 owner precheck、回写 owner confirm result 或推进 handoff completed 的公开链路。
|
||||
- `MarketTargetOwnerFacade` 当前只生成目标页并做 owner/action 白名单,接口边界明确不消费 token、不写目标 owner facts。
|
||||
- `getGovernanceImpact` 当前主要聚合 Market 自有 authorization / installation / handoff / governance facts,不证明 AI / Knowledge / Content 目标 owner 的真实影响闭环。
|
||||
|
||||
### publish / review / governance
|
||||
|
||||
以下 operation 可作为后续强候选,但不推荐直接作为第一批:
|
||||
|
||||
- `savePublishDraft`
|
||||
- `runPublishCheck`
|
||||
- `submitPublishRequest`
|
||||
- `withdrawPublishRequest`
|
||||
- `listMyPublishRecords`
|
||||
- `adminListPublishRequests`
|
||||
- `adminApprovePublishRequest`
|
||||
- `adminRejectPublishRequest`
|
||||
- `adminListMarketAssets`
|
||||
- `adminGetMarketAsset`
|
||||
- `adminPreviewGovernanceImpact`
|
||||
- `adminDelistAsset`
|
||||
- `adminRecallAsset`
|
||||
|
||||
已有证据显示它们具备草稿、检查、提交、审核、上架、治理 preview、delist/recall 和部分 Events outbox 链路。但它们仍不应直接 completed:
|
||||
|
||||
- 当前没有 Market Completed Approval 执行版冻结这 13 个 operation 的审批范围和 allowed diff。
|
||||
- 仍缺 fresh focused tests、P1R mixed gates、真实 `_test`、XML 防空跑和必要的 HTTP/API 或跨 owner runtime 验收。
|
||||
- `MarketEventPublishOutboxService` 当前 allowlist 是 `delist / recall`,不能把所有发布/审核/治理操作都等同于 Events 可见闭环。
|
||||
- 该切片不能代表 Market 32/32 domain completed。
|
||||
|
||||
### appeals / restore / projection
|
||||
|
||||
以下 operation 暂不推荐 first batch:
|
||||
|
||||
- `submitAppeal`
|
||||
- `supplementAppeal`
|
||||
- `withdrawAppeal`
|
||||
- `adminListAppeals`
|
||||
- `adminGetAppeal`
|
||||
- `adminResolveAppeal`
|
||||
|
||||
主要缺口:
|
||||
|
||||
- `submit/supplement/withdraw/resolve` 写入的是 `muse_market_appeal_event` 审计事实,不是统一 Events publish outbox。
|
||||
- `adminResolveAppeal` 的 restore / partially_restored 路径写 `actionType=restore`,当前 outbox service 会跳过。
|
||||
- `MarketAccountProjectionProvider` 只支持 `purchase / license / publish`,不覆盖申诉。
|
||||
- 因此不能把申诉审计、治理 outbox 和 Account projection 混成一个 completed 闭环。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不修改 7 个 OpenAPI:
|
||||
- `docs/api-contracts/account/openapi.yaml`
|
||||
- `docs/api-contracts/ai/openapi.yaml`
|
||||
- `docs/api-contracts/content/openapi.yaml`
|
||||
- `docs/api-contracts/events/openapi.yaml`
|
||||
- `docs/api-contracts/knowledge/openapi.yaml`
|
||||
- `docs/api-contracts/market/openapi.yaml`
|
||||
- `docs/api-contracts/meta/openapi.yaml`
|
||||
- 不修改 `muse-cloud/scripts/p1r-audit-api-coverage.py`。
|
||||
- 不修改 `docs/superpowers/reports/p1r-api-coverage.json`。
|
||||
- 不修改 `docs/superpowers/reports/p1r-api-coverage.md`。
|
||||
- 不修改 Market 业务实现。
|
||||
- 不把 Market 32 个 operation 推进 `completed`。
|
||||
- 不把 Market 加入 domain-level completed allowlist。
|
||||
- 不推进 Account remaining 23 / Content / 总 P1R completed。
|
||||
- 不把 P1R-7d Market governance event propagation evidence 当作 Market 整域 completed 证据。
|
||||
|
||||
## 后续执行版必须包含
|
||||
|
||||
1. Market 32 operation 的完整清单、当前状态和目标审批状态。
|
||||
2. 第一批候选 operation 的逐项 evidence map:Controller、Service、Mapper、权限、版本头、owner/tenant 隔离、幂等、审计、失败路径。
|
||||
3. 明确哪些 operation 继续 `needs_verification`,以及原因。
|
||||
4. `favoriteAsset` / `unfavoriteAsset` 合同处理方案:要么审批 OpenAPI `X-Command-Id` 修正,要么从第一批移除。
|
||||
5. `listMarketplaceRecommendations` fallback completed 口径:要么明确接受 fallback 推荐,要么保持 `needs_verification`。
|
||||
6. scanner approval 模型:只允许 operation-level allowlist,不允许 Market domain-level allowlist。
|
||||
7. `P1rMarketRealApiGateTest` 从禁止 completed 到审批后 partial completed gate 的修改策略。
|
||||
8. `P1rApiCoverageReportTest` summary 期望变更策略,必须只提升用户批准的 Market operation 数。
|
||||
9. legacy P1R mixed gate 同步策略:执行版必须枚举所有读取全局 summary 或 Market 状态的 gate,至少包括 `P1rApiCoverageReportTest`、`P1rMarketRealApiGateTest`、`P1rEventsRealApiGateTest`、`P1rAiRealApiGateTest`、`P1rKnowledgeRealApiGateTest`;需要同步断言的文件必须列入单独用户批准项、allowed diff 和 required XML 防空跑清单。
|
||||
10. Market partial completed 目标值必须公式化:若用户批准 `N` 个 Market operation,则 summary 只能变为 `completed=127+N`、`needsVerification=106-N`,Market 只能变为 `completed=N / needs_verification=32-N`;`N` 只能来自用户批准的 Market operation,不能由 Market domain-level allowlist 推导。
|
||||
11. Market focused tests 的 required class 清单与最低 XML tests 数;执行版必须 fresh rerun,不能只引用历史留痕。
|
||||
12. P1R mixed gates:必须覆盖 Market gate、coverage report gate、上述 legacy summary/Market gate,以及用于证明 Account remaining 23 / Content 不被推进的 gate;未列入 allowed diff 的非目标 gate 只允许运行和读取 XML,不能顺手改。
|
||||
13. 真实 PostgreSQL `_test`:至少覆盖 V1-V15 clean migrate;如新增 Market completed approval Flyway IT,应验证 V15 Market 表、索引、约束、trigger、favorite unique 和非法 insert 拒绝。
|
||||
14. 真实 HTTP + DB 或等价集成验收:覆盖上架资产可见性、publisher-only 非公开可见性、收藏 active/inactive、command replay、missing/invisible asset 拒绝。
|
||||
15. protected diff gate:7 个 OpenAPI 在批准前必须保持空 diff;若执行版要修正 market OpenAPI,必须把该文件列入单独用户批准和 allowed diff。
|
||||
16. allowed-diff gate:覆盖 unstaged、staged、untracked,并使用 `git -c core.quotePath=false`。
|
||||
17. rollback 策略:撤回 scanner allowlist、coverage report、gate test、可选 OpenAPI 修正和新增 memory,恢复目标 operation 为 `dedicated / needs_verification`。
|
||||
|
||||
## 验收标准
|
||||
|
||||
本审阅版可以视为完成的条件:
|
||||
|
||||
1. 文件写入 `docs/agent-specs/2026-06-11-P1RMarketCompletedApproval审阅版.md`。
|
||||
2. `.agent` 记录 Market completed approval 审阅版状态。
|
||||
3. `git diff --check` 通过。
|
||||
4. OpenAPI、scanner、coverage report 无 diff。
|
||||
5. 文档只定义审阅路线,不进入执行版或实现。
|
||||
6. fresh spec/scope review PASS。
|
||||
7. fresh quality/feasibility review PASS。
|
||||
|
||||
## 待确认项
|
||||
|
||||
1. 是否确认 Market 第一轮不做 domain-level 32/32 completed approval。
|
||||
2. 是否确认优先走 operation-level approval,并从 Marketplace 浏览/收藏切片开始。
|
||||
3. 是否允许后续执行版把 `docs/api-contracts/market/openapi.yaml` 的 `favoriteAsset` / `unfavoriteAsset` `X-Command-Id` 合同修正列为单独批准项。
|
||||
4. 是否接受 `listMarketplaceRecommendations` 当前 fallback recommendation 作为 MVP completed 口径;如果不接受,该 operation 第一轮继续 `needs_verification`。
|
||||
5. 是否确认 purchase / install / handoff / governanceImpact / appeals 在没有真实跨 owner 闭环前继续保持 `needs_verification`。
|
||||
6. 是否确认审阅版 fresh 双 review PASS 后,再写 Market completed approval 执行版。
|
||||
738
docs/agent-specs/2026-06-11-P1RMarketCompletedApproval执行版.md
Normal file
738
docs/agent-specs/2026-06-11-P1RMarketCompletedApproval执行版.md
Normal file
@ -0,0 +1,738 @@
|
||||
# P1R Market Completed Approval 执行版
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` to implement this plan task-by-task after the explicit approval gate. Steps use checkbox syntax for tracking.
|
||||
|
||||
日期:2026-06-11
|
||||
|
||||
## 结论
|
||||
|
||||
本执行版推荐第一轮只做 Market operation-level completed approval,不做 Market domain-level 32/32 completed。
|
||||
|
||||
第一轮推荐审批 4 个 Marketplace 详情/分类/收藏 operation:
|
||||
|
||||
```text
|
||||
market:getMarketplaceAsset
|
||||
market:listMarketplaceCategories
|
||||
market:favoriteAsset
|
||||
market:unfavoriteAsset
|
||||
```
|
||||
|
||||
审批后目标 coverage summary 为:
|
||||
|
||||
```text
|
||||
total=233
|
||||
completed=131
|
||||
needsVerification=102
|
||||
incomplete=0
|
||||
genericPersistence=0
|
||||
ssePlaceholder=0
|
||||
```
|
||||
|
||||
Market 域目标状态为:
|
||||
|
||||
```text
|
||||
market total=32
|
||||
completed=4
|
||||
needsVerification=28
|
||||
```
|
||||
|
||||
本执行版只定义实现前置批准项、允许变更范围、TDD 顺序、验证命令、XML 防空跑和回滚策略。当前不得直接实现,不得修改 OpenAPI、scanner、coverage report、业务实现或 gate test。
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Current["当前 Market<br/>32 dedicated / needs_verification"] --> Exec["执行版<br/>锁定第一批 4 个 operation"]
|
||||
Exec --> Review["fresh execution spec/scope review<br/>fresh execution quality/feasibility review"]
|
||||
Review --> Approval{"用户明确批准<br/>4 ops / operation-level / OpenAPI commandId / allowed diff"}
|
||||
Approval -->|否| Stay["保持当前 coverage<br/>233/127/106"]
|
||||
Approval -->|是| Red["TDD RED<br/>先改 Market/Coverage/legacy gates<br/>旧 report 必须失败"]
|
||||
Red --> Green["TDD GREEN<br/>Market OpenAPI commandId + scanner operation allowlist + regenerate report"]
|
||||
Green --> Verify["focused + P1R mixed + Flyway _test<br/>XML 防空跑 + allowed/protected diff"]
|
||||
Verify --> FreshReview["fresh implementation review"]
|
||||
```
|
||||
|
||||
## 当前事实
|
||||
|
||||
正确 worktree:
|
||||
|
||||
```text
|
||||
/Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0
|
||||
```
|
||||
|
||||
当前 HEAD:
|
||||
|
||||
```text
|
||||
f18116a test(p1r): 收口 Account 第一批 completed approval 门禁
|
||||
```
|
||||
|
||||
当前 coverage summary:
|
||||
|
||||
```text
|
||||
233 127 106 0 0 0
|
||||
```
|
||||
|
||||
当前 Market 事实:
|
||||
|
||||
- Market 32 个 operation 全部为 `dedicated / needs_verification`。
|
||||
- `APPROVED_COMPLETED_DOMAINS` 当前只包含 `ai`、`knowledge`。
|
||||
- `APPROVED_COMPLETED_OPERATIONS` 当前只包含 `events:streamEvents`、Meta 16 个 operation、Account 第一批 10 个 operation。
|
||||
- `P1rMarketRealApiGateTest` 当前明确禁止 Market operation 标记为 `completed`。
|
||||
- `P1rApiCoverageReportTest` 当前断言 summary `completed=127`,并断言 Market 32 个 operation 继续 `needs_verification`。
|
||||
- `P1rEventsRealApiGateTest`、`P1rAiRealApiGateTest`、`P1rKnowledgeRealApiGateTest` 仍硬编码当前 summary 或 Market needs_verification 口径;实现前必须把这些 legacy mixed gate 同步列入单独批准项。
|
||||
- `favoriteAsset` / `unfavoriteAsset` 的 Java Controller 和 Service 强制校验 `X-Command-Id`,但 `docs/api-contracts/market/openapi.yaml` 当前未声明该 header,coverage report 因此显示 `requiresCommandId=false`。
|
||||
- `listMarketplaceRecommendations` 当前是 `fallback_popular` / `fallback_newest` 可解释排序,不是独立个性化推荐系统读模型。
|
||||
|
||||
## 执行边界
|
||||
|
||||
### 必须先获用户明确批准
|
||||
|
||||
实现前必须同时获得以下 4 项批准:
|
||||
|
||||
1. 批准第一轮 Market completed operation 精确清单为本执行版列出的 4 个 operation。
|
||||
2. 批准只走 operation-level approval,不把 `market` 加入 domain-level completed allowlist。
|
||||
3. 批准只在 `docs/api-contracts/market/openapi.yaml` 的 `favoriteAsset` / `unfavoriteAsset` 增加 required `X-Command-Id` header 合同;不修改其它 OpenAPI。
|
||||
4. 批准按本执行版修改 scanner、coverage report、Market/Coverage gate、必要 legacy mixed gate、Market focused tests、新增 Market HTTP+DB `_test` IT、Flyway assertion、memory 和 `.agent`。
|
||||
|
||||
未获 4 项批准前,不得实施本执行版。
|
||||
|
||||
如果用户不批准第 3 项,必须把 `favoriteAsset` / `unfavoriteAsset` 移出第一批,并重写执行版为纯读候选。
|
||||
|
||||
`market:listMarketplaceAssets` 与 `market:listMarketplaceRecommendations` 本轮明确不进入 completed approval;前者需要先补 pageNo/pageSize 1..100 边界证据,后者需要先解决 OpenAPI 个性化推荐描述与当前 fallback 排序实现的合同不一致。
|
||||
|
||||
### 本轮允许变更
|
||||
|
||||
获批后只允许修改以下路径:
|
||||
|
||||
- `docs/api-contracts/market/openapi.yaml`,仅限为 `favoriteAsset` / `unfavoriteAsset` 增加 required `X-Command-Id` header。
|
||||
- `muse-cloud/scripts/p1r-audit-api-coverage.py`
|
||||
- `docs/superpowers/reports/p1r-api-coverage.json`
|
||||
- `docs/superpowers/reports/p1r-api-coverage.md`
|
||||
- `muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rApiCoverageReportTest.java`
|
||||
- `muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rMarketRealApiGateTest.java`
|
||||
- `muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rEventsRealApiGateTest.java`
|
||||
- `muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rAiRealApiGateTest.java`
|
||||
- `muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rKnowledgeRealApiGateTest.java`
|
||||
- `muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rMarketDiscoveryFavoriteCompletedApprovalIT.java`,新增 Market 详情/分类/收藏 HTTP + 真实 PostgreSQL `_test` 证据。
|
||||
- `muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rMarketFlywayMigrationIT.java`,仅限补充 V6/V8/V15 Market 表、索引、约束、trigger 和 favorite 非法 insert/update assertion。
|
||||
- `muse-cloud/muse-module-market/muse-module-market-server/src/test/java/cn/iocoder/muse/module/market/controller/app/muse/AppMuseMarketplaceAssetControllerTest.java`
|
||||
- `muse-cloud/muse-module-market/muse-module-market-server/src/test/java/cn/iocoder/muse/module/market/application/muse/MarketAssetQueryServiceTest.java`
|
||||
- `muse-cloud/muse-module-market/muse-module-market-server/src/test/java/cn/iocoder/muse/module/market/application/muse/MarketFavoriteServiceTest.java`
|
||||
- `muse-cloud/muse-module-market/muse-module-market-server/src/test/java/cn/iocoder/muse/module/market/dal/mysql/muse/MuseMarketAssetMapperTest.java`
|
||||
- `docs/agent-specs/.agent`
|
||||
- `docs/agent-specs/2026-06-11-P1RMarketCompletedApproval审阅版.md`
|
||||
- `docs/agent-specs/2026-06-11-P1RMarketCompletedApproval执行版.md`
|
||||
- `docs/memorys/2026-06-11-P1RMarket状态推进.md`
|
||||
|
||||
如 implementation 发现必须修改 Market 业务实现、其它 OpenAPI、数据库迁移、Account/Content/Meta gate 或其它文件,必须停下说明原因并重新取得用户批准。
|
||||
|
||||
### 本轮禁止变更
|
||||
|
||||
- 不修改除 `docs/api-contracts/market/openapi.yaml` 之外的 OpenAPI。
|
||||
- 不修改 Market 业务实现来掩盖 coverage 缺口。
|
||||
- 不新增或修改 Market domain-level completed allowlist。
|
||||
- 不推进 Market purchase / install / handoff / governanceImpact。
|
||||
- 不推进 Market publish / review / governance 管理切片。
|
||||
- 不推进 Market appeals / restore / projection 切片。
|
||||
- 不推进 Account remaining 23、Content 或总 P1R completed。
|
||||
|
||||
## 第一轮审批清单
|
||||
|
||||
| operation key | Method | Path | 目标状态 | 完成证据边界 |
|
||||
|---|---|---|---|---|
|
||||
| `market:getMarketplaceAsset` | `GET` | `/app-api/muse/marketplace/assets/{assetId}` | `dedicated / completed` | App 资产详情、可见性、missing/invisible asset 拒绝、license/category/governance status DTO |
|
||||
| `market:listMarketplaceCategories` | `GET` | `/app-api/muse/marketplace/categories` | `dedicated / completed` | 分类聚合、fallback recommendation slot、exposure summary unavailable 明示 |
|
||||
| `market:favoriteAsset` | `POST` | `/app-api/muse/marketplace/assets/{assetId}/favorite` | `dedicated / completed` | required `X-Command-Id`、登录 owner、可见资产、command replay、favorite active、唯一约束和审计事实 |
|
||||
| `market:unfavoriteAsset` | `DELETE` | `/app-api/muse/marketplace/assets/{assetId}/favorite` | `dedicated / completed` | required `X-Command-Id`、登录 owner、可见资产、command replay、favorite inactive、不删除审计事实 |
|
||||
|
||||
## 保持 needs_verification 的 Market operation
|
||||
|
||||
以下 28 个 operation 必须保持 `dedicated / needs_verification`:
|
||||
|
||||
```text
|
||||
market:listMarketplaceAssets
|
||||
market:listMarketplaceRecommendations
|
||||
market:adminListAppeals
|
||||
market:adminGetAppeal
|
||||
market:adminResolveAppeal
|
||||
market:adminListMarketAssets
|
||||
market:adminGetMarketAsset
|
||||
market:adminDelistAsset
|
||||
market:adminPreviewGovernanceImpact
|
||||
market:adminRecallAsset
|
||||
market:adminListPublishRequests
|
||||
market:adminApprovePublishRequest
|
||||
market:adminRejectPublishRequest
|
||||
market:submitAppeal
|
||||
market:supplementAppeal
|
||||
market:withdrawAppeal
|
||||
market:createBindPrecheck
|
||||
market:getGovernanceImpact
|
||||
market:installMarketplaceAsset
|
||||
market:purchaseAsset
|
||||
market:createMarketplaceHandoff
|
||||
market:getHandoffStatus
|
||||
market:cancelHandoff
|
||||
market:listMyPublishRecords
|
||||
market:savePublishDraft
|
||||
market:runPublishCheck
|
||||
market:submitPublishRequest
|
||||
market:withdrawPublishRequest
|
||||
```
|
||||
|
||||
保留原因:
|
||||
|
||||
- `listMarketplaceAssets` 当前缺少 pageNo/pageSize 1..100 边界证据;实现层会透传 pageSize,底层 `PAGE_SIZE_NONE=-1` 可能变成全量查询,不能在未获业务实现批准前标为 completed。
|
||||
- `listMarketplaceRecommendations` 当前 OpenAPI 描述是个性化推荐,实际实现是 `fallback_popular` / `fallback_newest` 排序;合同与实现语义不闭合,不能在本轮作为 completed。
|
||||
- purchase / install / handoff / governanceImpact 缺目标 owner 消费闭环、Account 读侧 E2E 和跨 owner governance 影响闭环。
|
||||
- publish / review / governance 证据较强,但仍需单独执行版冻结 13 个 operation 的审批范围、Events outbox 边界和 fresh completed 级验证。
|
||||
- appeals / restore / projection 写入的是申诉审计、治理 action 或 Account projection 子集,不能混成统一 completed 闭环。
|
||||
|
||||
## 证据矩阵
|
||||
|
||||
| 证据类型 | 必跑或必查内容 | 目的 |
|
||||
|---|---|---|
|
||||
| Market focused tests | `AppMuseMarketplaceAssetControllerTest`、`MarketAssetQueryServiceTest`、`MarketFavoriteServiceTest`、`MuseMarketAssetMapperTest` | 证明 4 个 operation 的 HTTP contract、版本头、commandId、owner、可见性、幂等和 favorite active/inactive |
|
||||
| Market HTTP+DB `_test` | 新增 `P1rMarketDiscoveryFavoriteCompletedApprovalIT` | 证明 4 个 operation 经 HTTP 入口访问真实 PostgreSQL `_test` 数据,覆盖 detail/category/favorite/unfavorite 的持久化和错误路径 |
|
||||
| P1R Market gates | `P1rMarketRealApiGateTest`、`P1rMarketRouteOwnershipTest`、`P1rMarketMigrationSqlTest` | 证明 only 4 Market operation completed,28 个继续 needs_verification,路由仍 dedicated |
|
||||
| P1R mixed gates | Coverage、Market、Events、AI、Knowledge、Account、Content、Meta gates | 防止非目标 domain 被连带推进,并同步 legacy summary / Market 状态断言 |
|
||||
| Flyway `_test` | `P1rMarketFlywayMigrationIT` fresh rerun | 证明 V1-V15 clean migrate、Market V6/V8/V15 表/索引/约束/trigger、favorite unique/command 约束和非法 insert 拒绝 |
|
||||
| Coverage scanner | `python3 muse-cloud/scripts/p1r-audit-api-coverage.py --check` | 证明 scanner/report 与 operation-level allowlist、OpenAPI commandId 口径一致 |
|
||||
| XML 防空跑 | 逐个读取 surefire XML,检查 tests、failures、errors、skipped | 防止 Maven 未跑目标类或旧 XML 假绿 |
|
||||
| OpenAPI allowed diff | 只允许 `market/openapi.yaml` 增加 favorite/unfavorite required `X-Command-Id` | 修正合同与实现不一致,不扩大其它合同 |
|
||||
| Business protected diff | Market 业务实现 staged/unstaged diff 必须为空 | 证明没有通过改实现掩盖 coverage 缺口 |
|
||||
| Allowed diff | staged、unstaged、untracked 均必须落在允许清单 | 证明没有顺手扩大 scope |
|
||||
|
||||
说明:standalone MockMvc、mock Service、mapper focused tests 只能作为支持证据,不能单独支撑 `favoriteAsset` / `unfavoriteAsset` completed。实现阶段必须新增 Market HTTP + 真实 PostgreSQL `_test` harness;若现有 Spring 测试结构无法稳定做到 WebApplicationContext/MockMvc + real mapper + `_test` DB,必须停下修订执行版,不能用 mock 组合替代。
|
||||
|
||||
## TDD 执行步骤
|
||||
|
||||
### Task 0:实现前现场确认
|
||||
|
||||
- [ ] 确认 worktree:
|
||||
|
||||
```bash
|
||||
pwd
|
||||
git -c core.quotePath=false status --short --branch
|
||||
git log --oneline -5
|
||||
jq -r '.summary | [.totalOperations,.completedOperations,.needsVerificationOperations,.incompleteOperations,.genericPersistenceOperations,.ssePlaceholderOperations] | @tsv' docs/superpowers/reports/p1r-api-coverage.json
|
||||
```
|
||||
|
||||
预期:
|
||||
|
||||
```text
|
||||
/Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0
|
||||
233 127 106 0 0 0
|
||||
```
|
||||
|
||||
- [ ] 确认 Market 32 个 operation 仍全部是 `dedicated / needs_verification`:
|
||||
|
||||
```bash
|
||||
jq -r '.operations[] | select(.domain=="market") | [.operationId,.implementationStatus,.completionStatus,.requiresCommandId] | @tsv' docs/superpowers/reports/p1r-api-coverage.json
|
||||
```
|
||||
|
||||
- [ ] 确认实施前只有文档 diff,没有 OpenAPI、scanner、coverage report、业务实现 diff:
|
||||
|
||||
```bash
|
||||
git -c core.quotePath=false status --short --branch
|
||||
git diff -- docs/api-contracts muse-cloud/scripts/p1r-audit-api-coverage.py docs/superpowers/reports
|
||||
git diff -- muse-cloud/muse-module-market/muse-module-market-server/src/main/java
|
||||
```
|
||||
|
||||
### XML 防空跑通用规则
|
||||
|
||||
除 TDD RED 失败验证外,所有 Maven test 验证命令都必须执行以下规则,不能只读取历史 surefire XML:
|
||||
|
||||
- 命令前记录 `export RUN_START_EPOCH=$(date +%s)`。
|
||||
- 命令前删除本任务要求检查的 `target/surefire-reports/TEST-*.xml`。
|
||||
- 命令后逐个确认 XML 存在、`mtime >= RUN_START_EPOCH`、`tests` 不低于本执行版最低值、`failures=0`、`errors=0`、`skipped=0`。
|
||||
- 任一 XML 缺失、mtime 早于运行开始时间、tests 低于最低值或存在失败/跳过,都必须视为验证失败。
|
||||
|
||||
TDD RED 失败验证必须同样删除目标 XML 并记录 `RUN_START_EPOCH`,但 RED 的预期是 Maven 失败;此时只校验失败发生在本次运行窗口内,并人工确认失败点来自本执行版列出的旧 report mismatch,不要求 `failures=0`。
|
||||
|
||||
推荐校验脚本:
|
||||
|
||||
```bash
|
||||
python3 - <<'PY'
|
||||
import os
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
run_start = int(os.environ["RUN_START_EPOCH"])
|
||||
checks = {
|
||||
"muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rApiCoverageReportTest.xml": 6,
|
||||
}
|
||||
failed = False
|
||||
for path_text, min_tests in checks.items():
|
||||
path = Path(path_text)
|
||||
if not path.exists():
|
||||
print(f"MISSING_XML {path}")
|
||||
failed = True
|
||||
continue
|
||||
if int(path.stat().st_mtime) < run_start:
|
||||
print(f"STALE_XML {path}")
|
||||
failed = True
|
||||
continue
|
||||
root = ET.parse(path).getroot()
|
||||
tests = int(root.attrib.get("tests", "0"))
|
||||
failures = int(root.attrib.get("failures", "0"))
|
||||
errors = int(root.attrib.get("errors", "0"))
|
||||
skipped = int(root.attrib.get("skipped", "0"))
|
||||
print(path, tests, failures, errors, skipped)
|
||||
if tests < min_tests or failures or errors or skipped:
|
||||
failed = True
|
||||
if failed:
|
||||
sys.exit(1)
|
||||
PY
|
||||
```
|
||||
|
||||
执行具体任务时必须把 `checks` 替换成该任务表格内列出的 XML 路径和最低 tests。
|
||||
|
||||
### Task 1:TDD RED,先改 gate 期望
|
||||
|
||||
获批后先修改:
|
||||
|
||||
- `P1rApiCoverageReportTest.java`
|
||||
- `P1rMarketRealApiGateTest.java`
|
||||
- `P1rEventsRealApiGateTest.java`
|
||||
- `P1rAiRealApiGateTest.java`
|
||||
- `P1rKnowledgeRealApiGateTest.java`
|
||||
|
||||
RED 期望:
|
||||
|
||||
- summary 期望 `completed=131`、`needsVerification=102`,但旧 report 仍是 `127/106`,必须失败。
|
||||
- Market 4 个 approved operation 期望 `completed`,但旧 report 仍是 `needs_verification`,必须失败。
|
||||
- Market 28 个未批准 operation 继续期望 `needs_verification`,其中必须包含 `listMarketplaceAssets` 与 `listMarketplaceRecommendations`。
|
||||
- `favoriteAsset` / `unfavoriteAsset` 期望 `requiresCommandId=true`,但旧 report 仍是 `false`,必须失败。
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
cd muse-cloud
|
||||
export RUN_START_EPOCH=$(date +%s)
|
||||
rm -f \
|
||||
muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rApiCoverageReportTest.xml \
|
||||
muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rMarketRealApiGateTest.xml \
|
||||
muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rEventsRealApiGateTest.xml \
|
||||
muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rAiRealApiGateTest.xml \
|
||||
muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rKnowledgeRealApiGateTest.xml
|
||||
JAVA_HOME=$(/usr/libexec/java_home -v 21) PATH="$JAVA_HOME/bin:$PATH" \
|
||||
mvn -o test -pl muse-server -am \
|
||||
-Dtest=P1rApiCoverageReportTest,P1rMarketRealApiGateTest,P1rEventsRealApiGateTest,P1rAiRealApiGateTest,P1rKnowledgeRealApiGateTest \
|
||||
-Dsurefire.failIfNoSpecifiedTests=false
|
||||
```
|
||||
|
||||
预期:至少一个失败点来自旧 report 的 `127/106`、Market 4 个 operation 尚未 completed,或 favorite/unfavorite `requiresCommandId=false`。若直接通过,必须停下排查假绿。
|
||||
|
||||
### Task 2:OpenAPI commandId 合同修正
|
||||
|
||||
只修改 `docs/api-contracts/market/openapi.yaml`:
|
||||
|
||||
- 在 `favoriteAsset` parameters 中追加 required header `X-Command-Id`。
|
||||
- 在 `unfavoriteAsset` parameters 中追加 required header `X-Command-Id`。
|
||||
- 不修改其它 Market operation。
|
||||
- 不修改其它 OpenAPI 文件。
|
||||
|
||||
推荐写法:
|
||||
|
||||
```yaml
|
||||
- name: X-Command-Id
|
||||
in: header
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: 幂等命令 ID,收藏和取消收藏必须传入。
|
||||
```
|
||||
|
||||
修正后用 scanner 验证 report 中:
|
||||
|
||||
```text
|
||||
market:favoriteAsset requiresCommandId=true
|
||||
market:unfavoriteAsset requiresCommandId=true
|
||||
```
|
||||
|
||||
### Task 3:TDD GREEN,最小 scanner/report 状态推进
|
||||
|
||||
只修改 `muse-cloud/scripts/p1r-audit-api-coverage.py`:
|
||||
|
||||
- 不修改 `APPROVED_COMPLETED_DOMAINS`。
|
||||
- 只向 `APPROVED_COMPLETED_OPERATIONS` 追加 4 个 `market:*` operation key:`market:getMarketplaceAsset`、`market:listMarketplaceCategories`、`market:favoriteAsset`、`market:unfavoriteAsset`。
|
||||
- 保持 completed 必须仍是 `dedicated` 的校验。
|
||||
|
||||
生成 report:
|
||||
|
||||
```bash
|
||||
python3 muse-cloud/scripts/p1r-audit-api-coverage.py --check
|
||||
```
|
||||
|
||||
预期 summary:
|
||||
|
||||
```text
|
||||
total=233
|
||||
completed=131
|
||||
needsVerification=102
|
||||
incomplete=0
|
||||
genericPersistence=0
|
||||
ssePlaceholder=0
|
||||
```
|
||||
|
||||
预期 Market:
|
||||
|
||||
```text
|
||||
completed=4
|
||||
needsVerification=28
|
||||
favoriteAsset requiresCommandId=true
|
||||
unfavoriteAsset requiresCommandId=true
|
||||
```
|
||||
|
||||
### Task 4:Market focused verification
|
||||
|
||||
运行 Market 本域 focused tests:
|
||||
|
||||
```bash
|
||||
cd muse-cloud
|
||||
export RUN_START_EPOCH=$(date +%s)
|
||||
rm -f \
|
||||
muse-module-market/muse-module-market-server/target/surefire-reports/TEST-cn.iocoder.muse.module.market.controller.app.muse.AppMuseMarketplaceAssetControllerTest.xml \
|
||||
muse-module-market/muse-module-market-server/target/surefire-reports/TEST-cn.iocoder.muse.module.market.application.muse.MarketAssetQueryServiceTest.xml \
|
||||
muse-module-market/muse-module-market-server/target/surefire-reports/TEST-cn.iocoder.muse.module.market.application.muse.MarketFavoriteServiceTest.xml \
|
||||
muse-module-market/muse-module-market-server/target/surefire-reports/TEST-cn.iocoder.muse.module.market.dal.mysql.muse.MuseMarketAssetMapperTest.xml \
|
||||
muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rMarketMigrationSqlTest.xml \
|
||||
muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rMarketRouteOwnershipTest.xml \
|
||||
muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rMarketRealApiGateTest.xml \
|
||||
muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rApiCoverageReportTest.xml
|
||||
JAVA_HOME=$(/usr/libexec/java_home -v 21) PATH="$JAVA_HOME/bin:$PATH" \
|
||||
mvn -o test -pl muse-module-market/muse-module-market-server,muse-server -am \
|
||||
-Dtest=AppMuseMarketplaceAssetControllerTest,MarketAssetQueryServiceTest,MarketFavoriteServiceTest,MuseMarketAssetMapperTest,P1rMarketMigrationSqlTest,P1rMarketRouteOwnershipTest,P1rMarketRealApiGateTest,P1rApiCoverageReportTest \
|
||||
-Dsurefire.failIfNoSpecifiedTests=false
|
||||
```
|
||||
|
||||
必须读取以下 XML 并检查 `failures=0`、`errors=0`、`skipped=0`:
|
||||
|
||||
| Test class | 最低 tests |
|
||||
|---|---:|
|
||||
| `AppMuseMarketplaceAssetControllerTest` | 11 |
|
||||
| `MarketAssetQueryServiceTest` | 6 |
|
||||
| `MarketFavoriteServiceTest` | 7 |
|
||||
| `MuseMarketAssetMapperTest` | 2 |
|
||||
| `P1rMarketMigrationSqlTest` | 9 |
|
||||
| `P1rMarketRouteOwnershipTest` | 3 |
|
||||
| `P1rMarketRealApiGateTest` | 6 |
|
||||
| `P1rApiCoverageReportTest` | 6 |
|
||||
|
||||
必须补齐或确认以下断言:
|
||||
|
||||
- MockMvc `getMarketplaceAsset` 覆盖 visible asset、missing asset、invisible asset。
|
||||
- MockMvc `listMarketplaceCategories` 覆盖分类聚合、fallback slot、exposure summary unavailable。
|
||||
- MockMvc 缺少 `X-Command-Id` 的 `favoriteAsset` 返回 `MARKET_COMMAND_ID_REQUIRED`。
|
||||
- MockMvc 缺少 `X-Command-Id` 的 `unfavoriteAsset` 返回 `MARKET_COMMAND_ID_REQUIRED`。
|
||||
- Service 空白 commandId 的 `favoriteAsset` 返回 `MARKET_COMMAND_ID_REQUIRED`。
|
||||
- Service 空白 commandId 的 `unfavoriteAsset` 返回 `MARKET_COMMAND_ID_REQUIRED`。
|
||||
- `unfavoriteAsset` 只能把 favorite fact 置为 inactive,不允许 delete。
|
||||
- `listMarketplaceAssets` 与 `listMarketplaceRecommendations` 可保留现有回归测试,但不得作为本轮 completed evidence;P1R gate 必须继续断言它们是 `needs_verification`。
|
||||
|
||||
如测试数量因补断言增加,XML 检查的最低 tests 可高于上表,但不能低于上表。
|
||||
|
||||
### Task 5:Market HTTP + 真实 PostgreSQL `_test` completed gate
|
||||
|
||||
新增 `P1rMarketDiscoveryFavoriteCompletedApprovalIT`,使用独立 `_test` 数据库和真实 mapper/service 数据,不能使用生产库或非 `_test` 库:
|
||||
|
||||
```bash
|
||||
source ~/.config/muse-repo/infra.env
|
||||
export P1R_FLYWAY_PASSWORD="$MUSE_POSTGRES_PASSWORD"
|
||||
export P1R_MARKET_COMPLETED_APPROVAL_TEST_DB=muse_p1r_market_discovery_favorite_completed_approval_test
|
||||
|
||||
PGPASSWORD="$MUSE_POSTGRES_PASSWORD" psql \
|
||||
-h "$MUSE_POSTGRES_HOST" \
|
||||
-p "$MUSE_POSTGRES_PORT" \
|
||||
-U "$MUSE_POSTGRES_USERNAME" \
|
||||
-d postgres \
|
||||
-tc "SELECT 1 FROM pg_database WHERE datname = '$P1R_MARKET_COMPLETED_APPROVAL_TEST_DB'" | grep -q 1 || \
|
||||
PGPASSWORD="$MUSE_POSTGRES_PASSWORD" psql \
|
||||
-h "$MUSE_POSTGRES_HOST" \
|
||||
-p "$MUSE_POSTGRES_PORT" \
|
||||
-U "$MUSE_POSTGRES_USERNAME" \
|
||||
-d postgres \
|
||||
-c "CREATE DATABASE $P1R_MARKET_COMPLETED_APPROVAL_TEST_DB"
|
||||
|
||||
cd muse-cloud
|
||||
export RUN_START_EPOCH=$(date +%s)
|
||||
rm -f muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rMarketDiscoveryFavoriteCompletedApprovalIT.xml
|
||||
JAVA_HOME=$(/usr/libexec/java_home -v 21) PATH="$JAVA_HOME/bin:$PATH" \
|
||||
mvn -o test -pl muse-server -am \
|
||||
-Dtest=P1rMarketDiscoveryFavoriteCompletedApprovalIT \
|
||||
-Dflyway.postgresql.transactional.lock=false \
|
||||
-Dp1r.flyway.locations=filesystem:sql/muse \
|
||||
-Dp1r.flyway.url="jdbc:postgresql://$MUSE_POSTGRES_HOST:$MUSE_POSTGRES_PORT/$P1R_MARKET_COMPLETED_APPROVAL_TEST_DB" \
|
||||
-Dp1r.flyway.user="$MUSE_POSTGRES_USERNAME" \
|
||||
-Djava.net.useSystemProxies=false \
|
||||
-DsocksProxyHost= -DsocksProxyPort= \
|
||||
-Dhttp.proxyHost= -Dhttp.proxyPort= \
|
||||
-Dhttps.proxyHost= -Dhttps.proxyPort= \
|
||||
-Dsurefire.failIfNoSpecifiedTests=false
|
||||
```
|
||||
|
||||
该 IT 必须至少覆盖以下 8 个测试,XML 最低 tests 为 8:
|
||||
|
||||
| Test class | 最低 tests |
|
||||
|---|---:|
|
||||
| `P1rMarketDiscoveryFavoriteCompletedApprovalIT` | 8 |
|
||||
|
||||
必须证明:
|
||||
|
||||
- `GET /app-api/muse/marketplace/assets/{assetId}` 对 `_test` 库中的 visible listed asset 返回真实详情、版本、license、category、governance status 和 favorite 状态。
|
||||
- `GET /app-api/muse/marketplace/assets/{assetId}` 对 missing 或 invisible asset 返回错误,不泄露非公开资产。
|
||||
- `GET /app-api/muse/marketplace/categories` 从真实资产/分类数据生成分类聚合,并明确 exposure summary unavailable / fallback slot。
|
||||
- `POST /favorite` 缺少 `X-Command-Id` 返回 `MARKET_COMMAND_ID_REQUIRED`;带 commandId 时写入 active favorite fact 和 command fact。
|
||||
- 同一 commandId 重放 `POST /favorite` 不产生重复 favorite fact,返回 replay 语义。
|
||||
- `DELETE /favorite` 带 commandId 后把 favorite fact 置为 inactive,不物理删除审计事实;同一 commandId 重放不重复写入。
|
||||
- `DELETE /favorite` 缺少 `X-Command-Id` 返回 `MARKET_COMMAND_ID_REQUIRED`,且不写 command fact、不改变 favorite fact。
|
||||
- `POST /favorite` 和 `DELETE /favorite` 对 missing 或 invisible asset 返回错误,并断言不写 command fact、不写或不改变 favorite fact。
|
||||
|
||||
该 IT 必须在测试启动时校验库名以 `_test` 结尾;密码只能从 `P1R_MARKET_COMPLETED_PASSWORD`、`P1R_FLYWAY_PASSWORD` 或 `MUSE_POSTGRES_PASSWORD` 环境变量读取,不能通过 JVM system property 传入;测试必须断言拒绝 `p1r.market.completed.password` 或任何 password system property;日志和断言不得输出明文数据库密码。
|
||||
|
||||
### Task 6:真实 PostgreSQL `_test` Flyway gate
|
||||
|
||||
fresh rerun `P1rMarketFlywayMigrationIT`。使用独立 `_test` 库,不能使用生产库或非 `_test` 库:
|
||||
|
||||
```bash
|
||||
source ~/.config/muse-repo/infra.env
|
||||
export P1R_FLYWAY_PASSWORD="$MUSE_POSTGRES_PASSWORD"
|
||||
export P1R_MARKET_TEST_DB=muse_p1r_market_completed_approval_test
|
||||
|
||||
PGPASSWORD="$MUSE_POSTGRES_PASSWORD" psql \
|
||||
-h "$MUSE_POSTGRES_HOST" \
|
||||
-p "$MUSE_POSTGRES_PORT" \
|
||||
-U "$MUSE_POSTGRES_USERNAME" \
|
||||
-d postgres \
|
||||
-tc "SELECT 1 FROM pg_database WHERE datname = '$P1R_MARKET_TEST_DB'" | grep -q 1 || \
|
||||
PGPASSWORD="$MUSE_POSTGRES_PASSWORD" psql \
|
||||
-h "$MUSE_POSTGRES_HOST" \
|
||||
-p "$MUSE_POSTGRES_PORT" \
|
||||
-U "$MUSE_POSTGRES_USERNAME" \
|
||||
-d postgres \
|
||||
-c "CREATE DATABASE $P1R_MARKET_TEST_DB"
|
||||
|
||||
cd muse-cloud
|
||||
export RUN_START_EPOCH=$(date +%s)
|
||||
rm -f muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rMarketFlywayMigrationIT.xml
|
||||
JAVA_HOME=$(/usr/libexec/java_home -v 21) PATH="$JAVA_HOME/bin:$PATH" \
|
||||
mvn -o test -pl muse-server -am \
|
||||
-Dtest=P1rMarketFlywayMigrationIT \
|
||||
-Dflyway.postgresql.transactional.lock=false \
|
||||
-Dp1r.flyway.locations=filesystem:sql/muse \
|
||||
-Dp1r.flyway.url="jdbc:postgresql://$MUSE_POSTGRES_HOST:$MUSE_POSTGRES_PORT/$P1R_MARKET_TEST_DB" \
|
||||
-Dp1r.flyway.user="$MUSE_POSTGRES_USERNAME" \
|
||||
-Djava.net.useSystemProxies=false \
|
||||
-DsocksProxyHost= -DsocksProxyPort= \
|
||||
-Dhttp.proxyHost= -Dhttp.proxyPort= \
|
||||
-Dhttps.proxyHost= -Dhttps.proxyPort= \
|
||||
-Dsurefire.failIfNoSpecifiedTests=false
|
||||
```
|
||||
|
||||
必须检查 XML:
|
||||
|
||||
| Test class | 最低 tests |
|
||||
|---|---:|
|
||||
| `P1rMarketFlywayMigrationIT` | 4 |
|
||||
|
||||
必须在该 IT 内新增或补齐以下 assertion,但不得新增迁移或修改业务实现:
|
||||
|
||||
- `muse_market_asset`、`muse_market_asset_version`、`muse_market_favorite` 存在。
|
||||
- `uk_muse_market_favorite_user_asset`、`uk_muse_market_favorite_command`、`idx_muse_market_favorite_user` 存在。
|
||||
- `trg_muse_market_favorite_updated_at` 存在。
|
||||
- 同一 `(tenant_id, user_id, asset_id)` 不能重复 active favorite fact。
|
||||
- 同一 `(tenant_id, command_id)` 不能重复 command fact。
|
||||
- favorite status 更新为 inactive 后 `updated_at` trigger 可执行。
|
||||
|
||||
### Task 7:P1R mixed gate
|
||||
|
||||
运行 mixed gates:
|
||||
|
||||
```bash
|
||||
cd muse-cloud
|
||||
export RUN_START_EPOCH=$(date +%s)
|
||||
rm -f \
|
||||
muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rApiCoverageReportTest.xml \
|
||||
muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rMarketRealApiGateTest.xml \
|
||||
muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rEventsRealApiGateTest.xml \
|
||||
muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rAiRealApiGateTest.xml \
|
||||
muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rKnowledgeRealApiGateTest.xml \
|
||||
muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rAccountRealApiGateTest.xml \
|
||||
muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rContentRealApiGateTest.xml \
|
||||
muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rMetaRealApiGateTest.xml
|
||||
JAVA_HOME=$(/usr/libexec/java_home -v 21) PATH="$JAVA_HOME/bin:$PATH" \
|
||||
mvn -o test -pl muse-server -am \
|
||||
-Dtest=P1rApiCoverageReportTest,P1rMarketRealApiGateTest,P1rEventsRealApiGateTest,P1rAiRealApiGateTest,P1rKnowledgeRealApiGateTest,P1rAccountRealApiGateTest,P1rContentRealApiGateTest,P1rMetaRealApiGateTest \
|
||||
-Dsurefire.failIfNoSpecifiedTests=false
|
||||
```
|
||||
|
||||
必须读取以下 XML:
|
||||
|
||||
| Test class | 最低 tests |
|
||||
|---|---:|
|
||||
| `P1rApiCoverageReportTest` | 6 |
|
||||
| `P1rMarketRealApiGateTest` | 6 |
|
||||
| `P1rEventsRealApiGateTest` | 6 |
|
||||
| `P1rAiRealApiGateTest` | 7 |
|
||||
| `P1rKnowledgeRealApiGateTest` | 8 |
|
||||
| `P1rAccountRealApiGateTest` | 4 |
|
||||
| `P1rContentRealApiGateTest` | 5 |
|
||||
| `P1rMetaRealApiGateTest` | 4 |
|
||||
|
||||
每个 XML 均必须 `failures=0`、`errors=0`、`skipped=0`。
|
||||
|
||||
这些 gate 必须共同证明:
|
||||
|
||||
- summary 只从 `127/106` 推进到 `131/102`。
|
||||
- Market 只从 `0/32` 推进到 `4/28`。
|
||||
- Account 仍保持 `10 completed / 23 needs_verification`。
|
||||
- Content 仍保持 `0 completed / 51 needs_verification`。
|
||||
- Meta、AI、Knowledge、Events 状态不变。
|
||||
|
||||
### Task 8:报告与 diff gate
|
||||
|
||||
检查 summary:
|
||||
|
||||
```bash
|
||||
jq -r '.summary | [.totalOperations,.completedOperations,.needsVerificationOperations,.incompleteOperations,.genericPersistenceOperations,.ssePlaceholderOperations] | @tsv' docs/superpowers/reports/p1r-api-coverage.json
|
||||
```
|
||||
|
||||
预期:
|
||||
|
||||
```text
|
||||
233 131 102 0 0 0
|
||||
```
|
||||
|
||||
检查 Market 4/28:
|
||||
|
||||
```bash
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
data = json.loads(Path("docs/superpowers/reports/p1r-api-coverage.json").read_text())
|
||||
counter = Counter(op["completionStatus"] for op in data["operations"] if op["domain"] == "market")
|
||||
print(counter)
|
||||
for op in data["operations"]:
|
||||
if op["domain"] == "market" and op["operationId"] in {"favoriteAsset", "unfavoriteAsset"}:
|
||||
print(op["operationId"], op["requiresCommandId"], op["completionStatus"])
|
||||
PY
|
||||
```
|
||||
|
||||
预期:
|
||||
|
||||
```text
|
||||
Counter({'needs_verification': 28, 'completed': 4})
|
||||
favoriteAsset True completed
|
||||
unfavoriteAsset True completed
|
||||
```
|
||||
|
||||
检查 OpenAPI diff 只限 Market:
|
||||
|
||||
```bash
|
||||
git diff --quiet -- docs/api-contracts/account/openapi.yaml docs/api-contracts/ai/openapi.yaml docs/api-contracts/content/openapi.yaml docs/api-contracts/events/openapi.yaml docs/api-contracts/knowledge/openapi.yaml docs/api-contracts/meta/openapi.yaml
|
||||
git diff --cached --quiet -- docs/api-contracts/account/openapi.yaml docs/api-contracts/ai/openapi.yaml docs/api-contracts/content/openapi.yaml docs/api-contracts/events/openapi.yaml docs/api-contracts/knowledge/openapi.yaml docs/api-contracts/meta/openapi.yaml
|
||||
git diff -- docs/api-contracts/market/openapi.yaml
|
||||
```
|
||||
|
||||
检查 Market 业务实现不被修改:
|
||||
|
||||
```bash
|
||||
git diff --quiet -- muse-cloud/muse-module-market/muse-module-market-server/src/main/java
|
||||
git diff --cached --quiet -- muse-cloud/muse-module-market/muse-module-market-server/src/main/java
|
||||
```
|
||||
|
||||
检查 whitespace:
|
||||
|
||||
```bash
|
||||
git diff --check
|
||||
```
|
||||
|
||||
检查 allowed diff:
|
||||
|
||||
```bash
|
||||
git -c core.quotePath=false status --short
|
||||
```
|
||||
|
||||
输出只能包含本执行版允许路径。若出现非批准 OpenAPI、业务实现、迁移或非批准 gate 文件,必须停止。
|
||||
|
||||
### Task 9:memory 与 `.agent`
|
||||
|
||||
实现通过后新增:
|
||||
|
||||
- `docs/memorys/2026-06-11-P1RMarket状态推进.md`
|
||||
|
||||
内容必须记录:
|
||||
|
||||
- 批准的 4 个 Market operation。
|
||||
- 保持 `needs_verification` 的 28 个 Market operation 及原因,必须点名 `listMarketplaceAssets` 与 `listMarketplaceRecommendations`。
|
||||
- OpenAPI commandId 合同修正范围。
|
||||
- `listMarketplaceRecommendations` 未进入本轮 completed,原因是个性化推荐 OpenAPI 描述与 fallback 排序实现未闭合。
|
||||
- `listMarketplaceAssets` 未进入本轮 completed,原因是 pageNo/pageSize 边界证据未闭合。
|
||||
- scanner/report/gate 修改范围。
|
||||
- 所有验证命令、结果、XML 计数、Market HTTP+DB `_test` 库名、Flyway `_test` 库名。
|
||||
- protected diff 与 allowed diff 结果。
|
||||
- 明确说明不代表 Market 32/32 completed,不代表 Account remaining 23 / Content / 总 P1R completed。
|
||||
|
||||
同时更新 `docs/agent-specs/.agent`,但只能记录事实,不写未验证结论。
|
||||
|
||||
### Task 10:fresh implementation review
|
||||
|
||||
实现、验证和文档留痕后,必须派发两类 fresh reviewer:
|
||||
|
||||
- spec/correctness reviewer:检查 4 个 operation 是否严格匹配本执行版,28 个 operation 是否仍 needs_verification,Market domain allowlist 是否未新增,OpenAPI commandId 修正是否只限 favorite/unfavorite。
|
||||
- quality/data-integrity/testing reviewer:检查 scanner/report/gate、legacy mixed gate 同步、HTTP+DB `_test`、XML 防空跑、Flyway `_test`、allowed/protected diff、rollback 是否可靠。
|
||||
|
||||
双 PASS 前不得提交、push 或宣称 Market completed approval 已收口。
|
||||
|
||||
## 回滚策略
|
||||
|
||||
如实现后需要撤回:
|
||||
|
||||
1. 从 `APPROVED_COMPLETED_OPERATIONS` 移除 4 个 `market:*` key。
|
||||
2. 处理 `docs/api-contracts/market/openapi.yaml` 中 favorite/unfavorite 的 required `X-Command-Id` header:
|
||||
- 如果用户要求完整撤回本轮合同修正,则移除 header。
|
||||
- 如果用户单独决定保留实现与 OpenAPI 一致的 commandId 合同,则保留 header,只撤回 completed approval。
|
||||
3. 重新运行:
|
||||
|
||||
```bash
|
||||
python3 muse-cloud/scripts/p1r-audit-api-coverage.py --check
|
||||
```
|
||||
|
||||
4. 恢复 gate 期望。完整撤回 OpenAPI header 时:
|
||||
|
||||
```text
|
||||
summary completed=127
|
||||
summary needsVerification=106
|
||||
market completed=0
|
||||
market needsVerification=32
|
||||
favoriteAsset requiresCommandId=false
|
||||
unfavoriteAsset requiresCommandId=false
|
||||
```
|
||||
|
||||
如果保留 OpenAPI commandId 合同,则 gate 必须恢复为:
|
||||
|
||||
```text
|
||||
summary completed=127
|
||||
summary needsVerification=106
|
||||
market completed=0
|
||||
market needsVerification=32
|
||||
favoriteAsset requiresCommandId=true
|
||||
unfavoriteAsset requiresCommandId=true
|
||||
```
|
||||
|
||||
5. 修订本轮 memory 并保留失败原因;除非用户明确要求,不直接删除失败留痕。
|
||||
6. 重新运行 Market/P1R focused gates 和 `git diff --check`。
|
||||
|
||||
## 验收标准
|
||||
|
||||
执行版本身可以进入实现前批准点的条件:
|
||||
|
||||
1. 本文件已写入 `docs/agent-specs/2026-06-11-P1RMarketCompletedApproval执行版.md`。
|
||||
2. `.agent` 记录执行版当前状态。
|
||||
3. `git diff --check` 通过。
|
||||
4. OpenAPI、scanner、coverage report、业务实现当前无新增 diff;`market/openapi.yaml` 也必须在用户批准前保持无 diff。
|
||||
5. fresh execution spec/scope review PASS。
|
||||
6. fresh execution quality/feasibility review PASS。
|
||||
|
||||
实现完成条件必须等用户批准后另行满足,不由本执行版写入自动成立。
|
||||
@ -226,6 +226,12 @@ paths:
|
||||
parameters:
|
||||
- $ref: '../openapi-base.yaml#/components/parameters/XApiVersion'
|
||||
- $ref: '#/components/parameters/assetId'
|
||||
- name: X-Command-Id
|
||||
in: header
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: 幂等命令 ID,收藏资产必须传入。
|
||||
responses:
|
||||
'200':
|
||||
description: 收藏成功
|
||||
@ -245,6 +251,12 @@ paths:
|
||||
parameters:
|
||||
- $ref: '../openapi-base.yaml#/components/parameters/XApiVersion'
|
||||
- $ref: '#/components/parameters/assetId'
|
||||
- name: X-Command-Id
|
||||
in: header
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: 幂等命令 ID,取消收藏必须传入。
|
||||
responses:
|
||||
'200':
|
||||
description: 取消收藏成功
|
||||
|
||||
262
docs/memorys/2026-06-11-P1RMarket状态推进.md
Normal file
262
docs/memorys/2026-06-11-P1RMarket状态推进.md
Normal file
@ -0,0 +1,262 @@
|
||||
# P1R Market 状态推进
|
||||
|
||||
日期:2026-06-11
|
||||
|
||||
## 结论
|
||||
|
||||
本轮只推进 Market 第一批 4 个 operation-level completed approval:
|
||||
|
||||
- `market:getMarketplaceAsset`
|
||||
- `market:listMarketplaceCategories`
|
||||
- `market:favoriteAsset`
|
||||
- `market:unfavoriteAsset`
|
||||
|
||||
Market 仍不是整域 completed。`listMarketplaceAssets`、`listMarketplaceRecommendations` 以及 purchase/install/handoff/governance/publish/review/appeal/projection 相关 28 个 Market operation 继续 `dedicated/needs_verification`。
|
||||
|
||||
本轮未修改 Market 业务实现,未修改数据库迁移,未把 `market` 加入 domain-level completed allowlist。
|
||||
|
||||
## 工作区与基线
|
||||
|
||||
工作区:
|
||||
|
||||
```text
|
||||
/Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0
|
||||
```
|
||||
|
||||
基线 HEAD:
|
||||
|
||||
```text
|
||||
f18116a test(p1r): 收口 Account 第一批 completed approval 门禁
|
||||
```
|
||||
|
||||
实施前 coverage summary:
|
||||
|
||||
```text
|
||||
233 127 106 0 0 0
|
||||
```
|
||||
|
||||
实施后 coverage summary:
|
||||
|
||||
```text
|
||||
233 131 102 0 0 0
|
||||
```
|
||||
|
||||
实施后 Market 状态:
|
||||
|
||||
```text
|
||||
completed=4
|
||||
needs_verification=28
|
||||
```
|
||||
|
||||
## 允许变更与实际变更
|
||||
|
||||
OpenAPI 只修改:
|
||||
|
||||
- `docs/api-contracts/market/openapi.yaml`
|
||||
|
||||
变更内容只是在:
|
||||
|
||||
- `POST /app-api/muse/marketplace/assets/{assetId}/favorite`
|
||||
- `DELETE /app-api/muse/marketplace/assets/{assetId}/favorite`
|
||||
|
||||
增加 required `X-Command-Id` header。原因是 Java Controller / Service 已强制校验 command id,而原 OpenAPI 没有声明,coverage report 因此误显示 `requiresCommandId=false`。
|
||||
|
||||
scanner 只修改:
|
||||
|
||||
- `muse-cloud/scripts/p1r-audit-api-coverage.py`
|
||||
|
||||
实际只追加 4 个 operation-level completed key,没有把 `market` 加入 `APPROVED_COMPLETED_DOMAINS`。
|
||||
|
||||
coverage report 已重新生成:
|
||||
|
||||
- `docs/superpowers/reports/p1r-api-coverage.json`
|
||||
- `docs/superpowers/reports/p1r-api-coverage.md`
|
||||
|
||||
## TDD 证据
|
||||
|
||||
RED 阶段先修改 gate 期望值和 Market operation 断言,在旧 scanner/report 下运行 focused P1R gates,预期失败已出现:
|
||||
|
||||
- 失败点包括 expected completed=131 but was 127。
|
||||
- 失败点包括 Market expected 4 completed but was 0。
|
||||
- 该失败证明 gate 会阻止未获 scanner/report 支撑的 completed 推进。
|
||||
|
||||
GREEN 阶段完成 OpenAPI commandId 合同修正、scanner operation-level allowlist、report 重新生成和 focused evidence tests 后,目标验证通过。
|
||||
|
||||
## MockMvc HTTP 入口 + DB 证据
|
||||
|
||||
新增:
|
||||
|
||||
- `muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rMarketDiscoveryFavoriteCompletedApprovalIT.java`
|
||||
|
||||
验证方式:
|
||||
|
||||
- 使用 Spring Mock web context + `MockMvc`。
|
||||
- 使用真实 PostgreSQL `_test` 数据库。
|
||||
- 使用 Flyway clean/migrate V1-V15。
|
||||
- 密码只从环境变量读取,禁止通过 JVM system property 传入。
|
||||
|
||||
说明:这里的 HTTP 入口证据是执行版要求的 WebApplicationContext + MockMvc 请求路径证据,不是启动真实 socket 端口的 live servlet container 证据。
|
||||
|
||||
验证数据库:
|
||||
|
||||
```text
|
||||
muse_p1r_market_discovery_favorite_completed_approval_test
|
||||
```
|
||||
|
||||
通过结果:
|
||||
|
||||
```text
|
||||
P1rMarketDiscoveryFavoriteCompletedApprovalIT: tests=9, failures=0, errors=0, skipped=0
|
||||
```
|
||||
|
||||
覆盖内容:
|
||||
|
||||
- 资产详情 happy path。
|
||||
- missing asset / invisible asset 不泄露详情、不写 command。
|
||||
- 分类聚合包含 fallback slot 与 unavailable exposure summary。
|
||||
- favorite 缺少 `X-Command-Id` 不写 DB。
|
||||
- favorite 写 active favorite 与 command,重复 command 回放不重复写。
|
||||
- unfavorite 写 inactive,不删除审计事实,重复 command 回放不重复写。
|
||||
- missing/invisible target 对 favorite/unfavorite 不污染 command/favorite fact。
|
||||
|
||||
## Flyway 证据
|
||||
|
||||
增强:
|
||||
|
||||
- `muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rMarketFlywayMigrationIT.java`
|
||||
|
||||
验证数据库:
|
||||
|
||||
```text
|
||||
muse_p1r_market_completed_approval_test
|
||||
```
|
||||
|
||||
通过结果:
|
||||
|
||||
```text
|
||||
P1rMarketFlywayMigrationIT: tests=4, failures=0, errors=0, skipped=0
|
||||
```
|
||||
|
||||
验证内容:
|
||||
|
||||
- V1-V15 clean migrate。
|
||||
- `muse_market_favorite` 表存在。
|
||||
- favorite user/asset/command 相关索引存在。
|
||||
- `uk_muse_market_favorite_user_asset` 唯一约束存在。
|
||||
- `trg_muse_market_favorite_updated_at` trigger 存在。
|
||||
- 同 tenant/user/asset duplicate insert 被 PostgreSQL 唯一约束拒绝,SQLState 为 `23505`。
|
||||
- 同 tenant/command_id duplicate insert 被 PostgreSQL partial unique index 拒绝,SQLState 为 `23505`。
|
||||
- favorite status 更新为 `inactive` 时,`trg_muse_market_favorite_updated_at` 会覆盖旧 `update_time`。
|
||||
|
||||
额外组合验证:
|
||||
|
||||
```text
|
||||
P1rMarketDiscoveryFavoriteCompletedApprovalIT -> P1rMarketFlywayMigrationIT: tests=13, failures=0, errors=0, skipped=0
|
||||
P1rMarketFlywayMigrationIT -> P1rMarketDiscoveryFavoriteCompletedApprovalIT: tests=13, failures=0, errors=0, skipped=0
|
||||
```
|
||||
|
||||
该组合验证使用真实 PostgreSQL `_test` 库:
|
||||
|
||||
```text
|
||||
muse_p1r_market_combined_completed_approval_test
|
||||
```
|
||||
|
||||
组合验证用于证明新增 Discovery IT 与增强 Flyway IT 对 `p1r.flyway.url/user` 的脱敏不会污染同一 Surefire JVM 内其他测试类。曾复现一次失败:Discovery IT 先运行后,Flyway IT 读到脱敏 system property,报缺少原始 `p1r.flyway.url`。修复方式是在 Discovery IT 与 Flyway IT 中都保存原始 `p1r.flyway.url/user`,并在 `@AfterAll` 恢复;随后补充 `-Dsurefire.runOrder=reversealphabetical`,日志确认先跑 Flyway IT、再跑 Discovery IT,合计 13/13 pass。
|
||||
|
||||
注意:Surefire XML 的 `<properties>` 会记录原始 `p1r.flyway.url` host 与 `p1r.flyway.user`,未发现 password 泄露。raw XML 只能作为本地验证产物,不应直接外发;如需外发测试报告,应先清洗 host/user 或改造为不通过 JVM system property 传递 URL/user。
|
||||
|
||||
review 处理:
|
||||
|
||||
- correctness reviewer 指出的 system property 污染为有效问题,已修复并用双向 combined IT 13/13 pass 关闭。
|
||||
- data/testing reviewer 提到“真正 HTTP”若理解为 socket 级 live server则当前证据不足。核对执行版后,本轮要求是 WebApplicationContext/MockMvc + real mapper + `_test` DB,不是 live socket server;本文档已把措辞收窄为 MockMvc HTTP 入口,避免过度声明。
|
||||
- data/testing reviewer 提到 favorite `status` 没有 DB-level CHECK、`asset_id/user_id` 没有 FK。该问题属于后续 schema hardening;本轮执行版明确禁止改迁移,只能在现有 V8/V15 约束上补强 unique/index/trigger 断言,不把它扩成新增迁移。
|
||||
- 最终 fresh implementation review 已双 PASS:Raman spec/correctness review PASS、Hubble data-integrity/testing review PASS,均无 P0/P1/P2/P3 阻塞项。两名 reviewer 均确认 `market` 未进入 domain-level allowlist、只推进 4 个 `market:*` operation、Market 仍为 4 completed / 28 needs_verification、`listMarketplaceAssets` 与 `listMarketplaceRecommendations` 继续 needs_verification、OpenAPI diff 仅限 favorite/unfavorite required `X-Command-Id`、Market main/java 与 SQL diff 为空、13 个目标 XML 合计 85 tests 且 failures/errors/skipped 均为 0。非阻塞注意事项仍是 raw Surefire XML 暴露 host/user 但不含 password,外发前需清洗。
|
||||
|
||||
## focused gates
|
||||
|
||||
Market focused verification 通过:
|
||||
|
||||
```text
|
||||
AppMuseMarketplaceAssetControllerTest: tests=11, failures=0, errors=0, skipped=0
|
||||
MarketAssetQueryServiceTest: tests=6, failures=0, errors=0, skipped=0
|
||||
MarketFavoriteServiceTest: tests=7, failures=0, errors=0, skipped=0
|
||||
MuseMarketAssetMapperTest: tests=2, failures=0, errors=0, skipped=0
|
||||
P1rMarketMigrationSqlTest: tests=9, failures=0, errors=0, skipped=0
|
||||
P1rMarketRouteOwnershipTest: tests=4, failures=0, errors=0, skipped=0
|
||||
P1rMarketRealApiGateTest: tests=6, failures=0, errors=0, skipped=0
|
||||
P1rApiCoverageReportTest: tests=6, failures=0, errors=0, skipped=0
|
||||
```
|
||||
|
||||
跨域 P1R focused gates 通过:
|
||||
|
||||
```text
|
||||
P1rApiCoverageReportTest: tests=6, failures=0, errors=0, skipped=0
|
||||
P1rMarketRealApiGateTest: tests=6, failures=0, errors=0, skipped=0
|
||||
P1rEventsRealApiGateTest: tests=6, failures=0, errors=0, skipped=0
|
||||
P1rAiRealApiGateTest: tests=7, failures=0, errors=0, skipped=0
|
||||
P1rKnowledgeRealApiGateTest: tests=8, failures=0, errors=0, skipped=0
|
||||
```
|
||||
|
||||
合计:
|
||||
|
||||
```text
|
||||
P1R mixed gates: 33/33 pass
|
||||
```
|
||||
|
||||
## scanner 与 diff 证据
|
||||
|
||||
scanner:
|
||||
|
||||
```bash
|
||||
python3 muse-cloud/scripts/p1r-audit-api-coverage.py --check
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
```text
|
||||
Generated docs/superpowers/reports/p1r-api-coverage.json
|
||||
Generated docs/superpowers/reports/p1r-api-coverage.md
|
||||
233 131 102 0 0 0
|
||||
```
|
||||
|
||||
Market operation 状态确认:
|
||||
|
||||
```text
|
||||
getMarketplaceAsset completed false
|
||||
listMarketplaceCategories completed false
|
||||
favoriteAsset completed true
|
||||
unfavoriteAsset completed true
|
||||
listMarketplaceAssets needs_verification false
|
||||
listMarketplaceRecommendations needs_verification false
|
||||
```
|
||||
|
||||
diff check:
|
||||
|
||||
```text
|
||||
git diff --check: pass
|
||||
```
|
||||
|
||||
OpenAPI diff:
|
||||
|
||||
```text
|
||||
docs/api-contracts/market/openapi.yaml
|
||||
```
|
||||
|
||||
仅包含 favorite/unfavorite 两个 operation 的 required `X-Command-Id` header。
|
||||
|
||||
## 后续边界
|
||||
|
||||
本轮完成后仍不得宣称:
|
||||
|
||||
- Market 32/32 completed。
|
||||
- Content completed。
|
||||
- Account remaining 23 completed。
|
||||
- 总 P1R completed。
|
||||
|
||||
后续如果继续 Market completed approval,推荐单独处理:
|
||||
|
||||
1. `listMarketplaceAssets`:先补 pageNo/pageSize 1..100 边界与防全量查询证据。
|
||||
2. `listMarketplaceRecommendations`:先解决 OpenAPI 个性化推荐语义与 fallback 排序实现的合同不一致。
|
||||
3. publish/review/governance 13 个 operation:另写执行版冻结范围、Events outbox 边界和 fresh runtime evidence。
|
||||
4. purchase/install/handoff/governanceImpact:必须先补目标 owner 消费闭环与 Account 读侧 E2E。
|
||||
5. appeal/projection:不能把申诉审计、治理 outbox 和 Account projection 混成一个 completed 闭环。
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"generatedAt": "2026-05-25T14:22:23+00:00",
|
||||
"generatedAt": "2026-05-25T07:06:40+00:00",
|
||||
"sourceContracts": [
|
||||
"docs/api-contracts/account/openapi.yaml",
|
||||
"docs/api-contracts/ai/openapi.yaml",
|
||||
@ -11,9 +11,9 @@
|
||||
],
|
||||
"summary": {
|
||||
"totalOperations": 233,
|
||||
"completedOperations": 127,
|
||||
"completedOperations": 131,
|
||||
"incompleteOperations": 0,
|
||||
"needsVerificationOperations": 106,
|
||||
"needsVerificationOperations": 102,
|
||||
"catchAllOperations": 0,
|
||||
"genericPersistenceOperations": 0,
|
||||
"ssePlaceholderOperations": 0,
|
||||
@ -13968,7 +13968,7 @@
|
||||
],
|
||||
"targetStage": "P1R-6 Market Real API",
|
||||
"implementationStatus": "dedicated",
|
||||
"completionStatus": "needs_verification",
|
||||
"completionStatus": "completed",
|
||||
"controllerFiles": [
|
||||
"muse-cloud/muse-module-market/muse-module-market-server/src/main/java/cn/iocoder/muse/module/market/controller/app/muse/AppMuseMarketplaceAssetController.java"
|
||||
],
|
||||
@ -14006,7 +14006,7 @@
|
||||
"muse-cloud/muse-module-market/muse-module-market-server/src/main/java/cn/iocoder/muse/module/market/application/muse/facade/MarketTargetOwnerFacade.java",
|
||||
"muse-cloud/muse-module-market/muse-module-market-server/src/main/java/cn/iocoder/muse/module/market/application/muse/facade/UnavailableMarketTargetOwnerFacade.java"
|
||||
],
|
||||
"notes": "Dedicated entry exists; later P1R stage must verify DTO, state machine, persistence, and real external closure."
|
||||
"notes": "Dedicated entry exists; user-approved P1R acceptance evidence allows this operation to advance to completed coverage."
|
||||
},
|
||||
{
|
||||
"domain": "market",
|
||||
@ -14069,14 +14069,14 @@
|
||||
"path": "/app-api/muse/marketplace/assets/{assetId}/favorite",
|
||||
"operationId": "unfavoriteAsset",
|
||||
"isWrite": true,
|
||||
"requiresCommandId": false,
|
||||
"requiresCommandId": true,
|
||||
"externalDependencies": [
|
||||
"Account",
|
||||
"MarketAuthorization"
|
||||
],
|
||||
"targetStage": "P1R-6 Market Real API",
|
||||
"implementationStatus": "dedicated",
|
||||
"completionStatus": "needs_verification",
|
||||
"completionStatus": "completed",
|
||||
"controllerFiles": [
|
||||
"muse-cloud/muse-module-market/muse-module-market-server/src/main/java/cn/iocoder/muse/module/market/controller/app/muse/AppMuseMarketplaceAssetController.java"
|
||||
],
|
||||
@ -14114,7 +14114,7 @@
|
||||
"muse-cloud/muse-module-market/muse-module-market-server/src/main/java/cn/iocoder/muse/module/market/application/muse/facade/MarketTargetOwnerFacade.java",
|
||||
"muse-cloud/muse-module-market/muse-module-market-server/src/main/java/cn/iocoder/muse/module/market/application/muse/facade/UnavailableMarketTargetOwnerFacade.java"
|
||||
],
|
||||
"notes": "Dedicated entry exists; later P1R stage must verify DTO, state machine, persistence, and real external closure."
|
||||
"notes": "Dedicated entry exists; user-approved P1R acceptance evidence allows this operation to advance to completed coverage."
|
||||
},
|
||||
{
|
||||
"domain": "market",
|
||||
@ -14123,14 +14123,14 @@
|
||||
"path": "/app-api/muse/marketplace/assets/{assetId}/favorite",
|
||||
"operationId": "favoriteAsset",
|
||||
"isWrite": true,
|
||||
"requiresCommandId": false,
|
||||
"requiresCommandId": true,
|
||||
"externalDependencies": [
|
||||
"Account",
|
||||
"MarketAuthorization"
|
||||
],
|
||||
"targetStage": "P1R-6 Market Real API",
|
||||
"implementationStatus": "dedicated",
|
||||
"completionStatus": "needs_verification",
|
||||
"completionStatus": "completed",
|
||||
"controllerFiles": [
|
||||
"muse-cloud/muse-module-market/muse-module-market-server/src/main/java/cn/iocoder/muse/module/market/controller/app/muse/AppMuseMarketplaceAssetController.java"
|
||||
],
|
||||
@ -14168,7 +14168,7 @@
|
||||
"muse-cloud/muse-module-market/muse-module-market-server/src/main/java/cn/iocoder/muse/module/market/application/muse/facade/MarketTargetOwnerFacade.java",
|
||||
"muse-cloud/muse-module-market/muse-module-market-server/src/main/java/cn/iocoder/muse/module/market/application/muse/facade/UnavailableMarketTargetOwnerFacade.java"
|
||||
],
|
||||
"notes": "Dedicated entry exists; later P1R stage must verify DTO, state machine, persistence, and real external closure."
|
||||
"notes": "Dedicated entry exists; user-approved P1R acceptance evidence allows this operation to advance to completed coverage."
|
||||
},
|
||||
{
|
||||
"domain": "market",
|
||||
@ -14346,7 +14346,7 @@
|
||||
],
|
||||
"targetStage": "P1R-6 Market Real API",
|
||||
"implementationStatus": "dedicated",
|
||||
"completionStatus": "needs_verification",
|
||||
"completionStatus": "completed",
|
||||
"controllerFiles": [
|
||||
"muse-cloud/muse-module-market/muse-module-market-server/src/main/java/cn/iocoder/muse/module/market/controller/app/muse/AppMuseMarketplaceAssetController.java"
|
||||
],
|
||||
@ -14384,7 +14384,7 @@
|
||||
"muse-cloud/muse-module-market/muse-module-market-server/src/main/java/cn/iocoder/muse/module/market/application/muse/facade/MarketTargetOwnerFacade.java",
|
||||
"muse-cloud/muse-module-market/muse-module-market-server/src/main/java/cn/iocoder/muse/module/market/application/muse/facade/UnavailableMarketTargetOwnerFacade.java"
|
||||
],
|
||||
"notes": "Dedicated entry exists; later P1R stage must verify DTO, state machine, persistence, and real external closure."
|
||||
"notes": "Dedicated entry exists; user-approved P1R acceptance evidence allows this operation to advance to completed coverage."
|
||||
},
|
||||
{
|
||||
"domain": "market",
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
# P1R API Coverage Report
|
||||
|
||||
- Generated at: `2026-05-25T14:22:23+00:00`
|
||||
- Generated at: `2026-05-25T07:06:40+00:00`
|
||||
- Total operations: `233`
|
||||
- Completed: `127`
|
||||
- Needs verification: `106`
|
||||
- Completed: `131`
|
||||
- Needs verification: `102`
|
||||
- Incomplete: `0`
|
||||
- Catch-all: `0`
|
||||
- Generic persistence: `0`
|
||||
@ -214,14 +214,14 @@
|
||||
| market | app | POST | `/app-api/muse/marketplace/appeals/{appealId}/supplements` | `supplementAppeal` | dedicated | needs_verification | P1R-6 Market Real API |
|
||||
| market | app | POST | `/app-api/muse/marketplace/appeals/{appealId}/withdraw` | `withdrawAppeal` | dedicated | needs_verification | P1R-6 Market Real API |
|
||||
| market | app | GET | `/app-api/muse/marketplace/assets` | `listMarketplaceAssets` | dedicated | needs_verification | P1R-6 Market Real API |
|
||||
| market | app | GET | `/app-api/muse/marketplace/assets/{assetId}` | `getMarketplaceAsset` | dedicated | needs_verification | P1R-6 Market Real API |
|
||||
| market | app | GET | `/app-api/muse/marketplace/assets/{assetId}` | `getMarketplaceAsset` | dedicated | completed | P1R-6 Market Real API |
|
||||
| market | app | POST | `/app-api/muse/marketplace/assets/{assetId}/bind-precheck` | `createBindPrecheck` | dedicated | needs_verification | P1R-6 Market Real API |
|
||||
| market | app | DELETE | `/app-api/muse/marketplace/assets/{assetId}/favorite` | `unfavoriteAsset` | dedicated | needs_verification | P1R-6 Market Real API |
|
||||
| market | app | POST | `/app-api/muse/marketplace/assets/{assetId}/favorite` | `favoriteAsset` | dedicated | needs_verification | P1R-6 Market Real API |
|
||||
| market | app | DELETE | `/app-api/muse/marketplace/assets/{assetId}/favorite` | `unfavoriteAsset` | dedicated | completed | P1R-6 Market Real API |
|
||||
| market | app | POST | `/app-api/muse/marketplace/assets/{assetId}/favorite` | `favoriteAsset` | dedicated | completed | P1R-6 Market Real API |
|
||||
| market | app | GET | `/app-api/muse/marketplace/assets/{assetId}/governance-impact` | `getGovernanceImpact` | dedicated | needs_verification | P1R-6 Market Real API |
|
||||
| market | app | POST | `/app-api/muse/marketplace/assets/{assetId}/install` | `installMarketplaceAsset` | dedicated | needs_verification | P1R-6 Market Real API |
|
||||
| market | app | POST | `/app-api/muse/marketplace/assets/{assetId}/purchase` | `purchaseAsset` | dedicated | needs_verification | P1R-6 Market Real API |
|
||||
| market | app | GET | `/app-api/muse/marketplace/categories` | `listMarketplaceCategories` | dedicated | needs_verification | P1R-6 Market Real API |
|
||||
| market | app | GET | `/app-api/muse/marketplace/categories` | `listMarketplaceCategories` | dedicated | completed | P1R-6 Market Real API |
|
||||
| market | app | POST | `/app-api/muse/marketplace/handoffs` | `createMarketplaceHandoff` | dedicated | needs_verification | P1R-6 Market Real API |
|
||||
| market | app | GET | `/app-api/muse/marketplace/handoffs/{handoffToken}` | `getHandoffStatus` | dedicated | needs_verification | P1R-6 Market Real API |
|
||||
| market | app | POST | `/app-api/muse/marketplace/handoffs/{handoffToken}/cancel` | `cancelHandoff` | dedicated | needs_verification | P1R-6 Market Real API |
|
||||
|
||||
@ -78,6 +78,13 @@ class P1rAiRealApiGateTest {
|
||||
entry("adminGetBusinessAuditEvent", "GET /admin-api/muse/audit/business-events/{eventId}")
|
||||
);
|
||||
|
||||
private static final Set<String> APPROVED_MARKET_COMPLETED_OPERATIONS = Set.of(
|
||||
"getMarketplaceAsset",
|
||||
"listMarketplaceCategories",
|
||||
"favoriteAsset",
|
||||
"unfavoriteAsset"
|
||||
);
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
@ -120,8 +127,8 @@ class P1rAiRealApiGateTest {
|
||||
void should_count_ai_and_knowledge_operations_as_completed() throws IOException {
|
||||
JsonNode report = readReport();
|
||||
|
||||
assertEquals(127, report.path("summary").path("completedOperations").asInt(),
|
||||
"completedOperations 必须只来自 AI 41 + Knowledge 59 + Events streamEvents 1 + Meta 16 + Account 10");
|
||||
assertEquals(131, report.path("summary").path("completedOperations").asInt(),
|
||||
"completedOperations 必须只来自 AI 41 + Knowledge 59 + Events streamEvents 1 + Meta 16 + Account 10 + Market 4");
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -150,6 +157,7 @@ class P1rAiRealApiGateTest {
|
||||
assertDomainStatusCount("content", 51);
|
||||
assertMetaStatusCount(16, 0);
|
||||
assertAccountStatusCount(10, 23);
|
||||
assertMarketStatusCount(4, 28);
|
||||
}
|
||||
|
||||
private void assertAccountStatusCount(int expectedCompleted, int expectedNeedsVerification) throws IOException {
|
||||
@ -214,6 +222,33 @@ class P1rAiRealApiGateTest {
|
||||
assertEquals(expectedCount, actualCount, domain + " operation 数量必须保持不变");
|
||||
}
|
||||
|
||||
private void assertMarketStatusCount(int expectedCompleted, int expectedNeedsVerification) throws IOException {
|
||||
int completed = 0;
|
||||
int needsVerification = 0;
|
||||
int total = 0;
|
||||
for (JsonNode operation : readOperations()) {
|
||||
if (!"market".equals(operation.path("domain").asText())) {
|
||||
continue;
|
||||
}
|
||||
total++;
|
||||
String operationId = operation.path("operationId").asText("<missing-operationId>");
|
||||
assertEquals("dedicated", operation.path("implementationStatus").asText(),
|
||||
"market/" + operationId + " 必须保持 dedicated");
|
||||
if (APPROVED_MARKET_COMPLETED_OPERATIONS.contains(operationId)) {
|
||||
completed++;
|
||||
assertEquals("completed", operation.path("completionStatus").asText(),
|
||||
"market/" + operationId + " 已获本轮批准后必须 completed");
|
||||
} else {
|
||||
needsVerification++;
|
||||
assertEquals("needs_verification", operation.path("completionStatus").asText(),
|
||||
"market/" + operationId + " 未获本轮批准,必须继续 needs_verification");
|
||||
}
|
||||
}
|
||||
assertEquals(32, total, "Market operation 数量必须保持 32");
|
||||
assertEquals(expectedCompleted, completed, "Market 第一批 completed approval 只能推进 4 个 operation");
|
||||
assertEquals(expectedNeedsVerification, needsVerification, "Market 剩余 28 个 operation 必须继续 needs_verification");
|
||||
}
|
||||
|
||||
private JsonNode readAiOperations() throws IOException {
|
||||
ArrayNode aiOperations = objectMapper.createArrayNode();
|
||||
for (JsonNode operation : readOperations()) {
|
||||
|
||||
@ -80,7 +80,11 @@ class P1rApiCoverageReportTest {
|
||||
"account:adminGetBalanceSnapshots",
|
||||
"account:getAppBalanceSnapshots",
|
||||
"account:adminCreateQuotaAdjustment",
|
||||
"account:adminListQuotaAdjustments"
|
||||
"account:adminListQuotaAdjustments",
|
||||
"market:getMarketplaceAsset",
|
||||
"market:listMarketplaceCategories",
|
||||
"market:favoriteAsset",
|
||||
"market:unfavoriteAsset"
|
||||
);
|
||||
|
||||
/** 已批准的 MetaSchema 管理接口。 */
|
||||
@ -121,6 +125,14 @@ class P1rApiCoverageReportTest {
|
||||
"adminListQuotaAdjustments"
|
||||
);
|
||||
|
||||
/** 已批准的 Market 第一批详情、分类和收藏接口,仍保持 operation-level approval。 */
|
||||
private static final Set<String> APPROVED_MARKET_COMPLETED_OPERATIONS = Set.of(
|
||||
"getMarketplaceAsset",
|
||||
"listMarketplaceCategories",
|
||||
"favoriteAsset",
|
||||
"unfavoriteAsset"
|
||||
);
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
@ -222,12 +234,12 @@ class P1rApiCoverageReportTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 completed approval 只推进已批准的 Events SSE、Meta 与 Account operation,不连带推进其它 owner domain。
|
||||
* 验证 completed approval 只推进已批准的 Events SSE、Meta、Account 与 Market operation,不连带推进其它 owner domain。
|
||||
*
|
||||
* @throws IOException 读取覆盖报告失败时抛出
|
||||
*/
|
||||
@Test
|
||||
void should_only_promote_approved_events_meta_and_account_operations() throws IOException {
|
||||
void should_only_promote_approved_events_meta_account_and_market_operations() throws IOException {
|
||||
JsonNode operations = readOperations();
|
||||
|
||||
int completed = 0;
|
||||
@ -236,7 +248,7 @@ class P1rApiCoverageReportTest {
|
||||
completed++;
|
||||
}
|
||||
}
|
||||
assertEquals(127, completed, "Account 第一批 10 个 operation-level approval 后 completed 总数只能从 117 增至 127");
|
||||
assertEquals(131, completed, "Market 第一批 4 个 operation-level approval 后 completed 总数只能从 127 增至 131");
|
||||
|
||||
assertOperationStatus("events", "streamEvents", "dedicated", "completed");
|
||||
for (String operationId : APPROVED_META_SCHEMA_COMPLETED_OPERATIONS) {
|
||||
@ -248,7 +260,14 @@ class P1rApiCoverageReportTest {
|
||||
for (String operationId : APPROVED_ACCOUNT_COMPLETED_OPERATIONS) {
|
||||
assertOperationStatus("account", operationId, "dedicated", "completed");
|
||||
}
|
||||
assertDomainStatusCount("market", "dedicated", "needs_verification", 32);
|
||||
for (String operationId : APPROVED_MARKET_COMPLETED_OPERATIONS) {
|
||||
assertOperationStatus("market", operationId, "dedicated", "completed");
|
||||
}
|
||||
assertOperationStatus("market", "listMarketplaceAssets", "dedicated", "needs_verification");
|
||||
assertOperationStatus("market", "listMarketplaceRecommendations", "dedicated", "needs_verification");
|
||||
assertOperationStatus("market", "favoriteAsset", "dedicated", "completed", true);
|
||||
assertOperationStatus("market", "unfavoriteAsset", "dedicated", "completed", true);
|
||||
assertDomainStatusCount("market", "dedicated", "needs_verification", 28);
|
||||
assertDomainStatusCount("account", "dedicated", "needs_verification", 23);
|
||||
assertDomainStatusCount("content", "dedicated", "needs_verification", 51);
|
||||
}
|
||||
@ -284,6 +303,23 @@ class P1rApiCoverageReportTest {
|
||||
throw new AssertionError(domain + ":" + operationId + " 必须存在于 P1R coverage report");
|
||||
}
|
||||
|
||||
private void assertOperationStatus(String domain, String operationId, String implementationStatus,
|
||||
String completionStatus, boolean requiresCommandId) throws IOException {
|
||||
for (JsonNode operation : readOperations()) {
|
||||
if (domain.equals(operation.path("domain").asText())
|
||||
&& operationId.equals(operation.path("operationId").asText())) {
|
||||
assertEquals(implementationStatus, operation.path("implementationStatus").asText(),
|
||||
operationId + " implementationStatus 不符合预期");
|
||||
assertEquals(completionStatus, operation.path("completionStatus").asText(),
|
||||
operationId + " completionStatus 不符合预期");
|
||||
assertEquals(requiresCommandId, operation.path("requiresCommandId").asBoolean(),
|
||||
operationId + " requiresCommandId 不符合 OpenAPI 合同");
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new AssertionError(domain + ":" + operationId + " 必须存在于 P1R coverage report");
|
||||
}
|
||||
|
||||
private void assertDomainStatusCount(String domain, String implementationStatus, String completionStatus, int expected)
|
||||
throws IOException {
|
||||
int count = 0;
|
||||
|
||||
@ -38,6 +38,13 @@ class P1rEventsRealApiGateTest {
|
||||
"missing"
|
||||
);
|
||||
|
||||
private static final Set<String> APPROVED_MARKET_COMPLETED_OPERATIONS = Set.of(
|
||||
"getMarketplaceAsset",
|
||||
"listMarketplaceCategories",
|
||||
"favoriteAsset",
|
||||
"unfavoriteAsset"
|
||||
);
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
@ -81,10 +88,10 @@ class P1rEventsRealApiGateTest {
|
||||
void should_keep_completed_approval_summary_at_approved_operation_boundary() throws IOException {
|
||||
JsonNode summary = readReport().path("summary");
|
||||
|
||||
assertEquals(127, summary.path("completedOperations").asInt(),
|
||||
"completedOperations 只能来自 AI 41 + Knowledge 59 + Events streamEvents 1 + Meta 16 + Account 10");
|
||||
assertEquals(106, summary.path("needsVerificationOperations").asInt(),
|
||||
"needsVerificationOperations 必须扣除已批准的 Events streamEvents、Meta 16 和 Account 10");
|
||||
assertEquals(131, summary.path("completedOperations").asInt(),
|
||||
"completedOperations 只能来自 AI 41 + Knowledge 59 + Events streamEvents 1 + Meta 16 + Account 10 + Market 4");
|
||||
assertEquals(102, summary.path("needsVerificationOperations").asInt(),
|
||||
"needsVerificationOperations 必须扣除已批准的 Events streamEvents、Meta 16、Account 10 和 Market 4");
|
||||
assertEquals(0, summary.path("incompleteOperations").asInt(),
|
||||
"Events 退出 placeholder 后不应再留下 incomplete operation");
|
||||
assertEquals(0, summary.path("genericPersistenceOperations").asInt(),
|
||||
@ -94,8 +101,10 @@ class P1rEventsRealApiGateTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_keep_all_market_operations_dedicated_and_needs_verification() throws IOException {
|
||||
void should_keep_only_first_batch_market_operations_completed() throws IOException {
|
||||
JsonNode marketOperations = readMarketOperations();
|
||||
int completed = 0;
|
||||
int needsVerification = 0;
|
||||
|
||||
assertEquals(EXPECTED_MARKET_OPERATION_COUNT, marketOperations.size(),
|
||||
"P1R-7a 不能改变 Market 32 个 operation 的覆盖清单");
|
||||
@ -104,9 +113,22 @@ class P1rEventsRealApiGateTest {
|
||||
String operationId = operation.path("operationId").asText("<missing-operationId>");
|
||||
assertEquals("dedicated", operation.path("implementationStatus").asText(),
|
||||
"Market/" + operationId + " 必须继续保持 dedicated");
|
||||
if (APPROVED_MARKET_COMPLETED_OPERATIONS.contains(operationId)) {
|
||||
completed++;
|
||||
assertEquals("completed", operation.path("completionStatus").asText(),
|
||||
"Market/" + operationId + " 已获本轮批准后必须 completed");
|
||||
continue;
|
||||
}
|
||||
needsVerification++;
|
||||
assertEquals("needs_verification", operation.path("completionStatus").asText(),
|
||||
"Market/" + operationId + " 必须继续保持 needs_verification,不能提前 completed");
|
||||
"Market/" + operationId + " 未获本轮批准,必须继续保持 needs_verification");
|
||||
}
|
||||
assertEquals(4, completed, "Market 第一批 completed approval 只能推进 4 个 operation");
|
||||
assertEquals(28, needsVerification, "Market 剩余 28 个 operation 必须继续 needs_verification");
|
||||
assertMarketOperationStatus("listMarketplaceAssets", "needs_verification", false);
|
||||
assertMarketOperationStatus("listMarketplaceRecommendations", "needs_verification", false);
|
||||
assertMarketOperationStatus("favoriteAsset", "completed", true);
|
||||
assertMarketOperationStatus("unfavoriteAsset", "completed", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -151,6 +173,21 @@ class P1rEventsRealApiGateTest {
|
||||
return readOperationsByDomain("account");
|
||||
}
|
||||
|
||||
private void assertMarketOperationStatus(String operationId, String expectedCompletionStatus,
|
||||
boolean expectedRequiresCommandId) throws IOException {
|
||||
for (JsonNode operation : readMarketOperations()) {
|
||||
if (!operationId.equals(operation.path("operationId").asText())) {
|
||||
continue;
|
||||
}
|
||||
assertEquals(expectedCompletionStatus, operation.path("completionStatus").asText(),
|
||||
"Market/" + operationId + " completionStatus 不符合本轮审批边界");
|
||||
assertEquals(expectedRequiresCommandId, operation.path("requiresCommandId").asBoolean(),
|
||||
"Market/" + operationId + " requiresCommandId 不符合 OpenAPI 合同");
|
||||
return;
|
||||
}
|
||||
throw new AssertionError("Market/" + operationId + " 必须存在于 coverage report");
|
||||
}
|
||||
|
||||
private JsonNode readOperationsByDomain(String domain) throws IOException {
|
||||
ArrayNode matchingOperations = objectMapper.createArrayNode();
|
||||
for (JsonNode operation : readOperations()) {
|
||||
|
||||
@ -100,6 +100,13 @@ class P1rKnowledgeRealApiGateTest {
|
||||
entry("getLocalKnowledge", "GET /app-api/muse/works/{workId}/local-knowledge")
|
||||
);
|
||||
|
||||
private static final Set<String> APPROVED_MARKET_COMPLETED_OPERATIONS = Set.of(
|
||||
"getMarketplaceAsset",
|
||||
"listMarketplaceCategories",
|
||||
"favoriteAsset",
|
||||
"unfavoriteAsset"
|
||||
);
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
@ -159,8 +166,8 @@ class P1rKnowledgeRealApiGateTest {
|
||||
void should_count_ai_and_knowledge_operations_as_completed() throws IOException {
|
||||
JsonNode report = readReport();
|
||||
|
||||
assertEquals(127, report.path("summary").path("completedOperations").asInt(),
|
||||
"completedOperations 必须只来自 AI 41 + Knowledge 59 + Events streamEvents 1 + Meta 16 + Account 10");
|
||||
assertEquals(131, report.path("summary").path("completedOperations").asInt(),
|
||||
"completedOperations 必须只来自 AI 41 + Knowledge 59 + Events streamEvents 1 + Meta 16 + Account 10 + Market 4");
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -169,6 +176,7 @@ class P1rKnowledgeRealApiGateTest {
|
||||
assertDomainStatusCount("content", 51);
|
||||
assertMetaStatusCount(16, 0);
|
||||
assertAccountStatusCount(10, 23);
|
||||
assertMarketStatusCount(4, 28);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -236,6 +244,33 @@ class P1rKnowledgeRealApiGateTest {
|
||||
assertEquals(expectedNeedsVerification, needsVerification, "Account 剩余 23 个 operation 必须继续 needs_verification");
|
||||
}
|
||||
|
||||
private void assertMarketStatusCount(int expectedCompleted, int expectedNeedsVerification) throws IOException {
|
||||
int completed = 0;
|
||||
int needsVerification = 0;
|
||||
int total = 0;
|
||||
for (JsonNode operation : readOperations()) {
|
||||
if (!"market".equals(operation.path("domain").asText())) {
|
||||
continue;
|
||||
}
|
||||
total++;
|
||||
String operationId = operation.path("operationId").asText("<missing-operationId>");
|
||||
assertEquals("dedicated", operation.path("implementationStatus").asText(),
|
||||
"market/" + operationId + " 必须保持 dedicated");
|
||||
if (APPROVED_MARKET_COMPLETED_OPERATIONS.contains(operationId)) {
|
||||
completed++;
|
||||
assertEquals("completed", operation.path("completionStatus").asText(),
|
||||
"market/" + operationId + " 已获本轮批准后必须 completed");
|
||||
} else {
|
||||
needsVerification++;
|
||||
assertEquals("needs_verification", operation.path("completionStatus").asText(),
|
||||
"market/" + operationId + " 未获本轮批准,必须继续 needs_verification");
|
||||
}
|
||||
}
|
||||
assertEquals(32, total, "Market operation 数量必须保持 32");
|
||||
assertEquals(expectedCompleted, completed, "Market 第一批 completed approval 只能推进 4 个 operation");
|
||||
assertEquals(expectedNeedsVerification, needsVerification, "Market 剩余 28 个 operation 必须继续 needs_verification");
|
||||
}
|
||||
|
||||
private void assertMetaStatusCount(int expectedCompleted, int expectedNeedsVerification) throws IOException {
|
||||
int completed = 0;
|
||||
int needsVerification = 0;
|
||||
|
||||
@ -0,0 +1,834 @@
|
||||
package cn.iocoder.muse.server.framework.api;
|
||||
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.muse.framework.common.biz.infra.logger.ApiErrorLogCommonApi;
|
||||
import cn.iocoder.muse.framework.common.biz.infra.logger.dto.ApiErrorLogCreateReqDTO;
|
||||
import cn.iocoder.muse.framework.common.enums.UserTypeEnum;
|
||||
import cn.iocoder.muse.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.muse.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.muse.framework.datasource.config.MuseDataSourceAutoConfiguration;
|
||||
import cn.iocoder.muse.framework.mybatis.config.MuseMybatisAutoConfiguration;
|
||||
import cn.iocoder.muse.framework.security.core.LoginUser;
|
||||
import cn.iocoder.muse.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.muse.framework.tenant.core.context.TenantContextHolder;
|
||||
import cn.iocoder.muse.framework.web.config.MuseWebAutoConfiguration;
|
||||
import cn.iocoder.muse.module.market.application.muse.MarketAssetQueryServiceImpl;
|
||||
import cn.iocoder.muse.module.market.application.muse.MarketCommandServiceImpl;
|
||||
import cn.iocoder.muse.module.market.application.muse.MarketFavoriteServiceImpl;
|
||||
import cn.iocoder.muse.module.market.controller.app.muse.AppMuseMarketplaceAssetController;
|
||||
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
|
||||
import com.github.yulichang.autoconfigure.MybatisPlusJoinAutoConfiguration;
|
||||
import org.flywaydb.core.Flyway;
|
||||
import org.flywaydb.core.api.output.MigrateResult;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInstance;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.test.context.DynamicPropertySource;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
import static cn.iocoder.muse.module.market.enums.ErrorCodeConstants.MARKET_ASSET_NOT_EXISTS;
|
||||
import static cn.iocoder.muse.module.market.enums.ErrorCodeConstants.MARKET_COMMAND_ID_REQUIRED;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* P1R Market 第一批 completed approval:资产发现和收藏的 HTTP + 真实 PostgreSQL 证据。
|
||||
*
|
||||
* <p>本测试只允许连接显式传入的 PostgreSQL {@code _test} 库,启动最小 Spring MVC 上下文,
|
||||
* 通过 {@code /app-api/muse/**} 真实前缀进入 Controller,并读取数据库事实验证 Market
|
||||
* 详情、分类、收藏和命令幂等链路。这里不导入 Market 全包,避免无关 worker 或外部依赖污染证据。</p>
|
||||
*/
|
||||
@SpringBootTest(
|
||||
classes = P1rMarketDiscoveryFavoriteCompletedApprovalIT.CompletedApprovalConfiguration.class,
|
||||
webEnvironment = SpringBootTest.WebEnvironment.MOCK
|
||||
)
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class P1rMarketDiscoveryFavoriteCompletedApprovalIT {
|
||||
|
||||
private static final String TARGET_VERSION = "15";
|
||||
private static final Long TENANT_ID = 100L;
|
||||
private static final Long LOGIN_USER_ID = 9001L;
|
||||
private static final Long PUBLISHER_USER_ID = 2001L;
|
||||
private static final Set<String> CREDENTIAL_QUERY_KEYS = Set.of(
|
||||
"user", "username", "password", "pass", "pwd", "sslpassword", "ssl_password",
|
||||
"token", "secret", "api_key", "apikey", "bearer", "access_token", "refresh_token");
|
||||
|
||||
private static volatile CompletedApprovalSettings cachedSettings;
|
||||
private static volatile boolean originalFlywayPropertiesCaptured;
|
||||
private static volatile String originalFlywayUrlSystemProperty;
|
||||
private static volatile String originalFlywayUserSystemProperty;
|
||||
|
||||
@Autowired
|
||||
private DataSource dataSource;
|
||||
@Autowired
|
||||
private WebApplicationContext webApplicationContext;
|
||||
|
||||
private MockMvc mockMvc;
|
||||
private SeedFacts seedFacts;
|
||||
|
||||
@DynamicPropertySource
|
||||
static void registerCompletedApprovalProperties(DynamicPropertyRegistry registry) {
|
||||
CompletedApprovalSettings settings = settings();
|
||||
redactFlywaySystemProperties(settings.jdbcUrl(), settings.jdbcUser());
|
||||
registry.add("spring.application.name", () -> "p1r-market-completed-approval-it");
|
||||
registry.add("muse.info.base-package", () -> "cn.iocoder.muse.module.market");
|
||||
registry.add("muse.web.admin-ui.url", () -> "http://localhost");
|
||||
registry.add("spring.datasource.url", settings::jdbcUrl);
|
||||
registry.add("spring.datasource.username", settings::jdbcUser);
|
||||
registry.add("spring.datasource.password", settings::jdbcPassword);
|
||||
registry.add("spring.datasource.driver-class-name", () -> "org.postgresql.Driver");
|
||||
registry.add("spring.main.banner-mode", () -> "off");
|
||||
registry.add("spring.main.lazy-initialization", () -> "true");
|
||||
registry.add("mybatis-plus.global-config.db-config.id-type", () -> "AUTO");
|
||||
}
|
||||
|
||||
@BeforeAll
|
||||
void migrateMarketSchema() {
|
||||
CompletedApprovalSettings settings = settings();
|
||||
silenceFlywayInfoLogs();
|
||||
Flyway flyway = Flyway.configure()
|
||||
.dataSource(settings.jdbcUrl(), settings.jdbcUser(), settings.jdbcPassword())
|
||||
.locations(resolveMuseSqlLocation(settings.flywayLocations()))
|
||||
.schemas("public")
|
||||
.defaultSchema("public")
|
||||
.target(TARGET_VERSION)
|
||||
.cleanDisabled(false)
|
||||
.load();
|
||||
cleanSchema(flyway, settings);
|
||||
MigrateResult result = migrateSchema(flyway, settings);
|
||||
assertTrue(result.migrationsExecuted >= 15,
|
||||
"Market completed approval 必须在隔离库执行 V1-V15 全量迁移,实际: " + result.migrationsExecuted);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
|
||||
resetMarketTables();
|
||||
this.seedFacts = seedMarketFacts();
|
||||
setRuntimeContext();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
SecurityContextHolder.clearContext();
|
||||
TenantContextHolder.clear();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
void restoreFlywaySystemProperties() {
|
||||
restoreOriginalFlywaySystemProperties();
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_rejectPasswordSystemProperty() {
|
||||
System.setProperty("p1r.market.completed.password", "must-not-be-used");
|
||||
try {
|
||||
AssertionError error = assertThrows(AssertionError.class,
|
||||
P1rMarketDiscoveryFavoriteCompletedApprovalIT::assertNoPasswordSystemProperties);
|
||||
assertTrue(error.getMessage().contains("p1r.market.completed.password"),
|
||||
"拒绝 JVM password system property 时必须指出属性名");
|
||||
} finally {
|
||||
System.clearProperty("p1r.market.completed.password");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_getVisibleAssetDetailFromRealPostgresql() throws Exception {
|
||||
mockMvc.perform(get("/app-api/muse/marketplace/assets/{assetId}", seedFacts.visibleAssetId())
|
||||
.header("X-API-Version", "1"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.assetId").value(String.valueOf(seedFacts.visibleAssetId())))
|
||||
.andExpect(jsonPath("$.data.name").value("P1R Market Listed Agent"))
|
||||
.andExpect(jsonPath("$.data.version").value("1.0.0"))
|
||||
.andExpect(jsonPath("$.data.versionId").value(seedFacts.visibleVersionId()))
|
||||
.andExpect(jsonPath("$.data.licenseInfo.licenseType").value("standard"))
|
||||
.andExpect(jsonPath("$.data.categoryId").value(10))
|
||||
.andExpect(jsonPath("$.data.governanceStatus.listingStatus").value("listed"))
|
||||
.andExpect(jsonPath("$.data.governanceStatus.sourceStatus").value("available"))
|
||||
.andExpect(jsonPath("$.data.userActions.canFavorite").value(true))
|
||||
.andExpect(jsonPath("$.data.userActions.isFavorite").value(false));
|
||||
|
||||
assertEquals(0, commandCount(), "纯详情查询不能写 Market command fact");
|
||||
assertEquals(0, favoriteCount(seedFacts.visibleAssetId()), "纯详情查询不能写 Market favorite fact");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_rejectMissingAssetDetailWithoutLeaking() throws Exception {
|
||||
mockMvc.perform(get("/app-api/muse/marketplace/assets/{assetId}", 999999L)
|
||||
.header("X-API-Version", "1"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(MARKET_ASSET_NOT_EXISTS.getCode()))
|
||||
.andExpect(jsonPath("$.msg").exists());
|
||||
|
||||
assertEquals(0, commandCount(), "missing detail 不能写 Market command fact");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_rejectInvisibleAssetDetailWithoutLeaking() throws Exception {
|
||||
mockMvc.perform(get("/app-api/muse/marketplace/assets/{assetId}", seedFacts.invisibleAssetId())
|
||||
.header("X-API-Version", "1"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(MARKET_ASSET_NOT_EXISTS.getCode()))
|
||||
.andExpect(jsonPath("$.msg").exists());
|
||||
|
||||
assertEquals(0, commandCount(), "不可见资产详情不能写 Market command fact");
|
||||
assertEquals(0, favoriteCount(seedFacts.invisibleAssetId()), "不可见资产详情不能写 Market favorite fact");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_listCategoriesWithFallbackSlotAndUnavailableExposureSummary() throws Exception {
|
||||
mockMvc.perform(get("/app-api/muse/marketplace/categories")
|
||||
.header("X-API-Version", "1")
|
||||
.param("includeExposureSummary", "true"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.categories", hasSize(2)))
|
||||
.andExpect(jsonPath("$.data.categories[0].categoryId").value(10))
|
||||
.andExpect(jsonPath("$.data.categories[0].assetCount").value(1))
|
||||
.andExpect(jsonPath("$.data.recommendationSlots[0].slotId").value("fallback_popular"))
|
||||
.andExpect(jsonPath("$.data.recommendationSlots[0].assetIds",
|
||||
hasItem(seedFacts.visibleAssetId().intValue())))
|
||||
.andExpect(jsonPath("$.data.exposureSummary.totalExposureRange").value("unavailable"))
|
||||
.andExpect(jsonPath("$.data.exposureSummary.blockedReason").value("exposure_model_not_available"));
|
||||
|
||||
assertEquals(0, commandCount(), "分类查询不能写 Market command fact");
|
||||
assertEquals(0, favoriteCount(seedFacts.visibleAssetId()), "分类查询不能写 Market favorite fact");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_rejectFavoriteMissingCommandIdWithoutDbWrite() throws Exception {
|
||||
mockMvc.perform(post("/app-api/muse/marketplace/assets/{assetId}/favorite", seedFacts.visibleAssetId())
|
||||
.header("X-API-Version", "1"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(MARKET_COMMAND_ID_REQUIRED.getCode()))
|
||||
.andExpect(jsonPath("$.msg").exists());
|
||||
|
||||
assertEquals(0, commandCount(), "缺少 X-Command-Id 的收藏请求不能写 command fact");
|
||||
assertEquals(0, favoriteCount(seedFacts.visibleAssetId()), "缺少 X-Command-Id 的收藏请求不能写 favorite fact");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_writeActiveFavoriteAndReplaySameCommand() throws Exception {
|
||||
String commandId = "p1r-market-favorite-replay";
|
||||
|
||||
mockMvc.perform(post("/app-api/muse/marketplace/assets/{assetId}/favorite", seedFacts.visibleAssetId())
|
||||
.header("X-API-Version", "1")
|
||||
.header("X-Command-Id", commandId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data").value(true));
|
||||
mockMvc.perform(post("/app-api/muse/marketplace/assets/{assetId}/favorite", seedFacts.visibleAssetId())
|
||||
.header("X-API-Version", "1")
|
||||
.header("X-Command-Id", commandId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data").value(true));
|
||||
|
||||
assertEquals(1, favoriteCount(seedFacts.visibleAssetId()), "重复收藏命令不能插入重复 favorite fact");
|
||||
assertEquals("active", favoriteStatus(seedFacts.visibleAssetId()), "收藏后 favorite fact 必须为 active");
|
||||
assertEquals(commandId, favoriteCommandId(seedFacts.visibleAssetId()), "favorite fact 必须保留首次成功 commandId");
|
||||
CommandFact command = commandFact(commandId);
|
||||
assertNotNull(command, "收藏成功必须写 Market command fact");
|
||||
assertEquals("favoriteAsset", command.operationId());
|
||||
assertEquals("completed", command.status());
|
||||
assertEquals(LOGIN_USER_ID, command.actorUserId());
|
||||
assertEquals(PUBLISHER_USER_ID, command.ownerUserId());
|
||||
assertEquals(seedFacts.visibleAssetId(), command.targetId());
|
||||
assertCommandResultSnapshot(command, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_unfavoriteToInactiveAndReplayWithoutDeletingFact() throws Exception {
|
||||
String favoriteCommandId = "p1r-market-unfavorite-seed";
|
||||
String unfavoriteCommandId = "p1r-market-unfavorite-replay";
|
||||
mockMvc.perform(post("/app-api/muse/marketplace/assets/{assetId}/favorite", seedFacts.visibleAssetId())
|
||||
.header("X-API-Version", "1")
|
||||
.header("X-Command-Id", favoriteCommandId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
|
||||
mockMvc.perform(delete("/app-api/muse/marketplace/assets/{assetId}/favorite", seedFacts.visibleAssetId())
|
||||
.header("X-API-Version", "1")
|
||||
.header("X-Command-Id", unfavoriteCommandId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data").value(true));
|
||||
mockMvc.perform(delete("/app-api/muse/marketplace/assets/{assetId}/favorite", seedFacts.visibleAssetId())
|
||||
.header("X-API-Version", "1")
|
||||
.header("X-Command-Id", unfavoriteCommandId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data").value(true));
|
||||
|
||||
assertEquals(1, favoriteCount(seedFacts.visibleAssetId()), "取消收藏不能物理删除 favorite 审计事实");
|
||||
assertEquals("inactive", favoriteStatus(seedFacts.visibleAssetId()), "取消收藏后 favorite fact 必须为 inactive");
|
||||
assertEquals(unfavoriteCommandId, favoriteCommandId(seedFacts.visibleAssetId()),
|
||||
"favorite fact 必须保留取消收藏 commandId");
|
||||
assertEquals(2, commandCount(), "favorite + unfavorite 各只允许写 1 条 command fact");
|
||||
CommandFact command = commandFact(unfavoriteCommandId);
|
||||
assertNotNull(command, "取消收藏成功必须写 Market command fact");
|
||||
assertEquals("unfavoriteAsset", command.operationId());
|
||||
assertEquals("completed", command.status());
|
||||
assertCommandResultSnapshot(command, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_rejectUnfavoriteMissingCommandIdAndMissingOrInvisibleTargetsWithoutDbWrite() throws Exception {
|
||||
String seedCommandId = "p1r-market-delete-missing-header-seed";
|
||||
mockMvc.perform(post("/app-api/muse/marketplace/assets/{assetId}/favorite", seedFacts.visibleAssetId())
|
||||
.header("X-API-Version", "1")
|
||||
.header("X-Command-Id", seedCommandId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
assertEquals("active", favoriteStatus(seedFacts.visibleAssetId()), "前置收藏必须先写入 active favorite fact");
|
||||
|
||||
mockMvc.perform(delete("/app-api/muse/marketplace/assets/{assetId}/favorite", seedFacts.visibleAssetId())
|
||||
.header("X-API-Version", "1"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(MARKET_COMMAND_ID_REQUIRED.getCode()))
|
||||
.andExpect(jsonPath("$.msg").exists());
|
||||
assertEquals("active", favoriteStatus(seedFacts.visibleAssetId()),
|
||||
"缺少 X-Command-Id 的取消收藏不能改变已有 favorite fact");
|
||||
assertEquals(1, commandCount(), "缺少 X-Command-Id 的取消收藏不能新增 command fact");
|
||||
|
||||
String missingFavoriteCommand = "p1r-market-missing-favorite";
|
||||
String invisibleFavoriteCommand = "p1r-market-invisible-favorite";
|
||||
String missingUnfavoriteCommand = "p1r-market-missing-unfavorite";
|
||||
String invisibleUnfavoriteCommand = "p1r-market-invisible-unfavorite";
|
||||
mockMvc.perform(post("/app-api/muse/marketplace/assets/{assetId}/favorite", 999999L)
|
||||
.header("X-API-Version", "1")
|
||||
.header("X-Command-Id", missingFavoriteCommand))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(MARKET_ASSET_NOT_EXISTS.getCode()));
|
||||
mockMvc.perform(post("/app-api/muse/marketplace/assets/{assetId}/favorite", seedFacts.invisibleAssetId())
|
||||
.header("X-API-Version", "1")
|
||||
.header("X-Command-Id", invisibleFavoriteCommand))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(MARKET_ASSET_NOT_EXISTS.getCode()));
|
||||
mockMvc.perform(delete("/app-api/muse/marketplace/assets/{assetId}/favorite", 999999L)
|
||||
.header("X-API-Version", "1")
|
||||
.header("X-Command-Id", missingUnfavoriteCommand))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(MARKET_ASSET_NOT_EXISTS.getCode()));
|
||||
mockMvc.perform(delete("/app-api/muse/marketplace/assets/{assetId}/favorite", seedFacts.invisibleAssetId())
|
||||
.header("X-API-Version", "1")
|
||||
.header("X-Command-Id", invisibleUnfavoriteCommand))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(MARKET_ASSET_NOT_EXISTS.getCode()));
|
||||
|
||||
assertEquals(0, commandCount(missingFavoriteCommand));
|
||||
assertEquals(0, commandCount(invisibleFavoriteCommand));
|
||||
assertEquals(0, commandCount(missingUnfavoriteCommand));
|
||||
assertEquals(0, commandCount(invisibleUnfavoriteCommand));
|
||||
assertEquals(0, favoriteCount(seedFacts.invisibleAssetId()), "不可见资产不能被收藏或取消收藏写入 favorite fact");
|
||||
assertEquals("active", favoriteStatus(seedFacts.visibleAssetId()), "错误目标请求不能污染已存在的 visible favorite fact");
|
||||
}
|
||||
|
||||
private void setRuntimeContext() {
|
||||
TenantContextHolder.setTenantId(TENANT_ID);
|
||||
LoginUser loginUser = new LoginUser();
|
||||
loginUser.setId(LOGIN_USER_ID);
|
||||
loginUser.setUserType(UserTypeEnum.MEMBER.getValue());
|
||||
loginUser.setTenantId(TENANT_ID);
|
||||
loginUser.setVisitTenantId(TENANT_ID);
|
||||
SecurityFrameworkUtils.setLoginUser(loginUser, new MockHttpServletRequest());
|
||||
}
|
||||
|
||||
private void resetMarketTables() throws SQLException {
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
Statement statement = connection.createStatement()) {
|
||||
// 每个测试重置 Market 目标表,保证 HTTP 请求写出的 command/favorite fact 可被精确计数。
|
||||
statement.execute("""
|
||||
TRUNCATE TABLE
|
||||
muse_market_command,
|
||||
muse_market_favorite,
|
||||
muse_market_asset_version,
|
||||
muse_market_asset
|
||||
RESTART IDENTITY CASCADE
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
private SeedFacts seedMarketFacts() throws SQLException {
|
||||
try (Connection connection = dataSource.getConnection()) {
|
||||
Long visibleAssetId = insertAsset(connection, "P1R Market Listed Agent", "listed",
|
||||
PUBLISHER_USER_ID, "10", 30L, "[\"agent\",\"p1r\"]");
|
||||
Long visibleVersionId = insertVersion(connection, visibleAssetId, "1.0.0");
|
||||
updateCurrentVersion(connection, visibleAssetId, visibleVersionId);
|
||||
Long secondVisibleAssetId = insertAsset(connection, "P1R Market Listed Prompt", "listed",
|
||||
2002L, "20", 10L, "[\"prompt\"]");
|
||||
Long secondVisibleVersionId = insertVersion(connection, secondVisibleAssetId, "1.1.0");
|
||||
updateCurrentVersion(connection, secondVisibleAssetId, secondVisibleVersionId);
|
||||
Long invisibleAssetId = insertAsset(connection, "P1R Market Invisible Draft", "draft",
|
||||
3001L, "30", 1L, "[\"draft\"]");
|
||||
Long invisibleVersionId = insertVersion(connection, invisibleAssetId, "0.1.0");
|
||||
updateCurrentVersion(connection, invisibleAssetId, invisibleVersionId);
|
||||
return new SeedFacts(visibleAssetId, visibleVersionId, secondVisibleAssetId, invisibleAssetId);
|
||||
}
|
||||
}
|
||||
|
||||
private Long insertAsset(Connection connection, String name, String listingStatus, Long publisherId,
|
||||
String category, Long installCount, String tags) throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement("""
|
||||
INSERT INTO muse_market_asset(name, description, asset_type, category, source_id,
|
||||
publisher_id, listing_status, license_type, status,
|
||||
rating, install_count, tags, tenant_id)
|
||||
VALUES (?, ?, 'agent', ?, 7001, ?, ?, 'standard', ?, 4.8, ?, CAST(? AS jsonb), ?)
|
||||
RETURNING id
|
||||
""")) {
|
||||
statement.setString(1, name);
|
||||
statement.setString(2, name + " description");
|
||||
statement.setString(3, category);
|
||||
statement.setLong(4, publisherId);
|
||||
statement.setString(5, listingStatus);
|
||||
statement.setString(6, listingStatus);
|
||||
statement.setLong(7, installCount);
|
||||
statement.setString(8, tags);
|
||||
statement.setLong(9, TENANT_ID);
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
assertTrue(resultSet.next(), "seed Market asset 必须返回 id");
|
||||
return resultSet.getLong(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Long insertVersion(Connection connection, Long assetId, String version) throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement("""
|
||||
INSERT INTO muse_market_asset_version(asset_id, version, change_note, version_snapshot, status, tenant_id)
|
||||
VALUES (?, ?, 'P1R completed approval seed version', CAST(? AS jsonb), 'listed', ?)
|
||||
RETURNING id
|
||||
""")) {
|
||||
statement.setLong(1, assetId);
|
||||
statement.setString(2, version);
|
||||
statement.setString(3, "{\"source\":\"p1r-market-completed-approval\"}");
|
||||
statement.setLong(4, TENANT_ID);
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
assertTrue(resultSet.next(), "seed Market asset version 必须返回 id");
|
||||
return resultSet.getLong(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateCurrentVersion(Connection connection, Long assetId, Long versionId) throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement("""
|
||||
UPDATE muse_market_asset
|
||||
SET current_version_id = ?
|
||||
WHERE tenant_id = ?
|
||||
AND id = ?
|
||||
""")) {
|
||||
statement.setLong(1, versionId);
|
||||
statement.setLong(2, TENANT_ID);
|
||||
statement.setLong(3, assetId);
|
||||
assertEquals(1, statement.executeUpdate(), "seed Market asset 必须能设置 current_version_id");
|
||||
}
|
||||
}
|
||||
|
||||
private int commandCount() throws SQLException {
|
||||
return queryInt("SELECT COUNT(*) FROM muse_market_command WHERE tenant_id = ?", TENANT_ID);
|
||||
}
|
||||
|
||||
private int commandCount(String commandId) throws SQLException {
|
||||
return queryInt("SELECT COUNT(*) FROM muse_market_command WHERE tenant_id = ? AND command_id = ?",
|
||||
TENANT_ID, commandId);
|
||||
}
|
||||
|
||||
private int favoriteCount(Long assetId) throws SQLException {
|
||||
return queryInt("SELECT COUNT(*) FROM muse_market_favorite WHERE tenant_id = ? AND user_id = ? AND asset_id = ?",
|
||||
TENANT_ID, LOGIN_USER_ID, assetId);
|
||||
}
|
||||
|
||||
private String favoriteStatus(Long assetId) throws SQLException {
|
||||
return queryString("""
|
||||
SELECT status
|
||||
FROM muse_market_favorite
|
||||
WHERE tenant_id = ?
|
||||
AND user_id = ?
|
||||
AND asset_id = ?
|
||||
""", TENANT_ID, LOGIN_USER_ID, assetId);
|
||||
}
|
||||
|
||||
private String favoriteCommandId(Long assetId) throws SQLException {
|
||||
return queryString("""
|
||||
SELECT command_id
|
||||
FROM muse_market_favorite
|
||||
WHERE tenant_id = ?
|
||||
AND user_id = ?
|
||||
AND asset_id = ?
|
||||
""", TENANT_ID, LOGIN_USER_ID, assetId);
|
||||
}
|
||||
|
||||
private CommandFact commandFact(String commandId) throws SQLException {
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("""
|
||||
SELECT operation_id, status, actor_user_id, owner_user_id, target_id, result_snapshot::text
|
||||
FROM muse_market_command
|
||||
WHERE tenant_id = ?
|
||||
AND command_id = ?
|
||||
""")) {
|
||||
statement.setLong(1, TENANT_ID);
|
||||
statement.setString(2, commandId);
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
if (!resultSet.next()) {
|
||||
return null;
|
||||
}
|
||||
return new CommandFact(resultSet.getString(1), resultSet.getString(2), resultSet.getLong(3),
|
||||
resultSet.getLong(4), resultSet.getLong(5), resultSet.getString(6));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void assertCommandResultSnapshot(CommandFact command, boolean expectedFavorite) {
|
||||
Map<?, ?> snapshot = JsonUtils.parseObject(command.resultSnapshot(), Map.class);
|
||||
assertNotNull(snapshot, "Market command result_snapshot 必须是 JSON 对象");
|
||||
assertEquals(expectedFavorite, snapshot.get("isFavorite"),
|
||||
"Market command result_snapshot 必须记录 isFavorite=" + expectedFavorite);
|
||||
}
|
||||
|
||||
private int queryInt(String sql, Object... args) throws SQLException {
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
bind(statement, args);
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
assertTrue(resultSet.next(), "计数查询必须返回一行");
|
||||
return resultSet.getInt(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String queryString(String sql, Object... args) throws SQLException {
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
bind(statement, args);
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
assertTrue(resultSet.next(), "事实查询必须返回一行");
|
||||
return resultSet.getString(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void bind(PreparedStatement statement, Object... args) throws SQLException {
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
Object value = args[i];
|
||||
if (value instanceof Long longValue) {
|
||||
statement.setLong(i + 1, longValue);
|
||||
} else if (value instanceof Integer intValue) {
|
||||
statement.setInt(i + 1, intValue);
|
||||
} else {
|
||||
statement.setString(i + 1, String.valueOf(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static CompletedApprovalSettings settings() {
|
||||
if (cachedSettings == null) {
|
||||
cachedSettings = CompletedApprovalSettings.fromPropertiesAndEnvironment();
|
||||
}
|
||||
return cachedSettings;
|
||||
}
|
||||
|
||||
private static void cleanSchema(Flyway flyway, CompletedApprovalSettings settings) {
|
||||
try {
|
||||
flyway.clean();
|
||||
} catch (RuntimeException exception) {
|
||||
throw sanitizedFlywayFailure("Flyway clean 失败", settings, exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static MigrateResult migrateSchema(Flyway flyway, CompletedApprovalSettings settings) {
|
||||
try {
|
||||
return flyway.migrate();
|
||||
} catch (RuntimeException exception) {
|
||||
throw sanitizedFlywayFailure("Flyway migrate 失败", settings, exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static AssertionError sanitizedFlywayFailure(String action, CompletedApprovalSettings settings,
|
||||
RuntimeException exception) {
|
||||
String sanitizedMessage = Objects.toString(exception.getMessage(), "")
|
||||
.replace(settings.jdbcUrl(), maskedUrl(settings.jdbcUrl()))
|
||||
.replace("for user '" + settings.jdbcUser() + "'", "for user '<user-redacted>'");
|
||||
return new AssertionError(action + ": " + sanitizedMessage);
|
||||
}
|
||||
|
||||
private static String requiredProperty(String name) {
|
||||
String value = System.getProperty(name);
|
||||
assertTrue(value != null && !value.isBlank(), "缺少必需系统属性: " + name);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static String requiredPasswordEnvironment() {
|
||||
String password = firstNonBlankEnvironment("P1R_MARKET_COMPLETED_PASSWORD",
|
||||
"P1R_FLYWAY_PASSWORD", "MUSE_POSTGRES_PASSWORD");
|
||||
assertTrue(password != null,
|
||||
"缺少必需环境变量: P1R_MARKET_COMPLETED_PASSWORD、P1R_FLYWAY_PASSWORD 或 MUSE_POSTGRES_PASSWORD");
|
||||
return password;
|
||||
}
|
||||
|
||||
private static String firstNonBlankEnvironment(String... names) {
|
||||
// 数据库密码只能来自环境变量,避免 Surefire XML 或 JVM 参数泄露。
|
||||
for (String name : names) {
|
||||
String value = System.getenv(name);
|
||||
if (value != null && !value.isBlank()) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void assertNoPasswordSystemProperties() {
|
||||
Properties properties = System.getProperties();
|
||||
List<String> passwordProperties = properties.stringPropertyNames().stream()
|
||||
.filter(P1rMarketDiscoveryFavoriteCompletedApprovalIT::isForbiddenPasswordSystemProperty)
|
||||
.sorted()
|
||||
.toList();
|
||||
assertTrue(passwordProperties.isEmpty(),
|
||||
"数据库密码不能通过 JVM system property 传入: " + passwordProperties);
|
||||
}
|
||||
|
||||
private static boolean isForbiddenPasswordSystemProperty(String name) {
|
||||
String normalized = name.toLowerCase(Locale.ROOT);
|
||||
return normalized.contains("password")
|
||||
&& (normalized.startsWith("p1r.")
|
||||
|| normalized.startsWith("p1r_")
|
||||
|| normalized.contains(".flyway.")
|
||||
|| normalized.contains(".market.")
|
||||
|| normalized.contains(".datasource."));
|
||||
}
|
||||
|
||||
private static void assertNoCredentialQuery(String url) {
|
||||
int queryStart = url.indexOf('?');
|
||||
if (queryStart < 0) {
|
||||
return;
|
||||
}
|
||||
String query = url.substring(queryStart + 1);
|
||||
for (String parameter : query.split("&")) {
|
||||
String key = parameter;
|
||||
int equalsStart = key.indexOf('=');
|
||||
if (equalsStart >= 0) {
|
||||
key = key.substring(0, equalsStart);
|
||||
}
|
||||
assertFalse(isCredentialQueryKey(key),
|
||||
"p1r.flyway.url 不能携带凭据 query 参数;请通过用户名属性和密码环境变量传入");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isCredentialQueryKey(String rawKey) {
|
||||
String key = rawKey.trim().toLowerCase(Locale.ROOT).replace('-', '_');
|
||||
return CREDENTIAL_QUERY_KEYS.contains(key)
|
||||
|| key.endsWith("_token")
|
||||
|| key.endsWith("_secret")
|
||||
|| key.endsWith("_password");
|
||||
}
|
||||
|
||||
private static void assertTestDatabaseUrl(String url) {
|
||||
String databaseName = jdbcDatabaseName(url);
|
||||
assertTrue(databaseName.endsWith("_test"),
|
||||
"p1r.flyway.url 必须指向 _test 后缀隔离库,避免清理非测试库: " + maskedUrl(url));
|
||||
}
|
||||
|
||||
private static String resolveMuseSqlLocation(String requestedLocations) {
|
||||
assertEquals("filesystem:sql/muse", requestedLocations,
|
||||
"P1R Market completed approval IT 要求显式使用 filesystem:sql/muse");
|
||||
Path current = Path.of(System.getProperty("user.dir")).toAbsolutePath();
|
||||
String relativeLocation = requestedLocations.substring("filesystem:".length());
|
||||
for (Path cursor = current; cursor != null; cursor = cursor.getParent()) {
|
||||
Path candidate = cursor.resolve(relativeLocation);
|
||||
if (Files.isDirectory(candidate)) {
|
||||
return "filesystem:" + candidate;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("无法从当前目录向上找到 sql/muse: " + current);
|
||||
}
|
||||
|
||||
private static String jdbcDatabaseName(String url) {
|
||||
String urlWithoutQuery = jdbcUrlWithoutQuery(url);
|
||||
int databaseStart = urlWithoutQuery.lastIndexOf('/');
|
||||
assertTrue(databaseStart >= 0 && databaseStart < urlWithoutQuery.length() - 1,
|
||||
"p1r.flyway.url 必须包含真实数据库名: " + maskedUrl(url));
|
||||
return urlWithoutQuery.substring(databaseStart + 1);
|
||||
}
|
||||
|
||||
private static String jdbcUrlWithoutQuery(String url) {
|
||||
int queryStart = url.indexOf('?');
|
||||
return queryStart < 0 ? url : url.substring(0, queryStart);
|
||||
}
|
||||
|
||||
private static String maskedUrl(String url) {
|
||||
String urlWithoutQuery = jdbcUrlWithoutQuery(url);
|
||||
int databaseStart = urlWithoutQuery.lastIndexOf('/');
|
||||
if (databaseStart < 0) {
|
||||
return maskJdbcHost(urlWithoutQuery) + maskedQuerySuffix(url);
|
||||
}
|
||||
String prefix = urlWithoutQuery.substring(0, databaseStart + 1);
|
||||
String database = urlWithoutQuery.substring(databaseStart + 1);
|
||||
return maskJdbcHost(prefix) + database + maskedQuerySuffix(url);
|
||||
}
|
||||
|
||||
private static String maskedQuerySuffix(String url) {
|
||||
return url.indexOf('?') < 0 ? "" : "?<query-redacted>";
|
||||
}
|
||||
|
||||
private static String maskJdbcHost(String urlPart) {
|
||||
return urlPart.replaceAll("//([^:/?#]+)", "//<host>");
|
||||
}
|
||||
|
||||
private static void redactFlywaySystemProperties(String url, String user) {
|
||||
captureOriginalFlywaySystemProperties();
|
||||
System.setProperty("p1r.flyway.url", maskedUrl(url));
|
||||
System.setProperty("p1r.flyway.user", user == null || user.isBlank() ? "<user-redacted>" : "<user-redacted>");
|
||||
}
|
||||
|
||||
private static void captureOriginalFlywaySystemProperties() {
|
||||
if (originalFlywayPropertiesCaptured) {
|
||||
return;
|
||||
}
|
||||
// 该 IT 会为日志脱敏 p1r.flyway.*;先保存原值,避免同一 Surefire JVM 中污染后续 Flyway IT。
|
||||
originalFlywayUrlSystemProperty = System.getProperty("p1r.flyway.url");
|
||||
originalFlywayUserSystemProperty = System.getProperty("p1r.flyway.user");
|
||||
originalFlywayPropertiesCaptured = true;
|
||||
}
|
||||
|
||||
private static void restoreOriginalFlywaySystemProperties() {
|
||||
if (!originalFlywayPropertiesCaptured) {
|
||||
return;
|
||||
}
|
||||
// 恢复用户传入的原始连接属性,让组合运行的 Flyway 验收测试继续读取真实 _test 库地址。
|
||||
restoreSystemProperty("p1r.flyway.url", originalFlywayUrlSystemProperty);
|
||||
restoreSystemProperty("p1r.flyway.user", originalFlywayUserSystemProperty);
|
||||
}
|
||||
|
||||
private static void restoreSystemProperty(String name, String value) {
|
||||
if (value == null) {
|
||||
System.clearProperty(name);
|
||||
return;
|
||||
}
|
||||
System.setProperty(name, value);
|
||||
}
|
||||
|
||||
private static void silenceFlywayInfoLogs() {
|
||||
try {
|
||||
Object flywayLogger = LoggerFactory.getLogger("org.flywaydb");
|
||||
Class<?> levelClass = Class.forName("ch.qos.logback.classic.Level");
|
||||
Object warnLevel = levelClass.getField("WARN").get(null);
|
||||
flywayLogger.getClass().getMethod("setLevel", levelClass).invoke(flywayLogger, warnLevel);
|
||||
} catch (ReflectiveOperationException | LinkageError ignored) {
|
||||
// 日志实现不是 logback 时不影响迁移验收;测试自身仍只输出脱敏 URL。
|
||||
}
|
||||
}
|
||||
|
||||
private record SeedFacts(Long visibleAssetId,
|
||||
Long visibleVersionId,
|
||||
Long secondVisibleAssetId,
|
||||
Long invisibleAssetId) {
|
||||
}
|
||||
|
||||
private record CommandFact(String operationId,
|
||||
String status,
|
||||
Long actorUserId,
|
||||
Long ownerUserId,
|
||||
Long targetId,
|
||||
String resultSnapshot) {
|
||||
}
|
||||
|
||||
private record CompletedApprovalSettings(String jdbcUrl,
|
||||
String jdbcUser,
|
||||
String jdbcPassword,
|
||||
String flywayLocations) {
|
||||
|
||||
static CompletedApprovalSettings fromPropertiesAndEnvironment() {
|
||||
assertNoPasswordSystemProperties();
|
||||
String url = requiredProperty("p1r.flyway.url");
|
||||
String user = requiredProperty("p1r.flyway.user");
|
||||
String password = requiredPasswordEnvironment();
|
||||
String locations = requiredProperty("p1r.flyway.locations");
|
||||
assertNoCredentialQuery(url);
|
||||
assertTestDatabaseUrl(url);
|
||||
return new CompletedApprovalSettings(url, user, password, locations);
|
||||
}
|
||||
}
|
||||
|
||||
@SpringBootConfiguration
|
||||
@ImportAutoConfiguration({
|
||||
JacksonAutoConfiguration.class,
|
||||
HttpMessageConvertersAutoConfiguration.class,
|
||||
DataSourceAutoConfiguration.class,
|
||||
DataSourceTransactionManagerAutoConfiguration.class,
|
||||
JdbcTemplateAutoConfiguration.class,
|
||||
TransactionAutoConfiguration.class,
|
||||
RestTemplateAutoConfiguration.class,
|
||||
WebMvcAutoConfiguration.class,
|
||||
MuseDataSourceAutoConfiguration.class,
|
||||
MuseMybatisAutoConfiguration.class,
|
||||
MybatisPlusAutoConfiguration.class,
|
||||
MybatisPlusJoinAutoConfiguration.class,
|
||||
MuseWebAutoConfiguration.class
|
||||
})
|
||||
@Import({
|
||||
AppMuseMarketplaceAssetController.class,
|
||||
MarketAssetQueryServiceImpl.class,
|
||||
MarketFavoriteServiceImpl.class,
|
||||
MarketCommandServiceImpl.class,
|
||||
SpringUtil.class
|
||||
})
|
||||
static class CompletedApprovalConfiguration {
|
||||
|
||||
@Bean
|
||||
ApiErrorLogCommonApi apiErrorLogCommonApi() {
|
||||
return new ApiErrorLogCommonApi() {
|
||||
@Override
|
||||
public CommonResult<Boolean> createApiErrorLog(ApiErrorLogCreateReqDTO createDTO) {
|
||||
return CommonResult.success(true);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ package cn.iocoder.muse.server.framework.api;
|
||||
import org.flywaydb.core.Flyway;
|
||||
import org.flywaydb.core.api.MigrationInfo;
|
||||
import org.flywaydb.core.api.output.MigrateResult;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -50,6 +51,7 @@ class P1rMarketFlywayMigrationIT {
|
||||
"muse_market_publish_request",
|
||||
"muse_market_appeal",
|
||||
"muse_market_handoff",
|
||||
"muse_market_favorite",
|
||||
"muse_market_purchase",
|
||||
"muse_market_command",
|
||||
"muse_market_authorization_snapshot",
|
||||
@ -72,6 +74,8 @@ class P1rMarketFlywayMigrationIT {
|
||||
"uk_muse_market_asset_command",
|
||||
"idx_muse_market_install_user",
|
||||
"uk_muse_market_install_command",
|
||||
"uk_muse_market_favorite_command",
|
||||
"idx_muse_market_favorite_user",
|
||||
"uk_muse_market_purchase_command",
|
||||
"idx_muse_market_purchase_user",
|
||||
"idx_muse_market_purchase_asset",
|
||||
@ -118,6 +122,7 @@ class P1rMarketFlywayMigrationIT {
|
||||
|
||||
private static final List<String> REQUIRED_CONSTRAINTS = List.of(
|
||||
"uk_muse_market_install",
|
||||
"uk_muse_market_favorite_user_asset",
|
||||
"uk_muse_market_purchase_command",
|
||||
"uk_muse_market_command",
|
||||
"uk_muse_market_auth_snapshot",
|
||||
@ -141,6 +146,7 @@ class P1rMarketFlywayMigrationIT {
|
||||
|
||||
private static final List<String> REQUIRED_V15_TRIGGERS = List.of(
|
||||
"trg_muse_market_command_updated_at",
|
||||
"trg_muse_market_favorite_updated_at",
|
||||
"trg_muse_market_auth_snapshot_updated_at",
|
||||
"trg_muse_market_auth_summary_updated_at",
|
||||
"trg_muse_market_handoff_event_updated_at",
|
||||
@ -172,6 +178,16 @@ class P1rMarketFlywayMigrationIT {
|
||||
"access_token",
|
||||
"refresh_token"
|
||||
);
|
||||
private static volatile String rememberedFlywayUrl;
|
||||
private static volatile String rememberedFlywayUser;
|
||||
private static volatile boolean originalFlywayPropertiesCaptured;
|
||||
private static volatile String originalFlywayUrlSystemProperty;
|
||||
private static volatile String originalFlywayUserSystemProperty;
|
||||
|
||||
@AfterAll
|
||||
static void restoreFlywaySystemProperties() {
|
||||
restoreOriginalFlywaySystemProperties();
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_migrateV1ToV15OnRealPostgresqlAndVerifyMarketSchema() throws Exception {
|
||||
@ -259,8 +275,76 @@ class P1rMarketFlywayMigrationIT {
|
||||
"合法 _test 库携带 query 时必须保留真实 database name,并整体脱敏 query");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_verifyMarketFavoriteSchemaAndRejectDuplicateFacts() throws Exception {
|
||||
String url = requiredProperty("p1r.flyway.url");
|
||||
String user = requiredProperty("p1r.flyway.user");
|
||||
String password = requiredPasswordEnvironment();
|
||||
String requestedLocations = requiredProperty("p1r.flyway.locations");
|
||||
redactFlywaySystemProperties(url, user);
|
||||
assertEquals("filesystem:sql/muse", requestedLocations,
|
||||
"P1R Market Flyway 收藏表验收要求显式使用 filesystem:sql/muse");
|
||||
assertNoCredentialQuery(url);
|
||||
assertTestDatabaseUrl(url);
|
||||
silenceFlywayInfoLogs();
|
||||
Flyway flyway = Flyway.configure()
|
||||
.dataSource(url, user, password)
|
||||
.locations(resolveMuseSqlLocation(requestedLocations))
|
||||
.schemas("public")
|
||||
.defaultSchema("public")
|
||||
.target(TARGET_VERSION)
|
||||
.cleanDisabled(false)
|
||||
.load();
|
||||
cleanSchema(flyway, url, user);
|
||||
migrateSchema(flyway, url, user);
|
||||
|
||||
try (Connection connection = DriverManager.getConnection(url, user, password)) {
|
||||
assertColumnsExist(connection, "muse_market_favorite",
|
||||
"asset_id", "user_id", "status", "command_id", "tenant_id", "update_time");
|
||||
assertEquals(List.of("tenant_id", "user_id", "asset_id"),
|
||||
constraintColumns(connection, "uk_muse_market_favorite_user_asset"),
|
||||
"收藏事实唯一约束必须按 tenant/user/asset 分层,不能重复插入同一收藏事实");
|
||||
assertEquals(List.of("tenant_id", "user_id", "status"),
|
||||
indexColumns(connection, "idx_muse_market_favorite_user"),
|
||||
"收藏列表查询索引必须覆盖 tenant/user/status");
|
||||
assertPartialCommandIndex(connection, "uk_muse_market_favorite_command");
|
||||
assertTrue(exists(connection,
|
||||
"""
|
||||
SELECT 1
|
||||
FROM information_schema.triggers
|
||||
WHERE trigger_schema = 'public'
|
||||
AND trigger_name = ?
|
||||
""",
|
||||
"trg_muse_market_favorite_updated_at"),
|
||||
"收藏状态更新必须有 update_time 触发器");
|
||||
|
||||
resetFavoriteProbeRows(connection);
|
||||
insertFavoriteProbe(connection, "p1r-market-favorite-schema-cmd-1");
|
||||
SQLException duplicateFact = assertThrows(SQLException.class,
|
||||
() -> insertFavoriteProbe(connection, "p1r-market-favorite-schema-cmd-2"));
|
||||
assertEquals("23505", duplicateFact.getSQLState(),
|
||||
"同一 tenant/user/asset 的重复收藏事实必须由 PostgreSQL 唯一约束拒绝");
|
||||
SQLException duplicateCommand = assertThrows(SQLException.class,
|
||||
() -> insertFavoriteProbe(connection, 990004L, 990005L, "p1r-market-favorite-schema-cmd-1"));
|
||||
assertEquals("23505", duplicateCommand.getSQLState(),
|
||||
"同一 tenant/command_id 的重复收藏命令必须由 PostgreSQL partial unique index 拒绝");
|
||||
assertFavoriteUpdateTriggerExecutes(connection);
|
||||
}
|
||||
}
|
||||
|
||||
private static String requiredProperty(String name) {
|
||||
String value = System.getProperty(name);
|
||||
if ("p1r.flyway.url".equals(name)) {
|
||||
if (value != null && !value.contains("<host>")) {
|
||||
rememberedFlywayUrl = value;
|
||||
}
|
||||
value = value != null && value.contains("<host>") ? rememberedFlywayUrl : value;
|
||||
} else if ("p1r.flyway.user".equals(name)) {
|
||||
if (value != null && !value.contains("<user-redacted>")) {
|
||||
rememberedFlywayUser = value;
|
||||
}
|
||||
value = value != null && value.contains("<user-redacted>") ? rememberedFlywayUser : value;
|
||||
}
|
||||
assertTrue(value != null && !value.isBlank(), "缺少必需系统属性: " + name);
|
||||
return value;
|
||||
}
|
||||
@ -307,10 +391,36 @@ class P1rMarketFlywayMigrationIT {
|
||||
|
||||
private static void redactFlywaySystemProperties(String url, String user) {
|
||||
// WHY:Surefire XML 会记录 JVM system property;读取后立即脱敏,避免报告文件残留真实连接信息。
|
||||
captureOriginalFlywaySystemProperties();
|
||||
System.setProperty("p1r.flyway.url", maskedUrl(url));
|
||||
System.setProperty("p1r.flyway.user", maskUser(user));
|
||||
}
|
||||
|
||||
private static void captureOriginalFlywaySystemProperties() {
|
||||
if (originalFlywayPropertiesCaptured) {
|
||||
return;
|
||||
}
|
||||
originalFlywayUrlSystemProperty = System.getProperty("p1r.flyway.url");
|
||||
originalFlywayUserSystemProperty = System.getProperty("p1r.flyway.user");
|
||||
originalFlywayPropertiesCaptured = true;
|
||||
}
|
||||
|
||||
private static void restoreOriginalFlywaySystemProperties() {
|
||||
if (!originalFlywayPropertiesCaptured) {
|
||||
return;
|
||||
}
|
||||
restoreSystemProperty("p1r.flyway.url", originalFlywayUrlSystemProperty);
|
||||
restoreSystemProperty("p1r.flyway.user", originalFlywayUserSystemProperty);
|
||||
}
|
||||
|
||||
private static void restoreSystemProperty(String name, String value) {
|
||||
if (value == null) {
|
||||
System.clearProperty(name);
|
||||
return;
|
||||
}
|
||||
System.setProperty(name, value);
|
||||
}
|
||||
|
||||
private static void cleanSchema(Flyway flyway, String url, String user) {
|
||||
try {
|
||||
flyway.clean();
|
||||
@ -432,6 +542,13 @@ class P1rMarketFlywayMigrationIT {
|
||||
constraintColumns(connection, "uk_muse_market_install"),
|
||||
"安装幂等唯一约束必须按 tenant/user/asset/version 分层");
|
||||
|
||||
assertColumnsExist(connection, "muse_market_favorite",
|
||||
"asset_id", "user_id", "status", "command_id", "tenant_id");
|
||||
assertEquals(List.of("tenant_id", "user_id", "asset_id"),
|
||||
constraintColumns(connection, "uk_muse_market_favorite_user_asset"),
|
||||
"收藏事实唯一约束必须按 tenant/user/asset 分层");
|
||||
assertPartialCommandIndex(connection, "uk_muse_market_favorite_command");
|
||||
|
||||
assertColumnsExist(connection, "muse_market_purchase",
|
||||
"asset_id", "asset_version_id", "user_id", "status", "purchase_payload", "command_id");
|
||||
assertTrue(columnIsNotNullable(connection, "muse_market_purchase", "command_id"),
|
||||
@ -584,6 +701,52 @@ class P1rMarketFlywayMigrationIT {
|
||||
}
|
||||
}
|
||||
|
||||
private static void resetFavoriteProbeRows(Connection connection) throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement("""
|
||||
DELETE FROM muse_market_favorite
|
||||
WHERE tenant_id = 990001
|
||||
""")) {
|
||||
statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private static void insertFavoriteProbe(Connection connection, String commandId) throws SQLException {
|
||||
insertFavoriteProbe(connection, 990003L, 990002L, commandId);
|
||||
}
|
||||
|
||||
private static void insertFavoriteProbe(Connection connection, Long assetId, Long userId,
|
||||
String commandId) throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement("""
|
||||
INSERT INTO muse_market_favorite(asset_id, user_id, status, command_id, tenant_id)
|
||||
VALUES (?, ?, 'active', ?, 990001)
|
||||
""")) {
|
||||
statement.setLong(1, assetId);
|
||||
statement.setLong(2, userId);
|
||||
statement.setString(3, commandId);
|
||||
statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertFavoriteUpdateTriggerExecutes(Connection connection) throws SQLException {
|
||||
insertFavoriteProbe(connection, 990006L, 990007L, "p1r-market-favorite-schema-cmd-3");
|
||||
try (PreparedStatement statement = connection.prepareStatement("""
|
||||
UPDATE muse_market_favorite
|
||||
SET status = 'inactive',
|
||||
update_time = TIMESTAMP '2000-01-01 00:00:00'
|
||||
WHERE tenant_id = 990001
|
||||
AND user_id = 990007
|
||||
AND asset_id = 990006
|
||||
RETURNING status, update_time > TIMESTAMP '2000-01-02 00:00:00'
|
||||
""")) {
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
assertTrue(resultSet.next(), "收藏状态更新必须返回被 trigger 处理后的行");
|
||||
assertEquals("inactive", resultSet.getString(1), "收藏状态必须能更新为 inactive");
|
||||
assertTrue(resultSet.getBoolean(2),
|
||||
"收藏状态更新时 trigger 必须覆盖手工写入的旧 update_time");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean columnExists(Connection connection, String tableName, String columnName) throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement("""
|
||||
SELECT 1
|
||||
|
||||
@ -19,8 +19,8 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
/**
|
||||
* P1R-6 Task 11 Market 真实 API 覆盖门禁。
|
||||
*
|
||||
* <p>该测试只证明 Market 32 个 operation 已退出合同兜底并进入 dedicated / needs_verification;
|
||||
* 它不证明 Market 已经完成跨 owner 端到端验收,也不允许把 Market 提前标记为 completed。</p>
|
||||
* <p>该测试证明 Market 第一批 4 个 operation 只以 operation-level 方式推进到 completed;
|
||||
* 它不证明 Market domain-level completed,也不允许把剩余 28 个 operation 提前标记为 completed。</p>
|
||||
*/
|
||||
class P1rMarketRealApiGateTest {
|
||||
|
||||
@ -71,6 +71,13 @@ class P1rMarketRealApiGateTest {
|
||||
entry("adminResolveAppeal", "POST /admin-api/muse/market/appeals/{appealId}/resolve")
|
||||
);
|
||||
|
||||
private static final Set<String> APPROVED_MARKET_COMPLETED_OPERATIONS = Set.of(
|
||||
"getMarketplaceAsset",
|
||||
"listMarketplaceCategories",
|
||||
"favoriteAsset",
|
||||
"unfavoriteAsset"
|
||||
);
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
@ -97,22 +104,37 @@ class P1rMarketRealApiGateTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_keep_all_market_operations_dedicated_and_needs_verification() throws IOException {
|
||||
void should_promote_only_first_batch_market_operations_to_completed() throws IOException {
|
||||
int completed = 0;
|
||||
int needsVerification = 0;
|
||||
for (JsonNode operation : readMarketOperations()) {
|
||||
String operationId = operation.path("operationId").asText("<missing-operationId>");
|
||||
assertEquals("dedicated", operation.path("implementationStatus").asText(),
|
||||
operationId + " 当前 P1R-6 目标必须是 dedicated");
|
||||
if (APPROVED_MARKET_COMPLETED_OPERATIONS.contains(operationId)) {
|
||||
completed++;
|
||||
assertEquals("completed", operation.path("completionStatus").asText(),
|
||||
operationId + " 已获用户批准后必须标记 completed");
|
||||
continue;
|
||||
}
|
||||
needsVerification++;
|
||||
assertEquals("needs_verification", operation.path("completionStatus").asText(),
|
||||
operationId + " 当前 P1R-6 目标必须保持 needs_verification,不能提前宣称 completed");
|
||||
operationId + " 未进入 Market 第一批审批,必须继续保持 needs_verification");
|
||||
}
|
||||
assertEquals(4, completed, "Market 第一批 operation-level completed approval 只能推进 4 个 operation");
|
||||
assertEquals(28, needsVerification, "Market 剩余 28 个 operation 必须继续 needs_verification");
|
||||
assertMarketOperationStatus("listMarketplaceAssets", "needs_verification", false);
|
||||
assertMarketOperationStatus("listMarketplaceRecommendations", "needs_verification", false);
|
||||
assertMarketOperationStatus("favoriteAsset", "completed", true);
|
||||
assertMarketOperationStatus("unfavoriteAsset", "completed", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_keep_completion_summary_owned_by_approved_scopes_only() throws IOException {
|
||||
JsonNode report = readReport();
|
||||
|
||||
assertEquals(127, report.path("summary").path("completedOperations").asInt(),
|
||||
"completedOperations 必须仍只来自 AI 41 + Knowledge 59 + Events streamEvents 1 + Meta 16 + Account 10");
|
||||
assertEquals(131, report.path("summary").path("completedOperations").asInt(),
|
||||
"completedOperations 必须只来自 AI 41 + Knowledge 59 + Events streamEvents 1 + Meta 16 + Account 10 + Market 4");
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -206,6 +228,21 @@ class P1rMarketRealApiGateTest {
|
||||
"Meta operation-level approval 后不应再保留 FunctionChain / ProtectionNode needs_verification");
|
||||
}
|
||||
|
||||
private void assertMarketOperationStatus(String operationId, String expectedCompletionStatus,
|
||||
boolean expectedRequiresCommandId) throws IOException {
|
||||
for (JsonNode operation : readMarketOperations()) {
|
||||
if (!operationId.equals(operation.path("operationId").asText())) {
|
||||
continue;
|
||||
}
|
||||
assertEquals(expectedCompletionStatus, operation.path("completionStatus").asText(),
|
||||
"market/" + operationId + " completionStatus 不符合本轮审批边界");
|
||||
assertEquals(expectedRequiresCommandId, operation.path("requiresCommandId").asBoolean(),
|
||||
"market/" + operationId + " requiresCommandId 不符合 OpenAPI 合同");
|
||||
return;
|
||||
}
|
||||
throw new AssertionError("market/" + operationId + " 必须存在于 coverage report");
|
||||
}
|
||||
|
||||
private JsonNode readMarketOperations() throws IOException {
|
||||
ArrayNode marketOperations = objectMapper.createArrayNode();
|
||||
for (JsonNode operation : readOperations()) {
|
||||
|
||||
@ -74,6 +74,11 @@ APPROVED_COMPLETED_OPERATIONS = {
|
||||
"account:getAppBalanceSnapshots",
|
||||
"account:adminCreateQuotaAdjustment",
|
||||
"account:adminListQuotaAdjustments",
|
||||
# P1R Market completed approval 第一批仍使用 operation-level 清单,避免整域 Market 被误升 completed。
|
||||
"market:getMarketplaceAsset",
|
||||
"market:listMarketplaceCategories",
|
||||
"market:favoriteAsset",
|
||||
"market:unfavoriteAsset",
|
||||
}
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user