forgedock 1.34.0 → 1.36.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/README.md +78 -324
- package/bin/engine/events.mjs +152 -0
- package/bin/engine/events.test.mjs +128 -0
- package/bin/engine/telemetry-seam.mjs +4 -3
- package/bin/engine-cli.mjs +181 -26
- package/bin/experiment.mjs +55 -0
- package/bin/forgedock.mjs +19 -1
- package/bin/hooks/reread-nudge.mjs +44 -5
- package/bin/lean/batch.mjs +66 -3
- package/bin/lean/batch.test.mjs +176 -1
- package/bin/lean/budget.mjs +14 -3
- package/bin/lean/budget.test.mjs +11 -1
- package/bin/lean/conflictres.mjs +93 -0
- package/bin/lean/conflictres.test.mjs +93 -0
- package/bin/lean/conformance.mjs +30 -9
- package/bin/lean/conformance.test.mjs +51 -0
- package/bin/lean/effects.mjs +14 -3
- package/bin/lean/execution.mjs +2 -0
- package/bin/lean/i18n.mjs +65 -0
- package/bin/lean/i18n.test.mjs +47 -0
- package/bin/lean/lane.mjs +16 -0
- package/bin/lean/lean.mjs +147 -36
- package/bin/lean/lean.test.mjs +319 -8
- package/bin/lean/orchestrate.mjs +130 -13
- package/bin/lean/postdeploy-check.mjs +149 -0
- package/bin/lean/postdeploy-check.test.mjs +146 -0
- package/bin/lean/postdeploy-monitor.mjs +87 -0
- package/bin/lean/postdeploy-monitor.test.mjs +121 -0
- package/bin/lean/precheck.mjs +58 -8
- package/bin/lean/precheck.test.mjs +77 -3
- package/bin/lean/prefilter.mjs +29 -4
- package/bin/lean/prefilter.test.mjs +13 -5
- package/bin/lean/probe.mjs +2 -1
- package/bin/lean/prompts.mjs +36 -6
- package/bin/lean/sandbox-provider.contract.test.mjs +28 -0
- package/bin/lean/sandbox-remote.mjs +7 -0
- package/bin/lean/schedd.mjs +34 -5
- package/bin/lean/schedd.test.mjs +84 -0
- package/bin/lean/sequence.mjs +26 -0
- package/bin/lean/sequence.test.mjs +41 -2
- package/bin/lean/toolstate.mjs +77 -0
- package/bin/lean/toolstate.test.mjs +83 -0
- package/bin/lean/verify.mjs +18 -2
- package/bin/lean/verify.test.mjs +29 -3
- package/bin/stats/aggregate.mjs +1 -1
- package/bin/stats/experiment.mjs +44 -0
- package/bin/stats/github-source.mjs +2 -1
- package/bin/stats/index.mjs +42 -2
- package/bin/stats/release.mjs +75 -0
- package/bin/stats/release.test.mjs +68 -0
- package/commands/orchestrate.md +8 -3
- package/docs/CONFIG.md +61 -2
- package/docs/EVENTS.md +68 -0
- package/docs/STATS.md +31 -0
- package/docs/TELEMETRY.md +3 -1
- package/docs/site/install.md +89 -0
- package/package.json +1 -1
- package/scripts/bench-lightlane.mjs +2 -1
- package/scripts/eval-gate-scorecard.mjs +7 -0
- package/scripts/file-deferred-followups.sh +1 -0
- package/scripts/file-deferred-followups.test.sh +8 -0
package/bin/lean/lean.test.mjs
CHANGED
|
@@ -118,10 +118,14 @@ function world(o = {}) {
|
|
|
118
118
|
if (a[0] === "write-tree") return w.salvageTrees.length ? w.salvageTrees.shift() : "";
|
|
119
119
|
if (a[0] === "rev-parse" && a[1] === "HEAD^{tree}") return "0".repeat(40);
|
|
120
120
|
if (a.includes("commit-tree")) return "c".repeat(40);
|
|
121
|
+
if (a[0] === "diff" && a.includes("--diff-filter=A")) return (w.added ?? []).join("\0"); // #4590: files the branch added vs the base
|
|
122
|
+
if (a[0] === "rm" && a[1] === "--cached" && w.added) w.added = w.added.filter((f) => !a.includes(f));
|
|
121
123
|
if (a[0] === "diff" && a.includes("--diff-filter=U")) return w.conflictFiles.join("\n");
|
|
122
124
|
if (a[0] === "diff" && a[1] === "--check") return "";
|
|
123
125
|
if (a[0] === "grep") return w.markers ? "src/foo.js:3:<<<<<<< HEAD" : "";
|
|
124
126
|
if (a[0] === "merge" && a[1] === "--abort") { w.mergeInProgress = false; return ""; }
|
|
127
|
+
if (a[0] === "rev-parse" && a.length === 2 && (o.shipped ?? []).includes(String(a[1]).replace(/^origin\//, ""))) return `tip-${a[1]}`; // #4623
|
|
128
|
+
if (a[0] === "rev-list" && a.includes("--merges")) { const l = String(a.at(-1)).slice(1).replace(/^origin\//, ""); return (o.shipped ?? []).includes(l) ? `m1 s1 tip-origin/${l}\n` : ""; }
|
|
125
129
|
if (a[0] === "rev-parse" && a.length === 2 && /^origin\//.test(String(a[1]))) return w.baseHeads.length > 1 ? w.baseHeads.shift() : w.baseHeads[0]; // #4386: the base head moves only when a test says so
|
|
126
130
|
if (a[0] === "rev-parse") return w.head;
|
|
127
131
|
if (a[0] === "diff" && a[1] === "--name-only" && !a.some((x) => String(x).includes("..."))) return w.changed.filter((f) => !(o.equalToBase ?? []).includes(f)).join("\n");
|
|
@@ -129,6 +133,7 @@ function world(o = {}) {
|
|
|
129
133
|
if (a[0] === "diff" && a[1] === "--numstat") return w.numstat ?? "";
|
|
130
134
|
if (a[0] === "ls-files") return [...new Set(["src/foo.js", "src/bar.js", "test/foo.test.js", ...w.changed])].join("\n");
|
|
131
135
|
if (a[0] === "diff") return "diff --git a/src/foo.js b/src/foo.js\n+fix";
|
|
136
|
+
if (a[0] === "merge-base" && a[1] === "--is-ancestor") { if ([...(o.shipped ?? []), ...(o.unusedAncestors ?? [])].includes(String(a[2]).replace(/^origin\//, ""))) return ""; throw new Error("not an ancestor"); } // #4593
|
|
132
137
|
if (a[0] === "ls-remote" && String(a.at(-1)).startsWith("refs/heads/")) { const b = a.at(-1).slice("refs/heads/".length); return (o.branchesOnOrigin ?? ["staging", "main"]).includes(b) ? `abc\trefs/heads/${b}\n` : ""; }
|
|
133
138
|
if (a[0] === "ls-remote") return w.remote;
|
|
134
139
|
if (a[0] === "show" && w.baseFiles) { if (!(a[1] in w.baseFiles)) throw new Error(`fatal: path not in ${a[1]}`); return w.baseFiles[a[1]]; }
|
|
@@ -791,8 +796,9 @@ describe("conformance gate (frozen contract, precheck, structured review)", () =
|
|
|
791
796
|
|
|
792
797
|
for (const d of ["defer", "post-deploy"]) {
|
|
793
798
|
it(`a contracted ${d} is filed only after the reviewer accepts it, then linked in the PR and on close`, async () => {
|
|
794
|
-
const k = two(d, { why: d === "defer" ? "separate module" : "only measurable after deploy" });
|
|
799
|
+
const k = two(d, { why: d === "defer" ? "separate module" : "only measurable after deploy", ...(d === "post-deploy" ? { check: "login-smoke" } : {}) });
|
|
795
800
|
const w = world({ contract: k, claims: [{ id: "R1", status: "implemented" }], reviews: [conf({}, { ...k, requirements: [k.requirements[0], { ...k.requirements[1], tracked: "#900" }] })] });
|
|
801
|
+
if (d === "post-deploy") w.baseFiles = { "origin/staging:.forge/post-deploy-checks.json": JSON.stringify({ "login-smoke": "true" }) };
|
|
796
802
|
const r = await run(w);
|
|
797
803
|
assert.equal(r.terminalReason, "merged");
|
|
798
804
|
assert.equal(w.followups.length, 1); assert.equal(w.followups[0].parent, 7);
|
|
@@ -1618,9 +1624,9 @@ describe("hotspot prevention (forge#4195): plan facts, build rule, unexempted gr
|
|
|
1618
1624
|
assert.equal(hotspotBuildRule(facts([], { file_lines: 1000, growth_lines: 50 })).trim(), "Don't grow a file over 1000 lines by more than 50 lines. Put new code next to it.");
|
|
1619
1625
|
assert.equal(hotspotBuildRule(null), ""); assert.equal(hotspotBuildRule(facts([], { enabled: false })), "");
|
|
1620
1626
|
const plan = builderPlanPrompt(7) + hotspotPlanNote(facts([HOT, { ...HOT, path: "a/b.py" }, { ...HOT, path: "c/d.py" }]));
|
|
1621
|
-
// 1850: #4404 (verify out_of_band) and #4396 (split size rule) grew the plan prompt to ~1803 tokens on CI paths.
|
|
1622
|
-
assert.ok(approxTokens(plan) <=
|
|
1623
|
-
assert.ok(approxTokens(builderImplementPrompt() + hotspotBuildRule(facts([]))) <=
|
|
1627
|
+
// 1850: #4404 (verify out_of_band) and #4396 (split size rule) grew the plan prompt to ~1803 tokens on CI paths; 1900: #4589 (check timeout is not a block, one block-rule line).
|
|
1628
|
+
assert.ok(approxTokens(plan) <= 1900, `plan prompt with hotspots is ${approxTokens(plan)} tokens`);
|
|
1629
|
+
assert.ok(approxTokens(builderImplementPrompt() + hotspotBuildRule(facts([]))) <= 1600); // 1600: #4591 (never hand-name migration files) and #4596 (i18n translate rule), one line each
|
|
1624
1630
|
});
|
|
1625
1631
|
|
|
1626
1632
|
it("#4519 generated_paths: both prompts name the globs (even with the gate off), and an amendment adding one blocks", async () => {
|
|
@@ -2038,7 +2044,7 @@ describe("comment templates", () => {
|
|
|
2038
2044
|
it("plan and implement prompts carry the stage templates and the contract schema within budget", () => {
|
|
2039
2045
|
const plan = builderPlanPrompt(7), impl = builderImplementPrompt();
|
|
2040
2046
|
assert.ok(approxTokens(plan) <= 1800, `plan prompt is ${approxTokens(plan)} tokens`);
|
|
2041
|
-
assert.ok(approxTokens(impl) <=
|
|
2047
|
+
assert.ok(approxTokens(impl) <= 1550, `implement prompt is ${approxTokens(impl)} tokens`); // 1550: #4596 i18n translate rule
|
|
2042
2048
|
for (const h of ["### What Was Claimed", "### What We Found", "### Root Cause", "### Affected Files", "### Sibling Sweep", "### History", "### Recommendation", "### Related Issues",
|
|
2043
2049
|
"investigation.md", "contract.json", "PLANNING ONLY", '"requirements"', '"deliverables"', '"preserve"', '"validate"', "verbatim", "defer", "post-deploy"]) assert.ok(plan.includes(h), h);
|
|
2044
2050
|
for (const h of ["### Approach", "### Changes", "### Acceptance Criteria Status", "### Verification", "### Notes", "implementation.md", "result.json",
|
|
@@ -2071,6 +2077,83 @@ describe("merge_wait policy", () => {
|
|
|
2071
2077
|
const w2 = world(); w2.io.requiredChecks = async () => ["build"]; w2.io.checkStates = async () => states({ build: "pending" });
|
|
2072
2078
|
assert.equal((await run(w2, { ciMaxMinutes: 5 })).terminalReason, "ci-pending");
|
|
2073
2079
|
});
|
|
2080
|
+
describe("#4624 merge-time rerun / wait / flaky branch of mergeFixRound", () => {
|
|
2081
|
+
const RUN = (n, attempt) => `https://github.com/o/r/actions/runs/${n}/job/${attempt}`;
|
|
2082
|
+
const NO_FIX = { ...parseVerify({}), maxRounds: 0 };
|
|
2083
|
+
const WAIT = 20000;
|
|
2084
|
+
const waits = (w) => { const n = { n: 0 }; const s = w.io.sleep; w.io.sleep = async (ms) => { if (ms === WAIT) n.n++; return s(ms); }; return n; };
|
|
2085
|
+
const fixPrompts = (w) => { const p = []; const inner = w.io.runner; w.io.runner = async (c) => { if (/CI failed on the pushed branch/.test(c.prompt)) p.push(c.prompt); return inner(c); }; return p; };
|
|
2086
|
+
const events = (dir, name) => readLog(dir, 7).filter((e) => e.event === name);
|
|
2087
|
+
/** `build` (run 77) is red; `urlAfter` is its URL once the rerun was triggered, `stateAfter` its state then. */
|
|
2088
|
+
const redBuild = (stateAfter, urlAfter) => {
|
|
2089
|
+
const w = world(); w.reran = [];
|
|
2090
|
+
w.io.requiredChecks = async () => ["build"];
|
|
2091
|
+
w.io.rerunFailedJobs = async (id) => { w.reran.push(id); };
|
|
2092
|
+
w.io.checkStates = async () => (w.reran.length ? [{ name: "build", state: stateAfter, url: urlAfter }] : [{ name: "build", state: "failure", url: RUN(77, 1) }]);
|
|
2093
|
+
w.io.failedLogs = async () => "boom";
|
|
2094
|
+
return w;
|
|
2095
|
+
};
|
|
2096
|
+
|
|
2097
|
+
it("a failed required check is rerun once, waited on while its URL is unchanged, and merges when the new attempt is green without a fix round", async () => {
|
|
2098
|
+
const w = redBuild("success", RUN(77, 2)); const p = fixPrompts(w), dir = tmpDir("core-");
|
|
2099
|
+
// the rerun was triggered, but the check still reports the old attempt for two more polls
|
|
2100
|
+
const states = w.io.checkStates; let stale = 2;
|
|
2101
|
+
w.io.checkStates = async () => (w.reran.length && stale-- > 0 ? [{ name: "build", state: "failure", url: RUN(77, 1) }] : states());
|
|
2102
|
+
const n = waits(w);
|
|
2103
|
+
const r = await run(w, { dir });
|
|
2104
|
+
assert.equal(r.terminalReason, "merged"); assert.deepEqual(w.reran, ["77"]); assert.equal(p.length, 0);
|
|
2105
|
+
assert.equal(n.n, 3, "one wait after the rerun, then two while the URL is unchanged");
|
|
2106
|
+
assert.deepEqual(events(dir, "CI_FLAKE_RERUN").map((e) => [e.trigger, e.result, e.runs]), [["merge", "rerun-started", ["77"]]]);
|
|
2107
|
+
assert.equal(events(dir, "CI_FIX_ROUND").length, 0);
|
|
2108
|
+
});
|
|
2109
|
+
|
|
2110
|
+
it("a rerun whose check URL never changes is waited on 20 times, then falls through to the fix round; the head is rerun only once", async () => {
|
|
2111
|
+
const w = redBuild("failure", RUN(77, 1)); const n = waits(w), dir = tmpDir("core-");
|
|
2112
|
+
const r = await run(w, { dir, verify: NO_FIX });
|
|
2113
|
+
assert.equal(r.terminalReason, "needs-human"); assert.equal(r.code, OUTCOME_CODES.CI_RED); assert.equal(w.merges.length, 0);
|
|
2114
|
+
assert.equal(n.n, 21, "the wait after the rerun plus 20 bounded waits");
|
|
2115
|
+
assert.deepEqual(w.reran, ["77"]);
|
|
2116
|
+
assert.equal(events(dir, "CI_FLAKE_RERUN").length, 1);
|
|
2117
|
+
});
|
|
2118
|
+
|
|
2119
|
+
it("a rerun that reports a new attempt still red goes to the fix round and reaches the builder", async () => {
|
|
2120
|
+
const w = redBuild("failure", RUN(77, 2)); const p = fixPrompts(w), n = waits(w), dir = tmpDir("core-");
|
|
2121
|
+
await run(w, { dir, verify: { ...parseVerify({}), maxRounds: 1 } });
|
|
2122
|
+
assert.ok(n.n >= 1); assert.equal(p.length, 1); assert.equal(w.reran[0], "77");
|
|
2123
|
+
assert.deepEqual(events(dir, "CI_FIX_ROUND").map((e) => [e.trigger, e.jobs]), [["merge", ["build"]]]);
|
|
2124
|
+
});
|
|
2125
|
+
|
|
2126
|
+
it("a rerun that cannot start is logged rerun-failed and the lane does not wait", async () => {
|
|
2127
|
+
const w = redBuild("failure", RUN(77, 1)); const n = waits(w), dir = tmpDir("core-");
|
|
2128
|
+
w.io.rerunFailedJobs = async () => { throw new Error("HTTP 403"); };
|
|
2129
|
+
const r = await run(w, { dir, verify: NO_FIX });
|
|
2130
|
+
assert.equal(r.code, OUTCOME_CODES.CI_RED); assert.equal(n.n, 0);
|
|
2131
|
+
assert.deepEqual(events(dir, "CI_FLAKE_RERUN").map((e) => e.result), ["rerun-failed"]);
|
|
2132
|
+
});
|
|
2133
|
+
|
|
2134
|
+
it("verify.flaky: a required check still red after the rerun is reported and ends needs-human; nothing merged, builder not resumed", async () => {
|
|
2135
|
+
const w = redBuild("failure", RUN(77, 2)); const p = fixPrompts(w), dir = tmpDir("core-");
|
|
2136
|
+
const r = await run(w, { dir, verify: { ...parseVerify({}), flaky: ["build"] } });
|
|
2137
|
+
assert.equal(r.terminalReason, "needs-human"); assert.equal(r.code, OUTCOME_CODES.CI_RED);
|
|
2138
|
+
assert.equal(w.merges.length, 0); assert.equal(p.length, 0); assert.deepEqual(w.reran, ["77"]);
|
|
2139
|
+
assert.deepEqual(events(dir, "CI_FLAKY_REPORTED").map((e) => [e.trigger, e.jobs]), [["merge", ["build"]]]);
|
|
2140
|
+
assert.ok(JSON.stringify(w.comments).includes("still failed on PR #"), "needs-human message names the rerun");
|
|
2141
|
+
});
|
|
2142
|
+
|
|
2143
|
+
it("verify.flaky with a mix: only the non-flaky required check reaches the builder", async () => {
|
|
2144
|
+
const w = world(); w.reran = [];
|
|
2145
|
+
w.io.requiredChecks = async () => ["build", "lint"];
|
|
2146
|
+
w.io.rerunFailedJobs = async (id) => { w.reran.push(id); };
|
|
2147
|
+
w.io.checkStates = async () => [{ name: "build", state: "failure", url: RUN(77, w.reran.length + 1) }, { name: "lint", state: "failure", url: RUN(78, w.reran.length + 1) }];
|
|
2148
|
+
w.io.failedLogs = async () => "boom";
|
|
2149
|
+
const dir = tmpDir("core-");
|
|
2150
|
+
await run(w, { dir, verify: { ...parseVerify({}), flaky: ["build"], maxRounds: 1 } });
|
|
2151
|
+
assert.deepEqual(w.reran.slice(0, 2).sort(), ["77", "78"], "both red runs rerun on the first head");
|
|
2152
|
+
assert.deepEqual(events(dir, "CI_FLAKY_REPORTED")[0].jobs, ["build"]);
|
|
2153
|
+
assert.deepEqual(events(dir, "CI_FIX_ROUND").map((e) => e.jobs), [["lint"]]);
|
|
2154
|
+
});
|
|
2155
|
+
});
|
|
2156
|
+
|
|
2074
2157
|
it("all waits for every check (waitCiGreen); none merges immediately", async () => {
|
|
2075
2158
|
const w = world({ waits: [{ rc: 2, text: "" }] }); let req = 0; w.io.requiredChecks = async () => { req++; return []; };
|
|
2076
2159
|
assert.equal((await run(w, { mergeWait: "all" })).terminalReason, "merged");
|
|
@@ -2103,6 +2186,9 @@ describe("prompts", () => {
|
|
|
2103
2186
|
const p = reviewerPrompt({ issue: { number: 7, title: "t", body: "b" }, comments: [{ body: "please also cover bar" }, { body: "<!-- FORGE:STARTED -->\nx" }], contract: CONTRACT, result: { status: "fixed" }, files: ["src/foo.js"], diff: "x".repeat(2000) });
|
|
2104
2187
|
for (const t of ["<issue_body>", "<issue_comments>\nplease also cover bar\n</issue_comments>", "<contract>", '"R1"', "<builder_result>", "<changed_files>\nsrc/foo.js", "<diff>"]) assert.ok(p.includes(t), t);
|
|
2105
2188
|
assert.doesNotMatch(p, /FORGE:STARTED/);
|
|
2189
|
+
const am = reviewerPrompt({ issue: { number: 7, title: "t", body: "b" }, contract: { ...CONTRACT, requirements: [{ ...CONTRACT.requirements[0], amendment: { text: "reveal once", why: "hash-only token" } }] }, diff: "" });
|
|
2190
|
+
assert.ok(am.includes("reveal once") && am.includes("hash-only token"));
|
|
2191
|
+
assert.match(REVIEWER_SYSTEM, /amendment\.text/);
|
|
2106
2192
|
assert.ok(approxTokens(REVIEWER_SYSTEM) + approxTokens(reviewerPrompt({ issue: { number: 7, title: "t", body: "b" }, contract: CONTRACT, diff: "" })) <= 1800);
|
|
2107
2193
|
});
|
|
2108
2194
|
|
|
@@ -2243,6 +2329,51 @@ describe("orchestrate", () => {
|
|
|
2243
2329
|
assert.deepEqual(rows.map((r) => [r.issue, r.outcome, r.detail]), [[9, "needs-human", "ci.red"], [1, "skipped", "dependency #9 needs-human"]]);
|
|
2244
2330
|
});
|
|
2245
2331
|
|
|
2332
|
+
it("#4595 a dependent skipped for a needs-human blocker is re-admitted when the blocker merges meanwhile; its dependents follow", async () => {
|
|
2333
|
+
const log = [];
|
|
2334
|
+
let merged = false;
|
|
2335
|
+
const rows = await orchestrate({ issues: [9, 1, 2], parallel: 1,
|
|
2336
|
+
readBody: async (n) => ({ 1: "**Depends on**: #9", 2: "**Depends on**: #1" })[n] ?? "",
|
|
2337
|
+
dispatchCheck: async (n) => (n === 9 && merged ? { outcome: "merged", pr: 90, detail: "PR #90 already merged" } : null),
|
|
2338
|
+
runOne: async (n) => { log.push(n); if (n === 9) { merged = true; return { terminalReason: "needs-human", detail: "ci.red" }; } return { terminalReason: "merged", pr: 100 + n }; } });
|
|
2339
|
+
assert.deepEqual(log, [9, 1, 2]);
|
|
2340
|
+
assert.deepEqual(rows.map((r) => [r.issue, r.outcome]), [[9, "merged"], [1, "merged"], [2, "merged"]]);
|
|
2341
|
+
assert.equal(rows[0].pr, 90);
|
|
2342
|
+
});
|
|
2343
|
+
|
|
2344
|
+
it("#4595 an out-of-set parked blocker that closes re-admits the skipped issue", async () => {
|
|
2345
|
+
const log = [];
|
|
2346
|
+
let closed = false;
|
|
2347
|
+
const rows = await orchestrate({ issues: [1], parallel: 1, readBody: async () => "",
|
|
2348
|
+
issueLabels: async () => ["needs-human"],
|
|
2349
|
+
dispatchCheck: async (n) => (n === 9 && closed ? { outcome: "invalid", detail: "issue closed" } : null),
|
|
2350
|
+
runOne: async (n) => { log.push(n); if (log.length === 1) { closed = true; return { terminalReason: "dependency-open", dependencies: [9] }; } return { terminalReason: "merged" }; } });
|
|
2351
|
+
assert.deepEqual(log, [1, 1]);
|
|
2352
|
+
assert.deepEqual(rows.map((r) => [r.issue, r.outcome]), [[1, "merged"]]);
|
|
2353
|
+
});
|
|
2354
|
+
|
|
2355
|
+
it("#4614 an out-of-set blocker that dispatchCheck reports merged but the lane still sees open re-admits once, then stays skipped", async () => {
|
|
2356
|
+
const log = [];
|
|
2357
|
+
let checks = 0;
|
|
2358
|
+
const rows = await orchestrate({ issues: [20], parallel: 1, readBody: async () => "",
|
|
2359
|
+
issueLabels: async () => ["needs-human"],
|
|
2360
|
+
dispatchCheck: async (n) => { checks++; return n === 10 ? { outcome: "merged", pr: 77, detail: "PR #77 already merged" } : null; },
|
|
2361
|
+
runOne: async (n) => { log.push(n); if (log.length > 5) throw new Error("re-admission loop"); return { terminalReason: "dependency-open", dependencies: [10] }; } });
|
|
2362
|
+
assert.deepEqual(log, [20, 20], "one re-admission, no loop");
|
|
2363
|
+
assert.ok(checks <= 3, `bounded dispatch checks (${checks})`);
|
|
2364
|
+
assert.deepEqual(rows.map((r) => [r.issue, r.outcome]), [[20, "skipped"]]);
|
|
2365
|
+
});
|
|
2366
|
+
|
|
2367
|
+
it("#4595 an unreadable or still-open blocker leaves the dependent skipped; no dispatchCheck changes nothing", async () => {
|
|
2368
|
+
for (const dispatchCheck of [async () => { throw new Error("gh down"); }, async () => null, undefined]) {
|
|
2369
|
+
const log = [];
|
|
2370
|
+
const rows = await orchestrate({ issues: [9, 1], parallel: 1, readBody: async (n) => (n === 1 ? "**Depends on**: #9" : ""), ...(dispatchCheck ? { dispatchCheck } : {}),
|
|
2371
|
+
runOne: async (n) => { log.push(n); return { terminalReason: "needs-human", detail: "ci.red" }; } });
|
|
2372
|
+
assert.deepEqual(log, [9]);
|
|
2373
|
+
assert.deepEqual(rows.map((r) => [r.issue, r.outcome]), [[9, "needs-human"], [1, "skipped"]]);
|
|
2374
|
+
}
|
|
2375
|
+
});
|
|
2376
|
+
|
|
2246
2377
|
it("#4124 a repeated dependency-open stops after the requeue cap", async () => {
|
|
2247
2378
|
const h = harness({}, { 1: "dependency-open" });
|
|
2248
2379
|
const clock = { t: 0 }, starts = [];
|
|
@@ -3724,6 +3855,34 @@ describe("lane resolution in core work-on (#4125)", () => {
|
|
|
3724
3855
|
});
|
|
3725
3856
|
});
|
|
3726
3857
|
|
|
3858
|
+
describe("shipped lane follows the open PR base (#4593)", () => {
|
|
3859
|
+
const pr = (base) => [{ number: 77, headRefOid: "aaaa1111aaaa1111", baseRefName: base }];
|
|
3860
|
+
const go = (o) => run(world({ milestone: "Old", branchesOnOrigin: ["staging", "milestone/old", "milestone/new"], remote: "abc\trefs/heads/feat/issue-7-lean", ...o }), { lane: undefined });
|
|
3861
|
+
it("proceeds on the PR base when the issue's lane is shipped", async () => {
|
|
3862
|
+
const w = world({ milestone: "Old", branchesOnOrigin: ["staging", "milestone/old", "milestone/new"], shipped: ["milestone/old"], prList: pr("milestone/new"), remote: "abc\trefs/heads/feat/issue-7-lean" });
|
|
3863
|
+
const r = await run(w, { lane: undefined });
|
|
3864
|
+
assert.notEqual(r.terminalReason, "needs-human");
|
|
3865
|
+
assert.equal(r.terminalReason, "merged");
|
|
3866
|
+
assert.ok(w.calls.some((c) => c[0] === "git" && c.includes("origin/milestone/new..refs/heads/feat/issue-7-lean")), "work is compared against the PR base");
|
|
3867
|
+
});
|
|
3868
|
+
it("unused lane at the staging tip is not shipped (#4623)", async () => {
|
|
3869
|
+
// ancestor of staging but no merge commit took it: the hand-retargeted PR must stop, not be followed
|
|
3870
|
+
const w = world({ milestone: "Old", branchesOnOrigin: ["staging", "milestone/old", "milestone/new"], unusedAncestors: ["milestone/old"], prList: pr("milestone/new"), remote: "abc\trefs/heads/feat/issue-7-lean" });
|
|
3871
|
+
const r = await run(w, { lane: undefined });
|
|
3872
|
+
assert.deepEqual([r.terminalReason, r.code], ["needs-human", "lane.mismatch"]);
|
|
3873
|
+
});
|
|
3874
|
+
it("stays needs-human when the lane is not shipped, the base is shipped, missing, or the default branch", async () => {
|
|
3875
|
+
for (const [o, code] of [
|
|
3876
|
+
[{ prList: pr("milestone/new") }, "lane.mismatch"],
|
|
3877
|
+
[{ shipped: ["milestone/old", "milestone/new"], prList: pr("milestone/new") }, "lane.mismatch"],
|
|
3878
|
+
[{ shipped: ["milestone/old"], prList: pr("milestone/gone") }, "lane.mismatch"],
|
|
3879
|
+
[{ shipped: ["milestone/old"], prList: pr("main") }, "lane.default_branch"]]) {
|
|
3880
|
+
const r = await go(o);
|
|
3881
|
+
assert.deepEqual([r.terminalReason, r.code], ["needs-human", code], JSON.stringify(o));
|
|
3882
|
+
}
|
|
3883
|
+
});
|
|
3884
|
+
});
|
|
3885
|
+
|
|
3727
3886
|
describe("lane entry points (#4125)", () => {
|
|
3728
3887
|
const quiet = async (fn) => { const l = console.log; console.log = () => {}; try { return await fn(); } finally { console.log = l; } };
|
|
3729
3888
|
const deps = () => ({ dir: tmpDir("core-cli-"), emitTelemetry: async () => {}, leanIo: {}, io: { gh: async (a) => (a.includes("defaultBranchRef") ? "main\n" : "o/r\n") },
|
|
@@ -3876,6 +4035,17 @@ describe("verify-by-CI (#4167): ci mode runs the heavy checks on CI and feeds fa
|
|
|
3876
4035
|
const ciValidation = [{ id: "V1", command: "node --test test/foo.test.js", ci: true }];
|
|
3877
4036
|
const events = (dir) => readLog(dir, 7).filter((e) => e.event === "CI_VERIFY");
|
|
3878
4037
|
|
|
4038
|
+
it("untrack commit before CI verify: a tool cache the builder committed does not move HEAD under the CI lookup (#4621)", async () => {
|
|
4039
|
+
const w = withCi(world({ validation: ciValidation })); w.added = ["src/foo.js", "apps/worker/.eslintcache"];
|
|
4040
|
+
const inner = w.io.git;
|
|
4041
|
+
w.io.git = async (a, o) => { const out = await inner(a, o); if (a[0] === "commit" && a.some((x) => /untrack lane tool state/.test(x))) w.head = `dddd${w.calls.length}`.padEnd(16, "0"); return out; };
|
|
4042
|
+
const dir = tmpDir("core-ci-");
|
|
4043
|
+
const r = await run(w, { verify: CI, dir });
|
|
4044
|
+
assert.equal(r.terminalReason, "merged");
|
|
4045
|
+
assert.deepEqual(events(dir).map((e) => e.result), ["pass"], "CI found the run for the pushed tip");
|
|
4046
|
+
assert.ok(!builders(w).some((b) => /CI (is )?unavailable|cannot run/i.test(String(b[4]))), "no ci_unavailable builder round");
|
|
4047
|
+
});
|
|
4048
|
+
|
|
3879
4049
|
it("green CI: the builder is told not to run test suites, the branch is pushed and verified before review, validate left to CI passes enforce", async () => {
|
|
3880
4050
|
const w = withCi(world({ validation: ciValidation }));
|
|
3881
4051
|
const dir = tmpDir("core-ci-");
|
|
@@ -3964,7 +4134,7 @@ describe("verify-by-CI (#4167): ci mode runs the heavy checks on CI and feeds fa
|
|
|
3964
4134
|
assert.equal(readVerify(d), undefined);
|
|
3965
4135
|
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 e2e_label: full-ci\n e2e_checks: \"e2e (1),e2e (2)\"\n");
|
|
3966
4136
|
const e2e = { mode: "auto", label: "full-ci", workflow: null, pathsInput: null, checks: ["e2e (1)", "e2e (2)"], paths: ["e2e/**", "**/e2e/**", "*.e2e.*", "*.e2e-spec.*", "cypress/**", "playwright/**"], blocking: "changed" };
|
|
3967
|
-
assert.deepEqual(readVerify(d), { mode: "ci", workflow: "verify.yml", pathsInput: null, skipPaths: ["*.md", "docs/**"], maxRounds: 3, precheckTimeoutSeconds: 60, precheckTotalSeconds: 600, checkRun: false, checks: null, postMerge: null, duplicateExports: true, e2e });
|
|
4137
|
+
assert.deepEqual(readVerify(d), { mode: "ci", workflow: "verify.yml", pathsInput: null, skipPaths: ["*.md", "docs/**"], maxRounds: 3, precheckTimeoutSeconds: 60, precheckTotalSeconds: 600, checkRun: false, checks: null, postMerge: null, flaky: [], duplicateExports: true, invariants: [], e2e });
|
|
3968
4138
|
});
|
|
3969
4139
|
|
|
3970
4140
|
describe("#4387 a no-commit CI fix round reruns the failed jobs once", () => {
|
|
@@ -4003,6 +4173,31 @@ describe("verify-by-CI (#4167): ci mode runs the heavy checks on CI and feeds fa
|
|
|
4003
4173
|
assert.deepEqual(w.reran, ["77"]); assert.equal(w.merges.length, 0);
|
|
4004
4174
|
assert.deepEqual(flakeEvents(dir).map((e) => e.result), ["fail"]);
|
|
4005
4175
|
});
|
|
4176
|
+
|
|
4177
|
+
/** #4586: count the builder CI-fix prompts of a world. */
|
|
4178
|
+
const fixPrompts = (w) => { const n = { n: 0 }; const inner = w.io.runner; w.io.runner = async (c) => { if (/CI failed on the pushed branch/.test(c.prompt)) n.n++; return inner(c); }; return n; };
|
|
4179
|
+
|
|
4180
|
+
it("#4586 a flake that goes green on the rerun spends no fix round and never resumes the builder", async () => {
|
|
4181
|
+
const w = flaky("success"), n = fixPrompts(w), dir = tmpDir("core-ci-");
|
|
4182
|
+
const r = await run(w, { verify: PUSH, dir });
|
|
4183
|
+
assert.equal(r.terminalReason, "merged"); assert.equal(n.n, 0); assert.deepEqual(w.reran, ["77"]);
|
|
4184
|
+
});
|
|
4185
|
+
|
|
4186
|
+
it("#4586 a failure that repeats on the same head after the rerun counts one round and reaches the builder", async () => {
|
|
4187
|
+
const w = flaky("failure"), n = fixPrompts(w), dir = tmpDir("core-ci-");
|
|
4188
|
+
const r = await run(w, { verify: PUSH, dir });
|
|
4189
|
+
assert.equal(r.code, "ci.red"); assert.ok(n.n >= 1);
|
|
4190
|
+
assert.equal(readLog(dir, 7).filter((e) => e.event === "CI_FIX_ROUND").length, 1, "one counted round");
|
|
4191
|
+
assert.deepEqual(w.reran, ["77"], "one rerun per head, none after the round");
|
|
4192
|
+
});
|
|
4193
|
+
|
|
4194
|
+
it("#4586 verify.flaky: a listed failing job is reported, never reruns, never feeds the builder, and the lane merges", async () => {
|
|
4195
|
+
const w = flaky("failure"), n = fixPrompts(w), dir = tmpDir("core-ci-");
|
|
4196
|
+
w.io.checkStates = async () => [{ name: "test", state: "success", url: RUN, head: w.head }];
|
|
4197
|
+
const r = await run(w, { verify: { ...PUSH, flaky: ["test"] }, dir });
|
|
4198
|
+
assert.equal(r.terminalReason, "merged"); assert.equal(n.n, 0); assert.deepEqual(w.reran, []);
|
|
4199
|
+
assert.deepEqual(readLog(dir, 7).filter((e) => e.event === "CI_FLAKY_REPORTED").map((e) => e.jobs), [["test"]]);
|
|
4200
|
+
});
|
|
4006
4201
|
});
|
|
4007
4202
|
});
|
|
4008
4203
|
|
|
@@ -4392,6 +4587,25 @@ describe("local-mode CI fix rounds at merge and declared pre-PR check commands (
|
|
|
4392
4587
|
assert.ok(log.findIndex((e) => e.event === "PRECHECK_COMMANDS") < log.findIndex((e) => e.event === "PR_OPENED"));
|
|
4393
4588
|
});
|
|
4394
4589
|
|
|
4590
|
+
it("#4585 verify.invariants run in the pre-PR round even with no learned commands, and the prompts carry the checklist", async () => {
|
|
4591
|
+
const w = world(); const ran = [];
|
|
4592
|
+
w.io.runCommand = async ({ command }) => { ran.push(command); return { rc: 0, output: "" }; };
|
|
4593
|
+
const r = await run(w, { verify: { invariants: ["bash scripts/inv.sh"] } });
|
|
4594
|
+
assert.equal(r.terminalReason, "merged");
|
|
4595
|
+
assert.deepEqual(ran, ["bash scripts/inv.sh"]);
|
|
4596
|
+
const b = builders(w);
|
|
4597
|
+
assert.match(b[0][4], /Repo-wide invariants[\s\S]*bash scripts\/inv\.sh/); assert.match(b[1][4], /Invariants checklist[\s\S]*bash scripts\/inv\.sh/);
|
|
4598
|
+
const w2 = world(); let n = 0; w2.io.runCommand = async () => { n++; return { rc: 0, output: "" }; };
|
|
4599
|
+
await run(w2); assert.equal(n, 0, "no invariants: nothing runs");
|
|
4600
|
+
});
|
|
4601
|
+
|
|
4602
|
+
it("#4585 a failing invariant resumes the builder like a failed check command", async () => {
|
|
4603
|
+
const w = world(); let n = 0;
|
|
4604
|
+
w.io.runCommand = async () => (n++ ? { rc: 0, output: "" } : { rc: 1, output: "registry missing table x" });
|
|
4605
|
+
await run(w, { verify: { invariants: ["bash scripts/inv.sh"] } });
|
|
4606
|
+
assert.ok(n >= 2, "the invariant is rerun after the builder fix round");
|
|
4607
|
+
});
|
|
4608
|
+
|
|
4395
4609
|
it("pre-PR: a passing set adds no builder turn; no commands configured runs nothing", async () => {
|
|
4396
4610
|
const w = world(); let n = 0;
|
|
4397
4611
|
w.io.runCommand = async () => { n++; return { rc: 0, output: "" }; };
|
|
@@ -4430,7 +4644,7 @@ describe("#4364 in-session forgedock check: plan hand-off and self-check record"
|
|
|
4430
4644
|
it("writes check-plan.json beside the contract and the implement prompt asks for forgedock check", async () => {
|
|
4431
4645
|
const w = world(); w.io.runCommand = async () => ({ rc: 0, output: "" });
|
|
4432
4646
|
assert.equal((await run(w, { precheck: { commands: [" make lint ", ""], maxMinutes: 5 }, verify: { mode: "hybrid" } })).terminalReason, "merged");
|
|
4433
|
-
assert.deepEqual(JSON.parse(w.files[`${WT}/.forge-lean/check-plan.json`]), { v: 1, base: "origin/staging", mode: "hybrid", learned: ["make lint"], maxMinutes: 5, precheckTotalSeconds: 600, precheckTimeoutSeconds: 300 });
|
|
4647
|
+
assert.deepEqual(JSON.parse(w.files[`${WT}/.forge-lean/check-plan.json`]), { v: 1, base: "origin/staging", mode: "hybrid", learned: ["make lint"], invariants: [], maxMinutes: 5, precheckTotalSeconds: 600, precheckTimeoutSeconds: 300 });
|
|
4434
4648
|
assert.match(builders(w)[1][4], /forgedock\.mjs check` in the worktree[\s\S]*report the problem as blocked/);
|
|
4435
4649
|
});
|
|
4436
4650
|
|
|
@@ -5072,6 +5286,17 @@ describe("enumerating tests + sequence numbers at hand-off (#4322)", () => {
|
|
|
5072
5286
|
assert.equal(w.dupArgs.cwd, WT);
|
|
5073
5287
|
});
|
|
5074
5288
|
|
|
5289
|
+
it("#4596 English copies in a locale catalog resume the builder once to translate; a leftover is an anomaly", async () => {
|
|
5290
|
+
const w = hworld(); const c = [[{ path: "messages/ta.json", key: "calendarView.day", value: "Day view", source: "messages/en.json" }], []]; w.cpCalls = 0;
|
|
5291
|
+
w.io.englishCopies = async (x) => { w.cpCalls++; w.cpArgs = x; return c.length > 1 ? c.shift() : c[0]; };
|
|
5292
|
+
assert.equal((await run(w)).terminalReason, "merged");
|
|
5293
|
+
const b = builders(w);
|
|
5294
|
+
assert.equal(b.length, 3);
|
|
5295
|
+
assert.match(b[2][4], /messages\/ta\.json: calendarView\.day = "Day view" \(same as messages\/en\.json\)[\s\S]*Translate each/);
|
|
5296
|
+
assert.ok(w.seq.indexOf("pr") > w.seq.lastIndexOf("builder-resume"), "fixed before the PR");
|
|
5297
|
+
assert.equal(w.cpArgs.cwd, WT);
|
|
5298
|
+
});
|
|
5299
|
+
|
|
5075
5300
|
it("#4406 a duplicate that remains after the round is an anomaly; verify.duplicate_exports false skips the check", async () => {
|
|
5076
5301
|
let w = hworld(); w.io.duplicateExports = async () => DUP;
|
|
5077
5302
|
assert.equal((await run(w)).terminalReason, "merged");
|
|
@@ -5082,6 +5307,23 @@ describe("enumerating tests + sequence numbers at hand-off (#4322)", () => {
|
|
|
5082
5307
|
assert.equal(builders(w).length, 2);
|
|
5083
5308
|
});
|
|
5084
5309
|
|
|
5310
|
+
const HAND = ["db/2026_x.sql: not produced by the migration generator (no journal entry added for it)"];
|
|
5311
|
+
it("#4591 a hand-named migration resumes the builder once with the generator rule; a remainder is an anomaly; clean costs no round", async () => {
|
|
5312
|
+
const w = hworld(); const d = [HAND, []];
|
|
5313
|
+
w.io.migrationGenerator = async (x) => { w.genArgs = x; return d.length > 1 ? d.shift() : d[0]; };
|
|
5314
|
+
assert.equal((await run(w)).terminalReason, "merged");
|
|
5315
|
+
const b = builders(w);
|
|
5316
|
+
assert.equal(b.length, 3);
|
|
5317
|
+
assert.match(b[2][4], /db\/2026_x\.sql[\s\S]*Never name or number migration files by hand/);
|
|
5318
|
+
assert.equal(w.genArgs.cwd, WT);
|
|
5319
|
+
const w2 = hworld(); w2.io.migrationGenerator = async () => HAND;
|
|
5320
|
+
assert.equal((await run(w2)).terminalReason, "merged");
|
|
5321
|
+
assert.match(traj(w2)[0], /Hand-named migration\(s\) remain \(hand-off\)/);
|
|
5322
|
+
const w3 = hworld(); w3.io.migrationGenerator = async () => [];
|
|
5323
|
+
await run(w3);
|
|
5324
|
+
assert.equal(builders(w3).length, 2);
|
|
5325
|
+
});
|
|
5326
|
+
|
|
5085
5327
|
it("no enumerating tests and no collisions: no new plan step, no extra builder call (prompts unchanged)", async () => {
|
|
5086
5328
|
const w = hworld();
|
|
5087
5329
|
assert.equal((await run(w)).terminalReason, "merged");
|
|
@@ -5184,6 +5426,31 @@ describe("enumerating tests + sequence numbers at hand-off (#4322)", () => {
|
|
|
5184
5426
|
assert.match(w.comments.find((c) => c.includes("<!-- FORGE:BASE_SYNC -->")), /deterministic: base side taken[\s\S]*renumbered deterministically[\s\S]*not required \(deterministic renumber \+ base merge\)/);
|
|
5185
5427
|
});
|
|
5186
5428
|
|
|
5429
|
+
it("#4588: base-sync conflicts the engine resolves (union / JSON map / generated) need no builder, re-review or sequence resolver", async () => {
|
|
5430
|
+
const w = dworld({ seq: [[], []], prView: [CONFLICTING, OKSTATE], mergeResults: ["conflict"], conflictFiles: ["src/index.ts", "i18n/status.json"] }), asked = [];
|
|
5431
|
+
w.io.conflictResolve = async (x) => { asked.push(x); w.head = "eeee0000eeee0000"; w.conflictFiles = []; w.mergeInProgress = false; return { resolved: x.files, remaining: [] }; };
|
|
5432
|
+
assert.equal((await run(w, { baseMerge: { unionGlobs: ["src/index.ts"], jsonGlobs: ["i18n/status.json"], generatedGlobs: [], generatedCommand: "" } })).terminalReason, "merged");
|
|
5433
|
+
assert.deepEqual(asked[0].files, ["src/index.ts", "i18n/status.json"]);
|
|
5434
|
+
assert.equal(asked[0].cfg.jsonGlobs[0], "i18n/status.json");
|
|
5435
|
+
assert.equal(w.resolveArgs, undefined, "sequence resolver not asked");
|
|
5436
|
+
assert.equal(builders(w).length, 2, "plan + implement only");
|
|
5437
|
+
assert.equal(reviewers(w).length, 1, "no re-review");
|
|
5438
|
+
assert.match(w.comments.find((c) => c.includes("<!-- FORGE:BASE_SYNC -->")), /deterministic: 2 union \/ JSON-map \/ generated file/);
|
|
5439
|
+
});
|
|
5440
|
+
|
|
5441
|
+
it("#4588: files the engine cannot resolve go to the sequence resolver, and the builder when that fails; without config nothing changes", async () => {
|
|
5442
|
+
const w = dworld({ prView: [CONFLICTING, OKSTATE], mergeResults: ["conflict"], conflictFiles: ["src/index.ts", "db/meta/_journal.json"], resolve: { unsupported: "x" } });
|
|
5443
|
+
w.io.conflictResolve = async () => ({ resolved: ["src/index.ts"], remaining: ["db/meta/_journal.json"] });
|
|
5444
|
+
assert.equal((await run(w, { baseMerge: { unionGlobs: ["src/index.ts"] } })).terminalReason, "merged");
|
|
5445
|
+
assert.deepEqual(w.resolveArgs.files, ["db/meta/_journal.json"]);
|
|
5446
|
+
assert.ok(builders(w).length >= 3, "the builder finishes the merge");
|
|
5447
|
+
const n = dworld({ prView: [CONFLICTING, OKSTATE], mergeResults: ["conflict"], conflictFiles: ["db/meta/_journal.json"] });
|
|
5448
|
+
let called = false; n.io.conflictResolve = async () => { called = true; return { resolved: [], remaining: [] }; };
|
|
5449
|
+
await run(n);
|
|
5450
|
+
assert.equal(called, false, "no baseMerge config: resolver not consulted");
|
|
5451
|
+
assert.deepEqual(n.resolveArgs.files, ["db/meta/_journal.json"]);
|
|
5452
|
+
});
|
|
5453
|
+
|
|
5187
5454
|
it("a schema-changing migration without lean.sequence.regenerate falls back to the builder renumber and the trajectory records why", async () => {
|
|
5188
5455
|
const w = dworld({ seq: [COLL, []], det: [{ unsupported: "schema-changing migration db/0007_usage.sql needs lean.sequence.regenerate (its snapshot cannot be cloned)" }] });
|
|
5189
5456
|
assert.equal((await run(w, { sequenceRegenerate: null })).terminalReason, "merged");
|
|
@@ -5679,6 +5946,17 @@ describe("e2e merge gate (#4403)", () => {
|
|
|
5679
5946
|
assert.equal(r2.terminalReason, "needs-human"); assert.equal(w2.merges.length, 0);
|
|
5680
5947
|
});
|
|
5681
5948
|
|
|
5949
|
+
it("#4624 verify.flaky: a flaky-listed own e2e failure is reported as an anomaly, never reaches the builder, and the lane merges; a non-listed one still does", async () => {
|
|
5950
|
+
const w = withE2e(world({ changed: SPECS }), ["failure"]);
|
|
5951
|
+
const fixes = []; const inner = w.io.runner; w.io.runner = async (c) => { if (/CI failed on the pushed branch/.test(c.prompt)) fixes.push(c.prompt); return inner(c); };
|
|
5952
|
+
const r = await go(w, { verify: { ...cfg(), flaky: ["e2e"] } });
|
|
5953
|
+
assert.equal(r.terminalReason, "merged"); assert.equal(fixes.length, 0);
|
|
5954
|
+
assert.ok(traj(w).some((t) => /Known-flaky e2e failure\(s\) reported, not blocking \(verify\.flaky\): e2e/.test(t)));
|
|
5955
|
+
const w2 = withE2e(world({ changed: SPECS }), ["failure"]);
|
|
5956
|
+
const r2 = await go(w2, { verify: { ...cfg(), flaky: ["other"], maxRounds: 0 } });
|
|
5957
|
+
assert.equal(r2.code, OUTCOME_CODES.CI_RED); assert.equal(w2.merges.length, 0);
|
|
5958
|
+
});
|
|
5959
|
+
|
|
5682
5960
|
it("verify.e2e: never (or no label/workflow configured) keeps today's behaviour: no label, no e2e wait, no event", async () => {
|
|
5683
5961
|
for (const verify of [cfg({ e2e: "never" }), parseVerify({})]) {
|
|
5684
5962
|
const w = withE2e(world({ changed: SPECS }), ["failure"]);
|
|
@@ -6015,7 +6293,7 @@ describe("#4395 lane profile", () => {
|
|
|
6015
6293
|
assert.match(p, /Issue independent read-only calls \(reads, greps, git log\/blame, gh\) in the same turn or one shell call\./);
|
|
6016
6294
|
assert.match(p, /Read each file's ranges once .* and keep notes; don't re-read it\./);
|
|
6017
6295
|
}
|
|
6018
|
-
assert.ok(approxTokens(builderImplementPrompt()) <=
|
|
6296
|
+
assert.ok(approxTokens(builderImplementPrompt()) <= 1550 && approxTokens(builderPlanPrompt(7)) <= 1800);
|
|
6019
6297
|
});
|
|
6020
6298
|
|
|
6021
6299
|
it("runCliBackendAsync reports the first stream turn's context as baselineTokens; makeLeanRunner drops it on resume", async () => {
|
|
@@ -6222,3 +6500,36 @@ describe("#4450 light lane: one builder session, derived contract, upgrade to th
|
|
|
6222
6500
|
assert.equal(events(dir, "LIGHT_DONE").length + events(dir, "LIGHT_UPGRADE").length, 0);
|
|
6223
6501
|
});
|
|
6224
6502
|
});
|
|
6503
|
+
|
|
6504
|
+
describe("tool state never rides the lane branch (#4590)", () => {
|
|
6505
|
+
const untracks = (w) => w.calls.filter((c) => c[0] === "git" && c[1] === "rm" && c[2] === "--cached");
|
|
6506
|
+
const subjects = (w) => w.calls.filter((c) => c[0] === "git" && c[1] === "commit" && c.some((x) => /untrack lane tool state/.test(x)));
|
|
6507
|
+
|
|
6508
|
+
it("a branch that added .forge-lean/ files and tool caches is untracked and committed before the PR push", async () => {
|
|
6509
|
+
const dir = tmpDir("toolstate-");
|
|
6510
|
+
const w = world({}); w.added = ["src/foo.js", ".forge-lean/result.json", "apps/worker/.eslintcache", "packages/a/tsconfig.tsbuildinfo"];
|
|
6511
|
+
assert.equal((await run(w, { dir })).terminalReason, "merged");
|
|
6512
|
+
const rm = untracks(w);
|
|
6513
|
+
assert.ok(rm.length >= 1, "git rm --cached ran");
|
|
6514
|
+
for (const f of [".forge-lean/result.json", "apps/worker/.eslintcache", "packages/a/tsconfig.tsbuildinfo"]) assert.ok(rm[0].includes(f), f);
|
|
6515
|
+
assert.ok(!rm[0].includes("src/foo.js"), "product files stay");
|
|
6516
|
+
assert.equal(subjects(w).length, 1, "one untrack commit; the next push finds the branch clean");
|
|
6517
|
+
const firstPush = w.calls.findIndex((c) => c[0] === "git" && c[1] === "push");
|
|
6518
|
+
assert.ok(w.calls.findIndex((c) => c === subjects(w)[0]) < firstPush, "committed before the push");
|
|
6519
|
+
assert.ok(readLog(dir, 7).some((e) => e.event === "TOOLSTATE_UNTRACKED"));
|
|
6520
|
+
});
|
|
6521
|
+
|
|
6522
|
+
it("a clean branch makes no untrack commit", async () => {
|
|
6523
|
+
const w = world({}); w.added = ["src/foo.js"];
|
|
6524
|
+
assert.equal((await run(w)).terminalReason, "merged");
|
|
6525
|
+
assert.equal(untracks(w).length, 0);
|
|
6526
|
+
assert.equal(subjects(w).length, 0);
|
|
6527
|
+
});
|
|
6528
|
+
});
|
|
6529
|
+
|
|
6530
|
+
describe("block rules: a check timeout is not a block (#4589)", () => {
|
|
6531
|
+
it("tells the builder rc 124 is host load, never a human block", async () => {
|
|
6532
|
+
const { blockCorrectionPrompt } = await import("./prompts.mjs");
|
|
6533
|
+
assert.match(blockCorrectionPrompt("x"), /times out \(rc 124\) is host load, never a block/);
|
|
6534
|
+
});
|
|
6535
|
+
});
|