import { Buffer } from "node:buffer"; import { describe, expect, it } from "vitest"; import { getSessionSyncWatermark, refreshSessionSyncAccessToken, SessionSyncApiError, startSessionSyncDeviceFlow, uploadSessionAnalytics, } from "../src/core/session-sync-api.ts"; function jsonResponse(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } }); } describe("session sync api", () => { it("starts the OAuth device flow with the session sync scope", async () => { let request: Request | undefined; const fetchMock: typeof fetch = async (input, init) => { request = new Request(input, init); return jsonResponse({ device_code: "pigd_1", user_code: "ABCD-EFGH", verification_uri: "https://pi.dev/pair", verification_uri_complete: "https://pi.dev/pair?code=ABCD-EFGH", expires_in: 300, interval: 2, }); }; const response = await startSessionSyncDeviceFlow("00000000-0000-4000-8000-000000000000", { baseUrl: "https://example.test/", fetch: fetchMock, }); expect(response.device_code).toBe("pigd_1"); expect(request?.url).toBe("https://example.test/api/oauth/device"); expect(request?.method).toBe("POST"); const body = new URLSearchParams(await request?.text()); expect(body.get("client_id")).toBe("pi-coding-agent"); expect(body.get("scope")).toBe("session_sync offline_access"); expect(body.get("device_id")).toBe("00000000-0000-4000-8000-000000000000"); }); it("refreshes tokens and reads watermarks", async () => { const urls: string[] = []; const fetchMock: typeof fetch = async (input, init) => { const request = new Request(input, init); urls.push(request.url); if (request.url.endsWith("/api/oauth/token")) { return jsonResponse({ token_type: "Bearer", access_token: "access-2", refresh_token: "refresh-2", expires_in: 86400, scope: "session_sync offline_access", }); } expect(request.headers.get("Authorization")).toBe("Bearer access-2"); return jsonResponse({ ok: true, watermark: "2026-01-01T00:00:00.000Z" }); }; const token = await refreshSessionSyncAccessToken("refresh-1", { baseUrl: "https://example.test", fetch: fetchMock, }); const watermark = await getSessionSyncWatermark(token.access_token, "device-1", { baseUrl: "https://example.test", fetch: fetchMock, }); expect(token.refresh_token).toBe("refresh-2"); expect(watermark.watermark).toBe("2026-01-01T00:00:00.000Z"); expect(urls).toEqual([ "https://example.test/api/oauth/token", "https://example.test/analytics/sessions/device-1", ]); }); it("uploads compressed NDJSON with sync headers and surfaces API errors", async () => { let request: Request | undefined; const fetchMock: typeof fetch = async (input, init) => { request = new Request(input, init); return jsonResponse( { ok: true, records_received: 1, first_record_timestamp: "2026-01-01T00:00:00.000Z", last_record_timestamp: "2026-01-01T00:00:00.000Z", received_bytes: 10, watermark: "2026-01-02T00:00:00.000Z", }, 201, ); }; const response = await uploadSessionAnalytics({ baseUrl: "https://example.test", 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.records_received).toBe(1); 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({ baseUrl: "https://example.test", 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(new SessionSyncApiError(400, "invalid_payload", "bad line")); }); });