Files
pi/packages/coding-agent/test/activity-sync.test.ts
Vegard Stikbakke 71918c6575 feat(coding-agent): add pi.dev integration
Add pi.dev account integration, profile setup, shared session support, activity sync, and slash command support using shared pi.dev OAuth/client utilities.
2026-06-09 09:26:10 +02:00

236 lines
6.9 KiB
TypeScript

import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { syncSessionAnalytics } from "../src/core/activity-sync/activity-sync.ts";
import { loadActivitySyncState } from "../src/core/activity-sync/state.ts";
import { AuthStorage } from "../src/core/auth-storage.ts";
import { SettingsManager } from "../src/core/settings-manager.ts";
const tempDirs: string[] = [];
function createTempDir(): string {
const dir = mkdtempSync(join(tmpdir(), "pi-activity-sync-"));
tempDirs.push(dir);
return dir;
}
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true });
}
});
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
function writeSessionFile(sessionsRoot: string): void {
const sessionDir = join(sessionsRoot, "default");
mkdirSync(sessionDir, { recursive: true });
writeFileSync(
join(sessionDir, "session-1.jsonl"),
`${[
{
type: "session",
version: 3,
id: "session-1",
timestamp: "2026-01-01T00:00:00.000Z",
cwd: "/tmp",
},
{
type: "model_change",
id: "entry-1",
parentId: null,
timestamp: "2026-01-01T00:00:01.000Z",
provider: "openai",
modelId: "gpt-4.1",
},
]
.map((record) => JSON.stringify(record))
.join("\n")}\n`,
);
}
function createActivitySyncAuthStorage(refresh = "refresh-1"): AuthStorage {
return AuthStorage.inMemory({
"pi.dev": {
type: "oauth",
access: "access-old",
refresh,
expires: Date.now() - 1,
scope: "activity_sync offline_access",
},
});
}
describe("syncSessionAnalytics", () => {
it("returns not_authenticated after recording lastAttemptAt", async () => {
const agentDir = createTempDir();
const result = await syncSessionAnalytics({
agentDir,
settingsManager: SettingsManager.inMemory(),
now: new Date("2026-01-01T00:00:00.000Z"),
});
expect(result).toEqual({ status: "not_authenticated" });
expect(await loadActivitySyncState(agentDir)).toMatchObject({
lastAttemptAt: "2026-01-01T00:00:00.000Z",
});
});
it("updates lastAttemptAt on no_changes", async () => {
const agentDir = createTempDir();
const sessionsRoot = createTempDir();
const authStorage = createActivitySyncAuthStorage();
const fetchMock: typeof fetch = async (input, init) => {
const request = new Request(input, init);
if (request.url.endsWith("/api/oauth/token")) {
return jsonResponse({
token_type: "Bearer",
access_token: "access-1",
refresh_token: "refresh-2",
expires_in: 86400,
scope: "activity_sync offline_access",
});
}
return jsonResponse({ ok: true, watermark: null });
};
const result = await syncSessionAnalytics({
agentDir,
sessionsRoot,
settingsManager: SettingsManager.inMemory(),
authStorage,
fetch: fetchMock,
now: new Date("2026-01-02T00:00:00.000Z"),
});
expect(result).toMatchObject({
status: "no_changes",
filesScanned: 0,
serverWatermark: null,
});
expect(await loadActivitySyncState(agentDir)).toMatchObject({
lastAttemptAt: "2026-01-02T00:00:00.000Z",
});
expect(authStorage.get("pi.dev")).toMatchObject({ refresh: "refresh-2" });
});
it("clears stale pi.dev credentials that the server rejects", async () => {
const agentDir = createTempDir();
const authStorage = createActivitySyncAuthStorage();
const fetchMock: typeof fetch = async () =>
jsonResponse({ error: "invalid_grant", description: "stale refresh token" }, 400);
const result = await syncSessionAnalytics({
agentDir,
settingsManager: SettingsManager.inMemory(),
authStorage,
fetch: fetchMock,
now: new Date("2026-01-02T00:00:00.000Z"),
});
expect(result).toEqual({ status: "not_authenticated" });
expect(authStorage.get("pi.dev")).toBeUndefined();
expect(await loadActivitySyncState(agentDir)).toMatchObject({
lastAttemptAt: "2026-01-02T00:00:00.000Z",
});
});
it("uploads with an idempotency key without persisting payload files", async () => {
const agentDir = createTempDir();
const sessionsRoot = createTempDir();
writeSessionFile(sessionsRoot);
const authStorage = createActivitySyncAuthStorage();
const idempotencyKeys: string[] = [];
const fetchMock: typeof fetch = async (input, init) => {
const request = new Request(input, init);
if (request.url.endsWith("/api/oauth/token")) {
return jsonResponse({
token_type: "Bearer",
access_token: "access-1",
refresh_token: "refresh-2",
expires_in: 86400,
scope: "activity_sync offline_access",
});
}
if (request.method === "GET") return jsonResponse({ ok: true, watermark: null });
idempotencyKeys.push(request.headers.get("Idempotency-Key") ?? "");
expect((await request.arrayBuffer()).byteLength).toBeGreaterThan(0);
return jsonResponse({
ok: true,
accepted: true,
received_bytes: 21,
watermark: "2026-01-02T00:00:00.000Z",
});
};
const result = await syncSessionAnalytics({
agentDir,
sessionsRoot,
settingsManager: SettingsManager.inMemory(),
authStorage,
fetch: fetchMock,
now: new Date("2026-01-03T00:00:00.000Z"),
});
expect(result).toMatchObject({
status: "uploaded",
recordsSent: 2,
serverWatermark: null,
watermark: "2026-01-02T00:00:00.000Z",
});
expect(idempotencyKeys).toHaveLength(1);
expect(idempotencyKeys[0]).toMatch(/^[0-9a-f-]{36}$/);
expect(existsSync(join(agentDir, "activity-sync-payloads"))).toBe(false);
expect(await loadActivitySyncState(agentDir)).toMatchObject({
lastSuccessAt: "2026-01-03T00:00:00.000Z",
});
expect(authStorage.get("pi.dev")).toMatchObject({ refresh: "refresh-2" });
});
it("includes the server watermark when upload fails", async () => {
const agentDir = createTempDir();
const sessionsRoot = createTempDir();
writeSessionFile(sessionsRoot);
const authStorage = createActivitySyncAuthStorage();
const fetchMock: typeof fetch = async (input, init) => {
const request = new Request(input, init);
if (request.url.endsWith("/api/oauth/token")) {
return jsonResponse({
token_type: "Bearer",
access_token: "access-1",
refresh_token: "refresh-2",
expires_in: 86400,
scope: "activity_sync offline_access",
});
}
if (request.method === "GET")
return jsonResponse({
ok: true,
watermark: "2026-01-01T00:00:00.000Z",
});
return jsonResponse({}, 503);
};
const result = await syncSessionAnalytics({
agentDir,
sessionsRoot,
settingsManager: SettingsManager.inMemory(),
authStorage,
fetch: fetchMock,
now: new Date("2026-01-03T00:00:00.000Z"),
});
expect(result).toMatchObject({
status: "failed",
serverWatermark: "2026-01-01T00:00:00.000Z",
error: "POST /analytics/activity/:deviceId failed: HTTP 503",
});
});
});