In progress<\/h2>[\s\S]*?<\/ol>/)![0]).toContain("Fix");
expect(thread(html)).toContain("none of your runners are online");
@@ -143,6 +143,7 @@ describe("decision inbox", () => {
const [job] = await store.listJobs(db, 10);
const stopped = await submit(`${jobs}/${job!.id}/cancel`, []);
expect(stopped.status).toBe(303);
+ expect(stopped.headers.get("Location")).toBe("/inbox?done=stop");
expect((await store.readJob(db, job!.id))!.state).toBe("cancelled");
// A stopped job sets its item aside until the pull request changes.
expect(await inbox()).toMatch(/Skipped and finished \(1\)[\s\S]*Fix/);
@@ -158,11 +159,18 @@ describe("decision inbox", () => {
expect(moved.status).toBe(403);
expect(await moved.text()).toContain("head moved since you looked; review it again");
github.refuse = "At least 1 approving review is required by reviewers with write access.";
- const refused = await submit(merge, [["head", head]]);
+ const refused = await submit(merge, [["head", head]], {
+ Referer: `${origin}/inbox?project=10&item=pulls%2F2`,
+ });
expect(refused.status).toBe(403);
- expect(await refused.text()).toContain(
+ // Any refused action reads neutrally, says why, and leads back to its item.
+ const explained = await refused.text();
+ expect(explained).toContain("Couldn't do that
");
+ expect(explained).not.toContain("Not recorded");
+ expect(explained).toContain(
"GitHub refused the merge: At least 1 approving review is required by reviewers with write access.",
);
+ expect(explained).toContain('Return to the item');
// Each click is one request bound to its head; a refusal is never retried.
expect(github.merged).toHaveLength(2);
github.refuse = null;
@@ -172,7 +180,7 @@ describe("decision inbox", () => {
["version", "0"],
]);
expect(merged.status).toBe(303);
- expect(merged.headers.get("Location")).toBe("/inbox");
+ expect(merged.headers.get("Location")).toBe("/inbox?done=merge");
expect(github.merged.at(-1)).toEqual({ sha: head, merge_method: "squash" });
// The merged item is finished, and no new work starts before GitHub is read again.
const html = await inbox();
@@ -268,6 +276,12 @@ describe("decision inbox", () => {
).toBe(409);
const answered = await submit(`${jobs}/${id}/answer`, answer);
expect(answered.status).toBe(303);
+ // The inbox moves on to the next decision and notes the answer there.
+ expect(answered.headers.get("Location")).toBe("/inbox?done=answer");
+ const next = await inbox({ done: "answer" });
+ expect(next).toContain('
Answer recorded.
'); + expect(thread(next)).toContain("Fix"); + expect(next.match(/In progress<\/h2>[\s\S]*?<\/ol>/)![0]).toContain("First");
const job = (await store.readJob(db, id))!;
expect(job.state).toBe("queued");
expect(job.questions[0]!.answer).toEqual({
@@ -452,6 +466,7 @@ describe("decision inbox", () => {
["version", "0"],
]);
expect(skipped.status).toBe(303);
+ expect(skipped.headers.get("Location")).toBe("/inbox?done=skip");
let html = await inbox();
expect(html.match(/Up next<\/h2>[\s\S]*?<\/ol>/)![0]).not.toContain("Idea");
expect(html).toMatch(/Skipped and finished \(1\)[\s\S]*Idea/);
@@ -462,10 +477,13 @@ describe("decision inbox", () => {
it("reports a project without current authority and hides its items", async () => {
github.role = "write";
- const html = await inbox();
+ const html = await inbox({ done: "answer" });
expect(html).toContain("Unavailable: Current GitHub maintainer authority required");
expect(html).not.toContain("Idea");
+ // With no decision left, the notice shows on the empty state.
+ expect(html).toContain("Answer recorded.");
expect(html).toContain("Nothing needs a decision right now.");
+ expect(await inbox({ done: "constructor" })).not.toContain('
'); }); it("keeps paused projects' questions but offers no new work", async () => { diff --git a/quest/m0/kix/README.md b/quest/m0/kix/README.md index 6fc6af67..2020c36b 100644 --- a/quest/m0/kix/README.md +++ b/quest/m0/kix/README.md @@ -104,7 +104,6 @@ operator-paid-hosting decisions: - [Webhook reconcile](/quest/m0/kix/reconcile.md) - new issues and PRs arrive in the inbox ready to act on, without a manual refresh - [Merge jobs](/quest/m0/kix/merge-job.md) - a merge pick fixes conflicts, CI, and reviews on a runner, then lands the PR - [Co-maintainer inbox](/quest/m0/kix/members.md) - every current maintainer sees enrolled projects in their inbox -- [Answer flow](/quest/m0/kix/answer-flow.md) - move to the next decision after an answer, with neutral refusals - [Retry from the branch](/quest/m0/kix/retry.md) - a retried job continues from the head its failed attempt published - [Decision paper trail](/quest/m0/kix/paper-trail.md) - planning PRs carry the recorded questions and answers, rendered by the service - [Agent activity](/quest/m0/kix/activity.md) - agent cards and map highlights built from checkpoint summaries diff --git a/quest/m0/kix/answer-flow.md b/quest/m0/kix/answer-flow.md deleted file mode 100644 index 8497c1d6..00000000 --- a/quest/m0/kix/answer-flow.md +++ /dev/null @@ -1,20 +0,0 @@ -# [XS] Move to the next decision after an answer - -## Goal - -After a maintainer answers a decision in the inbox, the page moves on to the -next active decision with a short "Answer recorded" notice. When an action is -refused, the page has a neutral title and says why, whatever the action was. - -## Plan - -Decided 2026-10-19 as a follow-up of the decision inbox (#144), which stays -on the item just answered, and titles every refusal "Not recorded" even for -a refused "Refresh from GitHub": - -- Redirect to the next active decision, or to the inbox's empty state, and - show the notice there. The answered item stays reachable under its - earlier answers. -- Retitle the refused-action page neutrally (for example "Couldn't do - that"), with the specific reason and a link back to the item. -- Keep the form posts working without JavaScript. Update the inbox tests.