mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
149 lines
6.2 KiB
TypeScript
149 lines
6.2 KiB
TypeScript
/**
|
|
* usage-reaches-session-stats.e2e.test.ts — the premise #193 rests on, checked
|
|
* against the REAL pi runtime.
|
|
*
|
|
* Every unit test for usage reporting asserts that our tool results carry a
|
|
* `usage` field. None of them can establish the thing that makes carrying it
|
|
* worth doing: that pi picks it up. That happens entirely inside pi —
|
|
* `createToolResultMessage` copies `AgentToolResult.usage` onto the persisted
|
|
* message, and `getSessionStats()` folds `toolResult.usage` into the tokens and
|
|
* cost the footer, the statusline and `/cost` read. Mock pi, and a release that
|
|
* stopped doing either would leave the whole feature reporting into a void with
|
|
* a green suite.
|
|
*
|
|
* So this drives a real `AgentSession` and reads its real `getSessionStats()`,
|
|
* with the exact object `PendingUsagePool.drain()` produces — including the
|
|
* `cacheRead` our own display total drops but this report must carry, and the
|
|
* cost breakdown whose `total` pi reads with no guard at all.
|
|
*
|
|
* No network/LLM and no model turn: the message is appended through pi's own
|
|
* `sessionManager.appendMessage`, because what is under test is the accounting,
|
|
* not the streaming that would normally produce the message.
|
|
*
|
|
* This test is also what set the peer floor. Pi began folding `toolResult.usage`
|
|
* into `getSessionStats()` in 0.81.0, when the computation moved to walking
|
|
* session entries through `addUsageToTotals`; every 0.80.x sums assistant
|
|
* messages alone and drops the field. Running unconditionally is the point —
|
|
* against a Pi that does not aggregate, this fails rather than skipping, which
|
|
* is how the range stays honest. `peerDependencies` moved to `>=0.81.0` for
|
|
* exactly this reason, so the CI floor job runs it too.
|
|
*/
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { PendingUsagePool } from "../../src/usage.js";
|
|
import { fauxModelBackend } from "../helpers/faux-model-backend.js";
|
|
import { registerFauxProvider } from "../helpers/pi-ai.js";
|
|
|
|
// Real pi session construction; a cold first run under full-suite CPU
|
|
// contention can exceed vitest's 5s default.
|
|
vi.setConfig({ testTimeout: 30_000 });
|
|
|
|
describe("subagent usage reaches the parent session's stats (real pi)", () => {
|
|
let cwd: string;
|
|
let faux: ReturnType<typeof registerFauxProvider>;
|
|
|
|
beforeEach(() => {
|
|
cwd = mkdtempSync(join(tmpdir(), "subagents-usage-e2e-"));
|
|
faux = registerFauxProvider({ provider: "faux", models: [{ id: "faux-1", contextWindow: 200_000 }] });
|
|
});
|
|
afterEach(() => {
|
|
faux.unregister();
|
|
rmSync(cwd, { recursive: true, force: true });
|
|
});
|
|
|
|
/** A real session, in memory, on a faux model. */
|
|
async function realSession() {
|
|
const model = faux.getModel();
|
|
const backend = fauxModelBackend(model);
|
|
const { session } = await createAgentSession({
|
|
cwd,
|
|
sessionManager: SessionManager.inMemory(cwd),
|
|
model: model as any,
|
|
modelRegistry: backend.modelRegistry,
|
|
modelRuntime: backend.modelRuntime,
|
|
tools: [],
|
|
} as any);
|
|
return session;
|
|
}
|
|
|
|
/** The tool result our `Agent` tool returns, as pi would persist it. */
|
|
function toolResultCarrying(usage: unknown) {
|
|
return {
|
|
role: "toolResult" as const,
|
|
toolCallId: "tc-1",
|
|
toolName: "Agent",
|
|
content: [{ type: "text" as const, text: "Agent completed." }],
|
|
isError: false,
|
|
timestamp: 1,
|
|
usage,
|
|
};
|
|
}
|
|
|
|
it("pi adds our reported tokens and cost to getSessionStats()", async () => {
|
|
const session = await realSession();
|
|
try {
|
|
const before = session.getSessionStats();
|
|
|
|
const pool = new PendingUsagePool();
|
|
pool.add({ input: 1000, output: 400, cacheWrite: 100, cacheRead: 9000, cost: 0.0123 });
|
|
pool.add({ input: 2000, output: 600, cacheWrite: 200, cacheRead: 18_000, cost: 0.0077 });
|
|
const usage = pool.drain();
|
|
|
|
session.sessionManager.appendMessage(toolResultCarrying(usage) as any);
|
|
const after = session.getSessionStats();
|
|
|
|
// Exactly what we reported, on every component pi tracks — cacheRead
|
|
// included, which is the one pi counts for its own messages and our own
|
|
// display total leaves out.
|
|
expect(after.tokens.input - before.tokens.input).toBe(3000);
|
|
expect(after.tokens.output - before.tokens.output).toBe(1000);
|
|
expect(after.tokens.cacheWrite - before.tokens.cacheWrite).toBe(300);
|
|
expect(after.tokens.cacheRead - before.tokens.cacheRead).toBe(27_000);
|
|
|
|
// The cost: the whole point of the feature for anyone watching a
|
|
// statusline. `addUsageToTotals` reads `usage.cost.total` with no guard,
|
|
// so an incomplete object would have thrown before reaching here.
|
|
expect(after.cost - before.cost).toBeCloseTo(0.02, 10);
|
|
} finally {
|
|
session.dispose?.();
|
|
}
|
|
});
|
|
|
|
it("leaves the context-window percentage alone", async () => {
|
|
// pi derives context usage from assistant messages only. If that ever
|
|
// changed, a delegating session would look like it was filling its context
|
|
// with work that happened somewhere else entirely — and users would compact
|
|
// for no reason.
|
|
const session = await realSession();
|
|
try {
|
|
const before = session.getSessionStats().contextUsage?.percent ?? null;
|
|
|
|
const pool = new PendingUsagePool();
|
|
pool.add({ input: 150_000, output: 400, cacheWrite: 100, cost: 1.5 });
|
|
session.sessionManager.appendMessage(toolResultCarrying(pool.drain()) as any);
|
|
|
|
expect(session.getSessionStats().contextUsage?.percent ?? null).toBe(before);
|
|
} finally {
|
|
session.dispose?.();
|
|
}
|
|
});
|
|
|
|
it("counts nothing for a tool result that carries no usage", async () => {
|
|
// The `reportUsage: false` shape, and every other tool in the session.
|
|
const session = await realSession();
|
|
try {
|
|
const before = session.getSessionStats();
|
|
session.sessionManager.appendMessage(toolResultCarrying(undefined) as any);
|
|
const after = session.getSessionStats();
|
|
|
|
expect(after.tokens.input).toBe(before.tokens.input);
|
|
expect(after.cost).toBe(before.cost);
|
|
} finally {
|
|
session.dispose?.();
|
|
}
|
|
});
|
|
});
|