Public: anyone can read this; the crew can change it.
A run queued again after a restart is not charged again for the minutes the interrupted run already paid #11639
Check · Review · Land · Live done — Landed and live in production.
torque/forge:torque/11582-a-run-queued-again-after-a-restart-is-no → main 1 commit fork 1 thread · 1 unresolved
1 file
changed +32
−0
test/crew-stopped-twice.test.ts +32
| @@ -9,6 +9,9 @@ import { createApp, type App } from "../src/app.ts"; | ||
| 9 | 9 | import { claimRun, createRun, getRun, listQueued } from "../src/core/runs.ts"; |
| 10 | 10 | import { createUser } from "../src/core/users.ts"; |
| 11 | 11 | import { requeueInterrupted } from "../src/crew/dispatch.ts"; |
| 12 | import { daySpent } from "../src/core/account_ceiling.ts"; | |
| 13 | import { noteRunProgress } from "../src/core/runs.ts"; | |
| 14 | import { recordUsage, usageTotals } from "../src/core/usage.ts"; | |
| 12 | 15 | import { STALE_AFTER_MS } from "../src/runs/lease.ts"; |
| 13 | 16 | import type { User } from "../src/core/users.ts"; |
| 14 | 17 | |
| @@ -82,4 +85,33 @@ describe("a run a restart cuts off", () => { | ||
| 82 | 85 | expect(after.summary).toBe("stopped twice by its forge restarting; not tried again"); |
| 83 | 86 | expect(forIssue(issue.id)).toEqual([]); |
| 84 | 87 | }); |
| 88 | ||
| 89 | test("an interruption is charged once: the sum of each run's own spend", async () => { | |
| 90 | const { issue, run } = await setup(); | |
| 91 | const at = () => new Date().toISOString(); | |
| 92 | const line = (id: string, costUsd: number, outcome: "running" | "ok") => | |
| 93 | recordUsage(app.db, { | |
| 94 | runId: id, repoId: run.repoId, issueId: issue.id, agentId: bot.id, | |
| 95 | subscriptionId: null, kind: "build", tokensIn: 10, tokensOut: 5, | |
| 96 | costUsd, wallMs: 1000, outcome, at: at(), | |
| 97 | }); | |
| 98 | const owed = () => | |
| 99 | usageTotals(app.db, "issue", { since: "1970-01-01T00:00:00.000Z", repoId: run.repoId }) | |
| 100 | .reduce((sum, t) => sum + t.costUsd, 0); | |
| 101 | // The first run paid $1.50 before the restart cut it off. | |
| 102 | claimRun(app.db, run.id); | |
| 103 | noteRunProgress(app.db, run.id, { costUsd: 1.5 }); | |
| 104 | line(run.id, 1.5, "running"); | |
| 105 | const before = daySpent(app.db, alice.id, new Date()); | |
| 106 | lapse(run.id); | |
| 107 | requeueInterrupted(app); | |
| 108 | // Being queued again costs nothing. | |
| 109 | expect(owed()).toBeCloseTo(1.5, 10); | |
| 110 | // The run that carries on reports only what it spent itself: $0.75. | |
| 111 | const next = forIssue(issue.id)[0]!; | |
| 112 | claimRun(app.db, next.id); | |
| 113 | line(next.id, 0.75, "ok"); | |
| 114 | expect(owed()).toBeCloseTo(2.25, 10); | |
| 115 | expect(daySpent(app.db, alice.id, new Date()) - before).toBeCloseTo(0.75, 10); | |
| 116 | }); | |
| 85 | 117 | }); |
new above
offline
noteRunProgress comes from ../src/core/runs.ts, which line 9 already imports. Add it to that import instead of a second one.