forgedock 1.30.0 → 1.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/engine/outcome-codes.mjs +3 -2
- package/bin/engine/runlog.mjs +82 -12
- package/bin/engine/telemetry-seam.mjs +11 -4
- package/bin/engine/telemetry-seam.test.mjs +18 -0
- package/bin/engine-cli.mjs +409 -38
- package/bin/engine.mjs +47 -37
- package/bin/forgedock.mjs +35 -3
- package/bin/gc.mjs +50 -17
- package/bin/hooks/reread-nudge.mjs +65 -0
- package/bin/labels.json +5 -0
- package/bin/lean/apishape.mjs +170 -0
- package/bin/lean/apishape.test.mjs +187 -0
- package/bin/lean/batch.mjs +32 -0
- package/bin/lean/batch.test.mjs +338 -2
- package/bin/lean/budget.mjs +89 -0
- package/bin/lean/budget.test.mjs +106 -0
- package/bin/lean/checks.mjs +32 -9
- package/bin/lean/checks.test.mjs +78 -1
- package/bin/lean/conformance.e2e.test.mjs +27 -5
- package/bin/lean/conformance.mjs +150 -15
- package/bin/lean/conformance.test.mjs +175 -3
- package/bin/lean/context.mjs +128 -44
- package/bin/lean/context.test.mjs +157 -5
- package/bin/lean/decompose.mjs +90 -0
- package/bin/lean/decompose.test.mjs +105 -0
- package/bin/lean/depcache.mjs +11 -7
- package/bin/lean/depcache.test.mjs +81 -0
- package/bin/lean/e2e.mjs +102 -0
- package/bin/lean/e2e.test.mjs +86 -0
- package/bin/lean/effects.mjs +48 -15
- package/bin/lean/explore.mjs +47 -0
- package/bin/lean/explore.test.mjs +61 -0
- package/bin/lean/facts.mjs +84 -0
- package/bin/lean/facts.test.mjs +66 -0
- package/bin/lean/fixround.mjs +90 -0
- package/bin/lean/fixround.test.mjs +117 -0
- package/bin/lean/heavy.mjs +35 -2
- package/bin/lean/heavy.test.mjs +32 -2
- package/bin/lean/lean.mjs +712 -134
- package/bin/lean/lean.test.mjs +1626 -37
- package/bin/lean/ledger.mjs +5 -3
- package/bin/lean/ledger.test.mjs +18 -0
- package/bin/lean/light.mjs +135 -0
- package/bin/lean/light.test.mjs +173 -0
- package/bin/lean/lightlane.mjs +219 -0
- package/bin/lean/lightlane.test.mjs +237 -0
- package/bin/lean/limits.test.mjs +36 -1
- package/bin/lean/orchestrate.mjs +303 -41
- package/bin/lean/pitfalls.mjs +136 -0
- package/bin/lean/pitfalls.test.mjs +117 -0
- package/bin/lean/postmerge.mjs +85 -0
- package/bin/lean/postmerge.test.mjs +79 -0
- package/bin/lean/precheck.mjs +138 -13
- package/bin/lean/precheck.test.mjs +118 -1
- package/bin/lean/prefilter.mjs +220 -5
- package/bin/lean/prefilter.test.mjs +260 -4
- package/bin/lean/preflight.mjs +108 -3
- package/bin/lean/preflight.test.mjs +48 -1
- package/bin/lean/probe.mjs +297 -0
- package/bin/lean/probe.test.mjs +210 -0
- package/bin/lean/prompts.mjs +104 -13
- package/bin/lean/render.mjs +7 -3
- package/bin/lean/resume.mjs +67 -0
- package/bin/lean/resume.test.mjs +84 -0
- package/bin/lean/review-trigger.mjs +12 -0
- package/bin/lean/review-trigger.test.mjs +17 -1
- package/bin/lean/routing.mjs +74 -0
- package/bin/lean/routing.test.mjs +67 -0
- package/bin/lean/sched.mjs +16 -1
- package/bin/lean/sched.test.mjs +175 -1
- package/bin/lean/schedd.mjs +55 -9
- package/bin/lean/schedd.test.mjs +170 -3
- package/bin/lean/selfreview.mjs +37 -0
- package/bin/lean/selfreview.test.mjs +51 -0
- package/bin/lean/seqlease.mjs +40 -0
- package/bin/lean/seqlease.test.mjs +101 -0
- package/bin/lean/sequence.mjs +296 -13
- package/bin/lean/sequence.test.mjs +263 -2
- package/bin/lean/toolchain.mjs +60 -15
- package/bin/lean/toolchain.test.mjs +120 -3
- package/bin/lean/toolstats.mjs +72 -0
- package/bin/lean/toolstats.test.mjs +47 -0
- package/bin/lean/verifier.mjs +232 -0
- package/bin/lean/verifier.test.mjs +163 -0
- package/bin/lean/verify.mjs +72 -10
- package/bin/lean/verify.test.mjs +68 -3
- package/bin/review-staging.mjs +533 -0
- package/bin/runner.mjs +82 -13
- package/bin/stats/aggregate.mjs +87 -2
- package/bin/stats/efficiency.mjs +335 -0
- package/bin/stats/efficiency.test.mjs +266 -0
- package/bin/stats/github-source.mjs +8 -3
- package/bin/stats/index.mjs +91 -6
- package/bin/stats/local-source.mjs +145 -8
- package/bin/stats/query.mjs +181 -3
- package/bin/stats/render.mjs +22 -0
- package/bin/stats/stats.test.mjs +403 -8
- package/commands/orchestrate.md +3 -1
- package/commands/review-pr-agents/protocols.md +1 -1
- package/commands/review-pr-staging.md +84 -831
- package/commands/review-pr.md +1 -1
- package/commands/work-on/build/implement.md +1 -0
- package/commands/work-on/build/validate.md +11 -1
- package/docs/CONFIG.md +80 -5
- package/docs/OUTCOME-CODES.md +2 -1
- package/docs/STATS.md +66 -1
- package/docs/TELEMETRY.md +1 -1
- package/docs/eval/corpus/lightlane-v1.json +55 -0
- package/docs/eval/lightlane-bench.md +91 -0
- package/docs/spec/review-protocol.md +1 -1
- package/package.json +1 -1
- package/scripts/bench-lightlane.mjs +286 -0
- package/scripts/ci-local/lib.mjs +20 -0
- package/scripts/ci-local/plan.mjs +2 -12
- package/scripts/ci-local/scoping.mjs +10 -1
- package/scripts/ci-local.test.sh +2 -0
- package/scripts/fixtures/apishape/other/go-change/base/x.go +3 -0
- package/scripts/fixtures/apishape/other/go-change/head/x.go +3 -0
- package/scripts/fixtures/apishape/python/generator-function/base/seq.py +3 -0
- package/scripts/fixtures/apishape/python/generator-function/head/seq.py +2 -0
- package/scripts/fixtures/apishape/python/ichunked/base/more.py +17 -0
- package/scripts/fixtures/apishape/python/ichunked/head/more.py +20 -0
- package/scripts/fixtures/apishape/python/private-edit/base/calc.py +6 -0
- package/scripts/fixtures/apishape/python/private-edit/head/calc.py +9 -0
- package/scripts/fixtures/apishape/python/removed/base/ops.py +6 -0
- package/scripts/fixtures/apishape/python/removed/head/ops.py +2 -0
- package/scripts/fixtures/apishape/python/renamed/base/text.py +2 -0
- package/scripts/fixtures/apishape/python/renamed/head/text.py +2 -0
- package/scripts/fixtures/apishape/python/signature/base/shapes.py +2 -0
- package/scripts/fixtures/apishape/python/signature/head/shapes.py +2 -0
- package/scripts/fixtures/apishape/python/sync-async/base/client.py +7 -0
- package/scripts/fixtures/apishape/python/sync-async/head/client.py +7 -0
- package/scripts/fixtures/apishape/ts/generator-function/base/seq.js +3 -0
- package/scripts/fixtures/apishape/ts/generator-function/head/seq.js +5 -0
- package/scripts/fixtures/apishape/ts/private-edit/base/calc.ts +7 -0
- package/scripts/fixtures/apishape/ts/private-edit/head/calc.ts +9 -0
- package/scripts/fixtures/apishape/ts/removed/base/ops.ts +7 -0
- package/scripts/fixtures/apishape/ts/removed/head/ops.ts +3 -0
- package/scripts/fixtures/apishape/ts/renamed/base/text.ts +3 -0
- package/scripts/fixtures/apishape/ts/renamed/head/text.ts +3 -0
- package/scripts/fixtures/apishape/ts/signature/base/shapes.ts +3 -0
- package/scripts/fixtures/apishape/ts/signature/head/shapes.ts +3 -0
- package/scripts/fixtures/apishape/ts/sync-async/base/client.mjs +5 -0
- package/scripts/fixtures/apishape/ts/sync-async/head/client.mjs +5 -0
- package/scripts/forge-root.test.sh +2 -2
- package/scripts/publish-staging-sync.test.sh +62 -4
- package/scripts/review-finalize.sh +17 -6
- package/scripts/review-finalize.test.sh +47 -0
- package/scripts/review-pr-arguments.test.sh +13 -1
- package/scripts/review-pr-delta-wiring.test.sh +2 -2
- package/scripts/review-pr-gate2.test.sh +6 -14
- package/scripts/review-pr-staging-finding-filter.test.sh +4 -4
- package/scripts/review-pr-staging-scope.test.sh +39 -33
- package/scripts/review-prepare.sh +73 -6
- package/scripts/review-prepare.test.sh +85 -0
- package/scripts/review-staging-post.sh +293 -0
- package/scripts/review-staging-post.test.sh +143 -0
- package/scripts/review-staging-pre.sh +401 -0
- package/scripts/review-staging-pre.test.sh +150 -0
- package/scripts/symbol-index.mjs +26 -7
- package/scripts/symbol-index.py +55 -9
- package/scripts/trusted-comments.test.sh +1 -1
package/bin/lean/lean.test.mjs
CHANGED
|
@@ -4,16 +4,21 @@ import { describe, it } from "node:test";
|
|
|
4
4
|
import { laneBackendId } from "./limits.mjs";
|
|
5
5
|
import assert from "node:assert/strict";
|
|
6
6
|
import { tmpDir } from "../tests/_tmp.mjs";
|
|
7
|
-
import { writeFileSync, existsSync, readFileSync, mkdirSync, symlinkSync } from "node:fs";
|
|
7
|
+
import { writeFileSync, existsSync, readFileSync, mkdirSync, symlinkSync, readdirSync } from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
-
import { runCore, contractHash, isProtectedLaneBranch } from "./lean.mjs";
|
|
9
|
+
import { runCore, contractHash, isProtectedLaneBranch, classifyBlock } from "./lean.mjs";
|
|
10
|
+
import { parseFactsComment, normaliseFacts, renderFactsRecord, FACTS_MARKER } from "./facts.mjs";
|
|
10
11
|
import { makeLedger } from "./ledger.mjs";
|
|
12
|
+
import { parseVerify } from "./verify.mjs";
|
|
13
|
+
import { worktreeFingerprint } from "./precheck.mjs";
|
|
11
14
|
import { renderInvestigation, renderPlan, renderImplementation, renderReview, renderPrBody, renderTrajectory, renderStructure, renderContract, contractJson, parseContractComment, sanitizeMd, capComment, section, cell, NR, MAX_COMMENT_CHARS } from "./render.mjs";
|
|
12
15
|
import { orchestrate, parseDeps, formatTable, pickNext } from "./orchestrate.mjs";
|
|
13
|
-
import { REVIEWER_SYSTEM, approxTokens, builderPlanPrompt, builderImplementPrompt, reviewerPrompt, hotspotPlanNote, hotspotBuildRule } from "./prompts.mjs";
|
|
16
|
+
import { REVIEWER_SYSTEM, REVIEW_RUBRIC, approxTokens, builderPlanPrompt, builderImplementPrompt, reviewerPrompt, hotspotPlanNote, hotspotBuildRule } from "./prompts.mjs";
|
|
14
17
|
import { parseReview, unnamedEdges, structureNote, applyAmendments, validateContract, scopeText, DECISION_SOURCES } from "./conformance.mjs";
|
|
15
18
|
import { mapRecord } from "../engine/telemetry-seam.mjs";
|
|
16
|
-
import { readLog } from "../engine/runlog.mjs";
|
|
19
|
+
import { readLog as readRepoLog } from "../engine/runlog.mjs";
|
|
20
|
+
// #4432: the run log is keyed by repo + issue; every lane in this file runs in repo o/r.
|
|
21
|
+
const readLog = (dir, issue, repo = "o/r") => readRepoLog(dir, issue, { repo });
|
|
17
22
|
import { makeRunTmp, wrapLaneLimits, systemdRunAvailable, makeLeanRunner, requiredChecksFor, procsInDir, makeLeanIo } from "./effects.mjs";
|
|
18
23
|
import { retrying, PUSH_TRANSIENT_RE } from "./retry.mjs";
|
|
19
24
|
import { checkAdmission, hostParallelism } from "./admission.mjs";
|
|
@@ -59,7 +64,7 @@ function world(o = {}) {
|
|
|
59
64
|
contract: o.contract ?? CONTRACT, issueComments: o.issueComments, issueStates: o.issueStates,
|
|
60
65
|
changed: o.changed ?? ["src/foo.js", "test/foo.test.js"], reviews: [...(o.reviews ?? [])], waits: [...(o.waits ?? [])], clock: 0,
|
|
61
66
|
result: o.result ?? { status: "fixed", summary: "fixed foo", evidence: [], tests: ["test/foo.test.js"] },
|
|
62
|
-
prList: o.prList ?? [], remote: o.remote ?? "", resumeResult: o.resumeResult, seq: [], mtimes: {}, envs: [], pushes: 0, pushErrors: [...(o.pushErrors ?? [])], commentErrors: [...(o.commentErrors ?? [])], dirty: o.dirty ?? false, lastSubject: o.lastSubject ?? "", prView: [...(o.prView ?? [])], mergeResults: [...(o.mergeResults ?? [])], mergeErrors: [...(o.mergeErrors ?? [])], conflictFiles: [], mergeInProgress: false, markers: false, mergeAttempts: 0, branchDeletes: [],
|
|
67
|
+
prList: o.prList ?? [], remote: o.remote ?? "", resumeResult: o.resumeResult, seq: [], mtimes: {}, envs: [], pushes: 0, pushErrors: [...(o.pushErrors ?? [])], commentErrors: [...(o.commentErrors ?? [])], dirty: o.dirty ?? false, lastSubject: o.lastSubject ?? "", prView: [...(o.prView ?? [])], mergeResults: [...(o.mergeResults ?? [])], mergeErrors: [...(o.mergeErrors ?? [])], conflictFiles: [], baseHeads: [...(o.baseHeads ?? ["ba5e000000000000"])], mergeInProgress: false, markers: false, mergeAttempts: 0, branchDeletes: [],
|
|
63
68
|
stages: { ...STAGE_MD, ...(o.stages ?? {}) }, builderN: 0,
|
|
64
69
|
salvageTrees: [...(o.salvageTrees ?? [])], alive: new Set((o.procs ?? []).map((p) => p.pid)), kills: [],
|
|
65
70
|
};
|
|
@@ -75,8 +80,8 @@ function world(o = {}) {
|
|
|
75
80
|
if (k === "pr list") return JSON.stringify(w.prList);
|
|
76
81
|
if (k === "repo view") return `${o.defaultBranch ?? "main"}\n`;
|
|
77
82
|
if (k === "issue view" && a.includes("milestone")) { w.seq.push("lane"); return JSON.stringify({ milestone: o.milestone ? { title: o.milestone } : null }); }
|
|
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 } : {}) });
|
|
83
|
+
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: o.title ?? "foo breaks", body: b, state: "OPEN", comments: o.viewComments ?? [] }); }
|
|
84
|
+
if (k === "issue view") return JSON.stringify({ number: Number(a[2]), title: a[2] === "7" && o.title ? o.title : "foo breaks", body: a[2] === "7" && o.body ? o.body : ISSUE_BODY, state: w.issueStates?.[a[2]] ?? "OPEN", comments: [], ...(o.labels && a[2] === "7" ? { labels: o.labels } : {}) });
|
|
80
85
|
if (k === "pr edit") { w.prEdits.push(a); return ""; }
|
|
81
86
|
if (k === "label create") { w.labelCreates = [...(w.labelCreates ?? []), a]; return ""; }
|
|
82
87
|
if (k === "issue comment" && w.commentErrors.length) throw new Error(w.commentErrors.shift());
|
|
@@ -115,6 +120,7 @@ function world(o = {}) {
|
|
|
115
120
|
if (a[0] === "diff" && a[1] === "--check") return "";
|
|
116
121
|
if (a[0] === "grep") return w.markers ? "src/foo.js:3:<<<<<<< HEAD" : "";
|
|
117
122
|
if (a[0] === "merge" && a[1] === "--abort") { w.mergeInProgress = false; return ""; }
|
|
123
|
+
if (a[0] === "rev-parse" && a.length === 2 && /^origin\//.test(String(a[1]))) return w.baseHeads.length > 1 ? w.baseHeads.shift() : w.baseHeads[0]; // #4386: the base head moves only when a test says so
|
|
118
124
|
if (a[0] === "rev-parse") return w.head;
|
|
119
125
|
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");
|
|
120
126
|
if (a[0] === "diff" && a[1] === "--name-only") return w.changed.join("\n");
|
|
@@ -132,10 +138,12 @@ function world(o = {}) {
|
|
|
132
138
|
w.seq.push(c.role + (c.resume ? "-resume" : ""));
|
|
133
139
|
w.envs.push(c.env ?? null);
|
|
134
140
|
if (c.role === "builder") {
|
|
135
|
-
const
|
|
141
|
+
const cost = c.resume ? (w.sessCost = Math.round(((w.sessCost ?? 0) + 0.1) * 1e9) / 1e9) : (w.sessCost = 0.1); // #4407: a resumed CLI session reports its running total
|
|
142
|
+
const fix = Boolean(c.resume) || /no earlier session/.test(c.prompt); // #4363: a fresh fix session acts like the old resume
|
|
143
|
+
const r = fix ? (w.resumeResult ?? w.result) : w.result;
|
|
136
144
|
const kind = /PLANNING ONLY/.test(c.prompt) ? "plan" : /contract.json is not valid/.test(c.prompt) ? "contract-fix" : "work";
|
|
137
145
|
const n = ++w.builderN;
|
|
138
|
-
if (o.builder) { await o.builder(w, put, kind, n, c); return { text: "done", sessionId: "sess-1", usage: { input_tokens: 10, output_tokens: 5, cache_read_input_tokens: 100 }, costUsd:
|
|
146
|
+
if (o.builder) { await o.builder(w, put, kind, n, c); return { text: "done", sessionId: "sess-1", usage: { input_tokens: 10, output_tokens: 5, cache_read_input_tokens: 100 }, costUsd: cost, durationMs: 1000, turns: 3, model: "default" }; }
|
|
139
147
|
if (kind === "plan") {
|
|
140
148
|
if (o.live) await o.live(w, put);
|
|
141
149
|
else if (!o.noStages) writeStages(["investigation"]);
|
|
@@ -146,14 +154,14 @@ function world(o = {}) {
|
|
|
146
154
|
} else {
|
|
147
155
|
if (!(`${WT}/.forge-lean/contract.json` in w.files) && w.contract && r?.status !== "blocked") put("contract.json", JSON.stringify(w.contract));
|
|
148
156
|
if (!o.noStages && !w.implWritten && !o.live) { writeStages(["implementation"]); w.implWritten = true; }
|
|
149
|
-
if (
|
|
157
|
+
if (fix && n > 2 && /does not conform/.test(c.prompt) && o.resumeChanged) w.changed = o.resumeChanged;
|
|
150
158
|
if (/Merging origin\//.test(c.prompt) && o.syncChanged) w.changed = o.syncChanged;
|
|
151
159
|
if (r?.status === "blocked" || ["invalid", "already-fixed"].includes(r?.status)) put("result.json", JSON.stringify(r));
|
|
152
160
|
else if (r) put("result.json", JSON.stringify({ ...r, requirements: o.claims ?? w.contract.requirements.filter((x) => x.disposition === "deliver").map((x) => ({ id: x.id, status: "implemented", evidence: "src/foo.js:3" })),
|
|
153
|
-
validation: (
|
|
161
|
+
validation: (fix && n > 2 ? o.resumeValidation : null) ?? o.validation ?? (w.contract.validate ?? []).map((v) => ({ id: v.id, command: v.command, exit: 0 })), amendments: (fix && n > 2 ? o.resumeAmendments : null) ?? o.amendments ?? [] }));
|
|
154
162
|
}
|
|
155
|
-
if (
|
|
156
|
-
return { text: "done", sessionId: "sess-1", usage: { input_tokens: 10, output_tokens: 5, cache_read_input_tokens: 100 }, costUsd:
|
|
163
|
+
if ((fix && !o.freshNoCommit) || /Merging origin\//.test(c.prompt)) { w.head = `bbbb${w.calls.length}`.padEnd(16, "0"); if (!o.leaveConflicts) { w.conflictFiles = []; w.mergeInProgress = false; } }
|
|
164
|
+
return { text: "done", sessionId: "sess-1", usage: { input_tokens: 10, output_tokens: 5, cache_read_input_tokens: 100 }, costUsd: cost, durationMs: 1000, turns: 3, model: "default" };
|
|
157
165
|
}
|
|
158
166
|
return { text: w.reviews.length ? w.reviews.shift() : ok(), sessionId: "rev", usage: { input_tokens: 4, output_tokens: 2 }, costUsd: 0.05, durationMs: 500, turns: 2, model: c.model };
|
|
159
167
|
},
|
|
@@ -179,7 +187,7 @@ describe("core work-on", () => {
|
|
|
179
187
|
const r = await run(w, { dir });
|
|
180
188
|
assert.equal(r.terminalReason, "merged");
|
|
181
189
|
assert.equal(r.pr, 77);
|
|
182
|
-
assert.deepEqual(w.comments.map(markerOf), ["FORGE:STARTED", "FORGE:INVESTIGATION", "FORGE:CONTRACT", "FORGE:IMPLEMENTATION", "FORGE:REVIEW", "FORGE:TRAJECTORY"]);
|
|
190
|
+
assert.deepEqual(w.comments.map(markerOf), ["FORGE:STARTED", "FORGE:INVESTIGATION", "FORGE:CONTRACT", "FORGE:IMPLEMENTATION", "FORGE:REVIEW", "FORGE:FACTS", "FORGE:TRAJECTORY"]);
|
|
183
191
|
assert.match(w.comments[1], /<!-- FORGE:VALIDATE -->/, "VALIDATE stays as an alias marker");
|
|
184
192
|
assert.match(w.comments[1], /^<!-- FORGE:INVESTIGATION -->\n<!-- FORGE:VALIDATE -->\n## Investigation Report\n\*\*Verdict\*\*: CONFIRMED/);
|
|
185
193
|
assert.match(w.comments[2], /^<!-- FORGE:CONTRACT -->\n<!-- FORGE:PLAN -->\n## Builder Contract \(frozen `[0-9a-f]{12}`\)/, "PLAN stays as an alias marker");
|
|
@@ -282,7 +290,7 @@ describe("core work-on", () => {
|
|
|
282
290
|
assert.equal(r.terminalReason, "merged");
|
|
283
291
|
const b = builders(w);
|
|
284
292
|
assert.equal(b.length, 3);
|
|
285
|
-
assert.equal(b[2][2], "
|
|
293
|
+
assert.equal(b[2][2], null, "#4363: a FAIL citing file:line runs in a fresh session");
|
|
286
294
|
assert.match(b[2][4], /conformance\.unimplemented: R1 partial: src\/foo.js:9 still wrong/);
|
|
287
295
|
assert.equal(reviewers(w).length, 2);
|
|
288
296
|
assert.equal(w.prs.length, 1);
|
|
@@ -428,7 +436,7 @@ describe("core work-on", () => {
|
|
|
428
436
|
|
|
429
437
|
it("#4124 split-required files sub-issues with Parent / Depends-on lines and ends decomposed", async () => {
|
|
430
438
|
const w = depWorld({ changed: [], result: { status: "blocked", summary: "epic", block: { kind: "split-required", subissues: [
|
|
431
|
-
{ title: "part A", body: "do A" }, { title: "part B", body: "do B", depends_on: [0] }] } } }, {});
|
|
439
|
+
{ title: "part A", body: "do A", paths: ["src/a"] }, { title: "part B", body: "do B", paths: ["src/b"], depends_on: [0] }] } } }, {});
|
|
432
440
|
const r = await run(w);
|
|
433
441
|
assert.equal(r.terminalReason, "decomposed");
|
|
434
442
|
assert.deepEqual(r.subissues, [201, 202]);
|
|
@@ -436,11 +444,70 @@ describe("core work-on", () => {
|
|
|
436
444
|
assert.match(bodyB, /^\*\*Parent\*\*: #7$/m);
|
|
437
445
|
assert.match(bodyB, /^\*\*Depends on\*\*: #201$/m);
|
|
438
446
|
assert.ok(w.labels.includes("decomposed"));
|
|
447
|
+
assert.equal(r.decomposition.independent, true);
|
|
448
|
+
assert.ok(w.comments.some((c) => /FORGE:DECOMPOSED/.test(c) && /Rationale: independent: yes; size: no estimate vs budget 25 files \/ 1500 lines -> oversize no/.test(c)));
|
|
439
449
|
noNeedsHuman(w);
|
|
440
450
|
});
|
|
441
451
|
|
|
452
|
+
it("#4396 a split of overlapping parts within the size budget is rejected once toward one checklist lane; repeated, it is needs-human", async () => {
|
|
453
|
+
const over = { status: "blocked", summary: "epic", block: { kind: "split-required", size: { files: 6, lines: 300 }, subissues: [
|
|
454
|
+
{ title: "part A", body: "do A", paths: ["src/alerts"] }, { title: "part B", body: "do B", paths: ["src/alerts/routing.ts"] }] } };
|
|
455
|
+
const w = depWorld({ changed: [], result: over }, {});
|
|
456
|
+
const r = await run(w);
|
|
457
|
+
assert.equal(r.terminalReason, "needs-human");
|
|
458
|
+
assert.equal(r.code, "builder.block_untyped");
|
|
459
|
+
assert.equal(w.created.length, 0, "no sub-issue is filed");
|
|
460
|
+
const resumed = builders(w).filter((c) => c[2]);
|
|
461
|
+
assert.equal(resumed.length, 1);
|
|
462
|
+
assert.match(resumed[0][4], /split is not justified/);
|
|
463
|
+
assert.match(resumed[0][4], /ordered checklist/);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it("#4396 an oversize split of overlapping parts is admitted and records the size rationale", async () => {
|
|
467
|
+
const big = { status: "blocked", summary: "epic", block: { kind: "split-required", size: { files: 40, lines: 4000 }, subissues: [
|
|
468
|
+
{ title: "part A", body: "do A", paths: ["src/alerts"] }, { title: "part B", body: "do B", paths: ["src/alerts/x.ts"] }] } };
|
|
469
|
+
const w = depWorld({ changed: [], result: big }, {});
|
|
470
|
+
const r = await run(w);
|
|
471
|
+
assert.equal(r.terminalReason, "decomposed");
|
|
472
|
+
assert.deepEqual({ independent: r.decomposition.independent, oversize: r.decomposition.oversize }, { independent: false, oversize: true });
|
|
473
|
+
assert.ok(w.comments.some((c) => /FORGE:DECOMPOSED/.test(c) && /size: 40 files, 4000 lines .* oversize yes/.test(c)));
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it("#4385 split siblings that declare a shared hub file are serialised (chained, no duplicate transitive edge); disjoint or undeclared files stay parallel", () => {
|
|
477
|
+
const hub = ["src/domain/index.ts", "./src/domain/events.ts"];
|
|
478
|
+
const BIG = { files: 40, lines: 2000 }; // #4396: shared or undeclared files are admitted only over the size budget
|
|
479
|
+
const b = classifyBlock({ block: { kind: "split-required", size: BIG, subissues: [
|
|
480
|
+
{ title: "state machine", body: "a", files: [...hub, "src/domain/state.ts"] },
|
|
481
|
+
{ title: "next-date", body: "b", files: [...hub, "src/domain/next.ts"] },
|
|
482
|
+
{ title: "timeline", body: "c", files: ["/src/domain/index.ts", "src/domain/events.ts", "src/domain/timeline.ts"] }] } }, 7);
|
|
483
|
+
assert.equal(b.kind, "split");
|
|
484
|
+
assert.deepEqual(b.subissues.map((x) => x.dependsOn), [[], [0], [1]]);
|
|
485
|
+
assert.deepEqual(b.serialized.map((e) => [e.from, e.to, e.files]), [[1, 0, ["src/domain/index.ts", "src/domain/events.ts"]], [2, 1, ["src/domain/index.ts", "src/domain/events.ts"]]]);
|
|
486
|
+
const disjoint = classifyBlock({ block: { kind: "split-required", subissues: [
|
|
487
|
+
{ title: "a", body: "a", files: ["src/a.ts"] }, { title: "b", body: "b", files: ["src/b.ts"] }] } }, 7);
|
|
488
|
+
assert.deepEqual(disjoint.subissues.map((x) => x.dependsOn), [[], []]);
|
|
489
|
+
assert.deepEqual(disjoint.serialized, []);
|
|
490
|
+
const legacy = classifyBlock({ block: { kind: "split-required", size: BIG, subissues: [
|
|
491
|
+
{ title: "a", body: "a" }, { title: "b", body: "b" }, { title: "c", body: "c", depends_on: [0] }] } }, 7);
|
|
492
|
+
assert.deepEqual(legacy.subissues.map((x) => x.dependsOn), [[], [], [0]]);
|
|
493
|
+
assert.ok(legacy.subissues.every((x) => !("files" in x)));
|
|
494
|
+
const declared = classifyBlock({ block: { kind: "split-required", size: BIG, subissues: [
|
|
495
|
+
{ title: "a", body: "a", files: ["x.ts"] }, { title: "b", body: "b", files: ["y.ts"], depends_on: [0] }, { title: "c", body: "c", files: ["x.ts"], depends_on: [1] }] } }, 7);
|
|
496
|
+
assert.deepEqual(declared.subissues.map((x) => x.dependsOn), [[], [0], [1]]);
|
|
497
|
+
assert.deepEqual(declared.serialized, []);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it("#4385 a serialised sibling is filed with the engine-added Depends-on line", async () => {
|
|
501
|
+
const w = depWorld({ changed: [], result: { status: "blocked", summary: "epic", block: { kind: "split-required", size: { files: 40, lines: 2000 }, subissues: [
|
|
502
|
+
{ title: "part A", body: "do A", files: ["src/domain/index.ts"] }, { title: "part B", body: "do B", files: ["src/domain/index.ts", "src/b.ts"] }] } } }, {});
|
|
503
|
+
const r = await run(w);
|
|
504
|
+
assert.equal(r.terminalReason, "decomposed");
|
|
505
|
+
const bodyB = w.created[1][w.created[1].indexOf("--body") + 1];
|
|
506
|
+
assert.match(bodyB, /^\*\*Depends on\*\*: #201$/m);
|
|
507
|
+
});
|
|
508
|
+
|
|
442
509
|
it("#4153 a split of a milestone issue files every sub-issue with the parent's milestone; no milestone passes none", async () => {
|
|
443
|
-
const split = { status: "blocked", summary: "epic", block: { kind: "split-required", subissues: [{ title: "part A", body: "do A" }, { title: "part B", body: "do B", depends_on: [0] }] } };
|
|
510
|
+
const split = { status: "blocked", summary: "epic", block: { kind: "split-required", subissues: [{ title: "part A", body: "do A", paths: ["src/a"] }, { title: "part B", body: "do B", paths: ["src/b"], depends_on: [0] }] } };
|
|
444
511
|
const w = depWorld({ changed: [], result: split, milestone: "Remote MCP Server", branchesOnOrigin: ["staging", "milestone/remote-mcp-server"] }, {});
|
|
445
512
|
const r = await run(w, { lane: null });
|
|
446
513
|
assert.equal(r.terminalReason, "decomposed");
|
|
@@ -462,7 +529,7 @@ describe("core work-on", () => {
|
|
|
462
529
|
noNeedsHuman(w);
|
|
463
530
|
});
|
|
464
531
|
|
|
465
|
-
const splitBlock = { status: "blocked", summary: "epic", block: { kind: "split-required", subissues: [{ title: "part A", body: "do A" }, { title: "part B", body: "do B", depends_on: [0] }] } };
|
|
532
|
+
const splitBlock = { status: "blocked", summary: "epic", block: { kind: "split-required", subissues: [{ title: "part A", body: "do A", paths: ["src/a"] }, { title: "part B", body: "do B", paths: ["src/b"], depends_on: [0] }] } };
|
|
466
533
|
it("#4337 without decomposition.close_parent a split keeps the parent open (opt-in, default false)", async () => {
|
|
467
534
|
const w = depWorld({ changed: [], result: splitBlock }, {});
|
|
468
535
|
assert.equal((await run(w)).terminalReason, "decomposed");
|
|
@@ -859,7 +926,7 @@ describe("in-batch recovery of mechanical needs-human (#4267)", () => {
|
|
|
859
926
|
};
|
|
860
927
|
const quiet = {
|
|
861
928
|
invalid: () => world({ changed: [], result: { status: "invalid", summary: "not a bug", evidence: ["src/foo.js:3 already guards null"], tests: [] } }),
|
|
862
|
-
decomposed: () => withDeps(world({ changed: [], result: { status: "blocked", summary: "epic", block: { kind: "split-required", subissues: [{ title: "part A", body: "do A" }] } } }), {}),
|
|
929
|
+
decomposed: () => withDeps(world({ changed: [], result: { status: "blocked", summary: "epic", block: { kind: "split-required", subissues: [{ title: "part A", body: "do A", paths: ["src/a"] }] } } }), {}),
|
|
863
930
|
"dependency-open": () => withDeps(world({ changed: [], result: { status: "blocked", summary: "needs #3", block: { kind: "dependency", issues: ["#3"] } } }), { 3: { state: "OPEN", labels: [] } }),
|
|
864
931
|
"engine-error": () => failBuilder(world(), new Error("boom")),
|
|
865
932
|
deferred: () => failBuilder(world(), limitErr),
|
|
@@ -1168,6 +1235,84 @@ describe("conformance shadow mode (the default)", () => {
|
|
|
1168
1235
|
assert.equal((await run(w)).code, "contract.invalid"); assert.equal(builders(w).length, 2, "plan + one correction");
|
|
1169
1236
|
assert.match(builders(w)[1][4], /R1: quote is not verbatim[\s\S]*V1: needs command 'jq'/);
|
|
1170
1237
|
});
|
|
1238
|
+
it("#4393 replaying the AlterLab#36619 contract flags V4 at preflight, before the builder starts", async () => {
|
|
1239
|
+
const V4 = "uv run --no-project --python 3.12 python -m shared.detection.corpus.tools.check_manifest";
|
|
1240
|
+
const c = { ...CONTRACT, validate: [{ id: "V1", command: "node --test test/foo.test.js" }, { id: "V2", command: "python3 -m pytest -q tests/test_corpus.py" }, { id: "V3", command: "uv run pytest -q tests/test_manifest.py" }, { id: "V4", command: V4 }] };
|
|
1241
|
+
const fixed = { ...c, validate: [...c.validate.slice(0, 3), { id: "V4", command: "uv run python -m shared.detection.corpus.tools.check_manifest" }] };
|
|
1242
|
+
const dir = tmpDir("preflight-run-");
|
|
1243
|
+
const w = world({ contract: c, fixedContract: fixed, validation: fixed.validate.map((v) => ({ id: v.id, command: v.command, exit: 0 })) });
|
|
1244
|
+
const ran = [];
|
|
1245
|
+
w.io.probeCommand = async () => ({ rc: 0, output: "" }); // uv, python3 and pytest are on the host
|
|
1246
|
+
w.io.preflightRun = async (a) => { ran.push([a.command, a.cwd, w.seq.length]); return a.command === V4 ? { rc: 1, output: "Traceback (most recent call last):\nModuleNotFoundError: No module named 'httpx'\n" } : { rc: 1, output: "AssertionError: expected RED on base" }; };
|
|
1247
|
+
assert.equal((await run(w, { dir })).terminalReason, "merged");
|
|
1248
|
+
const b = builders(w);
|
|
1249
|
+
assert.equal(b.length, 3, "plan + one contract correction + implement");
|
|
1250
|
+
assert.match(b[1][4], /V4: running it on the base worktree stopped before any test ran: needs Python module 'httpx'[\s\S]*repo's own toolchain/);
|
|
1251
|
+
assert.doesNotMatch(b[1][4], /V[123]:/, "RED on base is not a miss");
|
|
1252
|
+
assert.match(b[2][4], /The engine froze/, "the correction lands before the implement builder");
|
|
1253
|
+
assert.ok(ran.every(([, cwd]) => cwd === WT));
|
|
1254
|
+
const firstImpl = w.seq.indexOf("builder-resume", w.seq.indexOf("builder-resume") + 1);
|
|
1255
|
+
assert.ok(ran.every(([, , at]) => at < firstImpl), "every run happens before the implement session");
|
|
1256
|
+
const pf = readLog(dir, 7).filter((e) => e.event === "VALIDATE_PREFLIGHT");
|
|
1257
|
+
assert.deepEqual(pf.map((e) => [e.corrected, e.found.map((m) => [m.ids, m.tool, m.kind, m.source])]), [[true, [[["V4"], "httpx", "python-module", "run"]]]]);
|
|
1258
|
+
// a timed-out run is no evidence; an unset $VAR is a miss without running the command
|
|
1259
|
+
const t = world({ contract: { ...CONTRACT, validate: [{ id: "V1", command: "node --test test/foo.test.js" }, { id: "V2", command: 'node scripts/x.mjs "$NO_SUCH_VAR_4393"' }] } });
|
|
1260
|
+
const tRan = []; t.io.preflightRun = async (a) => { tRan.push(a.command); return { rc: 124, output: "ModuleNotFoundError: No module named 'httpx'\n(timed out after 30s)" }; };
|
|
1261
|
+
assert.equal((await run(t)).terminalReason, "merged");
|
|
1262
|
+
assert.deepEqual(tRan, ["node --test test/foo.test.js", "node --test test/foo.test.js"], "V2 skipped (env miss), V1 run again after the correction");
|
|
1263
|
+
assert.match(builders(t)[1][4], /V2: uses \$NO_SUCH_VAR_4393, which is unset or empty/);
|
|
1264
|
+
});
|
|
1265
|
+
it("#4393 files the preflight runs create or modify are reverted; paths dirty before are not", async () => {
|
|
1266
|
+
const w = world({ dirty: true });
|
|
1267
|
+
let st = " M src/foo.js\n";
|
|
1268
|
+
const git0 = w.io.git, reverts = [];
|
|
1269
|
+
w.io.git = async (a, o) => { if (a[0] === "status" && a[2] === "--untracked-files=all") return st; if (a[0] === "clean" || (a[0] === "checkout" && a[1] === "--")) { reverts.push(a.join(" ")); return ""; } return git0(a, o); };
|
|
1270
|
+
w.io.preflightRun = async () => { st += "?? .pytest_cache_x/log\n M src/bar.js\n"; return { rc: 0, output: "" }; };
|
|
1271
|
+
await run(w);
|
|
1272
|
+
assert.deepEqual(reverts, ["clean -fdq -- .pytest_cache_x/log", "checkout -- src/bar.js"]);
|
|
1273
|
+
assert.ok(traj(w).some((x) => /Validate preflight reverted 2 path\(s\) its runs changed/.test(x)));
|
|
1274
|
+
});
|
|
1275
|
+
it("#4353 the #4348 contract shape: an allowlist validate is dropped at freeze (CONTRACT_SCOPE_VALIDATE_DROPPED), the amended lane merges, and a real out-of-scope edit is still scope_creep", async () => {
|
|
1276
|
+
const ALLOW = 'test -z "$(git diff --name-only origin/staging...HEAD | grep -v -x -e src/foo.js -e test/foo.test.js)"';
|
|
1277
|
+
const c = { ...CONTRACT, validate: [...CONTRACT.validate, { id: "V2", command: ALLOW }] }, files = ["src/foo.js", "test/foo.test.js", "docs/foo.md"];
|
|
1278
|
+
const shape = (changed) => ({ contract: c, changed, amendments: [{ kind: "add-deliverable", path: "docs/foo.md", why: "the docs-freshness rule needs the module doc" }],
|
|
1279
|
+
validation: [{ id: "V1", command: CONTRACT.validate[0].command, exit: 0 }, { id: "V2", command: ALLOW, exit: 1 }], reviews: [conf({}, CONTRACT, files)] });
|
|
1280
|
+
const dir = tmpDir("scope-drop-");
|
|
1281
|
+
const w = world(shape(files));
|
|
1282
|
+
w.io.runCommand = async () => ({ rc: 1, output: "" });
|
|
1283
|
+
assert.equal((await run(w, { dir })).terminalReason, "merged", "the lane is not ended by scope");
|
|
1284
|
+
assert.equal(builders(w).length, 2, "plan + implement: the drop costs no contract correction");
|
|
1285
|
+
const log = readLog(dir, 7);
|
|
1286
|
+
assert.deepEqual(log.filter((e) => e.event === "CONTRACT_SCOPE_VALIDATE_DROPPED").map((e) => e.runs), [[{ id: "V2", command: ALLOW }]]);
|
|
1287
|
+
assert.equal(log.filter((e) => e.event === "VALIDATE_SUPERSEDED").length, 0, "nothing left for the #4350 path to supersede");
|
|
1288
|
+
const posted = w.comments.find((x) => x.includes("FORGE:CONTRACT"));
|
|
1289
|
+
assert.ok(posted.includes("V1") && !posted.includes("grep -v -x"), "the frozen contract has no allowlist validate");
|
|
1290
|
+
assert.ok(traj(w).some((t) => /Scope-check validate V2 dropped at contract freeze/.test(t)));
|
|
1291
|
+
const creep = world(shape([...files, "src/other.js"]));
|
|
1292
|
+
creep.io.runCommand = async () => ({ rc: 1, output: "" });
|
|
1293
|
+
assert.equal((await run(creep)).code, "conformance.scope_creep");
|
|
1294
|
+
assert.ok(builders(creep).some((b) => /changed outside the contract's deliverables: src\/other\.js/.test(b[4])));
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
it("#4404 a Lighthouse requirement is tagged out_of_band at freeze; with instrumentation the lane merges and the PR body names the job", async () => {
|
|
1298
|
+
const LH = "Lighthouse performance score >= 95 on the landing page.";
|
|
1299
|
+
const body = `${ISSUE_BODY}\n- ${LH}`;
|
|
1300
|
+
const c = { ...CONTRACT, requirements: [...CONTRACT.requirements, { id: "R2", quote: LH, disposition: "deliver", evidence: "lighthouse-nightly" }] };
|
|
1301
|
+
const tagged = { ...c, requirements: [c.requirements[0], { ...c.requirements[1], verify: "out_of_band" }] };
|
|
1302
|
+
const review = (instrumented) => conf({ issue_coverage: [...JSON.parse(conf().slice(8, -4)).issue_coverage, { quote: LH, contract_ids: ["R2"], status: "covered" }],
|
|
1303
|
+
requirements: [{ id: "R1", status: "implemented", evidence: "src/foo.js:3", validated_by: "V1" }, { id: "R2", status: "partial", evidence: instrumented ? "src/foo.js:3" : "", validated_by: "V1", instrumented, note: "measured by lighthouse-nightly" }] });
|
|
1304
|
+
const dir = tmpDir("oob-");
|
|
1305
|
+
const w = world({ contract: c, issueBodies: [body], claims: [{ id: "R1", status: "implemented", evidence: "src/foo.js:3" }, { id: "R2", status: "partial", instrumented: true, evidence: "src/foo.js:3" }], reviews: [review(true)] });
|
|
1306
|
+
assert.equal((await run(w, { dir })).terminalReason, "merged");
|
|
1307
|
+
assert.deepEqual(readLog(dir, 7).filter((e) => e.event === "CONTRACT_OUT_OF_BAND_TAGGED").map((e) => e.ids), [["R2"]]);
|
|
1308
|
+
assert.ok(w.comments.find((x) => x.includes("FORGE:CONTRACT")).includes('"verify": "out_of_band"'), "the frozen contract carries the tag");
|
|
1309
|
+
assert.ok(traj(w).some((t) => /R2 tagged verify out_of_band at contract freeze/.test(t)));
|
|
1310
|
+
assert.match(w.prs[0][w.prs[0].indexOf("--body") + 1], /## Verified out of band\n- R2: Lighthouse performance score >= 95 on the landing page\. \(job `lighthouse-nightly`\)/);
|
|
1311
|
+
// without the instrumentation it is still unimplemented
|
|
1312
|
+
const no = world({ contract: tagged, issueBodies: [body], claims: [{ id: "R1", status: "implemented", evidence: "src/foo.js:3" }, { id: "R2", status: "partial", instrumented: true, evidence: "src/foo.js:3" }], reviews: [review(false), review(false)] });
|
|
1313
|
+
const r = await run(no);
|
|
1314
|
+
assert.equal(r.code, "conformance.unimplemented");
|
|
1315
|
+
});
|
|
1171
1316
|
|
|
1172
1317
|
it("a failed validate fetches the lane and the resume says how to catch up with a moved base (merge, never rebase)", async () => {
|
|
1173
1318
|
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 }] });
|
|
@@ -1238,6 +1383,62 @@ describe("conformance shadow mode (the default)", () => {
|
|
|
1238
1383
|
assert.ok(w.seq.indexOf("pr") > -1 && traj(w)[0].includes("Tracked R2 (defer) in #900"));
|
|
1239
1384
|
});
|
|
1240
1385
|
|
|
1386
|
+
it("#4398 a diff on a declared review.risk_paths glob makes the shadow gate enforce: a nonconforming risk-path PR never merges", async () => {
|
|
1387
|
+
const dir = tmpDir("risk-");
|
|
1388
|
+
const w = world({ contract: { ...CONTRACT, preserve: [] }, reviews: [omission(), omission()] });
|
|
1389
|
+
const r = await runShadow(w, { dir, reviewTrigger: { mode: "shadow", riskPaths: ["src/**"] } });
|
|
1390
|
+
assert.equal(r.terminalReason, "needs-human"); assert.equal(w.merges.length, 0);
|
|
1391
|
+
assert.equal(r.code, "conformance.omission"); assert.equal(builders(w).length, 3, "one builder resume, as in enforce");
|
|
1392
|
+
assert.deepEqual(r.conformance, { mode: "enforce", would: null, risk: true });
|
|
1393
|
+
const log = readLog(dir, 7);
|
|
1394
|
+
assert.deepEqual(log.filter((e) => e.event === "CONFORMANCE_RISK_ENFORCED").map((e) => e.paths), [["src/foo.js"]], "logged once (sticky across the resume)");
|
|
1395
|
+
assert.equal(log.find((e) => e.event === "RUN_TERMINAL").conformance_mode, "enforce");
|
|
1396
|
+
assert.ok(traj(w)[0].includes("Conformance enforced (review.risk_paths): src/foo.js"));
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
it("#4398 risk-enforced lanes merge once conforming; built-in risk words and unmatched globs stay shadow", async () => {
|
|
1400
|
+
const w = world({ contract: { ...CONTRACT, preserve: [] }, reviews: [omission(), ok({}, { ...CONTRACT, preserve: [] })] });
|
|
1401
|
+
const r = await runShadow(w, { reviewTrigger: { mode: "shadow", riskPaths: ["src/foo.js"] } });
|
|
1402
|
+
assert.equal(r.terminalReason, "merged"); assert.equal(builders(w).length, 3);
|
|
1403
|
+
assert.match(w.comments.find((c) => c.includes("<!-- FORGE:REVIEW -->")), /\*\*Verdict:\*\* CONFORMS/);
|
|
1404
|
+
const s = world({ contract: { ...CONTRACT, preserve: [] }, changed: ["src/auth/foo.js", "test/foo.test.js"], reviews: [omission()] });
|
|
1405
|
+
const rs = await runShadow(s, { reviewTrigger: { mode: "shadow", riskPaths: ["db/**"] } });
|
|
1406
|
+
assert.equal(rs.terminalReason, "merged", "an auth path with no declared glob stays shadow");
|
|
1407
|
+
assert.deepEqual(rs.conformance, { mode: "shadow", would: "nonconforming", risk: false });
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
it("#4398 an investigation whose question is deferred closes partial (not planned), carried forward in the tracking issue", async () => {
|
|
1411
|
+
const k = { ...CONTRACT, preserve: [], requirements: [CONTRACT.requirements[0], { id: "R2", quote: "Existing callers of foo keep working.", disposition: "defer", why: "w" }] };
|
|
1412
|
+
const w = world({ title: "investigate(stealth): which signal is rejected", contract: k, claims: [{ id: "R1", status: "implemented" }], reviews: [conf({}, k)] });
|
|
1413
|
+
const r = await runShadow(w);
|
|
1414
|
+
assert.equal(r.terminalReason, "merged"); assert.equal(r.close, "partial");
|
|
1415
|
+
assert.deepEqual(w.closed.map((a) => a[a.indexOf("--reason") + 1]), ["not planned"]);
|
|
1416
|
+
assert.ok(w.comments.some((c) => c.includes("<!-- FORGE:TRACKED -->") && /Closed as partial[\s\S]*carried forward in #900/.test(c)));
|
|
1417
|
+
assert.ok(traj(w)[0].includes("| Close | issue closed (partial) |"));
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
it("#4398 an investigation whose answer the review marks deferred is tracked before the PR and closes partial, even though shadow merges it", async () => {
|
|
1421
|
+
const k = { ...CONTRACT, preserve: [] };
|
|
1422
|
+
const w = world({ title: "investigate: foo", contract: k, reviews: [conf({ requirements: [{ id: "R1", status: "deferred", evidence: "", validated_by: "", note: "question open" }] }, k)] });
|
|
1423
|
+
const r = await runShadow(w);
|
|
1424
|
+
assert.equal(r.terminalReason, "merged"); assert.equal(r.close, "partial");
|
|
1425
|
+
assert.equal(w.followups.length, 1); assert.match(w.followups[0].items[0].key, /^R1: /); assert.match(w.followups[0].items[0].why, /investigation question not answered/);
|
|
1426
|
+
assert.match(w.prs[0].join(" "), /#900/);
|
|
1427
|
+
assert.deepEqual(w.closed.map((a) => a[a.indexOf("--reason") + 1]), ["not planned"]);
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
it("#4398 an answered investigation and a non-investigation with a deferral both close completed", async () => {
|
|
1431
|
+
const a = world({ title: "investigate(x): why foo breaks", contract: { ...CONTRACT, preserve: [] }, reviews: [conf({}, { ...CONTRACT, preserve: [] })] });
|
|
1432
|
+
const ra = await runShadow(a);
|
|
1433
|
+
assert.equal(ra.terminalReason, "merged"); assert.equal(ra.close, undefined);
|
|
1434
|
+
assert.deepEqual(a.closed.map((x) => x[x.indexOf("--reason") + 1]), ["completed"]);
|
|
1435
|
+
const k = { ...CONTRACT, preserve: [], requirements: [CONTRACT.requirements[0], { id: "R2", quote: "Existing callers of foo keep working.", disposition: "defer", why: "w" }] };
|
|
1436
|
+
const b = world({ contract: k, claims: [{ id: "R1", status: "implemented" }], reviews: [conf({}, k)] });
|
|
1437
|
+
await runShadow(b);
|
|
1438
|
+
assert.deepEqual(b.closed.map((x) => x[x.indexOf("--reason") + 1]), ["completed"]);
|
|
1439
|
+
assert.ok(traj(b)[0].includes("| Close | issue closed (completed) |"));
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1241
1442
|
it("#4160 base-sync merge files (equal to the base) drop out of the scope check", async () => {
|
|
1242
1443
|
const w = world({ changed: ["src/foo.js", "src/merged-from-base.js", "test/foo.test.js"], equalToBase: ["src/merged-from-base.js"] });
|
|
1243
1444
|
assert.equal((await run(w)).terminalReason, "merged");
|
|
@@ -1294,7 +1495,7 @@ describe("structure delta (forge#4159): undeclared package edges block, import g
|
|
|
1294
1495
|
assert.equal(r.terminalReason, "merged");
|
|
1295
1496
|
assert.equal(builders(w).length, 3); assert.equal(reviewers(w).length, 1);
|
|
1296
1497
|
assert.match(builders(w)[2][4], /@s\/db -> @s\/core \(dependencies, packages\/db\/package\.json\)[\s\S]*add it to the contract with a reason, or remove it/);
|
|
1297
|
-
assert.equal(builders(w)[2][2],
|
|
1498
|
+
assert.equal(builders(w)[2][2], null, "#4363: a fresh fix session");
|
|
1298
1499
|
assert.deepEqual(w.structureCalls[0], { cwd: "/wt/lean-7", base: "origin/staging" });
|
|
1299
1500
|
const e = readLog(dir, 7).find((x) => x.event === "STRUCTURE_DELTA");
|
|
1300
1501
|
assert.equal(e.edges_added, 1);
|
|
@@ -1377,6 +1578,7 @@ describe("structure delta (forge#4159): undeclared package edges block, import g
|
|
|
1377
1578
|
assert.equal(reviewerPrompt(args), reviewerPrompt({ ...args, structure: "" }));
|
|
1378
1579
|
assert.ok(reviewerPrompt({ ...args, structure: "x\ny" }).endsWith("</diff>\n<structure>x y</structure>"));
|
|
1379
1580
|
assert.match(builderPlanPrompt(7), /"the lint allows it" or "there is a precedent" is not a reason on its own/);
|
|
1581
|
+
assert.match(builderPlanPrompt(7), /Scope is enforced by the engine[^\n]*do not write a file-allowlist or denylist validate[^\n]*the engine drops such a validate at freeze/, "#4353");
|
|
1380
1582
|
assert.match(builderPlanPrompt(7), /must be a deliverable on that package\.json whose change names the package and gives the reason/);
|
|
1381
1583
|
assert.match(builderImplementPrompt(), /blocked by the engine: add it through add-deliverable with a why naming the package/);
|
|
1382
1584
|
});
|
|
@@ -1407,7 +1609,8 @@ describe("hotspot prevention (forge#4195): plan facts, build rule, unexempted gr
|
|
|
1407
1609
|
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.");
|
|
1408
1610
|
assert.equal(hotspotBuildRule(null), ""); assert.equal(hotspotBuildRule(facts([], { enabled: false })), "");
|
|
1409
1611
|
const plan = builderPlanPrompt(7) + hotspotPlanNote(facts([HOT, { ...HOT, path: "a/b.py" }, { ...HOT, path: "c/d.py" }]));
|
|
1410
|
-
|
|
1612
|
+
// 1850: #4404 (verify out_of_band) and #4396 (split size rule) grew the plan prompt to ~1803 tokens on CI paths.
|
|
1613
|
+
assert.ok(approxTokens(plan) <= 1850, `plan prompt with hotspots is ${approxTokens(plan)} tokens`);
|
|
1411
1614
|
assert.ok(approxTokens(builderImplementPrompt() + hotspotBuildRule(facts([]))) <= 1500);
|
|
1412
1615
|
});
|
|
1413
1616
|
|
|
@@ -1719,6 +1922,17 @@ describe("comment templates", () => {
|
|
|
1719
1922
|
assert.match(renderPrBody({ issue: 1, result: null }), /## Root cause\nnot reported[\s\S]*Closes #1$/);
|
|
1720
1923
|
});
|
|
1721
1924
|
|
|
1925
|
+
it("#4404 out-of-band requirements are listed in the PR body with their job and marked in the contract and review tables", () => {
|
|
1926
|
+
const c = { requirements: [{ id: "R1", quote: "Fix the leak.", disposition: "deliver" }, { id: "R2", quote: "Lighthouse >= 95 on /home.", disposition: "deliver", verify: "out_of_band", evidence: "lighthouse-nightly" }], deliverables: [{ id: "D1", path: "a.js", change: "x", requirements: ["R1"] }] };
|
|
1927
|
+
const body = renderPrBody({ issue: 4, contract: c, result: null });
|
|
1928
|
+
assert.match(body, /## Verified out of band\n- R2: Lighthouse >= 95 on \/home\. \(job `lighthouse-nightly`\)\n\nCloses #4$/);
|
|
1929
|
+
assert.doesNotMatch(renderPrBody({ issue: 4, contract: { ...c, requirements: [c.requirements[0]] }, result: null }), /out of band/);
|
|
1930
|
+
assert.match(renderContract(c, "abc"), /\| R2 \| Lighthouse >= 95 on \/home\. \| deliver, verified out of band \(lighthouse-nightly\) \|/);
|
|
1931
|
+
const rv = { requirements: [{ id: "R1", status: "implemented" }, { id: "R2", status: "partial" }], issue_coverage: [], scope: [], preserve: [] };
|
|
1932
|
+
assert.match(renderReview(rv, c), /\| R2 \| partial \(out of band: lighthouse-nightly\) \|/);
|
|
1933
|
+
assert.match(renderReview(rv, c), /\| R1 \| implemented \|/);
|
|
1934
|
+
});
|
|
1935
|
+
|
|
1722
1936
|
it("trajectory rows: merged, invalid, needs-human; missing rows read 'not reached'", () => {
|
|
1723
1937
|
const base = { issue: 9, validate: ["CONFIRMED", "2 evidence item(s)"], plan: "posted", implement: ["2 file(s)", "b"], review: ["PASS", "1 round"], pr: ["#5", "opened"], merge: ["all", "merged at abc"], close: ["issue closed (completed)", "label workflow:merged"], decisions: ["d1"], anomalies: [], usage: "cost $1" };
|
|
1724
1938
|
const m = renderTrajectory(base);
|
|
@@ -1741,10 +1955,21 @@ describe("comment templates", () => {
|
|
|
1741
1955
|
assert.match(t, /does not conform to the issue and its frozen contract after one builder resume/);
|
|
1742
1956
|
});
|
|
1743
1957
|
|
|
1958
|
+
it("#4401 the lane posts FORGE:FACTS from result.json facts, its validation and changed files; never quotable as a requirement", async () => {
|
|
1959
|
+
const w = world({ result: { status: "fixed", summary: "s", validation: [{ id: "V1", command: "node --test test/foo.test.js", exit: 0 }],
|
|
1960
|
+
facts: { rejected: [{ approach: "detection-only guard", why: "hides the cause" }], hotspots: [{ at: "src/foo.js:3", why: "null path" }] } } });
|
|
1961
|
+
assert.equal((await run(w)).terminalReason, "merged");
|
|
1962
|
+
const f = parseFactsComment(w.comments.find((c) => markerOf(c) === "FORGE:FACTS"));
|
|
1963
|
+
assert.deepEqual(f.rejected, [{ approach: "detection-only guard", why: "hides the cause" }]);
|
|
1964
|
+
assert.deepEqual(f.baselines, [{ command: "node --test test/foo.test.js", exit: 0 }]);
|
|
1965
|
+
assert.ok(f.files.length > 0 && !f.files.some((x) => x.startsWith(".forge-lean/")), JSON.stringify(f.files));
|
|
1966
|
+
assert.ok(!scopeText({ title: "t", body: "b" }, [{ body: w.comments.find((c) => markerOf(c) === "FORGE:FACTS") }]).includes("detection-only"));
|
|
1967
|
+
});
|
|
1968
|
+
|
|
1744
1969
|
it("missing stage files in a real run are rendered from result.json and noted as anomalies", async () => {
|
|
1745
1970
|
const w = world({ noStages: true, result: { status: "fixed", summary: "fixed foo", evidence: ["src/foo.js:3 x"], root_cause: "bad at src/foo.js:3", changes: [{ file: "src/foo.js", change: "guard", why: "w" }], tests: [{ command: "t", result: "pass", fails_without_fix: false }] } });
|
|
1746
1971
|
await run(w);
|
|
1747
|
-
assert.deepEqual(w.comments.map(markerOf), ["FORGE:STARTED", "FORGE:INVESTIGATION", "FORGE:CONTRACT", "FORGE:IMPLEMENTATION", "FORGE:REVIEW", "FORGE:TRAJECTORY"]);
|
|
1972
|
+
assert.deepEqual(w.comments.map(markerOf), ["FORGE:STARTED", "FORGE:INVESTIGATION", "FORGE:CONTRACT", "FORGE:IMPLEMENTATION", "FORGE:REVIEW", "FORGE:FACTS", "FORGE:TRAJECTORY"]);
|
|
1748
1973
|
assert.match(w.comments[1], /Verdict\*\*: CONFIRMED[\s\S]*Root Cause\nnot reported/, "the plan call has no result.json: a placeholder, still posted");
|
|
1749
1974
|
assert.match(w.comments[3], /fails without fix: no/);
|
|
1750
1975
|
assert.match(traj(w)[0], /investigation.md missing or empty: rendered from result.json/);
|
|
@@ -1844,6 +2069,10 @@ describe("prompts", () => {
|
|
|
1844
2069
|
it("the conformance reviewer asks for rows, not an opinion, and stays within budget", () => {
|
|
1845
2070
|
assert.ok(approxTokens(REVIEWER_SYSTEM) <= 1500);
|
|
1846
2071
|
for (const k of ["issue_coverage", "requirements", "scope", "preserve", "regressions", "engine computes the verdict", "reply without rows is rejected", "untrusted data"]) assert.ok(REVIEWER_SYSTEM.includes(k), k);
|
|
2072
|
+
// #4404: out-of-band requirements are judged by their instrumentation, in the reviewer, the shared rubric and the plan prompt
|
|
2073
|
+
assert.match(REVIEWER_SYSTEM, /out_of_band row[\s\S]*instrumented: true only when its budget test or bench is committed/);
|
|
2074
|
+
assert.match(REVIEW_RUBRIC, /out-of-band requirement is delivered by its instrumentation/);
|
|
2075
|
+
assert.match(builderPlanPrompt(7), /"verify":"out_of_band" with "evidence" naming the job or workflow/);
|
|
1847
2076
|
assert.doesNotMatch(REVIEWER_SYSTEM, /Does this change fix the issue as described\?/);
|
|
1848
2077
|
});
|
|
1849
2078
|
|
|
@@ -1959,6 +2188,27 @@ describe("orchestrate", () => {
|
|
|
1959
2188
|
assert.deepEqual(rows.map((r) => [r.issue, r.outcome]), [[1, "merged"], [2, "merged"], [9, "merged"]]);
|
|
1960
2189
|
});
|
|
1961
2190
|
|
|
2191
|
+
it("#4387 an out-of-set dependency labelled needs-human is not pulled in; the issue and its dependents end skipped", async () => {
|
|
2192
|
+
const log = [], asked = [];
|
|
2193
|
+
const rows = await orchestrate({ issues: [1, 2], parallel: 2,
|
|
2194
|
+
readBody: async (n) => (n === 2 ? "**Depends on**: #1" : ""),
|
|
2195
|
+
issueLabels: async (n) => { asked.push(n); return n === 9 ? ["bug", "needs-human"] : []; },
|
|
2196
|
+
runOne: async (n) => { log.push(n); return n === 1 ? { terminalReason: "dependency-open", dependencies: [9], detail: "depends on #9" } : { terminalReason: "merged" }; } });
|
|
2197
|
+
assert.deepEqual(log, [1], "no lane for #9 and none for the dependent #2");
|
|
2198
|
+
assert.deepEqual(asked, [9]);
|
|
2199
|
+
assert.deepEqual(rows.map((r) => [r.issue, r.outcome]), [[1, "skipped"], [2, "skipped"]]);
|
|
2200
|
+
assert.equal(rows[0].detail, "dependency #9 needs-human");
|
|
2201
|
+
assert.match(rows[1].detail, /#1/);
|
|
2202
|
+
});
|
|
2203
|
+
|
|
2204
|
+
it("#4387 a dependency that already ended needs-human in this batch is not run again", async () => {
|
|
2205
|
+
const log = [];
|
|
2206
|
+
const rows = await orchestrate({ issues: [9, 1], parallel: 1, readBody: async () => "",
|
|
2207
|
+
runOne: async (n) => { log.push(n); return n === 9 ? { terminalReason: "needs-human", detail: "ci.red" } : { terminalReason: "dependency-open", dependencies: [9] }; } });
|
|
2208
|
+
assert.deepEqual(log, [9, 1]);
|
|
2209
|
+
assert.deepEqual(rows.map((r) => [r.issue, r.outcome, r.detail]), [[9, "needs-human", "ci.red"], [1, "skipped", "dependency #9 needs-human"]]);
|
|
2210
|
+
});
|
|
2211
|
+
|
|
1962
2212
|
it("#4124 a repeated dependency-open stops after the requeue cap", async () => {
|
|
1963
2213
|
const h = harness({}, { 1: "dependency-open" });
|
|
1964
2214
|
const clock = { t: 0 }, starts = [];
|
|
@@ -2101,6 +2351,24 @@ describe("orchestrate", () => {
|
|
|
2101
2351
|
assert.equal(res.ok, true);
|
|
2102
2352
|
});
|
|
2103
2353
|
|
|
2354
|
+
it("#4361 a preflight GC that ran out of budget resumes at the next admission interval even off the ceiling", async () => {
|
|
2355
|
+
const leanIo = { gh: async (a) => (a[0] === "repo" ? "main\n" : a.includes("milestone") ? JSON.stringify({ milestone: null }) : JSON.stringify({ body: "" })),
|
|
2356
|
+
git: async (a) => (a[0] === "ls-remote" && a.at(-1) === "refs/heads/staging" ? "abc\tref\n" : "") };
|
|
2357
|
+
const gcCalls = [], logs = [];
|
|
2358
|
+
let t = 0;
|
|
2359
|
+
const deferred = [5, 0];
|
|
2360
|
+
const l = console.log; console.log = (s) => logs.push(String(s));
|
|
2361
|
+
try {
|
|
2362
|
+
await orchestrateFromCli(["1", "2", "3", "--parallel", "1", "--repo", "o/r", "--no-prefilter"], { dir: tmpDir("core-gc-"), batchRoot: tmpDir("core-gc-batch-"), allowTmpfs: true, emitTelemetry: async () => {}, io: { gh: async () => "o/r\n" }, leanIo,
|
|
2363
|
+
leanModule: { runCore: async (o) => { t += 6 * 60 * 1000; return { terminalReason: "merged", pr: o.issue, cost_usd: 0, wall_ms: 1 }; } },
|
|
2364
|
+
gc: async ({ phase }) => { gcCalls.push(phase); return { reclaimed: 1, deferred: deferred.shift() ?? 0, bytes: 0 }; },
|
|
2365
|
+
host: { loadAvg1: () => 0, cores: () => 4, memAvailableMb: () => 64000 }, now: () => t, hygiene: { auto_gc: true, max_host_load_ratio: 0.8 } });
|
|
2366
|
+
} finally { console.log = l; }
|
|
2367
|
+
// preflight deferred 5 items: the next due admission finishes them; once nothing is pending, no more GC off the ceiling
|
|
2368
|
+
assert.deepEqual(gcCalls, ["preflight", "admission"]);
|
|
2369
|
+
assert.ok(logs.includes("gc (preflight): 5 item(s) deferred to the next admission"), logs.join("\n"));
|
|
2370
|
+
});
|
|
2371
|
+
|
|
2104
2372
|
it("#4125 a lane-refused issue starts no run and its dependents are skipped, not reported as a cycle", async () => {
|
|
2105
2373
|
const runs = [];
|
|
2106
2374
|
const rows = await orchestrate({ issues: [1, 2, 3], parallel: 2, readBody: async (n) => (n === 2 ? "**Depends on**: #1" : ""),
|
|
@@ -2212,7 +2480,9 @@ describe("orchestrate maintenance budget (#4197)", () => {
|
|
|
2212
2480
|
await orchestrateFromCli(["1", "--repo", "o/r", "--no-prefilter"], { ...mk(), maintenance: null });
|
|
2213
2481
|
assert.deepEqual(ran, [1]);
|
|
2214
2482
|
// No maintenance scan without the budget; labels (host priority, #4191) ride on the body call, never a call of their own.
|
|
2215
|
-
|
|
2483
|
+
// #4399: the batch-start acceptance-probe sweep (`forge-probe` search) is not a maintenance scan.
|
|
2484
|
+
assert.ok(calls.some((c) => c.startsWith("issue list") && c.includes("forge-probe")), calls.join("\n"));
|
|
2485
|
+
assert.ok(!calls.some((c) => (c.startsWith("issue list") && !c.includes("forge-probe")) || c.endsWith("--json labels")), calls.join("\n"));
|
|
2216
2486
|
ran.length = 0; calls.length = 0;
|
|
2217
2487
|
const res = await orchestrateFromCli(["1", "--repo", "o/r", "--no-prefilter"], { ...mk(), maintenance: { share: 0.5, whenIdle: true, labels: ["maintenance"] } });
|
|
2218
2488
|
assert.deepEqual(ran, [1, 9]);
|
|
@@ -2239,6 +2509,22 @@ describe("runCliBackendAsync", () => {
|
|
|
2239
2509
|
assert.equal(a.sessionId, "s-1"); assert.equal(a.text, "hello"); assert.equal(a.costUsd, 0.25);
|
|
2240
2510
|
});
|
|
2241
2511
|
|
|
2512
|
+
it("#4390: contextMax stops a stream session whose turn context reaches it, with aggregated usage and turns", async () => {
|
|
2513
|
+
const { writeFileSync, chmodSync } = await import("node:fs");
|
|
2514
|
+
const { runCliBackendAsync } = await import("../runner.mjs");
|
|
2515
|
+
const d = tmpDir("core-async-");
|
|
2516
|
+
const line = (id, ctx) => JSON.stringify({ type: "assistant", message: { id, content: [{ type: "tool_use", name: "Bash", input: { command: "ls" } }], usage: { input_tokens: 10, output_tokens: 7, cache_read_input_tokens: ctx, cache_creation_input_tokens: 0 } } });
|
|
2517
|
+
writeFileSync(`${d}/fake`, `#!/bin/sh\necho '${line("m1", 500)}'\necho '${line("m1", 500)}'\necho '${line("m2", 2000)}'\nexec sleep 30\n`); chmodSync(`${d}/fake`, 0o755);
|
|
2518
|
+
const t0 = Date.now();
|
|
2519
|
+
await assert.rejects(runCliBackendAsync({ spec: { name: "t" }, userMessage: "go", args: ["1"], cwd: d, bin: `${d}/fake`, logger: { log() {} }, stream: true, contextMax: 1000 }), (e) => {
|
|
2520
|
+
assert.equal(e.code, "CLI_CONTEXT"); assert.equal(e.context, 2010); assert.equal(e.turns, 2, "lines of one message count once");
|
|
2521
|
+
assert.deepEqual(e.usage, { input_tokens: 20, output_tokens: 14, cache_creation_input_tokens: 0, cache_read_input_tokens: 2500 });
|
|
2522
|
+
assert.equal(e.lastTool.name, "Bash");
|
|
2523
|
+
return true;
|
|
2524
|
+
});
|
|
2525
|
+
assert.ok(Date.now() - t0 < 15000, "killed, not waited out");
|
|
2526
|
+
});
|
|
2527
|
+
|
|
2242
2528
|
it("surfaces a failing CLI as the same CLI_BACKEND_FAILED error", async () => {
|
|
2243
2529
|
const { writeFileSync, chmodSync } = await import("node:fs");
|
|
2244
2530
|
const { runCliBackendAsync } = await import("../runner.mjs");
|
|
@@ -2355,6 +2641,58 @@ describe("base-sync (#4100)", () => {
|
|
|
2355
2641
|
assert.ok(w.edits.some((e) => e.includes("needs-human")));
|
|
2356
2642
|
});
|
|
2357
2643
|
|
|
2644
|
+
/** #4406: io.postMergeChecks reports the scoped typecheck; `tsc` fails `fails` times on the merged tree, then passes. */
|
|
2645
|
+
const pmWorld = (fails, o = {}) => {
|
|
2646
|
+
const w = world({ prView: [CONFLICTING, OKSTATE], mergeResults: ["clean"], reviews: [ok(), ok()], ...o });
|
|
2647
|
+
w.pmRuns = [];
|
|
2648
|
+
w.io.postMergeChecks = async (x) => { w.pmArgs = x; return [{ kind: "typecheck", command: "tsc -p apps/web", cwd: ".", mode: "scoped" }, { kind: "lint", command: "eslint .", cwd: ".", mode: "scoped" }]; };
|
|
2649
|
+
w.io.runCommand = async ({ command }) => {
|
|
2650
|
+
if (command !== "tsc -p apps/web") return { rc: 0, output: "" };
|
|
2651
|
+
w.pmRuns.push(w.calls.length);
|
|
2652
|
+
return w.pmRuns.length <= fails ? { rc: 2, output: "apps/web/__tests__/cross-tenant-actions.test.ts(41,3): error TS1117: An object literal cannot have multiple properties with the same name." } : { rc: 0, output: "" };
|
|
2653
|
+
};
|
|
2654
|
+
return w;
|
|
2655
|
+
};
|
|
2656
|
+
|
|
2657
|
+
it("#4406 a base-sync whose merged tree fails typecheck resumes the builder with the errors before pushing", async () => {
|
|
2658
|
+
const w = pmWorld(1);
|
|
2659
|
+
const r = await run(w);
|
|
2660
|
+
assert.equal(r.terminalReason, "merged");
|
|
2661
|
+
assert.equal(w.pmRuns.length, 2, "typecheck on the merged tree, then again after the fix");
|
|
2662
|
+
const fix = w.calls.findIndex((c) => c[0] === "runner" && c[1] === "builder" && /TS1117/.test(c[4]));
|
|
2663
|
+
assert.ok(fix > 0, "builder resumed with the typecheck errors");
|
|
2664
|
+
assert.match(w.calls[fix][4], /After merging the base into this branch[\s\S]*tsc -p apps\/web/);
|
|
2665
|
+
const pushes = w.calls.map((c, i) => [c, i]).filter(([c]) => c[0] === "git" && c[1] === "push" && !c.includes("--delete")).map(([, i]) => i);
|
|
2666
|
+
assert.equal(pushes.length, 2, "initial push plus the sync push");
|
|
2667
|
+
assert.ok(fix < pushes[1] && w.pmRuns[0] < pushes[1], "fixed before the base-sync push");
|
|
2668
|
+
assert.equal(reviewers(w).length, 2, "the post-merge fix is re-reviewed");
|
|
2669
|
+
assert.match(syncComment(w), /builder fixed post-merge check failures[\s\S]*Tests run:\*\* tsc -p apps\/web: pass/);
|
|
2670
|
+
assert.equal(w.pmArgs.cwd, WT);
|
|
2671
|
+
noRebaseNoForce(w);
|
|
2672
|
+
});
|
|
2673
|
+
|
|
2674
|
+
it("#4406 a passing post-merge typecheck keeps the clean-merge path: no builder, no re-review; ci mode runs no host typecheck", async () => {
|
|
2675
|
+
let w = pmWorld(0);
|
|
2676
|
+
assert.equal((await run(w)).terminalReason, "merged");
|
|
2677
|
+
assert.equal(w.pmRuns.length, 1);
|
|
2678
|
+
assert.equal(builders(w).length, 2);
|
|
2679
|
+
assert.equal(reviewers(w).length, 1);
|
|
2680
|
+
w = pmWorld(1);
|
|
2681
|
+
await run(w, { verify: { mode: "ci" } }).catch(() => null);
|
|
2682
|
+
assert.equal(w.pmRuns.length, 0, "ci mode: the scoped typecheck is left to CI");
|
|
2683
|
+
w = pmWorld(9);
|
|
2684
|
+
await run(w, { verify: { postMerge: "make check" } });
|
|
2685
|
+
assert.equal(w.pmRuns.length, 0, "verify.post_merge replaces the scoped typecheck");
|
|
2686
|
+
});
|
|
2687
|
+
|
|
2688
|
+
it("#4406 a merged tree that still fails after verify.max_rounds is an anomaly; the sync still pushes", async () => {
|
|
2689
|
+
const w = pmWorld(9, { reviews: [ok(), ok(), ok()] });
|
|
2690
|
+
const r = await run(w, { verify: { maxRounds: 1 } });
|
|
2691
|
+
assert.equal(r.terminalReason, "merged");
|
|
2692
|
+
assert.equal(w.pmRuns.length, 2);
|
|
2693
|
+
assert.match(traj(w)[0], /Post-merge check\(s\) still fail after merging origin\/staging/);
|
|
2694
|
+
});
|
|
2695
|
+
|
|
2358
2696
|
describe("#4266 PR turns conflicting during the CI wait", () => {
|
|
2359
2697
|
const syncCount = (w) => gitCalls(w).filter((x) => x[0] === "merge" && x[1] === "--no-edit").length;
|
|
2360
2698
|
/** required check `build` reports nothing (never started) until a base-sync merged, then `then`. */
|
|
@@ -2828,12 +3166,13 @@ describe("batch robustness (#4112-#4115)", () => {
|
|
|
2828
3166
|
assert.equal(r.heavyQueueMax, 4);
|
|
2829
3167
|
});
|
|
2830
3168
|
|
|
2831
|
-
it("#4331 pre-PR checks: scoped by the test map, concurrent up to FORGE_HOST_PARALLELISM
|
|
3169
|
+
it("#4331/#4344 pre-PR checks: scoped by the test map, concurrent up to FORGE_HOST_PARALLELISM with one heavy slot and a worker share each, timed", async () => {
|
|
2832
3170
|
const w = heavyWorld(); w.changed = ["commands/foo.md"];
|
|
2833
3171
|
w.baseFiles = { "origin/staging:.forge/test-map.json": JSON.stringify({ version: 1, entries: [
|
|
2834
3172
|
{ globs: ["commands/**"], command: "bash scripts/check-a.sh", cwd: "." }, { globs: ["bin/**"], command: "bash scripts/check-b.sh", cwd: "." }] }) };
|
|
2835
3173
|
const ran = []; let live = 0, peak = 0;
|
|
2836
|
-
|
|
3174
|
+
const envs = [];
|
|
3175
|
+
w.io.runCommand = async ({ command, env }) => { ran.push(command); envs.push(env.FORGE_HOST_PARALLELISM); peak = Math.max(peak, ++live); await new Promise((r) => setImmediate(r)); w.clock += 1000; live--; return { rc: 0, output: "" }; };
|
|
2837
3176
|
const prev = process.env.FORGE_HOST_PARALLELISM; process.env.FORGE_HOST_PARALLELISM = "2";
|
|
2838
3177
|
const dir = tmpDir("core-");
|
|
2839
3178
|
let r;
|
|
@@ -2841,9 +3180,10 @@ describe("batch robustness (#4112-#4115)", () => {
|
|
|
2841
3180
|
finally { if (prev === undefined) delete process.env.FORGE_HOST_PARALLELISM; else process.env.FORGE_HOST_PARALLELISM = prev; }
|
|
2842
3181
|
assert.deepEqual(ran, ["bash scripts/check-a.sh", "make lint", "make fmt"], "mapped check-b misses commands/foo.md; unmapped ones run");
|
|
2843
3182
|
assert.equal(peak, 2);
|
|
2844
|
-
assert.equal(w.acq.filter((a) => a.phase === "precheck").length,
|
|
3183
|
+
assert.equal(w.acq.filter((a) => a.phase === "precheck").length, 3, "one heavy slot per command");
|
|
3184
|
+
assert.deepEqual(envs, ["1", "1", "1"], "N=2 shared by 2 concurrent commands, not multiplied");
|
|
2845
3185
|
const [pre] = readLog(dir, 7).filter((e) => e.event === "PRECHECK_COMMANDS");
|
|
2846
|
-
assert.equal(pre.parallelism, 2); assert.ok(pre.ms > 0); assert.ok(pre.results.every((x) => Number.isFinite(x.ms)));
|
|
3186
|
+
assert.equal(pre.parallelism, 2); assert.equal(pre.workers, 1); assert.ok(pre.ms > 0); assert.ok(pre.results.every((x) => Number.isFinite(x.ms)));
|
|
2847
3187
|
assert.ok(r.precheckMs > 0); assert.equal(r.precheckDeferred, 0);
|
|
2848
3188
|
});
|
|
2849
3189
|
|
|
@@ -3065,6 +3405,84 @@ describe("batch robustness (#4112-#4115)", () => {
|
|
|
3065
3405
|
}
|
|
3066
3406
|
});
|
|
3067
3407
|
|
|
3408
|
+
// ---- #4405 ----
|
|
3409
|
+
const HEAVY_CHILD = (d) => `printf '#!/bin/sh\\nsleep 1.5\\n' > ${d}/vitest; chmod +x ${d}/vitest`;
|
|
3410
|
+
it("#4405 a silent session with a running heavy-step child is not idle-killed within the extended window", async () => {
|
|
3411
|
+
const { runCliBackendAsync } = await import("../runner.mjs");
|
|
3412
|
+
const { isHeavyProcess } = await import("./heavy.mjs");
|
|
3413
|
+
const d0 = tmpDir("core-cli-");
|
|
3414
|
+
const { d, bin } = await fakeCli("waits", `${TOOL}\n${HEAVY_CHILD(d0)}\n${d0}/vitest run\n${RESULT}`);
|
|
3415
|
+
const extended = [];
|
|
3416
|
+
const busy = (children) => { const work = children.filter((c) => isHeavyProcess(c.cmd)); return work.length ? { children: work, heavyQueue: { queued: 0, running: 1 } } : null; };
|
|
3417
|
+
const r = await runCliBackendAsync({ spec: { name: "t" }, userMessage: "go", args: ["1"], cwd: d, bin, logger: { log() {} }, stream: true, idleMs: 300, maxMs: 20000,
|
|
3418
|
+
busy, busyMs: 10000, onIdleExtend: (i) => extended.push(i) });
|
|
3419
|
+
assert.equal(r.text, "all done");
|
|
3420
|
+
assert.equal(extended.length, 1, "the extension is reported once per silent stretch");
|
|
3421
|
+
assert.ok(extended[0].children.some((c) => /vitest run/.test(c.cmd)), JSON.stringify(extended[0]));
|
|
3422
|
+
});
|
|
3423
|
+
|
|
3424
|
+
it("#4405 a busy session past idle + busy window is killed as CLI_IDLE with extended and heavyQueue", async () => {
|
|
3425
|
+
const { runCliBackendAsync } = await import("../runner.mjs");
|
|
3426
|
+
const { d, bin } = await fakeCli("stuck", `${TOOL}\nsleep 30 &\nwait`);
|
|
3427
|
+
const t0 = Date.now();
|
|
3428
|
+
await assert.rejects(runCliBackendAsync({ spec: { name: "t" }, userMessage: "go", args: ["1"], cwd: d, bin, logger: { log() {} }, stream: true, idleMs: 300, maxMs: 20000,
|
|
3429
|
+
busy: () => ({ children: [], heavyQueue: { queued: 1, running: 0 } }), busyMs: 600 }), (e) => {
|
|
3430
|
+
assert.equal(e.code, "CLI_IDLE"); assert.equal(e.extended, true); assert.deepEqual(e.heavyQueue, { queued: 1, running: 0 });
|
|
3431
|
+
return true;
|
|
3432
|
+
});
|
|
3433
|
+
const took = Date.now() - t0;
|
|
3434
|
+
assert.ok(took >= 850 && took < 8000, `killed after idle + busy window, not before (${took} ms)`);
|
|
3435
|
+
});
|
|
3436
|
+
|
|
3437
|
+
it("#4405 the CLI_TIMEOUT ceiling still applies during an idle extension", async () => {
|
|
3438
|
+
const { runCliBackendAsync } = await import("../runner.mjs");
|
|
3439
|
+
const { d, bin } = await fakeCli("busyceil", `${TOOL}\nsleep 30 &\nwait`);
|
|
3440
|
+
const t0 = Date.now();
|
|
3441
|
+
await assert.rejects(runCliBackendAsync({ spec: { name: "t" }, userMessage: "go", args: ["1"], cwd: d, bin, logger: { log() {} }, stream: true, idleMs: 300, maxMs: 1200,
|
|
3442
|
+
busy: () => ({ children: [], heavyQueue: { queued: 0, running: 1 } }), busyMs: 60000 }), (e) => { assert.equal(e.code, "CLI_TIMEOUT", e.message); return true; });
|
|
3443
|
+
assert.ok(Date.now() - t0 < 6000, "killed at maxMs, not after idle + busy window");
|
|
3444
|
+
});
|
|
3445
|
+
|
|
3446
|
+
it("#4405 a silent session with no work children is still killed at the idle timeout when busy is wired", async () => {
|
|
3447
|
+
const { runCliBackendAsync } = await import("../runner.mjs");
|
|
3448
|
+
const { isHeavyProcess } = await import("./heavy.mjs");
|
|
3449
|
+
const { d, bin } = await fakeCli("idle2", `${TOOL}\nsleep 30 &\nwait`);
|
|
3450
|
+
const t0 = Date.now(); let asked = 0;
|
|
3451
|
+
await assert.rejects(runCliBackendAsync({ spec: { name: "t" }, userMessage: "go", args: ["1"], cwd: d, bin, logger: { log() {} }, stream: true, idleMs: 400, maxMs: 20000,
|
|
3452
|
+
busy: (ch) => { asked++; return ch.some((c) => isHeavyProcess(c.cmd)) ? { children: ch } : null; }, busyMs: 60000 }), (e) => {
|
|
3453
|
+
assert.equal(e.code, "CLI_IDLE"); assert.equal(e.extended, false); assert.equal(e.heavyQueue, null);
|
|
3454
|
+
assert.ok(e.children.some((c) => /sleep 30/.test(c.cmd)));
|
|
3455
|
+
return true;
|
|
3456
|
+
});
|
|
3457
|
+
assert.ok(asked >= 1); assert.ok(Date.now() - t0 < 3000, "killed at the idle timeout, not the busy window");
|
|
3458
|
+
});
|
|
3459
|
+
|
|
3460
|
+
it("#4405 the builder call wires busy (heavy children or this session's heavy-queue entries) and the kill event carries heavyQueue", async () => {
|
|
3461
|
+
const w = world({ dirty: true }); const dir = tmpDir("core-busy-");
|
|
3462
|
+
const lanes = [];
|
|
3463
|
+
w.io.heavy = { acquire: async () => ({ waitMs: 0, queued: 0, release() {} }), lane: (pids) => { lanes.push(pids); return pids.includes(42) ? { queued: 1, running: 0 } : { queued: 0, running: 0 }; } };
|
|
3464
|
+
const orig = w.io.runner; let n = 0, seen = null;
|
|
3465
|
+
w.io.runner = async (c) => {
|
|
3466
|
+
if (c.role === "builder" && n++ === 1) {
|
|
3467
|
+
seen = c;
|
|
3468
|
+
c.onIdleExtend(c.busy([{ pid: 42, cmd: "node heavy.mjs run --name npm -- npm test" }]));
|
|
3469
|
+
throw Object.assign(new Error("idle"), { code: "CLI_IDLE", lastTool: null, children: [], extended: true, heavyQueue: { queued: 1, running: 0 } });
|
|
3470
|
+
}
|
|
3471
|
+
return orig(c);
|
|
3472
|
+
};
|
|
3473
|
+
const r = await run(w, { dir, cliBusyMinutes: 30 });
|
|
3474
|
+
assert.equal(r.terminalReason, "merged");
|
|
3475
|
+
assert.equal(seen.busyMs, 30 * 60000);
|
|
3476
|
+
assert.equal(seen.busy([{ pid: 5, cmd: "sleep 10" }]), null, "a poller alone is not activity");
|
|
3477
|
+
assert.deepEqual(seen.busy([{ pid: 42, cmd: "sleep 10" }]), { children: [], heavyQueue: { queued: 1, running: 0 } }, "this session's queued heavy entry is activity");
|
|
3478
|
+
assert.equal(seen.busy([{ pid: 6, cmd: "/bin/sh /x/vitest run" }]).children.length, 1, "a test runner child is activity");
|
|
3479
|
+
assert.deepEqual(lanes.at(-1), [6]);
|
|
3480
|
+
const log = readLog(dir, 7);
|
|
3481
|
+
assert.deepEqual(log.find((e) => e.event === "CLI_IDLE_EXTENDED").heavyQueue, { queued: 1, running: 0 });
|
|
3482
|
+
const kill = log.find((e) => e.event === "CLI_IDLE_KILL");
|
|
3483
|
+
assert.deepEqual(kill.heavyQueue, { queued: 1, running: 0 }); assert.equal(kill.extended, true);
|
|
3484
|
+
});
|
|
3485
|
+
|
|
3068
3486
|
it("#4112 a rerun resumes from the checkpoint branch and tells the builder to continue", async () => {
|
|
3069
3487
|
const w = world({ prList: [], remote: "abc\trefs/heads/feat/issue-7-lean", lastSubject: "wip(forge): checkpoint after idle timeout" });
|
|
3070
3488
|
const r = await run(w);
|
|
@@ -3350,7 +3768,7 @@ describe("verify-by-CI (#4167): ci mode runs the heavy checks on CI and feeds fa
|
|
|
3350
3768
|
const r = await run(w, { verify: CI });
|
|
3351
3769
|
assert.equal(r.terminalReason, "merged");
|
|
3352
3770
|
const fix = builders(w).find((b) => /CI failed on the pushed branch/.test(b[4]));
|
|
3353
|
-
assert.ok(fix, "a CI-fix
|
|
3771
|
+
assert.ok(fix, "a CI-fix round ran"); assert.equal(fix[2], null);
|
|
3354
3772
|
assert.match(fix[4], /poetry\.lock is not up to date/); assert.match(fix[4], /actions\/runs\/100/);
|
|
3355
3773
|
assert.deepEqual(w.ci.logsFor, [100]);
|
|
3356
3774
|
assert.equal(w.ci.dispatched.length, 2, "the fixed head is verified again");
|
|
@@ -3405,8 +3823,47 @@ describe("verify-by-CI (#4167): ci mode runs the heavy checks on CI and feeds fa
|
|
|
3405
3823
|
const d = tmpDir("verify-yaml-");
|
|
3406
3824
|
writeFileSync(`${d}/forge.yaml`, "engine:\n conformance: shadow\n");
|
|
3407
3825
|
assert.equal(readVerify(d), undefined);
|
|
3408
|
-
writeFileSync(`${d}/forge.yaml`, "verify:\n mode: ci\n workflow: verify.yml\n skip_paths: \"*.md,docs/**\"\n max_rounds: 3\n precheck_timeout_seconds: 60\n");
|
|
3409
|
-
|
|
3826
|
+
writeFileSync(`${d}/forge.yaml`, "verify:\n mode: ci\n workflow: verify.yml\n skip_paths: \"*.md,docs/**\"\n max_rounds: 3\n precheck_timeout_seconds: 60\n e2e_label: full-ci\n e2e_checks: \"e2e (1),e2e (2)\"\n");
|
|
3827
|
+
const e2e = { mode: "auto", label: "full-ci", workflow: null, pathsInput: null, checks: ["e2e (1)", "e2e (2)"], paths: ["e2e/**", "**/e2e/**", "*.e2e.*", "*.e2e-spec.*", "cypress/**", "playwright/**"], blocking: "changed" };
|
|
3828
|
+
assert.deepEqual(readVerify(d), { mode: "ci", workflow: "verify.yml", pathsInput: null, skipPaths: ["*.md", "docs/**"], maxRounds: 3, precheckTimeoutSeconds: 60, precheckTotalSeconds: 600, checkRun: false, checks: null, postMerge: null, duplicateExports: true, e2e });
|
|
3829
|
+
});
|
|
3830
|
+
|
|
3831
|
+
describe("#4387 a no-commit CI fix round reruns the failed jobs once", () => {
|
|
3832
|
+
const PUSH = { mode: "ci", skipPaths: ["*.md"], maxRounds: 2 };
|
|
3833
|
+
const RUN = "https://github.com/o/r/actions/runs/77/job/1";
|
|
3834
|
+
/** Commit checks: `test` red (id 1) until a rerun, then `after` (id 2); a non-required storybook stays red; the builder never commits a CI fix. */
|
|
3835
|
+
const flaky = (after) => {
|
|
3836
|
+
const w = world({ validation: ciValidation });
|
|
3837
|
+
w.reran = [];
|
|
3838
|
+
w.io.requiredChecks = async () => ["test"];
|
|
3839
|
+
w.io.commitChecks = async () => [{ name: "storybook (web)", state: "failure", id: 3, url: "https://github.com/o/r/actions/runs/78/job/1" },
|
|
3840
|
+
w.reran.length ? { name: "test", state: after, id: 4, url: RUN } : { name: "test", state: "failure", id: 1, url: RUN }];
|
|
3841
|
+
w.io.failedLogs = async () => "expected 26, got 25";
|
|
3842
|
+
w.io.rerunFailedJobs = async (id) => { w.reran.push(id); };
|
|
3843
|
+
w.io.checkStates = async () => [{ name: "test", state: after, url: RUN, head: w.head }];
|
|
3844
|
+
const inner = w.io.runner;
|
|
3845
|
+
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; };
|
|
3846
|
+
return w;
|
|
3847
|
+
};
|
|
3848
|
+
const flakeEvents = (dir) => readLog(dir, 7).filter((e) => e.event === "CI_FLAKE_RERUN");
|
|
3849
|
+
|
|
3850
|
+
it("green after the rerun: the lane continues to review and merges; CI_FLAKE_RERUN is logged", async () => {
|
|
3851
|
+
const w = flaky("success"), dir = tmpDir("core-ci-");
|
|
3852
|
+
const r = await run(w, { verify: PUSH, dir });
|
|
3853
|
+
assert.equal(r.terminalReason, "merged");
|
|
3854
|
+
assert.deepEqual(w.reran, ["77"], "only the required check's run is rerun, once");
|
|
3855
|
+
assert.ok(reviewers(w).length >= 1);
|
|
3856
|
+
assert.deepEqual(flakeEvents(dir).map((e) => [e.result, e.jobs]), [["pass", ["test"]]]);
|
|
3857
|
+
assert.match(traj(w)[0], /after a rerun of the failed jobs/);
|
|
3858
|
+
});
|
|
3859
|
+
|
|
3860
|
+
it("still red after the rerun: ci.red as before, with CI_FLAKE_RERUN in the run log", async () => {
|
|
3861
|
+
const w = flaky("failure"), dir = tmpDir("core-ci-");
|
|
3862
|
+
const r = await run(w, { verify: PUSH, dir });
|
|
3863
|
+
assert.equal(r.terminalReason, "needs-human"); assert.equal(r.code, "ci.red");
|
|
3864
|
+
assert.deepEqual(w.reran, ["77"]); assert.equal(w.merges.length, 0);
|
|
3865
|
+
assert.deepEqual(flakeEvents(dir).map((e) => e.result), ["fail"]);
|
|
3866
|
+
});
|
|
3410
3867
|
});
|
|
3411
3868
|
});
|
|
3412
3869
|
|
|
@@ -3420,7 +3877,7 @@ describe("lane resource ledger + teardown on every exit path (#4179)", () => {
|
|
|
3420
3877
|
w.io.docker = async (a) => { w.docker.push(a); return ""; };
|
|
3421
3878
|
w.io.killGroup = async (pid, marker) => { w.killed.push([pid, marker]); return true; };
|
|
3422
3879
|
w.io.runTmp = { prune: async () => {}, path: (id) => `/home/x/.forge/tmp/runs/${id}`, make: async (_i, id) => `/home/x/.forge/tmp/runs/${id}`, remove: async (d) => { w.tmpRemoved.push(d); return true; } };
|
|
3423
|
-
w.ledgerFile = join(root, o.batch ?? "adhoc", "
|
|
3880
|
+
w.ledgerFile = join(root, o.batch ?? "adhoc", "o__r__lean-7.json"); // #4432: repo-scoped lane file
|
|
3424
3881
|
const r0 = w.io.runner;
|
|
3425
3882
|
w.io.runner = async (c) => {
|
|
3426
3883
|
if (c.role === "builder") {
|
|
@@ -3554,6 +4011,35 @@ describe("lane resource ledger + teardown on every exit path (#4179)", () => {
|
|
|
3554
4011
|
});
|
|
3555
4012
|
});
|
|
3556
4013
|
|
|
4014
|
+
describe("two repos with the same issue number on one host (#4432)", () => {
|
|
4015
|
+
/** A lane of `repo` whose ledger lives under the shared `root` (paths recorded) and whose FORGE:STATE holds `lease`. */
|
|
4016
|
+
const lane = (root, lease) => {
|
|
4017
|
+
const w = world(); w.ledgerPaths = []; w.leaseWrites = [];
|
|
4018
|
+
w.io.ledger = (opts) => { const l = makeLedger({ root, ...opts }); w.ledgerPaths.push(l.path); return l; };
|
|
4019
|
+
w.io.projector = { readState: async () => (lease ? { v: 1, lease } : null), writeState: async (_i, st) => { w.leaseWrites.push(st); } };
|
|
4020
|
+
return w;
|
|
4021
|
+
};
|
|
4022
|
+
|
|
4023
|
+
it("separate run logs and ledger files; a live lease in one repo never defers the other", async () => {
|
|
4024
|
+
const dir = tmpDir("core-shared-"), root = tmpDir("ledger-shared-");
|
|
4025
|
+
const held = lane(root, { by: "core_9999", until: 1e15, host: "other-host", pid: 9999 });
|
|
4026
|
+
const r0 = await run(held, { repo: "acme/one", dir, hostName: "h1" });
|
|
4027
|
+
assert.equal(r0.terminalReason, "deferred");
|
|
4028
|
+
assert.equal(r0.code, "lease-held");
|
|
4029
|
+
const two = lane(root, null);
|
|
4030
|
+
assert.equal((await run(two, { repo: "acme/two", dir, hostName: "h1" })).terminalReason, "merged", "repo one's lease is not repo two's");
|
|
4031
|
+
const one = lane(root, null);
|
|
4032
|
+
assert.equal((await run(one, { repo: "acme/one", dir, hostName: "h1" })).terminalReason, "merged");
|
|
4033
|
+
assert.deepEqual(readdirSync(dir).filter((f) => f.endsWith(".jsonl")).sort(), ["acme__one__7.jsonl", "acme__two__7.jsonl"]);
|
|
4034
|
+
for (const repo of ["acme/one", "acme/two"]) {
|
|
4035
|
+
const log = readLog(dir, 7, repo);
|
|
4036
|
+
assert.deepEqual(log.filter((e) => e.event === "RUN_START").map((e) => e.repo), [repo]);
|
|
4037
|
+
assert.equal(log.filter((e) => e.event === "RUN_TERMINAL").length, 1);
|
|
4038
|
+
}
|
|
4039
|
+
assert.deepEqual([one.ledgerPaths, two.ledgerPaths], [[join(root, "adhoc", "acme__one__lean-7.json")], [join(root, "adhoc", "acme__two__lean-7.json")]]);
|
|
4040
|
+
});
|
|
4041
|
+
});
|
|
4042
|
+
|
|
3557
4043
|
describe("host-aware verification concurrency (#4182)", () => {
|
|
3558
4044
|
const host = (o = {}) => ({ memAvailableMb: () => 8192, freeMb: async () => 8192, loadAvg1: () => o.load ?? 1, cores: () => o.cores ?? 4, activeLanes: o.activeLanes ?? (() => o.lanes ?? 2), tmpPaths: () => ["/tmp"] });
|
|
3559
4045
|
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;
|
|
@@ -3618,7 +4104,7 @@ describe("shared dependency cache wiring (#4220)", () => {
|
|
|
3618
4104
|
const d = { calls: [] };
|
|
3619
4105
|
w.io.deps = {
|
|
3620
4106
|
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" }; },
|
|
3621
|
-
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") });
|
|
4107
|
+
prepare: async (a) => { d.calls.push({ ...a, toolchainCalled: !!w.toolchainCalled, runnerCallsBefore: w.calls.filter((c) => c[0] === "runner").length, worktreeAdded: w.calls.some((c) => c[1] === "worktree" && c[2] === "add") });
|
|
3622
4108
|
if (o.prepareThrows) throw new Error("disk on fire");
|
|
3623
4109
|
return o.results ?? [{ status: "hit", eco: "pnpm", key: "0123456789abcdef", ms: 1200, reason: null }]; },
|
|
3624
4110
|
};
|
|
@@ -3661,6 +4147,24 @@ describe("shared dependency cache wiring (#4220)", () => {
|
|
|
3661
4147
|
for (const e of w.envs) assert.equal(e.UV_CACHE_DIR, undefined);
|
|
3662
4148
|
});
|
|
3663
4149
|
|
|
4150
|
+
it("#4347 prepare runs after the toolchain step with its env; a failed or shimless toolchain step leaves the host PATH and the run merges", async () => {
|
|
4151
|
+
const cases = [
|
|
4152
|
+
[async () => { throw new Error("toolchain boom"); }, undefined],
|
|
4153
|
+
[async () => ({ env: null, summary: "pnpm@9.12.0" }), undefined],
|
|
4154
|
+
[async () => ({ env: { PATH: "/shim/bin:/host/bin", COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" }, summary: "pnpm@9.12.0" }), "/shim/bin:/host/bin"],
|
|
4155
|
+
];
|
|
4156
|
+
for (const [tc, path] of cases) {
|
|
4157
|
+
const dir = tmpDir("deps-tc-");
|
|
4158
|
+
const w = world(); const d = depsIo(w);
|
|
4159
|
+
w.io.prepareToolchain = async (a) => { w.toolchainCalled = true; return tc(a); };
|
|
4160
|
+
assert.equal((await run(w, { dir })).terminalReason, "merged");
|
|
4161
|
+
assert.equal(d.calls.length, 1);
|
|
4162
|
+
assert.equal(d.calls[0].toolchainCalled, true, "toolchain prepared before dependency prep");
|
|
4163
|
+
assert.equal(d.calls[0].env?.PATH, path);
|
|
4164
|
+
if (!path) assert.equal(d.calls[0].env, undefined);
|
|
4165
|
+
}
|
|
4166
|
+
});
|
|
4167
|
+
|
|
3664
4168
|
it("#4220 both builder prompts tell the builder how to treat a shared read-only node_modules/.venv (within budget)", () => {
|
|
3665
4169
|
for (const p of [builderPlanPrompt(7), builderImplementPrompt()]) {
|
|
3666
4170
|
for (const needle of ["hardlinked read-only from a shared cache", "rm -rf", "EACCES"]) assert.ok(p.includes(needle), needle);
|
|
@@ -3687,7 +4191,7 @@ describe("local-mode CI fix rounds at merge and declared pre-PR check commands (
|
|
|
3687
4191
|
const r = await run(w, { dir });
|
|
3688
4192
|
assert.equal(r.terminalReason, "merged");
|
|
3689
4193
|
const fix = ciFixes(w);
|
|
3690
|
-
assert.equal(fix.length, 1); assert.equal(fix[0][2],
|
|
4194
|
+
assert.equal(fix.length, 1); assert.equal(fix[0][2], null);
|
|
3691
4195
|
assert.match(fix[0][4], /mkdtempSync, not tmpDir/); assert.match(fix[0][4], /actions\/runs\/555/);
|
|
3692
4196
|
assert.deepEqual(w.logsFor, ["555"]);
|
|
3693
4197
|
assert.equal(reviewers(w).length, 2, "the fixed head is reviewed again before merge");
|
|
@@ -3714,7 +4218,7 @@ describe("local-mode CI fix rounds at merge and declared pre-PR check commands (
|
|
|
3714
4218
|
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; };
|
|
3715
4219
|
const r = await run(w, { verify: { maxRounds: 5 } });
|
|
3716
4220
|
assert.equal(r.terminalReason, "needs-human"); assert.equal(r.code, "ci.red");
|
|
3717
|
-
assert.equal(ciFixes(w).length,
|
|
4221
|
+
assert.equal(ciFixes(w).length, 2, "#4363: fresh, then the one fallback resume, in one round"); assert.equal(w.merges.length, 0);
|
|
3718
4222
|
});
|
|
3719
4223
|
|
|
3720
4224
|
it("a rollup still reporting the pre-fix head counts as pending, not as a second failure", async () => {
|
|
@@ -3741,8 +4245,8 @@ describe("local-mode CI fix rounds at merge and declared pre-PR check commands (
|
|
|
3741
4245
|
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]]);
|
|
3742
4246
|
assert.equal(ran[0][2], 300000);
|
|
3743
4247
|
const fix = builders(w).filter((b) => /declared check commands failed/.test(b[4]));
|
|
3744
|
-
assert.equal(fix.length, 1); assert.equal(fix[0][2],
|
|
3745
|
-
assert.ok(w.seq.indexOf("builder
|
|
4248
|
+
assert.equal(fix.length, 1); assert.equal(fix[0][2], null); assert.match(fix[0][4], /mkdtempSync in x\.test\.mjs/); assert.match(fix[0][4], /exit 1/);
|
|
4249
|
+
assert.ok(w.seq.indexOf("builder", w.seq.indexOf("check:1")) < w.seq.indexOf("pr"), "the fix runs before the PR is opened");
|
|
3746
4250
|
const log = readLog(dir, 7);
|
|
3747
4251
|
const pre = log.filter((e) => e.event === "PRECHECK_COMMANDS");
|
|
3748
4252
|
assert.deepEqual(pre.map((e) => e.results), [[{ command: "bash scripts/check-test-tmp-cleanup.sh", rc: 1, ms: 0 }], [{ command: "bash scripts/check-test-tmp-cleanup.sh", rc: 0, ms: 0 }]]);
|
|
@@ -3765,7 +4269,7 @@ describe("local-mode CI fix rounds at merge and declared pre-PR check commands (
|
|
|
3765
4269
|
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; };
|
|
3766
4270
|
const r = await run(w, { precheck: { commands: ["make lint"] }, verify: { maxRounds: 3 } });
|
|
3767
4271
|
assert.equal(r.terminalReason, "merged");
|
|
3768
|
-
assert.equal(builders(w).filter((b) => /declared check commands failed/.test(b[4])).length,
|
|
4272
|
+
assert.equal(builders(w).filter((b) => /declared check commands failed/.test(b[4])).length, 2, "#4363: fresh, then one fallback resume");
|
|
3769
4273
|
assert.match(traj(w)[0], /builder made no new commit for them/);
|
|
3770
4274
|
});
|
|
3771
4275
|
|
|
@@ -3780,6 +4284,39 @@ describe("local-mode CI fix rounds at merge and declared pre-PR check commands (
|
|
|
3780
4284
|
});
|
|
3781
4285
|
});
|
|
3782
4286
|
|
|
4287
|
+
describe("#4364 in-session forgedock check: plan hand-off and self-check record", () => {
|
|
4288
|
+
const SELF = `${WT}/.forge-lean/self-check.json`;
|
|
4289
|
+
const fp = (w) => worktreeFingerprint({ git: (a) => w.io.git(a, { cwd: WT }), base: "origin/staging" });
|
|
4290
|
+
|
|
4291
|
+
it("writes check-plan.json beside the contract and the implement prompt asks for forgedock check", async () => {
|
|
4292
|
+
const w = world(); w.io.runCommand = async () => ({ rc: 0, output: "" });
|
|
4293
|
+
assert.equal((await run(w, { precheck: { commands: [" make lint ", ""], maxMinutes: 5 }, verify: { mode: "hybrid" } })).terminalReason, "merged");
|
|
4294
|
+
assert.deepEqual(JSON.parse(w.files[`${WT}/.forge-lean/check-plan.json`]), { v: 1, base: "origin/staging", mode: "hybrid", learned: ["make lint"], maxMinutes: 5, precheckTotalSeconds: 600, precheckTimeoutSeconds: 300 });
|
|
4295
|
+
assert.match(builders(w)[1][4], /forgedock\.mjs check` in the worktree[\s\S]*report the problem as blocked/);
|
|
4296
|
+
});
|
|
4297
|
+
|
|
4298
|
+
it("fixed without a forgedock check run logs SELF_CHECK_MISSING skipped and still merges (not a failure)", async () => {
|
|
4299
|
+
const w = world(); const dir = tmpDir("core-");
|
|
4300
|
+
assert.equal((await run(w, { dir })).terminalReason, "merged");
|
|
4301
|
+
assert.deepEqual(readLog(dir, 7).filter((e) => e.event === "SELF_CHECK_MISSING").map((e) => e.state), ["skipped"]);
|
|
4302
|
+
});
|
|
4303
|
+
|
|
4304
|
+
it("a green run on the final tree logs nothing; a record of another tree is stale on SELF_CHECK_MISSING and a failed pre-PR round", async () => {
|
|
4305
|
+
const w = world(); const dir = tmpDir("core-");
|
|
4306
|
+
w.files[SELF] = JSON.stringify({ v: 1, fingerprint: await fp(w), ok: true });
|
|
4307
|
+
assert.equal((await run(w, { dir })).terminalReason, "merged");
|
|
4308
|
+
assert.equal(readLog(dir, 7).filter((e) => e.event === "SELF_CHECK_MISSING").length, 0);
|
|
4309
|
+
const w2 = world(); const dir2 = tmpDir("core-");
|
|
4310
|
+
w2.files[SELF] = JSON.stringify({ v: 1, fingerprint: "other-tree", ok: true });
|
|
4311
|
+
let rc = 1; w2.io.runCommand = async () => ({ rc, output: "boom" });
|
|
4312
|
+
const inner = w2.io.runner; w2.io.runner = async (c) => { if (/declared check commands failed/.test(c.prompt)) rc = 0; return inner(c); };
|
|
4313
|
+
assert.equal((await run(w2, { dir: dir2, precheck: { commands: ["make lint"] } })).terminalReason, "merged");
|
|
4314
|
+
const log = readLog(dir2, 7);
|
|
4315
|
+
assert.deepEqual(log.filter((e) => e.event === "SELF_CHECK_MISSING").map((e) => e.state), ["stale"]);
|
|
4316
|
+
assert.deepEqual(log.filter((e) => e.event === "PRECHECK_COMMANDS").map((e) => [e.results[0].rc, e.self_check]), [[1, "stale"], [0, undefined]]);
|
|
4317
|
+
});
|
|
4318
|
+
});
|
|
4319
|
+
|
|
3783
4320
|
describe("test map and repo facts caches (#4272)", () => {
|
|
3784
4321
|
const MAPF = "origin/staging:.forge/test-map.json", FACTSF = `${WT}/.forge/repo-facts.json`;
|
|
3785
4322
|
const map = (entries) => JSON.stringify({ version: 1, entries });
|
|
@@ -4282,6 +4819,22 @@ describe("context pack modes (forge#4275)", () => {
|
|
|
4282
4819
|
assert.deepEqual([m.leaseWrites.at(-1).terminalCode, m.leaseWrites.at(-1).failureClasses], [OUTCOME_CODES.COMPLETED, []]);
|
|
4283
4820
|
});
|
|
4284
4821
|
|
|
4822
|
+
it("#4444 ab: the lane runs its issue's arm, logs it as context_pack and tags context_ab", async () => {
|
|
4823
|
+
const { contextArm } = await import("./context.mjs");
|
|
4824
|
+
const arm = contextArm({ mode: "ab", repo: "o/r", issue: 7 }).mode;
|
|
4825
|
+
const w = withPack(), dir = tmpDir("core-");
|
|
4826
|
+
const r = await run(w, { dir, contextPack: { mode: "ab" } });
|
|
4827
|
+
assert.equal(r.terminalReason, "merged");
|
|
4828
|
+
const plan = builders(w)[0][4];
|
|
4829
|
+
assert.equal(/Context pack \(hints, not proof/.test(plan), arm === "on");
|
|
4830
|
+
const log = readLog(dir, 7);
|
|
4831
|
+
const cp = log.find((e) => e.event === "CONTEXT_PACK"), term = log.find((e) => e.event === "RUN_TERMINAL");
|
|
4832
|
+
assert.deepEqual([cp.mode, cp.context_ab, term.context_pack, term.context_ab], [arm, true, arm, true]);
|
|
4833
|
+
const plain = withPack(), d2 = tmpDir("core-");
|
|
4834
|
+
await run(plain, { dir: d2, contextPack: { mode: "shadow" } });
|
|
4835
|
+
assert.equal("context_ab" in readLog(d2, 7).find((e) => e.event === "RUN_TERMINAL"), false);
|
|
4836
|
+
});
|
|
4837
|
+
|
|
4285
4838
|
it("forge.yaml engine.context_pack* is read flat; invalid mode is ignored", () => {
|
|
4286
4839
|
const d = tmpDir("ctxcfg-");
|
|
4287
4840
|
writeFileSync(join(d, "forge.yaml"), "engine:\n context_pack: on\n context_pack_sources: graph,attempts\n context_pack_bytes: 4096\n");
|
|
@@ -4367,6 +4920,29 @@ describe("enumerating tests + sequence numbers at hand-off (#4322)", () => {
|
|
|
4367
4920
|
return w;
|
|
4368
4921
|
};
|
|
4369
4922
|
|
|
4923
|
+
const DUP = [{ symbol: "undoActivityAction", path: "apps/web/feed/actions.ts", others: ["apps/web/inbox/actions.ts"] }];
|
|
4924
|
+
it("#4406 a lane export that another module of the same app declares resumes the builder with both paths", async () => {
|
|
4925
|
+
const w = hworld(); const d = [DUP, []]; w.dupCalls = 0;
|
|
4926
|
+
w.io.duplicateExports = async (x) => { w.dupCalls++; w.dupArgs = x; return d.length > 1 ? d.shift() : d[0]; };
|
|
4927
|
+
assert.equal((await run(w)).terminalReason, "merged");
|
|
4928
|
+
const b = builders(w);
|
|
4929
|
+
assert.equal(b.length, 3);
|
|
4930
|
+
assert.match(b[2][4], /undoActivityAction: apps\/web\/feed\/actions\.ts \(also exported by apps\/web\/inbox\/actions\.ts\)[\s\S]*Rename the new export or reuse the existing one/);
|
|
4931
|
+
assert.ok(w.seq.indexOf("pr") > w.seq.lastIndexOf("builder-resume"), "fixed before the PR");
|
|
4932
|
+
assert.equal(w.dupCalls, 2);
|
|
4933
|
+
assert.equal(w.dupArgs.cwd, WT);
|
|
4934
|
+
});
|
|
4935
|
+
|
|
4936
|
+
it("#4406 a duplicate that remains after the round is an anomaly; verify.duplicate_exports false skips the check", async () => {
|
|
4937
|
+
let w = hworld(); w.io.duplicateExports = async () => DUP;
|
|
4938
|
+
assert.equal((await run(w)).terminalReason, "merged");
|
|
4939
|
+
assert.match(traj(w)[0], /Duplicate export\(s\) remain \(hand-off\): undoActivityAction in apps\/web\/feed\/actions\.ts/);
|
|
4940
|
+
w = hworld(); let called = 0; w.io.duplicateExports = async () => { called++; return DUP; };
|
|
4941
|
+
await run(w, { verify: { duplicateExports: false } });
|
|
4942
|
+
assert.equal(called, 0);
|
|
4943
|
+
assert.equal(builders(w).length, 2);
|
|
4944
|
+
});
|
|
4945
|
+
|
|
4370
4946
|
it("no enumerating tests and no collisions: no new plan step, no extra builder call (prompts unchanged)", async () => {
|
|
4371
4947
|
const w = hworld();
|
|
4372
4948
|
assert.equal((await run(w)).terminalReason, "merged");
|
|
@@ -4421,4 +4997,1017 @@ describe("enumerating tests + sequence numbers at hand-off (#4322)", () => {
|
|
|
4421
4997
|
assert.equal((await run(w)).terminalReason, "merged");
|
|
4422
4998
|
assert.match(builders(w)[2][4], /conflicts in: db\/meta\/_journal\.json[\s\S]*do not text-merge it\. Renumber the lane's files/);
|
|
4423
4999
|
});
|
|
5000
|
+
|
|
5001
|
+
describe("#4386 deterministic renumber, base-sync and merge-gate late binding", () => {
|
|
5002
|
+
const MOVES = [{ from: "db/0007_usage.sql", to: "db/0008_usage.sql" }];
|
|
5003
|
+
const syncs = (w) => w.calls.filter((c) => c[0] === "git" && c[1] === "merge" && c[2] === "--no-edit").length;
|
|
5004
|
+
/** `det`: sequenceRenumber results per call; `resolve`: sequenceResolve result; `mine`: sequenceSnapshot (the lane's numbered files). */
|
|
5005
|
+
const dworld = ({ det = [{ ok: true, moves: MOVES, commit: "d".repeat(40) }], resolve = { ok: true }, mine = [{ path: "db/0007_usage.sql", blob: "b1" }], ...o } = {}) => {
|
|
5006
|
+
const w = hworld(o), d = [...det];
|
|
5007
|
+
w.renumbers = []; w.leases = { acquired: [], released: 0 };
|
|
5008
|
+
w.io.sequenceRenumber = async (x) => { w.renumbers.push(x); w.head = `dddd${w.calls.length}`.padEnd(16, "0"); return d.length > 1 ? d.shift() : d[0]; };
|
|
5009
|
+
w.io.sequenceResolve = async (x) => { w.resolveArgs = x; w.conflictFiles = []; w.mergeInProgress = false; return resolve; };
|
|
5010
|
+
w.io.sequenceSnapshot = async () => mine;
|
|
5011
|
+
w.io.sequenceLease = { acquire: async (x) => { w.leases.acquired.push(x); return { waitMs: 0, timedOut: false, release: () => { w.leases.released++; } }; } };
|
|
5012
|
+
return w;
|
|
5013
|
+
};
|
|
5014
|
+
|
|
5015
|
+
it("hand-off: a taken number is renumbered by the engine; no builder session, the trajectory says so", async () => {
|
|
5016
|
+
const w = dworld({ seq: [COLL, []] });
|
|
5017
|
+
assert.equal((await run(w)).terminalReason, "merged");
|
|
5018
|
+
assert.equal(builders(w).length, 2, "plan + implement only");
|
|
5019
|
+
assert.equal(w.renumbers.length, 1);
|
|
5020
|
+
assert.deepEqual(w.verifyArgs.moves, MOVES);
|
|
5021
|
+
assert.match(traj(w).at(-1), /renumbered deterministically \(no builder\): db\/0007_usage\.sql → db\/0008_usage\.sql/);
|
|
5022
|
+
});
|
|
5023
|
+
|
|
5024
|
+
it("base-sync with conflicts only in sequence files: base side taken and renumbered by the engine, zero builder sessions, zero re-reviews", async () => {
|
|
5025
|
+
const w = dworld({ seq: [[], COLL, []], prView: [CONFLICTING, OKSTATE], mergeResults: ["conflict"], conflictFiles: ["db/meta/_journal.json", "db/meta/0007_snapshot.json"] });
|
|
5026
|
+
assert.equal((await run(w)).terminalReason, "merged");
|
|
5027
|
+
assert.equal(builders(w).length, 2, "no builder for the conflict or the renumber");
|
|
5028
|
+
assert.equal(reviewers(w).length, 1, "no re-review");
|
|
5029
|
+
assert.deepEqual(w.resolveArgs.files, ["db/meta/_journal.json", "db/meta/0007_snapshot.json"]);
|
|
5030
|
+
assert.ok(w.renumbers[0].laneRef, "renumbered from the pre-merge lane head");
|
|
5031
|
+
assert.match(w.comments.find((c) => c.includes("<!-- FORGE:BASE_SYNC -->")), /deterministic: base side taken[\s\S]*renumbered deterministically[\s\S]*not required \(deterministic renumber \+ base merge\)/);
|
|
5032
|
+
});
|
|
5033
|
+
|
|
5034
|
+
it("a schema-changing migration without lean.sequence.regenerate falls back to the builder renumber and the trajectory records why", async () => {
|
|
5035
|
+
const w = dworld({ seq: [COLL, []], det: [{ unsupported: "schema-changing migration db/0007_usage.sql needs lean.sequence.regenerate (its snapshot cannot be cloned)" }] });
|
|
5036
|
+
assert.equal((await run(w, { sequenceRegenerate: null })).terminalReason, "merged");
|
|
5037
|
+
assert.equal(builders(w).length, 3, "the builder renumbers");
|
|
5038
|
+
assert.match(builders(w)[2][4], /already uses sequence numbers/);
|
|
5039
|
+
assert.match(traj(w).at(-1), /Deterministic renumber not possible \(schema-changing migration db\/0007_usage\.sql needs lean\.sequence\.regenerate/);
|
|
5040
|
+
assert.equal(w.renumbers[0].regenerate, null);
|
|
5041
|
+
});
|
|
5042
|
+
|
|
5043
|
+
it("syncs overtaken by a new base head do not count toward MAX_SYNCS: overtaken 3 times still merges; 2 syncs against one base head is base.conflict", async () => {
|
|
5044
|
+
let w = dworld({ prView: [CONFLICTING, CONFLICTING, CONFLICTING, CONFLICTING, CONFLICTING, OKSTATE], mergeResults: ["clean", "clean", "clean"], baseHeads: ["base0", "base1", "base2", "base3"], mine: [] }); // the loop re-reads mergeability after each sync
|
|
5045
|
+
assert.equal((await run(w)).terminalReason, "merged");
|
|
5046
|
+
assert.equal(syncs(w), 3);
|
|
5047
|
+
w = dworld({ prView: [CONFLICTING], mergeResults: ["clean", "clean", "clean"], baseHeads: ["base1"], mine: [] });
|
|
5048
|
+
const r = await run(w);
|
|
5049
|
+
assert.equal(r.code, OUTCOME_CODES.BASE_CONFLICT);
|
|
5050
|
+
assert.equal(syncs(w), 2);
|
|
5051
|
+
});
|
|
5052
|
+
|
|
5053
|
+
it("merge gate: under the sequence lease a number taken since review is renumbered, CI waited again, merged; a lane without numbered files never takes the lease", async () => {
|
|
5054
|
+
let w = dworld({ seq: [[], COLL, COLL, []] }); // hand-off clean; the late check and the gate's base-sync see the taken number
|
|
5055
|
+
assert.equal((await run(w)).terminalReason, "merged");
|
|
5056
|
+
assert.equal(w.leases.acquired.length, 1);
|
|
5057
|
+
assert.deepEqual(w.leases.acquired[0].dirs, ["db"]);
|
|
5058
|
+
assert.equal(w.leases.acquired[0].lane, "staging");
|
|
5059
|
+
assert.equal(w.renumbers.length, 1, "late renumber at the gate");
|
|
5060
|
+
assert.equal(builders(w).length, 2);
|
|
5061
|
+
assert.equal(reviewers(w).length, 1, "no re-review for the late renumber");
|
|
5062
|
+
assert.match(w.comments.find((c) => c.includes("<!-- FORGE:BASE_SYNC -->")), /merge gate: sequence number taken/);
|
|
5063
|
+
assert.ok(w.leases.released >= 1);
|
|
5064
|
+
assert.deepEqual(w.merges[0].slice(-2), ["--match-head-commit", w.head]);
|
|
5065
|
+
w = dworld({ mine: [] });
|
|
5066
|
+
assert.equal((await run(w)).terminalReason, "merged");
|
|
5067
|
+
assert.equal(w.leases.acquired.length, 0);
|
|
5068
|
+
});
|
|
5069
|
+
});
|
|
5070
|
+
});
|
|
5071
|
+
|
|
5072
|
+
describe("#4363 fresh fix sessions", () => {
|
|
5073
|
+
const FRESH = /no earlier session/;
|
|
5074
|
+
const prechecked = (o = {}) => {
|
|
5075
|
+
const w = world(o); let rc = 1;
|
|
5076
|
+
w.io.runCommand = async () => ({ rc, output: rc ? `${"x".repeat(300000)}\nlint: src/foo.js:3 unused var` : "" });
|
|
5077
|
+
const inner = w.io.runner;
|
|
5078
|
+
w.io.runner = async (c) => { const res = await inner(c); if (/declared check commands failed/.test(c.prompt) && !o.freshNoCommit) rc = 0; return res; };
|
|
5079
|
+
return w;
|
|
5080
|
+
};
|
|
5081
|
+
const usage = (dir) => readLog(dir, 7).filter((e) => e.event === "PHASE_ATTEMPT_USAGE" && e.phase === "implement");
|
|
5082
|
+
|
|
5083
|
+
it("a precheck failure runs a fresh session: no resume id, the reviewer model by default, a prompt under 60K with no transcript; plan/build keep the builder model", async () => {
|
|
5084
|
+
const w = prechecked(); const dir = tmpDir("core-");
|
|
5085
|
+
const r = await run(w, { dir, builderModel: "opus-b", builderRouting: { mode: "off" }, precheck: { commands: ["make lint"] } });
|
|
5086
|
+
assert.equal(r.terminalReason, "merged");
|
|
5087
|
+
const fix = builders(w).filter((b) => /declared check commands failed/.test(b[4]));
|
|
5088
|
+
assert.equal(fix.length, 1);
|
|
5089
|
+
assert.equal(fix[0][2], null, "no resume id"); assert.equal(fix[0][3], "claude-sonnet-5-5");
|
|
5090
|
+
assert.ok(fix[0][4].length < 60000, `prompt ${fix[0][4].length} chars`);
|
|
5091
|
+
for (const re of [FRESH, /## Issue: foo breaks/, /Frozen contract/, /src\/foo\.js:3 unused var/]) assert.ok(re.test(fix[0][4]), String(re));
|
|
5092
|
+
assert.ok(!/PLANNING ONLY|The engine froze/.test(fix[0][4]), "no plan or build prompt text");
|
|
5093
|
+
for (const b of builders(w).filter((b) => !FRESH.test(b[4]))) assert.equal(b[3], "opus-b", "plan and build keep the builder model");
|
|
5094
|
+
const u = usage(dir);
|
|
5095
|
+
assert.deepEqual(u.map((e) => e.mode ?? null), [null, null, "fresh"]);
|
|
5096
|
+
});
|
|
5097
|
+
|
|
5098
|
+
it("lean.fix_model (fixModel) picks the fix-session model", async () => {
|
|
5099
|
+
const w = prechecked();
|
|
5100
|
+
await run(w, { builderModel: "opus-b", fixModel: "haiku-fix", builderRouting: { mode: "off" }, precheck: { commands: ["make lint"] } });
|
|
5101
|
+
assert.deepEqual(builders(w).filter((b) => FRESH.test(b[4])).map((b) => b[3]), ["haiku-fix"]);
|
|
5102
|
+
});
|
|
5103
|
+
|
|
5104
|
+
it("a fresh session that makes no commit falls back to one resume of the build session in the same round; max_rounds is unchanged", async () => {
|
|
5105
|
+
const w = prechecked({ freshNoCommit: true }); const dir = tmpDir("core-");
|
|
5106
|
+
const inner = w.io.runner;
|
|
5107
|
+
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; };
|
|
5108
|
+
const r = await run(w, { dir, precheck: { commands: ["make lint"] }, verify: { maxRounds: 3 } });
|
|
5109
|
+
assert.equal(r.terminalReason, "merged");
|
|
5110
|
+
const fix = builders(w).filter((b) => /declared check commands failed/.test(b[4]));
|
|
5111
|
+
assert.deepEqual(fix.map((b) => b[2]), [null, "sess-1"], "fresh, then exactly one resume");
|
|
5112
|
+
assert.doesNotMatch(fix[1][4], FRESH, "the resume gets the plain task prompt");
|
|
5113
|
+
assert.deepEqual(usage(dir).map((e) => e.mode ?? null), [null, null, "fresh", "resume"]);
|
|
5114
|
+
assert.match(traj(w)[0], /Fresh precheck fix session made no commit; builder session resumed once/);
|
|
5115
|
+
assert.match(traj(w)[0], /builder made no new commit for them/);
|
|
5116
|
+
});
|
|
5117
|
+
|
|
5118
|
+
it("a conformance FAIL citing file:line is fresh; one without a file:line resumes; the one review retry is unchanged", async () => {
|
|
5119
|
+
const w1 = world({ reviews: [bad("src/foo.js:9 still wrong"), ok()] }); const d1 = tmpDir("core-");
|
|
5120
|
+
assert.equal((await run(w1, { dir: d1 })).terminalReason, "merged");
|
|
5121
|
+
assert.deepEqual(usage(d1).map((e) => e.mode ?? null), [null, null, "fresh"]);
|
|
5122
|
+
const vague = conf({ verdict: "NONCONFORMING", requirements: [{ id: "R1", status: "partial", evidence: "the foo module", validated_by: "V1", note: "null still throws" }] });
|
|
5123
|
+
const w2 = world({ reviews: [vague, ok()] }); const d2 = tmpDir("core-");
|
|
5124
|
+
assert.equal((await run(w2, { dir: d2 })).terminalReason, "merged");
|
|
5125
|
+
assert.equal(builders(w2)[2][2], "sess-1");
|
|
5126
|
+
assert.deepEqual(usage(d2).map((e) => e.mode ?? null), [null, null, "resume"]);
|
|
5127
|
+
const w3 = world({ reviews: [bad(), bad(), bad()] });
|
|
5128
|
+
const r3 = await run(w3);
|
|
5129
|
+
assert.equal(r3.terminalReason, "needs-human"); assert.equal(reviewers(w3).length, 2, "still one review retry");
|
|
5130
|
+
});
|
|
5131
|
+
});
|
|
5132
|
+
|
|
5133
|
+
describe("#4374 builder self-review against the shared rubric", () => {
|
|
5134
|
+
const SR = [1, 2, 3, 4].map((n) => ({ question: String(n), answer: "yes", evidence: "src/foo.js:3" }));
|
|
5135
|
+
const fixed = { status: "fixed", summary: "fixed foo", evidence: [], tests: ["test/foo.test.js"], self_review: SR };
|
|
5136
|
+
const selfEvents = (dir) => readLog(dir, 7).filter((e) => /^SELF_REVIEW/.test(e.event));
|
|
5137
|
+
|
|
5138
|
+
it("one rubric constant, verbatim in reviewerPrompt and in the implement prompt with the git diff self-check", () => {
|
|
5139
|
+
assert.ok(REVIEW_RUBRIC.length > 0);
|
|
5140
|
+
assert.ok(reviewerPrompt({ issue: { number: 7, title: "t", body: "b" }, contract: CONTRACT, diff: "" }).includes(`<rubric>\n${REVIEW_RUBRIC}\n</rubric>`));
|
|
5141
|
+
for (const mode of ["local", "ci", "hybrid"]) {
|
|
5142
|
+
const p = builderImplementPrompt(mode);
|
|
5143
|
+
assert.ok(p.includes(`<rubric>\n${REVIEW_RUBRIC}\n</rubric>`), mode);
|
|
5144
|
+
assert.match(p, /Last step before writing \.forge-lean\/result\.json: self-review[\s\S]*git diff <base>\.\.\.HEAD[\s\S]*evidence for each[\s\S]*fix any "no" in this session/);
|
|
5145
|
+
assert.match(p, /"self_review":\[\{"question":"1","answer":"yes\|no","evidence":""\}\]/);
|
|
5146
|
+
assert.ok(p.indexOf("<rubric>") < p.indexOf("Finally write"), "the self-review comes before the result");
|
|
5147
|
+
}
|
|
5148
|
+
});
|
|
5149
|
+
|
|
5150
|
+
it("a fixed result without self_review is logged SELF_REVIEW_MISSING, never a failure or an extra runner call", async () => {
|
|
5151
|
+
const dir = tmpDir("selfrev-"), w = world({ reviews: [ok()] });
|
|
5152
|
+
assert.equal((await run(w, { dir })).terminalReason, "merged");
|
|
5153
|
+
assert.equal(builders(w).length, 2); assert.equal(reviewers(w).length, 1);
|
|
5154
|
+
assert.deepEqual(selfEvents(dir).map((e) => [e.event, e.state]), [["SELF_REVIEW_MISSING", "missing"]]);
|
|
5155
|
+
});
|
|
5156
|
+
|
|
5157
|
+
it("the run log records the self-review and whether the first reviewer judgement agreed with it", async () => {
|
|
5158
|
+
const dir = tmpDir("selfrev-"), w = world({ result: fixed, reviews: [ok()] });
|
|
5159
|
+
assert.equal((await run(w, { dir })).terminalReason, "merged");
|
|
5160
|
+
assert.equal(builders(w).length, 2); assert.equal(reviewers(w).length, 1);
|
|
5161
|
+
assert.deepEqual(selfEvents(dir).map((e) => e.event), ["SELF_REVIEW", "SELF_REVIEW_AGREED"]);
|
|
5162
|
+
const dir2 = tmpDir("selfrev-"), w2 = world({ result: fixed, resumeResult: fixed, reviews: [bad(), ok()] });
|
|
5163
|
+
await run(w2, { dir: dir2 });
|
|
5164
|
+
const ev2 = selfEvents(dir2);
|
|
5165
|
+
assert.deepEqual(ev2.map((e) => e.event), ["SELF_REVIEW", "SELF_REVIEW_DISAGREED"], "compared once, on the first judgement");
|
|
5166
|
+
assert.equal(ev2[1].reviewer, "nonconforming"); assert.deepEqual(ev2[1].self_no, []);
|
|
5167
|
+
});
|
|
5168
|
+
});
|
|
5169
|
+
|
|
5170
|
+
describe("#4362 attempt triggers", () => {
|
|
5171
|
+
const usage = (dir) => readLog(dir, 7).filter((e) => e.event === "PHASE_ATTEMPT_USAGE");
|
|
5172
|
+
|
|
5173
|
+
it("every builder/fixRound/attempt call site in lean.mjs passes a string-literal trigger from TRIGGERS", async () => {
|
|
5174
|
+
const { TRIGGERS } = await import("./fixround.mjs");
|
|
5175
|
+
const src = readFileSync(new URL("./lean.mjs", import.meta.url), "utf8");
|
|
5176
|
+
const lines = src.split("\n");
|
|
5177
|
+
const at = (re) => lines.map((l, i) => [l, i + 1]).filter(([l]) => re.test(l));
|
|
5178
|
+
const builderSites = at(/builder\(\+\+attemptN,/), fixSites = at(/fixRound\("/), attemptSites = at(/attempt\("(?!implement)/);
|
|
5179
|
+
assert.ok(builderSites.length >= 9 && fixSites.length >= 9 && attemptSites.length >= 2, "the call sites were found");
|
|
5180
|
+
const known = (lit, l, n) => assert.ok(lit && TRIGGERS.includes(lit), `lean.mjs:${n} passes no known trigger: ${l.trim().slice(0, 120)}`);
|
|
5181
|
+
const fixLines = at(/async function fixRound\(/)[0][1];
|
|
5182
|
+
for (const [l, n] of builderSites) {
|
|
5183
|
+
if (n > fixLines && n < fixLines + 10 && /trigger \}\)/.test(l)) continue; // fixRound forwards its own trigger, checked below
|
|
5184
|
+
known(/trigger: "([a-z_]+)"/.exec(l)?.[1], l, n);
|
|
5185
|
+
}
|
|
5186
|
+
// #4375: a fixRound call may pass the failure text after its trigger
|
|
5187
|
+
for (const [l, n] of fixSites) known(/\), (?:[\w.]+|\[\]), "([a-z_]+)"(?:, .*)?\);$/.exec(l)?.[1], l, n);
|
|
5188
|
+
for (const [l, n] of attemptSites) known(/, "([a-z_]+)"\);/.exec(l)?.[1], l, n);
|
|
5189
|
+
assert.match(src, /opts\.trigger = "timeout_restart"/, "a timeout restart is tagged");
|
|
5190
|
+
});
|
|
5191
|
+
|
|
5192
|
+
it("a precheck fix run tags every attempt (plan, build, precheck; review) and is not first pass; a clean run is", async () => {
|
|
5193
|
+
const w = world(); let rc = 1;
|
|
5194
|
+
w.io.runCommand = async () => ({ rc, output: rc ? "lint: src/foo.js:3 unused var" : "" });
|
|
5195
|
+
const inner = w.io.runner;
|
|
5196
|
+
w.io.runner = async (c) => { const res = await inner(c); if (/declared check commands failed/.test(c.prompt)) rc = 0; return res; };
|
|
5197
|
+
const dir = tmpDir("core-");
|
|
5198
|
+
const r = await run(w, { dir, precheck: { commands: ["make lint"] } });
|
|
5199
|
+
assert.equal(r.terminalReason, "merged");
|
|
5200
|
+
const u = usage(dir);
|
|
5201
|
+
const { TRIGGERS } = await import("./fixround.mjs");
|
|
5202
|
+
for (const e of u) assert.ok(TRIGGERS.includes(e.trigger), `${e.phase} attempt ${e.attempt}: ${e.trigger}`);
|
|
5203
|
+
assert.deepEqual(u.filter((e) => e.phase === "implement").map((e) => e.trigger), ["plan", "build", "precheck"]);
|
|
5204
|
+
assert.ok(u.filter((e) => e.phase === "review").every((e) => e.trigger === "review"));
|
|
5205
|
+
assert.equal(r.firstPass, false);
|
|
5206
|
+
const clean = await run(world(), { dir: tmpDir("core-") });
|
|
5207
|
+
assert.equal(clean.terminalReason, "merged"); assert.equal(clean.firstPass, true);
|
|
5208
|
+
});
|
|
5209
|
+
|
|
5210
|
+
it("an idle-killed build retries as timeout_restart (restart_of build) and still counts as reaching the build", async () => {
|
|
5211
|
+
const w = world({ dirty: true }); const dir = tmpDir("core-idle-");
|
|
5212
|
+
const orig = w.io.runner; let n = 0;
|
|
5213
|
+
w.io.runner = async (c) => { if (c.role === "builder" && n++ === 1) throw Object.assign(new Error("idle"), { code: "CLI_IDLE" }); return orig(c); };
|
|
5214
|
+
const r = await run(w, { dir });
|
|
5215
|
+
assert.equal(r.terminalReason, "merged");
|
|
5216
|
+
assert.deepEqual(usage(dir).filter((e) => e.phase === "implement").map((e) => [e.attempt, e.trigger, e.restart_of ?? null]), [[1, "plan", null], [2, "timeout_restart", "build"]]);
|
|
5217
|
+
assert.equal(r.firstPass, true);
|
|
5218
|
+
});
|
|
5219
|
+
|
|
5220
|
+
it("#4390: a build stopped at the context ceiling is checkpointed and restarted fresh from contract, diff and last validate", async () => {
|
|
5221
|
+
const w = world({ dirty: true }); const dir = tmpDir("core-ctx-");
|
|
5222
|
+
const orig = w.io.runner, seen = []; let n = 0, contractAtKill = null;
|
|
5223
|
+
w.io.runner = async (c) => {
|
|
5224
|
+
if (c.role === "builder") seen.push({ contextMax: c.contextMax ?? 0, resume: c.resume ?? null, prompt: c.prompt });
|
|
5225
|
+
if (c.role === "builder" && n++ === 1) {
|
|
5226
|
+
contractAtKill = w.files[`${WT}/.forge-lean/contract.json`];
|
|
5227
|
+
w.files[`${WT}/.forge-lean/self-check.json`] = '{"v":1,"ok":false,"validate":[{"id":"V1","rc":1}]}';
|
|
5228
|
+
throw Object.assign(new Error("ctx"), { code: "CLI_CONTEXT", context: 130000, turns: 40, usage: { input_tokens: 40, output_tokens: 900, cache_read_input_tokens: 4000000, cache_creation_input_tokens: 5000 } });
|
|
5229
|
+
}
|
|
5230
|
+
return orig(c);
|
|
5231
|
+
};
|
|
5232
|
+
const r = await run(w, { dir, contextCeilingTokens: 120000 });
|
|
5233
|
+
assert.equal(r.terminalReason, "merged");
|
|
5234
|
+
assert.equal(seen[0].contextMax, 0, "the plan call runs uncapped");
|
|
5235
|
+
assert.equal(seen[1].contextMax, 120000, "the build call carries the ceiling");
|
|
5236
|
+
const restart = seen[2];
|
|
5237
|
+
assert.equal(restart.resume, null, "the restart is a fresh session");
|
|
5238
|
+
assert.match(restart.prompt, /The engine froze \.forge-lean\/contract\.json/);
|
|
5239
|
+
assert.match(restart.prompt, /reached the 120000-token context ceiling/);
|
|
5240
|
+
assert.match(restart.prompt, /<diff>\ndiff --git a\/src\/foo\.js/, "changed hunks carried");
|
|
5241
|
+
assert.match(restart.prompt, /"validate":\[\{"id":"V1","rc":1\}\]/, "last validate result carried");
|
|
5242
|
+
assert.equal(w.files[`${WT}/.forge-lean/contract.json`], contractAtKill, "contract unchanged by the restart");
|
|
5243
|
+
assert.ok(w.calls.some((c) => c[0] === "git" && c[1] === "commit" && c.some((x) => /checkpoint after context ceiling/.test(x))), "dirty work checkpointed on the branch");
|
|
5244
|
+
assert.ok(readLog(dir, 7).some((e) => e.event === "CONTEXT_CEILING" && e.context_tokens === 130000 && e.turns === 40));
|
|
5245
|
+
const impl = usage(dir).filter((e) => e.phase === "implement");
|
|
5246
|
+
assert.deepEqual(impl.map((e) => [e.attempt, e.trigger, e.restart_of ?? null]), [[1, "plan", null], [2, "build", null], [2, "context_restart", "build"]]);
|
|
5247
|
+
assert.equal(impl[1].usage.cache_read_input_tokens, 4000000, "the stopped session's tokens are recorded");
|
|
5248
|
+
assert.equal(r.firstPass, true);
|
|
5249
|
+
});
|
|
5250
|
+
|
|
5251
|
+
it("#4390: context restarts are bounded; after three the call runs uncapped, and 0 disables the ceiling", async () => {
|
|
5252
|
+
const w = world(); const dir = tmpDir("core-ctx-");
|
|
5253
|
+
const orig = w.io.runner, caps = [];
|
|
5254
|
+
w.io.runner = async (c) => {
|
|
5255
|
+
if (c.role === "builder") caps.push(c.contextMax ?? 0);
|
|
5256
|
+
if (c.role === "builder" && c.contextMax > 0) throw Object.assign(new Error("ctx"), { code: "CLI_CONTEXT", context: 999, turns: 1, usage: null });
|
|
5257
|
+
return orig(c);
|
|
5258
|
+
};
|
|
5259
|
+
const r = await run(w, { dir, contextCeilingTokens: 500 });
|
|
5260
|
+
assert.equal(r.terminalReason, "merged");
|
|
5261
|
+
assert.deepEqual(caps.slice(0, 5), [0, 500, 500, 500, 0]);
|
|
5262
|
+
const off = world(); const offCaps = []; const o2 = off.io.runner;
|
|
5263
|
+
off.io.runner = async (c) => { if (c.role === "builder") offCaps.push(c.contextMax ?? 0); return o2(c); };
|
|
5264
|
+
assert.equal((await run(off, { dir: tmpDir("core-ctx-"), contextCeilingTokens: 0 })).terminalReason, "merged");
|
|
5265
|
+
assert.ok(offCaps.every((x) => x === 0));
|
|
5266
|
+
});
|
|
5267
|
+
});
|
|
5268
|
+
|
|
5269
|
+
describe("#4407 per-attempt cost deltas", () => {
|
|
5270
|
+
const bad1 = { ...CONTRACT, requirements: [{ id: "R1", quote: "make foo nicer", disposition: "deliver" }] };
|
|
5271
|
+
const usage = (dir) => readLog(dir, 7).filter((e) => e.event === "PHASE_ATTEMPT_USAGE");
|
|
5272
|
+
const sum = (xs) => Math.round(xs.reduce((a, x) => a + x, 0) * 1e9) / 1e9;
|
|
5273
|
+
|
|
5274
|
+
it("the deltas of a resumed builder session sum to its final total and the lane cost is their sum", async () => {
|
|
5275
|
+
// plan (fresh) -> contract correction (resume) -> build (resume): the CLI reports 0.1, 0.2, 0.3 for the one session
|
|
5276
|
+
const w = world({ contract: bad1, fixedContract: CONTRACT }); const dir = tmpDir("core-4407-");
|
|
5277
|
+
const r = await run(w, { dir });
|
|
5278
|
+
assert.equal(r.terminalReason, "merged");
|
|
5279
|
+
const impl = usage(dir).filter((e) => e.phase === "implement");
|
|
5280
|
+
assert.deepEqual(impl.map((e) => e.trigger), ["plan", "contract_correction", "build"]);
|
|
5281
|
+
for (const e of impl) assert.ok(Number.isFinite(e.cost_usd_delta) && Number.isFinite(e.cost_usd_session), `attempt ${e.attempt} lacks the cost fields`);
|
|
5282
|
+
assert.deepEqual(impl.map((e) => e.cost_usd_session), [0.1, 0.2, 0.3]);
|
|
5283
|
+
assert.equal(sum(impl.map((e) => e.cost_usd_delta)), impl.at(-1).cost_usd_session, "sum of deltas = final session total");
|
|
5284
|
+
const all = usage(dir);
|
|
5285
|
+
assert.equal(Math.round(r.cost_usd * 1e9) / 1e9, sum(all.map((e) => e.cost_usd_delta)), "the lane cost (batch/orchestrate rows) sums the deltas");
|
|
5286
|
+
assert.ok(r.cost_usd < sum(all.map((e) => e.cost_usd)), "not the sum of running totals");
|
|
5287
|
+
});
|
|
5288
|
+
|
|
5289
|
+
it("the trajectory cost line and the run telemetry carry the deltas", async () => {
|
|
5290
|
+
const w = world({ contract: bad1, fixedContract: CONTRACT });
|
|
5291
|
+
const r = await run(w);
|
|
5292
|
+
assert.equal(r.terminalReason, "merged");
|
|
5293
|
+
assert.ok(traj(w)[0].includes("cost $0.3500"), traj(w)[0]);
|
|
5294
|
+
const tel = w.telemetry.filter((e) => e.event === "PHASE_ATTEMPT_USAGE" && e.phase === "implement");
|
|
5295
|
+
assert.deepEqual(tel.map((e) => e.cost_usd_delta), [0.1, 0.1, 0.1]);
|
|
5296
|
+
});
|
|
5297
|
+
});
|
|
5298
|
+
|
|
5299
|
+
describe("#4392: implement turn budget", () => {
|
|
5300
|
+
/** The build is stopped at its turn budget; `wrap(w, put)` acts as the wrap-up session (after the normal fake). */
|
|
5301
|
+
const budgetWorld = (wrap, { overrun = false } = {}) => {
|
|
5302
|
+
const w = world({ dirty: true }); const orig = w.io.runner; w.seen = []; let stopped = false;
|
|
5303
|
+
w.io.runner = async (c) => {
|
|
5304
|
+
if (c.role === "builder") w.seen.push({ turnMax: c.turnMax ?? 0, resume: c.resume ?? null, prompt: c.prompt });
|
|
5305
|
+
if (c.role === "builder" && c.turnMax > 0 && !stopped) { stopped = true; throw Object.assign(new Error("turns"), { code: "CLI_TURNS", turns: c.turnMax, turnMax: c.turnMax, usage: null }); }
|
|
5306
|
+
if (c.role === "builder" && /Turn budget spent/.test(c.prompt)) {
|
|
5307
|
+
if (overrun) throw Object.assign(new Error("turns"), { code: "CLI_TURNS", turns: c.turnMax, turnMax: c.turnMax, usage: null });
|
|
5308
|
+
const r = await orig(c); wrap?.(w); return r;
|
|
5309
|
+
}
|
|
5310
|
+
return orig(c);
|
|
5311
|
+
};
|
|
5312
|
+
return w;
|
|
5313
|
+
};
|
|
5314
|
+
const usage = (dir) => readLog(dir, 7).filter((e) => e.event === "PHASE_ATTEMPT_USAGE");
|
|
5315
|
+
const TB = { S: 25, M: 60, L: 100 }; // the CLI defaults (readCoreLimits); runCore alone is unbudgeted
|
|
5316
|
+
const stopWith = (digest) => (w) => { w.files[`${WT}/.forge-lean/result.json`] = JSON.stringify({ status: "budget-exceeded", summary: "too big", digest }); };
|
|
5317
|
+
|
|
5318
|
+
it("an overrun ends needs-human budget.exceeded with the builder's digest, checkpointed, recoverable once", async () => {
|
|
5319
|
+
const w = budgetWorld(stopWith("done: parser. remaining: 3 adapters. recommend split")); const dir = tmpDir("core-tb-");
|
|
5320
|
+
const r = await run(w, { dir, turnBudgets: TB });
|
|
5321
|
+
assert.equal(r.terminalReason, "needs-human"); assert.equal(r.code, OUTCOME_CODES.BUDGET_EXCEEDED);
|
|
5322
|
+
assert.match(r.detail, /size S, 25 turns/); assert.match(r.detail, /remaining: 3 adapters/);
|
|
5323
|
+
assert.equal(classifyNeedsHuman({ outcome: r.terminalReason, code: r.code }), "recoverable");
|
|
5324
|
+
assert.deepEqual(r.turns, { size: "S", budget: 25, build: 28, total: 31, exceeded: true });
|
|
5325
|
+
assert.deepEqual(w.seen.map((x) => x.turnMax), [0, 25, 5], "plan uncapped, build gets the budget, wrap-up the grant");
|
|
5326
|
+
assert.equal(w.seen[2].resume, null, "the wrap-up is a fresh session");
|
|
5327
|
+
assert.match(w.seen[2].prompt, /The engine froze \.forge-lean\/contract\.json/); assert.match(w.seen[2].prompt, /at most 5 turns/);
|
|
5328
|
+
assert.ok(w.calls.some((c) => c[0] === "git" && c[1] === "commit" && c.some((x) => /checkpoint after turn budget/.test(x))), "work checkpointed");
|
|
5329
|
+
assert.ok(w.comments.some((c) => c.includes("FORGE:NEEDS_HUMAN") && c.includes("remaining: 3 adapters")));
|
|
5330
|
+
const log = readLog(dir, 7);
|
|
5331
|
+
assert.ok(log.some((e) => e.event === "TURN_BUDGET_EXCEEDED" && e.size === "S" && e.budget === 25 && e.turns === 25 && e.grant === 5));
|
|
5332
|
+
assert.deepEqual(log.find((e) => e.event === "RUN_TERMINAL").turns, r.turns);
|
|
5333
|
+
assert.deepEqual(usage(dir).filter((e) => e.phase === "implement").map((e) => [e.trigger, e.restart_of ?? null]), [["plan", null], ["build", null], ["budget_wrapup", "build"]]);
|
|
5334
|
+
});
|
|
5335
|
+
|
|
5336
|
+
it("a wrap-up that finishes flows on to merge; an overrun wrap-up stops with the diff stat as digest", async () => {
|
|
5337
|
+
const fin = budgetWorld(null);
|
|
5338
|
+
const r = await run(fin, { dir: tmpDir("core-tb-"), turnBudgets: TB });
|
|
5339
|
+
assert.equal(r.terminalReason, "merged"); assert.equal(r.turns.exceeded, true); assert.equal(r.firstPass, true);
|
|
5340
|
+
const over = budgetWorld(null, { overrun: true });
|
|
5341
|
+
const o2 = await run(over, { dir: tmpDir("core-tb-"), turnBudgets: TB });
|
|
5342
|
+
assert.equal(o2.code, OUTCOME_CODES.BUDGET_EXCEEDED); assert.match(o2.detail, /No builder digest; changes so far/);
|
|
5343
|
+
});
|
|
5344
|
+
|
|
5345
|
+
it("size comes from a size: label over the contract; 0 disables the budget and nothing is capped", async () => {
|
|
5346
|
+
const w = world({ labels: [{ name: "size:L" }] }); const caps = []; const orig = w.io.runner;
|
|
5347
|
+
w.io.runner = async (c) => { if (c.role === "builder") caps.push(c.turnMax ?? 0); return orig(c); };
|
|
5348
|
+
const r = await run(w, { dir: tmpDir("core-tb-"), turnBudgets: { L: 70 } });
|
|
5349
|
+
assert.equal(r.terminalReason, "merged"); assert.deepEqual(caps, [0, 70]);
|
|
5350
|
+
assert.deepEqual(r.turns, { size: "L", budget: 70, build: 3, total: 6, exceeded: false });
|
|
5351
|
+
const off = world(); const offCaps = []; const o2 = off.io.runner;
|
|
5352
|
+
off.io.runner = async (c) => { if (c.role === "builder") offCaps.push(c.turnMax ?? 0); return o2(c); };
|
|
5353
|
+
const r2 = await run(off, { dir: tmpDir("core-tb-"), turnBudgets: { S: 0, M: 0, L: 0 } });
|
|
5354
|
+
assert.equal(r2.terminalReason, "merged"); assert.ok(offCaps.every((x) => x === 0)); assert.equal(r2.turns.budget, 0);
|
|
5355
|
+
});
|
|
5356
|
+
});
|
|
5357
|
+
|
|
5358
|
+
describe("learned pitfalls (#4375)", () => {
|
|
5359
|
+
const memStore = () => { const recs = []; return { recs, read: async () => recs.map((r) => JSON.parse(JSON.stringify(r))), append: async (_repo, r) => { recs.push(r); } }; };
|
|
5360
|
+
const packFacts = async (a) => (await import("./context.mjs")).gatherContextFacts({ ...a, io: {}, sources: ["pitfalls"] });
|
|
5361
|
+
const failingPrecheck = (w, passAfterFix = true) => {
|
|
5362
|
+
let rc = 1;
|
|
5363
|
+
w.io.runCommand = async () => ({ rc, output: rc ? "> eslint .\nsrc/foo.js:3:7 error 'tmp' is assigned a value but never used no-unused-vars" : "" });
|
|
5364
|
+
const inner = w.io.runner;
|
|
5365
|
+
w.io.runner = async (c) => { const res = await inner(c); if (passAfterFix && /declared check commands failed/.test(c.prompt)) rc = 0; return res; };
|
|
5366
|
+
};
|
|
5367
|
+
|
|
5368
|
+
it("a precheck rework round that fixes the failure records one pitfall; the next lane's pack carries it, never posted", async () => {
|
|
5369
|
+
const store = memStore();
|
|
5370
|
+
const w = world(); w.io.pitfalls = store; failingPrecheck(w);
|
|
5371
|
+
const dir = tmpDir("core-pit-");
|
|
5372
|
+
assert.equal((await run(w, { dir, precheck: { commands: ["npm run lint"] } })).terminalReason, "merged");
|
|
5373
|
+
const pits = store.recs.filter((r) => r.t === "pitfall");
|
|
5374
|
+
assert.equal(pits.length, 1);
|
|
5375
|
+
assert.equal(pits[0].trigger, "precheck");
|
|
5376
|
+
assert.match(pits[0].sig, /^precheck:[0-9a-f]{12}$/);
|
|
5377
|
+
assert.deepEqual(pits[0].globs, ["src/*.js", "test/*.js"]);
|
|
5378
|
+
assert.match(pits[0].lesson, /^precheck: src\/foo\.js:3:7 error 'tmp' is assigned a value but never used no-unused-vars$/);
|
|
5379
|
+
assert.equal(store.recs.filter((r) => r.t === "lane").length, 1, "one lane row per lane");
|
|
5380
|
+
assert.deepEqual(readLog(dir, 7).filter((e) => e.event === "PITFALL_RECORDED").map((e) => [e.trigger, e.sig]), [["precheck", pits[0].sig]]);
|
|
5381
|
+
|
|
5382
|
+
// the next lane naming the same file gets it in its pack (mode on), logs it, and posts only the count
|
|
5383
|
+
const w2 = world({ issueBodies: [`${ISSUE_BODY}\n\nFix it in src/foo.js.`] }); w2.io.pitfalls = store; w2.io.contextFacts = packFacts;
|
|
5384
|
+
const dir2 = tmpDir("core-pit2-");
|
|
5385
|
+
assert.equal((await run(w2, { dir: dir2, contextPack: { mode: "on" } })).terminalReason, "merged");
|
|
5386
|
+
assert.match(builders(w2)[0][4], /Recurring pitfalls in this repo[\s\S]*no-unused-vars/);
|
|
5387
|
+
const cp = readLog(dir2, 7).find((e) => e.event === "CONTEXT_PACK");
|
|
5388
|
+
assert.deepEqual(cp.pitfalls, [{ sig: pits[0].sig, match: "files" }]);
|
|
5389
|
+
assert.equal(readLog(dir2, 7).find((e) => e.event === "RUN_TERMINAL").pitfalls_matched, 1);
|
|
5390
|
+
for (const c of [...w.comments, ...w2.comments]) assert.ok(!c.includes("no-unused-vars") || !c.startsWith("<!-- FORGE:CONTEXT"), "pitfall text never in FORGE:CONTEXT");
|
|
5391
|
+
assert.ok(!w2.comments.some((c) => c.includes("'tmp' is assigned a value")), "no posted comment carries the lesson");
|
|
5392
|
+
const mapped = [...w.telemetry, ...w2.telemetry].map((x) => JSON.stringify(mapRecord(x) ?? null));
|
|
5393
|
+
assert.ok(!mapped.some((x) => /never used|pitfall|precheck:[0-9a-f]{12}/.test(x)), "nothing pitfall-shaped reaches the FORGE:TELEMETRY record");
|
|
5394
|
+
});
|
|
5395
|
+
|
|
5396
|
+
it("a rework round whose gate never passes records nothing; no io.pitfalls is today's run", async () => {
|
|
5397
|
+
const store = memStore();
|
|
5398
|
+
const w = world(); w.io.pitfalls = store; failingPrecheck(w, false);
|
|
5399
|
+
const dir = tmpDir("core-pit3-");
|
|
5400
|
+
assert.equal((await run(w, { dir, precheck: { commands: ["npm run lint"] } })).terminalReason, "merged");
|
|
5401
|
+
assert.equal(store.recs.filter((r) => r.t === "pitfall").length, 0);
|
|
5402
|
+
assert.ok(!readLog(dir, 7).some((e) => e.event === "PITFALL_RECORDED"));
|
|
5403
|
+
const plain = world(); failingPrecheck(plain);
|
|
5404
|
+
const d2 = tmpDir("core-pit4-");
|
|
5405
|
+
assert.equal((await run(plain, { dir: d2, precheck: { commands: ["npm run lint"] } })).terminalReason, "merged");
|
|
5406
|
+
assert.ok(!readLog(d2, 7).some((e) => e.event === "PITFALL_RECORDED"));
|
|
5407
|
+
assert.equal(readLog(d2, 7).find((e) => e.event === "RUN_TERMINAL").pitfalls_matched, undefined);
|
|
5408
|
+
});
|
|
5409
|
+
|
|
5410
|
+
it("a ledger that throws never fails the lane", async () => {
|
|
5411
|
+
const w = world({ issueBodies: [`${ISSUE_BODY}\n\nsrc/foo.js`] });
|
|
5412
|
+
w.io.pitfalls = { read: async () => { throw new Error("EACCES"); }, append: async () => { throw new Error("EROFS"); } };
|
|
5413
|
+
w.io.contextFacts = packFacts; failingPrecheck(w);
|
|
5414
|
+
assert.equal((await run(w, { contextPack: { mode: "on" }, precheck: { commands: ["npm run lint"] } })).terminalReason, "merged");
|
|
5415
|
+
});
|
|
5416
|
+
});
|
|
5417
|
+
|
|
5418
|
+
describe("explorer subagent (#4391)", () => {
|
|
5419
|
+
const capture = (w) => { const seen = []; const inner = w.io.runner; w.io.runner = async (c) => { seen.push({ role: c.role, agents: c.agents ?? null }); return inner(c); }; return seen; };
|
|
5420
|
+
|
|
5421
|
+
it("on: builder calls get the explorer agent, plan/implement prompts the rule, reviewer none; RUN_TERMINAL tags explore", async () => {
|
|
5422
|
+
const w = world(), seen = capture(w), dir = tmpDir("core-");
|
|
5423
|
+
assert.equal((await run(w, { dir, explore: { mode: "on", model: "claude-haiku-5-5" } })).terminalReason, "merged");
|
|
5424
|
+
const b = seen.filter((s) => s.role === "builder");
|
|
5425
|
+
assert.ok(b.length >= 2 && b.every((s) => s.agents?.explorer?.model === "claude-haiku-5-5"));
|
|
5426
|
+
assert.ok(seen.filter((s) => s.role !== "builder").every((s) => s.agents === null));
|
|
5427
|
+
const [plan, impl] = builders(w).map((c) => c[4]);
|
|
5428
|
+
assert.ok(plan.startsWith(builderPlanPrompt(7)));
|
|
5429
|
+
assert.match(plan, /subagent_type "explorer"[\s\S]*Open a file yourself only to edit it or to verify a digest claim/);
|
|
5430
|
+
assert.match(impl, /subagent_type "explorer"/);
|
|
5431
|
+
assert.equal(readLog(dir, 7).find((e) => e.event === "RUN_TERMINAL").explore, "on");
|
|
5432
|
+
assert.match(traj(w).at(-1), /Explorer subagent \(on\): model claude-haiku-5-5/);
|
|
5433
|
+
});
|
|
5434
|
+
|
|
5435
|
+
it("off (default): no agents on any call and prompts identical to an explicit off", async () => {
|
|
5436
|
+
const w = world(), seen = capture(w), off = world(), dir = tmpDir("core-");
|
|
5437
|
+
await run(w, { dir });
|
|
5438
|
+
await run(off, { explore: { mode: "off" } });
|
|
5439
|
+
assert.ok(seen.every((s) => s.agents === null));
|
|
5440
|
+
assert.deepEqual(builders(w).map((c) => c[4]), builders(off).map((c) => c[4]));
|
|
5441
|
+
assert.ok(!builders(w).some((c) => c[4].includes("explorer")));
|
|
5442
|
+
assert.equal(readLog(dir, 7).find((e) => e.event === "RUN_TERMINAL").explore, "off");
|
|
5443
|
+
});
|
|
5444
|
+
});
|
|
5445
|
+
|
|
5446
|
+
describe("e2e merge gate (#4403)", () => {
|
|
5447
|
+
const SPECS = ["src/foo.js", "test/foo.test.js", "e2e/foo.spec.ts"];
|
|
5448
|
+
const RUN = "https://github.com/o/r/actions/runs/777/job/1";
|
|
5449
|
+
/** commitChecks: the e2e job is skipped until the PR carries the label; then `seq` is consumed (the last repeats). */
|
|
5450
|
+
const withE2e = (w, seq, log = "FAIL e2e/foo.spec.ts > handles null") => {
|
|
5451
|
+
w.labelled = () => w.prEdits.some((a) => a.includes("--add-label"));
|
|
5452
|
+
w.io.commitChecks = async () => {
|
|
5453
|
+
if (!w.labelled()) return [{ name: "e2e", state: "success", conclusion: "skipped", url: RUN }];
|
|
5454
|
+
const st = seq.length > 1 ? seq.shift() : seq[0];
|
|
5455
|
+
return [{ name: "e2e", state: "success", conclusion: "skipped", url: RUN }, { name: "e2e", state: st, conclusion: st === "pending" ? null : st, url: RUN }];
|
|
5456
|
+
};
|
|
5457
|
+
w.io.failedLogs = async () => log;
|
|
5458
|
+
return w;
|
|
5459
|
+
};
|
|
5460
|
+
const cfg = (extra = {}) => parseVerify({ e2e_label: "full-ci", e2e_checks: "e2e", ...extra });
|
|
5461
|
+
const go = (w, extra = {}) => run(w, { conformance: "shadow", verify: cfg(), ...extra });
|
|
5462
|
+
const e2eEvents = (dir) => readLog(dir, 7).filter((e) => e.event === "E2E_VERIFY");
|
|
5463
|
+
|
|
5464
|
+
it("#4403 repro: a lane that changes an e2e spec labels the PR, waits on the e2e check and merges only after it passes", async () => {
|
|
5465
|
+
const w = withE2e(world({ changed: SPECS, reviews: [conf({}, CONTRACT, SPECS)] }), ["pending", "pending", "success"]);
|
|
5466
|
+
const dir = tmpDir("core-");
|
|
5467
|
+
const r = await go(w, { dir });
|
|
5468
|
+
assert.equal(r.terminalReason, "merged");
|
|
5469
|
+
const label = w.prEdits.find((a) => a.includes("--add-label"));
|
|
5470
|
+
assert.ok(label, "the PR is labelled for the e2e run"); assert.equal(label[label.indexOf("--add-label") + 1], "full-ci");
|
|
5471
|
+
const ev = e2eEvents(dir);
|
|
5472
|
+
assert.deepEqual(ev.map((e) => [e.result, e.specs]), [["pass", ["e2e/foo.spec.ts"]]]);
|
|
5473
|
+
assert.ok(w.seq.indexOf("merge") > -1);
|
|
5474
|
+
});
|
|
5475
|
+
|
|
5476
|
+
it("the lane's own spec failing in e2e: builder fix round, re-review, then merged once e2e is green; no rounds left -> ci.red, nothing merged", async () => {
|
|
5477
|
+
const w = withE2e(world({ changed: SPECS }), ["failure", "success"]);
|
|
5478
|
+
const r = await go(w);
|
|
5479
|
+
assert.equal(r.terminalReason, "merged");
|
|
5480
|
+
const fix = builders(w).filter((b) => /CI failed on the pushed branch/.test(b[4]));
|
|
5481
|
+
assert.equal(fix.length, 1); assert.match(fix[0][4], /e2e\/foo\.spec\.ts/);
|
|
5482
|
+
assert.equal(reviewers(w).length, 2, "the fixed head is reviewed again");
|
|
5483
|
+
const w2 = withE2e(world({ changed: SPECS }), ["failure"]);
|
|
5484
|
+
const r2 = await go(w2, { verify: { ...cfg(), maxRounds: 0 } });
|
|
5485
|
+
assert.equal(r2.terminalReason, "needs-human"); assert.equal(r2.code, OUTCOME_CODES.CI_RED); assert.equal(w2.merges.length, 0);
|
|
5486
|
+
});
|
|
5487
|
+
|
|
5488
|
+
it("an unrelated pre-existing e2e failure is reported, not blocking; e2e_blocking: all blocks it", async () => {
|
|
5489
|
+
const w = withE2e(world({ changed: SPECS }), ["failure"], "FAIL e2e/rosters.spec.ts > lists rosters");
|
|
5490
|
+
const dir = tmpDir("core-");
|
|
5491
|
+
const r = await go(w, { dir });
|
|
5492
|
+
assert.equal(r.terminalReason, "merged");
|
|
5493
|
+
assert.deepEqual(e2eEvents(dir).map((e) => [e.result, e.own]), [["fail", []]]);
|
|
5494
|
+
assert.ok(traj(w).some((t) => /Pre-existing e2e failure\(s\) reported, not blocking: e2e/.test(t)));
|
|
5495
|
+
const w2 = withE2e(world({ changed: SPECS }), ["failure"], "FAIL e2e/rosters.spec.ts > lists rosters");
|
|
5496
|
+
const r2 = await go(w2, { verify: { ...cfg({ e2e_blocking: "all" }), maxRounds: 0 } });
|
|
5497
|
+
assert.equal(r2.terminalReason, "needs-human"); assert.equal(w2.merges.length, 0);
|
|
5498
|
+
});
|
|
5499
|
+
|
|
5500
|
+
it("verify.e2e: never (or no label/workflow configured) keeps today's behaviour: no label, no e2e wait, no event", async () => {
|
|
5501
|
+
for (const verify of [cfg({ e2e: "never" }), parseVerify({})]) {
|
|
5502
|
+
const w = withE2e(world({ changed: SPECS }), ["failure"]);
|
|
5503
|
+
const dir = tmpDir("core-");
|
|
5504
|
+
const r = await go(w, { dir, verify });
|
|
5505
|
+
assert.equal(r.terminalReason, "merged");
|
|
5506
|
+
assert.equal(w.prEdits.filter((a) => a.includes("--add-label")).length, 0);
|
|
5507
|
+
assert.equal(e2eEvents(dir).length, 0);
|
|
5508
|
+
}
|
|
5509
|
+
});
|
|
5510
|
+
|
|
5511
|
+
it("a lane without e2e specs or [type:e2e] criteria never runs e2e; e2e that never starts is needs-human, nothing merged", async () => {
|
|
5512
|
+
const w = withE2e(world(), ["failure"]);
|
|
5513
|
+
assert.equal((await go(w)).terminalReason, "merged");
|
|
5514
|
+
assert.equal(w.prEdits.filter((a) => a.includes("--add-label")).length, 0);
|
|
5515
|
+
const w2 = world({ changed: SPECS });
|
|
5516
|
+
w2.io.commitChecks = async () => [{ name: "e2e", state: "success", conclusion: "skipped", url: RUN }];
|
|
5517
|
+
const r2 = await go(w2);
|
|
5518
|
+
assert.equal(r2.terminalReason, "needs-human"); assert.equal(w2.merges.length, 0);
|
|
5519
|
+
});
|
|
5520
|
+
});
|
|
5521
|
+
|
|
5522
|
+
describe("#4397 independent adversarial verifier", () => {
|
|
5523
|
+
const finding = { file: "src/foo.js", line: 3, input: "foo(null)", expected: "0", actual: "throws TypeError", why: "no guard" };
|
|
5524
|
+
/** Verifier replies in order (an Error throws); every verifier call is recorded in w.vcalls and never reaches the reviewer queue. */
|
|
5525
|
+
const withVerifier = (w, replies = []) => {
|
|
5526
|
+
const orig = w.io.runner; w.vcalls = [];
|
|
5527
|
+
w.io.runner = async (c) => {
|
|
5528
|
+
if (c.role !== "verifier") return orig(c);
|
|
5529
|
+
w.vcalls.push(c);
|
|
5530
|
+
const r = replies.length ? replies.shift() : fence({ findings: [] });
|
|
5531
|
+
if (r instanceof Error) throw r;
|
|
5532
|
+
return { text: r, sessionId: "ver", usage: { input_tokens: 3, output_tokens: 1 }, costUsd: 0.01, durationMs: 300, turns: 2, model: c.model };
|
|
5533
|
+
};
|
|
5534
|
+
return w;
|
|
5535
|
+
};
|
|
5536
|
+
const vEvents = (dir) => readLog(dir, 7).filter((e) => e.event === "VERIFIER");
|
|
5537
|
+
const review = (w) => w.comments.find((c) => c.includes("<!-- FORGE:REVIEW -->"));
|
|
5538
|
+
|
|
5539
|
+
it("verifier off (default): no verifier call, no VERIFIER event, review comment unchanged", async () => {
|
|
5540
|
+
const w = withVerifier(world()); const dir = tmpDir("core-");
|
|
5541
|
+
assert.equal((await run(w, { dir })).terminalReason, "merged");
|
|
5542
|
+
assert.equal(w.vcalls.length, 0);
|
|
5543
|
+
assert.deepEqual(vEvents(dir), []);
|
|
5544
|
+
assert.doesNotMatch(review(w), /Independent verifier/);
|
|
5545
|
+
});
|
|
5546
|
+
|
|
5547
|
+
it("shadow: one fresh read-only call on a model other than the builder's, without the builder's result; findings are advisory", async () => {
|
|
5548
|
+
const w = withVerifier(world(), [fence({ findings: [finding, { file: "src/nope.js", line: 1 }] })]); const dir = tmpDir("core-");
|
|
5549
|
+
const r = await run(w, { dir, conformance: "shadow", builderModel: "claude-sonnet-5-5", verifier: { mode: "shadow" } });
|
|
5550
|
+
assert.equal(r.terminalReason, "merged", "shadow never blocks");
|
|
5551
|
+
assert.equal(w.vcalls.length, 1);
|
|
5552
|
+
const c = w.vcalls[0];
|
|
5553
|
+
assert.equal(c.resume, null); assert.equal(c.readOnly, true);
|
|
5554
|
+
assert.equal(c.model, "claude-opus-5-5", "the configured default is the builder's model, so it is swapped");
|
|
5555
|
+
assert.ok(c.maxMs > 0 && c.maxTurns > 0);
|
|
5556
|
+
assert.match(c.system, /BREAK/);
|
|
5557
|
+
assert.match(c.prompt, /foo crashes on null input\./); assert.match(c.prompt, /<contract>/); assert.match(c.prompt, /\+fix/);
|
|
5558
|
+
assert.doesNotMatch(c.prompt, /fixed foo|builder_result|<rubric>/, "no builder summary or rubric");
|
|
5559
|
+
assert.equal(builders(w).length, 2, "no fix round in shadow");
|
|
5560
|
+
const [e] = vEvents(dir);
|
|
5561
|
+
assert.equal(e.mode, "shadow"); assert.equal(e.model, "claude-opus-5-5"); assert.equal(e.swapped, true); assert.equal(e.findings, 1, "a finding at a missing path is dropped");
|
|
5562
|
+
assert.equal(e.cost_usd, 0.01); assert.ok(e.implement_cost_usd > 0); assert.equal(e.ratio, Math.round((0.01 / e.implement_cost_usd) * 1000) / 1000);
|
|
5563
|
+
assert.match(review(w), /### Independent verifier \(shadow, advisory\)[\s\S]*- `src\/foo\.js:3` input foo\(null\): throws TypeError \(expected 0\)/);
|
|
5564
|
+
const usage = readLog(dir, 7).filter((x) => x.event === "PHASE_ATTEMPT_USAGE" && x.phase === "verify");
|
|
5565
|
+
assert.equal(usage.length, 1); assert.equal(usage[0].trigger, "verifier");
|
|
5566
|
+
});
|
|
5567
|
+
|
|
5568
|
+
it("enforce: a finding feeds the existing conformance fix round (fresh session); the new head is verified again and merges", async () => {
|
|
5569
|
+
const w = withVerifier(world(), [fence({ findings: [finding] }), fence({ findings: [] })]); const dir = tmpDir("core-");
|
|
5570
|
+
const r = await run(w, { dir, verifier: { mode: "enforce", model: "claude-haiku-5-5" } });
|
|
5571
|
+
assert.equal(r.terminalReason, "merged");
|
|
5572
|
+
const b = builders(w);
|
|
5573
|
+
assert.equal(b.length, 3, "plan, implement, one fix round");
|
|
5574
|
+
assert.equal(b[2][2], null, "file:line findings run a fresh fix session");
|
|
5575
|
+
assert.match(b[2][4], /conformance\.regression: independent verifier: src\/foo\.js:3 input foo\(null\): throws TypeError/);
|
|
5576
|
+
assert.equal(w.vcalls.length, 2);
|
|
5577
|
+
assert.ok(w.vcalls.every((c) => c.model === "claude-haiku-5-5"));
|
|
5578
|
+
const ev = vEvents(dir);
|
|
5579
|
+
assert.deepEqual(ev.map((e) => [e.mode, e.findings]), [["enforce", 1], ["enforce", 0]]);
|
|
5580
|
+
assert.notEqual(ev[0].head, ev[1].head);
|
|
5581
|
+
assert.match(review(w), /### Independent verifier \(enforce\)[\s\S]*No concrete defect found\./);
|
|
5582
|
+
assert.ok(readLog(dir, 7).some((e) => e.event === "CONFORMANCE" && e.state === "conforms"));
|
|
5583
|
+
});
|
|
5584
|
+
|
|
5585
|
+
it("enforce: a head is verified once; a fix round that made no commit reuses the verdict and ends needs-human", async () => {
|
|
5586
|
+
const w = withVerifier(world({ freshNoCommit: true }), [fence({ findings: [finding] })]); const dir = tmpDir("core-");
|
|
5587
|
+
const r = await run(w, { dir, verifier: { mode: "enforce" } });
|
|
5588
|
+
assert.equal(r.terminalReason, "needs-human");
|
|
5589
|
+
assert.equal(w.vcalls.length, 1);
|
|
5590
|
+
assert.equal(vEvents(dir).length, 1);
|
|
5591
|
+
assert.equal(w.merges.length, 0);
|
|
5592
|
+
});
|
|
5593
|
+
|
|
5594
|
+
it("enforce with no findings merges after one verifier call; a verifier error is an anomaly, never a failure", async () => {
|
|
5595
|
+
const ok1 = withVerifier(world());
|
|
5596
|
+
assert.equal((await run(ok1, { verifier: { mode: "enforce" } })).terminalReason, "merged");
|
|
5597
|
+
assert.equal(ok1.vcalls.length, 1);
|
|
5598
|
+
assert.equal(builders(ok1).length, 2);
|
|
5599
|
+
const w = withVerifier(world(), [Object.assign(new Error("idle"), { code: "CLI_IDLE" })]); const dir = tmpDir("core-");
|
|
5600
|
+
assert.equal((await run(w, { dir, verifier: { mode: "enforce" } })).terminalReason, "merged");
|
|
5601
|
+
assert.equal(vEvents(dir)[0].error, "CLI_IDLE");
|
|
5602
|
+
assert.match(traj(w)[0], /Independent verifier failed at [0-9a-f]{12}: CLI_IDLE/);
|
|
5603
|
+
});
|
|
5604
|
+
|
|
5605
|
+
it("an empty diff skips the call; a failing review never reaches the verifier", async () => {
|
|
5606
|
+
const w = withVerifier(world()); const dir = tmpDir("core-");
|
|
5607
|
+
const g = w.io.git; w.io.git = async (a, o) => (a[0] === "diff" && a.length === 2 ? "" : g(a, o));
|
|
5608
|
+
await run(w, { dir, verifier: { mode: "shadow" } });
|
|
5609
|
+
assert.equal(w.vcalls.length, 0);
|
|
5610
|
+
assert.equal(vEvents(dir)[0].skipped, "empty_diff");
|
|
5611
|
+
const bad1 = withVerifier(world({ reviews: [bad(), bad()] }));
|
|
5612
|
+
await run(bad1, { verifier: { mode: "enforce" } });
|
|
5613
|
+
assert.equal(bad1.vcalls.length, 0);
|
|
5614
|
+
});
|
|
5615
|
+
});
|
|
5616
|
+
|
|
5617
|
+
describe("#4394 builder model routing", () => {
|
|
5618
|
+
const prechecked = () => { // the first precheck fails, the fix round makes it pass
|
|
5619
|
+
const w = world(); let rc = 1;
|
|
5620
|
+
w.io.runCommand = async () => ({ rc, output: rc ? "lint: src/foo.js:3 unused var" : "" });
|
|
5621
|
+
const inner = w.io.runner;
|
|
5622
|
+
w.io.runner = async (c) => { const res = await inner(c); if (/declared check commands failed/.test(c.prompt)) rc = 0; return res; };
|
|
5623
|
+
return w;
|
|
5624
|
+
};
|
|
5625
|
+
const events = (dir, name) => readLog(dir, 7).filter((e) => e.event === name);
|
|
5626
|
+
const isPlan = (b) => /PLANNING ONLY/.test(b[4]);
|
|
5627
|
+
const docsContract = { ...CONTRACT, deliverables: [{ id: "D1", path: "docs/foo.md", change: "document null", requirements: ["R1"] }, { id: "D2", path: "test/foo.test.js", change: "test", requirements: ["R1"] }] };
|
|
5628
|
+
|
|
5629
|
+
it("a docs/test-only contract builds on the mid model; the plan stays on builderModel; RUN_TERMINAL and the result carry the route", async () => {
|
|
5630
|
+
const w = world({ contract: docsContract }); const dir = tmpDir("core-");
|
|
5631
|
+
const r = await run(w, { dir, builderModel: "opus-b" });
|
|
5632
|
+
const b = builders(w);
|
|
5633
|
+
assert.ok(isPlan(b[0])); assert.equal(b[0][3], "opus-b", "the plan keeps the top model");
|
|
5634
|
+
assert.equal(b[1][3], "claude-sonnet-5-5", "the build runs on the mid model");
|
|
5635
|
+
const [route] = events(dir, "BUILDER_ROUTE");
|
|
5636
|
+
assert.deepEqual({ mode: route.mode, class: route.class, tier: route.tier, model: route.model }, { mode: "on", class: "docs-config-test", tier: "mid", model: "claude-sonnet-5-5" });
|
|
5637
|
+
const term = events(dir, "RUN_TERMINAL").at(-1);
|
|
5638
|
+
assert.deepEqual([term.builder_tier, term.builder_class, term.builder_escalated, term.builder_model], ["mid", "docs-config-test", false, "claude-sonnet-5-5"]);
|
|
5639
|
+
assert.deepEqual(r.builderRoute, { tier: "mid", class: "docs-config-test", escalated: false, model: "claude-sonnet-5-5" });
|
|
5640
|
+
assert.match(traj(w)[0], /Builder routed to the mid tier \(docs-config-test; claude-sonnet-5-5\)/);
|
|
5641
|
+
const t = w.telemetry.map((x) => mapRecord(x, 1)).find((x) => x?.event === "run_terminal");
|
|
5642
|
+
assert.equal(t.payload.builder_tier, "mid"); assert.equal(t.payload.builder_escalated, 0);
|
|
5643
|
+
});
|
|
5644
|
+
|
|
5645
|
+
it("lean.builder_mid_model picks the mid model; a declared risk path routes the build to the top model", async () => {
|
|
5646
|
+
const w1 = world(); await run(w1, { builderModel: "opus-b", builderRouting: { midModel: "haiku-mid" } });
|
|
5647
|
+
assert.deepEqual(builders(w1).slice(0, 2).map((b) => b[3]), ["opus-b", "haiku-mid"]);
|
|
5648
|
+
const w2 = world(); const dir = tmpDir("core-");
|
|
5649
|
+
await run(w2, { dir, builderModel: "opus-b", reviewTrigger: { mode: "shadow", riskPaths: ["src/**"] } });
|
|
5650
|
+
assert.ok(builders(w2).every((b) => b[3] === "opus-b"), "every builder call on the top model");
|
|
5651
|
+
const [route] = events(dir, "BUILDER_ROUTE");
|
|
5652
|
+
assert.equal(route.class, "risk"); assert.equal(route.tier, "top"); assert.deepEqual(route.risk, ["src/foo.js"]);
|
|
5653
|
+
});
|
|
5654
|
+
|
|
5655
|
+
it("a failed precheck on the mid tier escalates: BUILDER_ESCALATED, the fix round runs on the top model, RUN_TERMINAL records it", async () => {
|
|
5656
|
+
const w = prechecked(); const dir = tmpDir("core-");
|
|
5657
|
+
const r = await run(w, { dir, builderModel: "opus-b", fixModel: "haiku-fix", precheck: { commands: ["make lint"] } });
|
|
5658
|
+
assert.equal(r.terminalReason, "merged");
|
|
5659
|
+
const fix = builders(w).filter((b) => /declared check commands failed/.test(b[4]));
|
|
5660
|
+
assert.equal(fix.length, 1, "no extra fix round");
|
|
5661
|
+
assert.equal(fix[0][3], "opus-b", "the fresh fix session runs on the top model, not lean.fix_model");
|
|
5662
|
+
const esc = events(dir, "BUILDER_ESCALATED");
|
|
5663
|
+
assert.equal(esc.length, 1); assert.deepEqual([esc[0].from, esc[0].to, esc[0].trigger], ["claude-sonnet-5-5", "opus-b", "precheck"]);
|
|
5664
|
+
const term = events(dir, "RUN_TERMINAL").at(-1);
|
|
5665
|
+
assert.deepEqual([term.builder_tier, term.builder_class, term.builder_escalated, term.builder_model], ["mid", "small", true, "claude-sonnet-5-5"]);
|
|
5666
|
+
assert.equal(r.builderRoute.escalated, true);
|
|
5667
|
+
});
|
|
5668
|
+
|
|
5669
|
+
it("a conformance fix round on the mid tier does not escalate and keeps the fix model", async () => {
|
|
5670
|
+
const w = world({ reviews: [bad("src/foo.js:9 still wrong"), ok()] }); const dir = tmpDir("core-");
|
|
5671
|
+
assert.equal((await run(w, { dir, builderModel: "opus-b", fixModel: "haiku-fix" })).terminalReason, "merged");
|
|
5672
|
+
assert.equal(builders(w)[2][3], "haiku-fix");
|
|
5673
|
+
assert.deepEqual(events(dir, "BUILDER_ESCALATED"), []);
|
|
5674
|
+
});
|
|
5675
|
+
|
|
5676
|
+
it("builder_routing off: plan, build and the resume keep builderModel; the class is still recorded on the top tier", async () => {
|
|
5677
|
+
const w = world({ reviews: [conf({ verdict: "NONCONFORMING", requirements: [{ id: "R1", status: "partial", evidence: "the foo module", validated_by: "V1", note: "x" }] }), ok()] }); const dir = tmpDir("core-");
|
|
5678
|
+
await run(w, { dir, builderModel: "opus-b", builderRouting: { mode: "off" } });
|
|
5679
|
+
assert.ok(builders(w).length >= 3);
|
|
5680
|
+
assert.ok(builders(w).every((b) => b[3] === "opus-b"));
|
|
5681
|
+
const term = events(dir, "RUN_TERMINAL").at(-1);
|
|
5682
|
+
assert.deepEqual([term.builder_tier, term.builder_class, term.builder_model], ["top", "small", "opus-b"]);
|
|
5683
|
+
});
|
|
5684
|
+
|
|
5685
|
+
it("the verifier never runs on the routed mid model", async () => {
|
|
5686
|
+
const w = world(); w.vcalls = [];
|
|
5687
|
+
const orig = w.io.runner;
|
|
5688
|
+
w.io.runner = async (c) => {
|
|
5689
|
+
if (c.role !== "verifier") return orig(c);
|
|
5690
|
+
w.vcalls.push(c);
|
|
5691
|
+
return { text: fence({ findings: [] }), sessionId: "ver", usage: { input_tokens: 3, output_tokens: 1 }, costUsd: 0.01, durationMs: 300, turns: 2, model: c.model };
|
|
5692
|
+
};
|
|
5693
|
+
await run(w, { conformance: "shadow", builderModel: "claude-opus-5-5", verifier: { mode: "shadow", model: "claude-sonnet-5-5" } });
|
|
5694
|
+
assert.equal(builders(w)[1][3], "claude-sonnet-5-5");
|
|
5695
|
+
assert.equal(w.vcalls.length, 1);
|
|
5696
|
+
assert.notEqual(w.vcalls[0].model, "claude-sonnet-5-5");
|
|
5697
|
+
});
|
|
5698
|
+
});
|
|
5699
|
+
|
|
5700
|
+
describe("resume digest in builder prompts (#4400)", () => {
|
|
5701
|
+
const BOT = { author: { login: "forge[bot]" }, authorAssociation: "NONE" };
|
|
5702
|
+
const factsRec = `<!-- ${FACTS_MARKER} -->\n${renderFactsRecord(normaliseFacts({ validation: [{ command: "node --test test/foo.test.js", exit: 1 }] }, { files: ["src/foo.js"] }))}`;
|
|
5703
|
+
const needs = "<!-- FORGE:NEEDS_HUMAN -->\n## Needs human\nCI red: test/foo.test.js fails on the null case";
|
|
5704
|
+
const body = `${ISSUE_BODY}\n\n<!-- FORGE:STATE\n${JSON.stringify({ v: 1, issue: 7, terminal: true, terminalReason: "needs-human", terminalCode: "ci.red", failureClasses: ["ci.red"] })}\n-->`;
|
|
5705
|
+
const frozen = () => {
|
|
5706
|
+
const h = contractHash(contractJson(CONTRACT));
|
|
5707
|
+
return { body: `<!-- FORGE:CONTRACT -->\n<!-- FORGE:PLAN -->\n${renderContract(CONTRACT, h)}`, author_association: "OWNER", user: { type: "User", login: "op" } };
|
|
5708
|
+
};
|
|
5709
|
+
const resumedWorld = () => world({ prList: [], remote: "abc\trefs/heads/feat/issue-7-lean", issueComments: [frozen()], issueBodies: [body], viewComments: [{ body: factsRec, ...BOT }, { body: needs, ...BOT }] });
|
|
5710
|
+
|
|
5711
|
+
it("a resumed lane with a frozen contract gets files changed, the failing validate, the end reason and the requirements; RESUME_DIGEST is logged", async () => {
|
|
5712
|
+
const w = resumedWorld(), dir = tmpDir("lean-resume-");
|
|
5713
|
+
const r = await run(w, { dir });
|
|
5714
|
+
assert.equal(r.terminalReason, "merged");
|
|
5715
|
+
const p = builders(w)[0][4];
|
|
5716
|
+
for (const s of ["Resume digest of the last run", "- needs-human (ci.red); failures ci.red", "FORGE:NEEDS_HUMAN: CI red: test/foo.test.js fails on the null case",
|
|
5717
|
+
"- V1 `node --test test/foo.test.js`: exit 1 (failing)", "- R1: foo must handle a null argument without throwing.", "- src/foo.js", "Start from this state: do not re-derive it."]) assert.ok(p.includes(s), `missing ${s}\n${p}`);
|
|
5718
|
+
const e = readLog(dir, 7).find((x) => x.event === "RESUME_DIGEST");
|
|
5719
|
+
assert.ok(e && e.bytes > 0 && e.validates === 1 && e.files === 2 && e.reason === "needs-human", JSON.stringify(e));
|
|
5720
|
+
assert.match(traj(w)[0], /Resume digest given to the builder/);
|
|
5721
|
+
});
|
|
5722
|
+
|
|
5723
|
+
it("a recovery lane gets the digest after the recovery note", async () => {
|
|
5724
|
+
const w = resumedWorld();
|
|
5725
|
+
await run(w, { recover: { code: "ci.red", attempt: 1 } });
|
|
5726
|
+
const p = builders(w)[0][4];
|
|
5727
|
+
assert.ok(p.indexOf("Recovery pass (ci.red)") >= 0 && p.indexOf("Recovery pass (ci.red)") < p.indexOf("Resume digest of the last run"), p);
|
|
5728
|
+
});
|
|
5729
|
+
|
|
5730
|
+
it("a first run gets no digest; context_pack off gives none; the contract quotes are unchanged", async () => {
|
|
5731
|
+
const first = world({ issueBodies: [body], viewComments: [{ body: factsRec, ...BOT }] });
|
|
5732
|
+
await run(first);
|
|
5733
|
+
assert.ok(!builders(first).some((b) => b[4].includes("Resume digest")));
|
|
5734
|
+
const off = resumedWorld(), dir = tmpDir("lean-resume-off-");
|
|
5735
|
+
await run(off, { dir, contextPack: { mode: "off" } });
|
|
5736
|
+
assert.ok(!builders(off).some((b) => b[4].includes("Resume digest")));
|
|
5737
|
+
assert.ok(!readLog(dir, 7).some((x) => x.event === "RESUME_DIGEST"));
|
|
5738
|
+
assert.ok(!off.comments.some((c) => c.includes("FORGE:CONTRACT")), "the frozen contract is reused, not re-planned");
|
|
5739
|
+
});
|
|
5740
|
+
});
|
|
5741
|
+
|
|
5742
|
+
describe("#4395 lane profile", () => {
|
|
5743
|
+
const LANE = "# Hard rules\nNever push to main.";
|
|
5744
|
+
const capture = (w) => {
|
|
5745
|
+
const seen = [], orig = w.io.runner;
|
|
5746
|
+
w.io.runner = async (c) => { seen.push({ role: c.role, resume: c.resume ?? null, system: c.system, env: c.env ?? {}, noTools: c.noTools ?? false }); return orig(c); };
|
|
5747
|
+
return seen;
|
|
5748
|
+
};
|
|
5749
|
+
const usage = (dir) => readLog(dir, 7).filter((e) => e.event === "PHASE_ATTEMPT_USAGE");
|
|
5750
|
+
|
|
5751
|
+
it("without lane_instructions every session has auto-memory off, CLAUDE.md kept and system prompts unchanged", async () => {
|
|
5752
|
+
const w = world(), seen = capture(w), dir = tmpDir("core-lp-");
|
|
5753
|
+
const r = await run(w, { dir });
|
|
5754
|
+
assert.equal(r.terminalReason, "merged");
|
|
5755
|
+
assert.ok(seen.length >= 2);
|
|
5756
|
+
for (const c of seen) { assert.equal(c.env.CLAUDE_CODE_DISABLE_AUTO_MEMORY, "1"); assert.equal("CLAUDE_CODE_DISABLE_CLAUDE_MDS" in c.env, false); }
|
|
5757
|
+
assert.equal(seen.find((c) => c.role === "builder").system, "");
|
|
5758
|
+
assert.equal(seen.find((c) => c.role === "reviewer").system, REVIEWER_SYSTEM);
|
|
5759
|
+
assert.ok(usage(dir).every((e) => e.lane_profile === "memory-off"));
|
|
5760
|
+
assert.match(traj(w)[0], /Lane profile: auto-memory off, CLAUDE\.md loaded/);
|
|
5761
|
+
});
|
|
5762
|
+
|
|
5763
|
+
it("a valid lane file replaces CLAUDE.md: both switches, file appended to fresh sessions only", async () => {
|
|
5764
|
+
const w = world(), seen = capture(w), dir = tmpDir("core-lp-");
|
|
5765
|
+
w.files[`${WT}/devdocs/lane.md`] = `${LANE}\n`;
|
|
5766
|
+
const r = await run(w, { dir, laneInstructions: "devdocs/lane.md" });
|
|
5767
|
+
assert.equal(r.terminalReason, "merged");
|
|
5768
|
+
for (const c of seen) { assert.equal(c.env.CLAUDE_CODE_DISABLE_AUTO_MEMORY, "1"); assert.equal(c.env.CLAUDE_CODE_DISABLE_CLAUDE_MDS, "1"); }
|
|
5769
|
+
const fresh = seen.filter((c) => c.role === "builder" && !c.resume), resumed = seen.filter((c) => c.role === "builder" && c.resume);
|
|
5770
|
+
assert.ok(fresh.length >= 1 && resumed.length >= 1);
|
|
5771
|
+
for (const c of fresh) assert.equal(c.system, LANE);
|
|
5772
|
+
for (const c of resumed) assert.equal(c.system, "", "a resume sends no system prompt");
|
|
5773
|
+
assert.equal(seen.find((c) => c.role === "reviewer").system, `${REVIEWER_SYSTEM}\n\n${LANE}`);
|
|
5774
|
+
assert.ok(usage(dir).every((e) => e.lane_profile === "lane"));
|
|
5775
|
+
assert.match(traj(w)[0], /Lane profile: devdocs\/lane\.md \(\d+ bytes\) instead of CLAUDE\.md/);
|
|
5776
|
+
});
|
|
5777
|
+
|
|
5778
|
+
for (const [path, why] of [["devdocs/missing.md", /not readable/], ["../outside.md", /not a repo-relative path/], ["/etc/passwd", /not a repo-relative path/]]) {
|
|
5779
|
+
it(`a bad lane file (${path}) is an anomaly and falls back to CLAUDE.md with memory off`, async () => {
|
|
5780
|
+
const w = world(), seen = capture(w), dir = tmpDir("core-lp-");
|
|
5781
|
+
const r = await run(w, { dir, laneInstructions: path });
|
|
5782
|
+
assert.equal(r.terminalReason, "merged");
|
|
5783
|
+
for (const c of seen) { assert.equal(c.env.CLAUDE_CODE_DISABLE_AUTO_MEMORY, "1"); assert.equal("CLAUDE_CODE_DISABLE_CLAUDE_MDS" in c.env, false); }
|
|
5784
|
+
assert.equal(seen.find((c) => c.role === "builder").system, "");
|
|
5785
|
+
assert.match(traj(w)[0], new RegExp(`engine\\.lane_instructions .*${why.source}`));
|
|
5786
|
+
});
|
|
5787
|
+
}
|
|
5788
|
+
|
|
5789
|
+
it("an oversized lane file falls back", async () => {
|
|
5790
|
+
const w = world(), seen = capture(w);
|
|
5791
|
+
w.files[`${WT}/big.md`] = "x".repeat(16385);
|
|
5792
|
+
await run(w, { laneInstructions: "big.md" });
|
|
5793
|
+
assert.ok(seen.every((c) => !("CLAUDE_CODE_DISABLE_CLAUDE_MDS" in c.env)));
|
|
5794
|
+
assert.match(traj(w)[0], /over 16384 bytes/);
|
|
5795
|
+
});
|
|
5796
|
+
|
|
5797
|
+
it("FORGEDOCK_FULL_SESSION=1 leaves sessions untouched", async () => {
|
|
5798
|
+
const w = world(), seen = capture(w), dir = tmpDir("core-lp-");
|
|
5799
|
+
w.files[`${WT}/devdocs/lane.md`] = LANE;
|
|
5800
|
+
const prev = process.env.FORGEDOCK_FULL_SESSION; process.env.FORGEDOCK_FULL_SESSION = "1";
|
|
5801
|
+
try { await run(w, { dir, laneInstructions: "devdocs/lane.md" }); } finally { if (prev === undefined) delete process.env.FORGEDOCK_FULL_SESSION; else process.env.FORGEDOCK_FULL_SESSION = prev; }
|
|
5802
|
+
for (const c of seen) { assert.equal("CLAUDE_CODE_DISABLE_AUTO_MEMORY" in c.env, false); assert.equal("CLAUDE_CODE_DISABLE_CLAUDE_MDS" in c.env, false); }
|
|
5803
|
+
assert.equal(seen.find((c) => c.role === "builder").system, "");
|
|
5804
|
+
assert.ok(usage(dir).every((e) => e.lane_profile === "full"));
|
|
5805
|
+
});
|
|
5806
|
+
|
|
5807
|
+
it("PHASE_ATTEMPT_USAGE carries the runner's baseline_tokens", async () => {
|
|
5808
|
+
const w = world(), dir = tmpDir("core-lp-"), orig = w.io.runner;
|
|
5809
|
+
w.io.runner = async (c) => ({ ...(await orig(c)), baselineTokens: c.resume ? null : 21000 });
|
|
5810
|
+
await run(w, { dir });
|
|
5811
|
+
const u = usage(dir);
|
|
5812
|
+
assert.equal(u[0].baseline_tokens, 21000, "the fresh plan call reports its baseline");
|
|
5813
|
+
assert.ok(u.filter((e) => e.phase === "implement" && e.trigger === "build").every((e) => !("baseline_tokens" in e)), "a resumed build reports none");
|
|
5814
|
+
});
|
|
5815
|
+
|
|
5816
|
+
it("#4410 PHASE_ATTEMPT_USAGE carries the runner's tools stats only when present; makeLeanRunner derives them from toolUses", async () => {
|
|
5817
|
+
const tools = { calls: 4, turns: 2, per_turn: 2, rereads: 0, top: [] };
|
|
5818
|
+
const w = world(), dir = tmpDir("core-tools-"), orig = w.io.runner;
|
|
5819
|
+
w.io.runner = async (c) => ({ ...(await orig(c)), tools: c.role === "builder" ? tools : null });
|
|
5820
|
+
await run(w, { dir });
|
|
5821
|
+
const u = usage(dir);
|
|
5822
|
+
assert.ok(u.some((e) => e.phase === "implement" && e.trigger === "build" && e.tools?.per_turn === 2), "a builder attempt carries tools");
|
|
5823
|
+
const rev = readLog(dir, 7).filter((e) => e.event === "PHASE_ATTEMPT_USAGE" && e.phase === "review");
|
|
5824
|
+
assert.ok(rev.length && rev.every((e) => !("tools" in e)), "null tools leave the key absent");
|
|
5825
|
+
const uses = [{ msg: "m1", name: "Read", path: "a", cmd: null }, { msg: "m1", name: "Bash", path: null, cmd: "cat a" }, { msg: "m2", name: "Bash", path: null, cmd: "sed -n 1,2p a" }];
|
|
5826
|
+
const r = await makeLeanRunner({ runCli: async () => ({ text: "", toolUses: uses }) })({ role: "builder", issue: 7, cwd: dir, resume: null, system: "", prompt: "go" });
|
|
5827
|
+
assert.deepEqual(r.tools, { calls: 3, turns: 2, per_turn: 1.5, rereads: 1, top: [{ path: "a", reads: 3 }] });
|
|
5828
|
+
assert.equal((await makeLeanRunner({ runCli: async () => ({ text: "" }) })({ role: "builder", issue: 7, cwd: dir, resume: null, system: "", prompt: "go" })).tools, null);
|
|
5829
|
+
});
|
|
5830
|
+
|
|
5831
|
+
it("#4410 both builder prompts ask for same-turn read-only calls and read-once notes (within budget)", () => {
|
|
5832
|
+
for (const p of [builderPlanPrompt(7), builderImplementPrompt(), builderImplementPrompt("ci")]) {
|
|
5833
|
+
assert.match(p, /Issue independent read-only calls \(reads, greps, git log\/blame, gh\) in the same turn or one shell call\./);
|
|
5834
|
+
assert.match(p, /Read each file's ranges once .* and keep notes; don't re-read it\./);
|
|
5835
|
+
}
|
|
5836
|
+
assert.ok(approxTokens(builderImplementPrompt()) <= 1500 && approxTokens(builderPlanPrompt(7)) <= 1800);
|
|
5837
|
+
});
|
|
5838
|
+
|
|
5839
|
+
it("runCliBackendAsync reports the first stream turn's context as baselineTokens; makeLeanRunner drops it on resume", async () => {
|
|
5840
|
+
const { chmodSync } = await import("node:fs");
|
|
5841
|
+
const { runCliBackendAsync } = await import("../runner.mjs");
|
|
5842
|
+
const d = tmpDir("core-async-");
|
|
5843
|
+
const msg = (id, read) => JSON.stringify({ type: "assistant", message: { id, content: [], usage: { input_tokens: 10, output_tokens: 7, cache_read_input_tokens: read, cache_creation_input_tokens: 300 } } });
|
|
5844
|
+
const result = JSON.stringify({ type: "result", subtype: "success", is_error: false, result: "ok", session_id: "s-1", usage: { input_tokens: 20, output_tokens: 14 } });
|
|
5845
|
+
writeFileSync(`${d}/fake`, `#!/bin/sh\necho '${msg("m1", 5000)}'\necho '${msg("m2", 9000)}'\necho '${result}'\n`); chmodSync(`${d}/fake`, 0o755);
|
|
5846
|
+
const r = await runCliBackendAsync({ spec: { name: "t" }, userMessage: "go", args: ["1"], cwd: d, bin: `${d}/fake`, logger: { log() {} }, stream: true });
|
|
5847
|
+
assert.equal(r.baselineTokens, 5310);
|
|
5848
|
+
const fresh = await makeLeanRunner({ runCli: async () => ({ text: "", baselineTokens: 5310 }) })({ role: "builder", issue: 7, cwd: d, resume: null, system: "", prompt: "go" });
|
|
5849
|
+
assert.equal(fresh.baselineTokens, 5310);
|
|
5850
|
+
const resumed = await makeLeanRunner({ runCli: async () => ({ text: "", baselineTokens: 90000 }) })({ role: "builder", issue: 7, cwd: d, resume: "s-1", system: "", prompt: "go" });
|
|
5851
|
+
assert.equal(resumed.baselineTokens, null);
|
|
5852
|
+
});
|
|
5853
|
+
});
|
|
5854
|
+
|
|
5855
|
+
describe("#4450 light lane: one builder session, derived contract, upgrade to the full lane", () => {
|
|
5856
|
+
const RT = { path: "test/foo.test.js", id: "foo handles null", command: "node --test test/foo.test.js" };
|
|
5857
|
+
const BODY = "foo crashes on null input in src/foo.js.\n\nExpected behaviour: foo returns null.\n\n- foo must handle a null argument without throwing.\n- Existing callers of foo keep working.";
|
|
5858
|
+
const RESULT = { status: "fixed", summary: "fixed foo", requirement: "foo must handle a null argument without throwing.", repro_test: RT, tests: [{ command: RT.command, result: "pass", fails_without_fix: true }] };
|
|
5859
|
+
const lworld = (o = {}) => {
|
|
5860
|
+
const w = world({ labels: [{ name: "bug" }], body: BODY, result: RESULT, contract: { ...CONTRACT, repro_test: RT }, ...o });
|
|
5861
|
+
w.numstat = o.numstat ?? "5\t1\tsrc/foo.js\n20\t0\ttest/foo.test.js\n";
|
|
5862
|
+
w.rgCalls = []; w.ran = [];
|
|
5863
|
+
w.io.redGreen = async (x) => { w.rgCalls.push(x.command); return o.repro ?? { base: "FAILED", head: "RAN" }; };
|
|
5864
|
+
w.io.runCommand = async (a) => { w.ran.push(a.command); return { rc: 0, output: "" }; };
|
|
5865
|
+
w.io.apiShape = async () => o.api ?? { status: "none", changes: [], unknown: [] };
|
|
5866
|
+
return w;
|
|
5867
|
+
};
|
|
5868
|
+
const events = (dir, name) => readLog(dir, 7).filter((e) => e.event === name);
|
|
5869
|
+
const ON = { light: { mode: "on" } };
|
|
5870
|
+
|
|
5871
|
+
it("nothing triggers: exactly one builder session (no plan) plus the review; the derived contract is frozen and posted", async () => {
|
|
5872
|
+
const w = lworld(), dir = tmpDir("light-");
|
|
5873
|
+
const r = await run(w, { dir, ...ON });
|
|
5874
|
+
assert.equal(r.terminalReason, "merged");
|
|
5875
|
+
assert.equal(builders(w).length, 1, "one builder session");
|
|
5876
|
+
assert.ok(!/PLANNING ONLY/.test(builders(w)[0][4]));
|
|
5877
|
+
assert.match(builders(w)[0][4], /failing regression test first/);
|
|
5878
|
+
assert.equal(reviewers(w).length, 1, "plus the review call");
|
|
5879
|
+
const contract = w.comments.find((c) => c.includes("<!-- FORGE:CONTRACT -->"));
|
|
5880
|
+
assert.match(contract, /\| R1 \| foo must handle a null argument without throwing\. \| deliver \|/);
|
|
5881
|
+
assert.match(contract, /"repro_test"/);
|
|
5882
|
+
assert.equal(events(dir, "LIGHT_ELIGIBILITY")[0].verdict, "eligible");
|
|
5883
|
+
assert.equal(events(dir, "LIGHT_DONE").length, 1);
|
|
5884
|
+
assert.equal(events(dir, "LIGHT_UPGRADE").length, 0);
|
|
5885
|
+
assert.deepEqual(events(dir, "CONTRACT_FROZEN").map((e) => e.derived), ["light"]);
|
|
5886
|
+
assert.ok(w.ran.includes(RT.command), "the engine ran the targeted tests");
|
|
5887
|
+
assert.ok(w.rgCalls.length >= 1, "red/green repro gate ran");
|
|
5888
|
+
const lite = w.comments.filter((c) => c.includes("<!-- FORGE:LITE -->"));
|
|
5889
|
+
assert.equal(lite.length, 1, "#4452: one FORGE:LITE receipt");
|
|
5890
|
+
assert.match(lite[0], /\*\*Outcome:\*\* light/);
|
|
5891
|
+
assert.match(lite[0], /\*\*Diff:\*\* 2 file\(s\), \+25\/-1/);
|
|
5892
|
+
assert.match(lite[0], /\*\*Red\/green:\*\* base FAILED → head RAN/);
|
|
5893
|
+
assert.match(lite[0], /targeted tests: 1 command\(s\), passed/);
|
|
5894
|
+
assert.match(lite[0], /\*\*Cost:\*\* \$\d+\.\d{4} \(true per-session cost/);
|
|
5895
|
+
assert.match(lite[0], /\*\*Eligibility:\*\* eligible \(mode on\)/);
|
|
5896
|
+
assert.equal(typeof events(dir, "LIGHT_DONE")[0].cost_usd, "number");
|
|
5897
|
+
});
|
|
5898
|
+
|
|
5899
|
+
for (const [name, code, o] of [
|
|
5900
|
+
["oversized diff", "diff_lines", { numstat: "80\t3\tsrc/foo.js\n20\t0\ttest/foo.test.js\n" }],
|
|
5901
|
+
["non-red repro", "repro_not_red", { repro: { base: "PASSED", head: "RAN" } }],
|
|
5902
|
+
["API change", "api_shape", { api: { status: "changed", changes: [{ file: "src/foo.js", symbol: "foo", change: "sync-async", before: "def", after: "async def" }], unknown: [] } }],
|
|
5903
|
+
["risk path", "risk_path", { changed: ["src/auth/session.js", "test/foo.test.js"], numstat: "4\t0\tsrc/auth/session.js\n" }],
|
|
5904
|
+
]) {
|
|
5905
|
+
it(`${name}: upgrades to the full lane with the digest, keeping the light commits`, async () => {
|
|
5906
|
+
const w = lworld(o), dir = tmpDir("light-up-");
|
|
5907
|
+
await run(w, { dir, ...ON });
|
|
5908
|
+
const up = events(dir, "LIGHT_UPGRADE");
|
|
5909
|
+
assert.equal(up.length, 1);
|
|
5910
|
+
assert.ok(up[0].reasons.some((x) => x.code === code), JSON.stringify(up[0].reasons));
|
|
5911
|
+
const b = builders(w);
|
|
5912
|
+
assert.ok(b.length >= 3, "light session, then the full lane's plan and implement");
|
|
5913
|
+
assert.ok(!/PLANNING ONLY/.test(b[0][4]));
|
|
5914
|
+
assert.match(b[1][4], /PLANNING ONLY/);
|
|
5915
|
+
assert.match(b[1][4], /This is a rerun: the branch already holds commits/);
|
|
5916
|
+
assert.match(b[1][4], /Resume digest of the last run on this branch/);
|
|
5917
|
+
assert.match(b[1][4], new RegExp(`Light-lane upgrade[\\s\\S]*- ${code}:`));
|
|
5918
|
+
const later = w.calls.slice(w.calls.indexOf(b[0]), w.calls.indexOf(b[2])).filter((c) => c[0] === "git"); // light session -> full implement
|
|
5919
|
+
assert.ok(!later.some((c) => ["reset", "rebase", "clean"].includes(c[1]) || (c[1] === "checkout" && c.includes("-B")) || (c[1] === "branch" && c.includes("-D")) || (c[1] === "push" && c.some((x) => /^--force|^-f$/.test(String(x))))), "the light work is never discarded");
|
|
5920
|
+
assert.equal(events(dir, "LIGHT_DONE").length, 0);
|
|
5921
|
+
const lite = w.comments.filter((c) => c.includes("<!-- FORGE:LITE -->"));
|
|
5922
|
+
assert.equal(lite.length, 1, "#4452: the upgraded light lane still posts its receipt");
|
|
5923
|
+
assert.match(lite[0], new RegExp(`\\*\\*Upgrade reasons:\\*\\*[\\s\\S]*- ${code}:`));
|
|
5924
|
+
});
|
|
5925
|
+
}
|
|
5926
|
+
|
|
5927
|
+
it("#4453 the run result carries light, the eligibility verdict and the upgrade reason codes (for the three-way bench)", async () => {
|
|
5928
|
+
const lit = await run(lworld(), { dir: tmpDir("light-res-"), ...ON });
|
|
5929
|
+
assert.equal(lit.light, true); assert.equal(lit.light_eligibility, "eligible"); assert.ok(!("light_upgrade" in lit));
|
|
5930
|
+
const up = await run(lworld({ repro: { base: "PASSED", head: "RAN" } }), { dir: tmpDir("light-res-up-"), ...ON });
|
|
5931
|
+
assert.equal(up.light, false); assert.equal(up.light_eligibility, "eligible"); assert.ok(up.light_upgrade.includes("repro_not_red"), JSON.stringify(up.light_upgrade));
|
|
5932
|
+
const off = await run(lworld(), { dir: tmpDir("light-res-off-"), light: { mode: "off" } });
|
|
5933
|
+
assert.equal(off.light, false); assert.equal(off.light_eligibility, "off"); assert.ok(!("light_upgrade" in off));
|
|
5934
|
+
assert.equal(typeof off.cost_usd, "number"); assert.ok(Number.isFinite(off.wall_ms) && Number.isFinite(off.model_ms), "#4451 fields unchanged");
|
|
5935
|
+
});
|
|
5936
|
+
|
|
5937
|
+
it("shadow (the default) only reports the verdict: the lane keeps plan -> implement", async () => {
|
|
5938
|
+
const w = lworld(), dir = tmpDir("light-sh-");
|
|
5939
|
+
assert.equal((await run(w, { dir })).terminalReason, "merged");
|
|
5940
|
+
const e = events(dir, "LIGHT_ELIGIBILITY")[0];
|
|
5941
|
+
assert.equal(e.mode, "shadow"); assert.equal(e.verdict, "eligible"); assert.equal(e.light, false);
|
|
5942
|
+
assert.equal(builders(w).length, 2);
|
|
5943
|
+
assert.match(builders(w)[0][4], /PLANNING ONLY/);
|
|
5944
|
+
assert.ok(!/Light-lane upgrade/.test(builders(w)[0][4]));
|
|
5945
|
+
assert.ok(!w.comments.some((c) => c.includes("<!-- FORGE:LITE -->")), "#4452: no receipt outside a light lane");
|
|
5946
|
+
});
|
|
5947
|
+
|
|
5948
|
+
/** #4451: lworld plus counting fakes for the overhead the light lane skips; each call notes its position in w.calls */
|
|
5949
|
+
const overhead = (o = {}) => {
|
|
5950
|
+
const w = lworld(o);
|
|
5951
|
+
w.over = { pack: [], hot: [], structure: [] };
|
|
5952
|
+
w.io.contextFacts = async () => { w.over.pack.push(w.calls.length); return { sources: {}, items: [] }; };
|
|
5953
|
+
w.io.hotspots = async (x) => { w.over.hot.push(w.calls.length); return { enabled: true, files: [], base: x.base ?? null }; };
|
|
5954
|
+
w.io.structureDelta = async () => { w.over.structure.push(w.calls.length); return { package_edges_added: [], package_edges_removed: [], import_pairs: [] }; };
|
|
5955
|
+
return w;
|
|
5956
|
+
};
|
|
5957
|
+
it("#4451 a light lane that is not upgraded makes no context-pack, structure or hotspot calls", async () => {
|
|
5958
|
+
const w = overhead(), dir = tmpDir("light-over-");
|
|
5959
|
+
const r = await run(w, { dir, ...ON, contextPack: { mode: "on" } });
|
|
5960
|
+
assert.equal(r.terminalReason, "merged");
|
|
5961
|
+
assert.equal(events(dir, "LIGHT_DONE").length, 1);
|
|
5962
|
+
assert.deepEqual(w.over, { pack: [], hot: [], structure: [] });
|
|
5963
|
+
assert.equal(events(dir, "CONTEXT_PACK").length, 0);
|
|
5964
|
+
// only the merge-time record (local numstat, no structure-delta call) remains; no implement-time delta
|
|
5965
|
+
assert.deepEqual(events(dir, "STRUCTURE_DELTA").map((e) => e.phase), ["merge"]);
|
|
5966
|
+
assert.equal(events(dir, "RUN_TERMINAL")[0].light, true);
|
|
5967
|
+
});
|
|
5968
|
+
it("#4451 an upgraded light lane computes the hotspots and the context pack before the plan call and runs the structure delta", async () => {
|
|
5969
|
+
const w = overhead({ numstat: "80\t3\tsrc/foo.js\n20\t0\ttest/foo.test.js\n" }), dir = tmpDir("light-over-up-");
|
|
5970
|
+
await run(w, { dir, ...ON, contextPack: { mode: "on" } });
|
|
5971
|
+
assert.equal(events(dir, "LIGHT_UPGRADE").length, 1);
|
|
5972
|
+
const plan = w.calls.indexOf(builders(w)[1]);
|
|
5973
|
+
// each fake notes w.calls.length when called, i.e. the index the next call takes: <= plan means before the plan call
|
|
5974
|
+
assert.ok(w.over.hot.some((i) => i <= plan) && w.over.pack.some((i) => i <= plan), JSON.stringify({ plan, ...w.over }));
|
|
5975
|
+
assert.ok(w.over.hot.every((i) => i > w.calls.indexOf(builders(w)[0])), "not before the light session");
|
|
5976
|
+
assert.ok(w.over.structure.length >= 1);
|
|
5977
|
+
assert.equal(events(dir, "RUN_TERMINAL")[0].light, false);
|
|
5978
|
+
});
|
|
5979
|
+
it("#4451 a full lane (light off) keeps its context-pack, structure and hotspot calls", async () => {
|
|
5980
|
+
const w = overhead(), dir = tmpDir("light-over-off-");
|
|
5981
|
+
await run(w, { dir, light: { mode: "off" }, contextPack: { mode: "on" } });
|
|
5982
|
+
assert.ok(w.over.pack.length >= 1 && w.over.hot.length >= 1 && w.over.structure.length >= 1, JSON.stringify(w.over));
|
|
5983
|
+
});
|
|
5984
|
+
it("#4451 the light targeted tests run concurrently, never more than FORGE_HOST_PARALLELISM in flight", async () => {
|
|
5985
|
+
const cmds = ["node --test t1.js", "node --test t2.js", "node --test t3.js", "node --test t4.js"];
|
|
5986
|
+
const w = lworld({ result: { ...RESULT, tests: cmds.map((command) => ({ command, result: "pass" })) } }), dir = tmpDir("light-par-");
|
|
5987
|
+
let live = 0, max = 0;
|
|
5988
|
+
const seen = [];
|
|
5989
|
+
w.io.runCommand = async (a) => { live++; max = Math.max(max, live); seen.push(a.env?.FORGE_HOST_PARALLELISM); await new Promise((r) => setTimeout(r, 5)); live--; return { rc: 0, output: "" }; };
|
|
5990
|
+
const prev = process.env.FORGE_HOST_PARALLELISM;
|
|
5991
|
+
process.env.FORGE_HOST_PARALLELISM = "2";
|
|
5992
|
+
try { await run(w, { dir, ...ON }); } finally { if (prev === undefined) delete process.env.FORGE_HOST_PARALLELISM; else process.env.FORGE_HOST_PARALLELISM = prev; }
|
|
5993
|
+
assert.equal(events(dir, "LIGHT_DONE").length, 1);
|
|
5994
|
+
assert.equal(max, 2, "two in flight at once, capped at the host parallelism");
|
|
5995
|
+
assert.ok(seen.every((x) => x === undefined || x === "1"), `each command gets its worker share: ${seen}`);
|
|
5996
|
+
});
|
|
5997
|
+
it("#4451 lane telemetry records wall time and model time separately", async () => {
|
|
5998
|
+
const w = lworld(), dir = tmpDir("light-time-");
|
|
5999
|
+
const r = await run(w, { dir, ...ON });
|
|
6000
|
+
const model = events(dir, "PHASE_ATTEMPT_USAGE").reduce((n, e) => n + e.duration_ms, 0);
|
|
6001
|
+
assert.ok(model > 0);
|
|
6002
|
+
assert.equal(r.model_ms, model);
|
|
6003
|
+
assert.ok(Number.isFinite(r.wall_ms));
|
|
6004
|
+
const term = w.telemetry.find((t) => t.event === "RUN_TERMINAL");
|
|
6005
|
+
assert.equal(term.model_ms, model); assert.ok(Number.isFinite(term.wall_ms));
|
|
6006
|
+
});
|
|
6007
|
+
|
|
6008
|
+
it("a resumed lane never goes light, even in mode on", async () => {
|
|
6009
|
+
const w = lworld({ remote: "abc\trefs/heads/feat/issue-7-lean\n" }), dir = tmpDir("light-res-");
|
|
6010
|
+
await run(w, { dir, ...ON });
|
|
6011
|
+
assert.equal(events(dir, "LIGHT_DONE").length + events(dir, "LIGHT_UPGRADE").length, 0);
|
|
6012
|
+
});
|
|
4424
6013
|
});
|