forgedock 1.29.0 → 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 +102 -38
- 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 +32 -4
- package/bin/lean/context.test.mjs +71 -12
- 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 +207 -56
- package/bin/lean/lean.test.mjs +336 -9
- package/bin/lean/orchestrate.mjs +132 -83
- package/bin/lean/precheck.mjs +55 -0
- package/bin/lean/precheck.test.mjs +93 -0
- package/bin/lean/prefilter.mjs +96 -14
- package/bin/lean/prefilter.test.mjs +111 -2
- 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 +271 -7
- package/bin/lean/render.mjs +13 -0
- package/bin/lean/sequence.mjs +102 -0
- package/bin/lean/sequence.test.mjs +112 -0
- package/bin/lean/testmap.mjs +57 -3
- package/bin/lean/testmap.test.mjs +53 -1
- package/bin/lean/verify.mjs +7 -2
- package/bin/lean/verify.test.mjs +8 -2
- package/commands/deploy-pr.md +5 -4
- package/commands/orchestrate.md +7 -4
- package/commands/review-pr-staging.md +6 -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/code-index.sh +33 -33
- package/scripts/code-index.test.sh +21 -2
- package/scripts/fixtures/test-map/go/.github/workflows/ci.yml +2 -0
- package/scripts/repo-facts.test.sh +5 -5
- package/scripts/review-prepare.sh +28 -6
- package/scripts/review-prepare.test.sh +22 -2
- package/scripts/test-map.sh +186 -34
- package/scripts/test-map.test.sh +250 -7
- package/scripts/verify-phase-trail.test.sh +44 -44
- package/scripts/wait-ci-green.sh +1 -1
- package/scripts/wait-ci-green.test.sh +4 -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: [] } });
|
|
@@ -787,6 +876,24 @@ describe("in-batch recovery of mechanical needs-human (#4267)", () => {
|
|
|
787
876
|
assert.equal((await run(plain, ends["ci-pending"][1])).terminalReason, "ci-pending");
|
|
788
877
|
assert.equal(add(plain), 0, "no recovery: a ci-pending run is not labelled");
|
|
789
878
|
});
|
|
879
|
+
|
|
880
|
+
it("#4302: a non-final recovery attempt (recover.final false) never relabels needs-human; final true or absent relabels once", async () => {
|
|
881
|
+
const add = (w) => w.edits.filter((e) => e.join(" ") === "issue edit 7 -R o/r --add-label needs-human").length;
|
|
882
|
+
const nh = (w) => w.comments.filter((c) => c.startsWith("<!-- FORGE:NEEDS_HUMAN -->")).length;
|
|
883
|
+
const ends = {
|
|
884
|
+
"ci-pending": [{ waits: Array(500).fill({ rc: 2, text: "" }) }, { ciMaxMinutes: 10, mergeWait: "all" }],
|
|
885
|
+
"needs-human": [{ waits: [{ rc: 1, text: "lint failed" }] }, { mergeWait: "all" }],
|
|
886
|
+
};
|
|
887
|
+
for (const [reason, [wo, ro]] of Object.entries(ends)) {
|
|
888
|
+
for (const [final, want] of [[false, 0], [true, 1], [undefined, 1]]) {
|
|
889
|
+
const w = world(wo);
|
|
890
|
+
const r = await run(w, { recover: { code: "ci.red", attempt: 1, ...(final === undefined ? {} : { final }) }, ...ro });
|
|
891
|
+
assert.equal(r.terminalReason, reason);
|
|
892
|
+
assert.equal(add(w), want, `${reason} final=${final}: label`); assert.equal(nh(w), want, `${reason} final=${final}: comment`);
|
|
893
|
+
if (reason === "needs-human") assert.equal(r.recovery, true);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
});
|
|
790
897
|
});
|
|
791
898
|
|
|
792
899
|
describe("devdocs freshness gate (the repo's own scripts/check-devdocs-freshness.sh)", () => {
|
|
@@ -1008,6 +1115,60 @@ describe("conformance shadow mode (the default)", () => {
|
|
|
1008
1115
|
assert.match(nhBody(w), /validate V1 \(`node --test test\/foo\.test\.js`\) failed \(exit 1\)/);
|
|
1009
1116
|
});
|
|
1010
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
|
+
|
|
1011
1172
|
it("a failed validate fetches the lane and the resume says how to catch up with a moved base (merge, never rebase)", async () => {
|
|
1012
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 }] });
|
|
1013
1174
|
assert.equal((await runShadow(w)).terminalReason, "merged");
|
|
@@ -1306,17 +1467,35 @@ describe("bug lanes freeze a red-green repro_test (#4271)", () => {
|
|
|
1306
1467
|
assert.equal((await run(w)).terminalReason, "merged");
|
|
1307
1468
|
assert.match(builders(w)[0][4], /This issue is a bug\. When it is testable, .*"repro_test":\{"path"/);
|
|
1308
1469
|
assert.match(builders(w)[0][4], /"repro_test":"none","repro_test_reason"/);
|
|
1309
|
-
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 }]);
|
|
1310
1471
|
assert.match(w.comments.find((c) => c.includes("FORGE:CONTRACT")), /"repro_test"/, "repro_test is frozen in the contract JSON");
|
|
1311
1472
|
assert.ok(traj(w)[0].includes("repro_test test/foo.test.js foo handles null: BASE=FAILED HEAD=RAN"));
|
|
1312
1473
|
assert.equal(builders(w).length, 2); assert.equal(reviewers(w).length, 1);
|
|
1313
1474
|
const t = rworld([RED_GREEN], { labels: [{ name: "type:bug" }] });
|
|
1314
1475
|
assert.equal((await run(t)).terminalReason, "merged"); assert.equal(t.rgCalls.length, 1, "type:bug counts as a bug");
|
|
1315
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
|
+
});
|
|
1316
1495
|
|
|
1317
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 () => {
|
|
1318
1497
|
for (const conformance of ["enforce", "shadow"]) {
|
|
1319
|
-
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" }]) {
|
|
1320
1499
|
const w = rworld([rg]);
|
|
1321
1500
|
const r = await run(w, { conformance });
|
|
1322
1501
|
assert.equal(r.code, "conformance.unimplemented", `${conformance} ${JSON.stringify(rg)}`); assert.equal(w.merges.length, 0);
|
|
@@ -1330,6 +1509,27 @@ describe("bug lanes freeze a red-green repro_test (#4271)", () => {
|
|
|
1330
1509
|
assert.equal((await run(fixed, { conformance: "shadow" })).terminalReason, "merged"); assert.equal(fixed.rgCalls.length, 2);
|
|
1331
1510
|
});
|
|
1332
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
|
+
|
|
1333
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 () => {
|
|
1334
1534
|
const none = { ...CONTRACT, repro_test: "none", repro_test_reason: "spec-only change, nothing executable" };
|
|
1335
1535
|
const w = rworld([RED_GREEN], { contract: none });
|
|
@@ -1783,6 +1983,21 @@ describe("orchestrate", () => {
|
|
|
1783
1983
|
assert.ok(log.indexOf("end 12") < log.indexOf("start 2"));
|
|
1784
1984
|
});
|
|
1785
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
|
+
|
|
1786
2001
|
it("a dependency that closed as invalid does not block", async () => {
|
|
1787
2002
|
const h = harness({ 2: "**Depends on**: #1" }, { 1: "invalid" });
|
|
1788
2003
|
const rows = await orchestrate({ issues: [1, 2], parallel: 2, ...h });
|
|
@@ -2613,6 +2828,25 @@ describe("batch robustness (#4112-#4115)", () => {
|
|
|
2613
2828
|
assert.equal(r.heavyQueueMax, 4);
|
|
2614
2829
|
});
|
|
2615
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
|
+
|
|
2616
2850
|
it("#4169 without io.heavy (or heavy.enabled false) nothing is gated and the runner env is unchanged", async () => {
|
|
2617
2851
|
const w = world();
|
|
2618
2852
|
const r = await run(w);
|
|
@@ -3172,7 +3406,7 @@ describe("verify-by-CI (#4167): ci mode runs the heavy checks on CI and feeds fa
|
|
|
3172
3406
|
writeFileSync(`${d}/forge.yaml`, "engine:\n conformance: shadow\n");
|
|
3173
3407
|
assert.equal(readVerify(d), undefined);
|
|
3174
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");
|
|
3175
|
-
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 });
|
|
3176
3410
|
});
|
|
3177
3411
|
});
|
|
3178
3412
|
|
|
@@ -3511,7 +3745,7 @@ describe("local-mode CI fix rounds at merge and declared pre-PR check commands (
|
|
|
3511
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");
|
|
3512
3746
|
const log = readLog(dir, 7);
|
|
3513
3747
|
const pre = log.filter((e) => e.event === "PRECHECK_COMMANDS");
|
|
3514
|
-
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 }]]);
|
|
3515
3749
|
assert.ok(log.findIndex((e) => e.event === "PRECHECK_COMMANDS") < log.findIndex((e) => e.event === "PR_OPENED"));
|
|
3516
3750
|
});
|
|
3517
3751
|
|
|
@@ -3534,6 +3768,16 @@ describe("local-mode CI fix rounds at merge and declared pre-PR check commands (
|
|
|
3534
3768
|
assert.equal(builders(w).filter((b) => /declared check commands failed/.test(b[4])).length, 1);
|
|
3535
3769
|
assert.match(traj(w)[0], /builder made no new commit for them/);
|
|
3536
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
|
+
});
|
|
3537
3781
|
});
|
|
3538
3782
|
|
|
3539
3783
|
describe("test map and repo facts caches (#4272)", () => {
|
|
@@ -3547,10 +3791,20 @@ describe("test map and repo facts caches (#4272)", () => {
|
|
|
3547
3791
|
{ globs: ["**/*.py"], command: "uv run pytest", cwd: ".", source: "pyproject.toml" }, { globs: ["test/**"], command: "npm test", cwd: "pkg", source: "pkg/package.json" }]);
|
|
3548
3792
|
const dir = tmpDir("core-");
|
|
3549
3793
|
assert.equal((await run(w, { dir, precheck: { commands: ["make lint"] } })).terminalReason, "merged");
|
|
3550
|
-
|
|
3794
|
+
// #4315: the map adds to learned.test_commands, never replaces them
|
|
3795
|
+
assert.deepEqual(ran, ["make lint", "npm run lint", "cd pkg && npm test"]);
|
|
3551
3796
|
const pre = readLog(dir, 7).filter((e) => e.event === "PRECHECK_COMMANDS");
|
|
3552
|
-
assert.deepEqual(pre.map((e) => [e.source, e.results.map((r) => r.command)]), [["test-map", ["npm run lint", "cd pkg && npm test"]]]);
|
|
3553
|
-
assert.match(traj(w)[0], /Pre-PR checks selected from \.forge\/test-map\.json: 2 of 3 entries/);
|
|
3797
|
+
assert.deepEqual(pre.map((e) => [e.source, e.results.map((r) => r.command)]), [["test-map+learned", ["make lint", "npm run lint", "cd pkg && npm test"]]]);
|
|
3798
|
+
assert.match(traj(w)[0], /Pre-PR checks selected from \.forge\/test-map\.json: 2 of 3 entries, plus 1 learned/);
|
|
3799
|
+
});
|
|
3800
|
+
|
|
3801
|
+
it("#4315 a partial test map still runs the learned commands it does not cover, each once", async () => {
|
|
3802
|
+
const w = world(); const ran = recorder(w);
|
|
3803
|
+
w.baseFiles = { [MAPF]: map([{ globs: ["src/**"], command: "npm test", cwd: "." }, { globs: ["**/*.go"], command: "go test ./...", cwd: "." }]) };
|
|
3804
|
+
const dir = tmpDir("core-");
|
|
3805
|
+
assert.equal((await run(w, { dir, precheck: { commands: ["npm test", "make lint"] } })).terminalReason, "merged");
|
|
3806
|
+
assert.deepEqual(ran, ["npm test", "make lint"]);
|
|
3807
|
+
assert.deepEqual(readLog(dir, 7).filter((e) => e.event === "PRECHECK_COMMANDS").map((e) => Object.keys(e).includes("source")), [false]);
|
|
3554
3808
|
});
|
|
3555
3809
|
|
|
3556
3810
|
it("the main checkout's test map is used when the base ref has none; a map alone enables the step", async () => {
|
|
@@ -3589,7 +3843,7 @@ describe("test map and repo facts caches (#4272)", () => {
|
|
|
3589
3843
|
assert.equal((await run(w, { dir, precheck: { commands: ["true", "make lint"] } })).terminalReason, "merged");
|
|
3590
3844
|
assert.deepEqual(ran, ["true", "make lint"]);
|
|
3591
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")]),
|
|
3592
|
-
[[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 }], []]]);
|
|
3593
3847
|
const [plan, impl] = builders(w);
|
|
3594
3848
|
assert.equal(plan[4].startsWith(builderPlanPrompt(7)), true); assert.equal(impl[4].startsWith(builderImplementPrompt("local")), true);
|
|
3595
3849
|
for (const b of builders(w)) assert.doesNotMatch(b[4], /Repo facts/);
|
|
@@ -4095,3 +4349,76 @@ describe("review trigger (#4273): shadow logs and always reviews, enforce skips
|
|
|
4095
4349
|
assert.equal(at("engine:\n path: core\n"), undefined);
|
|
4096
4350
|
});
|
|
4097
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
|
+
});
|