{"version":3,"file":"footer.test.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.test.ts"],"names":[],"mappings":"","sourcesContent":["import type { Api, Model } from \"@apholdings/jensen-ai\";\nimport stripAnsi from \"strip-ansi\";\nimport { beforeAll, describe, expect, it } from \"vitest\";\nimport type { AgentSession } from \"../../../core/agent-session.js\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.js\";\nimport { initTheme } from \"../theme/theme.js\";\nimport { FooterComponent } from \"./footer.js\";\n\nbeforeAll(() => {\n\tinitTheme(\"dark\");\n});\n\nfunction createSession(options?: {\n\tmessages?: Array<Record<string, unknown>>;\n\tmodel?: Model<Api> | null;\n\tcontextUsage?: { tokens: number | null; contextWindow: number | null; percent: number | null };\n}): AgentSession {\n\treturn {\n\t\tstate: {\n\t\t\tmessages: options?.messages ?? [],\n\t\t\tmodel: options?.model ?? null,\n\t\t},\n\t\tgetContextUsage: () => options?.contextUsage ?? { tokens: 1200, contextWindow: 128000, percent: 0.9375 },\n\t} as unknown as AgentSession;\n}\n\nfunction createFooterData(): ReadonlyFooterDataProvider {\n\treturn {\n\t\tgetGitRepoName: () => \"jensen-code\",\n\t\tgetGitBranch: () => \"main\",\n\t\tgetExtensionStatuses: () => new Map(),\n\t\tonBranchChange: () => () => {},\n\t\tgetAvailableProviderCount: () => 0,\n\t};\n}\n\ndescribe(\"FooterComponent cost display\", () => {\n\tit(\"labels priced assistant-only usage as a session total when no turn boundary exists locally\", () => {\n\t\tconst component = new FooterComponent(\n\t\t\tcreateSession({\n\t\t\t\tmessages: [\n\t\t\t\t\t{\n\t\t\t\t\t\trole: \"assistant\",\n\t\t\t\t\t\tprovider: \"openrouter\",\n\t\t\t\t\t\tmodel: \"ai21/jamba-large-1.7\",\n\t\t\t\t\t\tusage: {\n\t\t\t\t\t\t\tinput: 1000,\n\t\t\t\t\t\t\toutput: 500,\n\t\t\t\t\t\t\tcacheRead: 0,\n\t\t\t\t\t\t\tcacheWrite: 0,\n\t\t\t\t\t\t\ttotalTokens: 1500,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t}),\n\t\t\tcreateFooterData(),\n\t\t);\n\n\t\tconst line = stripAnsi(component.render(160)[0] ?? \"\");\n\t\texpect(line).toContain(\"cost session $0.006000\");\n\t});\n\n\tit(\"shows turn and session totals when a local user-turn boundary exists\", () => {\n\t\tconst component = new FooterComponent(\n\t\t\tcreateSession({\n\t\t\t\tmessages: [\n\t\t\t\t\t{\n\t\t\t\t\t\trole: \"user\",\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: \"first turn\" }],\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\trole: \"assistant\",\n\t\t\t\t\t\tprovider: \"openrouter\",\n\t\t\t\t\t\tmodel: \"ai21/jamba-large-1.7\",\n\t\t\t\t\t\tusage: {\n\t\t\t\t\t\t\tinput: 1000,\n\t\t\t\t\t\t\toutput: 500,\n\t\t\t\t\t\t\tcacheRead: 0,\n\t\t\t\t\t\t\tcacheWrite: 0,\n\t\t\t\t\t\t\ttotalTokens: 1500,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\trole: \"user\",\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: \"second turn\" }],\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\trole: \"assistant\",\n\t\t\t\t\t\tprovider: \"openrouter\",\n\t\t\t\t\t\tmodel: \"ai21/jamba-large-1.7\",\n\t\t\t\t\t\tusage: {\n\t\t\t\t\t\t\tinput: 500,\n\t\t\t\t\t\t\toutput: 250,\n\t\t\t\t\t\t\tcacheRead: 0,\n\t\t\t\t\t\t\tcacheWrite: 0,\n\t\t\t\t\t\t\ttotalTokens: 750,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t}),\n\t\t\tcreateFooterData(),\n\t\t);\n\n\t\tconst line = stripAnsi(component.render(160)[0] ?? \"\");\n\t\texpect(line).toContain(\"cost turn $0.003000 · session $0.009000\");\n\t});\n\n\tit(\"marks the session total as partial when some assistant usage cannot be priced from local metadata\", () => {\n\t\tconst component = new FooterComponent(\n\t\t\tcreateSession({\n\t\t\t\tmessages: [\n\t\t\t\t\t{\n\t\t\t\t\t\trole: \"assistant\",\n\t\t\t\t\t\tprovider: \"openrouter\",\n\t\t\t\t\t\tmodel: \"ai21/jamba-large-1.7\",\n\t\t\t\t\t\tusage: {\n\t\t\t\t\t\t\tinput: 1000,\n\t\t\t\t\t\t\toutput: 500,\n\t\t\t\t\t\t\tcacheRead: 0,\n\t\t\t\t\t\t\tcacheWrite: 0,\n\t\t\t\t\t\t\ttotalTokens: 1500,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\trole: \"assistant\",\n\t\t\t\t\t\tprovider: \"unknown-provider\",\n\t\t\t\t\t\tmodel: \"custom-model\",\n\t\t\t\t\t\tusage: {\n\t\t\t\t\t\t\tinput: 250,\n\t\t\t\t\t\t\toutput: 125,\n\t\t\t\t\t\t\tcacheRead: 0,\n\t\t\t\t\t\t\tcacheWrite: 0,\n\t\t\t\t\t\t\ttotalTokens: 375,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t}),\n\t\t\tcreateFooterData(),\n\t\t);\n\n\t\tconst line = stripAnsi(component.render(160)[0] ?? \"\");\n\t\texpect(line).toContain(\"cost session $0.006000+\");\n\t});\n\n\tit(\"shows unknown only when there is assistant usage but no resolvable pricing metadata\", () => {\n\t\tconst component = new FooterComponent(\n\t\t\tcreateSession({\n\t\t\t\tmessages: [\n\t\t\t\t\t{\n\t\t\t\t\t\trole: \"assistant\",\n\t\t\t\t\t\tprovider: \"unknown-provider\",\n\t\t\t\t\t\tmodel: \"custom-model\",\n\t\t\t\t\t\tusage: {\n\t\t\t\t\t\t\tinput: 250,\n\t\t\t\t\t\t\toutput: 125,\n\t\t\t\t\t\t\tcacheRead: 0,\n\t\t\t\t\t\t\tcacheWrite: 0,\n\t\t\t\t\t\t\ttotalTokens: 375,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t}),\n\t\t\tcreateFooterData(),\n\t\t);\n\n\t\tconst line = stripAnsi(component.render(160)[0] ?? \"\");\n\t\texpect(line).toContain(\"cost session ?\");\n\t});\n\n\tit(\"shows no cost yet before any assistant usage exists\", () => {\n\t\tconst component = new FooterComponent(createSession({ messages: [] }), createFooterData());\n\t\tconst line = stripAnsi(component.render(160)[0] ?? \"\");\n\t\texpect(line).toContain(\"cost session --\");\n\t});\n});\n\ndescribe(\"FooterComponent delegated cost aggregation\", () => {\n\tfunction makeSubagentToolResult(cost: number, toolCallId = \"tc1\"): Record<string, unknown> {\n\t\treturn {\n\t\t\trole: \"toolResult\",\n\t\t\ttoolCallId,\n\t\t\ttoolName: \"subagent\",\n\t\t\tisError: false,\n\t\t\ttimestamp: Date.now(),\n\t\t\tcontent: [],\n\t\t\tdetails: {\n\t\t\t\tmode: \"single\",\n\t\t\t\tagentScope: \"user\",\n\t\t\t\tprojectAgentsDir: null,\n\t\t\t\tdiscoveryErrors: [],\n\t\t\t\tresults: [\n\t\t\t\t\t{\n\t\t\t\t\t\tagent: \"coder\",\n\t\t\t\t\t\tagentSource: \"user\",\n\t\t\t\t\t\ttask: \"do something\",\n\t\t\t\t\t\texitCode: 0,\n\t\t\t\t\t\tmessages: [],\n\t\t\t\t\t\tstderr: \"\",\n\t\t\t\t\t\tstopReason: \"end_turn\",\n\t\t\t\t\t\tusage: { input: 500, output: 250, cacheRead: 0, cacheWrite: 0, cost, contextTokens: 750, turns: 1 },\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t},\n\t\t};\n\t}\n\n\tit(\"adds one subagent cost to turn and session totals\", () => {\n\t\t// Parent assistant: $0.006000, subagent: $0.0025 → total $0.008500\n\t\tconst component = new FooterComponent(\n\t\t\tcreateSession({\n\t\t\t\tmessages: [\n\t\t\t\t\t{ role: \"user\", content: [{ type: \"text\", text: \"go\" }] },\n\t\t\t\t\t{\n\t\t\t\t\t\trole: \"assistant\",\n\t\t\t\t\t\tprovider: \"openrouter\",\n\t\t\t\t\t\tmodel: \"ai21/jamba-large-1.7\",\n\t\t\t\t\t\tusage: { input: 1000, output: 500, cacheRead: 0, cacheWrite: 0, totalTokens: 1500 },\n\t\t\t\t\t},\n\t\t\t\t\tmakeSubagentToolResult(0.0025),\n\t\t\t\t],\n\t\t\t}),\n\t\t\tcreateFooterData(),\n\t\t);\n\n\t\tconst line = stripAnsi(component.render(160)[0] ?? \"\");\n\t\texpect(line).toContain(\"cost turn $0.008500 · session $0.008500\");\n\t});\n\n\tit(\"aggregates multiple subagent costs in the same turn\", () => {\n\t\t// Parent: $0.006000, subagent1: $0.0015, subagent2: $0.0020 → total $0.009500\n\t\tconst component = new FooterComponent(\n\t\t\tcreateSession({\n\t\t\t\tmessages: [\n\t\t\t\t\t{ role: \"user\", content: [{ type: \"text\", text: \"go\" }] },\n\t\t\t\t\t{\n\t\t\t\t\t\trole: \"assistant\",\n\t\t\t\t\t\tprovider: \"openrouter\",\n\t\t\t\t\t\tmodel: \"ai21/jamba-large-1.7\",\n\t\t\t\t\t\tusage: { input: 1000, output: 500, cacheRead: 0, cacheWrite: 0, totalTokens: 1500 },\n\t\t\t\t\t},\n\t\t\t\t\tmakeSubagentToolResult(0.0015, \"tc1\"),\n\t\t\t\t\tmakeSubagentToolResult(0.002, \"tc2\"),\n\t\t\t\t],\n\t\t\t}),\n\t\t\tcreateFooterData(),\n\t\t);\n\n\t\tconst line = stripAnsi(component.render(160)[0] ?? \"\");\n\t\texpect(line).toContain(\"cost turn $0.009500 · session $0.009500\");\n\t});\n\n\tit(\"does not count subagent from prior turn in current turn cost\", () => {\n\t\t// Turn 1: parent $0.006000 + subagent $0.0025 = $0.008500\n\t\t// Turn 2: parent $0.003000\n\t\t// Session total = $0.011500, turn total = $0.003000\n\t\tconst component = new FooterComponent(\n\t\t\tcreateSession({\n\t\t\t\tmessages: [\n\t\t\t\t\t{ role: \"user\", content: [{ type: \"text\", text: \"first\" }] },\n\t\t\t\t\t{\n\t\t\t\t\t\trole: \"assistant\",\n\t\t\t\t\t\tprovider: \"openrouter\",\n\t\t\t\t\t\tmodel: \"ai21/jamba-large-1.7\",\n\t\t\t\t\t\tusage: { input: 1000, output: 500, cacheRead: 0, cacheWrite: 0, totalTokens: 1500 },\n\t\t\t\t\t},\n\t\t\t\t\tmakeSubagentToolResult(0.0025),\n\t\t\t\t\t{ role: \"user\", content: [{ type: \"text\", text: \"second\" }] },\n\t\t\t\t\t{\n\t\t\t\t\t\trole: \"assistant\",\n\t\t\t\t\t\tprovider: \"openrouter\",\n\t\t\t\t\t\tmodel: \"ai21/jamba-large-1.7\",\n\t\t\t\t\t\tusage: { input: 500, output: 250, cacheRead: 0, cacheWrite: 0, totalTokens: 750 },\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t}),\n\t\t\tcreateFooterData(),\n\t\t);\n\n\t\t// Use extra-wide render so the full cost string is not truncated.\n\t\t// $0.003000 + $0.006000 + $0.0025 = $0.0115 (>= 0.01 → toFixed(4))\n\t\tconst line = stripAnsi(component.render(300)[0] ?? \"\");\n\t\texpect(line).toContain(\"turn $0.003000\");\n\t\texpect(line).toContain(\"session $0.0115\");\n\t});\n\n\tit(\"does not double-count costs on repeated renders\", () => {\n\t\tconst component = new FooterComponent(\n\t\t\tcreateSession({\n\t\t\t\tmessages: [\n\t\t\t\t\t{ role: \"user\", content: [{ type: \"text\", text: \"go\" }] },\n\t\t\t\t\t{\n\t\t\t\t\t\trole: \"assistant\",\n\t\t\t\t\t\tprovider: \"openrouter\",\n\t\t\t\t\t\tmodel: \"ai21/jamba-large-1.7\",\n\t\t\t\t\t\tusage: { input: 1000, output: 500, cacheRead: 0, cacheWrite: 0, totalTokens: 1500 },\n\t\t\t\t\t},\n\t\t\t\t\tmakeSubagentToolResult(0.0025),\n\t\t\t\t],\n\t\t\t}),\n\t\t\tcreateFooterData(),\n\t\t);\n\n\t\tconst line1 = stripAnsi(component.render(160)[0] ?? \"\");\n\t\tconst line2 = stripAnsi(component.render(160)[0] ?? \"\");\n\t\tconst line3 = stripAnsi(component.render(160)[0] ?? \"\");\n\t\texpect(line1).toContain(\"cost turn $0.008500 · session $0.008500\");\n\t\texpect(line2).toBe(line1);\n\t\texpect(line3).toBe(line1);\n\t});\n\n\tit(\"shows delegated-only cost when there are no parent assistant messages in the turn\", () => {\n\t\t// Only a subagent result, no parent assistant message → delegated cost only\n\t\tconst component = new FooterComponent(\n\t\t\tcreateSession({\n\t\t\t\tmessages: [{ role: \"user\", content: [{ type: \"text\", text: \"go\" }] }, makeSubagentToolResult(0.005)],\n\t\t\t}),\n\t\t\tcreateFooterData(),\n\t\t);\n\n\t\tconst line = stripAnsi(component.render(160)[0] ?? \"\");\n\t\texpect(line).toContain(\"cost turn $0.005000 · session $0.005000\");\n\t});\n});\n"]}