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) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-18 00:38:03 +09:00
co-authored by Claude Opus 5
parent 639e1a49c9
commit a0e0be6522
@@ -597,3 +597,45 @@ test("publication history, immutable snapshots, conflict fixtures and 404 bodies
isProblem(404, "PUBLICATION_EVENT_NOT_FOUND"), 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 <total) must hold, or a
// filter that always returns 0 or the full count would still pass silently.
assert.ok(expected > 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");
});