mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 18:19:05 +02:00
fix: flush attempt receipts and surface checkpoint failures
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
3e77a70622
commit
f4100b256e
12 files changed
+198
-43
No files matched your search
@@ -31,6 +31,51 @@ const cases: Array<{ name: string; execute: () => Promise<Execute>; event: unkno
|
||||
describe("CLI adapter accounting on timeout", () => {
|
||||
const directories: string[] = [];
|
||||
afterEach(async () => { vi.clearAllMocks(); for (const dir of directories.splice(0)) await rm(dir, { recursive: true, force: true }); });
|
||||
it.each(cases.filter(fixture => fixture.name !== "kimi"))("$name flushes its attempt and surfaces checkpoint persistence failures", async (fixture) => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "paperclip-accounting-adapter-")); directories.push(dir);
|
||||
const command = join(dir, "runtime"); await writeFile(command, "#!/bin/sh\nprintf 'openai test\\n'\n", { mode: 0o755 });
|
||||
const stdout = JSON.stringify(fixture.event);
|
||||
processResult.mockImplementation(async (_run, _target, _command, _args, options) => {
|
||||
// Local process logging catches callback failures. Finalization must
|
||||
// independently enforce receipt durability despite that contract.
|
||||
await options.onLog("stdout", stdout).catch(() => {});
|
||||
return { exitCode: 0, signal: null, timedOut: false, stdout, stderr: "", pid: 123, startedAt: new Date().toISOString() };
|
||||
});
|
||||
const onUsage = vi.fn();
|
||||
const context: AdapterExecutionContext = {
|
||||
runId: "test-run", agent: { id: "test-agent", companyId: "test-company", name: "Accounting", adapterType: `${fixture.name}_local`, adapterConfig: {} },
|
||||
runtime: { sessionId: null, sessionParams: null, sessionDisplayId: null, taskKey: null },
|
||||
config: { engine: "cli", command, cwd: dir, model: ["opencode", "pi"].includes(fixture.name) ? "openai/test" : fixture.name === "cursor" ? "gpt-5" : "test", paperclipRuntimeSkills: [],
|
||||
env: { OPENAI_API_KEY: "test-placeholder", ANTHROPIC_API_KEY: "test-placeholder", GEMINI_API_KEY: "test-placeholder", OPENCODE_ALLOW_ALL_MODELS: "1", CLAUDE_CONFIG_DIR: dir } },
|
||||
context: {}, onLog: async () => {}, onUsage,
|
||||
};
|
||||
const execute = await fixture.execute();
|
||||
await execute(context);
|
||||
expect(onUsage.mock.calls.at(-1)![0]).toMatchObject({ usage: fixture.tokens, costUsd: fixture.price, complete: true });
|
||||
if (fixture.name === "claude") {
|
||||
const mixed = [
|
||||
{ type: "system", subtype: "init", model: "actual-model" },
|
||||
{ type: "result", total_cost_usd: 0.003, usage: { input_tokens: 3, output_tokens: 3 }, modelUsage: {
|
||||
"model-a": { inputTokens: 1, outputTokens: 1, costUSD: 0.001 },
|
||||
"model-b": { inputTokens: 2, outputTokens: 2, costUSD: 0.002 },
|
||||
} },
|
||||
];
|
||||
processResult.mockImplementation(async (_run, _target, _command, _args, options) => {
|
||||
const output = mixed.map(item => JSON.stringify(item)).join("\n");
|
||||
await options.onLog("stdout", output).catch(() => {});
|
||||
return { exitCode: 0, signal: null, timedOut: false, stdout: output, stderr: "", pid: 123, startedAt: new Date().toISOString() };
|
||||
});
|
||||
await execute(context);
|
||||
expect(onUsage.mock.calls.at(-1)![0]).toMatchObject({ model: "mixed", costUsd: 0.003, complete: true, usageByModel: [
|
||||
{ model: "model-a", costUsd: 0.001 }, { model: "model-b", costUsd: 0.002 },
|
||||
] });
|
||||
delete (mixed[1] as { modelUsage?: unknown }).modelUsage;
|
||||
await execute(context);
|
||||
expect(onUsage.mock.calls.at(-1)![0]).toMatchObject({ model: "actual-model", complete: true });
|
||||
}
|
||||
onUsage.mockRejectedValue(new Error("Receipt persistence failed"));
|
||||
await expect(execute(context)).rejects.toThrow("Receipt persistence failed");
|
||||
});
|
||||
it.each(cases)("$name retains observed accounting when the process times out", async (fixture) => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "paperclip-accounting-adapter-")); directories.push(dir);
|
||||
const command = join(dir, "runtime"); await writeFile(command, "#!/bin/sh\nprintf 'openai test\\n'\n", { mode: 0o755 });
|
||||
@@ -44,6 +89,7 @@ describe("CLI adapter accounting on timeout", () => {
|
||||
context: {}, onLog: async () => {},
|
||||
});
|
||||
expect(result.timedOut).toBe(true);
|
||||
expect(result.usageComplete).toBe(["codex", "cursor", "gemini"].includes(fixture.name));
|
||||
expect(result.usageBasis).toBe("per_run");
|
||||
expect(result.provider).toBeTruthy();
|
||||
expect(result.billingType).toBeTruthy();
|
||||
|
||||
Reference in new issue
Block a user