Files
stack/agents/researcher/work/queue-99/r1/build.patch
T
jason.woltjeandClaude Opus 5.5 8ac8055c4d researcher: row 99 round 1 packet, text-mode inbox fallback, phase B green (#1593)
Built by a Haiku subagent (decision 129). cli 133/0, nine scripts green,
7 of 7 mutants killed. Manifest 5c979e49.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 09:02:24 -05:00

123 lines
5.9 KiB
Diff

diff --git a/packages/cli/src/cli.mjs b/packages/cli/src/cli.mjs
index fcd7c0132..5d92a4d25 100644
--- a/packages/cli/src/cli.mjs
+++ b/packages/cli/src/cli.mjs
@@ -176,11 +176,18 @@ async function human(verb, rest, io, deps) {
if (json) return print(decisions, formatInbox);
const messages = [];
let after;
- do {
- const page = await call("messages", { limit: 200, ...(after ? { after } : {}) });
- messages.push(...page.messages.filter(m => !m.read));
- after = page.next;
- } while (after);
+ try {
+ do {
+ const page = await call("messages", { limit: 200, ...(after ? { after } : {}) });
+ messages.push(...page.messages.filter(m => !m.read));
+ after = page.next;
+ } while (after);
+ } catch (e) {
+ // A bus from before row 94 has no messages verb. Keep the decisions and say
+ // so. A partial list from earlier pages is dropped, not printed as the count.
+ if (e.code !== "unknown-verb") throw e;
+ return io.stdout.write(`${formatInbox(decisions)}\nmessages: not available until the bus restarts\n`);
+ }
return print({ decisions, messages }, value => formatInbox(value.decisions) +
`\nmessages: ${value.messages.length} unread\n` + value.messages.map(formatMessage).join(""));
}
diff --git a/packages/cli/tests/cli.test.mjs b/packages/cli/tests/cli.test.mjs
index 57f8ea22f..cd9dd40fb 100644
--- a/packages/cli/tests/cli.test.mjs
+++ b/packages/cli/tests/cli.test.mjs
@@ -3,6 +3,7 @@ import assert from "node:assert/strict";
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { main, readNotifyConfig } from "../src/cli.mjs";
import { CliError } from "../src/errors.mjs";
+import { formatInbox } from "../src/format.mjs";
import { hostDir, hostFile, startTimeOf } from "../src/host.mjs";
import { broker, io, notifyConfig, tmp } from "./helpers.mjs";
import { writeJson } from "../../business/tests/helpers.mjs";
@@ -43,6 +44,81 @@ test("inbox lists only decisions routed to the human, with what approving author
assert.deepEqual(s.calls.map((c) => c.verb), ["inbox", "messages", "inbox"]);
});
+// Text-mode `inbox` over a stub transport. `inbox` answers from the broker;
+// `messages` answers from messages(args), which returns a page or throws.
+function inboxStub(s, messages) {
+ const calls = [];
+ const transport = () => async (verb, args = {}) => {
+ calls.push({ verb, args });
+ if (verb === "inbox") return s.read("inbox");
+ return messages(args);
+ };
+ return { calls, transport };
+}
+
+const coded = (code) => Object.assign(new Error(code), { code });
+const unread = (id, body) => ({ id, from_role: "coder", class: "INFORM", in_reply_to: null, body, read: false });
+const NO_MESSAGES_VERB = "messages: not available until the bus restarts\n";
+
+test("text-mode inbox on a bus without the messages verb prints the decisions and the unavailable line, exit 0", async (t) => {
+ const s = setup(t);
+ s.raise("git.push.protected", { target: "refactor", blocking: true, task_ref: "vikunja:1/7" });
+ const { calls, transport } = inboxStub(s, () => {
+ throw coded("unknown-verb");
+ });
+ const x = io();
+ assert.equal(await exitOf(main(["inbox", "--business", "demo"], x, { system: { dataRoot: s.dataRoot }, transport })), 0);
+ assert.equal(x.out.text, `${formatInbox(s.read("inbox"))}\n${NO_MESSAGES_VERB}`);
+ assert.deepEqual(calls.map((c) => c.verb), ["inbox", "messages"]);
+});
+
+test("text-mode inbox still fails on any other messages error, and prints nothing", async (t) => {
+ const s = setup(t);
+ s.raise("git.push.protected", { target: "refactor" });
+ for (const code of ["unauthenticated", "invalid-response", "outcome-unknown"]) {
+ const { transport } = inboxStub(s, () => {
+ throw coded(code);
+ });
+ const x = io();
+ const err = await main(["inbox", "--business", "demo"], x, { system: { dataRoot: s.dataRoot }, transport }).catch((e) => e);
+ assert.equal(err?.code, code);
+ assert.equal(x.out.text, "");
+ }
+});
+
+test("text-mode inbox prints the unread count and every unread message across pages", async (t) => {
+ const s = setup(t);
+ s.raise("git.push.protected", { target: "refactor" });
+ const { calls, transport } = inboxStub(s, (args) =>
+ args.after
+ ? { messages: [unread("msg-0003-cccccccc", "third\n")], next: null }
+ : { messages: [unread("msg-0001-aaaaaaaa", "first\n"), { ...unread("msg-0002-bbbbbbbb", "read\n"), read: true }], next: "msg-0002-bbbbbbbb" },
+ );
+ const x = io();
+ assert.equal(await exitOf(main(["inbox", "--business", "demo"], x, { system: { dataRoot: s.dataRoot }, transport })), 0);
+ assert.equal(
+ x.out.text,
+ `${formatInbox(s.read("inbox"))}\nmessages: 2 unread\ncoder (INFORM) msg-0001:\nfirst\ncoder (INFORM) msg-0003:\nthird\n`,
+ );
+ assert.deepEqual(
+ calls.filter((c) => c.verb === "messages").map((c) => c.args),
+ [{ limit: 200 }, { limit: 200, after: "msg-0002-bbbbbbbb" }],
+ );
+});
+
+test("a later page answered unknown-verb drops the partial list and prints only the unavailable line", async (t) => {
+ const s = setup(t);
+ s.raise("git.push.protected", { target: "refactor" });
+ const { calls, transport } = inboxStub(s, (args) => {
+ if (args.after) throw coded("unknown-verb");
+ return { messages: [unread("msg-0001-aaaaaaaa", "first\n")], next: "msg-0001-aaaaaaaa" };
+ });
+ const x = io();
+ assert.equal(await exitOf(main(["inbox", "--business", "demo"], x, { system: { dataRoot: s.dataRoot }, transport })), 0);
+ assert.equal(x.out.text, `${formatInbox(s.read("inbox"))}\n${NO_MESSAGES_VERB}`);
+ assert.deepEqual(calls.map((c) => c.verb), ["inbox", "messages", "messages"]);
+});
+
test("decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow", async (t) => {
const s = setup(t);
const d = s.raise("git.push.protected", { target: "refactor", blocking: true, task_ref: "vikunja:1/7" });