Files
pi/packages/coding-agent/test/activity-sync-api.test.ts
2026-06-09 09:49:24 +02:00

88 lines
2.9 KiB
TypeScript

import { Buffer } from "node:buffer";
import { afterEach, describe, expect, it, vi } from "vitest";
import { getActivitySyncWatermark, uploadSessionAnalytics } from "../src/core/activity-sync/api.ts";
import type { PiDevApiError } from "../src/core/pi-dev/http.ts";
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } });
}
afterEach(() => {
vi.unstubAllEnvs();
});
describe("activity sync api", () => {
it("reads watermarks", async () => {
vi.stubEnv("PI_DEV_URL", "https://example.test");
const urls: string[] = [];
const fetchMock: typeof fetch = async (input, init) => {
const request = new Request(input, init);
urls.push(request.url);
expect(request.headers.get("Authorization")).toBe("Bearer access-2");
return jsonResponse({ ok: true, watermark: "2026-01-01T00:00:00.000Z" });
};
const watermark = await getActivitySyncWatermark("access-2", "device-1", {
fetch: fetchMock,
});
expect(watermark.watermark).toBe("2026-01-01T00:00:00.000Z");
expect(urls).toEqual(["https://example.test/analytics/activity/device-1"]);
});
it("uploads compressed NDJSON with sync headers and surfaces API errors", async () => {
vi.stubEnv("PI_DEV_URL", "https://example.test");
let request: Request | undefined;
const fetchMock: typeof fetch = async (input, init) => {
request = new Request(input, init);
return jsonResponse(
{
ok: true,
accepted: true,
received_bytes: 10,
watermark: "2026-01-02T00:00:00.000Z",
},
202,
);
};
const response = await uploadSessionAnalytics({
fetch: fetchMock,
accessToken: "access-1",
deviceId: "device-1",
watermark: "2026-01-02T00:00:00.000Z",
idempotencyKey: "retry-key",
body: Buffer.from("payload"),
contentEncoding: "zstd",
});
expect(response.accepted).toBe(true);
expect(request?.headers.get("Authorization")).toBe("Bearer access-1");
expect(request?.headers.get("Content-Type")).toBe("application/x-ndjson");
expect(request?.headers.get("Content-Encoding")).toBe("zstd");
expect(request?.headers.get("Pi-Sync-Watermark")).toBe("2026-01-02T00:00:00.000Z");
expect(request?.headers.get("Idempotency-Key")).toBe("retry-key");
const failingFetch: typeof fetch = async () =>
jsonResponse({ ok: false, error: "invalid_payload", description: "bad line" }, 400);
await expect(
uploadSessionAnalytics({
fetch: failingFetch,
accessToken: "access-1",
deviceId: "device-1",
watermark: "2026-01-02T00:00:00.000Z",
idempotencyKey: "retry-key",
body: Buffer.from("payload"),
contentEncoding: "zstd",
}),
).rejects.toMatchObject({
name: "PiDevApiError",
status: 400,
errorCode: "invalid_payload",
description: "bad line",
operation: "POST /analytics/activity/:deviceId",
message: "POST /analytics/activity/:deviceId failed: invalid_payload: bad line",
} satisfies Partial<PiDevApiError>);
});
});