From af675e7d0c40f037db9e6332fec4f53c1db394c7 Mon Sep 17 00:00:00 2001 From: Tapish Khandelwal Date: Mon, 21 Sep 2026 17:15:56 +0530 Subject: [PATCH 1/4] test(gstr3b): pin one automatic re-click after a silent generation timeout Co-Authored-By: Claude Opus 5 --- .../gstr3b-artifact-acquisition.test.ts | 91 ++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/tests/background/gstr3b-artifact-acquisition.test.ts b/tests/background/gstr3b-artifact-acquisition.test.ts index 7b73531e..7f7c4f6e 100644 --- a/tests/background/gstr3b-artifact-acquisition.test.ts +++ b/tests/background/gstr3b-artifact-acquisition.test.ts @@ -2,13 +2,16 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type * as ArtifactDownloadModule from "../../src/background/artifact-download"; const mocks = vi.hoisted(() => ({ executeScript: vi.fn(), + addListener: vi.fn(), removeListener: vi.fn(), downloadAcquiredArtifact: vi.fn(), })); vi.mock("wxt/browser", () => ({ browser: { scripting: { executeScript: mocks.executeScript }, - downloads: { onCreated: { addListener: vi.fn(), removeListener: mocks.removeListener } }, + downloads: { + onCreated: { addListener: mocks.addListener, removeListener: mocks.removeListener }, + }, }, })); vi.mock("../../src/background/artifact-download", async (importOriginal) => ({ @@ -16,7 +19,11 @@ vi.mock("../../src/background/artifact-download", async (importOriginal) => ({ downloadAcquiredArtifact: mocks.downloadAcquiredArtifact, })); -import { acquireGstr3bPdfAfterPreflight } from "../../src/background/gstr3b-artifact-acquisition"; +import { + acquireGstr3bPdfAfterPreflight, + GSTR3B_FIRST_CAPTURE_WAIT_MS, + GSTR3B_RECLICK_CAPTURE_WAIT_MS, +} from "../../src/background/gstr3b-artifact-acquisition"; import { acquirePageGeneratedArtifact } from "../../src/background/gstr2b-artifact-acquisition"; import { GSTR1_EXCEL_NO_DETAILS_DIALOG_SELECTOR, @@ -139,6 +146,86 @@ describe("GSTR-3B page-generated acquisition", () => { expect(mocks.downloadAcquiredArtifact).not.toHaveBeenCalled(); }); + describe("one automatic re-click after a silent generation timeout (#386)", () => { + // Live, 2026-09-21: 38 GSTR-3B captures took 0.2-1.5 s and 8 more produced nothing for the full + // 20 s; a manual retry then succeeded each time. So the first wait is short, and a timeout with + // no browser download at all gets one more click, which the owner approved. Any download seen + // from the tab means the portal did act, and that stays the existing review path. + const pdf = () => { + const bytes = new Uint8Array(1024); + bytes.set(new TextEncoder().encode("%PDF-1.7")); + return Buffer.from(bytes).toString("base64"); + }; + const input = { + deliver: async () => ({ ok: true as const, safeSignals: [] }), + financialYear: "2024-25", + filename: "synthetic.pdf", + period: "April", + requestId: "reclick", + returnPeriod: "042024", + tabId: 17, + }; + const timeoutArgs = () => + mocks.executeScript.mock.calls.map( + (call) => (call[0] as { args: [{ timeoutMs?: number }] }).args[0].timeoutMs, + ); + + it("clicks once more after a short silent timeout and keeps the file", async () => { + mocks.executeScript + .mockResolvedValueOnce([ + { result: { ok: false, reason: "generation-timeout", safeSignals: [] } }, + ]) + .mockResolvedValueOnce([ + { result: { ok: true, base64: pdf(), blobUrl: "blob:synthetic/3b", safeSignals: [] } }, + ]); + + const result = await acquireGstr3bPdfAfterPreflight(input); + + expect(result).toMatchObject({ ok: true }); + expect(result.safeSignals).toContain("filed-gstr3b-capture-reclicked"); + expect(timeoutArgs()).toEqual([GSTR3B_FIRST_CAPTURE_WAIT_MS, GSTR3B_RECLICK_CAPTURE_WAIT_MS]); + expect(GSTR3B_FIRST_CAPTURE_WAIT_MS + GSTR3B_RECLICK_CAPTURE_WAIT_MS).toBeLessThanOrEqual( + 20_000, + ); + }); + + it("re-clicks at most once", async () => { + mocks.executeScript.mockResolvedValue([ + { result: { ok: false, reason: "generation-timeout", safeSignals: [] } }, + ]); + + await expect(acquireGstr3bPdfAfterPreflight(input)).resolves.toMatchObject({ + ok: false, + reason: "generation-timeout", + }); + expect(mocks.executeScript).toHaveBeenCalledTimes(2); + }); + + it("does not re-click when the browser saw any download from the tab", async () => { + mocks.executeScript.mockImplementationOnce(async () => { + const listener = mocks.addListener.mock.calls.at(-1)?.[0] as + ((item: { id: number; tabId?: number; url?: string }) => void) | undefined; + listener?.({ id: 5, tabId: 17, url: "blob:https://return.gst.gov.in/late" }); + return [{ result: { ok: false, reason: "generation-timeout", safeSignals: [] } }]; + }); + + await expect(acquireGstr3bPdfAfterPreflight(input)).resolves.toMatchObject({ + ok: false, + reason: "generation-timeout", + }); + expect(mocks.executeScript).toHaveBeenCalledTimes(1); + }); + + it.each(["control-not-found", "page-period-mismatch", "too-large", "unexpected-content"])( + "does not re-click after %s", + async (reason) => { + mocks.executeScript.mockResolvedValue([{ result: { ok: false, reason, safeSignals: [] } }]); + await acquireGstr3bPdfAfterPreflight(input); + expect(mocks.executeScript).toHaveBeenCalledTimes(1); + }, + ); + }); + it("distinguishes an absent MAIN-world result from portal generation timeout", async () => { mocks.executeScript.mockResolvedValue([]); From 8d15bce4d10de06d30e0c9dc55e0abe8ba9e46f4 Mon Sep 17 00:00:00 2001 From: Tapish Khandelwal Date: Mon, 21 Sep 2026 17:15:56 +0530 Subject: [PATCH 2/4] fix(gstr3b): wait 5 s, then re-click once when the portal ignored the click Live, GSTR-3B captures either finished within 1.5 s or produced nothing in 20 s, and a manual retry then succeeded. A timeout with no browser download from the tab now gets one more click (owner-approved), within the same 20 s total. Any download seen keeps the existing review path. Co-Authored-By: Claude Opus 5 --- src/background/artifact-download.ts | 3 ++ src/background/gstr3b-artifact-acquisition.ts | 37 +++++++++++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/background/artifact-download.ts b/src/background/artifact-download.ts index 9f325653..00cac34f 100644 --- a/src/background/artifact-download.ts +++ b/src/background/artifact-download.ts @@ -119,6 +119,8 @@ export async function downloadAcquiredArtifact( export function installPortalBlobDownloadSafetyNet(tabId: number): { bind(blobUrl: unknown): Promise; + /** Whether the browser created any blob download from this tab before a capture was bound. */ + sawDownload(): boolean; remove(): void; } { let expectedFingerprint: string | null = null; @@ -144,6 +146,7 @@ export function installPortalBlobDownloadSafetyNet(tabId: number): { if (!expectedFingerprint) return; for (const candidate of candidates.values()) void handle(candidate).catch(() => undefined); }, + sawDownload: () => candidates.size > 0, remove: () => browser.downloads.onCreated.removeListener(listener), }; } diff --git a/src/background/gstr3b-artifact-acquisition.ts b/src/background/gstr3b-artifact-acquisition.ts index 8852f381..b04dddb2 100644 --- a/src/background/gstr3b-artifact-acquisition.ts +++ b/src/background/gstr3b-artifact-acquisition.ts @@ -8,6 +8,13 @@ import { } from "../connectors/gst/portal-blob-shim"; import { downloadAcquiredArtifact, installPortalBlobDownloadSafetyNet } from "./artifact-download"; +/** + * Live, 38 GSTR-3B captures took 0.2-1.5 s and 8 produced nothing in 20 s (#386). The first wait is + * short; one re-click then gets the rest, so the total never exceeds the old 20 s limit. + */ +export const GSTR3B_FIRST_CAPTURE_WAIT_MS = 5_000; +export const GSTR3B_RECLICK_CAPTURE_WAIT_MS = 15_000; + type Gstr3bPdfDeliveryResult = | { ok: true; @@ -31,8 +38,7 @@ export async function acquireGstr3bPdfAfterPreflight(input: { }): Promise { const safetyNet = installPortalBlobDownloadSafetyNet(input.tabId); try { - let captured: PortalBlobShimResult | undefined; - try { + const capture = async (timeoutMs: number): Promise => { const [injection] = await browser.scripting.executeScript({ args: [ { @@ -44,16 +50,41 @@ export async function acquireGstr3bPdfAfterPreflight(input: { period: input.period, returnType: "GSTR-3B", }, + timeoutMs, }, ], func: capturePortalPdfBlob, target: { tabId: input.tabId }, world: "MAIN", }); - captured = injection?.result as PortalBlobShimResult | undefined; + return injection?.result as PortalBlobShimResult | undefined; + }; + let captured: PortalBlobShimResult | undefined; + let reclicked = false; + try { + captured = await capture(GSTR3B_FIRST_CAPTURE_WAIT_MS); + // The portal either answers a GSTR-3B click within about 1.5 s or not at all (#386). One more + // click is safe only when nothing reached the browser: the capture re-checks the page's target + // before clicking, the PDF is generated in the page with no server-side effect, and any + // download the portal did start keeps today's review path instead. + if ( + captured && + !captured.ok && + captured.reason === "generation-timeout" && + !safetyNet.sawDownload() + ) { + reclicked = true; + captured = await capture(GSTR3B_RECLICK_CAPTURE_WAIT_MS); + } } catch { return { ok: false, reason: "main-world-execution-failed", safeSignals: [] }; } + if (captured?.ok && reclicked) { + captured = { + ...captured, + safeSignals: [...captured.safeSignals, "filed-gstr3b-capture-reclicked"], + }; + } if (!captured?.ok) { return { ok: false, From 459268f29cd609a25f73137217f1f241e251c2b4 Mon Sep 17 00:00:00 2001 From: Tapish Khandelwal Date: Mon, 21 Sep 2026 17:22:39 +0530 Subject: [PATCH 3/4] test(gstr3b): pin that a failed re-click still records the second click Co-Authored-By: Claude Opus 5 --- tests/background/gstr3b-artifact-acquisition.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/background/gstr3b-artifact-acquisition.test.ts b/tests/background/gstr3b-artifact-acquisition.test.ts index 7f7c4f6e..851fe38a 100644 --- a/tests/background/gstr3b-artifact-acquisition.test.ts +++ b/tests/background/gstr3b-artifact-acquisition.test.ts @@ -194,10 +194,10 @@ describe("GSTR-3B page-generated acquisition", () => { { result: { ok: false, reason: "generation-timeout", safeSignals: [] } }, ]); - await expect(acquireGstr3bPdfAfterPreflight(input)).resolves.toMatchObject({ - ok: false, - reason: "generation-timeout", - }); + const result = await acquireGstr3bPdfAfterPreflight(input); + expect(result).toMatchObject({ ok: false, reason: "generation-timeout" }); + // Two clicks reached the portal; a review of the failed target must be able to see that. + expect(result.safeSignals).toContain("filed-gstr3b-capture-reclicked"); expect(mocks.executeScript).toHaveBeenCalledTimes(2); }); From f475b0629262a2059366f1bd91e80c246d5d08e4 Mon Sep 17 00:00:00 2001 From: Tapish Khandelwal Date: Mon, 21 Sep 2026 17:22:39 +0530 Subject: [PATCH 4/4] fix(gstr3b): record the re-click on a failed capture as well as a successful one Co-Authored-By: Claude Opus 5 --- src/background/gstr3b-artifact-acquisition.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/background/gstr3b-artifact-acquisition.ts b/src/background/gstr3b-artifact-acquisition.ts index b04dddb2..87d6100a 100644 --- a/src/background/gstr3b-artifact-acquisition.ts +++ b/src/background/gstr3b-artifact-acquisition.ts @@ -79,7 +79,9 @@ export async function acquireGstr3bPdfAfterPreflight(input: { } catch { return { ok: false, reason: "main-world-execution-failed", safeSignals: [] }; } - if (captured?.ok && reclicked) { + // Recorded on success and failure alike: two clicks reached the portal either way, and a review + // of a failed target must be able to see that. + if (captured && reclicked) { captured = { ...captured, safeSignals: [...captured.safeSignals, "filed-gstr3b-capture-reclicked"],