From 18e4d622eb069997422190217eaa9b1696cb20f0 Mon Sep 17 00:00:00 2001 From: gluedtea Date: Sat, 12 Sep 2026 01:49:24 +0800 Subject: [PATCH] fix: preserve pending install reports when retry fails Signed-off-by: gluedtea --- src/report.ts | 17 ++++++---- tests/report.test.ts | 76 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 85 insertions(+), 8 deletions(-) diff --git a/src/report.ts b/src/report.ts index b6987ae..872f1f2 100644 --- a/src/report.ts +++ b/src/report.ts @@ -64,12 +64,17 @@ export async function flushPendingReports(input: { const files = await readdir(dir).catch(() => []); for (const file of files) { const full = join(dir, file); - const report = JSON.parse(await readFile(full, "utf8")) as InstallReportPayload; - await apiPost("/xagent/plugin/install/report", report, { - baseUrl: input.baseUrl, - accessToken: input.credentials.accessToken - }).catch(() => undefined); - await rm(full, { force: true }); + try { + const report = JSON.parse(await readFile(full, "utf8")) as InstallReportPayload; + await apiPost("/xagent/plugin/install/report", report, { + baseUrl: input.baseUrl, + accessToken: input.credentials.accessToken + }); + await rm(full, { force: true }); + } catch { + // Keep failed reports for a later CLI run and continue with the queue. + continue; + } } } diff --git a/tests/report.test.ts b/tests/report.test.ts index 605eaf5..af9905d 100644 --- a/tests/report.test.ts +++ b/tests/report.test.ts @@ -1,6 +1,38 @@ -import { describe, expect, it } from "vitest"; +import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { collectFingerprint } from "../src/fingerprint.js"; -import { createInstallReport } from "../src/report.js"; +import { createInstallReport, flushPendingReports, submitInstallReport } from "../src/report.js"; + +const credentials = { + accessToken: "access-token", + refreshToken: "refresh-token", + accessExpire: 0, + userId: "user-1" +}; + +const report = createInstallReport({ + target: "generic", + login: { status: "success", subject: "user-1" }, + fingerprint: collectFingerprint({ cliVersion: "0.1.0", agentRuntime: "generic" }), + substep: { command: "install", status: "success", duration: 10 } +}); + +const temporaryConfigDirs: string[] = []; + +afterEach(async () => { + vi.unstubAllGlobals(); + vi.unstubAllEnvs(); + await Promise.all(temporaryConfigDirs.splice(0).map((directory) => rm(directory, { recursive: true, force: true }))); +}); + +async function useTemporaryConfig(): Promise { + const directory = await mkdtemp(join(tmpdir(), "xagt-report-test-")); + temporaryConfigDirs.push(directory); + vi.stubEnv("XDG_CONFIG_HOME", directory); + return directory; +} describe("report payload", () => { it("creates schema v1 report", () => { @@ -22,4 +54,44 @@ describe("report payload", () => { expect(report.target).toBe("generic"); expect(report.substep.status).toBe("success"); }); + + it("keeps a pending report when the initial submit and retry both fail", async () => { + const configDir = await useTemporaryConfig(); + vi.stubGlobal( + "fetch", + vi.fn() + .mockResolvedValueOnce(new Response(JSON.stringify({ msg: "unavailable" }), { status: 500 })) + .mockResolvedValueOnce(new Response(JSON.stringify({ msg: "still unavailable" }), { status: 500 })) + .mockResolvedValueOnce(new Response(JSON.stringify({ success: true }), { status: 200 })) + ); + + await submitInstallReport({ baseUrl: "https://api.example.test", credentials, report }); + const pendingDir = join(configDir, "xagt", "pending-reports"); + const [pendingFile] = await readdir(pendingDir); + expect(pendingFile).toBeDefined(); + expect(JSON.parse(await readFile(join(pendingDir, pendingFile!), "utf8"))).toEqual(JSON.parse(JSON.stringify(report))); + + await flushPendingReports({ baseUrl: "https://api.example.test", credentials }); + expect(await readdir(pendingDir)).toHaveLength(1); + + await flushPendingReports({ baseUrl: "https://api.example.test", credentials }); + expect(await readdir(pendingDir)).toHaveLength(0); + }); + + it("continues flushing later reports after an earlier report fails", async () => { + const configDir = await useTemporaryConfig(); + const pendingDir = join(configDir, "xagt", "pending-reports"); + await mkdir(pendingDir, { recursive: true }); + await writeFile(join(pendingDir, "a.json"), JSON.stringify(report), "utf8"); + await writeFile(join(pendingDir, "b.json"), JSON.stringify(report), "utf8"); + const fetchMock = vi.fn() + .mockResolvedValueOnce(new Response(JSON.stringify({ msg: "unavailable" }), { status: 500 })) + .mockResolvedValueOnce(new Response(JSON.stringify({ success: true }), { status: 200 })); + vi.stubGlobal("fetch", fetchMock); + + await flushPendingReports({ baseUrl: "https://api.example.test", credentials }); + + expect(fetchMock).toHaveBeenCalledTimes(2); + expect((await readdir(pendingDir)).sort()).toEqual(["a.json"]); + }); });