Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions src/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}

Expand Down
76 changes: 74 additions & 2 deletions tests/report.test.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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", () => {
Expand All @@ -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"]);
});
});