Merge reviewed ACPX receipt retention and deterministic test teardown

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Devin FoleyandPaperclip committed 2026-10-04 00:23:12 -07:00
commit 3787c14a93
3 files changed
+48 -1

No files matched your search

@@ -1450,6 +1450,45 @@ describe("shared ACPX engine runtime behavior", () => {
expect(statusLine?.text).toContain('"cost"');
});
it.each(["checkpoint", "terminal"])("returns received usage after the %s receipt sink rejects and still closes the runtime", async (failedSave) => {
const root = await makeTempRoot();
const close = vi.fn(async () => {});
const cancel = vi.fn(async () => {});
let reads = 0;
const execute = createAcpxEngineExecutor({
resolveBillingIdentity: () => ({ provider: "anthropic", biller: "anthropic", billingType: "api" }),
createRuntime: () => ({
...buildRuntime(),
getStatus: async () => ++reads === 1
? { usage: { cost: { amount: 0.4, currency: "USD" } } }
: { usage: { cumulative: { inputTokens: 120, outputTokens: 4500, cachedReadTokens: 900, cachedWriteTokens: 30 }, cost: { amount: 1.15, currency: "USD" } } },
startTurn: () => ({
events: (async function* () {
yield { type: "status", text: "usage", tag: "usage_update",
breakdown: { inputTokens: 100, outputTokens: 4000, cachedReadTokens: 800, cachedWriteTokens: 20 }, cost: { amount: 1.1, currency: "USD" } };
})(),
result: Promise.resolve({ status: "completed", stopReason: "end_turn" }), cancel,
}), close,
}) as never,
});
const onUsage = vi.fn(async (receipt: { complete: boolean }) => {
if (failedSave === "checkpoint" || receipt.complete) throw new Error("Receipt storage unavailable");
});
const result = await execute({ runId: `failed-${failedSave}-save`, agent: { id: "agent-1", companyId: "company-1" },
runtime: {}, config: { agent: "custom", agentCommand: "node ./fake-acp.js", stateDir: path.join(root, "state") },
context: {}, onMeta: async () => {}, onLog: async () => {}, onUsage } as never);
expect(result).toMatchObject({ exitCode: 1, usageBasis: "per_run", usageComplete: failedSave === "terminal",
provider: "anthropic", biller: "anthropic", billingType: "api" });
expect(result.errorMessage).toContain("Receipt storage unavailable");
expect(result.usage).toEqual(failedSave === "terminal"
? { inputTokens: 150, outputTokens: 4500, cachedInputTokens: 900 }
: { inputTokens: 120, outputTokens: 4000, cachedInputTokens: 800 });
expect(result.costUsd).toBeCloseTo(failedSave === "terminal" ? 0.75 : 0.7);
expect(onUsage).toHaveBeenCalledTimes(failedSave === "terminal" ? 2 : 1);
expect(cancel).toHaveBeenCalledTimes(1);
expect(close).toHaveBeenCalledTimes(1);
});
it("falls back to usage_update events when the runtime lacks getStatus", async () => {
const root = await makeTempRoot();
const stateDir = path.join(root, "state");
@@ -4704,6 +4704,9 @@ export function createAcpxEngineExecutor(deps: AcpxEngineExecutorOptions = {}) {
};
let eventBreakdown: AcpRuntimeUsageBreakdown | null = null;
let eventCostUsd: number | null = null;
// Receipt persistence is fallible; retain the provider evidence before
// calling the sink so the failure result can still return it to the host.
let observedUsage: Pick<AdapterExecutionResult, "usage" | "usageBasis" | "costUsd" | "usageComplete"> = { usageComplete: false };
let lastRuntimeEventAt: number | null = null;
let observedRuntimeEvents = 0;
// The turn-local state the sequence steps share. `promptBuild` sets the
@@ -4905,6 +4908,7 @@ export function createAcpxEngineExecutor(deps: AcpxEngineExecutorOptions = {}) {
eventBreakdown = event.breakdown ?? eventBreakdown;
eventCostUsd = usdCostAmount(event.cost) ?? eventCostUsd;
const checkpoint = summarizeAcpxTurnUsage({ preStatus: preTurnStatus, postStatus: null, eventBreakdown, eventCostUsd });
observedUsage = { usage: checkpoint.usage ?? undefined, costUsd: checkpoint.costUsd, usageBasis: "per_run", usageComplete: false };
await ctx.onUsage?.({ ...billingFields, usage: checkpoint.usage ?? undefined, costUsd: checkpoint.costUsd,
model: prepared.requestedModel, usageBasis: "per_run", complete: false });
}
@@ -4996,6 +5000,7 @@ export function createAcpxEngineExecutor(deps: AcpxEngineExecutorOptions = {}) {
// An interrupted stream is provisional unless the runtime supplies its
// post-terminal receipt. An in-stream usage_update alone cannot close it.
const usageComplete = !channelLost && (turnSucceeded || postTurnStatus?.usage != null);
observedUsage = { usage: turnUsage.usage ?? undefined, costUsd: turnUsage.costUsd, usageBasis: "per_run", usageComplete };
await ctx.onUsage?.({ ...billingFields, usage: turnUsage.usage ?? undefined, costUsd: turnUsage.costUsd,
model: prepared.requestedModel, usageBasis: "per_run", complete: usageComplete });
const failedTurn = terminal.status === "failed" || terminal.status === "cancelled" || timedOut;
@@ -5217,6 +5222,7 @@ export function createAcpxEngineExecutor(deps: AcpxEngineExecutorOptions = {}) {
errorMessage: message,
errorCode: timedOut ? "acpx_timeout" : (emitted?.classified.errorCode ?? null),
errorMeta: emitted?.classified.errorMeta,
...observedUsage,
...billingFields,
...referencedProjectStagingFailuresField,
model: prepared.requestedModel || null,
@@ -550,7 +550,9 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
}
await new Promise((resolve) => setTimeout(resolve, 50));
await waitForHeartbeatIdle(db, 5_000);
await new Promise((resolve) => setTimeout(resolve, 100));
// Terminal rows do not mean their background finalizers have returned.
// Drain tracked wakeups too: one can still create a run after the row poll.
await heartbeatService(db).drainActiveRunExecutions();
await db.delete(activityLog);
await db.delete(agentRuntimeState);
await db.delete(companySkills);