From a9d675f5a96198666a5b26ea9c43a6429fe25b28 Mon Sep 17 00:00:00 2001 From: Tapish Khandelwal Date: Sat, 12 Sep 2026 12:39:34 +0530 Subject: [PATCH 1/3] refactor(gst): make a target status answer its own questions What a status means was asked as literal comparisons in twenty-two modules. Nothing connected them, so each was an independent place to forget -- and when `not-generated` joined the union, five separate sites went on spelling the question `=== "not-filed"` and silently answered "no" for every period the portal declined to draft. `TARGET_STATUS_BEHAVIOUR` is a `Record` over the union, so adding a member does not compile until every question has been answered for it. That is the whole difference between this and the `Set` it replaces -- which sat directly beneath a comment explaining that a `Set` over a union is not exhaustiveness-checked, and had the defect that comment describes. Two questions are new because the code was asking them without naming them. `statedAbsence` is "is there a file to expect here?", which has one answer for both `not-filed` and `not-generated` even though a reader must keep them apart: one is a claim about the taxpayer, the other about the portal. `requiredEvidenceSignal` is the evidence a stored record claiming this status must carry, which two validators had each spelled out for `not-filed` alone. `isResolvedFullFiscalYearTargetStatus` and `needsExplicitFullFiscalYearRetry` are unchanged in behaviour and now read from the table. --- src/connectors/gst/filed-returns-contracts.ts | 122 ++++++++++++++++-- 1 file changed, 108 insertions(+), 14 deletions(-) diff --git a/src/connectors/gst/filed-returns-contracts.ts b/src/connectors/gst/filed-returns-contracts.ts index 7b33ffc8..fba2ff83 100644 --- a/src/connectors/gst/filed-returns-contracts.ts +++ b/src/connectors/gst/filed-returns-contracts.ts @@ -258,34 +258,128 @@ export function isFiledReturnsFullFiscalYearTargetStatus( return (FILED_RETURNS_FULL_FISCAL_YEAR_TARGET_STATUSES as readonly unknown[]).includes(value); } -// A target the portal has answered for. Retrying one cannot change its outcome, so a run counts it -// as done. Nine modules each kept their own copy of this pair, every copy spelled `downloaded` and -// `not-filed`, and none of them learned about `not-generated` -- which is how a live run stopped on -// the first period the portal declined to draft. -const RESOLVED_TARGET_STATUSES = new Set([ - "downloaded", - "not-filed", - "not-generated", -]); +/** + * What a status *means*, in one place, answered for every member. + * + * The questions below were previously asked as literal comparisons scattered across twenty-two + * modules. Nothing connected them, so each was an independent place to forget: `not-generated` was + * added to the union and five separate sites went on spelling the question `=== "not-filed"`, + * silently answering "no" for every period the portal declined to draft. + * + * A `Record` over the union is what makes that impossible. Adding a member does not compile until + * every question below has been answered for it -- which is the difference between this and the + * `Set` that used to live here, under a comment warning against exactly the `Set`. + */ +interface FiledReturnsTargetStatusBehaviour { + /** The run is still working on this by itself. Nobody needs to decide anything. */ + active: boolean; + /** The portal has answered. Retrying cannot change the outcome, so a run counts it as done. */ + resolved: boolean; + /** + * The portal stated that no artifact exists for this period. + * + * Deliberately one question with two members behind it. `not-filed` is a claim about the + * taxpayer and `not-generated` is a claim about the portal, so they must stay distinct wherever + * a person reads them -- but "is there a file to expect?" has the same answer for both, and + * every site that asked it by naming only `not-filed` was wrong. + */ + statedAbsence: boolean; + /** A local file was staged for this target. Narrower than `resolved`: an absence stages nothing. */ + producedFile: boolean; + /** + * Holds an answer a run must not discard or overwrite. + * + * Wider than `resolved`: a manually observed target was answered by a person rather than the + * portal, which does not resolve it but is still work that took a human and cannot be replaced. + */ + holdsAnswer: boolean; + /** + * The signal a stored record claiming this status must carry, or `null` where none applies. + * + * A status is a claim, and a claim without its evidence is how a ledger comes back asserting + * something no run established. `downloaded` is `null` here because its evidence is a richer + * predicate than a signal name -- see `hasPositiveFiledReturnsDownloadEvidence`. + */ + requiredEvidenceSignal: string | null; +} + +const TARGET_STATUS_BEHAVIOUR: Readonly< + Record +> = { + pending: base({ active: true }), + running: base({ active: true }), + downloaded: base({ resolved: true, producedFile: true, holdsAnswer: true }), + "manually-observed": base({ holdsAnswer: true }), + "not-filed": base({ + resolved: true, + statedAbsence: true, + holdsAnswer: true, + requiredEvidenceSignal: "filed-return-positively-not-filed", + }), + "not-generated": base({ + resolved: true, + statedAbsence: true, + holdsAnswer: true, + requiredEvidenceSignal: "filed-gstr2b-not-generated", + }), + "download-unconfirmed": base({}), + blocked: base({}), + failed: base({}), + cancelled: base({}), +}; + +/** Every question answers "no" unless a status says otherwise, so a new member starts inert. */ +function base( + overrides: Partial, +): FiledReturnsTargetStatusBehaviour { + return { + active: false, + resolved: false, + statedAbsence: false, + producedFile: false, + holdsAnswer: false, + requiredEvidenceSignal: null, + ...overrides, + }; +} + +export function filedReturnsTargetStatusBehaviour( + status: FiledReturnsFullFiscalYearTargetStatus, +): FiledReturnsTargetStatusBehaviour { + return TARGET_STATUS_BEHAVIOUR[status]; +} export function isResolvedFullFiscalYearTargetStatus( status: FiledReturnsFullFiscalYearTargetStatus, ): boolean { - return RESOLVED_TARGET_STATUSES.has(status); + return TARGET_STATUS_BEHAVIOUR[status].resolved; +} + +/** The portal said there is no artifact here -- whoever it made the claim about. */ +export function statesFullFiscalYearTargetAbsence( + status: FiledReturnsFullFiscalYearTargetStatus, +): boolean { + return TARGET_STATUS_BEHAVIOUR[status].statedAbsence; +} + +/** An answer a run must not discard or overwrite, whether the portal or a person gave it. */ +export function holdsFullFiscalYearTargetAnswer( + status: FiledReturnsFullFiscalYearTargetStatus, +): boolean { + return TARGET_STATUS_BEHAVIOUR[status].holdsAnswer; } /** * Unresolved, and not a state the run reaches by itself. What is left needs the user to choose. * - * Derived rather than listed: a status that is neither resolved nor pending/running belongs here by + * Derived rather than listed: a status that is neither resolved nor active belongs here by * definition, so a new one cannot land in neither bucket. */ export function needsExplicitFullFiscalYearRetry( status: FiledReturnsFullFiscalYearTargetStatus, ): boolean { - return ( - !isResolvedFullFiscalYearTargetStatus(status) && status !== "pending" && status !== "running" - ); + const behaviour = TARGET_STATUS_BEHAVIOUR[status]; + return !behaviour.resolved && !behaviour.active; } export interface FiledReturnsFullFiscalYearTarget { From 6b5d69cc15eedaa2de5df99578e85af6e8a18a58 Mon Sep 17 00:00:00 2001 From: Tapish Khandelwal Date: Sat, 12 Sep 2026 12:39:34 +0530 Subject: [PATCH 2/3] fix(gst): stop five sites asking about absence by naming one member of it Each asked "is there a file to expect for this period?" and spelled it `status === "not-filed"`, so each answered "no, keep working" for a period the portal had already declined to draft: - evidence retained from a discarded run dropped declined periods entirely; - a year of declined periods was not recorded as having produced no ZIP; - restaging reset declined periods to `blocked`, sending the run back to periods the portal has answered; - two ledger validators accepted a stored `not-generated` record carrying no evidence that anything had been declined, while rejecting the equivalent `not-filed` record. Two more sites asked the complement -- "does this ledger still hold anything unresolved?" -- as hand-written arrays, and the two copies had drifted apart: `filed-returns-current-state.ts` listed seven members and `local-data.ts` six, omitting `cancelled`. A ledger the panel was still surfacing as the current run could therefore be cleared from under it by the local-data guard. Both now ask the question rather than enumerate an answer. `hasTerminalPositiveTarget` was `resolved || "manually-observed"`, which is what `holdsAnswer` means, so it says that instead. --- ...l-supported-full-fiscal-year-validation.ts | 12 ++++++++---- src/background/filed-returns-current-state.ts | 16 +++++----------- ...iled-returns-full-fiscal-year-run-state.ts | 11 +++++------ .../filed-returns-full-fiscal-year-staging.ts | 3 ++- .../filed-returns-full-fiscal-year-summary.ts | 5 +++-- ...led-returns-full-fiscal-year-validation.ts | 12 ++++++++---- src/background/local-data.ts | 19 ++++++++----------- 7 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/background/filed-returns-all-supported-full-fiscal-year-validation.ts b/src/background/filed-returns-all-supported-full-fiscal-year-validation.ts index 1ec631e7..d3275e48 100644 --- a/src/background/filed-returns-all-supported-full-fiscal-year-validation.ts +++ b/src/background/filed-returns-all-supported-full-fiscal-year-validation.ts @@ -43,6 +43,7 @@ import { canonicalFullFiscalYearPlanPeriods, isCanonicalFullFiscalYearPeriodPlan, } from "./filed-returns-full-fiscal-year-validation"; +import { filedReturnsTargetStatusBehaviour } from "../connectors/gst/filed-returns-contracts"; export const ALL_SUPPORTED_FULL_FISCAL_YEAR_PLAN_VERSION = "all-supported-filed-returns-targets-v1" as const; @@ -522,10 +523,13 @@ function isTarget( ) { return false; } - return ( - verifiedTarget.status !== "not-filed" || - verifiedTarget.safeSignals.includes("filed-return-positively-not-filed") - ); + // The evidence a claim needs is a property of the status, not a rule each validator remembers. + // Spelled out, only `not-filed` was checked here and in the single-return validator, so a stored + // record could assert `not-generated` with nothing behind it -- in both. + const requiredEvidenceSignal = filedReturnsTargetStatusBehaviour( + verifiedTarget.status, + ).requiredEvidenceSignal; + return !requiredEvidenceSignal || verifiedTarget.safeSignals.includes(requiredEvidenceSignal); } function hasCanonicalConcreteArtifacts( diff --git a/src/background/filed-returns-current-state.ts b/src/background/filed-returns-current-state.ts index 55ed1f3e..aa513a03 100644 --- a/src/background/filed-returns-current-state.ts +++ b/src/background/filed-returns-current-state.ts @@ -18,6 +18,7 @@ import { readRetainedPlanLedgers, } from "./filed-returns-full-fiscal-year-run-state"; import { readCurrentFiledReturnsTargetReviewSummary } from "./filed-returns-target-review"; +import { isResolvedFullFiscalYearTargetStatus } from "../connectors/gst/filed-returns-contracts"; export interface FiledReturnsCurrentStateDeps { storageKeys: { @@ -143,17 +144,10 @@ function isRetainedZipRetrySummary( function isActionableFullFiscalYearLedger(ledger: FiledReturnsFullFiscalYearLedger): boolean { if (ledger.status === "complete") return false; - return ledger.targets.some((target) => - [ - "pending", - "running", - "download-unconfirmed", - "blocked", - "failed", - "cancelled", - "manually-observed", - ].includes(target.status), - ); + // The complement of "answered", asked as such. This was a hand-written list of seven members, + // and `local-data.ts` kept a second copy of the same idea that had six -- so a cancelled target + // made a ledger actionable here while leaving it clearable there. + return ledger.targets.some((target) => !isResolvedFullFiscalYearTargetStatus(target.status)); } function isNewerSinglePeriodSummary( diff --git a/src/background/filed-returns-full-fiscal-year-run-state.ts b/src/background/filed-returns-full-fiscal-year-run-state.ts index aaf6227a..7c49de08 100644 --- a/src/background/filed-returns-full-fiscal-year-run-state.ts +++ b/src/background/filed-returns-full-fiscal-year-run-state.ts @@ -6,8 +6,6 @@ import type { FiledReturnsFullFiscalYearLedger, PortalFlowStepResult, } from "../connectors/gst/filed-returns-contracts"; -import { isResolvedFullFiscalYearTargetStatus } from "../connectors/gst/filed-returns-contracts"; -import { isCleanedZipPhase } from "../connectors/gst/filed-returns-contracts"; import type { PackMessageResponse } from "../connectors/gst/messages"; import { filedReturnScopeId } from "../connectors/gst/filed-returns-return-descriptors"; import { normaliseFiledReturnsArtifactType } from "../connectors/gst/filed-returns-artifacts"; @@ -32,15 +30,16 @@ import { toFullFiscalYearSummary, } from "./filed-returns-full-fiscal-year-summary"; import { persistCanonicalFiledReturnsFlowSummary } from "./filed-returns-session-summary"; +import { + holdsFullFiscalYearTargetAnswer, + isCleanedZipPhase, +} from "../connectors/gst/filed-returns-contracts"; // A target the run should not silently discard: the portal answered, or a person reported what they // saw. Written as a list, this said `not-filed` but not `not-generated` -- two answers of the same // kind, one of which would have let a cancelled run holding it be replaced without asking. export function hasTerminalPositiveTarget(ledger: FiledReturnsFullFiscalYearLedger): boolean { - return ledger.targets.some( - (target) => - isResolvedFullFiscalYearTargetStatus(target.status) || target.status === "manually-observed", - ); + return ledger.targets.some((target) => holdsFullFiscalYearTargetAnswer(target.status)); } export function hasDownloadUnconfirmedTarget(ledger: FiledReturnsFullFiscalYearLedger): boolean { diff --git a/src/background/filed-returns-full-fiscal-year-staging.ts b/src/background/filed-returns-full-fiscal-year-staging.ts index 5d3e91cb..d9b889ee 100644 --- a/src/background/filed-returns-full-fiscal-year-staging.ts +++ b/src/background/filed-returns-full-fiscal-year-staging.ts @@ -21,6 +21,7 @@ import { import { durableFullFiscalYearArtifactSignals } from "./filed-returns-full-fiscal-year-validation"; import { discardFullFiscalYearFiledReturnsZip } from "./filed-returns-full-fiscal-year-zip"; import { readCanonicalFiledReturnsFlowSummary } from "./filed-returns-session-summary"; +import { statesFullFiscalYearTargetAbsence } from "../connectors/gst/filed-returns-contracts"; const FULL_YEAR_STAGED_SIGNAL_PREFIX = "full-fiscal-year-opfs-staged:"; @@ -267,7 +268,7 @@ export function markFullFiscalYearRestagingRequired( ): FiledReturnsFullFiscalYearLedger { const timestamp = now.toISOString(); const targets = ledger.targets.map((target) => - target.status === "not-filed" + statesFullFiscalYearTargetAbsence(target.status) ? target : { ...target, diff --git a/src/background/filed-returns-full-fiscal-year-summary.ts b/src/background/filed-returns-full-fiscal-year-summary.ts index e4b293a8..e24a7422 100644 --- a/src/background/filed-returns-full-fiscal-year-summary.ts +++ b/src/background/filed-returns-full-fiscal-year-summary.ts @@ -26,6 +26,7 @@ import { hasInconsistentFullFiscalYearCompletion, isFullFiscalYearLedgerStale, } from "./filed-returns-full-fiscal-year-ledger"; +import { statesFullFiscalYearTargetAbsence } from "../connectors/gst/filed-returns-contracts"; export function fullFiscalYearZipPhaseStep( ledger: FiledReturnsFullFiscalYearLedger, @@ -330,7 +331,7 @@ export function fullFiscalYearTargetEvidence( // run produced. Everything that depended on a file Pack no longer has goes. if (runDiscarded || (clearedWithoutDelivery && hadStagedFiles)) { return ledger.targets - .filter((target) => target.status === "not-filed") + .filter((target) => statesFullFiscalYearTargetAbsence(target.status)) .map((target) => ({ period: target.period, outcome: "not-filed" as const })); } // From the step as well as the ledger. An MV3 interruption produces a blocked @@ -459,7 +460,7 @@ export function completeFullFiscalYearStep( ...(unplanned.length > 0 ? ["full-fiscal-year-plan-narrower-than-eligible"] : []), ...(ledger.zipPhase === "cleaned-without-export" && ledger.targets.length > 0 && - ledger.targets.every((target) => target.status === "not-filed") + ledger.targets.every((target) => statesFullFiscalYearTargetAbsence(target.status)) ? ["full-fiscal-year-no-zip-artifacts"] : []), ], diff --git a/src/background/filed-returns-full-fiscal-year-validation.ts b/src/background/filed-returns-full-fiscal-year-validation.ts index 3b7c74fe..3c74236a 100644 --- a/src/background/filed-returns-full-fiscal-year-validation.ts +++ b/src/background/filed-returns-full-fiscal-year-validation.ts @@ -36,6 +36,7 @@ import { hasPositiveFiledReturnsDownloadEvidence, isValidFiledReturnsDownloadDiagnosticState, } from "./filed-returns-download-diagnostic-state"; +import { filedReturnsTargetStatusBehaviour } from "../connectors/gst/filed-returns-contracts"; export const FULL_FISCAL_YEAR_PLAN_VERSION = "filed-returns-targets-v3"; @@ -441,10 +442,13 @@ function isFullFiscalYearTarget( ) { return false; } - if ( - target.status === "not-filed" && - !target.safeSignals?.includes("filed-return-positively-not-filed") - ) { + // A status is a claim, and the evidence each claim needs is a property of the status rather + // than a rule this file remembers. Spelled out here, only `not-filed` was ever checked, so a + // stored record could assert `not-generated` with nothing behind it. + const requiredEvidenceSignal = filedReturnsTargetStatusBehaviour( + target.status, + ).requiredEvidenceSignal; + if (requiredEvidenceSignal && !target.safeSignals?.includes(requiredEvidenceSignal)) { return false; } return true; diff --git a/src/background/local-data.ts b/src/background/local-data.ts index 5a8f2ddc..65cf38b5 100644 --- a/src/background/local-data.ts +++ b/src/background/local-data.ts @@ -1,6 +1,5 @@ import { browser } from "wxt/browser"; import type { FiledReturnsFullFiscalYearLedger } from "../connectors/gst/filed-returns-contracts"; -import { isCleanedZipPhase } from "../connectors/gst/filed-returns-contracts"; import type { PackMessageResponse } from "../connectors/gst/messages"; import { readActiveFiledReturnsRunStorageState, @@ -29,6 +28,10 @@ import { clearAllSupportedFullFiscalYearLedgerPlans, readAllSupportedPlanLedgersStorageStateWithinOperation, } from "./filed-returns-all-supported-full-fiscal-year-run-state"; +import { + isCleanedZipPhase, + isResolvedFullFiscalYearTargetStatus, +} from "../connectors/gst/filed-returns-contracts"; export interface PackLocalDataDeps { clearableLocalStorageKeys: readonly string[]; @@ -209,16 +212,10 @@ function hasUnresolvedZipState(ledger: { function isUnresolvedFullFiscalYearLedger(ledger: FiledReturnsFullFiscalYearLedger): boolean { if (hasInconsistentFullFiscalYearCompletion(ledger)) return true; if (ledger.status === "complete" || ledger.status === "cancelled") return false; - return ledger.targets.some((target) => - [ - "pending", - "running", - "download-unconfirmed", - "blocked", - "failed", - "manually-observed", - ].includes(target.status), - ); + // The same question `filed-returns-current-state.ts` asks when it decides which ledger to show, + // and now the same answer. The two lists had drifted: this one omitted `cancelled`, so a ledger + // the panel was still surfacing could be cleared from under it. + return ledger.targets.some((target) => !isResolvedFullFiscalYearTargetStatus(target.status)); } async function readLocalValue(key: string): Promise { From dc67c929cd6c0747b5fae738104e0686dcd8398a Mon Sep 17 00:00:00 2001 From: Tapish Khandelwal Date: Sat, 12 Sep 2026 12:39:34 +0530 Subject: [PATCH 3/3] test(gst): pin what a status means, and the declined period it now covers The invariants hold the table together: a stated absence resolves a target and stages nothing, anything resolved is an answer worth keeping, and every status lands in exactly one of active, resolved, or needs-a-decision. Both behaviour cases fail when their site is reverted to the literal it used to spell. --- ...scal-year-declined-period-handling.test.ts | 69 ++++++++++++++++++ ...ed-returns-target-status-behaviour.test.ts | 71 +++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 tests/background/full-fiscal-year-declined-period-handling.test.ts create mode 100644 tests/connectors/filed-returns-target-status-behaviour.test.ts diff --git a/tests/background/full-fiscal-year-declined-period-handling.test.ts b/tests/background/full-fiscal-year-declined-period-handling.test.ts new file mode 100644 index 00000000..d0dc3fa7 --- /dev/null +++ b/tests/background/full-fiscal-year-declined-period-handling.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from "vitest"; +import { FILED_RETURNS_MONTHS } from "../../src/connectors/gst/filed-returns-scope"; +import { createFullFiscalYearLedger } from "../../src/background/filed-returns-full-fiscal-year-ledger"; +import { markFullFiscalYearRestagingRequired } from "../../src/background/filed-returns-full-fiscal-year-staging"; +import { isFullFiscalYearLedger } from "../../src/background/filed-returns-full-fiscal-year-validation"; +import { canonicalDurableTargetStatus } from "../../src/connectors/gst/filed-returns-durable-status"; +import { RECOVERY_SCOPE } from "./full-year-completion-fixtures.test-helpers"; + +// A period the portal declined to draft is an answer, not an outcome still owed. Five separate +// places asked "is there a file to expect here?" by spelling it `status === "not-filed"`, so each +// one silently answered "no, keep working" for every declined period. +function ledgerWith(statuses: Partial>) { + const base = createFullFiscalYearLedger( + RECOVERY_SCOPE, + new Date("2026-08-24T00:00:00.000Z"), + FILED_RETURNS_MONTHS, + ); + return { + ...base, + targets: base.targets.map((target) => { + const override = statuses[target.period]; + if (!override) return target; + const status = override.status as (typeof target)["status"]; + const safeSignals = override.safeSignals ?? []; + return { + ...target, + status, + attempts: 1, + ...canonicalDurableTargetStatus(target, status, safeSignals), + }; + }), + }; +} + +describe("restaging a fiscal year", () => { + it("leaves a declined period alone, as it already does a not-filed one", () => { + // Neither staged a file, so neither has anything to restage. Resetting a declined period to + // `blocked` sends the run back to a period the portal has already answered. + const ledger = ledgerWith({ + April: { status: "not-filed", safeSignals: ["filed-return-positively-not-filed"] }, + May: { status: "not-generated", safeSignals: ["filed-gstr2b-not-generated"] }, + }); + + const restaged = markFullFiscalYearRestagingRequired(ledger, new Date("2026-08-26T00:00:00Z")); + const byPeriod = new Map(restaged.targets.map((target) => [target.period, target.status])); + + expect(byPeriod.get("April")).toBe("not-filed"); + expect(byPeriod.get("May")).toBe("not-generated"); + }); +}); + +describe("reading a stored fiscal-year ledger back", () => { + it("refuses a declined period that carries no evidence it was declined", () => { + // A status is a claim. The same rule already rejected a `not-filed` record with no portal + // signal behind it; a `not-generated` record could assert the portal declined a period that + // nothing ever established. + const ledger = ledgerWith({ April: { status: "not-generated", safeSignals: [] } }); + + expect(isFullFiscalYearLedger(ledger)).toBe(false); + }); + + it("accepts a declined period that carries the portal's own signal", () => { + const ledger = ledgerWith({ + April: { status: "not-generated", safeSignals: ["filed-gstr2b-not-generated"] }, + }); + + expect(isFullFiscalYearLedger(ledger)).toBe(true); + }); +}); diff --git a/tests/connectors/filed-returns-target-status-behaviour.test.ts b/tests/connectors/filed-returns-target-status-behaviour.test.ts new file mode 100644 index 00000000..53ed64dc --- /dev/null +++ b/tests/connectors/filed-returns-target-status-behaviour.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from "vitest"; +import { + FILED_RETURNS_FULL_FISCAL_YEAR_TARGET_STATUSES, + filedReturnsTargetStatusBehaviour, + holdsFullFiscalYearTargetAnswer, + isResolvedFullFiscalYearTargetStatus, + needsExplicitFullFiscalYearRetry, + statesFullFiscalYearTargetAbsence, +} from "../../src/connectors/gst/filed-returns-contracts"; + +// What a status means was previously asked as literal comparisons in twenty-two modules, and +// nothing connected them: `not-generated` joined the union and five separate sites went on +// spelling the question `=== "not-filed"`. These are the invariants that make the table a single +// answer rather than a sixth copy of one. +describe("what a target status means", () => { + const statuses = FILED_RETURNS_FULL_FISCAL_YEAR_TARGET_STATUSES; + + it("answers every question for every member", () => { + for (const status of statuses) { + const behaviour = filedReturnsTargetStatusBehaviour(status); + expect(behaviour, status).toBeDefined(); + expect(Object.keys(behaviour).sort()).toEqual([ + "active", + "holdsAnswer", + "producedFile", + "requiredEvidenceSignal", + "resolved", + "statedAbsence", + ]); + } + }); + + it.each(statuses)("puts %s in exactly one of active, resolved, or needs-a-decision", (status) => { + const behaviour = filedReturnsTargetStatusBehaviour(status); + const buckets = [ + behaviour.active, + behaviour.resolved, + needsExplicitFullFiscalYearRetry(status), + ].filter(Boolean); + expect(buckets).toHaveLength(1); + }); + + it.each(statuses)("keeps %s consistent across the narrower questions", (status) => { + const behaviour = filedReturnsTargetStatusBehaviour(status); + // A stated absence is an answer from the portal, so it resolves the target. + if (behaviour.statedAbsence) expect(isResolvedFullFiscalYearTargetStatus(status)).toBe(true); + // A staged file is an answer too. + if (behaviour.producedFile) expect(isResolvedFullFiscalYearTargetStatus(status)).toBe(true); + // Anything resolved is an answer worth keeping; the reverse does not hold -- a manually + // observed target was answered by a person, which is work a run must not overwrite either. + if (behaviour.resolved) expect(holdsFullFiscalYearTargetAnswer(status)).toBe(true); + // An absence stages nothing, which is why restaging must leave it alone. + if (behaviour.statedAbsence) expect(behaviour.producedFile).toBe(false); + }); + + it("treats both stated absences alike, and nothing else as one", () => { + const absences = statuses.filter((status) => statesFullFiscalYearTargetAbsence(status)); + // Distinct to a reader -- one is a claim about the taxpayer, the other about the portal -- and + // the same answer to "is there a file to expect?". Every site that asked the second question + // by naming only `not-filed` was wrong. + expect([...absences].sort()).toEqual(["not-filed", "not-generated"]); + }); + + it("requires corroborating evidence for every claim that can be asserted without it", () => { + // `downloaded` is absent deliberately: its evidence is a richer predicate than a signal name. + const requiring = statuses + .filter((status) => filedReturnsTargetStatusBehaviour(status).requiredEvidenceSignal) + .sort(); + expect(requiring).toEqual(["not-filed", "not-generated"]); + }); +});