forgedock 1.42.0 → 1.43.1
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 +1 -0
- package/bin/engine-cli.mjs +91 -19
- package/bin/journey.mjs +2 -2
- package/bin/lean/autochildren.mjs +103 -0
- package/bin/lean/autochildren.test.mjs +126 -0
- package/bin/lean/batch.mjs +11 -3
- package/bin/lean/batch.test.mjs +66 -2
- package/bin/lean/conformance.mjs +140 -9
- package/bin/lean/conformance.test.mjs +164 -2
- package/bin/lean/effects.mjs +1 -1
- package/bin/lean/execution.test.mjs +9 -6
- package/bin/lean/fixround.test.mjs +9 -1
- package/bin/lean/guardgate.mjs +66 -7
- package/bin/lean/guardgate.test.mjs +109 -3
- package/bin/lean/heavy.test.mjs +11 -1
- package/bin/lean/issuewatch.mjs +2 -2
- package/bin/lean/lean.mjs +80 -24
- package/bin/lean/lean.test.mjs +119 -7
- package/bin/lean/orchestrate-blocked.test.mjs +16 -0
- package/bin/lean/orchestrate-children.test.mjs +83 -0
- package/bin/lean/orchestrate.mjs +51 -12
- package/bin/lean/postmerge.mjs +5 -2
- package/bin/lean/postmerge.test.mjs +7 -0
- package/bin/lean/preflight.mjs +21 -0
- package/bin/lean/preflight.test.mjs +15 -1
- package/bin/lean/probe.mjs +1 -1
- package/bin/lean/prompts.mjs +19 -4
- package/bin/lean/schedd.mjs +116 -19
- package/bin/lean/schedd.test.mjs +201 -2
- package/bin/lean/selftest-live.mjs +1 -1
- package/bin/lean/selftest-nightly.mjs +1 -1
- package/bin/lean/signoff.mjs +11 -1
- package/bin/lean/signoff.test.mjs +24 -1
- package/bin/lean/toolstate.mjs +19 -3
- package/bin/lean/toolstate.test.mjs +22 -0
- package/bin/lean/verify.mjs +9 -1
- package/bin/lean/verify.test.mjs +4 -0
- package/bin/pool/agent.mjs +1 -1
- package/bin/runner.mjs +1 -1
- package/commands/orchestrate.md +2 -2
- package/docs/CONFIG.md +8 -2
- package/docs/OUTCOME-CODES.md +1 -0
- package/docs/RELEASE-NOTES.md +3 -0
- package/package.json +1 -1
- package/scripts/ci-local/run.mjs +1 -1
- package/scripts/file-deferred-followups.sh +1 -0
- package/scripts/file-deferred-followups.test.sh +7 -0
- package/scripts/review-pr-staging-scope.test.sh +3 -3
- package/scripts/review-prepare.sh +3 -0
- package/scripts/review-staging-pre.sh +88 -43
- package/scripts/review-staging-pre.test.sh +42 -1
package/bin/lean/effects.mjs
CHANGED
|
@@ -254,7 +254,7 @@ export function runCheckCommand({ command, cwd, timeoutMs = 300000, env = null }
|
|
|
254
254
|
return new Promise((done) => {
|
|
255
255
|
let out = "", timedOut = false, child;
|
|
256
256
|
const add = (b) => { out += String(b); if (out.length > 2 * CHECK_OUTPUT_CAP) out = out.slice(-CHECK_OUTPUT_CAP); };
|
|
257
|
-
try { child = spawn("bash", ["-c", String(command)], { cwd, detached: true, stdio: ["ignore", "pipe", "pipe"], ...(env ? { env: { ...process.env, ...env } } : {}) }); }
|
|
257
|
+
try { child = spawn("bash", ["-c", String(command)], { cwd, detached: true, windowsHide: true, stdio: ["ignore", "pipe", "pipe"], ...(env ? { env: { ...process.env, ...env } } : {}) }); }
|
|
258
258
|
catch (e) { done({ rc: 127, output: String(e?.message ?? e) }); return; }
|
|
259
259
|
const kill = () => { try { process.kill(-child.pid, "SIGKILL"); } catch { try { child.kill("SIGKILL"); } catch { /* gone */ } } };
|
|
260
260
|
const timer = setTimeout(() => { timedOut = true; kill(); }, timeoutMs);
|
|
@@ -528,15 +528,18 @@ describe("execution budget and sandbox cost (#4255)", () => {
|
|
|
528
528
|
const prov = fakeProvider({ finish: (n) => ({ terminalReason: "merged", pr: 100 + n, cost_usd: 2 }), usage: async (h) => ({ minutes: 5 * h.issue, cost_usd: 0.1 * h.issue }) });
|
|
529
529
|
const events = [];
|
|
530
530
|
const file = join(work, "day.json");
|
|
531
|
-
const
|
|
531
|
+
const printed = []; // #4917: #3 stays pending (budget hold), so the batch is not finalised: no report.json, the printed report carries the sandbox lines
|
|
532
|
+
const log0 = console.log; console.log = (l) => { printed.push(String(l)); };
|
|
533
|
+
const res = await (async () => { try { return await orchestrateFromCli(["1", "2", "3", "--repo", "o/r", "--no-prefilter"], base(work, { batchId: "bc1",
|
|
532
534
|
execution: parseExecution({ mode: "remote", max_remote: "1", budget_minutes: "15" }), sandboxProvider: prov,
|
|
533
535
|
sandboxLedger: (n) => makeLedger({ root: ledRoot, batch: "bc1", lane: `sandbox-${n}`, owner: "t", issue: n }),
|
|
534
|
-
executionBudget: openExecutionBudget({ budget: parseExecution({ budget_minutes: "15" }).budget, file }), emitTelemetry: (rec) => { events.push(rec); } })));
|
|
536
|
+
executionBudget: openExecutionBudget({ budget: parseExecution({ budget_minutes: "15" }).budget, file }), emitTelemetry: (rec) => { events.push(rec); } })); } finally { console.log = log0; } })();
|
|
535
537
|
assert.deepEqual(res.rows.map((r) => [r.issue, r.outcome, r.sandbox?.minutes ?? null]), [[1, "merged", 5], [2, "merged", 10], [3, "held", null]]);
|
|
536
|
-
|
|
537
|
-
assert.deepEqual(
|
|
538
|
-
|
|
539
|
-
assert.match(
|
|
538
|
+
assert.equal(openBatch(join(work, "batches"), "bc1").readReport(), null);
|
|
539
|
+
assert.deepEqual(res.rows[1].sandbox, { minutes: 10, cost_usd: 0.2, attempts: 1 });
|
|
540
|
+
const text = printed.join("\n");
|
|
541
|
+
assert.match(text, /sandbox: 2 lanes, 15\.0 min, \$0\.30 \(token cost \$4\.00\); budget: batch 15 min/);
|
|
542
|
+
assert.match(text, /sbx-min/);
|
|
540
543
|
assert.deepEqual(events.filter((e) => e.event === "SANDBOX_USAGE").map((e) => [e.issue, e.minutes, e.cost_usd, e.batch]), [[1, 5, 0.1, "bc1"], [2, 10, 0.2, "bc1"]]);
|
|
541
544
|
assert.equal(Object.keys(JSON.parse(readFileSync(file, "utf8")).days[new Date().toISOString().slice(0, 10)]).length, 2);
|
|
542
545
|
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { describe, it } from "node:test";
|
|
4
4
|
import assert from "node:assert/strict";
|
|
5
5
|
import { MECHANICAL_KINDS, FIX_PROMPT_MAX, citesFileLine, freshFix, issueSummary, capText, TRIGGERS, REWORK_TRIGGERS, firstPassOf, formatFirstPassSummary, sessionCostDelta } from "./fixround.mjs";
|
|
6
|
-
import { fixSessionPrompt } from "./prompts.mjs";
|
|
6
|
+
import { fixSessionPrompt, ciFailurePrompt } from "./prompts.mjs";
|
|
7
7
|
|
|
8
8
|
describe("#4363 fresh fix sessions: which rounds, and the prompt caps", () => {
|
|
9
9
|
it("every mechanical kind is fresh; block/contract/conflict kinds are not", () => {
|
|
@@ -115,3 +115,11 @@ describe("#4407 sessionCostDelta: per-attempt cost from a resumed session's runn
|
|
|
115
115
|
assert.equal(sessionCostDelta(last, { resume: "b", sessionId: "b", cost: 2 }).delta, 0.5);
|
|
116
116
|
});
|
|
117
117
|
});
|
|
118
|
+
|
|
119
|
+
describe("#4987 CI fix prompt: unrelated failures", () => {
|
|
120
|
+
it("tells the builder an out-of-diff failing check is a note, not an amendment", () => {
|
|
121
|
+
const p = ciFailurePrompt([{ job: "core-tests" }], "ci");
|
|
122
|
+
assert.match(p, /not an amendment/);
|
|
123
|
+
assert.match(p, /unknown amendment kinds are rejected/);
|
|
124
|
+
});
|
|
125
|
+
});
|
package/bin/lean/guardgate.mjs
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
* Guard gate (#4693): execution-based, no pattern matching on diff text. A lane whose diff changes lines a merged fix
|
|
5
5
|
* protected (git blame on the lane's merge base names the guard's merge commit, or a later commit that kept the lines), or
|
|
6
6
|
* edits/deletes a guard's test file, reruns that guard's test files *as they exist on base* against the lane's tree. A
|
|
7
|
-
* failure is `conformance.guard_broken` (builder resumed once). The only override is a `supersedes` amendment (conformance.mjs).
|
|
7
|
+
* failure is `conformance.guard_broken` (builder resumed once) unless the same base test also fails on the merge-base tree (#4968). The only override is a `supersedes` amendment (conformance.mjs).
|
|
8
8
|
* Deterministic, 0 LLM calls; a lane touching nothing guarded does one local index read and no git work.
|
|
9
9
|
*/
|
|
10
10
|
import * as nodeFs from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
11
12
|
import { dirname, extname, join } from "node:path";
|
|
12
13
|
import { OUTCOME_CODES } from "../engine/outcome-codes.mjs";
|
|
13
14
|
import { collectGuards, guardsIndexPath, readIndex, writeIndex } from "./guards.mjs";
|
|
@@ -94,9 +95,15 @@ export async function selectGuards({ guards, files, git, base }) {
|
|
|
94
95
|
return { selected, mergeBase };
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
/**
|
|
98
|
-
|
|
98
|
+
/**
|
|
99
|
+
* The command that runs one test file: the repo's `verify.guard_command` template for the extension (`{path}` substituted,
|
|
100
|
+
* or the path appended), else the contract's own validate command with that path substituted, else a per-extension
|
|
101
|
+
* default; null when unknown. The command runs from the worktree root with the repo-relative path.
|
|
102
|
+
*/
|
|
103
|
+
export function commandFor(path, contract, guardCommand = {}) {
|
|
99
104
|
const ext = extname(path);
|
|
105
|
+
const tpl = String(guardCommand?.[ext.toLowerCase()] ?? "").trim();
|
|
106
|
+
if (tpl) return tpl.includes("{path}") ? tpl.replaceAll("{path}", path) : `${tpl} ${path}`;
|
|
100
107
|
for (const v of contract?.validate ?? []) {
|
|
101
108
|
const cmd = String(v?.command ?? "");
|
|
102
109
|
if (/(?:^|\s)(?:-k|-t|--grep|--test-name-pattern|--testNamePattern)(?:\s|=)|::/.test(cmd)) continue;
|
|
@@ -112,22 +119,61 @@ export function commandFor(path, contract) {
|
|
|
112
119
|
/** Sibling path for the base copy of a test file, so its relative imports resolve against the lane's tree. */
|
|
113
120
|
export const basePathOf = (path) => { const e = extname(path); return `${path.slice(0, path.length - e.length)}__guardbase${e}`; };
|
|
114
121
|
|
|
122
|
+
/** Names of the failing tests in a runner's output: TAP/node:test `not ok` lines (SKIP/TODO excluded), the spec reporter's `✖ name (Nms)`, pytest `FAILED path::name`. null when the output names none. */
|
|
123
|
+
export function failingNames(output) {
|
|
124
|
+
const names = new Set();
|
|
125
|
+
for (const raw of String(output ?? "").split("\n")) {
|
|
126
|
+
const line = raw.replace(/\r$/, "");
|
|
127
|
+
let m = /^\s*not ok \d+ - (.+?)\s*$/.exec(line);
|
|
128
|
+
if (m && !/\s#\s*(?:SKIP|TODO)\b/i.test(m[1])) { names.add(m[1].replace(/\s+#\s.*$/, "").trim()); continue; }
|
|
129
|
+
m = /^\s*✖ (.+?) \([\d.]+m?s\)\s*$/.exec(line) ?? /^\s*FAILED (\S+?)(?: - .*)?\s*$/.exec(line);
|
|
130
|
+
if (m) names.add(m[1]);
|
|
131
|
+
}
|
|
132
|
+
return names.size ? [...names] : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
115
135
|
const tailOf = (out) => String(out ?? "").split("\n").filter((l) => l.trim()).slice(-40).join("\n").slice(-1500);
|
|
116
136
|
|
|
117
137
|
/**
|
|
118
138
|
* Rerun the selected guards' base test files against the lane tree, in a small pool, under one wall-time cap.
|
|
119
139
|
* Returns {failures, runs, skipped[], ms}. A test that cannot be run (no base copy, unknown runner, over the cap) is skipped, never a failure.
|
|
120
140
|
*/
|
|
121
|
-
export async function runGuards({ selected, mergeBase, contract, git, runCommand, cwd, env, capMs = GUARD_CAP_MS, parallelism = 2, fs = nodeFs, now = Date.now }) {
|
|
141
|
+
export async function runGuards({ selected, mergeBase, contract, guardCommand, git, runCommand, cwd, env, capMs = GUARD_CAP_MS, parallelism = 2, fs = nodeFs, now = Date.now }) {
|
|
122
142
|
const t0 = now(), skipped = [], runs = [], jobs = [];
|
|
123
143
|
for (const { guard } of selected) {
|
|
124
144
|
for (const t of guard.tests ?? []) {
|
|
125
|
-
const path = norm(t.path), copy = basePathOf(path), command = commandFor(copy, contract);
|
|
145
|
+
const path = norm(t.path), copy = basePathOf(path), command = commandFor(copy, contract, guardCommand);
|
|
126
146
|
if (!command) { skipped.push(`#${guard.issue} ${path}: no runner known for ${extname(path) || "this file"}`); continue; }
|
|
127
147
|
jobs.push({ guard, path, copy, command });
|
|
128
148
|
}
|
|
129
149
|
}
|
|
130
150
|
const made = [];
|
|
151
|
+
let base = null; // #4968: one detached worktree of the merge base, made on the first lane failure and shared by the pool
|
|
152
|
+
const baseTree = () => (base ??= (async () => {
|
|
153
|
+
let dir = null;
|
|
154
|
+
try {
|
|
155
|
+
dir = fs.mkdtempSync(join(tmpdir(), "guardbase-"));
|
|
156
|
+
await git(["worktree", "add", "--detach", dir, mergeBase]);
|
|
157
|
+
try { if (fs.existsSync(join(cwd, "node_modules"))) fs.symlinkSync(join(cwd, "node_modules"), join(dir, "node_modules")); } catch { /* best effort */ }
|
|
158
|
+
return { dir };
|
|
159
|
+
} catch (e) { return { dir, error: String(e?.message ?? e).split("\n")[0].slice(0, 80) }; }
|
|
160
|
+
})());
|
|
161
|
+
/** Decide a failed lane run against the same test on the base tree: the names the lane newly fails, [] when base fails it too, null when base passes (every failure is the lane's). */
|
|
162
|
+
const newFailures = async (j, laneOut) => {
|
|
163
|
+
const left = capMs - (now() - t0);
|
|
164
|
+
if (left <= 0) return { skip: `over the ${Math.round(capMs / 1000)}s guard cap` };
|
|
165
|
+
const t = await baseTree();
|
|
166
|
+
if (t.error) return { skip: `baseline unavailable (${t.error})` };
|
|
167
|
+
const bcmd = commandFor(j.path, contract, guardCommand); // the same runner as the lane run (verify.guard_command, #4792)
|
|
168
|
+
let b;
|
|
169
|
+
try { b = await runCommand({ command: bcmd, cwd: t.dir, timeoutMs: left, env }); } catch (e) { b = { rc: 127, output: String(e?.message ?? e) }; }
|
|
170
|
+
if (b?.rc === TIMEOUT_RC || b?.rc === 127) return { skip: `baseline unavailable (${b.rc === 127 ? "runner unavailable" : "over the guard cap"})` };
|
|
171
|
+
if (b?.rc === 0) return { fresh: null };
|
|
172
|
+
const lane = failingNames(laneOut), prior = failingNames(b?.output);
|
|
173
|
+
if (!lane || !prior) return { skip: "fails on base" };
|
|
174
|
+
const fresh = lane.filter((n) => !prior.includes(n));
|
|
175
|
+
return fresh.length ? { fresh } : { skip: "fails on base" };
|
|
176
|
+
};
|
|
131
177
|
try {
|
|
132
178
|
const queue = jobs.slice();
|
|
133
179
|
const work = async () => {
|
|
@@ -142,12 +188,25 @@ export async function runGuards({ selected, mergeBase, contract, git, runCommand
|
|
|
142
188
|
try { r = await runCommand({ command: j.command, cwd, timeoutMs: left, env }); } catch (e) { r = { rc: 127, output: String(e?.message ?? e) }; }
|
|
143
189
|
if (r?.rc === TIMEOUT_RC) { skipped.push(`#${j.guard.issue} ${j.path}: over the ${Math.round(capMs / 1000)}s guard cap`); continue; }
|
|
144
190
|
if (r?.rc === 127) { skipped.push(`#${j.guard.issue} ${j.path}: runner unavailable`); continue; }
|
|
145
|
-
|
|
191
|
+
const rc = Number.isInteger(r?.rc) ? r.rc : 1, output = String(r?.output ?? "");
|
|
192
|
+
let failed = rc === 0 ? null : failingNames(output);
|
|
193
|
+
if (rc !== 0) {
|
|
194
|
+
const v = await newFailures(j, output);
|
|
195
|
+
if (v.skip) { skipped.push(`guard #${j.guard.issue} ${j.path}: ${v.skip}`); continue; }
|
|
196
|
+
if (v.fresh) failed = v.fresh;
|
|
197
|
+
}
|
|
198
|
+
runs.push({ issue: j.guard.issue, why: j.guard.why, test: j.path, command: j.command, rc, output, failed });
|
|
146
199
|
}
|
|
147
200
|
};
|
|
148
201
|
await Promise.all(Array.from({ length: Math.max(1, Math.min(parallelism, jobs.length)) }, work));
|
|
149
202
|
} finally {
|
|
150
203
|
for (const p of made) { try { fs.rmSync(p, { force: true }); } catch { /* best effort */ } }
|
|
204
|
+
const t = base ? await base : null;
|
|
205
|
+
if (t?.dir) {
|
|
206
|
+
try { await git(["worktree", "remove", "--force", t.dir]); } catch { /* best effort */ }
|
|
207
|
+
try { fs.rmSync(t.dir, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
208
|
+
try { await git(["worktree", "prune"]); } catch { /* best effort */ }
|
|
209
|
+
}
|
|
151
210
|
}
|
|
152
211
|
return { failures: guardFailures(runs), runs, skipped, ms: now() - t0 };
|
|
153
212
|
}
|
|
@@ -159,7 +218,7 @@ const supersedesHint = (guard) => `If this change intentionally changes the guar
|
|
|
159
218
|
export function guardFailures(runs) {
|
|
160
219
|
return (runs ?? []).filter((r) => r.rc !== 0).map((r) => ({
|
|
161
220
|
code: OUTCOME_CODES.CONFORMANCE_GUARD_BROKEN, kind: "guard_broken", guard: `#${r.issue}`, test: r.test,
|
|
162
|
-
detail: `${supersedesHint(`#${r.issue}`)} guard #${r.issue} broken (${String(r.why ?? "").slice(0, 200)}): base test ${r.test} fails against your tree (\`${r.command}\`, exit ${r.rc}):\n${tailOf(r.output)}`,
|
|
221
|
+
detail: `${supersedesHint(`#${r.issue}`)} guard #${r.issue} broken (${String(r.why ?? "").slice(0, 200)}): base test ${r.test} fails against your tree (\`${r.command}\`, exit ${r.rc})${r.failed?.length ? `; failing tests: ${r.failed.slice(0, 20).join(" | ")}` : ""}:\n${tailOf(r.output)}`,
|
|
163
222
|
}));
|
|
164
223
|
}
|
|
165
224
|
|
|
@@ -5,7 +5,7 @@ import assert from "node:assert/strict";
|
|
|
5
5
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
6
6
|
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
-
import { oldRanges, selectGuards, runGuards, commandFor, basePathOf, guardFailures, supersessionComment, supersededIds, loadGuards } from "./guardgate.mjs";
|
|
8
|
+
import { oldRanges, selectGuards, runGuards, commandFor, basePathOf, failingNames, guardFailures, supersessionComment, supersededIds, loadGuards } from "./guardgate.mjs";
|
|
9
9
|
import { applyAmendments, precheck } from "./conformance.mjs";
|
|
10
10
|
import { builderImplementPrompt, conformanceResumePrompt } from "./prompts.mjs";
|
|
11
11
|
import { tmpDir } from "../tests/_tmp.mjs";
|
|
@@ -19,13 +19,13 @@ const put = (cwd, p, body) => { mkdirSync(join(cwd, p, ".."), { recursive: true
|
|
|
19
19
|
const TEST_SRC = (v) => `import test from "node:test";\nimport assert from "node:assert/strict";\nimport { price } from "./a.mjs";\ntest("price is ${v}", () => assert.equal(price(), ${v}));\n`;
|
|
20
20
|
|
|
21
21
|
/** A repo whose fix commit (merge_sha) made price() return 2, guarded by src/a.test.mjs; the lane starts from it. */
|
|
22
|
-
function fixture() {
|
|
22
|
+
function fixture(testSrc = TEST_SRC(2)) {
|
|
23
23
|
const d = tmpDir("guardgate-");
|
|
24
24
|
sh(d, "init", "-q", "-b", "main");
|
|
25
25
|
put(d, "src/a.mjs", "export function price() {\n return 1;\n}\n");
|
|
26
26
|
sh(d, "add", "-A"); sh(d, "commit", "-qm", "init");
|
|
27
27
|
put(d, "src/a.mjs", "export function price() {\n return 2;\n}\n");
|
|
28
|
-
put(d, "src/a.test.mjs",
|
|
28
|
+
put(d, "src/a.test.mjs", testSrc);
|
|
29
29
|
sh(d, "add", "-A"); sh(d, "commit", "-qm", "fix: price");
|
|
30
30
|
const sha = sh(d, "rev-parse", "HEAD").trim();
|
|
31
31
|
put(d, "src/other.mjs", "export const x = 1;\n");
|
|
@@ -127,6 +127,24 @@ test("a base test that cannot run in the cap, or has no known runner, is skipped
|
|
|
127
127
|
assert.equal(existsSync(join(d, basePathOf("src/a.test.mjs"))), false);
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
+
test("a verify.guard_command template wins over the contract and the fallbacks (#4792)", async () => {
|
|
131
|
+
const gc = { ".py": "scripts/pytest {path}" };
|
|
132
|
+
assert.equal(commandFor("tests/test_a__guardbase.py", {}, gc), "scripts/pytest tests/test_a__guardbase.py");
|
|
133
|
+
assert.equal(commandFor("tests/test_a__guardbase.py", { validate: [{ command: "pytest -q tests/test_a.py" }] }, gc), "scripts/pytest tests/test_a__guardbase.py");
|
|
134
|
+
assert.equal(commandFor("tests/test_a__guardbase.py", {}, { ".py": "uv run pytest -q" }), "uv run pytest -q tests/test_a__guardbase.py");
|
|
135
|
+
assert.equal(commandFor("src/a.test__guardbase.mjs", {}, gc), "node --test src/a.test__guardbase.mjs");
|
|
136
|
+
const { d, guard } = fixture();
|
|
137
|
+
put(d, "src/a.mjs", "export function price() {\n return 1;\n}\n");
|
|
138
|
+
commit(d, "revert");
|
|
139
|
+
const git = gitIn(d);
|
|
140
|
+
const { selected, mergeBase } = await selectGuards({ guards: [guard], files: files(d), git, base: "origin/main" });
|
|
141
|
+
const seen = [];
|
|
142
|
+
const r = await runGuards({ selected, mergeBase, contract: {}, guardCommand: { ".mjs": "run-it {path}" }, git, runCommand: async (a) => { seen.push(a); return { rc: 0, output: "" }; }, cwd: d, env: {} });
|
|
143
|
+
assert.equal(r.runs.length, 1);
|
|
144
|
+
assert.equal(seen[0].command, "run-it src/a.test__guardbase.mjs");
|
|
145
|
+
assert.equal(seen[0].cwd, d);
|
|
146
|
+
});
|
|
147
|
+
|
|
130
148
|
test("pytest-style guards run through the contract's own validate command with the path substituted", () => {
|
|
131
149
|
assert.equal(commandFor("tests/test_a__guardbase.py", { validate: [{ command: "pytest -q tests/test_a.py" }] }), "pytest -q tests/test_a__guardbase.py");
|
|
132
150
|
assert.equal(commandFor("tests/test_a__guardbase.py", {}), "python -m pytest -q tests/test_a__guardbase.py");
|
|
@@ -189,3 +207,91 @@ test("an additive enum change that updates its coverage test fails the guard wit
|
|
|
189
207
|
const withIt = precheck({ contract: a.contract, ...base });
|
|
190
208
|
assert.deepEqual(withIt.failures.filter((f) => f.kind === "guard_broken"), []);
|
|
191
209
|
});
|
|
210
|
+
|
|
211
|
+
// #4968: a guard test already red on the merge base must not block the lane
|
|
212
|
+
const FLAKY = (v) => `${TEST_SRC(v)}test("env dependent", () => assert.fail("red on base"));\n`;
|
|
213
|
+
const revert = (d) => { put(d, "src/a.mjs", "export function price() {\n return 1;\n}\n"); commit(d, "revert"); };
|
|
214
|
+
|
|
215
|
+
test("a guard test that already fails on base is skipped (fails on base), not guard_broken", async () => {
|
|
216
|
+
const { d, guard } = fixture(FLAKY(2));
|
|
217
|
+
put(d, "src/a.mjs", "export function price() {\n return 1 + 1;\n}\n"); // harmless edit of the guarded line
|
|
218
|
+
commit(d, "refactor");
|
|
219
|
+
const { selected, r } = await gate(d, guard);
|
|
220
|
+
assert.equal(selected.length, 1);
|
|
221
|
+
assert.equal(r.failures.length, 0, JSON.stringify(r));
|
|
222
|
+
assert.ok(r.skipped.some((x) => /guard #7 .*fails on base/.test(x)), r.skipped.join("|"));
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("a test that passes on base and fails on the lane is blocked, with no skip", async () => {
|
|
226
|
+
const { d, guard } = fixture();
|
|
227
|
+
revert(d);
|
|
228
|
+
const { r } = await gate(d, guard);
|
|
229
|
+
assert.equal(r.failures.length, 1);
|
|
230
|
+
assert.deepEqual(r.skipped, []);
|
|
231
|
+
assert.match(r.failures[0].detail, /failing tests: price is 2:/);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("a new failure in a file that already had one is blocked and its name leads the detail", async () => {
|
|
235
|
+
const { d, guard } = fixture(FLAKY(2));
|
|
236
|
+
revert(d);
|
|
237
|
+
const { r } = await gate(d, guard);
|
|
238
|
+
assert.equal(r.failures.length, 1, JSON.stringify({ skipped: r.skipped }));
|
|
239
|
+
const detail = r.failures[0].detail;
|
|
240
|
+
assert.match(detail, /failing tests: price is 2:\n/, "only the new failure is named");
|
|
241
|
+
assert.ok(detail.indexOf("failing tests:") < detail.indexOf("env dependent"), "names precede the output tail (any reporter)");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("a passing guard adds no baseline worktree, and a failing one removes its worktree", async () => {
|
|
245
|
+
const pass = fixture();
|
|
246
|
+
const calls = [];
|
|
247
|
+
const spy = (cwd) => async (args) => { calls.push(args[0]); return sh(cwd, ...args); };
|
|
248
|
+
put(pass.d, "src/a.mjs", "export function price() {\n return 1 + 1;\n}\n"); commit(pass.d, "refactor");
|
|
249
|
+
let git = spy(pass.d);
|
|
250
|
+
let { selected, mergeBase } = await selectGuards({ guards: [pass.guard], files: files(pass.d), git, base: "origin/main" });
|
|
251
|
+
calls.length = 0;
|
|
252
|
+
const ok = await runGuards({ selected, mergeBase, contract: {}, git, runCommand, cwd: pass.d, env: process.env });
|
|
253
|
+
assert.equal(ok.failures.length, 0);
|
|
254
|
+
assert.ok(!calls.includes("worktree"), calls.join(","));
|
|
255
|
+
const bad = fixture();
|
|
256
|
+
revert(bad.d);
|
|
257
|
+
git = spy(bad.d);
|
|
258
|
+
({ selected, mergeBase } = await selectGuards({ guards: [bad.guard], files: files(bad.d), git, base: "origin/main" }));
|
|
259
|
+
calls.length = 0;
|
|
260
|
+
const r = await runGuards({ selected, mergeBase, contract: {}, git, runCommand, cwd: bad.d, env: process.env });
|
|
261
|
+
assert.equal(r.failures.length, 1);
|
|
262
|
+
assert.ok(calls.includes("worktree"));
|
|
263
|
+
assert.equal(sh(bad.d, "worktree", "list").trim().split("\n").length, 1, "baseline worktree removed");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("the baseline rerun uses the verify.guard_command runner, so a real break still blocks", async () => {
|
|
267
|
+
const { d, guard } = fixture();
|
|
268
|
+
revert(d);
|
|
269
|
+
const git = gitIn(d);
|
|
270
|
+
const { selected, mergeBase } = await selectGuards({ guards: [guard], files: files(d), git, base: "origin/main" });
|
|
271
|
+
const seen = [];
|
|
272
|
+
// run-it only exists through guard_command: the lane copy fails, the base copy passes
|
|
273
|
+
const r = await runGuards({ selected, mergeBase, contract: {}, guardCommand: { ".mjs": "run-it {path}" }, git, runCommand: async (a) => {
|
|
274
|
+
seen.push(a);
|
|
275
|
+
return a.cwd === d ? { rc: 1, output: "not ok 1 - price is 2\n" } : { rc: 0, output: "ok 1 - price is 2\n" };
|
|
276
|
+
}, cwd: d, env: {} });
|
|
277
|
+
assert.deepEqual(seen.map((s) => s.command), ["run-it src/a.test__guardbase.mjs", "run-it src/a.test.mjs"]);
|
|
278
|
+
assert.notEqual(seen[1].cwd, d, "baseline runs in the merge-base worktree");
|
|
279
|
+
assert.equal(r.failures.length, 1, JSON.stringify(r.skipped));
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("a baseline that cannot run is skipped, never blocking", async () => {
|
|
283
|
+
const { d, guard } = fixture();
|
|
284
|
+
revert(d);
|
|
285
|
+
const git = async (a) => { if (a[0] === "worktree" && a[1] === "add") throw new Error("no worktree"); return sh(d, ...a); };
|
|
286
|
+
const { selected, mergeBase } = await selectGuards({ guards: [guard], files: files(d), git, base: "origin/main" });
|
|
287
|
+
const r = await runGuards({ selected, mergeBase, contract: {}, git, runCommand, cwd: d, env: process.env });
|
|
288
|
+
assert.equal(r.failures.length, 0);
|
|
289
|
+
assert.match(r.skipped[0], /baseline unavailable \(no worktree\)/);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("failingNames reads TAP, spec and pytest output and ignores skips", () => {
|
|
293
|
+
assert.deepEqual(failingNames("ok 1 - a\nnot ok 2 - b c\n not ok 1 - nested # SKIP why\nnot ok 3 - d # TODO x"), ["b c"]);
|
|
294
|
+
assert.deepEqual(failingNames(" ✖ spec one (1.2ms)\n✖ failing tests:\n ✖ spec one (1.2ms)"), ["spec one"]);
|
|
295
|
+
assert.deepEqual(failingNames("FAILED tests/t.py::test_x - assert 1 == 2"), ["tests/t.py::test_x"]);
|
|
296
|
+
assert.equal(failingNames("all fine"), null);
|
|
297
|
+
});
|
package/bin/lean/heavy.test.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { tmpDir } from "../tests/_tmp.mjs";
|
|
|
9
9
|
import { guardChain, isPassThroughWrapper, SHIM_LOOP_MAX, classifyStep, heavyConfig, openHeavy, acquireHeavy, writeHeavyShims, heavyShimScript, sumWaits, runWrapped, HEAVY_BINS, isHeavyProcess } from "./heavy.mjs";
|
|
10
10
|
import { checkAdmission, ADMISSION_DEFAULTS, localLaneCap } from "./admission.mjs";
|
|
11
11
|
import { formatAlertLines, formatWaitSummary } from "./orchestrate.mjs";
|
|
12
|
-
import { readHeavy, liveLaneCap } from "../engine-cli.mjs";
|
|
12
|
+
import { readHeavy, readAdmission, liveLaneCap } from "../engine-cli.mjs";
|
|
13
13
|
|
|
14
14
|
// Injected pids, clock and host: `live` is the set of pids the registry sees as alive; `mem` the MemAvailable in MB.
|
|
15
15
|
const world = ({ mem = 12000, cores = 4, swapFree = 0 } = {}) => {
|
|
@@ -336,4 +336,14 @@ describe("validation steps and the local lane cap (#4777)", () => {
|
|
|
336
336
|
assert.equal(liveLaneCap(1, { cfg: {}, host: { cores: () => 8, memAvailableMb: () => 64000 } })(), 1);
|
|
337
337
|
assert.equal(liveLaneCap(3, { cfg: { enabled: false }, host: { cores: () => 8, memAvailableMb: () => 64000 } })(), 3);
|
|
338
338
|
});
|
|
339
|
+
|
|
340
|
+
it("batch cap follows the repo forge.yaml lane_cpu_reservation / lane_mem_mb, not the process cwd (#4828)", () => {
|
|
341
|
+
const d = tmpDir("lane-cap-");
|
|
342
|
+
writeFileSync(join(d, "forge.yaml"), "engine:\n lane_cpu_reservation: 4\n lane_mem_mb: 4096\n");
|
|
343
|
+
const host = { cores: () => 16, memAvailableMb: () => 2048 + 3 * 4096 + 10 };
|
|
344
|
+
assert.equal(liveLaneCap(null, { cfg: readAdmission(d), host })(), 3, "memory binds at 4096 MB per lane");
|
|
345
|
+
assert.equal(liveLaneCap(null, { cfg: {}, host })(), 6, "defaults: 2048 MB per lane");
|
|
346
|
+
writeFileSync(join(d, "forge.yaml"), "engine:\n lane_cpu_reservation: 8\n");
|
|
347
|
+
assert.equal(liveLaneCap(null, { cfg: readAdmission(d), host: { cores: () => 16, memAvailableMb: () => 64000 } })(), 2);
|
|
348
|
+
});
|
|
339
349
|
});
|
package/bin/lean/issuewatch.mjs
CHANGED
|
@@ -366,7 +366,7 @@ function realIo(repo, env = process.env) {
|
|
|
366
366
|
},
|
|
367
367
|
launch: (n, lane) => {
|
|
368
368
|
let res = null;
|
|
369
|
-
const p = spawn(process.execPath, [CLI, "run-issue", String(n), "--lane", lane], { detached: true, stdio: "ignore", cwd: process.cwd(), env });
|
|
369
|
+
const p = spawn(process.execPath, [CLI, "run-issue", String(n), "--lane", lane], { detached: true, windowsHide: true, stdio: "ignore", cwd: process.cwd(), env });
|
|
370
370
|
p.on("close", (code) => { res = { code: code ?? 1 }; }); p.on("error", () => { res = { code: 1 }; });
|
|
371
371
|
return { poll: () => res };
|
|
372
372
|
},
|
|
@@ -375,7 +375,7 @@ function realIo(repo, env = process.env) {
|
|
|
375
375
|
reported: (id) => existsSync(join(root, id, "report.json")),
|
|
376
376
|
start: async (issues, parallel, lane) => {
|
|
377
377
|
const before = new Set(listBatches(root));
|
|
378
|
-
spawn(process.execPath, [CLI, "orchestrate", ...issues.map(String), "--parallel", String(parallel), "--lane", lane, "--repo", repo], { detached: true, stdio: "ignore", cwd: process.cwd(), env }).unref();
|
|
378
|
+
spawn(process.execPath, [CLI, "orchestrate", ...issues.map(String), "--parallel", String(parallel), "--lane", lane, "--repo", repo], { detached: true, windowsHide: true, stdio: "ignore", cwd: process.cwd(), env }).unref();
|
|
379
379
|
for (let i = 0; i < 60; i++) { const id = listBatches(root).find((b) => !before.has(b)); if (id) return id; await new Promise((r) => setTimeout(r, 1000)); }
|
|
380
380
|
throw new Error("orchestrate did not create a batch within 60s");
|
|
381
381
|
},
|