From 024199ff7b6f49475a54f7b3c17bad3c1b3d201e Mon Sep 17 00:00:00 2001 From: Tapish Khandelwal Date: Wed, 5 Aug 2026 14:09:03 +0530 Subject: [PATCH 1/6] fix(recovery): persist direct download completion proof --- .../filed-returns-download-trigger.ts | 99 +++++++++++++++---- .../filed-returns-json-acquisition.ts | 4 +- src/background/gstr3b-artifact-acquisition.ts | 9 +- ...turns-download-trigger-acquisition.test.ts | 39 ++++++-- .../filed-returns-json-acquisition.test.ts | 13 ++- ...led-returns-session-write-boundary.test.ts | 58 +++++++++++ 6 files changed, 186 insertions(+), 36 deletions(-) diff --git a/src/background/filed-returns-download-trigger.ts b/src/background/filed-returns-download-trigger.ts index b8f0fe00..966c34c0 100644 --- a/src/background/filed-returns-download-trigger.ts +++ b/src/background/filed-returns-download-trigger.ts @@ -21,6 +21,7 @@ import { toPortalReturnPeriod } from "../connectors/gst/filed-returns-return-per import { downloadAcquiredArtifact } from "./artifact-download"; import { stageOffscreenFiledReturn } from "./offscreen-blob-url"; import { safeFiledReturnZipEntryPath } from "./filed-returns-download-filename"; +import { withFiledReturnsDownloadDiagnostic } from "./filed-returns-download-diagnostics"; import { clearArtifactAcquisitionCheckpoint, persistArtifactAcquisitionDownloadId, @@ -198,18 +199,15 @@ export async function triggerAndObserveFiledReturnDownload({ return delivery.ok ? { ok: true, - flowStep: { - connectorId: "gst", - scopeId: filedReturnScopeId("GSTR-3B"), - state: "downloaded", - safeSignals: [ - ...response.artifact.safeSignals, - ...delivery.safeSignals, - "extension-download-complete", - ], + flowStep: directCapturedArtifactFlowStep({ + artifactType, + downloadId: delivery.downloadId, + requestId, safeMessage: delivery.safeMessage ?? "Pack saved the portal-produced GSTR-3B data JSON.", - }, + safeSignals: [...response.artifact.safeSignals, ...delivery.safeSignals], + scope, + }), } : { ok: true, @@ -284,14 +282,15 @@ export async function triggerAndObserveFiledReturnDownload({ return acquired.ok ? { ok: true, - flowStep: { - connectorId: "gst", - scopeId: filedReturnScopeId("GSTR-3B"), - state: "downloaded", - safeSignals: acquired.safeSignals, + flowStep: directCapturedArtifactFlowStep({ + artifactType, + downloadId: acquired.downloadId, + requestId, safeMessage: acquired.safeMessage ?? "Pack saved the portal-produced filed GSTR-3B PDF.", - }, + safeSignals: acquired.safeSignals, + scope, + }), } : { ok: true, @@ -645,7 +644,7 @@ async function deliverValidatedArtifact({ `${staging.bundleKind}-opfs-staged`, `${staging.bundleKind}-opfs-staged:${artifactType}`, ], - downloadDiagnostic: stagedArtifactDiagnostic(scope, artifactType, mimeType, requestId), + downloadDiagnostic: capturedArtifactDiagnostic(scope, artifactType, mimeType, requestId), } : { ok: false, reason: result.errorCategory ?? "stage-failed", safeSignals }; } @@ -664,7 +663,14 @@ async function deliverValidatedArtifact({ return delivery.ok ? { ok: true, - safeSignals: [...safeSignals, ...delivery.safeSignals, "extension-download-complete"], + downloadDiagnostic: capturedArtifactDiagnostic( + scope, + artifactType, + mimeType, + requestId, + delivery.downloadId, + ), + safeSignals: [...safeSignals, ...delivery.safeSignals], ...(delivery.safeMessage ? { safeMessage: delivery.safeMessage } : {}), } : { @@ -674,11 +680,62 @@ async function deliverValidatedArtifact({ }; } -function stagedArtifactDiagnostic( +function directCapturedArtifactFlowStep({ + artifactType, + downloadId, + requestId, + safeMessage, + safeSignals, + scope, +}: { + artifactType: "PDF" | "JSON"; + downloadId: number | undefined; + requestId: string; + safeMessage: string; + safeSignals: string[]; + scope: FiledReturnsDownloadScope; +}): PortalFlowStepResult { + if (typeof downloadId !== "number" || !Number.isSafeInteger(downloadId) || downloadId < 0) { + return { + connectorId: "gst", + scopeId: filedReturnScopeId(scope.returnType), + state: "blocked", + safeSignals: [...safeSignals, "artifact-acquisition-failed", "artifact-delivery-unconfirmed"], + safeMessage: + "Pack could not retain the exact browser download identity, so it did not mark this target complete.", + }; + } + return withFiledReturnsDownloadDiagnostic({ + attemptClass: "captured-portal-request", + flowStep: { + connectorId: "gst", + scopeId: filedReturnScopeId(scope.returnType), + state: "downloaded", + safeSignals, + safeMessage, + }, + safeEvidence: { + byteCountClass: "non-empty", + downloadId, + mimeClass: artifactType === "PDF" ? "pdf" : "json", + urlClass: "unknown", + }, + target: { + actionId: requestId, + artifactType, + financialYear: scope.financialYear, + period: scope.period, + returnType: scope.returnType, + }, + }); +} + +function capturedArtifactDiagnostic( scope: FiledReturnsDownloadScope, artifactType: FiledReturnsConcreteArtifactType, mimeType: string, actionId: string, + downloadId?: number, ): FiledReturnsDownloadDiagnostic { const mimeClass = mimeType === "application/pdf" @@ -696,7 +753,9 @@ function stagedArtifactDiagnostic( actionId, artifactType, byteCountClass: "non-empty", - downloadPathClass: "captured-portal-request-data", + ...(downloadId === undefined + ? { downloadPathClass: "captured-portal-request-data" as const } + : { downloadId, downloadPathClass: "captured-portal-request-unknown" as const }), endpointClass, eventType: "filed-return-download-path", financialYear: scope.financialYear, diff --git a/src/background/filed-returns-json-acquisition.ts b/src/background/filed-returns-json-acquisition.ts index 74b7c344..ae851c63 100644 --- a/src/background/filed-returns-json-acquisition.ts +++ b/src/background/filed-returns-json-acquisition.ts @@ -7,6 +7,7 @@ type JsonReturnType = "GSTR-3B" | "GSTR-2B"; type JsonAcquisitionResult = | { ok: true; + downloadId?: number; downloadDiagnostic?: FiledReturnsDownloadDiagnostic; safeMessage?: string; safeSignals: string[]; @@ -77,7 +78,8 @@ export async function acquireFiledReturnJsonInMainWorld(input: { return delivery.ok ? { ok: true, - safeSignals: [...delivery.safeSignals, "extension-download-complete"], + downloadId: delivery.downloadId, + safeSignals: delivery.safeSignals, ...(delivery.safeMessage ? { safeMessage: delivery.safeMessage } : {}), } : { ok: false, reason: delivery.reason, safeSignals: delivery.safeSignals }; diff --git a/src/background/gstr3b-artifact-acquisition.ts b/src/background/gstr3b-artifact-acquisition.ts index 9e9c73e7..b0d41718 100644 --- a/src/background/gstr3b-artifact-acquisition.ts +++ b/src/background/gstr3b-artifact-acquisition.ts @@ -17,7 +17,7 @@ export async function acquireGstr3bPdfAfterPreflight(input: { onStarted?: (downloadId: number) => Promise; onStartCheckpointFailed?: (downloadId: number) => Promise; }): Promise< - | { ok: true; safeMessage?: string; safeSignals: string[] } + | { ok: true; downloadId: number; safeMessage?: string; safeSignals: string[] } | { ok: false; reason: string; @@ -78,11 +78,8 @@ export async function acquireGstr3bPdfAfterPreflight(input: { return delivery.ok ? { ok: true, - safeSignals: [ - ...captured.safeSignals, - ...delivery.safeSignals, - "extension-download-complete", - ], + downloadId: delivery.downloadId, + safeSignals: [...captured.safeSignals, ...delivery.safeSignals], ...(delivery.safeMessage ? { safeMessage: delivery.safeMessage } : {}), } : { ok: false, reason: delivery.reason, safeSignals: delivery.safeSignals }; diff --git a/tests/background/filed-returns-download-trigger-acquisition.test.ts b/tests/background/filed-returns-download-trigger-acquisition.test.ts index 16def8ae..c2436dd9 100644 --- a/tests/background/filed-returns-download-trigger-acquisition.test.ts +++ b/tests/background/filed-returns-download-trigger-acquisition.test.ts @@ -3,26 +3,28 @@ import type { PackMessageResponse } from "../../src/connectors/gst/messages"; const captureMocks = vi.hoisted(() => ({ acquireGstr3bPdfAfterPreflight: vi.fn(async () => ({ + downloadId: 91, ok: true as const, - safeSignals: ["synthetic-extension-download-complete"], + safeSignals: [] as string[], })), acquirePageGeneratedArtifact: vi.fn(async () => ({ ok: true as const, bytes: new Uint8Array([0x50, 0x4b]), mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - safeSignals: ["synthetic-extension-download-complete"], + safeSignals: [] as string[], })), acquireFiledReturnJsonInMainWorld: vi.fn(async () => ({ + downloadId: 91, ok: true as const, safeMessage: undefined as string | undefined, - safeSignals: ["synthetic-extension-download-complete"], + safeSignals: [] as string[], })), downloadAcquiredArtifact: vi.fn(async () => ({ ok: true as const, downloadId: 91, bytesReceived: 128, safeMessage: undefined as string | undefined, - safeSignals: ["synthetic-extension-download-complete"], + safeSignals: [] as string[], })), clearArtifactAcquisitionCheckpoint: vi.fn(async () => undefined), persistArtifactAcquisitionDownloadId: vi.fn(async () => undefined), @@ -260,7 +262,18 @@ describe("GSTR-3B artifact acquisition dispatch", () => { tabId: 17, }); - expect(response).toMatchObject({ flowStep: { state: "downloaded" } }); + expect(response).toMatchObject({ + flowStep: { + downloadDiagnostic: { + artifactType: "JSON", + byteCountClass: "non-empty", + downloadId: 91, + mimeClass: "json", + status: "downloaded", + }, + state: "downloaded", + }, + }); expect(captureMocks.acquireFiledReturnJsonInMainWorld).toHaveBeenCalledWith( expect.objectContaining({ filename: "ComplyEaze-Pack/2026-27/GSTR-3B/June-data.json", @@ -270,10 +283,11 @@ describe("GSTR-3B artifact acquisition dispatch", () => { it("surfaces a completed JSON filename override without treating the target as failed", async () => { captureMocks.acquireFiledReturnJsonInMainWorld.mockResolvedValueOnce({ + downloadId: 91, ok: true, safeMessage: "Another extension changed where this file was saved. Check browser Downloads before using it.", - safeSignals: ["download-filename-overridden"], + safeSignals: ["download-filename-overridden"] as string[], }); const response = await triggerAndObserveFiledReturnDownload({ @@ -320,7 +334,18 @@ describe("GSTR-3B artifact acquisition dispatch", () => { tabId: 17, }); - expect(response).toMatchObject({ flowStep: { state: "downloaded" } }); + expect(response).toMatchObject({ + flowStep: { + downloadDiagnostic: { + artifactType: "PDF", + byteCountClass: "non-empty", + downloadId: 91, + mimeClass: "pdf", + status: "downloaded", + }, + state: "downloaded", + }, + }); expect(captureMocks.acquireGstr3bPdfAfterPreflight).toHaveBeenCalledWith( expect.objectContaining({ filename: "ComplyEaze-Pack/2026-27/GSTR-3B/June-return.pdf" }), ); diff --git a/tests/background/filed-returns-json-acquisition.test.ts b/tests/background/filed-returns-json-acquisition.test.ts index 2dbd64c6..e359ceda 100644 --- a/tests/background/filed-returns-json-acquisition.test.ts +++ b/tests/background/filed-returns-json-acquisition.test.ts @@ -37,7 +37,12 @@ describe("filed-return JSON main-world acquisition", () => { }, }, ] as never); - mocks.downloadAcquiredArtifact.mockResolvedValue({ ok: true, safeSignals: ["synthetic"] }); + mocks.downloadAcquiredArtifact.mockResolvedValue({ + bytesReceived: 128, + downloadId: 91, + ok: true, + safeSignals: ["synthetic"], + }); await expect( acquireFiledReturnJsonInMainWorld({ @@ -47,7 +52,11 @@ describe("filed-return JSON main-world acquisition", () => { returnType: "GSTR-3B", tabId: 17, }), - ).resolves.toEqual({ ok: true, safeSignals: ["synthetic", "extension-download-complete"] }); + ).resolves.toEqual({ + downloadId: 91, + ok: true, + safeSignals: ["synthetic"], + }); expect(browser.scripting.executeScript).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/tests/background/filed-returns-session-write-boundary.test.ts b/tests/background/filed-returns-session-write-boundary.test.ts index c5272607..a0858c62 100644 --- a/tests/background/filed-returns-session-write-boundary.test.ts +++ b/tests/background/filed-returns-session-write-boundary.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { FiledReturnsFlowRunnerDeps } from "../../src/background/filed-returns-flow-runner"; import { persistFlowResponse } from "../../src/background/filed-returns-flow-runner-utils"; +import { artifactAcquisitionCheckpointKey } from "../../src/background/artifact-acquisition-state"; import { persistSummary } from "../../src/background/filed-returns-full-fiscal-year-run-state"; import { persistCanonicalFiledReturnsFlowSummary, @@ -90,6 +91,63 @@ describe("filed-return session write boundary", () => { expect(storage.session[COMPLETION_KEY]).toBeUndefined(); }); + it("persists a direct exact-ID completion before clearing its acquisition checkpoint", async () => { + const scope = { + artifactType: "PDF" as const, + financialYear: "2026-27", + period: "April", + returnType: "GSTR-3B" as const, + }; + const actionId = "00000000-0000-4000-8000-000000000091"; + const checkpointKey = artifactAcquisitionCheckpointKey(scope); + storage.session[checkpointKey] = { + ...scope, + armedAt: "2026-08-05T08:00:00.000Z", + downloadId: 91, + requestId: actionId, + state: "download-observing", + }; + + const response = await withPersistedSinglePeriodSummary( + scope, + { + ok: true, + flowStep: { + connectorId: "gst", + scopeId: filedReturnsScopeId(scope.returnType), + state: "downloaded", + safeSignals: ["target-period-verified"], + safeMessage: "Pack saved the selected filed return.", + downloadDiagnostic: { + actionId, + artifactType: "PDF", + byteCountClass: "non-empty", + downloadId: 91, + downloadPathClass: "captured-portal-request-unknown", + endpointClass: "gstr3b-portal-blob-captured-download", + eventType: "filed-return-download-path", + financialYear: scope.financialYear, + mimeClass: "pdf", + period: scope.period, + returnType: scope.returnType, + schemaVersion: "1.0", + status: "downloaded", + }, + }, + }, + deps, + true, + ); + + expect(response).toMatchObject({ + flowSummary: { + flowStep: { downloadDiagnostic: { downloadId: 91 } }, + status: "complete", + }, + }); + expect(storage.session[checkpointKey]).toBeUndefined(); + }); + it("persists a restart-safe GSTR-1 Return Dashboard navigation failure", async () => { const response = await withPersistedSinglePeriodSummary( { From a716b4b059844d567bc4b454456e3861e74e59f2 Mon Sep 17 00:00:00 2001 From: Tapish Khandelwal Date: Wed, 5 Aug 2026 15:59:32 +0530 Subject: [PATCH 2/6] fix(recovery): clear proven JSON checkpoint --- ...filed-returns-download-diagnostic-state.ts | 1 + .../filed-returns-download-diagnostics.ts | 4 +- src/connectors/gst/filed-returns-contracts.ts | 1 + ...turns-download-diagnostic-compatibility.ts | 5 +- src/connectors/gst/portal-blob-shim.ts | 18 ++- ...filed-returns-download-diagnostics.test.ts | 48 ++++++++ ...turns-download-trigger-acquisition.test.ts | 1 + ...led-returns-session-write-boundary.test.ts | 104 ++++++++++++++++++ .../gstr3b-artifact-acquisition.test.ts | 8 +- tests/connectors/portal-blob-shim.test.ts | 25 +++-- 10 files changed, 202 insertions(+), 13 deletions(-) diff --git a/src/background/filed-returns-download-diagnostic-state.ts b/src/background/filed-returns-download-diagnostic-state.ts index 063a816e..3fb4583f 100644 --- a/src/background/filed-returns-download-diagnostic-state.ts +++ b/src/background/filed-returns-download-diagnostic-state.ts @@ -29,6 +29,7 @@ const DOWNLOAD_DIAGNOSTIC_KEYS = [ const VALID_ENDPOINT_CLASSES = new Set([ "gstr3b-portal-rendered-download", "gstr3b-portal-blob-captured-download", + "gstr3b-main-world-json-captured-download", "gstr3b-browser-managed-direct-download", "gstr1-pdf-portal-rendered-download", "gstr1-excel-portal-rendered-download", diff --git a/src/background/filed-returns-download-diagnostics.ts b/src/background/filed-returns-download-diagnostics.ts index 5cb4d26c..28906283 100644 --- a/src/background/filed-returns-download-diagnostics.ts +++ b/src/background/filed-returns-download-diagnostics.ts @@ -49,7 +49,9 @@ function endpointClassForTarget( attemptClass: DownloadAttemptClass, ): FiledReturnsDownloadEndpointClass { if (target.returnType === "GSTR-3B" && attemptClass === "captured-portal-request") { - return "gstr3b-portal-blob-captured-download"; + return target.artifactType === "JSON" + ? "gstr3b-main-world-json-captured-download" + : "gstr3b-portal-blob-captured-download"; } if (target.returnType === "GSTR-3B" && attemptClass === "extension-direct") { return "gstr3b-browser-managed-direct-download"; diff --git a/src/connectors/gst/filed-returns-contracts.ts b/src/connectors/gst/filed-returns-contracts.ts index 88dff083..e84df06b 100644 --- a/src/connectors/gst/filed-returns-contracts.ts +++ b/src/connectors/gst/filed-returns-contracts.ts @@ -138,6 +138,7 @@ export interface FiledReturnsArtifactAcquisitionCompletion { export type FiledReturnsDownloadEndpointClass = | "gstr3b-portal-rendered-download" | "gstr3b-portal-blob-captured-download" + | "gstr3b-main-world-json-captured-download" | "gstr3b-browser-managed-direct-download" | "gstr1-pdf-portal-rendered-download" | "gstr1-excel-portal-rendered-download" diff --git a/src/connectors/gst/filed-returns-download-diagnostic-compatibility.ts b/src/connectors/gst/filed-returns-download-diagnostic-compatibility.ts index fef92a2f..2c5d7f6e 100644 --- a/src/connectors/gst/filed-returns-download-diagnostic-compatibility.ts +++ b/src/connectors/gst/filed-returns-download-diagnostic-compatibility.ts @@ -18,6 +18,9 @@ export function isFiledReturnsEndpointClassForArtifact( endpointClass === "gstr3b-browser-managed-direct-download" ); } + if (returnType === "GSTR-3B" && artifactType === "JSON") { + return endpointClass === "gstr3b-main-world-json-captured-download"; + } if (returnType === "GSTR-1" && artifactType === "PDF") { return ( endpointClass === "gstr1-pdf-portal-rendered-download" || @@ -49,7 +52,7 @@ export function isFiledReturnsEndpointPathPair( if (downloadPathClass.startsWith("extension-direct-")) { return endpointClass === "gstr3b-browser-managed-direct-download"; } - return endpointClass.includes("portal-blob-captured-download") + return endpointClass.includes("captured-download") ? downloadPathClass.startsWith("captured-portal-request-") : endpointClass.includes("portal-rendered-download") && downloadPathClass.startsWith("portal-click-"); diff --git a/src/connectors/gst/portal-blob-shim.ts b/src/connectors/gst/portal-blob-shim.ts index 0e1a1c43..56be3bfb 100644 --- a/src/connectors/gst/portal-blob-shim.ts +++ b/src/connectors/gst/portal-blob-shim.ts @@ -26,6 +26,22 @@ export function capturePortalPdfBlob(input: PortalBlobShimInput): Promise { + const prefix = + returnType === "GSTR-1" + ? "filed-gstr1" + : returnType === "GSTR-2B" + ? "filed-gstr2b" + : returnType === "GSTR-3B" + ? "filed-gstr3b" + : null; + return prefix + ? `${prefix}-portal-blob-download-captured` + : `portal-blob-shim-suppressed-via-${method}`; + }; const anchor = HTMLAnchorElement.prototype; const originalDispatch = anchor.dispatchEvent; const originalClick = anchor.click; @@ -60,7 +76,7 @@ export function capturePortalPdfBlob(input: PortalBlobShimInput): Promise finish({ ok: false, reason: "unexpected-content", safeSignals: [] }), ); diff --git a/tests/background/filed-returns-download-diagnostics.test.ts b/tests/background/filed-returns-download-diagnostics.test.ts index b1c15a7f..012adf31 100644 --- a/tests/background/filed-returns-download-diagnostics.test.ts +++ b/tests/background/filed-returns-download-diagnostics.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; +import { isFiledReturnsEndpointClassForArtifact } from "../../src/connectors/gst/filed-returns-download-diagnostic-compatibility"; import { withFiledReturnsDownloadDiagnostic } from "../../src/background/filed-returns-download-diagnostics"; describe("filed-return download diagnostics", () => { @@ -29,4 +30,51 @@ describe("filed-return download diagnostics", () => { expect(result.downloadDiagnostic?.downloadId).toBe(0); }); + + it("labels a GSTR-3B JSON MAIN-world capture precisely and rejects PDF-only paths", () => { + const result = withFiledReturnsDownloadDiagnostic({ + attemptClass: "captured-portal-request", + flowStep: { + connectorId: "gst", + scopeId: "gst-filed-returns-gstr3b-pdf-private-v0", + state: "downloaded", + safeSignals: ["target-period-verified"], + safeMessage: "The target download completed.", + }, + safeEvidence: { + downloadId: 3, + urlClass: "unknown", + mimeClass: "json", + byteCountClass: "non-empty", + }, + target: { + actionId: "00000000-0000-4000-8000-000000000003", + financialYear: "2025-26", + period: "May", + returnType: "GSTR-3B", + artifactType: "JSON", + }, + }); + + expect(result.downloadDiagnostic?.endpointClass).toBe( + "gstr3b-main-world-json-captured-download", + ); + expect( + isFiledReturnsEndpointClassForArtifact( + "gstr3b-main-world-json-captured-download", + "GSTR-3B", + "JSON", + ), + ).toBe(true); + expect( + isFiledReturnsEndpointClassForArtifact("gstr3b-portal-rendered-download", "GSTR-3B", "JSON"), + ).toBe(false); + expect( + isFiledReturnsEndpointClassForArtifact( + "gstr3b-browser-managed-direct-download", + "GSTR-3B", + "JSON", + ), + ).toBe(false); + }); }); diff --git a/tests/background/filed-returns-download-trigger-acquisition.test.ts b/tests/background/filed-returns-download-trigger-acquisition.test.ts index c2436dd9..c328dde8 100644 --- a/tests/background/filed-returns-download-trigger-acquisition.test.ts +++ b/tests/background/filed-returns-download-trigger-acquisition.test.ts @@ -268,6 +268,7 @@ describe("GSTR-3B artifact acquisition dispatch", () => { artifactType: "JSON", byteCountClass: "non-empty", downloadId: 91, + endpointClass: "gstr3b-main-world-json-captured-download", mimeClass: "json", status: "downloaded", }, diff --git a/tests/background/filed-returns-session-write-boundary.test.ts b/tests/background/filed-returns-session-write-boundary.test.ts index a0858c62..7496155b 100644 --- a/tests/background/filed-returns-session-write-boundary.test.ts +++ b/tests/background/filed-returns-session-write-boundary.test.ts @@ -148,6 +148,110 @@ describe("filed-return session write boundary", () => { expect(storage.session[checkpointKey]).toBeUndefined(); }); + it("persists a portal-blob captured completion before clearing its checkpoint", async () => { + const scope = { + artifactType: "PDF" as const, + financialYear: "2026-27", + period: "April", + returnType: "GSTR-3B" as const, + }; + const actionId = "00000000-0000-4000-8000-000000000092"; + const checkpointKey = artifactAcquisitionCheckpointKey(scope); + storage.session[checkpointKey] = { + ...scope, + armedAt: "2026-08-05T08:00:00.000Z", + downloadId: 92, + requestId: actionId, + state: "download-observing", + }; + + const response = await withPersistedSinglePeriodSummary( + scope, + { + ok: true, + flowStep: { + connectorId: "gst", + scopeId: filedReturnsScopeId(scope.returnType), + state: "downloaded", + safeSignals: ["filed-gstr3b-portal-blob-download-captured"], + safeMessage: "Pack saved the selected filed return.", + downloadDiagnostic: { + actionId, + artifactType: "PDF", + byteCountClass: "non-empty", + downloadId: 92, + downloadPathClass: "captured-portal-request-unknown", + endpointClass: "gstr3b-portal-blob-captured-download", + eventType: "filed-return-download-path", + financialYear: scope.financialYear, + mimeClass: "pdf", + period: scope.period, + returnType: scope.returnType, + schemaVersion: "1.0", + status: "downloaded", + }, + }, + }, + deps, + true, + ); + + expect(response).toMatchObject({ flowSummary: { status: "complete" } }); + expect(storage.session[checkpointKey]).toBeUndefined(); + }); + + it("persists a GSTR-3B JSON capture before clearing its exact-ID checkpoint", async () => { + const scope = { + artifactType: "JSON" as const, + financialYear: "2026-27", + period: "April", + returnType: "GSTR-3B" as const, + }; + const actionId = "00000000-0000-4000-8000-000000000093"; + const checkpointKey = artifactAcquisitionCheckpointKey(scope); + storage.session[checkpointKey] = { + ...scope, + armedAt: "2026-08-05T08:00:00.000Z", + downloadId: 93, + requestId: actionId, + state: "download-observing", + }; + + const response = await withPersistedSinglePeriodSummary( + scope, + { + ok: true, + flowStep: { + connectorId: "gst", + scopeId: filedReturnsScopeId(scope.returnType), + state: "downloaded", + safeSignals: ["target-period-verified"], + safeMessage: "Pack saved the portal-produced GSTR-3B data JSON.", + downloadDiagnostic: { + actionId, + artifactType: "JSON", + byteCountClass: "non-empty", + downloadId: 93, + downloadPathClass: "captured-portal-request-unknown", + endpointClass: "gstr3b-main-world-json-captured-download", + eventType: "filed-return-download-path", + financialYear: scope.financialYear, + mimeClass: "json", + period: scope.period, + returnType: scope.returnType, + schemaVersion: "1.0", + status: "downloaded", + }, + }, + }, + deps, + true, + ); + + expect(response).toMatchObject({ flowSummary: { status: "complete" } }); + expect(storage.session[checkpointKey]).toBeUndefined(); + }); + it("persists a restart-safe GSTR-1 Return Dashboard navigation failure", async () => { const response = await withPersistedSinglePeriodSummary( { diff --git a/tests/background/gstr3b-artifact-acquisition.test.ts b/tests/background/gstr3b-artifact-acquisition.test.ts index 8b30e9bc..56c8cb0e 100644 --- a/tests/background/gstr3b-artifact-acquisition.test.ts +++ b/tests/background/gstr3b-artifact-acquisition.test.ts @@ -31,7 +31,7 @@ describe("GSTR-3B page-generated acquisition", () => { ok: true, base64: Buffer.from(bytes).toString("base64"), blobUrl: "blob:synthetic/gstr3b", - safeSignals: ["portal-blob-shim-suppressed-via-dispatchEvent"], + safeSignals: ["filed-gstr3b-portal-blob-download-captured"], }, }, ]); @@ -50,7 +50,11 @@ describe("GSTR-3B page-generated acquisition", () => { returnPeriod: "042024", tabId: 17, }), - ).resolves.toMatchObject({ ok: true }); + ).resolves.toEqual({ + downloadId: 9, + ok: true, + safeSignals: ["filed-gstr3b-portal-blob-download-captured"], + }); expect(mocks.executeScript).toHaveBeenCalledWith( expect.objectContaining({ args: [ diff --git a/tests/connectors/portal-blob-shim.test.ts b/tests/connectors/portal-blob-shim.test.ts index 48df7c31..ed069c57 100644 --- a/tests/connectors/portal-blob-shim.test.ts +++ b/tests/connectors/portal-blob-shim.test.ts @@ -42,7 +42,10 @@ describe("capturePortalPdfBlob", () => { controlSelector: "button", expectedMime: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", }), - ).resolves.toMatchObject({ ok: true, safeSignals: ["portal-blob-shim-suppressed-via-click"] }); + ).resolves.toMatchObject({ + ok: true, + safeSignals: ["portal-blob-shim-suppressed-via-click"], + }); }); it("does not suppress an unrelated anchor and ignores non-matching blobs", async () => { @@ -107,7 +110,10 @@ describe("capturePortalPdfBlob", () => { expectedMime: "application/pdf", expectedTarget: { financialYear: "2024-25", period: "April", returnType: "GSTR-3B" }, }), - ).resolves.toMatchObject({ ok: true, safeSignals: ["portal-blob-shim-suppressed-via-click"] }); + ).resolves.toMatchObject({ + ok: true, + safeSignals: ["filed-gstr3b-portal-blob-download-captured"], + }); }); it("keeps the GSTR-1-style local scope guard despite matching page-wide decoy text", async () => { @@ -135,7 +141,7 @@ describe("capturePortalPdfBlob", () => { await expect(captureGstr2b()).resolves.toMatchObject({ ok: true, - safeSignals: ["portal-blob-shim-suppressed-via-click"], + safeSignals: ["filed-gstr2b-portal-blob-download-captured"], }); expect(gstr2b.alternateClick).not.toHaveBeenCalled(); }); @@ -146,7 +152,7 @@ describe("capturePortalPdfBlob", () => { await expect(captureGstr2b(["September", "Sep", "Sept"])).resolves.toMatchObject({ ok: true, - safeSignals: ["portal-blob-shim-suppressed-via-click"], + safeSignals: ["filed-gstr2b-portal-blob-download-captured"], }); }); @@ -156,7 +162,7 @@ describe("capturePortalPdfBlob", () => { await expect(captureGstr2b()).resolves.toMatchObject({ ok: true, - safeSignals: ["portal-blob-shim-suppressed-via-click"], + safeSignals: ["filed-gstr2b-portal-blob-download-captured"], }); }); @@ -331,7 +337,7 @@ describe("capturePortalPdfBlob", () => { await expect(captureGstr2b()).resolves.toMatchObject({ ok: true, - safeSignals: ["portal-blob-shim-suppressed-via-click"], + safeSignals: ["filed-gstr2b-portal-blob-download-captured"], }); }); @@ -352,7 +358,10 @@ describe("capturePortalPdfBlob", () => { expectedMime: "application/pdf", expectedTarget: { financialYear: "2024-25", period: "April", returnType: "GSTR-3B" }, }), - ).resolves.toMatchObject({ ok: true, safeSignals: ["portal-blob-shim-suppressed-via-click"] }); + ).resolves.toMatchObject({ + ok: true, + safeSignals: ["filed-gstr3b-portal-blob-download-captured"], + }); }); it("keeps the GSTR-2B scope proof intact across Chrome's serialized MAIN-world boundary", async () => { @@ -364,7 +373,7 @@ describe("capturePortalPdfBlob", () => { executeInMainWorld({ ...gstr2bInput, expectedPeriodTexts: ["April", "Apr"] }), ).resolves.toMatchObject({ ok: true, - safeSignals: ["portal-blob-shim-suppressed-via-click"], + safeSignals: ["filed-gstr2b-portal-blob-download-captured"], }); }); From 844c10478cd3e741ee2febf11b93b9720994db66 Mon Sep 17 00:00:00 2001 From: Tapish Khandelwal Date: Wed, 5 Aug 2026 16:27:34 +0530 Subject: [PATCH 3/6] fix(recovery): retain gstr2b JSON download proof --- scripts/create-live-run-evidence-template.mjs | 27 ++------ scripts/lib/live-run-evidence-types.ts | 2 + scripts/lib/live-run-evidence.ts | 23 ++----- ...filed-returns-download-diagnostic-state.ts | 1 + .../filed-returns-download-trigger.ts | 32 +++++----- src/connectors/gst/filed-returns-contracts.ts | 1 + ...turns-download-diagnostic-compatibility.ts | 5 +- ...filed-returns-download-diagnostics.test.ts | 24 +++++++ ...turns-download-trigger-acquisition.test.ts | 37 ++++++++--- ...led-returns-session-write-boundary.test.ts | 57 ++++++++++++++++- ...eturns-single-period-bundle-ledger.test.ts | 4 +- ...d-returns-target-download-recovery.test.ts | 5 +- .../filed-returns-target-review.test.ts | 5 +- .../full-fiscal-year-ledger.test.ts | 9 ++- tests/core/live-run-evidence.test.ts | 51 +++++++-------- .../create-live-run-evidence-template.test.ts | 64 +++++++++---------- 16 files changed, 215 insertions(+), 132 deletions(-) diff --git a/scripts/create-live-run-evidence-template.mjs b/scripts/create-live-run-evidence-template.mjs index b23b8d56..14bf9779 100644 --- a/scripts/create-live-run-evidence-template.mjs +++ b/scripts/create-live-run-evidence-template.mjs @@ -66,19 +66,6 @@ try { if (!supportsFiledReturnsArtifactType(returnType, artifactType)) { throw new Error("--artifact-type is not supported for --return-type."); } - if (artifactType === "JSON" && outcome === "pass") { - // Refuse rather than emit evidence nothing can back. A standalone JSON - // selection is acquired by a direct authenticated same-origin fetch whose - // success flow step attaches no download diagnostic, for GSTR-3B and - // GSTR-2B alike. Only JSON captured as part of an all-formats selection is - // staged and retains one. An earlier revision of this guard covered GSTR-3B - // only, on the reasoning that the canonical endpoint-class rule rejects that - // pairing while admitting GSTR-2B — but the schema admitting a class is not - // the runtime producing it, and it is the runtime that has to back a claim. - throw new Error( - "Passing standalone JSON evidence is not supported: the direct JSON fetch path retains no download diagnostic, so nothing can back it. Record this run as blocked, or capture JSON as part of an all-formats selection.", - ); - } if (scenario === "full-year" && period !== "FULL_FISCAL_YEAR") { throw new Error("Full-year evidence must use --period FULL_FISCAL_YEAR."); } @@ -334,19 +321,17 @@ function collectLimitations(input, { checks, outcome, profile, scenario }) { } function defaultEndpointClass(returnType, artifactType) { - // GSTR-3B portal data (JSON) has no endpoint class the runtime can back: its - // direct same-origin fetch path attaches no download diagnostic, and the - // canonical `isFiledReturnsEndpointClassForArtifact` admits no GSTR-3B/JSON - // pairing. `unknown` is the only truthful default; naming a blob-captured - // class let the generator emit passing evidence nothing could support. - // GSTR-2B JSON is different and is left alone — the canonical rule does admit - // it with the GSTR-2B blob-captured class. - if (returnType === "GSTR-3B" && artifactType === "JSON") return "unknown"; + if (returnType === "GSTR-3B" && artifactType === "JSON") { + return "gstr3b-main-world-json-captured-download"; + } if (returnType === "GSTR-3B") return "gstr3b-portal-blob-captured-download"; if (returnType === "GSTR-1" && artifactType === "EXCEL") { return "gstr1-excel-portal-blob-captured-download"; } if (returnType === "GSTR-1") return "gstr1-pdf-portal-blob-captured-download"; + if (returnType === "GSTR-2B" && artifactType === "JSON") { + return "gstr2b-main-world-json-captured-download"; + } if (returnType === "GSTR-2B") return "gstr2b-portal-blob-captured-download"; return "unknown"; } diff --git a/scripts/lib/live-run-evidence-types.ts b/scripts/lib/live-run-evidence-types.ts index f37164f3..8468756d 100644 --- a/scripts/lib/live-run-evidence-types.ts +++ b/scripts/lib/live-run-evidence-types.ts @@ -15,11 +15,13 @@ export type LiveRunDownloadPathClass = export type LiveRunEndpointClass = | "gstr3b-portal-rendered-download" | "gstr3b-portal-blob-captured-download" + | "gstr3b-main-world-json-captured-download" | "gstr1-pdf-portal-rendered-download" | "gstr1-excel-portal-rendered-download" | "gstr1-pdf-portal-blob-captured-download" | "gstr1-excel-portal-blob-captured-download" | "gstr2b-portal-blob-captured-download" + | "gstr2b-main-world-json-captured-download" | "filed-return-portal-rendered-download" | "unknown"; export type LiveRunEvidenceLimitation = diff --git a/scripts/lib/live-run-evidence.ts b/scripts/lib/live-run-evidence.ts index 720b739f..2595f7eb 100644 --- a/scripts/lib/live-run-evidence.ts +++ b/scripts/lib/live-run-evidence.ts @@ -122,11 +122,13 @@ const DOWNLOAD_EVIDENCE_KEYS = [ const DOWNLOAD_ENDPOINT_CLASSES = [ "gstr3b-portal-rendered-download", "gstr3b-portal-blob-captured-download", + "gstr3b-main-world-json-captured-download", "gstr1-pdf-portal-rendered-download", "gstr1-excel-portal-rendered-download", "gstr1-pdf-portal-blob-captured-download", "gstr1-excel-portal-blob-captured-download", "gstr2b-portal-blob-captured-download", + "gstr2b-main-world-json-captured-download", "filed-return-portal-rendered-download", "unknown", ] as const; @@ -208,18 +210,6 @@ export function validateLiveRunEvidence(input: unknown): LiveRunEvidenceValidati "artifactType", errors, ); - // A standalone JSON selection is acquired by the direct same-origin fetch in - // filed-returns-download-trigger.ts, whose success flow step carries no - // downloadDiagnostic at all — for either return type. Nothing the runtime - // retains can back a passing claim about it, so refuse rather than certify. - // JSON acquired as part of an all-formats selection is staged and does retain - // a diagnostic, so those rows stay valid; only the standalone selection is - // unbackable. - if (input.artifactType === "JSON" && input.outcome === "pass") { - errors.push( - "artifactType JSON cannot record a pass outcome: the standalone JSON path retains no download diagnostic, so no evidence can back it. Record the run as blocked, or capture JSON as part of an all-formats selection.", - ); - } requirePattern(input.financialYear, FINANCIAL_YEAR, "financialYear", errors); requireOneOf(input.period, PERIODS, "period", errors); requireOneOf(input.scenario, ["single-period", "full-year"], "scenario", errors); @@ -442,13 +432,8 @@ function validateDownloadEndpointPathConsistency( } function isSupportedLiveRunEvidenceEndpoint(entry: LiveRunDownloadEvidence): boolean { - // Defer entirely to the canonical predicate. The exception that used to sit - // here accepted `gstr3b-portal-blob-captured-download` for GSTR-3B JSON, a - // pairing the canonical rule rejects and the runtime never produces: the - // direct JSON fetch path attaches no diagnostic at all. Evidence for a JSON - // artifact therefore carries `unknown`, which the canonical predicate already - // admits. A local exception here could only ever certify something the - // runtime cannot back. + // Defer entirely to the canonical predicate so evidence can only describe a + // return/artifact acquisition path the runtime itself produces. return isFiledReturnsEndpointClassForArtifact( entry.endpointClass, entry.returnType, diff --git a/src/background/filed-returns-download-diagnostic-state.ts b/src/background/filed-returns-download-diagnostic-state.ts index 3fb4583f..79774c6c 100644 --- a/src/background/filed-returns-download-diagnostic-state.ts +++ b/src/background/filed-returns-download-diagnostic-state.ts @@ -36,6 +36,7 @@ const VALID_ENDPOINT_CLASSES = new Set([ "gstr1-pdf-portal-blob-captured-download", "gstr1-excel-portal-blob-captured-download", "gstr2b-portal-blob-captured-download", + "gstr2b-main-world-json-captured-download", "filed-return-portal-rendered-download", "unknown", ]); diff --git a/src/background/filed-returns-download-trigger.ts b/src/background/filed-returns-download-trigger.ts index 966c34c0..75ed2f3b 100644 --- a/src/background/filed-returns-download-trigger.ts +++ b/src/background/filed-returns-download-trigger.ts @@ -493,22 +493,18 @@ async function triggerPageGeneratedSinglePeriodArtifact( const acquired = returnType === "GSTR-2B" && artifactType === "JSON" && artifact.state === "ready" ? await acquireFiledReturnJsonInMainWorld({ - ...(deps.stageCapturedDownloads - ? { - deliver: ({ base64, mimeType }) => - deliverValidatedArtifact({ - artifactType, - base64, - callbacks, - deps, - filename: artifactFilename(scope, artifactType), - mimeType, - requestId, - returnType, - scope, - }), - } - : {}), + deliver: ({ base64, mimeType }) => + deliverValidatedArtifact({ + artifactType, + base64, + callbacks, + deps, + filename: artifactFilename(scope, artifactType), + mimeType, + requestId, + returnType, + scope, + }), filename: artifactFilename(scope, "JSON"), onStartCheckpointFailed: callbacks.onStartCheckpointFailed, onStarted: callbacks.onStarted, @@ -745,7 +741,9 @@ function capturedArtifactDiagnostic( : "spreadsheet"; const endpointClass = scope.returnType === "GSTR-2B" - ? "gstr2b-portal-blob-captured-download" + ? artifactType === "JSON" + ? "gstr2b-main-world-json-captured-download" + : "gstr2b-portal-blob-captured-download" : artifactType === "EXCEL" ? "gstr1-excel-portal-blob-captured-download" : "gstr1-pdf-portal-blob-captured-download"; diff --git a/src/connectors/gst/filed-returns-contracts.ts b/src/connectors/gst/filed-returns-contracts.ts index e84df06b..cf1fc385 100644 --- a/src/connectors/gst/filed-returns-contracts.ts +++ b/src/connectors/gst/filed-returns-contracts.ts @@ -145,6 +145,7 @@ export type FiledReturnsDownloadEndpointClass = | "gstr1-pdf-portal-blob-captured-download" | "gstr1-excel-portal-blob-captured-download" | "gstr2b-portal-blob-captured-download" + | "gstr2b-main-world-json-captured-download" | "filed-return-portal-rendered-download" | "unknown"; diff --git a/src/connectors/gst/filed-returns-download-diagnostic-compatibility.ts b/src/connectors/gst/filed-returns-download-diagnostic-compatibility.ts index 2c5d7f6e..abac1cce 100644 --- a/src/connectors/gst/filed-returns-download-diagnostic-compatibility.ts +++ b/src/connectors/gst/filed-returns-download-diagnostic-compatibility.ts @@ -33,9 +33,12 @@ export function isFiledReturnsEndpointClassForArtifact( endpointClass === "gstr1-excel-portal-blob-captured-download" ); } + if (returnType === "GSTR-2B" && artifactType === "JSON") { + return endpointClass === "gstr2b-main-world-json-captured-download"; + } return ( returnType === "GSTR-2B" && - (artifactType === "PDF" || artifactType === "JSON" || artifactType === "EXCEL") && + (artifactType === "PDF" || artifactType === "EXCEL") && (endpointClass === "filed-return-portal-rendered-download" || endpointClass === "gstr2b-portal-blob-captured-download") ); diff --git a/tests/background/filed-returns-download-diagnostics.test.ts b/tests/background/filed-returns-download-diagnostics.test.ts index 012adf31..a163701b 100644 --- a/tests/background/filed-returns-download-diagnostics.test.ts +++ b/tests/background/filed-returns-download-diagnostics.test.ts @@ -77,4 +77,28 @@ describe("filed-return download diagnostics", () => { ), ).toBe(false); }); + + it("accepts GSTR-2B JSON only through its MAIN-world capture class", () => { + expect( + isFiledReturnsEndpointClassForArtifact( + "gstr2b-main-world-json-captured-download", + "GSTR-2B", + "JSON", + ), + ).toBe(true); + expect( + isFiledReturnsEndpointClassForArtifact( + "gstr2b-portal-blob-captured-download", + "GSTR-2B", + "JSON", + ), + ).toBe(false); + expect( + isFiledReturnsEndpointClassForArtifact( + "filed-return-portal-rendered-download", + "GSTR-2B", + "JSON", + ), + ).toBe(false); + }); }); diff --git a/tests/background/filed-returns-download-trigger-acquisition.test.ts b/tests/background/filed-returns-download-trigger-acquisition.test.ts index c328dde8..7744af8a 100644 --- a/tests/background/filed-returns-download-trigger-acquisition.test.ts +++ b/tests/background/filed-returns-download-trigger-acquisition.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it, vi } from "vitest"; import type { PackMessageResponse } from "../../src/connectors/gst/messages"; +import type { acquireFiledReturnJsonInMainWorld } from "../../src/background/filed-returns-json-acquisition"; + +type JsonAcquisitionInput = Parameters[0]; const captureMocks = vi.hoisted(() => ({ acquireGstr3bPdfAfterPreflight: vi.fn(async () => ({ @@ -13,12 +16,16 @@ const captureMocks = vi.hoisted(() => ({ mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", safeSignals: [] as string[], })), - acquireFiledReturnJsonInMainWorld: vi.fn(async () => ({ - downloadId: 91, - ok: true as const, - safeMessage: undefined as string | undefined, - safeSignals: [] as string[], - })), + acquireFiledReturnJsonInMainWorld: vi.fn(async (input: JsonAcquisitionInput) => + input.deliver + ? input.deliver({ base64: "e30=", mimeType: "application/json" }) + : { + downloadId: 91, + ok: true as const, + safeMessage: undefined as string | undefined, + safeSignals: [] as string[], + }, + ), downloadAcquiredArtifact: vi.fn(async () => ({ ok: true as const, downloadId: 91, @@ -540,7 +547,7 @@ describe("GSTR-2B artifact acquisition dispatch", () => { ); it("writes GSTR-2B portal data with its data suffix", async () => { - await triggerAndObserveFiledReturnDownload({ + const response = await triggerAndObserveFiledReturnDownload({ activePeriod: "June", artifactType: "JSON", deps: { @@ -563,8 +570,22 @@ describe("GSTR-2B artifact acquisition dispatch", () => { }); expect(captureMocks.acquireFiledReturnJsonInMainWorld).toHaveBeenCalledWith( - expect.objectContaining({ filename: "ComplyEaze-Pack/2026-27/GSTR-2B/June-data.json" }), + expect.objectContaining({ + deliver: expect.any(Function), + filename: "ComplyEaze-Pack/2026-27/GSTR-2B/June-data.json", + }), ); + expect(response).toMatchObject({ + flowStep: { + downloadDiagnostic: { + artifactType: "JSON", + downloadId: 91, + endpointClass: "gstr2b-main-world-json-captured-download", + mimeClass: "json", + }, + state: "downloaded", + }, + }); }); it("keeps GSTR-2B JSON inside a selected-file staging handoff", async () => { diff --git a/tests/background/filed-returns-session-write-boundary.test.ts b/tests/background/filed-returns-session-write-boundary.test.ts index 7496155b..5a68bf6c 100644 --- a/tests/background/filed-returns-session-write-boundary.test.ts +++ b/tests/background/filed-returns-session-write-boundary.test.ts @@ -252,6 +252,58 @@ describe("filed-return session write boundary", () => { expect(storage.session[checkpointKey]).toBeUndefined(); }); + it("persists a GSTR-2B JSON capture before clearing its exact-ID checkpoint", async () => { + const scope = { + artifactType: "JSON" as const, + financialYear: "2026-27", + period: "April", + returnType: "GSTR-2B" as const, + }; + const actionId = "00000000-0000-4000-8000-000000000094"; + const checkpointKey = artifactAcquisitionCheckpointKey(scope); + storage.session[checkpointKey] = { + ...scope, + armedAt: "2026-08-05T08:00:00.000Z", + downloadId: 94, + requestId: actionId, + state: "download-observing", + }; + + const response = await withPersistedSinglePeriodSummary( + scope, + { + ok: true, + flowStep: { + connectorId: "gst", + scopeId: filedReturnsScopeId(scope.returnType), + state: "downloaded", + safeSignals: ["target-period-verified"], + safeMessage: "Pack saved the portal-produced GSTR-2B data JSON.", + downloadDiagnostic: { + actionId, + artifactType: "JSON", + byteCountClass: "non-empty", + downloadId: 94, + downloadPathClass: "captured-portal-request-unknown", + endpointClass: "gstr2b-main-world-json-captured-download", + eventType: "filed-return-download-path", + financialYear: scope.financialYear, + mimeClass: "json", + period: scope.period, + returnType: scope.returnType, + schemaVersion: "1.0", + status: "downloaded", + }, + }, + }, + deps, + true, + ); + + expect(response).toMatchObject({ flowSummary: { status: "complete" } }); + expect(storage.session[checkpointKey]).toBeUndefined(); + }); + it("persists a restart-safe GSTR-1 Return Dashboard navigation failure", async () => { const response = await withPersistedSinglePeriodSummary( { @@ -820,7 +872,10 @@ function selectedArtifactDiagnostic(artifactType: "PDF" | "EXCEL" | "JSON", acti artifactType, byteCountClass: "non-empty", downloadPathClass: "captured-portal-request-data", - endpointClass: "gstr2b-portal-blob-captured-download", + endpointClass: + artifactType === "JSON" + ? "gstr2b-main-world-json-captured-download" + : "gstr2b-portal-blob-captured-download", financialYear: "2025-26", mimeClass: artifactType === "PDF" ? "pdf" : artifactType === "JSON" ? "json" : "spreadsheet", period: "May", diff --git a/tests/background/filed-returns-single-period-bundle-ledger.test.ts b/tests/background/filed-returns-single-period-bundle-ledger.test.ts index 0a01cde0..34923f1e 100644 --- a/tests/background/filed-returns-single-period-bundle-ledger.test.ts +++ b/tests/background/filed-returns-single-period-bundle-ledger.test.ts @@ -535,7 +535,9 @@ function diagnostic( ? artifactType === "PDF" ? "gstr1-pdf-portal-blob-captured-download" : "gstr1-excel-portal-blob-captured-download" - : "gstr2b-portal-blob-captured-download", + : artifactType === "JSON" + ? "gstr2b-main-world-json-captured-download" + : "gstr2b-portal-blob-captured-download", eventType: "filed-return-download-path", financialYear: scope.financialYear, mimeClass: artifactType === "PDF" ? "pdf" : artifactType === "JSON" ? "json" : "spreadsheet", diff --git a/tests/background/filed-returns-target-download-recovery.test.ts b/tests/background/filed-returns-target-download-recovery.test.ts index c4586343..784ca1bd 100644 --- a/tests/background/filed-returns-target-download-recovery.test.ts +++ b/tests/background/filed-returns-target-download-recovery.test.ts @@ -1089,7 +1089,10 @@ function bundleDiagnostic(artifactType: "PDF" | "JSON" | "EXCEL"): FiledReturnsD artifactType, byteCountClass: "non-empty", downloadPathClass: "captured-portal-request-data", - endpointClass: "gstr2b-portal-blob-captured-download", + endpointClass: + artifactType === "JSON" + ? "gstr2b-main-world-json-captured-download" + : "gstr2b-portal-blob-captured-download", eventType: "filed-return-download-path", financialYear: ZIP_SCOPE.financialYear, mimeClass: artifactType === "PDF" ? "pdf" : artifactType === "JSON" ? "json" : "spreadsheet", diff --git a/tests/background/filed-returns-target-review.test.ts b/tests/background/filed-returns-target-review.test.ts index fa1d5210..295606f5 100644 --- a/tests/background/filed-returns-target-review.test.ts +++ b/tests/background/filed-returns-target-review.test.ts @@ -1677,7 +1677,10 @@ function diagnostic( returnType: "GSTR-2B", financialYear: "2025-26", period: "March", - endpointClass: "gstr2b-portal-blob-captured-download", + endpointClass: + artifactType === "JSON" + ? "gstr2b-main-world-json-captured-download" + : "gstr2b-portal-blob-captured-download", artifactType, downloadPathClass: "captured-portal-request-blob", downloadId: artifactType === "PDF" ? 41 : artifactType === "JSON" ? 43 : 42, diff --git a/tests/background/full-fiscal-year-ledger.test.ts b/tests/background/full-fiscal-year-ledger.test.ts index d9c280d3..5d6f05e3 100644 --- a/tests/background/full-fiscal-year-ledger.test.ts +++ b/tests/background/full-fiscal-year-ledger.test.ts @@ -931,7 +931,9 @@ function positiveTargetEvidence( returnType === "GSTR-3B" ? ("gstr3b-portal-blob-captured-download" as const) : returnType === "GSTR-2B" - ? ("gstr2b-portal-blob-captured-download" as const) + ? concreteArtifactType === "JSON" + ? ("gstr2b-main-world-json-captured-download" as const) + : ("gstr2b-portal-blob-captured-download" as const) : concreteArtifactType === "PDF" ? ("gstr1-pdf-portal-blob-captured-download" as const) : ("gstr1-excel-portal-blob-captured-download" as const), @@ -974,7 +976,10 @@ function targetDiagnostic( returnType: "GSTR-2B", financialYear: "2026-27", period: "April", - endpointClass: "gstr2b-portal-blob-captured-download", + endpointClass: + artifactType === "JSON" + ? "gstr2b-main-world-json-captured-download" + : "gstr2b-portal-blob-captured-download", artifactType, downloadPathClass: "captured-portal-request-blob", downloadId: artifactType === "PDF" ? 41 : artifactType === "EXCEL" ? 42 : 43, diff --git a/tests/core/live-run-evidence.test.ts b/tests/core/live-run-evidence.test.ts index 3b57d310..79544f4b 100644 --- a/tests/core/live-run-evidence.test.ts +++ b/tests/core/live-run-evidence.test.ts @@ -174,7 +174,10 @@ describe("live run evidence", () => { actionId: `ACTION-${index + 1}`, artifactType, returnType: "GSTR-2B" as const, - endpointClass: "gstr2b-portal-blob-captured-download" as const, + endpointClass: + artifactType === "JSON" + ? ("gstr2b-main-world-json-captured-download" as const) + : ("gstr2b-portal-blob-captured-download" as const), downloadPathClass: "captured-portal-request-data" as const, })); const missingArtifact = validateLiveRunEvidence({ @@ -200,31 +203,29 @@ describe("live run evidence", () => { }); it.each(["GSTR-2B", "GSTR-3B"] as const)( - "rejects passing standalone %s JSON evidence, which nothing can back", + "accepts passing standalone %s JSON evidence backed by exact download proof", (returnType) => { - // A standalone JSON selection is acquired by the direct same-origin fetch, - // whose success flow step carries no downloadDiagnostic for either return - // type. The schema admitting an endpoint class is not the runtime - // producing one, and it is the runtime that has to back the claim. - expect( - validateLiveRunEvidence({ - ...createValidEvidence(), - returnType, - artifactType: "JSON", - downloadEvidence: [ - { - ...createValidEvidence().downloadEvidence[0], - artifactType: "JSON", - returnType, - endpointClass: - returnType === "GSTR-2B" - ? "gstr2b-portal-blob-captured-download" - : "gstr3b-portal-blob-captured-download", - downloadPathClass: "captured-portal-request-data", - }, - ], - }), - ).toMatchObject({ ok: false }); + // A standalone JSON selection is acquired by a direct same-origin fetch, + // whose validated delivery carries the exact browser download identity. + const result = validateLiveRunEvidence({ + ...createValidEvidence(), + returnType, + artifactType: "JSON", + downloadEvidence: [ + { + ...createValidEvidence().downloadEvidence[0], + artifactType: "JSON", + returnType, + endpointClass: + returnType === "GSTR-2B" + ? "gstr2b-main-world-json-captured-download" + : "gstr3b-main-world-json-captured-download", + downloadPathClass: "captured-portal-request-data", + }, + ], + }); + + expect(result.ok, result.ok ? undefined : result.errors.join("\n")).toBe(true); }, ); diff --git a/tests/scripts/create-live-run-evidence-template.test.ts b/tests/scripts/create-live-run-evidence-template.test.ts index 9ecda9a8..905bc228 100644 --- a/tests/scripts/create-live-run-evidence-template.test.ts +++ b/tests/scripts/create-live-run-evidence-template.test.ts @@ -171,43 +171,37 @@ describe("live evidence template generator", () => { }); it.each(["GSTR-2B", "GSTR-3B"])( - "refuses passing standalone %s JSON evidence the runtime cannot back", + "creates passing standalone %s JSON evidence with its runtime-backed class", (returnType) => { - // A standalone JSON selection is acquired by the direct same-origin fetch, - // whose success flow step attaches no download diagnostic — for GSTR-2B and - // GSTR-3B alike. Only JSON captured inside an all-formats selection is - // staged and retains one, which is why the all-formats case above still - // passes. Emitting a blob-captured class here would certify a path that - // does not exist, which is worse than refusing. - const failed = spawnSync( - process.execPath, - [ - ...scriptArgs, - "--return-type", - returnType, - "--artifact-type", - "JSON", - "--financial-year", - "2025-26", - "--period", - "April", - "--outcome", - "pass", - "--clean-test-profile", - "--human-verified-account", - "--human-verified-periods", - "--all-files-non-empty", - "--clear-local-data-checked", - "--browser-summary-captured", - ...stableArgs, - ], - { encoding: "utf8" }, - ); + const evidence = runTemplate([ + "--return-type", + returnType, + "--artifact-type", + "JSON", + "--financial-year", + "2025-26", + "--period", + "April", + "--outcome", + "pass", + "--clean-test-profile", + "--human-verified-account", + "--human-verified-periods", + "--all-files-non-empty", + "--clear-local-data-checked", + "--browser-summary-captured", + ...stableArgs, + ]); - expect(failed.status).not.toBe(0); - expect(`${failed.stderr}${failed.stdout}`).toContain( - "Passing standalone JSON evidence is not supported", - ); + expect(validateLiveRunEvidence(evidence)).toMatchObject({ ok: true }); + expect(evidence.downloadEvidence).toEqual([ + expect.objectContaining({ + endpointClass: + returnType === "GSTR-2B" + ? "gstr2b-main-world-json-captured-download" + : "gstr3b-main-world-json-captured-download", + }), + ]); }, ); From c427fcbf9328dfd2d8e7fc2ec93cf938388b63eb Mon Sep 17 00:00:00 2001 From: Tapish Khandelwal Date: Wed, 5 Aug 2026 16:37:26 +0530 Subject: [PATCH 4/6] fix(recovery): persist gstr1 page-generated proof --- .../gst/filed-returns-durable-signals.ts | 2 + ...led-returns-session-write-boundary.test.ts | 56 +++++++++++++++++++ .../filed-returns-durable-signals.test.ts | 7 +++ 3 files changed, 65 insertions(+) diff --git a/src/connectors/gst/filed-returns-durable-signals.ts b/src/connectors/gst/filed-returns-durable-signals.ts index c26973d5..d0082774 100644 --- a/src/connectors/gst/filed-returns-durable-signals.ts +++ b/src/connectors/gst/filed-returns-durable-signals.ts @@ -113,6 +113,8 @@ const EXACT_DURABLE_SIGNALS = new Set([ "filed-return-portal-click-evidence-unavailable", "filed-return-positively-not-filed", "filed-return-result-row-ambiguous", + "page-generated-excel-ready", + "page-generated-pdf-ready", "filed-return-result-row-not-found", "filed-return-result-view-clicked", "filed-return-result-view-not-found", diff --git a/tests/background/filed-returns-session-write-boundary.test.ts b/tests/background/filed-returns-session-write-boundary.test.ts index 5a68bf6c..f910bc53 100644 --- a/tests/background/filed-returns-session-write-boundary.test.ts +++ b/tests/background/filed-returns-session-write-boundary.test.ts @@ -200,6 +200,62 @@ describe("filed-return session write boundary", () => { expect(storage.session[checkpointKey]).toBeUndefined(); }); + it("persists a GSTR-1 page-generated PDF completion before clearing its checkpoint", async () => { + const scope = { + artifactType: "PDF" as const, + financialYear: "2026-27", + period: "April", + returnType: "GSTR-1" as const, + }; + const actionId = "00000000-0000-4000-8000-000000000095"; + const checkpointKey = artifactAcquisitionCheckpointKey(scope); + storage.session[checkpointKey] = { + ...scope, + armedAt: "2026-08-05T08:00:00.000Z", + downloadId: 95, + requestId: actionId, + state: "download-observing", + }; + + const response = await withPersistedSinglePeriodSummary( + scope, + { + ok: true, + flowStep: { + connectorId: "gst", + scopeId: filedReturnsScopeId(scope.returnType), + state: "downloaded", + safeSignals: [ + "target-period-verified", + "page-generated-pdf-ready", + "filed-gstr1-portal-blob-download-captured", + ], + safeMessage: "Pack saved the selected filed return.", + downloadDiagnostic: { + actionId, + artifactType: "PDF", + byteCountClass: "non-empty", + downloadId: 95, + downloadPathClass: "captured-portal-request-unknown", + endpointClass: "gstr1-pdf-portal-blob-captured-download", + eventType: "filed-return-download-path", + financialYear: scope.financialYear, + mimeClass: "pdf", + period: scope.period, + returnType: scope.returnType, + schemaVersion: "1.0", + status: "downloaded", + }, + }, + }, + deps, + true, + ); + + expect(response).toMatchObject({ flowSummary: { status: "complete" } }); + expect(storage.session[checkpointKey]).toBeUndefined(); + }); + it("persists a GSTR-3B JSON capture before clearing its exact-ID checkpoint", async () => { const scope = { artifactType: "JSON" as const, diff --git a/tests/connectors/filed-returns-durable-signals.test.ts b/tests/connectors/filed-returns-durable-signals.test.ts index 1785eb82..2bb56df3 100644 --- a/tests/connectors/filed-returns-durable-signals.test.ts +++ b/tests/connectors/filed-returns-durable-signals.test.ts @@ -127,6 +127,13 @@ describe("filed-return durable signal contract", () => { expect(parseDurableFiledReturnsSignals(signals)).toEqual(signals); }); + it("retains categorical page-generated artifact readiness evidence", () => { + const signals = ["page-generated-pdf-ready", "page-generated-excel-ready"]; + + expect(parseDurableFiledReturnsSignals(signals)).toEqual(signals); + expect(isDurableFiledReturnsSignal("page-generated-private-value-ready")).toBe(false); + }); + it("retains final GSTR-2B capture-control rejections", () => { const signals = [ "gstr2b-capture-control-not-actionable", From e867589b7dd8ec37ba9d1048dee7366bac595358 Mon Sep 17 00:00:00 2001 From: Tapish Khandelwal Date: Wed, 5 Aug 2026 16:49:04 +0530 Subject: [PATCH 5/6] fix(recovery): explain expired reload proof --- src/background/filed-returns-target-review.ts | 27 +++++- .../gst/filed-returns-durable-signals.ts | 1 + src/entrypoints/popup/inline-status.tsx | 16 ++-- src/entrypoints/popup/recovery-actions.tsx | 14 ++- src/entrypoints/popup/run-summary.tsx | 1 + .../filed-returns-target-review.test.ts | 91 +++++++++++++++++++ .../filed-returns-durable-signals.test.ts | 1 + tests/popup/inline-status.test.tsx | 24 +++++ tests/popup/recovery-actions.test.ts | 30 ++++++ 9 files changed, 196 insertions(+), 9 deletions(-) diff --git a/src/background/filed-returns-target-review.ts b/src/background/filed-returns-target-review.ts index 16de97a9..e3247446 100644 --- a/src/background/filed-returns-target-review.ts +++ b/src/background/filed-returns-target-review.ts @@ -353,7 +353,32 @@ export async function reconcileRetainedArtifactAcquisition( : inspection.state === "retry-safe" ? review.artifactAcquisitionCompletion : undefined; - if (!evidence) return responseForFiledReturnsTargetReview(review); + if (!evidence) { + if (inspection.state !== "retry-safe" || review.artifactAcquisitionCompletion) { + return responseForFiledReturnsTargetReview(review); + } + if (review.safeSignals.includes("artifact-acquisition-session-proof-expired")) { + return responseForFiledReturnsTargetReview(review); + } + // Chrome clears storage.session when an extension is reloaded, while the + // local review deliberately survives. The exact browser download ID is + // session-only, so a later retry cannot safely recreate the correlation. + // Surface that boundary instead of offering a reconciliation action that + // can only return this same review. + const expiredProofReview: FiledReturnsTargetReview = { + ...review, + revision: targetReviewRevision(review) + 1, + safeSignals: uniqueSafeSignals([ + ...review.safeSignals, + "artifact-acquisition-session-proof-expired", + ]), + updatedAt: (deps.now?.() ?? new Date()).toISOString(), + }; + const parsedExpiredProofReview = parseFiledReturnsTargetReview(expiredProofReview); + if (!parsedExpiredProofReview) return responseForFiledReturnsTargetReview(review); + await browser.storage.local.set({ [key]: parsedExpiredProofReview }); + return responseForFiledReturnsTargetReview(parsedExpiredProofReview); + } let completionReview = review; if (inspection.state === "completed") { diff --git a/src/connectors/gst/filed-returns-durable-signals.ts b/src/connectors/gst/filed-returns-durable-signals.ts index d0082774..e85c91f0 100644 --- a/src/connectors/gst/filed-returns-durable-signals.ts +++ b/src/connectors/gst/filed-returns-durable-signals.ts @@ -495,6 +495,7 @@ const ARTIFACT_FAILURE_SIGNALS = new Set([ // this records that the completion is restored rather than re-observed. "artifact-acquisition-completion-restored", "artifact-acquisition-completion-pending-summary", + "artifact-acquisition-session-proof-expired", "artifact-acquisition-start-unreconciled", "artifact-acquisition-download-interrupted", "artifact-acquisition-download-unconfirmed", diff --git a/src/entrypoints/popup/inline-status.tsx b/src/entrypoints/popup/inline-status.tsx index 51732b75..1ef83fde 100644 --- a/src/entrypoints/popup/inline-status.tsx +++ b/src/entrypoints/popup/inline-status.tsx @@ -163,13 +163,15 @@ function getInlineStatusCopy( const canRetryTargetCleanup = signals.has("filed-returns-target-local-cleanup-required"); return { body: needsTargetReview - ? canReconcileTarget - ? `Resolve ${summary.currentPeriod} before choosing another period. Finish or cancel any open Save dialog, then reconcile the exact browser download.` - : canRetryTargetCleanup - ? `Resolve ${summary.currentPeriod} before choosing another period. Retry the local cleanup; Pack will not click the GST Portal again.` - : signals.has("single-period-zip-incomplete") - ? `Resolve ${summary.currentPeriod} before choosing another period. Open More run controls to discard the saved state and start the selected files again, or cancel and reset.` - : `Resolve ${summary.currentPeriod} before choosing another period. Check Browser Downloads, then open More run controls to record a manual observation, explicitly start fresh, or cancel and reset.` + ? signals.has("artifact-acquisition-session-proof-expired") + ? `Pack cannot reconcile ${summary.currentPeriod} after the extension reload cleared its temporary exact-download proof. Check Browser Downloads, then start fresh or cancel and reset.` + : canReconcileTarget + ? `Resolve ${summary.currentPeriod} before choosing another period. Finish or cancel any open Save dialog, then reconcile the exact browser download.` + : canRetryTargetCleanup + ? `Resolve ${summary.currentPeriod} before choosing another period. Retry the local cleanup; Pack will not click the GST Portal again.` + : signals.has("single-period-zip-incomplete") + ? `Resolve ${summary.currentPeriod} before choosing another period. Open More run controls to discard the saved state and start the selected files again, or cancel and reset.` + : `Resolve ${summary.currentPeriod} before choosing another period. Check Browser Downloads, then open More run controls to record a manual observation, explicitly start fresh, or cancel and reset.` : needsFullFiscalYearRecovery ? getFullFiscalYearRecoveryBody(summary.currentPeriod, signals) : summary.flowStep.safeMessage, diff --git a/src/entrypoints/popup/recovery-actions.tsx b/src/entrypoints/popup/recovery-actions.tsx index d1bceddb..cd745abe 100644 --- a/src/entrypoints/popup/recovery-actions.tsx +++ b/src/entrypoints/popup/recovery-actions.tsx @@ -65,7 +65,9 @@ export function RecoveryActions({ ) : null} {needsTargetReview ? ( <> -

Why Pack paused: {summary.flowStep.safeMessage}

+

+ Why Pack paused: {targetReviewRecoveryMessage(summary, signals)} +

{hasDiagnosticSignals(summary) ? (
Safe diagnostics @@ -184,6 +186,16 @@ export function RecoveryActions({ ); } +function targetReviewRecoveryMessage( + summary: FiledReturnsFlowSummary, + signals: ReadonlySet, +): string { + if (signals.has("artifact-acquisition-session-proof-expired")) { + return "The extension reload cleared Pack's temporary exact-download proof. Check Browser Downloads, then start fresh or cancel and reset."; + } + return summary.flowStep.safeMessage; +} + export function hasRecoveryActions(summary: FiledReturnsFlowSummary | null): boolean { return getRecoveryActionState(summary).visible; } diff --git a/src/entrypoints/popup/run-summary.tsx b/src/entrypoints/popup/run-summary.tsx index 9478351c..27b26680 100644 --- a/src/entrypoints/popup/run-summary.tsx +++ b/src/entrypoints/popup/run-summary.tsx @@ -33,6 +33,7 @@ export function hasDiagnosticSignals(summary: FiledReturnsFlowSummary): boolean /** Whether retry can inspect a retained exact-ID artifact download without a portal click. */ export function canReconcileFiledReturnsTarget(summary: FiledReturnsFlowSummary): boolean { const signals = summary.flowStep.safeSignals; + if (signals.includes("artifact-acquisition-session-proof-expired")) return false; if ( summary.scope.artifactType === "PDF_AND_EXCEL" && !signals.includes("filed-returns-download-reconciliation-required") diff --git a/tests/background/filed-returns-target-review.test.ts b/tests/background/filed-returns-target-review.test.ts index 295606f5..e51a2d80 100644 --- a/tests/background/filed-returns-target-review.test.ts +++ b/tests/background/filed-returns-target-review.test.ts @@ -312,6 +312,97 @@ describe("filed returns target review", () => { ]); }); + it("replaces a dead reconciliation action after an extension reload expires session proof", async () => { + const scope = { + artifactType: "PDF" as const, + financialYear: "2025-26", + period: "May", + returnType: "GSTR-1" as const, + }; + const localValues: Record = { + "target-review": { + revision: 1, + safeMessage: "Pack retained unresolved artifact recovery.", + safeSignals: ["artifact-acquisition-download-unreconciled"], + schemaVersion: "1.0", + scope, + status: "download-unconfirmed", + targetId: "GSTR-1:2025-26:May", + updatedAt: "2026-08-01T00:00:00.000Z", + }, + }; + browserMocks.storage.local.get.mockImplementation(async (key: unknown) => + typeof key === "string" && Object.hasOwn(localValues, key) ? { [key]: localValues[key] } : {}, + ); + browserMocks.storage.local.set.mockImplementation(async (values: Record) => { + Object.assign(localValues, values); + }); + browserMocks.storage.local.remove.mockImplementation(async (keys: unknown) => { + for (const key of Array.isArray(keys) ? keys : [keys]) { + if (typeof key === "string") delete localValues[key]; + } + }); + acquisitionMocks.inspectArtifactAcquisitionCheckpoint.mockResolvedValue({ + state: "retry-safe", + }); + + const response = await reconcileRetainedArtifactAcquisition(scope, { + storageKeys: { completion: "completion", targetReview: "target-review" }, + now: () => new Date("2026-08-01T00:00:05.000Z"), + }); + + expect(response).toMatchObject({ + flowSummary: { + flowStep: { + safeSignals: expect.arrayContaining(["artifact-acquisition-session-proof-expired"]), + }, + status: "blocked", + }, + }); + expect(localValues["target-review"]).toMatchObject({ + revision: 2, + safeSignals: expect.arrayContaining(["artifact-acquisition-session-proof-expired"]), + }); + expect(browserMocks.storage.session.values.completion).toBeUndefined(); + }); + + it("does not revise a target review again after marking session proof expired", async () => { + const scope = { + artifactType: "PDF" as const, + financialYear: "2025-26", + period: "May", + returnType: "GSTR-1" as const, + }; + const review = { + revision: 2, + safeMessage: "Pack could not verify the browser download for May.", + safeSignals: [ + "artifact-acquisition-download-unreconciled", + "artifact-acquisition-session-proof-expired", + ], + schemaVersion: "1.0", + scope, + status: "download-unconfirmed", + targetId: "GSTR-1:2025-26:May", + updatedAt: "2026-08-01T00:00:05.000Z", + }; + browserMocks.storage.local.get.mockImplementation(async (key: unknown) => + key === "target-review" ? { [key]: review } : {}, + ); + acquisitionMocks.inspectArtifactAcquisitionCheckpoint.mockResolvedValue({ + state: "retry-safe", + }); + + const response = await reconcileRetainedArtifactAcquisition(scope, { + storageKeys: { completion: "completion", targetReview: "target-review" }, + }); + + expect(response).toMatchObject({ + flowSummary: { flowStep: { safeSignals: expect.arrayContaining(review.safeSignals) } }, + }); + expect(browserMocks.storage.local.set).not.toHaveBeenCalled(); + }); + it("keeps a persisted acquisition completion when cleanup finishes before review removal", async () => { const scope = { artifactType: "PDF" as const, diff --git a/tests/connectors/filed-returns-durable-signals.test.ts b/tests/connectors/filed-returns-durable-signals.test.ts index 2bb56df3..fcc21e0e 100644 --- a/tests/connectors/filed-returns-durable-signals.test.ts +++ b/tests/connectors/filed-returns-durable-signals.test.ts @@ -112,6 +112,7 @@ describe("filed-return durable signal contract", () => { "artifact-acquisition-checkpoint-clear-failed", "artifact-acquisition-download-interrupted", "artifact-acquisition-download-reconciled", + "artifact-acquisition-session-proof-expired", ]; expect(parseDurableFiledReturnsSignals(signals)).toEqual(signals); expect(signals.every(isDurableFiledReturnsSignal)).toBe(true); diff --git a/tests/popup/inline-status.test.tsx b/tests/popup/inline-status.test.tsx index b9709fd5..59dfb52d 100644 --- a/tests/popup/inline-status.test.tsx +++ b/tests/popup/inline-status.test.tsx @@ -393,6 +393,30 @@ describe("inline filed-return recovery status", () => { expect(onRestartTarget).not.toHaveBeenCalled(); }); + it("does not offer reconciliation after an extension reload expires session-only proof", () => { + const targetReviewSummary: FiledReturnsFlowSummary = { + ...blockedSummary, + currentPeriod: "May", + flowStep: { + ...blockedSummary.flowStep, + safeSignals: [ + "filed-returns-target-review-required", + "artifact-acquisition-download-unreconciled", + "artifact-acquisition-session-proof-expired", + ], + }, + }; + + const action = getInlinePrimaryAction(blockedPresentation, targetReviewSummary, { + onOpenPortal: vi.fn(), + onRestartTarget: vi.fn(), + onRetryFullFiscalYearTarget: vi.fn(), + onRetryTarget: vi.fn(), + }); + + expect(action).toBeNull(); + }); + it("offers reconciliation for an observing selected-file ZIP", () => { const targetReviewSummary: FiledReturnsFlowSummary = { ...blockedSummary, diff --git a/tests/popup/recovery-actions.test.ts b/tests/popup/recovery-actions.test.ts index c0b5984a..5410ef29 100644 --- a/tests/popup/recovery-actions.test.ts +++ b/tests/popup/recovery-actions.test.ts @@ -9,6 +9,7 @@ import { canManuallyObserveFullFiscalYearTarget, RecoveryActions, } from "../../src/entrypoints/popup/recovery-actions"; +import { canReconcileFiledReturnsTarget } from "../../src/entrypoints/popup/run-summary"; import { RunEvidencePanel } from "../../src/entrypoints/popup/run-evidence-panel"; describe("popup full-year recovery actions", () => { @@ -209,6 +210,35 @@ describe("popup full-year recovery actions", () => { expect(markup).not.toContain("Retry this period"); }); + it("removes reconciliation after an extension reload expires session-only proof", () => { + const summary = targetReviewSummary(); + summary.flowStep.safeSignals.push( + "artifact-acquisition-download-unreconciled", + "artifact-acquisition-session-proof-expired", + ); + + expect(canReconcileFiledReturnsTarget(summary)).toBe(false); + + const markup = renderToStaticMarkup( + createElement(RecoveryActions, { + busy: null, + portalReady: true, + summary, + onAcknowledgeInterruptedRun: () => undefined, + onRetryFullFiscalYearTarget: () => undefined, + onRetryTarget: () => undefined, + onResolveFullFiscalYearTarget: () => undefined, + onResolveTarget: () => undefined, + onStartFresh: () => undefined, + }), + ); + + expect(markup).not.toContain("Reconcile browser download"); + expect(markup).toContain("extension reload cleared Pack's temporary exact-download proof"); + expect(markup).toContain("Discard saved state and start selected download"); + expect(markup).toContain("Cancel and reset"); + }); + it("offers reconciliation for an observing selected-file ZIP", () => { const summary = targetReviewSummary(); summary.scope.artifactType = "PDF_AND_EXCEL"; From dc4349c987ed76832e15878716732312f65a1ed5 Mon Sep 17 00:00:00 2001 From: Tapish Khandelwal Date: Wed, 5 Aug 2026 18:14:02 +0530 Subject: [PATCH 6/6] fix(recovery): persist single artifact download identity --- .../filed-returns-download-trigger.ts | 49 +++++++++ .../filed-returns-single-period-summary.ts | 21 +++- .../filed-returns-target-download-attempt.ts | 4 +- ...period-summary-artifact-checkpoint.test.ts | 102 +++++++++++++++++- 4 files changed, 172 insertions(+), 4 deletions(-) diff --git a/src/background/filed-returns-download-trigger.ts b/src/background/filed-returns-download-trigger.ts index 75ed2f3b..f175a809 100644 --- a/src/background/filed-returns-download-trigger.ts +++ b/src/background/filed-returns-download-trigger.ts @@ -28,6 +28,10 @@ import { persistArtifactAcquisitionIntent, persistArtifactAcquisitionUnconfirmedDownload, } from "./artifact-acquisition-state"; +import { + persistFiledReturnsTargetDownloadId, + persistFiledReturnsTargetDownloadIntent, +} from "./filed-returns-target-download-attempt"; import { gstr3bFullFiscalYearAcquisitionNotWiredStep, isGstr3bFullFiscalYearAcquisitionScope, @@ -35,6 +39,35 @@ import { type FlowStepResponse = Extract; +async function persistSingleArtifactRecoveryIntent( + scope: FiledReturnsDownloadScope, + artifactType: FiledReturnsConcreteArtifactType, + actionId: string, + deps: FiledReturnsFlowMessagingDeps, +): Promise { + if (!deps.storageKeys.targetReview || deps.stageCapturedDownloads) return true; + return persistFiledReturnsTargetDownloadIntent( + scope, + { + actionId, + artifactType, + kind: "single-artifact", + phase: "download-intent-persisted", + requestedAt: (deps.now?.() ?? new Date()).toISOString(), + }, + deps, + ); +} + +async function persistSingleArtifactRecoveryDownloadId( + scope: FiledReturnsDownloadScope, + downloadId: number, + deps: FiledReturnsFlowMessagingDeps, +): Promise { + if (!deps.storageKeys.targetReview || deps.stageCapturedDownloads) return true; + return persistFiledReturnsTargetDownloadId(scope, downloadId, deps); +} + export enum Gstr2bArtifactDispatchFailureReason { ContentUnavailable = "gstr2b-artifact-content-unavailable", PeriodInvalid = "gstr2b-artifact-period-invalid", @@ -461,6 +494,19 @@ async function triggerPageGeneratedSinglePeriodArtifact( // download needs this exact-ID checkpoint for recovery. const tracksBrowserDownload = !deps.stageCapturedDownloads; if (tracksBrowserDownload) { + if (!(await persistSingleArtifactRecoveryIntent(scope, artifactType, requestId, deps))) { + return { + ok: true, + flowStep: { + connectorId: "gst", + scopeId: filedReturnScopeId(returnType), + state: "blocked", + safeSignals: ["filed-return-download-id-persist-failed"], + safeMessage: + "Pack could not save the exact browser-download recovery checkpoint, so it did not start the download.", + }, + }; + } await persistArtifactAcquisitionIntent({ ...checkpointTarget, requestId }); } let checkpointHasDownloadId = false; @@ -478,6 +524,9 @@ async function triggerPageGeneratedSinglePeriodArtifact( requestId, state: "download-observing", }); + if (!(await persistSingleArtifactRecoveryDownloadId(scope, downloadId, deps))) { + throw new Error("single-artifact download ID checkpoint failed"); + } }, onStartCheckpointFailed: async (downloadId: number) => { checkpointHasDownloadId = true; diff --git a/src/background/filed-returns-single-period-summary.ts b/src/background/filed-returns-single-period-summary.ts index 6fd944ff..5aba5f8c 100644 --- a/src/background/filed-returns-single-period-summary.ts +++ b/src/background/filed-returns-single-period-summary.ts @@ -11,6 +11,10 @@ import { readArtifactAcquisitionCompletionEvidence, type ArtifactAcquisitionCompletionEvidence, } from "./artifact-acquisition-state"; +import { + clearFiledReturnsTargetReview, + readFiledReturnsTargetReview, +} from "./filed-returns-target-review"; export async function withPersistedSinglePeriodSummary( scope: FiledReturnsDownloadScope, @@ -26,7 +30,7 @@ export async function withPersistedSinglePeriodSummary( if (response.flowSummary) { const flowSummary = await persistProvidedSinglePeriodSummary(response.flowSummary, deps); if (flowSummary) - return responseAfterPersistedSummary(scope, response, flowSummary, checkpointEvidence); + return responseAfterPersistedSummary(scope, response, flowSummary, checkpointEvidence, deps); const responseWithoutSummary = { ...response }; delete responseWithoutSummary.flowSummary; const reconstructedSummary = await persistSinglePeriodSummary(scope, response.flowStep, deps); @@ -36,12 +40,13 @@ export async function withPersistedSinglePeriodSummary( responseWithoutSummary, reconstructedSummary, checkpointEvidence, + deps, ) : responseWithoutSummary; } const flowSummary = await persistSinglePeriodSummary(scope, response.flowStep, deps); return flowSummary - ? responseAfterPersistedSummary(scope, response, flowSummary, checkpointEvidence) + ? responseAfterPersistedSummary(scope, response, flowSummary, checkpointEvidence, deps) : response; } @@ -50,9 +55,21 @@ async function responseAfterPersistedSummary( response: Extract, flowSummary: FiledReturnsFlowSummary, checkpointEvidence: readonly ArtifactAcquisitionCompletionEvidence[], + deps: FiledReturnsFlowRunnerDeps, ): Promise { if (response.flowStep.state === "downloaded") { await clearArtifactAcquisitionCheckpointsAfterPersistedSummary(scope, checkpointEvidence); + const review = await readFiledReturnsTargetReview(scope, deps); + const attempt = review?.downloadAttempt; + const diagnostic = response.flowStep.downloadDiagnostic; + if ( + attempt?.kind === "single-artifact" && + attempt.phase === "download-observing" && + diagnostic?.actionId === attempt.actionId && + diagnostic.downloadId === attempt.downloadId + ) { + await clearFiledReturnsTargetReview(scope, deps, review!.revision ?? 1); + } } return { ...response, flowSummary }; } diff --git a/src/background/filed-returns-target-download-attempt.ts b/src/background/filed-returns-target-download-attempt.ts index aae0feea..2e98dfed 100644 --- a/src/background/filed-returns-target-download-attempt.ts +++ b/src/background/filed-returns-target-download-attempt.ts @@ -54,7 +54,9 @@ export async function persistFiledReturnsTargetDownloadId( if (intent.phase !== "download-intent-persisted") return null; const diagnosticState = diagnosticSource ? mergeFiledReturnsDownloadDiagnosticState(review, diagnosticSource, scope) - : copyFiledReturnsDownloadDiagnosticState(review); + : review.downloadDiagnostic || review.downloadDiagnostics + ? copyFiledReturnsDownloadDiagnosticState(review) + : {}; if (!diagnosticState) return null; return { ...review, diff --git a/tests/background/filed-returns-single-period-summary-artifact-checkpoint.test.ts b/tests/background/filed-returns-single-period-summary-artifact-checkpoint.test.ts index 6d17a645..7d2bd86c 100644 --- a/tests/background/filed-returns-single-period-summary-artifact-checkpoint.test.ts +++ b/tests/background/filed-returns-single-period-summary-artifact-checkpoint.test.ts @@ -1,9 +1,11 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ clearAfterPersist: vi.fn(async () => undefined), persist: vi.fn(), readCompletionEvidence: vi.fn(async () => []), + readTargetReview: vi.fn(async () => null), + clearTargetReview: vi.fn(async () => true), })); vi.mock("../../src/background/filed-returns-session-summary", () => ({ persistCanonicalFiledReturnsFlowSummary: mocks.persist, @@ -12,9 +14,20 @@ vi.mock("../../src/background/artifact-acquisition-state", () => ({ clearArtifactAcquisitionCheckpointsAfterPersistedSummary: mocks.clearAfterPersist, readArtifactAcquisitionCompletionEvidence: mocks.readCompletionEvidence, })); +vi.mock("../../src/background/filed-returns-target-review", () => ({ + clearFiledReturnsTargetReview: mocks.clearTargetReview, + readFiledReturnsTargetReview: mocks.readTargetReview, +})); import { withPersistedSinglePeriodSummary } from "../../src/background/filed-returns-single-period-summary"; describe("GSTR-3B artifact checkpoint completion ordering", () => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.readCompletionEvidence.mockResolvedValue([]); + mocks.readTargetReview.mockResolvedValue(null); + mocks.clearTargetReview.mockResolvedValue(true); + }); + it("clears artifact checkpoint ownership only after completion persists", async () => { mocks.persist.mockImplementation(async () => { expect(mocks.clearAfterPersist).not.toHaveBeenCalled(); @@ -36,8 +49,74 @@ describe("GSTR-3B artifact checkpoint completion ordering", () => { expect(mocks.persist).toHaveBeenCalledTimes(1); expect(mocks.clearAfterPersist).toHaveBeenCalledWith(scope, []); }); + + it("clears only the matching durable single-artifact attempt after completion persists", async () => { + mocks.persist.mockImplementation(async () => { + expect(mocks.clearTargetReview).not.toHaveBeenCalled(); + return { completedPeriods: ["April"], flowStep: matchingStep(), scope, status: "complete" }; + }); + mocks.readTargetReview.mockResolvedValue({ + downloadAttempt: { + actionId: "00000000-0000-4000-8000-000000000111", + artifactType: "PDF", + downloadId: 111, + kind: "single-artifact", + phase: "download-observing", + requestedAt: "2026-08-05T00:00:00.000Z", + }, + revision: 2, + } as never); + + await withPersistedSinglePeriodSummary( + scope, + { ok: true, flowStep: matchingStep() }, + deps(), + true, + ); + + expect(mocks.clearTargetReview).toHaveBeenCalledWith(scope, expect.anything(), 2); + }); + + it("retains a same-scope replacement attempt whose exact identity differs", async () => { + mocks.persist.mockResolvedValue({ + completedPeriods: ["April"], + flowStep: matchingStep(), + scope, + status: "complete", + }); + mocks.readTargetReview.mockResolvedValue({ + downloadAttempt: { + actionId: "00000000-0000-4000-8000-000000000112", + artifactType: "PDF", + downloadId: 112, + kind: "single-artifact", + phase: "download-observing", + requestedAt: "2026-08-05T00:00:00.000Z", + }, + revision: 2, + } as never); + + await withPersistedSinglePeriodSummary( + scope, + { ok: true, flowStep: matchingStep() }, + deps(), + true, + ); + + expect(mocks.clearTargetReview).not.toHaveBeenCalled(); + }); }); +function deps() { + return { + storageKeys: { + completion: "completion", + fullFiscalYearLedger: "ledger", + observation: "observation", + }, + } as never; +} + const scope = { artifactType: "PDF" as const, financialYear: "2024-25", @@ -53,3 +132,24 @@ function step() { safeSignals: [], }; } + +function matchingStep() { + return { + ...step(), + downloadDiagnostic: { + actionId: "00000000-0000-4000-8000-000000000111", + artifactType: "PDF" as const, + byteCountClass: "non-empty" as const, + downloadId: 111, + downloadPathClass: "captured-portal-request-unknown" as const, + endpointClass: "gstr3b-portal-blob-captured-download" as const, + eventType: "filed-return-download-path" as const, + financialYear: scope.financialYear, + mimeClass: "pdf" as const, + period: scope.period, + returnType: scope.returnType, + schemaVersion: "1.0" as const, + status: "downloaded" as const, + }, + }; +}