forgedock 1.24.5 → 1.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -43,6 +43,8 @@ function world(o = {}) {
43
43
  const k = a.slice(0, 2).join(" ");
44
44
  if (a[0] === "api") return "";
45
45
  if (k === "pr list") return JSON.stringify(w.prList);
46
+ if (k === "repo view") return `${o.defaultBranch ?? "main"}\n`;
47
+ if (k === "issue view" && a.includes("milestone")) { w.seq.push("lane"); return JSON.stringify({ milestone: o.milestone ? { title: o.milestone } : null }); }
46
48
  if (k === "issue view") return JSON.stringify({ number: 7, title: "foo breaks", body: "Bug in foo", state: "OPEN" });
47
49
  if (k === "issue comment" && w.commentErrors.length) throw new Error(w.commentErrors.shift());
48
50
  if (k === "issue comment") { w.comments.push(a[a.indexOf("--body") + 1]); w.seq.push("comment"); return `https://github.com/o/r/issues/7#issuecomment-${w.comments.length}\n`; }
@@ -77,6 +79,7 @@ function world(o = {}) {
77
79
  if (a[0] === "rev-parse") return w.head;
78
80
  if (a[0] === "diff" && a[1] === "--name-only") return w.changed.join("\n");
79
81
  if (a[0] === "diff") return "diff --git a/src/foo.js b/src/foo.js\n+fix";
82
+ if (a[0] === "ls-remote" && String(a.at(-1)).startsWith("refs/heads/")) { const b = a.at(-1).slice("refs/heads/".length); return (o.branchesOnOrigin ?? ["staging", "main"]).includes(b) ? `abc\trefs/heads/${b}\n` : ""; }
80
83
  if (a[0] === "ls-remote") return w.remote;
81
84
  if (a[0] === "push") w.seq.push("push");
82
85
  return "";
@@ -236,11 +239,86 @@ describe("core work-on", () => {
236
239
  assert.equal(builders(w).length, 1);
237
240
  });
238
241
 
239
- it("builder blocked, or no commit, is needs-human", async () => {
240
- assert.equal((await run(world({ result: { status: "blocked", summary: "need creds" } }))).terminalReason, "needs-human");
242
+ it("builder blocked on a typed human reason, or no commit, is needs-human", async () => {
243
+ assert.equal((await run(world({ result: { status: "blocked", summary: "need creds", block: { kind: "human", why: "credentials" } } }))).terminalReason, "needs-human");
241
244
  assert.equal((await run(world({ changed: [] }))).terminalReason, "needs-human");
242
245
  });
243
246
 
247
+ // #4124: a builder block is typed; only kind "human" with an allowed reason may reach needs-human.
248
+ const depWorld = (o, deps) => {
249
+ const w = world(o);
250
+ const gh = w.io.gh;
251
+ w.created = [];
252
+ w.io.gh = async (a) => {
253
+ if (a[0] === "issue" && a[1] === "view" && deps[a[2]]) return JSON.stringify(deps[a[2]]);
254
+ if (a[0] === "issue" && a[1] === "create") { w.created.push(a); return `https://github.com/o/r/issues/${200 + w.created.length}\n`; }
255
+ return gh(a);
256
+ };
257
+ return w;
258
+ };
259
+ const noNeedsHuman = (w) => {
260
+ assert.ok(!w.edits.some((e) => e.includes("--add-label") && e.includes("needs-human")), JSON.stringify(w.edits));
261
+ assert.ok(!w.comments.some((c) => c.includes("FORGE:NEEDS_HUMAN")));
262
+ };
263
+
264
+ it("#4124 a builder block naming an open issue ends dependency-open with dependencies, not needs-human", async () => {
265
+ const w = depWorld({ changed: [], result: { status: "blocked", summary: "needs #34927 first", block: { kind: "dependency", issues: ["#3", 7] } } },
266
+ { 3: { state: "OPEN", labels: [] } });
267
+ const dir = tmpDir("lean-dep-");
268
+ const r = await run(w, { dir });
269
+ assert.equal(r.terminalReason, "dependency-open");
270
+ assert.equal(r.code, "dependency.open");
271
+ assert.deepEqual(r.dependencies, [3]);
272
+ assert.equal(readLog(dir, 7).find((e) => e.event === "RUN_TERMINAL").code, "dependency.open");
273
+ assert.ok(w.comments.some((c) => c.includes("FORGE:DEPENDENCY") && c.includes("#3")));
274
+ assert.ok(w.edits.some((e) => e.includes("--remove-label")), "workflow labels are cleared");
275
+ assert.equal(builders(w).length, 1);
276
+ noNeedsHuman(w);
277
+ });
278
+
279
+ it("#4124 an open dependency that is itself needs-human ends dependency-blocked; this issue is not labelled needs-human", async () => {
280
+ const w = depWorld({ changed: [], result: { status: "blocked", summary: "upstream", block: { kind: "dependency", issues: [3] } } },
281
+ { 3: { state: "OPEN", labels: [{ name: "needs-human" }] } });
282
+ const r = await run(w);
283
+ assert.equal(r.terminalReason, "dependency-blocked");
284
+ assert.equal(r.code, "dependency.blocked");
285
+ assert.deepEqual(r.blockers, [3]);
286
+ noNeedsHuman(w);
287
+ });
288
+
289
+ it("#4124 split-required files sub-issues with Parent / Depends-on lines and ends decomposed", async () => {
290
+ const w = depWorld({ changed: [], result: { status: "blocked", summary: "epic", block: { kind: "split-required", subissues: [
291
+ { title: "part A", body: "do A" }, { title: "part B", body: "do B", depends_on: [0] }] } } }, {});
292
+ const r = await run(w);
293
+ assert.equal(r.terminalReason, "decomposed");
294
+ assert.deepEqual(r.subissues, [201, 202]);
295
+ const bodyB = w.created[1][w.created[1].indexOf("--body") + 1];
296
+ assert.match(bodyB, /^\*\*Parent\*\*: #7$/m);
297
+ assert.match(bodyB, /^\*\*Depends on\*\*: #201$/m);
298
+ assert.ok(w.labels.includes("decomposed"));
299
+ noNeedsHuman(w);
300
+ });
301
+
302
+ it("#4124 a prod-data (non-human) or untyped block gets one corrective resume; a builder that then proceeds merges", async () => {
303
+ const w = world({ result: { status: "blocked", summary: "needs live wire capture and prod baselines", block: { kind: "human", why: "prod-data" } },
304
+ resumeResult: { status: "fixed", summary: "captured via read-only monitoring", evidence: [], tests: [] } });
305
+ const r = await run(w);
306
+ assert.equal(r.terminalReason, "merged");
307
+ const resumed = builders(w).filter((c) => c[2]);
308
+ assert.equal(resumed.length, 1);
309
+ assert.match(resumed[0][4], /not a block that needs a human/);
310
+ noNeedsHuman(w);
311
+ });
312
+
313
+ it("#4124 a block that stays untyped after the correction is needs-human with builder.block_untyped; a closed dependency is not a block", async () => {
314
+ const w = depWorld({ changed: [], result: { status: "blocked", summary: "upstream", block: { kind: "dependency", issues: [3] } } },
315
+ { 3: { state: "CLOSED", labels: [] } });
316
+ const r = await run(w);
317
+ assert.equal(r.terminalReason, "needs-human");
318
+ assert.equal(r.code, "builder.block_untyped");
319
+ assert.equal(builders(w).filter((c) => c[2]).length, 1);
320
+ });
321
+
244
322
  it("worktree add retries a concurrent .git lock collision, then gives up after 5 tries", async () => {
245
323
  const w = world(); w.lockFails = 2;
246
324
  assert.equal((await run(w)).terminalReason, "merged");
@@ -259,7 +337,8 @@ describe("core work-on", () => {
259
337
  assert.equal(term[0].code, r.code);
260
338
  return term[0].code;
261
339
  };
262
- assert.equal(await code(world({ result: { status: "blocked", summary: "need creds" } })), "builder.blocked");
340
+ assert.equal(await code(world({ result: { status: "blocked", summary: "need creds", block: { kind: "human", why: "credentials" } } })), "builder.blocked");
341
+ assert.equal(await code(world({ result: { status: "blocked", summary: "need creds" } })), "builder.block_untyped");
263
342
  assert.equal(await code(world({ changed: [] })), "builder.no_commit");
264
343
  assert.equal(await code(world({ waits: [{ rc: 1, text: "lint failed" }] }), { mergeWait: "all" }), "ci.red");
265
344
  });
@@ -565,6 +644,11 @@ describe("orchestrate", () => {
565
644
  assert.deepEqual(parseDeps("x\n**Depends on**: #4\n**Depends on**: #7\n", 7), [4]);
566
645
  });
567
646
 
647
+ it("#4121 parseDeps reads every declaration form, all numbers, and ignores mid-line prose", () => {
648
+ assert.deepEqual(parseDeps("Depends on #3, #5\n**Depends on** #6\nBlocked by #8\n- depends on: #9 and #10\n**Blocked by:** #11 — why", 1), [3, 5, 6, 8, 9, 10, 11]);
649
+ assert.deepEqual(parseDeps("This is blocked by #2 and depends on #4\nsee **Depends on**: #5", 1), []);
650
+ });
651
+
568
652
  it("runs at most K at once", async () => {
569
653
  const h = harness({});
570
654
  const rows = await orchestrate({ issues: [1, 2, 3, 4, 5, 6], parallel: 2, ...h });
@@ -593,6 +677,44 @@ describe("orchestrate", () => {
593
677
  assert.match(rows[1].detail, /#1/);
594
678
  });
595
679
 
680
+ it("#4124 dependency-open requeues the issue behind an out-of-set dependency and does not skip its dependents", async () => {
681
+ const log = [];
682
+ let firstRun = true;
683
+ const rows = await orchestrate({ issues: [1, 2], parallel: 2,
684
+ readBody: async (n) => (n === 2 ? "**Depends on**: #1" : ""),
685
+ runOne: async (n) => {
686
+ log.push(n);
687
+ if (n === 1 && firstRun) { firstRun = false; return { terminalReason: "dependency-open", dependencies: [9], detail: "depends on #9" }; }
688
+ return { terminalReason: "merged", pr: 100 + n };
689
+ } });
690
+ assert.deepEqual(log, [1, 9, 1, 2]);
691
+ assert.deepEqual(rows.map((r) => [r.issue, r.outcome]), [[1, "merged"], [2, "merged"], [9, "merged"]]);
692
+ });
693
+
694
+ it("#4124 a repeated dependency-open stops after the requeue cap", async () => {
695
+ const h = harness({}, { 1: "dependency-open" });
696
+ const clock = { t: 0 }, starts = [];
697
+ const rows = await orchestrate({ issues: [1], readBody: h.readBody, now: () => clock.t, sleep: async (ms) => { clock.t += ms; },
698
+ runOne: async (n) => { if (n === 1) { starts.push(clock.t); return { terminalReason: "dependency-open", dependencies: [5] }; } return { terminalReason: "merged" }; } });
699
+ assert.equal(starts.length, 3);
700
+ assert.equal(rows[0].outcome, "dependency-open");
701
+ // #4121: #5 already merged but #1 still reports it open: wait before relaunching instead of a hot loop.
702
+ assert.ok(starts[2] - starts[1] >= 60_000, JSON.stringify(starts));
703
+ });
704
+
705
+ it("#4124 decomposed runs its sub-issues and the parent's dependents wait on them", async () => {
706
+ const log = [];
707
+ const rows = await orchestrate({ issues: [1, 2], parallel: 3,
708
+ readBody: async (n) => ({ 2: "**Depends on**: #1", 12: "**Parent**: #1\n**Depends on**: #11" })[n] ?? "",
709
+ runOne: async (n) => {
710
+ log.push(`start ${n}`); await new Promise((r) => setImmediate(r)); log.push(`end ${n}`);
711
+ return n === 1 ? { terminalReason: "decomposed", subissues: [11, 12] } : { terminalReason: "merged" };
712
+ } });
713
+ assert.deepEqual(rows.map((r) => [r.issue, r.outcome]), [[1, "decomposed"], [2, "merged"], [11, "merged"], [12, "merged"]]);
714
+ assert.ok(log.indexOf("end 11") < log.indexOf("start 12"));
715
+ assert.ok(log.indexOf("end 12") < log.indexOf("start 2"));
716
+ });
717
+
596
718
  it("a dependency that closed as invalid does not block", async () => {
597
719
  const h = harness({ 2: "**Depends on**: #1" }, { 1: "invalid" });
598
720
  const rows = await orchestrate({ issues: [1, 2], parallel: 2, ...h });
@@ -607,6 +729,22 @@ describe("orchestrate", () => {
607
729
  assert.equal(rows2[0].outcome, "engine-error");
608
730
  });
609
731
 
732
+ it("#4122 same-file issues run in parallel; a needs-human peer does not skip them", async () => {
733
+ const h = harness({ 1: "Fix `app/scrape_phases.py` retry", 2: "Also touches `app/scrape_phases.py`", 3: "See app/scrape_phases.py" }, { 1: "needs-human" });
734
+ const rows = await orchestrate({ issues: [1, 2, 3], parallel: 3, ...h });
735
+ assert.equal(h.log.maxActive, 3);
736
+ assert.deepEqual(rows.map((r) => r.outcome), ["needs-human", "merged", "merged"]);
737
+ });
738
+
739
+ it("#4122 /orchestrate spec builds no blocking edge from file overlap and skips only through explicit/logical edges", async () => {
740
+ const { readFileSync } = await import("node:fs");
741
+ const spec = readFileSync(new URL("../../commands/orchestrate.md", import.meta.url), "utf8");
742
+ assert.doesNotMatch(spec, /\*\*Conflict:\*\*/);
743
+ assert.doesNotMatch(spec, /run one after the\s+other/);
744
+ assert.match(spec, /File overlap is \*\*never\*\* an edge/);
745
+ assert.match(spec, /needs-human[\s\S]*Only\s+explicit or logical edges skip a dependent/);
746
+ });
747
+
610
748
  it("formatTable prints issue, outcome, PR, cost, minutes", () => {
611
749
  const t = formatTable([{ issue: 1, outcome: "merged", pr: 101, cost_usd: 0.5, minutes: 3.2 }, { issue: 2, outcome: "skipped", pr: null, cost_usd: 0, minutes: 0, detail: "dependency #1 ended needs-human" }]);
612
750
  assert.match(t, /issue\s+outcome\s+PR\s+cost\s+min/);
@@ -614,21 +752,47 @@ describe("orchestrate", () => {
614
752
  assert.match(t, /skipped \(dependency #1/);
615
753
  });
616
754
 
617
- it("orchestrateFromCli wires parallel, lane and prints the table", async () => {
755
+ it("orchestrateFromCli routes each issue to its own lane (no --lane), reports cross-lane edges, refuses --lane main", async () => {
618
756
  const seen = [];
619
757
  const logs = [];
758
+ const milestones = { 1: "M One", 2: null, 3: "M One" };
759
+ const leanIo = {
760
+ gh: async (a) => {
761
+ if (a[0] === "repo" && a[1] === "view") return "main\n";
762
+ if (a.includes("milestone")) return JSON.stringify({ milestone: milestones[a[2]] ? { title: milestones[a[2]] } : null });
763
+ return JSON.stringify({ body: a[2] === "1" ? "**Depends on**: #2" : "" });
764
+ },
765
+ git: async (a) => (a[0] === "ls-remote" && ["refs/heads/staging", "refs/heads/milestone/m-one"].includes(a.at(-1)) ? "abc\tref\n" : ""),
766
+ };
767
+ const deps = { dir: tmpDir("core-orch-"), batchRoot: tmpDir("core-orch-batch-"), allowTmpfs: true, emitTelemetry: async () => {}, io: { gh: async () => "o/r\n" }, leanIo,
768
+ leanModule: { runCore: async (o) => { seen.push([o.issue, o.lane, o.repo]); return { terminalReason: "merged", pr: 50 + o.issue, cost_usd: 1, wall_ms: 1 }; } } };
620
769
  const l = console.log; console.log = (s) => logs.push(String(s));
621
- try {
622
- const res = await orchestrateFromCli(["1", "2", "--parallel", "2", "--lane", "main", "--repo", "o/r"], {
623
- dir: tmpDir("core-orch-"), emitTelemetry: async () => {}, io: { gh: async () => "o/r\n" },
624
- leanIo: { gh: async () => JSON.stringify({ body: "" }) },
625
- leanModule: { runCore: async (o) => { seen.push([o.issue, o.lane, o.repo]); return { terminalReason: "merged", pr: 50 + o.issue, cost_usd: 1, wall_ms: 1 }; } } });
626
- assert.equal(res.ok, true);
627
- } finally { console.log = l; }
628
- assert.deepEqual(seen.sort(), [[1, "main", "o/r"], [2, "main", "o/r"]]);
770
+ let res;
771
+ try { res = await orchestrateFromCli(["1", "2", "3", "--parallel", "2", "--repo", "o/r", "--no-prefilter"], deps); } finally { console.log = l; }
772
+ assert.equal(res.ok, true);
773
+ assert.deepEqual(seen.sort((a, b) => a[0] - b[0]), [[1, "milestone/m-one", "o/r"], [2, "staging", "o/r"], [3, "milestone/m-one", "o/r"]]);
774
+ assert.deepEqual(res.crossLane, [{ issue: 1, lane: "milestone/m-one", dependency: 2, dependencyLane: "staging" }]);
775
+ assert.ok(logs.some((s) => /^cross-lane: #1 \(milestone\/m-one\) depends on #2 \(staging\)/.test(s)), logs.join("\n"));
629
776
  assert.ok(logs.some((s) => /issue\s+outcome/.test(s)));
777
+ // --lane is an assertion per issue: the staging issue matches, the milestone issues are refused without a run.
778
+ seen.length = 0;
779
+ console.log = () => {};
780
+ try { res = await orchestrateFromCli(["2", "3", "--repo", "o/r", "--lane", "staging", "--no-prefilter"], deps); } finally { console.log = l; }
781
+ assert.deepEqual(res.rows.map((r) => [r.issue, r.outcome, r.code ?? null]), [[2, "merged", null], [3, "lane-refused", "lane.mismatch"]]);
782
+ assert.deepEqual(seen, [[2, "staging", "o/r"]]);
783
+ await assert.rejects(orchestrateFromCli(["1", "--lane", "main", "--repo", "o/r"], { ...deps, io: { gh: async () => "main\n" } }), /lane\.default_branch/);
630
784
  await assert.rejects(orchestrateFromCli([], {}), /usage/);
631
785
  });
786
+
787
+ it("#4125 a lane-refused issue starts no run and its dependents are skipped, not reported as a cycle", async () => {
788
+ const runs = [];
789
+ const rows = await orchestrate({ issues: [1, 2, 3], parallel: 2, readBody: async (n) => (n === 2 ? "**Depends on**: #1" : ""),
790
+ resolveLane: async (n) => (n === 1 ? { ok: false, code: "lane.branch_missing", detail: "milestone/x missing" } : { ok: true, lane: "staging" }),
791
+ runOne: async (n, lane) => { runs.push([n, lane]); return { terminalReason: "merged" }; } });
792
+ assert.deepEqual(rows.map((r) => [r.issue, r.outcome]), [[1, "lane-refused"], [2, "skipped"], [3, "merged"]]);
793
+ assert.match(rows[1].detail, /dependency #1 ended lane-refused/);
794
+ assert.deepEqual(runs, [[3, "staging"]]);
795
+ });
632
796
  });
633
797
 
634
798
  describe("runCliBackendAsync", () => {
@@ -1216,3 +1380,197 @@ describe("batch robustness (#4112-#4115)", () => {
1216
1380
  assert.ok(w.calls.some((c) => c[0] === "git" && c[1] === "worktree" && c[2] === "add" && c.includes("origin/feat/issue-7-lean")));
1217
1381
  });
1218
1382
  });
1383
+
1384
+ describe("lane resolution in core work-on (#4125)", () => {
1385
+ const leased = (w) => { w.leaseWrites = []; w.io.projector = { readState: async () => null, writeState: async (_i, st) => { w.leaseWrites.push(st); } }; return w; };
1386
+ const touched = (w) => ({ comments: w.comments.length, leases: w.leaseWrites.length, worktree: w.calls.filter((c) => c[1] === "worktree").length,
1387
+ runner: w.calls.filter((c) => c[0] === "runner").length, labels: w.labels.length, prs: w.prs.length, merges: w.merges.length });
1388
+ const nothing = { comments: 0, leases: 0, worktree: 0, runner: 0, labels: 0, prs: 0, merges: 0 };
1389
+
1390
+ it("a milestone issue with no --lane builds on and opens its PR against milestone/<slug>", async () => {
1391
+ const w = leased(world({ milestone: "Adaptive Unblocking Control Loop", branchesOnOrigin: ["staging", "milestone/adaptive-unblocking-control-loop"] }));
1392
+ const r = await run(w, { lane: undefined });
1393
+ assert.equal(r.terminalReason, "merged");
1394
+ assert.equal(w.prs[0][w.prs[0].indexOf("--base") + 1], "milestone/adaptive-unblocking-control-loop");
1395
+ assert.match(w.comments[0], /base `milestone\/adaptive-unblocking-control-loop`/);
1396
+ assert.ok(w.calls.some((c) => c[0] === "git" && c[1] === "worktree" && c.includes("origin/milestone/adaptive-unblocking-control-loop")), "worktree based on the milestone lane");
1397
+ assert.equal(w.leaseWrites[0].lane, "milestone/adaptive-unblocking-control-loop");
1398
+ });
1399
+
1400
+ it("--lane staging on a milestone issue is refused (lane.mismatch) before any lease, comment or worktree", async () => {
1401
+ const w = leased(world({ milestone: "M1", branchesOnOrigin: ["staging", "milestone/m1"] }));
1402
+ const r = await run(w, { lane: "staging" });
1403
+ assert.deepEqual([r.terminalReason, r.code], ["lane-refused", "lane.mismatch"]);
1404
+ assert.deepEqual(touched(w), nothing);
1405
+ // An explicit override is allowed and recorded in the trajectory.
1406
+ const w2 = leased(world({ milestone: "M1", branchesOnOrigin: ["staging", "milestone/m1"] }));
1407
+ const r2 = await run(w2, { lane: "staging", laneOverride: true });
1408
+ assert.equal(r2.terminalReason, "merged");
1409
+ assert.equal(w2.prs[0][w2.prs[0].indexOf("--base") + 1], "staging");
1410
+ assert.ok(traj(w2)[0].includes("Lane override: staging instead of the resolved milestone/m1"));
1411
+ });
1412
+
1413
+ it("--lane main (or the configured default branch) is refused, even with an override", async () => {
1414
+ for (const [extra, o] of [[{ lane: "main" }, {}], [{ lane: "main", laneOverride: true }, {}], [{ lane: "prod", branches: { default: "prod" } }, { branchesOnOrigin: ["staging", "prod"] }]]) {
1415
+ const w = leased(world(o));
1416
+ const r = await run(w, extra);
1417
+ assert.deepEqual([r.terminalReason, r.code], ["lane-refused", "lane.default_branch"], JSON.stringify(extra));
1418
+ assert.deepEqual(touched(w), nothing);
1419
+ }
1420
+ });
1421
+
1422
+ it("a missing milestone/<slug> branch fails closed with lane.branch_missing", async () => {
1423
+ const w = leased(world({ milestone: "Not Yet" }));
1424
+ const r = await run(w, { lane: undefined });
1425
+ assert.deepEqual([r.terminalReason, r.code], ["lane-refused", "lane.branch_missing"]);
1426
+ assert.match(r.detail, /milestone\/not-yet/);
1427
+ assert.deepEqual(touched(w), nothing);
1428
+ });
1429
+
1430
+ it("an open PR targeting the default branch (or another lane) is never merged", async () => {
1431
+ for (const [base, code] of [["main", "lane.default_branch"], ["milestone/other", "lane.mismatch"]]) {
1432
+ const w = world({ prList: [{ number: 77, headRefOid: "aaaa1111aaaa1111", baseRefName: base }], remote: "abc\trefs/heads/feat/issue-7-lean" });
1433
+ const r = await run(w);
1434
+ assert.equal(r.terminalReason, "needs-human", base);
1435
+ assert.equal(r.code, code);
1436
+ assert.equal(w.merges.length, 0); assert.equal(w.prs.length, 0);
1437
+ assert.equal(builders(w).length, 0);
1438
+ }
1439
+ });
1440
+ });
1441
+
1442
+ describe("lane entry points (#4125)", () => {
1443
+ const quiet = async (fn) => { const l = console.log; console.log = () => {}; try { return await fn(); } finally { console.log = l; } };
1444
+ const deps = () => ({ dir: tmpDir("core-cli-"), emitTelemetry: async () => {}, leanIo: {}, io: { gh: async (a) => (a.includes("defaultBranchRef") ? "main\n" : "o/r\n") },
1445
+ leanModule: { runCore: async (o) => ({ terminalReason: "merged", seen: o }) }, runIssue: async () => ({ terminalReason: "merged" }) });
1446
+
1447
+ it("run-issue refuses --lane main on the core and full paths; the core path needs no --lane", async () => {
1448
+ await assert.rejects(runFromCli(["7", "--lane", "main", "--repo", "o/r"], deps()), /lane\.default_branch/);
1449
+ await assert.rejects(runFromCli(["7", "--lane", "main", "--path", "full"], deps()), /lane\.default_branch/);
1450
+ await assert.rejects(runFromCli(["7", "--path", "full"], deps()), /--lane is required with --path full/);
1451
+ const r = await quiet(() => runFromCli(["7", "--repo", "o/r"], deps()));
1452
+ assert.equal(r.seen.lane, undefined); assert.equal(r.seen.laneOverride, false);
1453
+ const r2 = await quiet(() => runFromCli(["7", "--repo", "o/r", "--lane", "staging", "--lane-override"], deps()));
1454
+ assert.deepEqual([r2.seen.lane, r2.seen.laneOverride], ["staging", true]);
1455
+ });
1456
+
1457
+ it("the core run-issue prints lane-refused with its code", async () => {
1458
+ const d = deps(); d.leanModule = { runCore: async () => ({ terminalReason: "lane-refused", code: "lane.mismatch", detail: "--lane staging does not match" }) };
1459
+ const lines = []; const l = console.log; console.log = (x) => lines.push(String(x));
1460
+ try { await runFromCli(["7", "--repo", "o/r", "--lane", "staging"], d); } finally { console.log = l; }
1461
+ assert.equal(lines.at(-1), "issue #7 → lane-refused lane.mismatch (--lane staging does not match)");
1462
+ });
1463
+
1464
+ it("no staging lane default remains in engine.mjs, runlog.mjs or engine-cli.mjs", async () => {
1465
+ const { readFileSync } = await import("node:fs");
1466
+ for (const f of ["../engine.mjs", "../engine/runlog.mjs", "../engine-cli.mjs"]) {
1467
+ const src = readFileSync(new URL(f, import.meta.url), "utf8");
1468
+ assert.doesNotMatch(src, /lane\s*=\s*"staging"|lane:\s*"staging"|"--lane"\)\s*\?\?\s*"staging"/, f);
1469
+ assert.doesNotMatch(src, /--lane main/, f);
1470
+ }
1471
+ });
1472
+ });
1473
+
1474
+ describe("large diffs never reach argv (#4129)", () => {
1475
+ const ARGV_SAFE = 128 * 1024; // Linux MAX_ARG_STRLEN is 131072 bytes per argument
1476
+ const bigDiff = (files, perFile) => Array.from({ length: files }, (_, i) =>
1477
+ `diff --git a/corpus/f${i}.txt b/corpus/f${i}.txt\n--- a/corpus/f${i}.txt\n+++ b/corpus/f${i}.txt\n@@ -0,0 +1 @@\n+${"golden — ".repeat(Math.ceil(perFile / 11))}\n`).join("");
1478
+
1479
+ it("review of a branch whose diff is over 256 KiB completes, with every CLI argv element under the bound", async () => {
1480
+ const { runCliBackend } = await import("../runner.mjs");
1481
+ const diff = bigDiff(8, 40 * 1024);
1482
+ assert.ok(Buffer.byteLength(diff) > 256 * 1024);
1483
+ const w = world();
1484
+ const g = w.io.git;
1485
+ w.io.git = async (a, o2) => {
1486
+ if (a[0] === "diff" && a[1] === "--stat=200") return " corpus/f0.txt | 1 +\n 8 files changed";
1487
+ if (a[0] === "diff" && a.length === 2) return diff;
1488
+ return g(a, o2);
1489
+ };
1490
+ const spawned = [];
1491
+ const r0 = w.io.runner;
1492
+ w.io.runner = async (c) => {
1493
+ if (c.role !== "reviewer") return r0(c);
1494
+ w.calls.push(["runner", c.role, null, c.model ?? null, c.prompt, c.readOnly ?? false, c.system]);
1495
+ const env = runCliBackend({ spec: { name: "core-reviewer" }, userMessage: c.prompt, systemPrompt: c.system, args: ["7"], cwd: tmpDir("core-4129-"), logger: { log() {} },
1496
+ spawnFn: (bin, args, o2) => { spawned.push({ args, input: o2.input }); return { status: 0, stdout: JSON.stringify({ result: ok(), session_id: "rev" }), stderr: "" }; } });
1497
+ return { text: env.text, sessionId: "rev", usage: {}, costUsd: 0, durationMs: 1, turns: 1, model: c.model };
1498
+ };
1499
+ const res = await run(w);
1500
+ assert.equal(res.terminalReason, "merged");
1501
+ assert.equal(spawned.length, 1);
1502
+ for (const a of spawned[0].args) assert.ok(Buffer.byteLength(String(a)) <= ARGV_SAFE, `argv element of ${Buffer.byteLength(String(a))} bytes`);
1503
+ const prompt = reviewers(w)[0][4];
1504
+ assert.ok(Buffer.byteLength(prompt) < 160 * 1024, `bounded prompt (${Buffer.byteLength(prompt)} bytes)`);
1505
+ assert.match(prompt, /<stat>[\s\S]*8 files changed/);
1506
+ assert.match(prompt, /<omitted_files>[\s\S]*corpus\/f7\.txt/);
1507
+ assert.match(prompt, /Read them in your worktree/);
1508
+ });
1509
+
1510
+ it("boundDiff keeps a fitting diff unchanged and otherwise keeps whole file diffs only", async () => {
1511
+ const { boundDiff } = await import("./prompts.mjs");
1512
+ const small = bigDiff(2, 100);
1513
+ assert.equal(boundDiff(small, { maxBytes: 10000 }), small);
1514
+ const d = bigDiff(5, 3000);
1515
+ const out = boundDiff(d, { stat: "5 files changed", base: "origin/staging", maxBytes: 7000 });
1516
+ assert.ok(Buffer.byteLength(out) < 7000 + 1000);
1517
+ const shown = (out.match(/^diff --git /gm) ?? []).length;
1518
+ assert.ok(shown >= 1 && shown < 5);
1519
+ assert.equal((out.match(/^- corpus\//gm) ?? []).length, 5 - shown);
1520
+ assert.match(out, /base origin\/staging/);
1521
+ });
1522
+
1523
+ it("runCliBackend sends a prompt over the argv bound on stdin, a small one stays in argv", async () => {
1524
+ const { runCliBackend, CLI_ARGV_PROMPT_MAX_BYTES } = await import("../runner.mjs");
1525
+ assert.ok(CLI_ARGV_PROMPT_MAX_BYTES <= ARGV_SAFE);
1526
+ const seen = [];
1527
+ const call = (msg) => runCliBackend({ spec: { name: "t" }, userMessage: msg, systemPrompt: "sys", args: ["1"], cwd: tmpDir("core-4129-"), logger: { log() {} },
1528
+ spawnFn: (_b, args, o2) => { seen.push({ args, input: o2.input }); return { status: 0, stdout: "{}", stderr: "" }; } });
1529
+ const big = "é".repeat(200 * 1024);
1530
+ call(big);
1531
+ assert.equal(seen[0].input, big);
1532
+ assert.ok(!seen[0].args.includes(big));
1533
+ for (const a of seen[0].args) assert.ok(Buffer.byteLength(String(a)) <= CLI_ARGV_PROMPT_MAX_BYTES);
1534
+ assert.equal(seen[0].args[0], "--print"); assert.equal(seen[0].args[1], "--output-format");
1535
+ call("go");
1536
+ assert.equal(seen[1].args[1], "go"); assert.equal(seen[1].input, undefined);
1537
+ });
1538
+
1539
+ it("runCliBackendAsync pipes a 300 KiB prompt to a real child (no E2BIG)", async () => {
1540
+ const { writeFileSync, chmodSync } = await import("node:fs");
1541
+ const { runCliBackendAsync } = await import("../runner.mjs");
1542
+ const d = tmpDir("core-4129-");
1543
+ const bin = `${d}/fake-claude`;
1544
+ writeFileSync(bin, '#!/bin/sh\nn=$(wc -c | tr -d " ")\necho "{\\"result\\":\\"$n $#\\",\\"session_id\\":\\"s\\"}"\n');
1545
+ chmodSync(bin, 0o755);
1546
+ const msg = "x".repeat(300 * 1024);
1547
+ const r = await runCliBackendAsync({ spec: { name: "t" }, userMessage: msg, systemPrompt: "sys", args: ["1"], cwd: d, bin, logger: { log() {} } });
1548
+ const [n] = r.text.split(" ");
1549
+ assert.equal(Number(n), msg.length);
1550
+ const small = await runCliBackendAsync({ spec: { name: "t" }, userMessage: "go", args: ["1"], cwd: d, bin, logger: { log() {} } });
1551
+ assert.equal(small.text.split(" ")[0], "0"); // small prompt: argv, stdin ignored
1552
+ });
1553
+
1554
+ it("a deterministic spawn failure is CLI_SPAWN_FAILED and ends needs-human with cli.spawn_failed, not engine-error", async () => {
1555
+ const { runCliBackend } = await import("../runner.mjs");
1556
+ assert.throws(() => runCliBackend({ spec: { name: "t" }, userMessage: "go", args: ["1"], cwd: tmpDir("core-4129-"), logger: { log() {} },
1557
+ spawnFn: () => ({ status: null, error: Object.assign(new Error("spawn E2BIG"), { code: "E2BIG" }) }) }),
1558
+ (e) => e.code === "CLI_SPAWN_FAILED" && e.spawnErrno === "E2BIG" && /E2BIG/.test(e.message));
1559
+ assert.throws(() => runCliBackend({ spec: { name: "t" }, userMessage: "go", args: ["1"], cwd: tmpDir("core-4129-"), logger: { log() {} },
1560
+ spawnFn: () => ({ status: null, error: Object.assign(new Error("spawn EACCES"), { code: "EACCES" }) }) }),
1561
+ (e) => e.code === "EACCES" && e.spawnDeterministic === true);
1562
+ const w = world();
1563
+ const r0 = w.io.runner;
1564
+ w.io.runner = async (c) => { if (c.role === "reviewer") throw Object.assign(new Error("Failed to invoke claude CLI: spawn E2BIG"), { code: "CLI_SPAWN_FAILED", spawnErrno: "E2BIG" }); return r0(c); };
1565
+ const res = await run(w);
1566
+ assert.equal(res.terminalReason, "needs-human");
1567
+ assert.equal(res.code, OUTCOME_CODES.CLI_SPAWN_FAILED);
1568
+ const w2 = world();
1569
+ const r2 = w2.io.runner;
1570
+ w2.io.runner = async (c) => { if (c.role === "builder") throw Object.assign(new Error("Failed to invoke claude CLI: spawn ENOENT"), { code: "ENOENT", spawnErrno: "ENOENT", spawnDeterministic: true }); return r2(c); };
1571
+ assert.equal((await run(w2)).code, OUTCOME_CODES.CLI_SPAWN_FAILED);
1572
+ assert.equal(OUTCOME_CODES.CLI_SPAWN_FAILED, "cli.spawn_failed");
1573
+ const { readFileSync } = await import("node:fs");
1574
+ assert.match(readFileSync(new URL("../../docs/OUTCOME-CODES.md", import.meta.url), "utf8"), /`cli\.spawn_failed`/);
1575
+ });
1576
+ });