Files
PaperClipAI/tests/runner-e2e/provisional-accounting.test.ts
T
DottaandPaperclip b6e690044a feat(evals): allow reviewed bounded provisional accounting (#17365)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Product E2E campaigns include model, setup, storage and
observability costs.
> - Some billing windows close after the tests and reports finish.
> - Requiring settled receipts for retained storage can block a good
campaign for weeks.
> - A reservation alone does not prove that a pending charge has an
upper bound.
> - This change permits provisional completion only with reviewed
evidence for the exact cost configuration.
> - Reports preserve unknown amounts and holds until later
reconciliation.

## Linked Issues or Issue Description

Refs: #17360, #17354

**What existing behavior does this improve?**

The Product E2E scheduled accounting gate and its retained cost reports.

**Current behavior**

Every pending receipt fails finalization. A completed test can wait
through the retained-storage billing window. The journal has dollar
reservations but no explicit qualification of their bounds.

**Proposed behavior**

Permit provisional completion when all pending charges have reviewed
full-lifecycle bounds, no requests remain in flight, no receipts
conflict, and known costs plus held amounts fit the campaign limit. Keep
all existing holds and receipt-finality rules. Missing evidence still
fails.

**Reason and benefit**

An operator can distinguish delayed but bounded billing from unbounded
or missing costs. The campaign can finish without claiming that its
invoice is settled.

**Breaking changes**

The journal accepts optional bound metadata and an invalidation record.
New reports add an optional bounded-pending count. Current readers
retain old journals and reports without granting them bounds. Older code
must not read new bound-bearing journals. The existing daily/weekly
enable gates and budget amounts stay unchanged.

**Additional context**

This implements the operator-approved provisional-cost policy for the
existing eval harness. It does not change product budgets or the
agent-eval roadmap. A targeted GitHub search found no duplicate open PR.

## What Changed

- Accounting owns a narrow evidence contract: a reviewed audit hash,
exact configuration hash and full-lifecycle scope.
- Gateway reservations retain that evidence before dispatch. Changed
limits or rates fail before dispatch. Unexpected routing, model identity
or excessive usage invalidates a bound durably.
- Operating allowances can carry separately reviewed bounds for each
cost component, including its full storage lifetime. No existing
allowance becomes qualified by default.
- Reports and Braintrust experiment metadata distinguish provisional
accounting, complete receipts, blocked accounting and unavailable
evidence. Public output omits private audit hashes.
- Scheduled finalization allows qualified provisional costs and
explicitly rejects spending overruns, including overruns with complete
receipts.
- Add regressions for legacy data, restarts, late reconciliation,
conflicts, malformed evidence, routing uncertainty and exceeded limits.
Document the evidence and migration boundaries.

## Verification

- Full offline eval-support suite: 4,062 Vitest tests passed, two
existing tests skipped; 137 Node tests passed.
- `pnpm test:e2e:runner:typecheck`: passed.
- `git diff --check`: passed.
- Read the retained hosted qualification journal with the new policy.
All six legacy pending charges remain unqualified. Known cost
`$0.421245200`, held `$19.327680000`, and committed `$19.748925200`
remain unchanged. The journal's SHA-256 is unchanged. The original
failed workflow remains failed.
- No paid models, network provider calls, receipt imports or reservation
releases were used for this verification.
- Rebased onto master `4d90634f74`; 142 targeted tests and typecheck
passed.
- Review reproduced a lost OpenAI input-total limit check when cache
details were missing (1 failed, 47 passed). Fix
`5fef9ba7c149951a51eae468614a4b89f0b3d6db` preserves unpriced provider
usage bounds. Then 178 focused tests, full offline support and typecheck
passed.
- Current-head hosted CI passed:
https://github.com/paperclipai/paperclip/actions/runs/40020746585
(attempt 2). Typecheck, build, all general/Runner/serialized tests and
browser gates are green.
- CI attempt 1 hit a PostgreSQL deadlock in unchanged chat-test cleanup;
its skipped flag restoration caused a second assertion failure. One
failed-jobs-only retry passed. Original failure evidence is preserved in
attempt 1. No paid eval campaign was rerun.
- Greptile reviewed exact head
`5fef9ba7c149951a51eae468614a4b89f0b3d6db` at 5/5; its one finding is
fixed and resolved. The optional Paperclip review is paused and supplied
no assessment.

## Risks

- Evidence hashes bind an audit to a configuration; they do not prove
the audit is true. Operators must review real provider limits, all price
tiers, full resource lifetimes and enforceable infrastructure limits
before configuring bounds.
- Provisional completion is not invoice settlement. Later billing can
reveal overruns, which must be reconciled and inspected. The ledger
cannot stop external billing.
- Existing pilot journals, configured allowances and price cards have no
new bound approval. This PR alone does not qualify or enable recurring
paid work.
- The gateway cannot qualify a canceled Braintrust request without route
confirmation. It preserves that unknown hold.
- No new dependency, secret, repository variable, workflow, schedule or
cloud resource changes.

## Model Used

OpenAI Codex, GPT-6 family. The exact deployment ID and context-window
size are not exposed in this session. Used reasoning, repository tools
and code execution. No paid evaluation model calls were made for this
change.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-23 13:57:57 -07:00

132 lines
8.4 KiB
TypeScript

import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, expect, it } from "vitest";
import { openEvalCostLedger, type EvalSpendLimits } from "./cost-ledger.js";
import { costBoundConfigurationSha256, reservationBound, type EvalCostBoundEvidence } from "./cost-bound-evidence.js";
import { EVAL_OVERHEAD_COMPONENTS } from "./operating-overhead.js";
import { evalAccountingStatus, readEvalOperationsReport, renderEvalOperationsReport, validateEvalOperationsReport } from "./operations-report.js";
import { scheduledCampaignFindings } from "./scheduled-finalization.js";
import type { RunnerE2ECampaign } from "./types.js";
const directories: string[] = [];
afterEach(async () => { for (const dir of directories.splice(0)) await rm(dir, { recursive: true, force: true }); });
const evidence: EvalCostBoundEvidence = { version: 1, scope: "full_lifecycle", evidenceSha256: "a".repeat(64), configurationSha256: costBoundConfigurationSha256({ fixture: true }) };
const identity = { executionId: "cell", attemptId: "one", phase: "agent" as const };
const bound = reservationBound("model_limits", evidence, { fixture: true });
async function fixture(overhead = false) {
const dir = await mkdtemp(path.join(tmpdir(), "provisional-accounting-")); directories.push(dir);
const file = path.join(dir, "ledger.jsonl");
const limits: EvalSpendLimits = { campaignId: "fixture", maxUsd: "20", maxRequests: 20, maxConcurrent: 2,
deadlineAt: "2099-01-01T00:00:00Z", maxAttemptsPerExecution: 1,
...(overhead ? { overhead: { version: 1 as const, reference: "fixture-audit", runner: "github-hosted" as const,
verifiedAt: new Date(Date.now() - 60_000).toISOString(), expiresAt: new Date(Date.now() + 86_400_000).toISOString(),
components: EVAL_OVERHEAD_COMPONENTS.map(component => ({ component, maxUsd: component === "shared_subscription" ? "0" : "1",
reference: `fixture-${component}`, provisionalBound: evidence })) } } : {}) };
if (limits.overhead) for (const component of limits.overhead.components) {
component.provisionalBound = { ...evidence, configurationSha256: costBoundConfigurationSha256({
reference: limits.overhead.reference, runner: limits.overhead.runner, verifiedAt: limits.overhead.verifiedAt, expiresAt: limits.overhead.expiresAt,
component: { component: component.component, maxUsd: component.maxUsd, reference: component.reference },
}) };
}
const ledger = await openEvalCostLedger(file, limits);
await ledger.admitAttempt(identity.executionId, identity.attemptId);
return { ledger, file, limits };
}
function accountingFindings(operations: Awaited<ReturnType<typeof readEvalOperationsReport>>) {
return scheduledCampaignFindings({ results: [], expected: [], operations } as unknown as RunnerE2ECampaign)
.filter(finding => finding.startsWith("accounting_"));
}
it("allows reviewed pending operating/model costs without changing holds or finality, including after restart", async () => {
const { ledger, file, limits } = await fixture(true);
await ledger.reserve("request", identity, "2", bound);
await ledger.observe({ observationId: "partial", requestId: "request", source: "provider", provider: "fixture", currency: "USD",
route: "direct", classification: "estimated", pricingVersion: "fixture", costUsd: "0.1", final: false });
await ledger.finish("request", "cancelled");
await ledger.close();
const original = await readFile(file, "utf8");
const report = await readEvalOperationsReport(file);
expect(report).toMatchObject({ unresolvedRequests: 5, boundedUnresolvedRequests: 5, knownUsd: "0.100000000", heldUsd: "4.900000000", committedUsd: "5.000000000" });
expect(evalAccountingStatus(report)).toBe("provisional");
expect(accountingFindings(report)).toEqual([]);
const rendered = renderEvalOperationsReport(report);
expect(rendered).toContain("Accounting status: provisional");
expect(rendered).toContain("requires later reconciliation");
expect(rendered).not.toContain(evidence.evidenceSha256);
const resumed = await openEvalCostLedger(file, limits);
try { expect(resumed.snapshot()).toMatchObject({ boundedUnresolvedRequests: 5, heldUsd: "4.900000000" }); }
finally { await resumed.close(); }
expect(await readFile(file, "utf8")).toBe(original);
});
it("keeps legacy reservations and legacy reports unqualified instead of inferring a bound from dollars", async () => {
const { ledger, file } = await fixture();
try {
await ledger.reserve("request", identity, "2");
await ledger.finish("request", "cancelled");
const report = await readEvalOperationsReport(file);
expect(evalAccountingStatus(report)).toBe("blocked");
expect(accountingFindings(report)).toEqual(["accounting_incomplete"]);
if (report.status === "unavailable") throw new Error("Missing fixture report");
delete report.boundedUnresolvedRequests;
validateEvalOperationsReport(report);
expect(evalAccountingStatus(report)).toBe("blocked");
expect(() => validateEvalOperationsReport({ ...report, boundedUnresolvedRequests: 2 })).toThrow();
} finally { await ledger.close(); }
});
it.each(["inflight", "invalidated", "partial-over-bound", "conflict", "over-budget"])("blocks %s even with reviewed bound evidence", async reason => {
const { ledger, file } = await fixture();
try {
await ledger.reserve("request", identity, "2", bound);
if (reason === "invalidated") await ledger.invalidateBound("request");
if (reason !== "inflight") await ledger.finish("request", "cancelled");
if (["partial-over-bound", "conflict", "over-budget"].includes(reason)) {
await ledger.observe({ observationId: "first", requestId: "request", source: "provider", provider: "fixture", currency: "USD", route: "direct",
classification: "reported", costUsd: reason === "over-budget" ? "21" : "3", final: reason !== "partial-over-bound" });
}
if (reason === "conflict") await ledger.observe({ observationId: "second", requestId: "request", source: "provider", provider: "fixture",
currency: "USD", route: "direct", classification: "reported", costUsd: "4", final: true });
const report = await readEvalOperationsReport(file);
expect(evalAccountingStatus(report)).toBe("blocked");
expect(accountingFindings(report)).toContain("accounting_incomplete");
if (reason === "over-budget") expect(accountingFindings(report)).toContain("accounting_over_budget");
expect(report).toMatchObject({ boundedUnresolvedRequests: 0 });
} finally { await ledger.close(); }
});
it("lets a later complete receipt settle the original reservation without treating the provisional hold as a charge", async () => {
const { ledger, file } = await fixture();
try {
await ledger.reserve("request", identity, "2", bound);
await ledger.finish("request", "cancelled");
expect(evalAccountingStatus(await readEvalOperationsReport(file))).toBe("provisional");
await ledger.observe({ observationId: "invoice", requestId: "request", source: "invoice", provider: "fixture", currency: "USD",
route: "direct", classification: "reconciled", costUsd: "0.2", final: true });
const report = await readEvalOperationsReport(file);
expect(evalAccountingStatus(report)).toBe("receipts_complete");
expect(report).toMatchObject({ boundedUnresolvedRequests: 0, knownUsd: "0.200000000", heldUsd: "0.000000000" });
} finally { await ledger.close(); }
});
it("rejects malformed or mismatched evidence before creating a reservation", async () => {
const { ledger } = await fixture();
try {
for (const invalid of [ { ...bound, calculationSha256: "missing" }, { ...bound, basis: "operating_allowance" },
{ ...bound, evidence: { ...evidence, scope: "current_day" } }, { ...bound, evidence: { ...evidence, evidenceSha256: "bad" } },
{ ...bound, evidence: { ...evidence, privateField: "not-allowed" } } ]) {
await expect(ledger.reserve("request", identity, "2", invalid as never)).rejects.toThrow();
}
expect(ledger.snapshot().requests).toHaveLength(0);
} finally { await ledger.close(); }
});
it("does not accept null as reviewed operating-bound evidence", async () => {
const { ledger, limits } = await fixture(true);
await ledger.close();
limits.overhead!.components[0]!.provisionalBound = null as never;
const directory = await mkdtemp(path.join(tmpdir(), "provisional-malformed-")); directories.push(directory);
await expect(openEvalCostLedger(path.join(directory, "ledger.jsonl"), limits)).rejects.toThrow("Invalid reviewed cost bound evidence");
});