import { describe, expect, it } from "vitest"; import { projectSessionForAnalytics, SESSION_ANALYTICS_SCHEMA_VERSION, type SessionAnalyticsRecord, } from "../src/core/session-analytics.ts"; import type { SessionEntry, SessionHeader } from "../src/core/session-manager.ts"; const header: SessionHeader = { type: "session", version: 3, id: "session-1", timestamp: "2026-01-02T03:04:05.000Z", cwd: "/tmp/project", parentSession: "/tmp/parent.jsonl", }; const entries: SessionEntry[] = [ { type: "model_change", id: "model-1", parentId: null, timestamp: "2026-01-02T03:04:06.000Z", provider: "anthropic", modelId: "claude-sonnet-4-5", }, { type: "message", id: "user-1", parentId: "model-1", timestamp: "2026-01-02T03:04:07.000Z", message: { role: "user", content: [ { type: "text", text: "secret user prompt" }, { type: "image", data: "secret-image-data", mimeType: "image/png" }, ], timestamp: 1767323047000, }, }, { type: "message", id: "assistant-1", parentId: "user-1", timestamp: "2026-01-02T03:04:08.000Z", message: { role: "assistant", content: [ { type: "text", text: "secret assistant answer" }, { type: "thinking", thinking: "secret reasoning" }, { type: "thinking", thinking: "", thinkingSignature: "secret-signature", redacted: true }, { type: "toolCall", id: "call-1", name: "read", arguments: { path: "secret/path.ts" } }, ], api: "anthropic-messages", provider: "anthropic", model: "claude-sonnet-4-5", responseModel: "claude-sonnet-4-5-20260101", responseId: "secret-response-id", usage: { input: 100, output: 20, cacheRead: 30, cacheWrite: 40, totalTokens: 190, cost: { input: 0.1, output: 0.2, cacheRead: 0.03, cacheWrite: 0.04, total: 0.37, }, }, stopReason: "toolUse", timestamp: 1767323048000, }, }, { type: "message", id: "tool-result-1", parentId: "assistant-1", timestamp: "2026-01-02T03:04:09.000Z", message: { role: "toolResult", toolCallId: "call-1", toolName: "read", content: [{ type: "text", text: "secret tool result" }], isError: true, timestamp: 1767323049000, }, }, { type: "compaction", id: "compaction-1", parentId: "tool-result-1", timestamp: "2026-01-02T03:04:10.000Z", summary: "secret compaction summary", firstKeptEntryId: "user-1", tokensBefore: 1234, details: { secret: "compaction details" }, fromHook: true, }, { type: "label", id: "label-1", parentId: "compaction-1", timestamp: "2026-01-02T03:04:11.000Z", targetId: "assistant-1", label: "secret label", }, { type: "session_info", id: "session-info-1", parentId: "label-1", timestamp: "2026-01-02T03:04:12.000Z", name: "secret session name", }, ]; describe("projectSessionForAnalytics", () => { it("projects a complete session into ordered analytics records", () => { const records = projectSessionForAnalytics(header, entries, { modifiedAt: "2026-01-03T04:05:06.000Z", hashString: (value) => `hashed:${value}`, }); expect(records).toEqual([ { recordType: "session", schemaVersion: SESSION_ANALYTICS_SCHEMA_VERSION, sessionId: "session-1", version: 3, createdAt: "2026-01-02T03:04:05.000Z", modifiedAt: "2026-01-03T04:05:06.000Z", parentSessionHash: "hashed:/tmp/parent.jsonl", }, { recordType: "entry", schemaVersion: SESSION_ANALYTICS_SCHEMA_VERSION, sessionId: "session-1", entryId: "model-1", parentEntryId: null, entryType: "model_change", timestamp: "2026-01-02T03:04:06.000Z", provider: "anthropic", modelId: "claude-sonnet-4-5", }, { recordType: "entry", schemaVersion: SESSION_ANALYTICS_SCHEMA_VERSION, sessionId: "session-1", entryId: "user-1", parentEntryId: "model-1", entryType: "message", timestamp: "2026-01-02T03:04:07.000Z", role: "user", contentStats: { stringContent: false, textBlocks: 1, imageBlocks: 1, thinkingBlocks: 0, redactedThinkingBlocks: 0, toolCallBlocks: 0, otherBlocks: 0, }, }, { recordType: "entry", schemaVersion: SESSION_ANALYTICS_SCHEMA_VERSION, sessionId: "session-1", entryId: "assistant-1", parentEntryId: "user-1", entryType: "message", timestamp: "2026-01-02T03:04:08.000Z", role: "assistant", api: "anthropic-messages", provider: "anthropic", model: "claude-sonnet-4-5", responseModel: "claude-sonnet-4-5-20260101", stopReason: "toolUse", hasError: false, usage: { input: 100, output: 20, cacheRead: 30, cacheWrite: 40, totalTokens: 190, costInput: 0.1, costOutput: 0.2, costCacheRead: 0.03, costCacheWrite: 0.04, costTotal: 0.37, }, contentStats: { stringContent: false, textBlocks: 1, imageBlocks: 0, thinkingBlocks: 2, redactedThinkingBlocks: 1, toolCallBlocks: 1, otherBlocks: 0, }, }, { recordType: "entry", schemaVersion: SESSION_ANALYTICS_SCHEMA_VERSION, sessionId: "session-1", entryId: "tool-result-1", parentEntryId: "assistant-1", entryType: "message", timestamp: "2026-01-02T03:04:09.000Z", role: "toolResult", isError: true, contentStats: { stringContent: false, textBlocks: 1, imageBlocks: 0, thinkingBlocks: 0, redactedThinkingBlocks: 0, toolCallBlocks: 0, otherBlocks: 0, }, }, { recordType: "entry", schemaVersion: SESSION_ANALYTICS_SCHEMA_VERSION, sessionId: "session-1", entryId: "compaction-1", parentEntryId: "tool-result-1", entryType: "compaction", timestamp: "2026-01-02T03:04:10.000Z", firstKeptEntryId: "user-1", tokensBefore: 1234, fromHook: true, hasDetails: true, }, { recordType: "entry", schemaVersion: SESSION_ANALYTICS_SCHEMA_VERSION, sessionId: "session-1", entryId: "label-1", parentEntryId: "compaction-1", entryType: "label", timestamp: "2026-01-02T03:04:11.000Z", targetId: "assistant-1", hasLabel: true, }, { recordType: "entry", schemaVersion: SESSION_ANALYTICS_SCHEMA_VERSION, sessionId: "session-1", entryId: "session-info-1", parentEntryId: "label-1", entryType: "session_info", timestamp: "2026-01-02T03:04:12.000Z", hasName: true, }, ] satisfies SessionAnalyticsRecord[]); }); it("omits raw private payload fields", () => { const serialized = JSON.stringify(projectSessionForAnalytics(header, entries)); expect(serialized).not.toContain("secret user prompt"); expect(serialized).not.toContain("secret assistant answer"); expect(serialized).not.toContain("secret reasoning"); expect(serialized).not.toContain("secret-image-data"); expect(serialized).not.toContain("secret/path.ts"); expect(serialized).not.toContain("secret tool result"); expect(serialized).not.toContain("secret compaction summary"); expect(serialized).not.toContain("compaction details"); expect(serialized).not.toContain("secret label"); expect(serialized).not.toContain("secret session name"); expect(serialized).not.toContain("secret-response-id"); expect(serialized).not.toContain("/tmp/project"); }); });