forgedock 1.29.1 → 1.30.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.
Files changed (38) hide show
  1. package/bin/engine/outcome-codes.mjs +4 -2
  2. package/bin/engine/phases.mjs +20 -3
  3. package/bin/engine-cli.mjs +94 -36
  4. package/bin/engine.mjs +3 -1
  5. package/bin/lean/batch.test.mjs +62 -1
  6. package/bin/lean/conformance.mjs +129 -8
  7. package/bin/lean/conformance.test.mjs +77 -3
  8. package/bin/lean/context.mjs +17 -12
  9. package/bin/lean/context.test.mjs +36 -5
  10. package/bin/lean/effects.mjs +20 -6
  11. package/bin/lean/enumerating.mjs +189 -0
  12. package/bin/lean/enumerating.test.mjs +97 -0
  13. package/bin/lean/lean.mjs +189 -43
  14. package/bin/lean/lean.test.mjs +305 -6
  15. package/bin/lean/orchestrate.mjs +104 -85
  16. package/bin/lean/precheck.mjs +55 -0
  17. package/bin/lean/precheck.test.mjs +93 -0
  18. package/bin/lean/prefilter.mjs +21 -4
  19. package/bin/lean/prefilter.test.mjs +24 -1
  20. package/bin/lean/preflight.mjs +133 -0
  21. package/bin/lean/preflight.test.mjs +61 -0
  22. package/bin/lean/prompts.mjs +37 -3
  23. package/bin/lean/recovery.test.mjs +75 -0
  24. package/bin/lean/sequence.mjs +102 -0
  25. package/bin/lean/sequence.test.mjs +112 -0
  26. package/bin/lean/testmap.mjs +43 -6
  27. package/bin/lean/testmap.test.mjs +38 -1
  28. package/bin/lean/verify.mjs +7 -2
  29. package/bin/lean/verify.test.mjs +8 -2
  30. package/commands/orchestrate.md +3 -1
  31. package/docs/CONFIG.md +8 -1
  32. package/docs/OUTCOME-CODES.md +6 -4
  33. package/docs/RELEASE-NOTES.md +10 -0
  34. package/package.json +1 -1
  35. package/scripts/acceptance-red-green.sh +13 -4
  36. package/scripts/acceptance-red-green.test.sh +14 -0
  37. package/scripts/test-map.sh +63 -17
  38. package/scripts/test-map.test.sh +55 -0
@@ -17,7 +17,7 @@ import { readLog } from "../engine/runlog.mjs";
17
17
  import { makeRunTmp, wrapLaneLimits, systemdRunAvailable, makeLeanRunner, requiredChecksFor, procsInDir, makeLeanIo } from "./effects.mjs";
18
18
  import { retrying, PUSH_TRANSIENT_RE } from "./retry.mjs";
19
19
  import { checkAdmission, hostParallelism } from "./admission.mjs";
20
- import { OUTCOME_CODES } from "../engine/outcome-codes.mjs";
20
+ import { OUTCOME_CODES, classifyNeedsHuman } from "../engine/outcome-codes.mjs";
21
21
  import * as refactorMod from "./refactor.mjs";
22
22
  import { readAdmission, readConformanceMode, readReviewIncomplete, readEnvConfig, readContextPack } from "../engine-cli.mjs";
23
23
  import { prepareToolchain } from "./toolchain.mjs";
@@ -319,6 +319,18 @@ describe("core work-on", () => {
319
319
  assert.ok(w.comments.some((c) => c.includes("FORGE:NEEDS_HUMAN") && c.includes("issue_coverage is missing")));
320
320
  });
321
321
 
322
+ it("#4327 a review incomplete only by unresolved evidence paths gets one more targeted re-ask, then merges", async () => {
323
+ const lost = () => conf({ requirements: [{ id: "R1", status: "implemented", evidence: "/layout.tsx:3", validated_by: "V1" }] });
324
+ const w = world({ reviews: [lost(), lost(), ok()] });
325
+ assert.equal((await run(w)).terminalReason, "merged");
326
+ const rv = reviewers(w);
327
+ assert.equal(rv.length, 3); assert.equal(builders(w).length, 2, "never charged to the builder");
328
+ assert.match(rv[2][4], /answer ONLY for these ids or sections: R1/); assert.match(rv[2][4], /evidence path \/layout\.tsx does not exist/);
329
+ const w2 = world({ reviews: [lost(), lost(), lost(), ok()] });
330
+ const r2 = await run(w2);
331
+ assert.equal(r2.code, "review.incomplete"); assert.equal(reviewers(w2).length, 3, "at most three reviewer runs"); assert.equal(w2.merges.length, 0);
332
+ });
333
+
322
334
  it("a CONFORMS verdict whose rows fail is not conforming (the engine computes the verdict)", async () => {
323
335
  const w = world({ reviews: [bad("half done", "CONFORMS"), ok()] });
324
336
  assert.equal((await run(w)).terminalReason, "merged");
@@ -450,6 +462,83 @@ describe("core work-on", () => {
450
462
  noNeedsHuman(w);
451
463
  });
452
464
 
465
+ const splitBlock = { status: "blocked", summary: "epic", block: { kind: "split-required", subissues: [{ title: "part A", body: "do A" }, { title: "part B", body: "do B", depends_on: [0] }] } };
466
+ it("#4337 without decomposition.close_parent a split keeps the parent open (opt-in, default false)", async () => {
467
+ const w = depWorld({ changed: [], result: splitBlock }, {});
468
+ assert.equal((await run(w)).terminalReason, "decomposed");
469
+ assert.equal(w.closed.length, 0);
470
+ assert.ok(w.labels.includes("decomposed"));
471
+ });
472
+
473
+ it("#4337 with decomposition.close_parent a split closes the parent not planned after its FORGE:DECOMPOSED trail and label, listing every sub-issue", async () => {
474
+ const w = depWorld({ changed: [], result: splitBlock }, {});
475
+ const r = await run(w, { closeParent: true });
476
+ assert.equal(r.terminalReason, "decomposed");
477
+ assert.equal(w.closed.length, 1);
478
+ const c = w.closed[0];
479
+ assert.equal(c[c.indexOf("--reason") + 1], "not planned");
480
+ const note = c[c.indexOf("--comment") + 1];
481
+ assert.match(note, /#201/); assert.match(note, /#202/);
482
+ assert.ok(w.labels.includes("decomposed"));
483
+ assert.ok(!w.edits.some((e) => e.includes("--remove-label") && e.join(" ").includes("workflow:decomposed")), "the decomposed label is kept");
484
+ const seq = w.seq.filter((x) => x === "label:decomposed" || x === "close");
485
+ assert.deepEqual(seq, ["label:decomposed", "close"]);
486
+ assert.ok(w.comments.findIndex((x) => x.includes("FORGE:DECOMPOSED")) >= 0);
487
+ });
488
+
489
+ it("#4337 no close when the FORGE:DECOMPOSED comment or the decomposed label could not be written (fail closed)", async () => {
490
+ const w = depWorld({ changed: [], result: splitBlock }, {});
491
+ const post = w.io.gh;
492
+ w.io.gh = async (a) => (a[0] === "issue" && a[1] === "comment" && a[a.indexOf("--body") + 1].includes("FORGE:DECOMPOSED") ? Promise.reject(new Error("HTTP 422")) : post(a));
493
+ assert.equal((await run(w, { closeParent: true })).terminalReason, "decomposed");
494
+ assert.equal(w.closed.length, 0);
495
+ const w2 = depWorld({ changed: [], result: splitBlock }, {});
496
+ const tl = w2.io.transitionLabel;
497
+ w2.io.transitionLabel = async (n, st) => { if (st === "decomposed") throw new Error("label failed"); return tl(n, st); };
498
+ assert.equal((await run(w2, { closeParent: true })).terminalReason, "decomposed");
499
+ assert.equal(w2.closed.length, 0);
500
+ });
501
+
502
+ const trail = { body: "<!-- FORGE:DECOMPOSED -->\n## Split into sub-issues\n- #31 part A\n- #32 part B\n", author_association: "NONE", user: { type: "Bot", login: "forge[bot]" } };
503
+ const revoke = { body: "<!-- FORGE:DECOMPOSED:REVOKED -->\nre-plan", author_association: "OWNER", user: { type: "User", login: "op" } };
504
+ it("#4337 a dependency block on a CLOSED decomposed parent resolves to its open sub-issues; no usable trail keeps the parent", async () => {
505
+ const parent = { state: "CLOSED", labels: [{ name: "workflow:decomposed" }] };
506
+ const block = { status: "blocked", summary: "needs #3", block: { kind: "dependency", issues: [3] } };
507
+ const w = depWorld({ changed: [], issueComments: [trail], result: block }, { 3: parent, 31: { state: "CLOSED", labels: [] }, 32: { state: "OPEN", labels: [] } });
508
+ const r = await run(w);
509
+ assert.equal(r.terminalReason, "dependency-open");
510
+ assert.deepEqual(r.dependencies, [32]);
511
+ for (const comments of [[], [trail, revoke]]) {
512
+ const w2 = depWorld({ changed: [], issueComments: comments, result: block }, { 3: parent });
513
+ const r2 = await run(w2);
514
+ assert.equal(r2.terminalReason, "dependency-open", JSON.stringify(comments));
515
+ assert.deepEqual(r2.dependencies, [3]);
516
+ }
517
+ const w3 = depWorld({ changed: [], issueComments: [trail], result: block }, { 3: parent, 31: { state: "CLOSED", labels: [] }, 32: { state: "CLOSED", labels: [] } });
518
+ assert.notEqual((await run(w3)).terminalReason, "dependency-open", "satisfied once every sub-issue is closed");
519
+ });
520
+
521
+ it("#4337 a run on a CLOSED decomposed tracker ends decomposed with its sub-issues; a trusted revoke reopens it and the run proceeds", async () => {
522
+ const w = world({ issueStates: { 7: "CLOSED" }, labels: [{ name: "workflow:decomposed" }], issueComments: [trail] });
523
+ const r = await run(w);
524
+ assert.equal(r.terminalReason, "decomposed");
525
+ assert.deepEqual(r.subissues, [31, 32]);
526
+ assert.equal(builders(w).length, 0);
527
+ assert.equal(w.patches.length, 0);
528
+ const w2 = world({ issueStates: { 7: "CLOSED" }, labels: [{ name: "workflow:decomposed" }], issueComments: [] });
529
+ assert.deepEqual((await run(w2)).subissues, [], "no trail: no sub-issues, so the batch skips its dependents (fail closed)");
530
+ const w3 = world({ issueStates: { 7: "CLOSED" } });
531
+ assert.equal((await run(w3)).terminalReason, "invalid", "a plain closed issue is still already closed");
532
+ const w4 = world({ issueStates: { 7: "CLOSED" }, labels: [{ name: "workflow:decomposed" }], issueComments: [trail, revoke] });
533
+ const gh = w4.io.gh;
534
+ w4.io.gh = async (a) => (a[0] === "api" && a.length === 2 && /issues\/7$/.test(a[1]) ? JSON.stringify({ state: "closed", labels: [{ name: "workflow:decomposed" }] }) : gh(a));
535
+ const r4 = await run(w4);
536
+ assert.equal(w4.patches.length, 1);
537
+ assert.ok(w4.patches[0].includes("state=open"));
538
+ assert.ok(builders(w4).length > 0, "the reopened issue is worked on");
539
+ assert.notEqual(r4.terminalReason, "decomposed");
540
+ });
541
+
453
542
  it("#4124 a prod-data (non-human) or untyped block gets one corrective resume; a builder that then proceeds merges", async () => {
454
543
  const w = world({ result: { status: "blocked", summary: "needs live wire capture and prod baselines", block: { kind: "human", why: "prod-data" } },
455
544
  resumeResult: { status: "fixed", summary: "captured via read-only monitoring", evidence: [], tests: [] } });
@@ -1026,6 +1115,60 @@ describe("conformance shadow mode (the default)", () => {
1026
1115
  assert.match(nhBody(w), /validate V1 \(`node --test test\/foo\.test\.js`\) failed \(exit 1\)/);
1027
1116
  });
1028
1117
 
1118
+ it("#4323 a validate the host cannot run (missing module) ends env.tool_missing naming it: no builder resume, no reviewer", async () => {
1119
+ const v = [{ id: "V1", command: "node --test test/foo.test.js", exit: 1 }];
1120
+ for (const conformance of ["shadow", "enforce"]) {
1121
+ const w = world({ validation: v }), ran = [];
1122
+ w.io.runCommand = async (a) => { ran.push(a); return { rc: 1, output: "ModuleNotFoundError: No module named 'yaml'" }; };
1123
+ const r = await run(w, { conformance });
1124
+ assert.equal(r.code, OUTCOME_CODES.ENV_TOOL_MISSING, conformance); assert.equal(w.merges.length, 0);
1125
+ assert.equal(builders(w).length, 2, "plan + implement only"); assert.equal(reviewers(w).length, 0);
1126
+ assert.match(nhBody(w), /Environment error \(not a conformance failure\): the host is missing yaml/);
1127
+ assert.deepEqual(ran.map((a) => [a.command, a.cwd]), [["node --test test/foo.test.js", WT]], "only the failed contract validate is rerun, in the worktree");
1128
+ assert.ok(ran[0].env && typeof ran[0].env === "object", "the rerun gets the lane env");
1129
+ }
1130
+ });
1131
+ it("#4323 a rerun that fails for a repo reason stays conformance.unimplemented; one that passes merges", async () => {
1132
+ const v = [{ id: "V1", command: "node --test test/foo.test.js", exit: 1 }];
1133
+ const w = world({ validation: v, resumeValidation: v });
1134
+ w.io.runCommand = async () => ({ rc: 1, output: "Error: Cannot find module './src/foo.js'" });
1135
+ assert.equal((await runShadow(w)).code, "conformance.unimplemented"); assert.equal(builders(w).length, 3);
1136
+ const p = world({ validation: v });
1137
+ p.io.runCommand = async () => ({ rc: 0, output: "" });
1138
+ assert.equal((await runShadow(p)).terminalReason, "merged"); assert.equal(builders(p).length, 2);
1139
+ });
1140
+
1141
+ it("#4328 freeze-time preflight: a validate needing a module the host lacks gets the one contract correction, before any build", async () => {
1142
+ const pyC = { ...CONTRACT, validate: [{ id: "V1", command: `python3 -c "import yaml"` }] }, uvC = { ...CONTRACT, validate: [{ id: "V1", command: `uv run python -c "import yaml"` }] };
1143
+ const probe = (w) => { const ran = []; w.io.probeCommand = async (a) => { ran.push(a); return { rc: /^'python3' .*'yaml'/.test(a.command) ? 3 : 0, output: "" }; }; return ran; };
1144
+ const dir = tmpDir("preflight-");
1145
+ const w = world({ contract: pyC, fixedContract: uvC, validation: [{ id: "V1", command: uvC.validate[0].command, exit: 0 }] }), ran = probe(w);
1146
+ assert.equal((await run(w, { dir })).terminalReason, "merged");
1147
+ assert.equal(builders(w).length, 3, "plan + one contract correction + implement");
1148
+ assert.match(builders(w)[1][4], /V1: needs Python module 'yaml' \(for python3\), which this host lacks[\s\S]*repo's own toolchain/);
1149
+ assert.ok(ran.every((a) => a.cwd === WT && a.env && typeof a.env === "object"), "probes run in the worktree with the lane env");
1150
+ assert.deepEqual(readLog(dir, 7).filter((e) => e.event === "VALIDATE_PREFLIGHT").map((e) => [e.corrected, e.misses.map((m) => m.tool)]), [[true, []]]);
1151
+ // still missing after the correction: advisory only, the lane goes on (the post-build rerun is the gate)
1152
+ const dir2 = tmpDir("preflight-");
1153
+ const s = world({ contract: pyC, fixedContract: pyC, validation: [{ id: "V1", command: pyC.validate[0].command, exit: 0 }] }); probe(s);
1154
+ assert.equal((await run(s, { dir: dir2 })).terminalReason, "merged"); assert.equal(builders(s).length, 3);
1155
+ assert.deepEqual(readLog(dir2, 7).filter((e) => e.event === "VALIDATE_PREFLIGHT").map((e) => [e.corrected, e.misses.map((m) => m.tool)]), [[false, ["yaml"]]]);
1156
+ assert.ok(traj(s).some((t) => /Validate preflight: the host lacks yaml after the contract correction/.test(t)));
1157
+ // every tool present: no correction, no event
1158
+ const dir3 = tmpDir("preflight-");
1159
+ const ok = world({ contract: uvC, validation: [{ id: "V1", command: uvC.validate[0].command, exit: 0 }] }); const ran3 = probe(ok);
1160
+ assert.equal((await run(ok, { dir: dir3 })).terminalReason, "merged"); assert.equal(builders(ok).length, 2);
1161
+ assert.deepEqual(ran3.map((a) => a.command), ["command -v -- 'uv' >/dev/null 2>&1"]);
1162
+ assert.equal(readLog(dir3, 7).filter((e) => e.event === "VALIDATE_PREFLIGHT").length, 0);
1163
+ });
1164
+ it("#4328 an invalid contract and a preflight miss share one correction; still invalid is contract.invalid as before", async () => {
1165
+ const bad = { ...CONTRACT, requirements: [{ id: "R1", quote: "make foo nicer", disposition: "deliver" }], validate: [{ id: "V1", command: "jq . a.json" }] };
1166
+ const w = world({ contract: bad, fixedContract: bad });
1167
+ w.io.probeCommand = async () => ({ rc: 1, output: "" });
1168
+ assert.equal((await run(w)).code, "contract.invalid"); assert.equal(builders(w).length, 2, "plan + one correction");
1169
+ assert.match(builders(w)[1][4], /R1: quote is not verbatim[\s\S]*V1: needs command 'jq'/);
1170
+ });
1171
+
1029
1172
  it("a failed validate fetches the lane and the resume says how to catch up with a moved base (merge, never rebase)", async () => {
1030
1173
  const w = world({ validation: [{ id: "V1", command: "node --test test/foo.test.js", exit: 1 }], resumeValidation: [{ id: "V1", command: "node --test test/foo.test.js", exit: 0 }] });
1031
1174
  assert.equal((await runShadow(w)).terminalReason, "merged");
@@ -1324,17 +1467,35 @@ describe("bug lanes freeze a red-green repro_test (#4271)", () => {
1324
1467
  assert.equal((await run(w)).terminalReason, "merged");
1325
1468
  assert.match(builders(w)[0][4], /This issue is a bug\. When it is testable, .*"repro_test":\{"path"/);
1326
1469
  assert.match(builders(w)[0][4], /"repro_test":"none","repro_test_reason"/);
1327
- assert.deepEqual(w.rgCalls, [{ cwd: "/wt/lean-7", base: "origin/staging", command: RT.command }]);
1470
+ assert.deepEqual(w.rgCalls.map(({ env, ...x }) => x), [{ cwd: "/wt/lean-7", base: "origin/staging", command: RT.command }]);
1328
1471
  assert.match(w.comments.find((c) => c.includes("FORGE:CONTRACT")), /"repro_test"/, "repro_test is frozen in the contract JSON");
1329
1472
  assert.ok(traj(w)[0].includes("repro_test test/foo.test.js foo handles null: BASE=FAILED HEAD=RAN"));
1330
1473
  assert.equal(builders(w).length, 2); assert.equal(reviewers(w).length, 1);
1331
1474
  const t = rworld([RED_GREEN], { labels: [{ name: "type:bug" }] });
1332
1475
  assert.equal((await run(t)).terminalReason, "merged"); assert.equal(t.rgCalls.length, 1, "type:bug counts as a bug");
1333
1476
  });
1477
+ it("#4325 the red-green check, declared checks, validate reruns and env.setup run with the builder's lane env", async () => {
1478
+ const v = [{ id: "V1", command: "node --test test/foo.test.js", exit: 1 }];
1479
+ const w = rworld([RED_GREEN], { validation: v }), ran = [], setups = [];
1480
+ w.io.runTmp = { prune: async () => {}, make: async (_i, id) => `/home/x/.forge/tmp/runs/${id}`, remove: async () => {} };
1481
+ w.io.prepareToolchain = async () => ({ env: { PATH: "/rt/toolchain/bin:/usr/bin" }, summary: "node 22" });
1482
+ w.io.deps = { env: () => ({ UV_CACHE_DIR: "/c/uv", TMPDIR: "/should-not-win" }), prepare: async () => [] };
1483
+ w.io.runSetup = async (a) => { setups.push(a); return { exit: 0, stdout: "", stderr: "" }; };
1484
+ w.io.runCommand = async (a) => { ran.push(a); return { rc: 0, output: "" }; };
1485
+ const prev = process.env.FORGE_HOST_PARALLELISM; process.env.FORGE_HOST_PARALLELISM = "3";
1486
+ try { assert.equal((await run(w, { envSetup: "make setup", precheck: { commands: ["npm run lint"] } })).terminalReason, "merged"); }
1487
+ finally { if (prev === undefined) delete process.env.FORGE_HOST_PARALLELISM; else process.env.FORGE_HOST_PARALLELISM = prev; }
1488
+ const lane = { UV_CACHE_DIR: "/c/uv", TMPDIR: w.envs[0].TMPDIR, FORGE_HOST_PARALLELISM: "3", PATH: "/rt/toolchain/bin:/usr/bin" };
1489
+ assert.match(lane.TMPDIR, /\/runs\/7-/, "the builder ran with the run TMPDIR");
1490
+ assert.deepEqual(w.rgCalls.map((x) => x.env), [lane], "red-green gets the lane env, not the daemon's");
1491
+ assert.deepEqual(ran.map((a) => [a.command, a.env]), [["npm run lint", lane], ["node --test test/foo.test.js", lane]]);
1492
+ assert.deepEqual(setups.map((a) => a.env), [lane]);
1493
+ assert.equal(w.envs[0].CLAUDE_CODE_TMPDIR, lane.TMPDIR); assert.equal(w.envs[0].UV_CACHE_DIR, "/c/uv"); assert.match(w.envs[0].FORGE_DOCKER_LABELS ?? "", /lean-7/);
1494
+ });
1334
1495
 
1335
1496
  it("a repro test that is not red on base and green on head fails the precheck in enforce and in shadow: one in-session resume, then needs-human", async () => {
1336
1497
  for (const conformance of ["enforce", "shadow"]) {
1337
- for (const rg of [{ base: "RAN", head: "RAN" }, { base: "SKIPPED", head: "FAILED" }, null]) {
1498
+ for (const rg of [{ base: "RAN", head: "RAN" }, { base: "SKIPPED", head: "FAILED" }]) {
1338
1499
  const w = rworld([rg]);
1339
1500
  const r = await run(w, { conformance });
1340
1501
  assert.equal(r.code, "conformance.unimplemented", `${conformance} ${JSON.stringify(rg)}`); assert.equal(w.merges.length, 0);
@@ -1348,6 +1509,27 @@ describe("bug lanes freeze a red-green repro_test (#4271)", () => {
1348
1509
  assert.equal((await run(fixed, { conformance: "shadow" })).terminalReason, "merged"); assert.equal(fixed.rgCalls.length, 2);
1349
1510
  });
1350
1511
 
1512
+ it("a repro test the harness cannot run on head (or no harness result) is env.repro_unrunnable: recoverable, no resume, no reviewer (#4334)", async () => {
1513
+ for (const conformance of ["enforce", "shadow"]) {
1514
+ for (const rg of [{ base: "SKIPPED", head: "UNRUNNABLE" }, null]) {
1515
+ const w = rworld([rg]);
1516
+ const r = await run(w, { conformance });
1517
+ assert.equal(r.code, "env.repro_unrunnable", `${conformance} ${JSON.stringify(rg)}`); assert.equal(r.terminalReason, "needs-human");
1518
+ assert.equal(classifyNeedsHuman(r), "recoverable");
1519
+ assert.equal(w.merges.length, 0); assert.equal(builders(w).length, 2, "plan + implement, no resume"); assert.equal(reviewers(w).length, 0);
1520
+ assert.match(nh(w), /the repro harness could not run the repro test on head; no builder resume was spent/);
1521
+ }
1522
+ }
1523
+ });
1524
+
1525
+ it("the repro harness runs with the lane env: toolchain PATH and the per-run TMPDIR (#4334)", async () => {
1526
+ const w = rworld([RED_GREEN]);
1527
+ w.io.runTmp = { prune: async () => {}, path: (id) => `/rt/${id}`, make: async (_i, id) => `/rt/${id}`, remove: async () => true };
1528
+ w.io.prepareToolchain = async () => ({ pins: [{}], env: { PATH: "/shim/bin:/host/bin" }, summary: "pnpm@10.4.1" });
1529
+ assert.equal((await run(w)).terminalReason, "merged");
1530
+ assert.equal(w.rgCalls[0].env.PATH, "/shim/bin:/host/bin"); assert.match(w.rgCalls[0].env.TMPDIR, /^\/rt\//);
1531
+ });
1532
+
1351
1533
  it("a non-testable bug records repro_test none with a reason and is not blocked; a missing repro_test gets the one contract correction", async () => {
1352
1534
  const none = { ...CONTRACT, repro_test: "none", repro_test_reason: "spec-only change, nothing executable" };
1353
1535
  const w = rworld([RED_GREEN], { contract: none });
@@ -1801,6 +1983,21 @@ describe("orchestrate", () => {
1801
1983
  assert.ok(log.indexOf("end 12") < log.indexOf("start 2"));
1802
1984
  });
1803
1985
 
1986
+ it("#4337 a CLOSED decomposed tracker in the batch gates its dependents on its sub-issues like resolveBlock; no trail skips them", async () => {
1987
+ const trail = { body: "<!-- FORGE:DECOMPOSED -->\n- #31 part A\n- #32 part B\n", author_association: "NONE", user: { type: "Bot", login: "forge[bot]" } };
1988
+ const tracker = async (comments) => run(world({ issueStates: { 7: "CLOSED" }, labels: [{ name: "workflow:decomposed" }], issueComments: comments }));
1989
+ const lane = await tracker([trail]);
1990
+ const log = [];
1991
+ const rows = await orchestrate({ issues: [7, 2], parallel: 3, readBody: async (n) => (n === 2 ? "**Depends on**: #7" : ""),
1992
+ runOne: async (n) => { log.push(`start ${n}`); await new Promise((r) => setImmediate(r)); log.push(`end ${n}`); return n === 7 ? lane : { terminalReason: "merged" }; } });
1993
+ assert.deepEqual(rows.map((r) => [r.issue, r.outcome]), [[7, "decomposed"], [2, "merged"], [31, "merged"], [32, "merged"]]);
1994
+ assert.ok(log.indexOf("end 31") < log.indexOf("start 2") && log.indexOf("end 32") < log.indexOf("start 2"), log.join(","));
1995
+ const failClosed = await tracker([]);
1996
+ const rows2 = await orchestrate({ issues: [7, 2], parallel: 3, readBody: async (n) => (n === 2 ? "**Depends on**: #7" : ""),
1997
+ runOne: async (n) => (n === 7 ? failClosed : { terminalReason: "merged" }) });
1998
+ assert.equal(rows2.find((r) => r.issue === 2).outcome, "skipped");
1999
+ });
2000
+
1804
2001
  it("a dependency that closed as invalid does not block", async () => {
1805
2002
  const h = harness({ 2: "**Depends on**: #1" }, { 1: "invalid" });
1806
2003
  const rows = await orchestrate({ issues: [1, 2], parallel: 2, ...h });
@@ -2631,6 +2828,25 @@ describe("batch robustness (#4112-#4115)", () => {
2631
2828
  assert.equal(r.heavyQueueMax, 4);
2632
2829
  });
2633
2830
 
2831
+ it("#4331 pre-PR checks: scoped by the test map, concurrent up to FORGE_HOST_PARALLELISM under one heavy slot, timed", async () => {
2832
+ const w = heavyWorld(); w.changed = ["commands/foo.md"];
2833
+ w.baseFiles = { "origin/staging:.forge/test-map.json": JSON.stringify({ version: 1, entries: [
2834
+ { globs: ["commands/**"], command: "bash scripts/check-a.sh", cwd: "." }, { globs: ["bin/**"], command: "bash scripts/check-b.sh", cwd: "." }] }) };
2835
+ const ran = []; let live = 0, peak = 0;
2836
+ w.io.runCommand = async ({ command }) => { ran.push(command); peak = Math.max(peak, ++live); await new Promise((r) => setImmediate(r)); w.clock += 1000; live--; return { rc: 0, output: "" }; };
2837
+ const prev = process.env.FORGE_HOST_PARALLELISM; process.env.FORGE_HOST_PARALLELISM = "2";
2838
+ const dir = tmpDir("core-");
2839
+ let r;
2840
+ try { r = await run(w, { dir, precheck: { commands: ["bash scripts/check-a.sh", "bash scripts/check-b.sh", "make lint", "make fmt"] } }); }
2841
+ finally { if (prev === undefined) delete process.env.FORGE_HOST_PARALLELISM; else process.env.FORGE_HOST_PARALLELISM = prev; }
2842
+ assert.deepEqual(ran, ["bash scripts/check-a.sh", "make lint", "make fmt"], "mapped check-b misses commands/foo.md; unmapped ones run");
2843
+ assert.equal(peak, 2);
2844
+ assert.equal(w.acq.filter((a) => a.phase === "precheck").length, 1, "one heavy slot for the round");
2845
+ const [pre] = readLog(dir, 7).filter((e) => e.event === "PRECHECK_COMMANDS");
2846
+ assert.equal(pre.parallelism, 2); assert.ok(pre.ms > 0); assert.ok(pre.results.every((x) => Number.isFinite(x.ms)));
2847
+ assert.ok(r.precheckMs > 0); assert.equal(r.precheckDeferred, 0);
2848
+ });
2849
+
2634
2850
  it("#4169 without io.heavy (or heavy.enabled false) nothing is gated and the runner env is unchanged", async () => {
2635
2851
  const w = world();
2636
2852
  const r = await run(w);
@@ -3190,7 +3406,7 @@ describe("verify-by-CI (#4167): ci mode runs the heavy checks on CI and feeds fa
3190
3406
  writeFileSync(`${d}/forge.yaml`, "engine:\n conformance: shadow\n");
3191
3407
  assert.equal(readVerify(d), undefined);
3192
3408
  writeFileSync(`${d}/forge.yaml`, "verify:\n mode: ci\n workflow: verify.yml\n skip_paths: \"*.md,docs/**\"\n max_rounds: 3\n precheck_timeout_seconds: 60\n");
3193
- assert.deepEqual(readVerify(d), { mode: "ci", workflow: "verify.yml", pathsInput: null, skipPaths: ["*.md", "docs/**"], maxRounds: 3, precheckTimeoutSeconds: 60, checkRun: false });
3409
+ assert.deepEqual(readVerify(d), { mode: "ci", workflow: "verify.yml", pathsInput: null, skipPaths: ["*.md", "docs/**"], maxRounds: 3, precheckTimeoutSeconds: 60, precheckTotalSeconds: 600, checkRun: false });
3194
3410
  });
3195
3411
  });
3196
3412
 
@@ -3529,7 +3745,7 @@ describe("local-mode CI fix rounds at merge and declared pre-PR check commands (
3529
3745
  assert.ok(w.seq.indexOf("builder-resume", w.seq.indexOf("check:1")) < w.seq.indexOf("pr"), "the fix runs before the PR is opened");
3530
3746
  const log = readLog(dir, 7);
3531
3747
  const pre = log.filter((e) => e.event === "PRECHECK_COMMANDS");
3532
- assert.deepEqual(pre.map((e) => e.results), [[{ command: "bash scripts/check-test-tmp-cleanup.sh", rc: 1 }], [{ command: "bash scripts/check-test-tmp-cleanup.sh", rc: 0 }]]);
3748
+ assert.deepEqual(pre.map((e) => e.results), [[{ command: "bash scripts/check-test-tmp-cleanup.sh", rc: 1, ms: 0 }], [{ command: "bash scripts/check-test-tmp-cleanup.sh", rc: 0, ms: 0 }]]);
3533
3749
  assert.ok(log.findIndex((e) => e.event === "PRECHECK_COMMANDS") < log.findIndex((e) => e.event === "PR_OPENED"));
3534
3750
  });
3535
3751
 
@@ -3552,6 +3768,16 @@ describe("local-mode CI fix rounds at merge and declared pre-PR check commands (
3552
3768
  assert.equal(builders(w).filter((b) => /declared check commands failed/.test(b[4])).length, 1);
3553
3769
  assert.match(traj(w)[0], /builder made no new commit for them/);
3554
3770
  });
3771
+
3772
+ it("#4318/#4331 pre-PR: a round has a total budget; a command gets what is left, the rest are deferred to CI, not a failure", async () => {
3773
+ const w = world(); const ran = [];
3774
+ w.io.runCommand = async ({ command, timeoutMs }) => { ran.push([command, timeoutMs]); w.clock += 60000; return { rc: 0, output: "" }; };
3775
+ const r = await run(w, { precheck: { commands: ["make a", "make b", "make c"] }, verify: { precheckTotalSeconds: 100 } });
3776
+ assert.equal(r.terminalReason, "merged");
3777
+ assert.deepEqual(ran, [["make a", 100000], ["make b", 40000]]);
3778
+ assert.equal(builders(w).filter((b) => /declared check commands failed/.test(b[4])).length, 0, "skipped is not failing");
3779
+ assert.match(traj(w)[0], /deferred-to-ci: pre-PR check budget 100 s reached; left to CI: `make c`/);
3780
+ });
3555
3781
  });
3556
3782
 
3557
3783
  describe("test map and repo facts caches (#4272)", () => {
@@ -3617,7 +3843,7 @@ describe("test map and repo facts caches (#4272)", () => {
3617
3843
  assert.equal((await run(w, { dir, precheck: { commands: ["true", "make lint"] } })).terminalReason, "merged");
3618
3844
  assert.deepEqual(ran, ["true", "make lint"]);
3619
3845
  assert.deepEqual(readLog(dir, 7).filter((e) => e.event === "PRECHECK_COMMANDS").map(({ round, results, ...rest }) => [round, results, Object.keys(rest).filter((k) => k === "source")]),
3620
- [[0, [{ command: "true", rc: 0 }, { command: "make lint", rc: 0 }], []]]);
3846
+ [[0, [{ command: "true", rc: 0, ms: 0 }, { command: "make lint", rc: 0, ms: 0 }], []]]);
3621
3847
  const [plan, impl] = builders(w);
3622
3848
  assert.equal(plan[4].startsWith(builderPlanPrompt(7)), true); assert.equal(impl[4].startsWith(builderImplementPrompt("local")), true);
3623
3849
  for (const b of builders(w)) assert.doesNotMatch(b[4], /Repo facts/);
@@ -4123,3 +4349,76 @@ describe("review trigger (#4273): shadow logs and always reviews, enforce skips
4123
4349
  assert.equal(at("engine:\n path: core\n"), undefined);
4124
4350
  });
4125
4351
  });
4352
+
4353
+ describe("enumerating tests + sequence numbers at hand-off (#4322)", () => {
4354
+ const LIST = [{ module: "src/foo.js", test: "test/registry.test.js", line: 4, why: "literal list of 3 sibling export names" }];
4355
+ const MISS = [{ symbol: "fooNew", module: "src/foo.js", test: "test/registry.test.js", line: 4 }];
4356
+ const COLL = [{ dir: "db", number: "0007", lane: "db/0007_usage.sql", base: ["db/0007_billing.sql"], next: "0008" }];
4357
+ const CONFLICTING = { mergeable: "CONFLICTING", mergeStateStatus: "DIRTY" }, OKSTATE = { mergeable: "MERGEABLE", mergeStateStatus: "CLEAN" };
4358
+ /** `seq`: sequenceCheck results per call; `verify`: sequenceVerify result; `missing`: enumeratingMissing results per call. */
4359
+ const hworld = ({ list = [], missing = [[]], seq = [[]], verify = { ok: true, problems: [] }, ...o } = {}) => {
4360
+ const w = world(o); w.enumCalls = []; w.seqCalls = 0;
4361
+ const m = [...missing], s = [...seq];
4362
+ w.io.enumeratingTests = async (x) => { w.enumCalls.push(x); return list; };
4363
+ w.io.enumeratingMissing = async () => (m.length > 1 ? m.shift() : m[0]);
4364
+ w.io.sequenceCheck = async () => { w.seqCalls++; return s.length > 1 ? s.shift() : s[0]; };
4365
+ w.io.sequenceSnapshot = async () => [{ path: "db/0007_usage.sql", blob: "b1" }];
4366
+ w.io.sequenceVerify = async (x) => { w.verifyArgs = x; return verify; };
4367
+ return w;
4368
+ };
4369
+
4370
+ it("no enumerating tests and no collisions: no new plan step, no extra builder call (prompts unchanged)", async () => {
4371
+ const w = hworld();
4372
+ assert.equal((await run(w)).terminalReason, "merged");
4373
+ const b = builders(w);
4374
+ assert.equal(b.length, 2);
4375
+ assert.ok(!/Enumerating tests/.test(b[0][4]) && !/Enumerating tests/.test(b[1][4]));
4376
+ assert.equal(w.enumCalls[0].cwd, WT);
4377
+ assert.equal(w.seqCalls, 1, "checked once at hand-off");
4378
+ });
4379
+
4380
+ it("the plan lists the enumerating tests and the implement rule cites them", async () => {
4381
+ const w = hworld({ list: LIST });
4382
+ assert.equal((await run(w)).terminalReason, "merged");
4383
+ const [plan, impl] = builders(w);
4384
+ assert.match(plan[4], /Enumerating tests \(found by the engine\)[\s\S]*src\/foo\.js -> test\/registry\.test\.js:4/);
4385
+ assert.match(impl[4], /one test-list edit per new symbol[\s\S]*not_members[\s\S]*test\/registry\.test\.js:4/);
4386
+ });
4387
+
4388
+ it("a new symbol missing from an enumerating test: one builder resume; still missing is an anomaly, not needs-human", async () => {
4389
+ let w = hworld({ missing: [MISS, []] });
4390
+ assert.equal((await run(w)).terminalReason, "merged");
4391
+ assert.equal(builders(w).length, 3);
4392
+ assert.match(builders(w)[2][4], /fooNew \(src\/foo\.js\) -> test\/registry\.test\.js:4/);
4393
+ w = hworld({ missing: [MISS] });
4394
+ assert.equal((await run(w)).terminalReason, "merged");
4395
+ assert.match(traj(w).at(-1), /Enumerating test\(s\) still miss new symbol\(s\): fooNew → test\/registry\.test\.js/);
4396
+ w = hworld({ missing: [MISS], result: { status: "fixed", summary: "s", evidence: [], tests: [], not_members: [{ symbol: "fooNew", test: "test/registry.test.js", why: "internal helper" }] } });
4397
+ assert.equal((await run(w)).terminalReason, "merged");
4398
+ assert.equal(builders(w).length, 2, "a justified not-member needs no resume");
4399
+ });
4400
+
4401
+ it("a sequence number the lane base already uses is renumbered at hand-off, before the PR; still colliding is base.conflict", async () => {
4402
+ let w = hworld({ seq: [COLL, []] });
4403
+ assert.equal((await run(w)).terminalReason, "merged");
4404
+ const b = builders(w);
4405
+ assert.equal(b.length, 3);
4406
+ assert.match(b[2][4], /origin\/staging already uses sequence numbers[\s\S]*db\/0007_usage\.sql -> number 0008 \(base has db\/0007_billing\.sql\)[\s\S]*git merge --no-edit origin\/staging/);
4407
+ assert.ok(w.seq.indexOf("pr") > w.seq.lastIndexOf("builder-resume"));
4408
+ assert.deepEqual(w.verifyArgs.before, [{ path: "db/0007_usage.sql", blob: "b1" }]);
4409
+ w = hworld({ seq: [COLL], verify: { ok: false, problems: ["db/0007_usage.sql still shares number 0007 with db/0007_billing.sql"] } });
4410
+ const r = await run(w);
4411
+ assert.equal(r.code, OUTCOME_CODES.BASE_CONFLICT);
4412
+ assert.equal(w.prs.length, 0);
4413
+ });
4414
+
4415
+ it("base-sync: a clean text merge that brings a taken number is renumbered and re-reviewed; a conflicted journal gets the no-text-merge rule", async () => {
4416
+ let w = hworld({ seq: [[], COLL, []], prView: [CONFLICTING, OKSTATE], mergeResults: ["clean"], reviews: [ok(), ok()] });
4417
+ assert.equal((await run(w)).terminalReason, "merged");
4418
+ assert.equal(reviewers(w).length, 2, "re-review after the renumber");
4419
+ assert.match(w.comments.find((c) => c.includes("<!-- FORGE:BASE_SYNC -->")), /clean merge[\s\S]*builder renumbered sequence-numbered files/);
4420
+ w = hworld({ prView: [CONFLICTING, OKSTATE], mergeResults: ["conflict"], conflictFiles: ["db/meta/_journal.json"], reviews: [ok(), ok()] });
4421
+ assert.equal((await run(w)).terminalReason, "merged");
4422
+ assert.match(builders(w)[2][4], /conflicts in: db\/meta\/_journal\.json[\s\S]*do not text-merge it\. Renumber the lane's files/);
4423
+ });
4424
+ });