import { mkdirSync, mkdtempSync, rmSync, utimesSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { buildSessionAnalyticsUpload } from "../src/core/session-analytics-reader.ts"; const tempDirs: string[] = []; function createTempDir(): string { const dir = mkdtempSync(join(tmpdir(), "pi-session-analytics-reader-")); tempDirs.push(dir); return dir; } afterEach(() => { for (const dir of tempDirs.splice(0)) { rmSync(dir, { recursive: true, force: true }); } }); function writeJsonl(path: string, lines: unknown[]): void { writeFileSync(path, `${lines.map((line) => JSON.stringify(line)).join("\n")}\n`); } function sessionHeader(id: string, timestamp: string): unknown { return { type: "session", version: 3, id, timestamp, cwd: `/work/${id}` }; } function entry(id: string, timestamp: string): unknown { return { type: "model_change", id, parentId: null, timestamp, provider: "anthropic", modelId: "model" }; } describe("buildSessionAnalyticsUpload", () => { it("selects files by mtime and includes old records before the scan cutoff", async () => { const root = createTempDir(); const project = join(root, "--project--"); mkdirSync(project, { recursive: true }); const oldFile = join(project, "old.jsonl"); const changedFile = join(project, "changed.jsonl"); writeJsonl(oldFile, [ sessionHeader("old", "2026-01-01T00:00:00.000Z"), entry("old-entry", "2026-01-01T00:00:01.000Z"), ]); writeJsonl(changedFile, [ sessionHeader("changed", "2026-01-01T00:00:00.000Z"), entry("included-old-entry", "2026-01-01T00:00:01.000Z"), entry("future-entry", "2026-01-03T00:00:00.000Z"), ]); utimesSync(oldFile, new Date("2026-01-01T00:10:00.000Z"), new Date("2026-01-01T00:10:00.000Z")); utimesSync(changedFile, new Date("2026-01-02T00:10:00.000Z"), new Date("2026-01-02T00:10:00.000Z")); const result = await buildSessionAnalyticsUpload({ sessionsRoot: root, serverWatermark: "2026-01-02T00:00:00.000Z", scanCutoff: new Date("2026-01-02T12:00:00.000Z"), }); expect(result.filesScanned).toBe(1); expect(result.malformedFiles).toBe(0); expect(result.scanCutoff).toBe("2026-01-02T12:00:00.000Z"); expect( result.records.map((record) => (record.recordType === "entry" ? record.entryId : record.sessionId)), ).toEqual(["changed", "included-old-entry"]); }); it("skips malformed selected files", async () => { const root = createTempDir(); const project = join(root, "--project--"); mkdirSync(project, { recursive: true }); const malformed = join(project, "malformed.jsonl"); writeFileSync(malformed, `${JSON.stringify(sessionHeader("bad", "2026-01-01T00:00:00.000Z"))}\nnot json\n`); const result = await buildSessionAnalyticsUpload({ sessionsRoot: root, serverWatermark: null, scanCutoff: new Date("2026-01-02T00:00:00.000Z"), }); expect(result.records).toEqual([]); expect(result.filesScanned).toBe(1); expect(result.malformedFiles).toBe(1); }); });