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.
- package/bin/engine/outcome-codes.mjs +4 -2
- package/bin/engine/phases.mjs +20 -3
- package/bin/engine-cli.mjs +94 -36
- package/bin/engine.mjs +3 -1
- package/bin/lean/batch.test.mjs +62 -1
- package/bin/lean/conformance.mjs +129 -8
- package/bin/lean/conformance.test.mjs +77 -3
- package/bin/lean/context.mjs +17 -12
- package/bin/lean/context.test.mjs +36 -5
- package/bin/lean/effects.mjs +20 -6
- package/bin/lean/enumerating.mjs +189 -0
- package/bin/lean/enumerating.test.mjs +97 -0
- package/bin/lean/lean.mjs +189 -43
- package/bin/lean/lean.test.mjs +305 -6
- package/bin/lean/orchestrate.mjs +104 -85
- package/bin/lean/precheck.mjs +55 -0
- package/bin/lean/precheck.test.mjs +93 -0
- package/bin/lean/prefilter.mjs +21 -4
- package/bin/lean/prefilter.test.mjs +24 -1
- package/bin/lean/preflight.mjs +133 -0
- package/bin/lean/preflight.test.mjs +61 -0
- package/bin/lean/prompts.mjs +37 -3
- package/bin/lean/recovery.test.mjs +75 -0
- package/bin/lean/sequence.mjs +102 -0
- package/bin/lean/sequence.test.mjs +112 -0
- package/bin/lean/testmap.mjs +43 -6
- package/bin/lean/testmap.test.mjs +38 -1
- package/bin/lean/verify.mjs +7 -2
- package/bin/lean/verify.test.mjs +8 -2
- package/commands/orchestrate.md +3 -1
- package/docs/CONFIG.md +8 -1
- package/docs/OUTCOME-CODES.md +6 -4
- package/docs/RELEASE-NOTES.md +10 -0
- package/package.json +1 -1
- package/scripts/acceptance-red-green.sh +13 -4
- package/scripts/acceptance-red-green.test.sh +14 -0
- package/scripts/test-map.sh +63 -17
- package/scripts/test-map.test.sh +55 -0
package/bin/lean/lean.test.mjs
CHANGED
|
@@ -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" }
|
|
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
|
+
});
|