Files
pi/packages/coding-agent/test/session-analytics.test.ts
2026-06-04 09:41:42 +02:00

277 lines
7.1 KiB
TypeScript

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");
});
});