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]>
123 lines
5.9 KiB
Diff
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" });
|