From a0e0be652259dd4b0ef2a8a2678d1879d36523a2 Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Tue, 18 Aug 2026 00:38:03 +0900 Subject: [PATCH] test: cover dashboard needsValidation and detail() nextAction regression risk Round 3 of the TechLog contract task added two pieces of genuinely new logic with no assertion on their output: the totals.needsValidation count in getDashboard(), and the detail() rewrite that avoids a self-reference when computing WorkingCopyDetail.nextAction. Both would have passed every existing test if they regressed. - needsValidation is checked against an independent count derived from listDocuments()'s per-document nextAction, with sanity bounds so a filter that always returns 0 or the full count can't pass silently. - detail()'s nextAction is asserted on two fixtures whose expected value is justified by the asserted preconditions alongside it (published-at-current-version -> NONE; INVALID-but-current-validation -> FIX_VALIDATION). Verified locally that hardcoding nextAction to a constant in detail() makes the second assertion fail, and that zeroing needsValidation's count makes the dashboard assertion fail. Co-Authored-By: Claude Opus 5 (1M context) --- .../tech-log/mock-studio-gateway.test.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/features/tech-log/mock-studio-gateway.test.ts b/tests/features/tech-log/mock-studio-gateway.test.ts index 6cd3928..8d3b668 100644 --- a/tests/features/tech-log/mock-studio-gateway.test.ts +++ b/tests/features/tech-log/mock-studio-gateway.test.ts @@ -597,3 +597,45 @@ test("publication history, immutable snapshots, conflict fixtures and 404 bodies isProblem(404, "PUBLICATION_EVENT_NOT_FOUND"), ); }); + +test("dashboard totals.needsValidation matches the documents whose next action is to validate or fix validation", async () => { + const gateway = gatewayAt(); + const dashboard = await gateway.getDashboard(); + const documents = await gateway.listDocuments({ limit: 100 }); + + const expected = documents.items.filter( + (item) => + item.nextAction === "VALIDATE" || item.nextAction === "FIX_VALIDATION", + ).length; + // Cross-checked against `listDocuments`, not against the dashboard's own + // arithmetic, so a broken `needsValidation` filter shows up as a mismatch + // instead of being restated. Both branches (>0 and 0); + assert.ok(expected < documents.items.length); + assert.equal(dashboard.totals.needsValidation, expected); +}); + +test("getDocument's nextAction reflects the assembled WorkingCopyDetail, not a cached or default value", async () => { + const gateway = gatewayAt(); + + // Published at exactly the current version (fixtures.ts: publishedVersion 4 + // === document version 4) is deriveNextAction's first, clock-independent + // branch: nextAction must be "NONE" regardless of validation or preview state. + const redis = await gateway.getDocument(FIXTURE_IDS.redisAdapterCase); + assert.equal(redis.document.version, 4); + assert.equal(redis.currentPublication?.status, "PUBLISHED"); + assert.equal(redis.currentPublication?.publishedVersion, 4); + assert.equal(redis.nextAction, "NONE"); + + // Not published, and validated INVALID at the current version within the + // validity window: nextAction must be "FIX_VALIDATION", which only follows + // from currentValidation actually being read and its freshness actually + // computed - a value a hardcoded or wrongly-sourced default would not produce. + const edgeToken = await gateway.getDocument(FIXTURE_IDS.edgeTokenQuestion); + assert.equal(edgeToken.document.version, 2); + assert.equal(edgeToken.currentPublication, null); + assert.equal(edgeToken.currentValidation?.status, "INVALID"); + assert.equal(edgeToken.currentValidation?.validatedVersion, 2); + assert.equal(edgeToken.nextAction, "FIX_VALIDATION"); +});