Public: anyone can read this; the crew can change it.
A run picked "none" never starts: it spends nothing and says "This repository has no model account" on its page and its issue #2128
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/2096-a-run-picked-none-never-starts-it-spends → main 1 commit fork
2 files
changed +63
−2
src/crew/runner.ts +1 −1
| @@ -923,7 +923,7 @@ async function work( | ||
| 923 | 923 | // else's run waits for a grant rather than spend what is not theirs. |
| 924 | 924 | if (picked.kind === "none") { |
| 925 | 925 | const said = |
| 926 | "no model credential reaches this repository; ask an admin to grant one"; | |
| 926 | "This repository has no model account: ask an admin to grant one"; | |
| 927 | 927 | write(said); |
| 928 | 928 | return { state: "failed", summary: said }; |
| 929 | 929 | } |
test/crew-nocredential.test.ts +62 −1
| @@ -8,7 +8,11 @@ import { existsSync } from "node:fs"; | ||
| 8 | 8 | import type { App } from "../src/app.ts"; |
| 9 | 9 | import type { Repo } from "../src/core/repos.ts"; |
| 10 | 10 | import type { User } from "../src/core/users.ts"; |
| 11 | import { addToCrew } from "../src/actions/crew.ts"; | |
| 12 | import { commitToBranch, createRepo } from "../src/actions/repos.ts"; | |
| 13 | import { createUser } from "../src/core/users.ts"; | |
| 11 | 14 | import { assignIssue, openIssue } from "../src/actions/issues.ts"; |
| 15 | import { getUsage } from "../src/core/usage.ts"; | |
| 12 | 16 | import { readRunLog } from "../src/runs/log.ts"; |
| 13 | 17 | import { |
| 14 | 18 | agentRunSettled, |
| @@ -47,7 +51,7 @@ describe("the crew, with a stand-in Claude Code: no credential reaches the repos | ||
| 47 | 51 | (r) => r.issueId === issue.value.id, |
| 48 | 52 | ); |
| 49 | 53 | const said = |
| 50 | "no model credential reaches this repository; ask an admin to grant one"; | |
| 54 | "This repository has no model account: ask an admin to grant one"; | |
| 51 | 55 | expect(run.state).toBe("failed"); |
| 52 | 56 | expect(run.summary).toContain(said); |
| 53 | 57 | expect(readRunLog(standIn.dataDir, run.id).lines.join("\n")).toContain( |
| @@ -55,8 +59,65 @@ describe("the crew, with a stand-in Claude Code: no credential reaches the repos | ||
| 55 | 59 | ); |
| 56 | 60 | // The agent never started, so no credential was handed to it. |
| 57 | 61 | expect(existsSync(standIn.argsFile)).toBe(false); |
| 62 | // Nothing spent: no ledger row for the run, no cost on it. | |
| 63 | expect(getUsage(app.db, run.id)?.costUsd ?? 0).toBe(0); | |
| 64 | expect(run.costUsd).toBe(0); | |
| 58 | 65 | } finally { |
| 59 | 66 | delete process.env.ANTHROPIC_API_KEY; |
| 60 | 67 | } |
| 61 | 68 | }, 60_000); |
| 69 | ||
| 70 | test("a fresh outsider's repository with an existing agent does not start; an admin's does", async () => { | |
| 71 | process.env.ANTHROPIC_API_KEY = "sk-forge-own-token"; | |
| 72 | try { | |
| 73 | const o = await createUser(app.db, { | |
| 74 | trusted: true, | |
| 75 | username: "otto", | |
| 76 | email: "[email protected]", | |
| 77 | password: "a-good-passphrase", | |
| 78 | }); | |
| 79 | if (!o.ok) throw new Error(o.error); | |
| 80 | const otto = o.value; | |
| 81 | const made = await createRepo(app, otto, { | |
| 82 | name: "yard", | |
| 83 | visibility: "private", | |
| 84 | }); | |
| 85 | if (!made.ok) throw new Error(String(made.error)); | |
| 86 | const yard = made.value; | |
| 87 | const seeded = await commitToBranch(app, otto, yard, { | |
| 88 | message: "Start", | |
| 89 | files: [{ path: "README.md", content: "# yard\n" }], | |
| 90 | }); | |
| 91 | if (!seeded.ok) throw new Error(JSON.stringify(seeded.error)); | |
| 92 | expect(addToCrew(app, otto, yard, "bot").ok).toBe(true); | |
| 93 | const issue = openIssue(app, otto, yard, { title: "Mow", body: "Soon." }); | |
| 94 | if (!issue.ok) throw new Error(String(issue.error)); | |
| 95 | expect(assignIssue(app, otto, yard, issue.value, bot).ok).toBe(true); | |
| 96 | const run = await agentRunSettled( | |
| 97 | app, | |
| 98 | yard, | |
| 99 | (r) => r.issueId === issue.value.id, | |
| 100 | ); | |
| 101 | expect(run.state).toBe("failed"); | |
| 102 | expect(run.summary).toContain("This repository has no model account"); | |
| 103 | expect(run.costUsd).toBe(0); | |
| 104 | expect(getUsage(app.db, run.id)?.costUsd ?? 0).toBe(0); | |
| 105 | expect(existsSync(standIn.argsFile)).toBe(false); | |
| 106 | ||
| 107 | // The same flow, the owner an admin: the run starts. | |
| 108 | app.db.query("UPDATE user SET is_admin = 1 WHERE id = ?1").run(otto.id); | |
| 109 | const again = openIssue(app, otto, yard, { title: "Rake", body: "Now." }); | |
| 110 | if (!again.ok) throw new Error(String(again.error)); | |
| 111 | expect(assignIssue(app, otto, yard, again.value, bot).ok).toBe(true); | |
| 112 | const started = await agentRunSettled( | |
| 113 | app, | |
| 114 | yard, | |
| 115 | (r) => r.issueId === again.value.id, | |
| 116 | ); | |
| 117 | expect(started.summary).not.toContain("no model account"); | |
| 118 | expect(existsSync(standIn.argsFile)).toBe(true); | |
| 119 | } finally { | |
| 120 | delete process.env.ANTHROPIC_API_KEY; | |
| 121 | } | |
| 122 | }, 90_000); | |
| 62 | 123 | }); |
new above
offline