From 8c4d05720248c86b6a28e35498c06bf542d5345d Mon Sep 17 00:00:00 2001 From: renzhiye Date: Fri, 3 Apr 2026 14:07:16 +0800 Subject: [PATCH] =?UTF-8?q?test(api):=20=E8=A1=A5=E9=BD=90=E5=A4=A9?= =?UTF-8?q?=E7=8C=AB=E6=81=A2=E5=A4=8D=E5=AE=A1=E8=AE=A1=E4=B8=8E=E6=8C=87?= =?UTF-8?q?=E6=A0=87=E5=9B=9E=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO.md | 6 +- .../api/src/server.dual-platform-live.test.ts | 109 ++++++++++++++++++ 2 files changed, 112 insertions(+), 3 deletions(-) diff --git a/TODO.md b/TODO.md index 45d7809..d08113e 100644 --- a/TODO.md +++ b/TODO.md @@ -73,7 +73,7 @@ - [ ] `S3-03` 阻塞恢复与 `L3 Browser Recovery` 落地(进行中) - [ ] `S3-04` 双平台候选确认与执行控制台落地(进行中:页面与状态展示已具备,真实并发执行待补) - [x] `S3-05` `PartialCompleted`、`Blocked`、`Failed` 汇总规则落地 -- [ ] `S3-06` 双平台主回归包落地(进行中:已新增 JD + 天猫 live 搜索、确认、执行、报告的主链 API 回归,并覆盖 `tmall SearchBlocked + jd Completed`、`tmall NoResult + jd Completed`、`tmall Blocked + jd Completed`、`tmall Blocked -> retry success -> report v2`、`tmall Blocked -> retry blocked -> report unchanged`,以及 `tmall SearchBlocked` 恢复后二次确认仅补跑新恢复平台的回归,待继续补更多失败/恢复组合) +- [ ] `S3-06` 双平台主回归包落地(进行中:已新增 JD + 天猫 live 搜索、确认、执行、报告的主链 API 回归,并覆盖 `tmall SearchBlocked + jd Completed`、`tmall NoResult + jd Completed`、`tmall Blocked + jd Completed`、`tmall Blocked -> retry success -> report v2`、`tmall Blocked -> retry blocked -> report unchanged`、`tmall SearchBlocked -> retry success -> audit/metrics`,以及 `tmall SearchBlocked` 恢复后二次确认仅补跑新恢复平台的回归,待继续补更多失败/恢复组合) ## `S4` @@ -86,7 +86,7 @@ ## `S5` -- [ ] `S5-01` 平台级定向重试稳定化(进行中) +- [ ] `S5-01` 平台级定向重试稳定化(进行中:已补天猫 `SearchBlocked` 恢复后的审计与 `retryCount/recoveryCount` 回归,待继续扩展更多失败来源与版本差异检测) - [ ] `S5-02` 性能与成本优化(未开始) - [ ] `S5-03` UAT 与试运行任务集执行(未开始) - [ ] `S5-04` 部署、值守、排障与热修手册落地(未开始) @@ -96,6 +96,6 @@ - [ ] `X-01` 上下游文档变更同步(进行中) - [ ] `X-02` 安全与合规检查(未开始) -- [ ] `X-03` 测试资产维护(进行中:已补天猫搜索解析/服务回归,以及双平台 live 主链、`SearchBlocked`、`NoResult`、`Blocked`、`Blocked -> retry success`、`Blocked -> retry blocked -> report unchanged` 与 `SearchBlocked` 恢复后二次确认不重复执行已完成平台的回归,真实 fixture/HAR 待补) +- [ ] `X-03` 测试资产维护(进行中:已补天猫搜索解析/服务回归,以及双平台 live 主链、`SearchBlocked`、`NoResult`、`Blocked`、`Blocked -> retry success`、`Blocked -> retry blocked -> report unchanged`、`SearchBlocked -> retry success -> audit/metrics` 与 `SearchBlocked` 恢复后二次确认不重复执行已完成平台的回归,真实 fixture/HAR 待补) - [ ] `X-04` 设计一致性与可访问性检查(进行中) - [ ] `X-05` 观测指标复盘(未开始) diff --git a/apps/api/src/server.dual-platform-live.test.ts b/apps/api/src/server.dual-platform-live.test.ts index 521d7ff..93eee34 100644 --- a/apps/api/src/server.dual-platform-live.test.ts +++ b/apps/api/src/server.dual-platform-live.test.ts @@ -903,6 +903,115 @@ describe("Dual-platform live loop", () => { await app.close(); }); + it("records recovery audit entries and retry metrics when a SearchBlocked Tmall platform recovers", async () => { + let allowTmallSearch = false; + const tmallLiveService = createTmallLiveServiceStub({ + async previewSearch(query) { + if (!allowTmallSearch) { + const error = new Error("Tmall search session appears invalid.") as Error & { + statusCode: number; + }; + error.statusCode = 409; + throw error; + } + + return { + query, + source: "html", + candidateCount: 1, + candidates: [ + { + candidateId: "tmall-934454505228", + platform: "tmall", + title: "Apple iPhone 15", + price: 4399, + priceLabel: "CNY 4399", + storeName: "Apple 官方旗舰店", + productUrl: "https://detail.tmall.com/item.htm?id=934454505228", + imageUrl: "https://img.alicdn.com/example.jpg", + salesHint: "已售 70万+", + specLabel: "128GB", + highlights: ["天猫", "官方旗舰店"] + } + ] + }; + } + }); + const app = createServer({ + jdLiveService: createJdLiveServiceStub(), + tmallLiveService + }); + await app.ready(); + await importDualLiveSessions(app); + + const createdTask = await createTask(app, "iPhone 15"); + expect( + createdTask.platformRuns.find((run: { platform: string }) => run.platform === "tmall") + ).toMatchObject({ + platform: "tmall", + status: "SearchBlocked" + }); + + allowTmallSearch = true; + + const retryResponse = await app.inject({ + method: "POST", + url: `/api/tasks/${createdTask.taskId}/platforms/tmall/retry` + }); + + expect(retryResponse.statusCode).toBe(200); + expect( + retryResponse + .json() + .task.platformRuns.find((run: { platform: string }) => run.platform === "tmall") + ).toMatchObject({ + platform: "tmall", + status: "AwaitingSelection" + }); + + const auditResponse = await app.inject({ + method: "GET", + url: `/api/tasks/${createdTask.taskId}/audit` + }); + + expect(auditResponse.statusCode).toBe(200); + expect(auditResponse.json().audit).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + action: "platform.retry_started", + platform: "tmall" + }), + expect.objectContaining({ + action: "task.recovery_completed", + platform: "tmall" + }), + expect.objectContaining({ + action: "platform.retry_completed", + platform: "tmall" + }) + ]) + ); + + const overviewResponse = await app.inject({ + method: "GET", + url: "/api/observability/overview" + }); + + expect(overviewResponse.statusCode).toBe(200); + expect( + overviewResponse + .json() + .overview.platformRuns.find((metric: { platform: string }) => metric.platform === "tmall") + ).toMatchObject({ + platform: "tmall", + retryCount: 1, + recoveryCount: 1 + }); + expect(overviewResponse.json().overview.audits.recoveryActions).toBe(3); + + await app.close(); + }); + it("keeps the current report version when a blocked Tmall retry does not recover", async () => { const tmallLiveService = createTmallLiveServiceStub({ async previewProduct() {