mirror of
https://github.com/earendil-works/pi.git
synced 2026-06-18 15:54:04 +08:00
152 lines
4.7 KiB
TypeScript
152 lines
4.7 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/session-sync.ts";
|
|
import { loadSessionSyncState, saveSessionSyncState } from "../src/core/session-sync-state.ts";
|
|
import { SettingsManager } from "../src/core/settings-manager.ts";
|
|
|
|
const tempDirs: string[] = [];
|
|
|
|
function createTempDir(): string {
|
|
const dir = mkdtempSync(join(tmpdir(), "pi-session-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`,
|
|
);
|
|
}
|
|
|
|
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 loadSessionSyncState(agentDir)).toMatchObject({ lastAttemptAt: "2026-01-01T00:00:00.000Z" });
|
|
});
|
|
|
|
it("updates lastAttemptAt on no_changes", async () => {
|
|
const agentDir = createTempDir();
|
|
const sessionsRoot = createTempDir();
|
|
await saveSessionSyncState({ refreshToken: "refresh-1" }, agentDir);
|
|
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: "session_sync offline_access",
|
|
});
|
|
}
|
|
return jsonResponse({ ok: true, watermark: null });
|
|
};
|
|
|
|
const result = await syncSessionAnalytics({
|
|
agentDir,
|
|
sessionsRoot,
|
|
settingsManager: SettingsManager.inMemory(),
|
|
fetch: fetchMock,
|
|
now: new Date("2026-01-02T00:00:00.000Z"),
|
|
});
|
|
|
|
expect(result).toMatchObject({ status: "no_changes", filesScanned: 0 });
|
|
expect(await loadSessionSyncState(agentDir)).toMatchObject({
|
|
refreshToken: "refresh-2",
|
|
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);
|
|
await saveSessionSyncState({ refreshToken: "refresh-1" }, agentDir);
|
|
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: "session_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,
|
|
records_received: 2,
|
|
first_record_timestamp: "2026-01-01T00:00:00.000Z",
|
|
last_record_timestamp: "2026-01-01T00:00:01.000Z",
|
|
received_bytes: 21,
|
|
watermark: "2026-01-02T00:00:00.000Z",
|
|
});
|
|
};
|
|
|
|
const result = await syncSessionAnalytics({
|
|
agentDir,
|
|
sessionsRoot,
|
|
settingsManager: SettingsManager.inMemory(),
|
|
fetch: fetchMock,
|
|
now: new Date("2026-01-03T00:00:00.000Z"),
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
status: "uploaded",
|
|
recordsSent: 2,
|
|
watermark: "2026-01-02T00:00:00.000Z",
|
|
});
|
|
expect(idempotencyKeys).toHaveLength(1);
|
|
expect(idempotencyKeys[0]).toMatch(/^[0-9a-f-]{36}$/);
|
|
expect(existsSync(join(agentDir, "session-sync-payloads"))).toBe(false);
|
|
expect(await loadSessionSyncState(agentDir)).toMatchObject({
|
|
refreshToken: "refresh-2",
|
|
lastSuccessAt: "2026-01-03T00:00:00.000Z",
|
|
});
|
|
});
|
|
});
|