forgedock 1.35.0 → 1.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -324
- package/bin/bot-token-status.mjs +26 -0
- package/bin/engine/events.mjs +161 -0
- package/bin/engine/events.test.mjs +142 -0
- package/bin/engine/outcome-codes.mjs +39 -1
- package/bin/engine/phases.mjs +65 -5
- package/bin/engine/telemetry-seam.mjs +5 -4
- package/bin/engine/telemetry-seam.test.mjs +4 -0
- package/bin/engine-cli.mjs +372 -46
- package/bin/engine.mjs +3 -1
- package/bin/experiment.mjs +55 -0
- package/bin/forgedock.mjs +108 -5
- package/bin/hooks/reread-nudge.mjs +15 -4
- package/bin/labels.json +50 -0
- package/bin/lean/batch.mjs +52 -6
- package/bin/lean/batch.test.mjs +176 -2
- package/bin/lean/conformance.e2e.test.mjs +1 -1
- package/bin/lean/conformance.mjs +136 -11
- package/bin/lean/conformance.test.mjs +148 -1
- package/bin/lean/decisions.mjs +181 -0
- package/bin/lean/decisions.test.mjs +71 -0
- package/bin/lean/effects.mjs +32 -14
- package/bin/lean/execution.mjs +62 -16
- package/bin/lean/execution.test.mjs +59 -2
- package/bin/lean/gatewatch.mjs +31 -0
- package/bin/lean/gatewatch.test.mjs +59 -0
- package/bin/lean/guardgate.mjs +170 -0
- package/bin/lean/guardgate.test.mjs +153 -0
- package/bin/lean/guards.mjs +243 -0
- package/bin/lean/guards.test.mjs +177 -0
- package/bin/lean/heavy.mjs +54 -8
- package/bin/lean/heavy.test.mjs +74 -1
- package/bin/lean/hostkill.mjs +1 -1
- package/bin/lean/i18n.mjs +65 -0
- package/bin/lean/i18n.test.mjs +47 -0
- package/bin/lean/integration.mjs +276 -0
- package/bin/lean/integration.test.mjs +189 -0
- package/bin/lean/lane.mjs +5 -2
- package/bin/lean/lean.mjs +169 -30
- package/bin/lean/lean.test.mjs +360 -24
- package/bin/lean/lifecycle.mjs +129 -0
- package/bin/lean/lifecycle.test.mjs +118 -0
- package/bin/lean/light.mjs +1 -1
- package/bin/lean/milestonesync.mjs +130 -0
- package/bin/lean/milestonesync.test.mjs +133 -0
- package/bin/lean/orchestrate.mjs +132 -20
- package/bin/lean/parked-sweep.mjs +45 -0
- package/bin/lean/parked-sweep.test.mjs +71 -0
- package/bin/lean/postdeploy-check.mjs +181 -0
- package/bin/lean/postdeploy-check.test.mjs +206 -0
- package/bin/lean/postdeploy-monitor.mjs +97 -0
- package/bin/lean/postdeploy-monitor.test.mjs +147 -0
- package/bin/lean/precheck.mjs +6 -4
- package/bin/lean/precheck.test.mjs +17 -2
- package/bin/lean/prefilter.mjs +106 -21
- package/bin/lean/prefilter.test.mjs +86 -6
- package/bin/lean/probe.mjs +4 -3
- package/bin/lean/probe.test.mjs +1 -1
- package/bin/lean/prompts.mjs +23 -9
- package/bin/lean/resume.test.mjs +1 -1
- package/bin/lean/retry.mjs +28 -0
- package/bin/lean/sandbox-provider.contract.test.mjs +36 -3
- package/bin/lean/sandbox-remote.mjs +11 -4
- package/bin/lean/schedd.mjs +77 -13
- package/bin/lean/schedd.test.mjs +131 -5
- package/bin/lean/sequence.mjs +24 -0
- package/bin/lean/sequence.test.mjs +31 -1
- package/bin/lean/signoff.mjs +66 -0
- package/bin/lean/signoff.test.mjs +102 -0
- package/bin/lean/train.mjs +177 -0
- package/bin/lean/train.test.mjs +113 -0
- package/bin/pool/cli.mjs +2 -0
- package/bin/pool/health.mjs +30 -0
- package/bin/review-context.mjs +298 -0
- package/bin/runner.mjs +26 -3
- package/bin/stats/aggregate.mjs +1 -1
- package/bin/stats/compounding.mjs +157 -0
- package/bin/stats/compounding.test.mjs +74 -0
- package/bin/stats/experiment.mjs +44 -0
- package/bin/stats/github-source.mjs +2 -1
- package/bin/stats/index.mjs +62 -3
- package/bin/stats/release.mjs +75 -0
- package/bin/stats/release.test.mjs +68 -0
- package/commands/bootstrap.md +10 -8
- package/commands/deploy-info.md +3 -0
- package/commands/issue.md +2 -0
- package/commands/milestone.md +43 -54
- package/commands/orchestrate/phase-4-execution.md +2 -0
- package/commands/orchestrate.md +15 -3
- package/commands/pipeline-status.md +20 -0
- package/commands/review-pr.md +1 -0
- package/commands/work-on/build/implement.md +1 -1
- package/commands/work-on/build.md +4 -2
- package/commands/work-on/investigate.md +3 -1
- package/commands/work-on/remediate.md +1 -1
- package/docs/CONFIG.md +120 -2
- package/docs/EVENTS.md +65 -0
- package/docs/OUTCOME-CODES.md +9 -1
- package/docs/STATS.md +51 -0
- package/docs/TELEMETRY.md +3 -1
- package/docs/site/install.md +89 -0
- package/docs/site/start-a-new-project.md +1 -1
- package/docs/spec/label-state-machine.md +6 -0
- package/package.json +1 -1
- package/pool/install/install-agent.sh +4 -2
- package/pool/install/install-router.sh +4 -2
- package/pool/install/install-schedd.sh +28 -0
- package/pool/install/unit-hardening.sh +34 -0
- package/scripts/acceptance-red-green.sh +19 -2
- package/scripts/acceptance-red-green.test.sh +2 -0
- package/scripts/bench-lightlane.mjs +2 -1
- package/scripts/bootstrap-create.mjs +3 -2
- package/scripts/bootstrap-validate.mjs +31 -4
- package/scripts/eval-gate-scorecard.mjs +7 -0
- package/scripts/file-deferred-followups.sh +286 -77
- package/scripts/file-deferred-followups.test.sh +117 -2
- package/scripts/hotspots.sh +23 -2
- package/scripts/hotspots.test.sh +9 -0
- package/scripts/plan-validate-live.mjs +111 -0
- package/scripts/readiness.mjs +141 -0
- package/scripts/review-prepare.sh +17 -1
- package/scripts/review-prepare.test.sh +17 -0
- package/scripts/settle-scope.mjs +74 -0
- package/scripts/systemd/forgedock-bot-token-refresh.service +7 -0
- package/scripts/systemd/forgedock-bot-token-refresh.timer +11 -0
- package/scripts/wait-ci-green.sh +14 -3
- package/scripts/wait-ci-green.test.sh +3 -1
- package/templates/bootstrap/needs-human-issue.md +4 -1
- package/templates/github-actions/migration-journal-guard.test.mjs +26 -0
package/bin/lean/lean.test.mjs
CHANGED
|
@@ -4,11 +4,13 @@ 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 { execFileSync } from "node:child_process";
|
|
7
8
|
import { writeFileSync, existsSync, readFileSync, mkdirSync, symlinkSync, readdirSync } from "node:fs";
|
|
8
9
|
import { join } from "node:path";
|
|
9
10
|
import { runCore, contractHash, isProtectedLaneBranch, classifyBlock } from "./lean.mjs";
|
|
10
11
|
import { parseFactsComment, normaliseFacts, renderFactsRecord, FACTS_MARKER } from "./facts.mjs";
|
|
11
12
|
import { makeLedger } from "./ledger.mjs";
|
|
13
|
+
import { writeIndex } from "./guards.mjs";
|
|
12
14
|
import { parseVerify } from "./verify.mjs";
|
|
13
15
|
import { worktreeFingerprint } from "./precheck.mjs";
|
|
14
16
|
import { renderInvestigation, renderPlan, renderImplementation, renderReview, renderPrBody, renderTrajectory, renderStructure, renderContract, contractJson, parseContractComment, sanitizeMd, capComment, section, cell, NR, MAX_COMMENT_CHARS } from "./render.mjs";
|
|
@@ -19,13 +21,14 @@ import { mapRecord } from "../engine/telemetry-seam.mjs";
|
|
|
19
21
|
import { readLog as readRepoLog } from "../engine/runlog.mjs";
|
|
20
22
|
// #4432: the run log is keyed by repo + issue; every lane in this file runs in repo o/r.
|
|
21
23
|
const readLog = (dir, issue, repo = "o/r") => readRepoLog(dir, issue, { repo });
|
|
22
|
-
import { makeRunTmp, wrapLaneLimits, systemdRunAvailable, makeLeanRunner, requiredChecksFor, procsInDir, makeLeanIo, makeGhCache } from "./effects.mjs";
|
|
23
|
-
import { retrying, ghCacheRetry, PUSH_TRANSIENT_RE } from "./retry.mjs";
|
|
24
|
+
import { makeRunTmp, wrapLaneLimits, laneCapNotice, systemdRunAvailable, makeLeanRunner, requiredChecksFor, procsInDir, makeLeanIo, makeGhCache } from "./effects.mjs";
|
|
25
|
+
import { retrying, ghCacheRetry, refreshAuthOnce, GH_AUTH_RE, PUSH_TRANSIENT_RE } from "./retry.mjs";
|
|
24
26
|
import { checkAdmission, hostParallelism } from "./admission.mjs";
|
|
25
27
|
import { OUTCOME_CODES, classifyNeedsHuman } from "../engine/outcome-codes.mjs";
|
|
26
28
|
import * as refactorMod from "./refactor.mjs";
|
|
27
29
|
import { readAdmission, readConformanceMode, readReviewIncomplete, readEnvConfig, readContextPack } from "../engine-cli.mjs";
|
|
28
30
|
import { prepareToolchain } from "./toolchain.mjs";
|
|
31
|
+
import { isExternalKill } from "./hostkill.mjs";
|
|
29
32
|
import { runFromCli, orchestrateFromCli, ENGINE_PATHS, readMaintenance } from "../engine-cli.mjs";
|
|
30
33
|
// orchestrateFromCli opens the host lane registry (#4191): keep it in a temp dir.
|
|
31
34
|
process.env.FORGEDOCK_SCHED_DIR = tmpDir("lean-sched-");
|
|
@@ -124,6 +127,8 @@ function world(o = {}) {
|
|
|
124
127
|
if (a[0] === "diff" && a[1] === "--check") return "";
|
|
125
128
|
if (a[0] === "grep") return w.markers ? "src/foo.js:3:<<<<<<< HEAD" : "";
|
|
126
129
|
if (a[0] === "merge" && a[1] === "--abort") { w.mergeInProgress = false; return ""; }
|
|
130
|
+
if (a[0] === "rev-parse" && a.length === 2 && (o.shipped ?? []).includes(String(a[1]).replace(/^origin\//, ""))) return `tip-${a[1]}`; // #4623
|
|
131
|
+
if (a[0] === "rev-list" && a.includes("--merges")) { const l = String(a.at(-1)).slice(1).replace(/^origin\//, ""); return (o.shipped ?? []).includes(l) ? `m1 s1 tip-origin/${l}\n` : ""; }
|
|
127
132
|
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
|
|
128
133
|
if (a[0] === "rev-parse") return w.head;
|
|
129
134
|
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");
|
|
@@ -131,7 +136,7 @@ function world(o = {}) {
|
|
|
131
136
|
if (a[0] === "diff" && a[1] === "--numstat") return w.numstat ?? "";
|
|
132
137
|
if (a[0] === "ls-files") return [...new Set(["src/foo.js", "src/bar.js", "test/foo.test.js", ...w.changed])].join("\n");
|
|
133
138
|
if (a[0] === "diff") return "diff --git a/src/foo.js b/src/foo.js\n+fix";
|
|
134
|
-
if (a[0] === "merge-base" && a[1] === "--is-ancestor") { if ((o.shipped ?? []).includes(String(a[2]).replace(/^origin\//, ""))) return ""; throw new Error("not an ancestor"); } // #4593
|
|
139
|
+
if (a[0] === "merge-base" && a[1] === "--is-ancestor") { if ([...(o.shipped ?? []), ...(o.unusedAncestors ?? [])].includes(String(a[2]).replace(/^origin\//, ""))) return ""; throw new Error("not an ancestor"); } // #4593
|
|
135
140
|
if (a[0] === "ls-remote" && String(a.at(-1)).startsWith("refs/heads/")) { const b = a.at(-1).slice("refs/heads/".length); return (o.branchesOnOrigin ?? ["staging", "main"]).includes(b) ? `abc\trefs/heads/${b}\n` : ""; }
|
|
136
141
|
if (a[0] === "ls-remote") return w.remote;
|
|
137
142
|
if (a[0] === "show" && w.baseFiles) { if (!(a[1] in w.baseFiles)) throw new Error(`fatal: path not in ${a[1]}`); return w.baseFiles[a[1]]; }
|
|
@@ -352,7 +357,7 @@ describe("core work-on", () => {
|
|
|
352
357
|
});
|
|
353
358
|
|
|
354
359
|
it("a reviewer that judges a deferral unjustified gets the builder resumed to implement it; nothing is filed for it", async () => {
|
|
355
|
-
const k = { ...CONTRACT, preserve: [], requirements: [CONTRACT.requirements[0], { id: "R2", quote: "Existing callers of foo keep working.", disposition: "defer", why: "later" }] };
|
|
360
|
+
const k = { ...CONTRACT, preserve: [], requirements: [CONTRACT.requirements[0], { id: "R2", quote: "Existing callers of foo keep working.", disposition: "defer", why: "later", bucket: "new-issue" }] };
|
|
356
361
|
const rej = conf({ requirements: [{ id: "R1", status: "implemented", evidence: "src/foo.js:3", validated_by: "V1" }, { id: "R2", status: "deferred", deferral_ok: false, note: "one line in bar.js; do it now" }] }, k);
|
|
357
362
|
const w = world({ contract: k, claims: [{ id: "R1", status: "implemented" }], reviews: [rej, rej] });
|
|
358
363
|
const r = await run(w);
|
|
@@ -439,6 +444,20 @@ describe("core work-on", () => {
|
|
|
439
444
|
noNeedsHuman(w);
|
|
440
445
|
});
|
|
441
446
|
|
|
447
|
+
it("#4686 a needs-human dependency whose PR is already on the lane is not dependency-blocked", async () => {
|
|
448
|
+
const w = depWorld({ changed: [], result: { status: "blocked", summary: "upstream", block: { kind: "dependency", issues: [3] } } },
|
|
449
|
+
{ 3: { state: "OPEN", labels: [{ name: "needs-human" }] } });
|
|
450
|
+
const gh = w.io.gh, git = w.io.git, seen = [];
|
|
451
|
+
w.io.gh = async (a) => (a[0] === "pr" && a[1] === "list" && a.includes("merged")
|
|
452
|
+
? JSON.stringify([{ number: 88, mergeCommit: { oid: "deadbeefcafe0000" }, closingIssuesReferences: [{ number: 3 }], title: "", body: "" }])
|
|
453
|
+
: gh(a));
|
|
454
|
+
w.io.git = async (a, o) => { if (a[0] === "merge-base" && a[2] === "deadbeefcafe0000") { seen.push(a.join(" ")); return ""; } return git(a, o); };
|
|
455
|
+
const r = await run(w);
|
|
456
|
+
assert.notEqual(r.terminalReason, "dependency-blocked");
|
|
457
|
+
assert.notEqual(r.terminalReason, "dependency-open");
|
|
458
|
+
assert.ok(seen.some((c) => c.includes("deadbeefcafe0000") && c.includes("origin/")), seen.join("|"));
|
|
459
|
+
});
|
|
460
|
+
|
|
442
461
|
it("#4124 split-required files sub-issues with Parent / Depends-on lines and ends decomposed", async () => {
|
|
443
462
|
const w = depWorld({ changed: [], result: { status: "blocked", summary: "epic", block: { kind: "split-required", subissues: [
|
|
444
463
|
{ title: "part A", body: "do A", paths: ["src/a"] }, { title: "part B", body: "do B", paths: ["src/b"], depends_on: [0] }] } } }, {});
|
|
@@ -656,6 +675,10 @@ describe("core work-on", () => {
|
|
|
656
675
|
const remote = { placement: { where: "remote", reason: "mode remote" }, hostCaps: { hostCapabilities: ["sops"] } };
|
|
657
676
|
assert.equal(await rb("this lane has no sops/age key", remote), "env.capability_remote");
|
|
658
677
|
assert.equal(await rb("owner must create a third-party account", remote), "builder.blocked");
|
|
678
|
+
// #4685: a missing tool is never a credential block on a remote lane; local lanes and real credential needs stay human.
|
|
679
|
+
assert.equal(await rb("validate V1 needs promtool via docker; no docker daemon in this sandbox", remote), "env.tool_missing");
|
|
680
|
+
assert.equal(await rb("ModuleNotFoundError: No module named 'yaml'", remote), "env.tool_missing");
|
|
681
|
+
assert.equal(await rb("promtool: command not found", { ...remote, placement: { where: "local", reason: "x" } }), "builder.blocked");
|
|
659
682
|
assert.equal(await rb("this lane has no sops/age key", { ...remote, hostCaps: undefined }), "builder.blocked");
|
|
660
683
|
assert.equal(await rb("this lane has no sops/age key", { ...remote, placement: { where: "local", reason: "x" } }), "builder.blocked");
|
|
661
684
|
assert.equal(await code(world({ changed: [] })), "builder.no_commit");
|
|
@@ -717,7 +740,7 @@ describe("core work-on", () => {
|
|
|
717
740
|
|
|
718
741
|
describe("conformance gate (frozen contract, precheck, structured review)", () => {
|
|
719
742
|
const Q2 = "Existing callers of foo keep working.";
|
|
720
|
-
const two = (d2 = "deliver", extra = {}) => ({ ...CONTRACT, preserve: [], requirements: [CONTRACT.requirements[0], { id: "R2", quote: Q2, disposition: d2, ...extra }],
|
|
743
|
+
const two = (d2 = "deliver", extra = {}) => ({ ...CONTRACT, preserve: [], requirements: [CONTRACT.requirements[0], { id: "R2", quote: Q2, disposition: d2, ...(d2 === "defer" || d2 === "post-deploy" ? { bucket: "post-deploy" } : {}), ...extra }],
|
|
721
744
|
deliverables: [...CONTRACT.deliverables, ...(d2 === "deliver" ? [{ id: "D2", path: "src/bar.js", change: "keep callers", requirements: ["R2"] }] : [])] });
|
|
722
745
|
const files2 = ["src/foo.js", "src/bar.js", "test/foo.test.js"];
|
|
723
746
|
const events = (dir) => readLog(dir, 7).map((e) => e.event);
|
|
@@ -783,7 +806,7 @@ describe("conformance gate (frozen contract, precheck, structured review)", () =
|
|
|
783
806
|
assert.equal(r.terminalReason, "merged");
|
|
784
807
|
assert.equal(w.followups.length, 1);
|
|
785
808
|
assert.deepEqual(w.followups[0].items.map((i) => i.key), ["R2: Existing callers of foo keep working."]);
|
|
786
|
-
assert.match(w.followups[0].items[0].why, /^
|
|
809
|
+
assert.match(w.followups[0].items[0].why, /^post-deploy: \[bucket:post-deploy\] callers live in another module/);
|
|
787
810
|
const body = w.prs[0][w.prs[0].indexOf("--body") + 1];
|
|
788
811
|
assert.match(body, /## Remaining work tracked\n- #900\n\nCloses #7$/);
|
|
789
812
|
assert.ok(w.comments.some((c) => c.includes("<!-- FORGE:TRACKED -->") && c.includes("#900")));
|
|
@@ -794,8 +817,9 @@ describe("conformance gate (frozen contract, precheck, structured review)", () =
|
|
|
794
817
|
|
|
795
818
|
for (const d of ["defer", "post-deploy"]) {
|
|
796
819
|
it(`a contracted ${d} is filed only after the reviewer accepts it, then linked in the PR and on close`, async () => {
|
|
797
|
-
const k = two(d, { why: d === "defer" ? "separate module" : "only measurable after deploy" });
|
|
820
|
+
const k = two(d, { why: d === "defer" ? "separate module" : "only measurable after deploy", ...(d === "post-deploy" ? { check: "login-smoke" } : {}) });
|
|
798
821
|
const w = world({ contract: k, claims: [{ id: "R1", status: "implemented" }], reviews: [conf({}, { ...k, requirements: [k.requirements[0], { ...k.requirements[1], tracked: "#900" }] })] });
|
|
822
|
+
if (d === "post-deploy") w.baseFiles = { "origin/staging:.forge/post-deploy-checks.json": JSON.stringify({ "login-smoke": "true" }) };
|
|
799
823
|
const r = await run(w);
|
|
800
824
|
assert.equal(r.terminalReason, "merged");
|
|
801
825
|
assert.equal(w.followups.length, 1); assert.equal(w.followups[0].parent, 7);
|
|
@@ -1374,6 +1398,20 @@ describe("conformance shadow mode (the default)", () => {
|
|
|
1374
1398
|
const ra = await runShadow(a);
|
|
1375
1399
|
assert.equal(ra.code, "conformance.unimplemented"); assert.match(nhBody(a), /amendment without why/);
|
|
1376
1400
|
});
|
|
1401
|
+
it("#4737 decision records are persisted after the merge, never at gate time", async () => {
|
|
1402
|
+
const dir = tmpDir("dec-"); const prev = process.env.FORGEDOCK_DECISIONS_DIR; process.env.FORGEDOCK_DECISIONS_DIR = dir;
|
|
1403
|
+
try {
|
|
1404
|
+
const amendments = [{ kind: "decision", why: "w", decision: { choice: "use X", anchors: ["src/db.ts"], why: "w" } }];
|
|
1405
|
+
const bad = world({ validation: [], amendments });
|
|
1406
|
+
assert.equal((await runShadow(bad)).code, "conformance.unimplemented");
|
|
1407
|
+
assert.ok(!bad.comments.some((c) => c.includes("FORGE:DECISION")), "a lane that never merges records nothing");
|
|
1408
|
+
assert.equal(readdirSync(dir).length, 0, "no index written");
|
|
1409
|
+
const ok = world({ amendments });
|
|
1410
|
+
assert.equal((await runShadow(ok)).terminalReason, "merged");
|
|
1411
|
+
assert.ok(ok.comments.some((c) => c.includes("FORGE:DECISION")), "recorded after the merge");
|
|
1412
|
+
assert.equal(readdirSync(dir).length, 1);
|
|
1413
|
+
} finally { if (prev === undefined) delete process.env.FORGEDOCK_DECISIONS_DIR; else process.env.FORGEDOCK_DECISIONS_DIR = prev; }
|
|
1414
|
+
});
|
|
1377
1415
|
it("#4160 scope creep found by the precheck stays recorded-only in shadow", async () => {
|
|
1378
1416
|
const w = world({ changed: ["src/foo.js", "src/billing.js", "test/foo.test.js"], reviews: [conf({}, CONTRACT, ["src/foo.js", "src/billing.js", "test/foo.test.js"])] });
|
|
1379
1417
|
assert.equal((await runShadow(w)).terminalReason, "merged");
|
|
@@ -1386,7 +1424,7 @@ describe("conformance shadow mode (the default)", () => {
|
|
|
1386
1424
|
});
|
|
1387
1425
|
|
|
1388
1426
|
it("#4160 shadow files accepted deferrals with track(), the same as enforce, and the PR links them", async () => {
|
|
1389
|
-
const k = { ...CONTRACT, preserve: [], requirements: [CONTRACT.requirements[0], { id: "R2", quote: "Existing callers of foo keep working.", disposition: "defer", why: "w" }] };
|
|
1427
|
+
const k = { ...CONTRACT, preserve: [], requirements: [CONTRACT.requirements[0], { id: "R2", quote: "Existing callers of foo keep working.", disposition: "defer", why: "w", bucket: "new-issue" }] };
|
|
1390
1428
|
const w = world({ contract: k, claims: [{ id: "R1", status: "implemented" }], reviews: [conf({}, k)] });
|
|
1391
1429
|
assert.equal((await runShadow(w)).terminalReason, "merged");
|
|
1392
1430
|
assert.equal(w.followups.length, 1); assert.match(w.followups[0].items[0].key, /^R2: /);
|
|
@@ -1420,7 +1458,7 @@ describe("conformance shadow mode (the default)", () => {
|
|
|
1420
1458
|
});
|
|
1421
1459
|
|
|
1422
1460
|
it("#4398 an investigation whose question is deferred closes partial (not planned), carried forward in the tracking issue", async () => {
|
|
1423
|
-
const k = { ...CONTRACT, preserve: [], requirements: [CONTRACT.requirements[0], { id: "R2", quote: "Existing callers of foo keep working.", disposition: "defer", why: "w" }] };
|
|
1461
|
+
const k = { ...CONTRACT, preserve: [], requirements: [CONTRACT.requirements[0], { id: "R2", quote: "Existing callers of foo keep working.", disposition: "defer", why: "w", bucket: "new-issue" }] };
|
|
1424
1462
|
const w = world({ title: "investigate(stealth): which signal is rejected", contract: k, claims: [{ id: "R1", status: "implemented" }], reviews: [conf({}, k)] });
|
|
1425
1463
|
const r = await runShadow(w);
|
|
1426
1464
|
assert.equal(r.terminalReason, "merged"); assert.equal(r.close, "partial");
|
|
@@ -1444,7 +1482,7 @@ describe("conformance shadow mode (the default)", () => {
|
|
|
1444
1482
|
const ra = await runShadow(a);
|
|
1445
1483
|
assert.equal(ra.terminalReason, "merged"); assert.equal(ra.close, undefined);
|
|
1446
1484
|
assert.deepEqual(a.closed.map((x) => x[x.indexOf("--reason") + 1]), ["completed"]);
|
|
1447
|
-
const k = { ...CONTRACT, preserve: [], requirements: [CONTRACT.requirements[0], { id: "R2", quote: "Existing callers of foo keep working.", disposition: "defer", why: "w" }] };
|
|
1485
|
+
const k = { ...CONTRACT, preserve: [], requirements: [CONTRACT.requirements[0], { id: "R2", quote: "Existing callers of foo keep working.", disposition: "defer", why: "w", bucket: "new-issue" }] };
|
|
1448
1486
|
const b = world({ contract: k, claims: [{ id: "R1", status: "implemented" }], reviews: [conf({}, k)] });
|
|
1449
1487
|
await runShadow(b);
|
|
1450
1488
|
assert.deepEqual(b.closed.map((x) => x[x.indexOf("--reason") + 1]), ["completed"]);
|
|
@@ -1621,9 +1659,9 @@ describe("hotspot prevention (forge#4195): plan facts, build rule, unexempted gr
|
|
|
1621
1659
|
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.");
|
|
1622
1660
|
assert.equal(hotspotBuildRule(null), ""); assert.equal(hotspotBuildRule(facts([], { enabled: false })), "");
|
|
1623
1661
|
const plan = builderPlanPrompt(7) + hotspotPlanNote(facts([HOT, { ...HOT, path: "a/b.py" }, { ...HOT, path: "c/d.py" }]));
|
|
1624
|
-
// 1850: #4404 (verify out_of_band) and #4396 (split size rule) grew the plan prompt to ~1803 tokens on CI paths; 1900: #4589 (check timeout is not a block, one block-rule line).
|
|
1625
|
-
assert.ok(approxTokens(plan) <=
|
|
1626
|
-
assert.ok(approxTokens(builderImplementPrompt() + hotspotBuildRule(facts([]))) <=
|
|
1662
|
+
// 1850: #4404 (verify out_of_band) and #4396 (split size rule) grew the plan prompt to ~1803 tokens on CI paths; 1900: #4589 (check timeout is not a block, one block-rule line). 2050: #4724 (deferred-row bucket rule).
|
|
1663
|
+
assert.ok(approxTokens(plan) <= 2050, `plan prompt with hotspots is ${approxTokens(plan)} tokens`);
|
|
1664
|
+
assert.ok(approxTokens(builderImplementPrompt() + hotspotBuildRule(facts([]))) <= 1650); // 1600: #4591 (never hand-name migration files) and #4596 (i18n translate rule), one line each; 1650: #4726 (e2e rule)
|
|
1627
1665
|
});
|
|
1628
1666
|
|
|
1629
1667
|
it("#4519 generated_paths: both prompts name the globs (even with the gate off), and an amendment adding one blocks", async () => {
|
|
@@ -1798,6 +1836,24 @@ describe("structure delta and decision provenance at merge (forge#4165)", () =>
|
|
|
1798
1836
|
return w;
|
|
1799
1837
|
};
|
|
1800
1838
|
|
|
1839
|
+
it("#4694 a guarded deliverable freezes H preserve rows listed under Decision sources; an unguarded lane gets none", async () => {
|
|
1840
|
+
const home = tmpDir("hist-home-"), prev = process.env.HOME;
|
|
1841
|
+
process.env.HOME = home;
|
|
1842
|
+
try {
|
|
1843
|
+
const plain = await run(mworld(), { dir: tmpDir("hist-a-") });
|
|
1844
|
+
assert.equal(plain.terminalReason, "merged");
|
|
1845
|
+
const guard = { v: 1, issue: 3, pr: 30, merge_sha: "s", tests: [{ path: "test/foo.test.js", ids: [] }], hunks: [{ file: "src/foo.js", start: 1, end: 3, symbol: null }], why: "keep foo stable" };
|
|
1846
|
+
writeIndex("o/r", [guard]);
|
|
1847
|
+
const withH = { ...contract, preserve: [...(contract.preserve ?? []), { id: "H1", what: "x" }] };
|
|
1848
|
+
const w = mworld({ reviews: [conf({}, withH, ["src/foo.js", "test/foo.test.js", "packages/db/package.json"])] });
|
|
1849
|
+
const r = await run(w, { dir: tmpDir("hist-b-") });
|
|
1850
|
+
assert.equal(r.terminalReason, "merged");
|
|
1851
|
+
const frozen = w.comments.filter((c) => c.includes("FORGE:CONTRACT")).map((c) => parseContractComment(c)).find(Boolean).contract;
|
|
1852
|
+
assert.deepEqual(frozen.preserve.filter((p) => p.source === "guard"), [{ id: "H1", what: "keep foo stable (#3, PR #30)", source: "guard" }]);
|
|
1853
|
+
assert.match(w.comments.find((c) => c.includes("FORGE:TRAJECTORY")), /\| Decision sources \| K1 doc, K2 precedent, H1 guard \|/);
|
|
1854
|
+
} finally { if (prev === undefined) delete process.env.HOME; else process.env.HOME = prev; }
|
|
1855
|
+
});
|
|
1856
|
+
|
|
1801
1857
|
it("a merged run writes one STRUCTURE_DELTA with edges, growth, provenance and the PR; the trajectory shows it; no extra gh call", async () => {
|
|
1802
1858
|
const dir = tmpDir("struct-merge-");
|
|
1803
1859
|
const w = mworld();
|
|
@@ -2040,8 +2096,8 @@ describe("comment templates", () => {
|
|
|
2040
2096
|
|
|
2041
2097
|
it("plan and implement prompts carry the stage templates and the contract schema within budget", () => {
|
|
2042
2098
|
const plan = builderPlanPrompt(7), impl = builderImplementPrompt();
|
|
2043
|
-
assert.ok(approxTokens(plan) <=
|
|
2044
|
-
assert.ok(approxTokens(impl) <=
|
|
2099
|
+
assert.ok(approxTokens(plan) <= 1950, `plan prompt is ${approxTokens(plan)} tokens`); // 1950: #4724 (deferred-row bucket rule)
|
|
2100
|
+
assert.ok(approxTokens(impl) <= 1650, `implement prompt is ${approxTokens(impl)} tokens`); // 1650: #4726 e2e rule; 1550: #4596 i18n translate rule
|
|
2045
2101
|
for (const h of ["### What Was Claimed", "### What We Found", "### Root Cause", "### Affected Files", "### Sibling Sweep", "### History", "### Recommendation", "### Related Issues",
|
|
2046
2102
|
"investigation.md", "contract.json", "PLANNING ONLY", '"requirements"', '"deliverables"', '"preserve"', '"validate"', "verbatim", "defer", "post-deploy"]) assert.ok(plan.includes(h), h);
|
|
2047
2103
|
for (const h of ["### Approach", "### Changes", "### Acceptance Criteria Status", "### Verification", "### Notes", "implementation.md", "result.json",
|
|
@@ -2053,12 +2109,23 @@ describe("merge_wait policy", () => {
|
|
|
2053
2109
|
const states = (o) => Object.entries(o).map(([name, state]) => ({ name, state }));
|
|
2054
2110
|
it("required with no protection (empty) merges immediately without polling", async () => {
|
|
2055
2111
|
const w = world(); let polled = 0;
|
|
2056
|
-
w.io.requiredChecks = async () => []; w.io.checkStates = async () =>
|
|
2112
|
+
w.io.requiredChecks = async () => []; w.io.checkStates = async () => []; // #4651: read once by the DCO gate, never polled
|
|
2057
2113
|
w.io.waitCiGreen = async () => { polled++; return { rc: 2, text: "" }; };
|
|
2058
2114
|
const r = await run(w);
|
|
2059
2115
|
assert.equal(r.terminalReason, "merged"); assert.equal(polled, 0);
|
|
2060
2116
|
assert.match(traj(w)[0], /\| Merge \| required \(no required checks: merged right after review\) \|/);
|
|
2061
2117
|
});
|
|
2118
|
+
it("a failed DCO check blocks the merge with ci.dco even when no check is required (#4651)", async () => {
|
|
2119
|
+
for (const mergeWait of ["required", "none"]) {
|
|
2120
|
+
const w = world(); w.io.requiredChecks = async () => []; w.io.checkStates = async () => [{ name: "DCO Sign-Off Verification", state: "failure", head: w.head }, { name: "build", state: "success" }];
|
|
2121
|
+
const r = await run(w, { mergeWait });
|
|
2122
|
+
assert.equal(r.terminalReason, "needs-human", mergeWait); assert.equal(r.code, "ci.dco", mergeWait); assert.equal(w.merges.length, 0, mergeWait);
|
|
2123
|
+
}
|
|
2124
|
+
});
|
|
2125
|
+
it("a passing or stale-head DCO check does not block the merge (#4651)", async () => {
|
|
2126
|
+
const w = world(); w.io.requiredChecks = async () => []; w.io.checkStates = async () => [{ name: "DCO Sign-Off Verification", state: "failure", head: "ffff0000" }, { name: "dco", state: "success", head: w.head }];
|
|
2127
|
+
assert.equal((await run(w)).terminalReason, "merged");
|
|
2128
|
+
});
|
|
2062
2129
|
it("required with a pending required check waits (backoff) then merges", async () => {
|
|
2063
2130
|
const w = world(); const seqs = [states({ build: "pending" }), states({ build: "pending" }), states({ build: "success", other: "failure" })]; let sleeps = 0;
|
|
2064
2131
|
w.io.requiredChecks = async () => ["build"]; w.io.checkStates = async () => seqs.shift();
|
|
@@ -2074,6 +2141,98 @@ describe("merge_wait policy", () => {
|
|
|
2074
2141
|
const w2 = world(); w2.io.requiredChecks = async () => ["build"]; w2.io.checkStates = async () => states({ build: "pending" });
|
|
2075
2142
|
assert.equal((await run(w2, { ciMaxMinutes: 5 })).terminalReason, "ci-pending");
|
|
2076
2143
|
});
|
|
2144
|
+
describe("#4624 merge-time rerun / wait / flaky branch of mergeFixRound", () => {
|
|
2145
|
+
const RUN = (n, attempt) => `https://github.com/o/r/actions/runs/${n}/job/${attempt}`;
|
|
2146
|
+
const NO_FIX = { ...parseVerify({}), maxRounds: 0 };
|
|
2147
|
+
const WAIT = 20000;
|
|
2148
|
+
const waits = (w) => { const n = { n: 0 }; const s = w.io.sleep; w.io.sleep = async (ms) => { if (ms === WAIT) n.n++; return s(ms); }; return n; };
|
|
2149
|
+
const fixPrompts = (w) => { const p = []; const inner = w.io.runner; w.io.runner = async (c) => { if (/CI failed on the pushed branch/.test(c.prompt)) p.push(c.prompt); return inner(c); }; return p; };
|
|
2150
|
+
const events = (dir, name) => readLog(dir, 7).filter((e) => e.event === name);
|
|
2151
|
+
/** `build` (run 77) is red; `urlAfter` is its URL once the rerun was triggered, `stateAfter` its state then. */
|
|
2152
|
+
const redBuild = (stateAfter, urlAfter) => {
|
|
2153
|
+
const w = world(); w.reran = [];
|
|
2154
|
+
w.io.requiredChecks = async () => ["build"];
|
|
2155
|
+
w.io.rerunFailedJobs = async (id) => { w.reran.push(id); };
|
|
2156
|
+
w.io.checkStates = async () => (w.reran.length ? [{ name: "build", state: stateAfter, url: urlAfter }] : [{ name: "build", state: "failure", url: RUN(77, 1) }]);
|
|
2157
|
+
w.io.failedLogs = async () => "boom";
|
|
2158
|
+
return w;
|
|
2159
|
+
};
|
|
2160
|
+
|
|
2161
|
+
it("a failed required check is rerun once, waited on while its URL is unchanged, and merges when the new attempt is green without a fix round", async () => {
|
|
2162
|
+
const w = redBuild("success", RUN(77, 2)); const p = fixPrompts(w), dir = tmpDir("core-");
|
|
2163
|
+
// the rerun was triggered, but the check still reports the old attempt for two more polls
|
|
2164
|
+
const states = w.io.checkStates; let stale = 2;
|
|
2165
|
+
w.io.checkStates = async () => (w.reran.length && stale-- > 0 ? [{ name: "build", state: "failure", url: RUN(77, 1) }] : states());
|
|
2166
|
+
const n = waits(w);
|
|
2167
|
+
const r = await run(w, { dir });
|
|
2168
|
+
assert.equal(r.terminalReason, "merged"); assert.deepEqual(w.reran, ["77"]); assert.equal(p.length, 0);
|
|
2169
|
+
assert.equal(n.n, 3, "one wait after the rerun, then two while the URL is unchanged");
|
|
2170
|
+
assert.deepEqual(events(dir, "CI_FLAKE_RERUN").map((e) => [e.trigger, e.result, e.runs]), [["merge", "rerun-started", ["77"]]]);
|
|
2171
|
+
assert.equal(events(dir, "CI_FIX_ROUND").length, 0);
|
|
2172
|
+
});
|
|
2173
|
+
|
|
2174
|
+
it("a rerun whose check URL never changes is waited on 20 times, then falls through to the fix round; the head is rerun only once", async () => {
|
|
2175
|
+
const w = redBuild("failure", RUN(77, 1)); const n = waits(w), dir = tmpDir("core-");
|
|
2176
|
+
const r = await run(w, { dir, verify: NO_FIX });
|
|
2177
|
+
assert.equal(r.terminalReason, "needs-human"); assert.equal(r.code, OUTCOME_CODES.CI_RED); assert.equal(w.merges.length, 0);
|
|
2178
|
+
assert.equal(n.n, 21, "the wait after the rerun plus 20 bounded waits");
|
|
2179
|
+
assert.deepEqual(w.reran, ["77"]);
|
|
2180
|
+
assert.equal(events(dir, "CI_FLAKE_RERUN").length, 1);
|
|
2181
|
+
});
|
|
2182
|
+
|
|
2183
|
+
it("#4708 merge-time CI fix round reviews a head with every validate recorded (the fix session dropped its rows)", async () => {
|
|
2184
|
+
const w = world({ resumeValidation: [] }); w.reran = [];
|
|
2185
|
+
w.io.requiredChecks = async () => ["build"];
|
|
2186
|
+
w.io.rerunFailedJobs = async (id) => { w.reran.push(id); };
|
|
2187
|
+
w.io.checkStates = async () => [{ name: "build", state: w.fixed ? "success" : "failure", url: RUN(77, w.reran.length + 1) }];
|
|
2188
|
+
w.io.failedLogs = async () => { w.fixed = true; return "boom"; };
|
|
2189
|
+
const dir = tmpDir("core-");
|
|
2190
|
+
const r = await run(w, { dir, verify: { ...parseVerify({}), maxRounds: 1 } });
|
|
2191
|
+
assert.equal(r.terminalReason, "merged", r.reason);
|
|
2192
|
+
assert.deepEqual(events(dir, "CI_FIX_ROUND").map((e) => e.trigger), ["merge"]);
|
|
2193
|
+
const written = JSON.parse(w.files[`${WT}/.forge-lean/result.json`]);
|
|
2194
|
+
assert.deepEqual(written.validation.map((x) => [x.id, x.carried === true]), [["V1", true]], "the earlier result is carried forward, marked");
|
|
2195
|
+
assert.ok(traj(w)[0].includes("carried forward across the CI fix round"));
|
|
2196
|
+
});
|
|
2197
|
+
|
|
2198
|
+
it("a rerun that reports a new attempt still red goes to the fix round and reaches the builder", async () => {
|
|
2199
|
+
const w = redBuild("failure", RUN(77, 2)); const p = fixPrompts(w), n = waits(w), dir = tmpDir("core-");
|
|
2200
|
+
await run(w, { dir, verify: { ...parseVerify({}), maxRounds: 1 } });
|
|
2201
|
+
assert.ok(n.n >= 1); assert.equal(p.length, 1); assert.equal(w.reran[0], "77");
|
|
2202
|
+
assert.deepEqual(events(dir, "CI_FIX_ROUND").map((e) => [e.trigger, e.jobs]), [["merge", ["build"]]]);
|
|
2203
|
+
});
|
|
2204
|
+
|
|
2205
|
+
it("a rerun that cannot start is logged rerun-failed and the lane does not wait", async () => {
|
|
2206
|
+
const w = redBuild("failure", RUN(77, 1)); const n = waits(w), dir = tmpDir("core-");
|
|
2207
|
+
w.io.rerunFailedJobs = async () => { throw new Error("HTTP 403"); };
|
|
2208
|
+
const r = await run(w, { dir, verify: NO_FIX });
|
|
2209
|
+
assert.equal(r.code, OUTCOME_CODES.CI_RED); assert.equal(n.n, 0);
|
|
2210
|
+
assert.deepEqual(events(dir, "CI_FLAKE_RERUN").map((e) => e.result), ["rerun-failed"]);
|
|
2211
|
+
});
|
|
2212
|
+
|
|
2213
|
+
it("verify.flaky: a required check still red after the rerun is reported and ends needs-human; nothing merged, builder not resumed", async () => {
|
|
2214
|
+
const w = redBuild("failure", RUN(77, 2)); const p = fixPrompts(w), dir = tmpDir("core-");
|
|
2215
|
+
const r = await run(w, { dir, verify: { ...parseVerify({}), flaky: ["build"] } });
|
|
2216
|
+
assert.equal(r.terminalReason, "needs-human"); assert.equal(r.code, OUTCOME_CODES.CI_RED);
|
|
2217
|
+
assert.equal(w.merges.length, 0); assert.equal(p.length, 0); assert.deepEqual(w.reran, ["77"]);
|
|
2218
|
+
assert.deepEqual(events(dir, "CI_FLAKY_REPORTED").map((e) => [e.trigger, e.jobs]), [["merge", ["build"]]]);
|
|
2219
|
+
assert.ok(JSON.stringify(w.comments).includes("still failed on PR #"), "needs-human message names the rerun");
|
|
2220
|
+
});
|
|
2221
|
+
|
|
2222
|
+
it("verify.flaky with a mix: only the non-flaky required check reaches the builder", async () => {
|
|
2223
|
+
const w = world(); w.reran = [];
|
|
2224
|
+
w.io.requiredChecks = async () => ["build", "lint"];
|
|
2225
|
+
w.io.rerunFailedJobs = async (id) => { w.reran.push(id); };
|
|
2226
|
+
w.io.checkStates = async () => [{ name: "build", state: "failure", url: RUN(77, w.reran.length + 1) }, { name: "lint", state: "failure", url: RUN(78, w.reran.length + 1) }];
|
|
2227
|
+
w.io.failedLogs = async () => "boom";
|
|
2228
|
+
const dir = tmpDir("core-");
|
|
2229
|
+
await run(w, { dir, verify: { ...parseVerify({}), flaky: ["build"], maxRounds: 1 } });
|
|
2230
|
+
assert.deepEqual(w.reran.slice(0, 2).sort(), ["77", "78"], "both red runs rerun on the first head");
|
|
2231
|
+
assert.deepEqual(events(dir, "CI_FLAKY_REPORTED")[0].jobs, ["build"]);
|
|
2232
|
+
assert.deepEqual(events(dir, "CI_FIX_ROUND").map((e) => e.jobs), [["lint"]]);
|
|
2233
|
+
});
|
|
2234
|
+
});
|
|
2235
|
+
|
|
2077
2236
|
it("all waits for every check (waitCiGreen); none merges immediately", async () => {
|
|
2078
2237
|
const w = world({ waits: [{ rc: 2, text: "" }] }); let req = 0; w.io.requiredChecks = async () => { req++; return []; };
|
|
2079
2238
|
assert.equal((await run(w, { mergeWait: "all" })).terminalReason, "merged");
|
|
@@ -2294,6 +2453,29 @@ describe("orchestrate", () => {
|
|
|
2294
2453
|
}
|
|
2295
2454
|
});
|
|
2296
2455
|
|
|
2456
|
+
it("#4687 a dependency skip waits (logged with its trigger) until the blocker merges; the re-admission is logged too", async () => {
|
|
2457
|
+
const rowsSeen = [];
|
|
2458
|
+
let merged = false;
|
|
2459
|
+
const rows = await orchestrate({ issues: [9, 1], parallel: 1, readBody: async (n) => (n === 1 ? "**Depends on**: #9" : ""),
|
|
2460
|
+
onUpdate: (r) => rowsSeen.push(r),
|
|
2461
|
+
dispatchCheck: async (n) => (n === 9 && merged ? { outcome: "merged", pr: 90, detail: "PR #90 already merged" } : null),
|
|
2462
|
+
runOne: async (n) => { if (n === 9) { merged = true; return { terminalReason: "needs-human", detail: "ci.red" }; } return { terminalReason: "merged", pr: 101 }; } });
|
|
2463
|
+
const skipRow = rowsSeen.find((r) => r.issue === 1 && r.outcome === "skipped");
|
|
2464
|
+
assert.equal(skipRow.waiting, true);
|
|
2465
|
+
assert.equal(skipRow.trigger, 9);
|
|
2466
|
+
assert.ok(rowsSeen.some((r) => r.issue === 1 && r.outcome === "readmitted" && r.trigger === 9));
|
|
2467
|
+
assert.deepEqual(rows.map((r) => [r.issue, r.outcome, r.waiting]), [[9, "merged", undefined], [1, "merged", undefined]]);
|
|
2468
|
+
});
|
|
2469
|
+
|
|
2470
|
+
it("#4687 a blocker that closed as not planned leaves its dependents skipped, no longer waiting", async () => {
|
|
2471
|
+
let checks = 0, ran = false;
|
|
2472
|
+
const rows = await orchestrate({ issues: [9, 1], parallel: 1, readBody: async (n) => (n === 1 ? "**Depends on**: #9" : ""),
|
|
2473
|
+
dispatchCheck: async (n) => { if (n === 9 && ran) { checks++; return { outcome: "invalid", detail: "issue closed (not planned)", final: true }; } return null; },
|
|
2474
|
+
runOne: async () => { ran = true; return { terminalReason: "needs-human", detail: "ci.red" }; } });
|
|
2475
|
+
assert.deepEqual(rows.map((r) => [r.issue, r.outcome, r.waiting]), [[9, "needs-human", undefined], [1, "skipped", undefined]]);
|
|
2476
|
+
assert.equal(checks, 1, "a final blocker is not re-checked");
|
|
2477
|
+
});
|
|
2478
|
+
|
|
2297
2479
|
it("#4124 a repeated dependency-open stops after the requeue cap", async () => {
|
|
2298
2480
|
const h = harness({}, { 1: "dependency-open" });
|
|
2299
2481
|
const clock = { t: 0 }, starts = [];
|
|
@@ -3137,22 +3319,53 @@ describe("batch robustness (#4112-#4115)", () => {
|
|
|
3137
3319
|
}
|
|
3138
3320
|
});
|
|
3139
3321
|
|
|
3140
|
-
it("#4115b lane limits wrap the spawn in systemd-run; unset or invalid limits
|
|
3141
|
-
const w = wrapLaneLimits("claude", ["--print", "x"], { memory_max: "3G", cpu_quota: "150%" });
|
|
3142
|
-
assert.deepEqual(w, { bin: "systemd-run", args: ["--user", "--scope", "-q", "-p", "MemoryMax=3G", "-p", "CPUQuota=150%", "--", "claude", "--print", "x"] });
|
|
3143
|
-
assert.deepEqual(wrapLaneLimits("claude", ["a"], null),
|
|
3144
|
-
assert.deepEqual(wrapLaneLimits("claude", ["a"], { memory_max: "lots; rm -rf /", cpu_quota: "x" }),
|
|
3322
|
+
it("#4115b lane limits wrap the spawn in systemd-run; unset or invalid limits get only the default TasksMax (#4690)", () => {
|
|
3323
|
+
const w = wrapLaneLimits("claude", ["--print", "x"], { memory_max: "3G", cpu_quota: "150%" }, "u1");
|
|
3324
|
+
assert.deepEqual(w, { bin: "systemd-run", unit: "u1", args: ["--user", "--scope", "-q", "--unit=u1", "-p", "TasksMax=1024", "-p", "MemoryMax=3G", "-p", "CPUQuota=150%", "--", "claude", "--print", "x"] });
|
|
3325
|
+
assert.deepEqual(wrapLaneLimits("claude", ["a"], null, "u2").args.slice(0, 6), ["--user", "--scope", "-q", "--unit=u2", "-p", "TasksMax=1024"]);
|
|
3326
|
+
assert.deepEqual(wrapLaneLimits("claude", ["a"], { memory_max: "lots; rm -rf /", cpu_quota: "x" }, "u3").args, ["--user", "--scope", "-q", "--unit=u3", "-p", "TasksMax=1024", "--", "claude", "a"]);
|
|
3145
3327
|
assert.equal(wrapLaneLimits("claude", ["a"], { memory_max: "1G" }).args.includes("CPUQuota=150%"), false);
|
|
3146
3328
|
});
|
|
3147
3329
|
|
|
3148
|
-
it("#
|
|
3330
|
+
it("#4690 tasks_max is configurable; 0 or off disables it and, with nothing else set, leaves the spawn unchanged", () => {
|
|
3331
|
+
assert.ok(wrapLaneLimits("c", [], { tasks_max: "64" }, "u").args.includes("TasksMax=64"));
|
|
3332
|
+
for (const off of ["0", "off", 0]) assert.deepEqual(wrapLaneLimits("c", ["a"], { tasks_max: off }), { bin: "c", args: ["a"] });
|
|
3333
|
+
assert.equal(wrapLaneLimits("c", [], { tasks_max: "off", memory_max: "1G" }, "u").args.some((x) => x.startsWith("TasksMax")), false);
|
|
3334
|
+
});
|
|
3335
|
+
|
|
3336
|
+
it("#4115b the runner wraps by default when systemd-run probes available, never when it does not (clean fallback, #4690)", async () => {
|
|
3149
3337
|
const seen = [];
|
|
3150
3338
|
const runCli = async (o) => { seen.push(o.wrap ? o.wrap("claude", ["a"]).bin : null); return { text: "", usage: null, costUsd: 0, durationMs: 0, turns: 0, sessionId: "s" }; };
|
|
3151
3339
|
const base = { role: "builder", issue: 1, cwd: process.cwd(), prompt: "p", system: "" };
|
|
3152
3340
|
await makeLeanRunner({ runCli, probe: () => true })({ ...base, laneLimits: { memory_max: "3G" } });
|
|
3153
3341
|
await makeLeanRunner({ runCli, probe: () => false })({ ...base, laneLimits: { memory_max: "3G" } });
|
|
3154
3342
|
await makeLeanRunner({ runCli, probe: () => true })({ ...base });
|
|
3155
|
-
assert.deepEqual(seen, ["systemd-run", null,
|
|
3343
|
+
assert.deepEqual(seen, ["systemd-run", null, "systemd-run"]);
|
|
3344
|
+
assert.match(laneCapNotice(() => false), /lanes are uncapped/);
|
|
3345
|
+
assert.equal(laneCapNotice(() => true), null);
|
|
3346
|
+
});
|
|
3347
|
+
|
|
3348
|
+
it("#4690 a scope whose events show a limit stops the lane with CLI_LANE_LIMIT naming it; a scope-less spawn never reads events", async () => {
|
|
3349
|
+
const { writeFileSync, chmodSync } = await import("node:fs");
|
|
3350
|
+
const { runCliBackendAsync } = await import("../runner.mjs");
|
|
3351
|
+
const d = tmpDir("core-limit-");
|
|
3352
|
+
writeFileSync(`${d}/fake-claude`, "#!/bin/sh\nsleep 30\n"); chmodSync(`${d}/fake-claude`, 0o755);
|
|
3353
|
+
const go = (wrap, ev) => runCliBackendAsync({ spec: { name: "t" }, userMessage: "go", args: ["1"], cwd: d, bin: `${d}/fake-claude`, logger: { log() {} }, idleMs: 200, maxMs: 20000, wrap, limitProbe: () => ev });
|
|
3354
|
+
const fake = (b, a) => ({ bin: b, args: a, unit: "forgedock-lane-x" });
|
|
3355
|
+
for (const [ev, limit] of [[{ tasks: 3, memory: 0 }, "tasks"], [{ tasks: 0, memory: 1 }, "memory"]]) {
|
|
3356
|
+
await assert.rejects(go(fake, ev), (e) => e.code === "CLI_LANE_LIMIT" && e.limit === limit);
|
|
3357
|
+
}
|
|
3358
|
+
await assert.rejects(go((b, a) => ({ bin: b, args: a }), { tasks: 9, memory: 9 }), (e) => e.code === "CLI_IDLE");
|
|
3359
|
+
});
|
|
3360
|
+
|
|
3361
|
+
it("#4690 a CLI_LANE_LIMIT ends needs-human env.lane_limit naming the limit: no retry, never engine-error or host kill", async () => {
|
|
3362
|
+
const lim = () => Object.assign(new Error("claude CLI lane hit its scope tasks limit"), { code: "CLI_LANE_LIMIT", limit: "tasks", unit: "u" });
|
|
3363
|
+
assert.equal(isExternalKill(lim()), false);
|
|
3364
|
+
const w = world({ dirty: true }); let builders = 0;
|
|
3365
|
+
w.io.runner = async (c) => { if (c.role === "builder") { builders++; throw lim(); } return { text: "" }; };
|
|
3366
|
+
const r = await run(w);
|
|
3367
|
+
assert.equal(r.terminalReason, "needs-human"); assert.equal(r.code, "env.lane_limit");
|
|
3368
|
+
assert.match(r.detail, /TasksMax/); assert.equal(builders, 1);
|
|
3156
3369
|
});
|
|
3157
3370
|
|
|
3158
3371
|
it("#4115b integration: a lane under systemd-run --scope really runs in a capped scope (skipped without systemd-run)", { skip: !systemdRunAvailable() }, async () => {
|
|
@@ -3164,6 +3377,37 @@ describe("batch robustness (#4112-#4115)", () => {
|
|
|
3164
3377
|
assert.match(r.text, /\.scope/);
|
|
3165
3378
|
});
|
|
3166
3379
|
|
|
3380
|
+
it("#4690 a fixture lane really forking past its task cap is stopped with CLI_LANE_LIMIT (no systemd needed: the probe counts real descendants)", async () => {
|
|
3381
|
+
const { writeFileSync, chmodSync } = await import("node:fs");
|
|
3382
|
+
const { runCliBackendAsync, listDescendants } = await import("../runner.mjs");
|
|
3383
|
+
const d = tmpDir("core-forkfix-");
|
|
3384
|
+
writeFileSync(`${d}/fake-claude`, "#!/bin/sh\ni=0\nwhile [ $i -lt 40 ]; do sleep 20 & i=$((i+1)); done\nsleep 20\n"); chmodSync(`${d}/fake-claude`, 0o755);
|
|
3385
|
+
const cap = 10;
|
|
3386
|
+
const limitProbe = (pid) => ({ tasks: listDescendants(pid).length > cap ? 1 : 0, memory: 0 });
|
|
3387
|
+
await assert.rejects(runCliBackendAsync({ spec: { name: "t" }, userMessage: "go", args: ["1"], cwd: d, bin: `${d}/fake-claude`, logger: { log() {} }, maxMs: 15000, limitProbe,
|
|
3388
|
+
wrap: (b, a) => ({ bin: b, args: a, unit: "forgedock-lane-fixture" }) }), (e) => e.code === "CLI_LANE_LIMIT" && e.limit === "tasks");
|
|
3389
|
+
});
|
|
3390
|
+
|
|
3391
|
+
// The scope's cgroup counters must be readable (cgroup v2, delegated user scope); plain systemd-run availability is not enough on CI.
|
|
3392
|
+
const scopeEventsReadable = () => {
|
|
3393
|
+
if (!systemdRunAvailable()) return false;
|
|
3394
|
+
try {
|
|
3395
|
+
const w = wrapLaneLimits("sh", ["-c", "cat /proc/self/cgroup"], { tasks_max: 50 });
|
|
3396
|
+
const out = execFileSync(w.bin, w.args, { encoding: "utf8", timeout: 8000 });
|
|
3397
|
+
const path = (out.split("\n").find((l) => l.startsWith("0::")) ?? "").slice(3).trim();
|
|
3398
|
+
return path.endsWith(".scope") && existsSync(`/sys/fs/cgroup${path}/pids.events`);
|
|
3399
|
+
} catch { return false; }
|
|
3400
|
+
};
|
|
3401
|
+
it("#4690 integration: a step forking past TasksMax in a real scope ends CLI_LANE_LIMIT (skipped without systemd-run)", { skip: !scopeEventsReadable() }, async () => {
|
|
3402
|
+
const { writeFileSync, chmodSync } = await import("node:fs");
|
|
3403
|
+
const { runCliBackendAsync } = await import("../runner.mjs");
|
|
3404
|
+
const d = tmpDir("core-fork-");
|
|
3405
|
+
// node (not sh): a refused fork must not end the fixture before the sampler sees the scope's pids.events
|
|
3406
|
+
writeFileSync(`${d}/fake-claude`, `#!${process.execPath}\nconst { spawn } = require("node:child_process");\nfor (let i = 0; i < 200; i++) { try { spawn("sleep", ["20"], { stdio: "ignore" }).on("error", () => {}); } catch {} }\nsetTimeout(() => {}, 20000);\n`); chmodSync(`${d}/fake-claude`, 0o755);
|
|
3407
|
+
await assert.rejects(runCliBackendAsync({ spec: { name: "t" }, userMessage: "go", args: ["1"], cwd: d, bin: `${d}/fake-claude`, logger: { log() {} }, maxMs: 15000, wrap: (b, a) => wrapLaneLimits(b, a, { tasks_max: 20 }) }),
|
|
3408
|
+
(e) => e.code === "CLI_LANE_LIMIT" && e.limit === "tasks");
|
|
3409
|
+
});
|
|
3410
|
+
|
|
3167
3411
|
const hostFor = (state) => ({ memAvailableMb: () => 99999, freeMb: async () => 99999, tmpPaths: () => ["/t"], loadAvg1: () => state.load(), cores: () => 2 });
|
|
3168
3412
|
const slowBuilder = (w, ms, hook) => { const orig = w.io.runner; w.io.runner = async (c) => { if (c.role !== "builder") return orig(c); hook?.(c); await new Promise((r) => setTimeout(r, ms)); return orig(c); }; };
|
|
3169
3413
|
|
|
@@ -3359,6 +3603,35 @@ describe("batch robustness (#4112-#4115)", () => {
|
|
|
3359
3603
|
assert.equal(n, 1);
|
|
3360
3604
|
});
|
|
3361
3605
|
|
|
3606
|
+
it("gh.auth (#4662) a 401 runs one token refresh and the retry succeeds, so the lane merges", async () => {
|
|
3607
|
+
const w = world();
|
|
3608
|
+
const gh = w.io.gh; let n = 0, refreshes = 0;
|
|
3609
|
+
w.io.gh = async (a, opts) => { if (n++ === 0) throw new Error("gh: Bad credentials (HTTP 401)"); return gh(a, opts); };
|
|
3610
|
+
w.io.refreshAuth = async () => { refreshes++; };
|
|
3611
|
+
assert.equal((await run(w)).terminalReason, "merged");
|
|
3612
|
+
assert.equal(refreshes, 1);
|
|
3613
|
+
});
|
|
3614
|
+
|
|
3615
|
+
it("gh.auth (#4662) a 401 that outlives the refresh ends deferred gh.auth, never engine-error, and refreshes once", async () => {
|
|
3616
|
+
const w = world();
|
|
3617
|
+
let refreshes = 0;
|
|
3618
|
+
w.io.gh = async () => { throw new Error("gh: Bad credentials (HTTP 401)"); };
|
|
3619
|
+
w.io.refreshAuth = async () => { refreshes++; };
|
|
3620
|
+
const r = await run(w);
|
|
3621
|
+
assert.equal(r.terminalReason, "deferred");
|
|
3622
|
+
assert.equal(r.code, OUTCOME_CODES.GH_AUTH);
|
|
3623
|
+
assert.ok(r.retryInSeconds > 0);
|
|
3624
|
+
assert.equal(refreshes, 1);
|
|
3625
|
+
});
|
|
3626
|
+
|
|
3627
|
+
it("gh.auth (#4662) refreshAuthOnce leaves non-auth errors alone and GH_AUTH_RE ignores 403 and 5xx", async () => {
|
|
3628
|
+
assert.ok(GH_AUTH_RE.test("HTTP 401: Bad credentials") && !GH_AUTH_RE.test("HTTP 403: Resource not accessible") && !GH_AUTH_RE.test("HTTP 502: Bad Gateway"));
|
|
3629
|
+
let refreshes = 0;
|
|
3630
|
+
const other = new Error("HTTP 404: Not Found");
|
|
3631
|
+
await assert.rejects(refreshAuthOnce(async () => { throw other; }, { refresh: async () => { refreshes++; } }), (e) => e === other);
|
|
3632
|
+
assert.equal(refreshes, 0);
|
|
3633
|
+
});
|
|
3634
|
+
|
|
3362
3635
|
it("#4507 makeGhCache: a per-root cache dir, reset swaps in a fresh dir, the no-root fallback is disposed", () => {
|
|
3363
3636
|
const root = tmpDir("ghcache-");
|
|
3364
3637
|
const c = makeGhCache({ root });
|
|
@@ -3785,6 +4058,12 @@ describe("shipped lane follows the open PR base (#4593)", () => {
|
|
|
3785
4058
|
assert.equal(r.terminalReason, "merged");
|
|
3786
4059
|
assert.ok(w.calls.some((c) => c[0] === "git" && c.includes("origin/milestone/new..refs/heads/feat/issue-7-lean")), "work is compared against the PR base");
|
|
3787
4060
|
});
|
|
4061
|
+
it("unused lane at the staging tip is not shipped (#4623)", async () => {
|
|
4062
|
+
// ancestor of staging but no merge commit took it: the hand-retargeted PR must stop, not be followed
|
|
4063
|
+
const w = world({ milestone: "Old", branchesOnOrigin: ["staging", "milestone/old", "milestone/new"], unusedAncestors: ["milestone/old"], prList: pr("milestone/new"), remote: "abc\trefs/heads/feat/issue-7-lean" });
|
|
4064
|
+
const r = await run(w, { lane: undefined });
|
|
4065
|
+
assert.deepEqual([r.terminalReason, r.code], ["needs-human", "lane.mismatch"]);
|
|
4066
|
+
});
|
|
3788
4067
|
it("stays needs-human when the lane is not shipped, the base is shipped, missing, or the default branch", async () => {
|
|
3789
4068
|
for (const [o, code] of [
|
|
3790
4069
|
[{ prList: pr("milestone/new") }, "lane.mismatch"],
|
|
@@ -3949,6 +4228,17 @@ describe("verify-by-CI (#4167): ci mode runs the heavy checks on CI and feeds fa
|
|
|
3949
4228
|
const ciValidation = [{ id: "V1", command: "node --test test/foo.test.js", ci: true }];
|
|
3950
4229
|
const events = (dir) => readLog(dir, 7).filter((e) => e.event === "CI_VERIFY");
|
|
3951
4230
|
|
|
4231
|
+
it("untrack commit before CI verify: a tool cache the builder committed does not move HEAD under the CI lookup (#4621)", async () => {
|
|
4232
|
+
const w = withCi(world({ validation: ciValidation })); w.added = ["src/foo.js", "apps/worker/.eslintcache"];
|
|
4233
|
+
const inner = w.io.git;
|
|
4234
|
+
w.io.git = async (a, o) => { const out = await inner(a, o); if (a[0] === "commit" && a.some((x) => /untrack lane tool state/.test(x))) w.head = `dddd${w.calls.length}`.padEnd(16, "0"); return out; };
|
|
4235
|
+
const dir = tmpDir("core-ci-");
|
|
4236
|
+
const r = await run(w, { verify: CI, dir });
|
|
4237
|
+
assert.equal(r.terminalReason, "merged");
|
|
4238
|
+
assert.deepEqual(events(dir).map((e) => e.result), ["pass"], "CI found the run for the pushed tip");
|
|
4239
|
+
assert.ok(!builders(w).some((b) => /CI (is )?unavailable|cannot run/i.test(String(b[4]))), "no ci_unavailable builder round");
|
|
4240
|
+
});
|
|
4241
|
+
|
|
3952
4242
|
it("green CI: the builder is told not to run test suites, the branch is pushed and verified before review, validate left to CI passes enforce", async () => {
|
|
3953
4243
|
const w = withCi(world({ validation: ciValidation }));
|
|
3954
4244
|
const dir = tmpDir("core-ci-");
|
|
@@ -4000,6 +4290,18 @@ describe("verify-by-CI (#4167): ci mode runs the heavy checks on CI and feeds fa
|
|
|
4000
4290
|
assert.match(traj(w)[0], /Validate V1 verified by CI at [0-9a-f]{0,12}/);
|
|
4001
4291
|
});
|
|
4002
4292
|
|
|
4293
|
+
it("#4708 remote placement: the merge-time CI fix round's head is CI-verified and reviewed with every validate recorded", async () => {
|
|
4294
|
+
const w = withCi(world({ resumeValidation: [] })); w.reran = [];
|
|
4295
|
+
w.io.requiredChecks = async () => ["build"];
|
|
4296
|
+
w.io.rerunFailedJobs = async (id) => { w.reran.push(id); };
|
|
4297
|
+
w.io.checkStates = async () => [{ name: "build", state: w.fixed ? "success" : "failure", url: `https://github.com/o/r/actions/runs/77/job/${w.reran.length + 1}` }];
|
|
4298
|
+
w.io.failedLogs = async () => { w.fixed = true; return "boom"; };
|
|
4299
|
+
const r = await run(w, { verify: CI });
|
|
4300
|
+
assert.equal(r.terminalReason, "merged", r.reason);
|
|
4301
|
+
assert.ok(w.ci.runs.some((x) => x.headSha === w.head), "the fix head is CI-verified before it is reviewed");
|
|
4302
|
+
assert.equal(JSON.parse(w.files[`${WT}/.forge-lean/result.json`]).validation[0].id, "V1");
|
|
4303
|
+
});
|
|
4304
|
+
|
|
4003
4305
|
it("docs-only lane: no push for CI, no dispatch; the decision is recorded", async () => {
|
|
4004
4306
|
const w = withCi(world({ changed: ["README.md", "docs/guide.md"], validation: ciValidation }));
|
|
4005
4307
|
const r = await run(w, { verify: CI, conformance: "shadow" });
|
|
@@ -5075,6 +5377,18 @@ describe("context pack modes (forge#4275)", () => {
|
|
|
5075
5377
|
assert.deepEqual([m.leaseWrites.at(-1).terminalCode, m.leaseWrites.at(-1).failureClasses], [OUTCOME_CODES.COMPLETED, []]);
|
|
5076
5378
|
});
|
|
5077
5379
|
|
|
5380
|
+
it("#4711 a merged lane run records mergedTo {lane, pr, sha} on the terminal FORGE:STATE", async () => {
|
|
5381
|
+
const w = world(); w.leaseWrites = []; w.io.projector = { readState: async () => null, writeState: async (_i, st) => { w.leaseWrites.push(st); } };
|
|
5382
|
+
const gh = w.io.gh;
|
|
5383
|
+
w.io.gh = async (a, o) => (a[0] === "pr" && a[1] === "view" && a.includes("mergeCommit") ? JSON.stringify({ mergeCommit: { oid: "feedface00112233" } }) : gh(a, o));
|
|
5384
|
+
const r = await run(w);
|
|
5385
|
+
assert.equal(r.terminalReason, "merged");
|
|
5386
|
+
const last = w.leaseWrites.at(-1);
|
|
5387
|
+
assert.ok(last.mergedTo?.lane && Number.isInteger(last.mergedTo.pr), JSON.stringify(last.mergedTo));
|
|
5388
|
+
assert.equal(last.mergedTo.sha, "feedface00112233");
|
|
5389
|
+
assert.ok(!w.leaseWrites.slice(0, -1).some((x) => x.mergedTo), "only the terminal write carries it");
|
|
5390
|
+
});
|
|
5391
|
+
|
|
5078
5392
|
it("#4444 ab: the lane runs its issue's arm, logs it as context_pack and tags context_ab", async () => {
|
|
5079
5393
|
const { contextArm } = await import("./context.mjs");
|
|
5080
5394
|
const arm = contextArm({ mode: "ab", repo: "o/r", issue: 7 }).mode;
|
|
@@ -5189,6 +5503,17 @@ describe("enumerating tests + sequence numbers at hand-off (#4322)", () => {
|
|
|
5189
5503
|
assert.equal(w.dupArgs.cwd, WT);
|
|
5190
5504
|
});
|
|
5191
5505
|
|
|
5506
|
+
it("#4596 English copies in a locale catalog resume the builder once to translate; a leftover is an anomaly", async () => {
|
|
5507
|
+
const w = hworld(); const c = [[{ path: "messages/ta.json", key: "calendarView.day", value: "Day view", source: "messages/en.json" }], []]; w.cpCalls = 0;
|
|
5508
|
+
w.io.englishCopies = async (x) => { w.cpCalls++; w.cpArgs = x; return c.length > 1 ? c.shift() : c[0]; };
|
|
5509
|
+
assert.equal((await run(w)).terminalReason, "merged");
|
|
5510
|
+
const b = builders(w);
|
|
5511
|
+
assert.equal(b.length, 3);
|
|
5512
|
+
assert.match(b[2][4], /messages\/ta\.json: calendarView\.day = "Day view" \(same as messages\/en\.json\)[\s\S]*Translate each/);
|
|
5513
|
+
assert.ok(w.seq.indexOf("pr") > w.seq.lastIndexOf("builder-resume"), "fixed before the PR");
|
|
5514
|
+
assert.equal(w.cpArgs.cwd, WT);
|
|
5515
|
+
});
|
|
5516
|
+
|
|
5192
5517
|
it("#4406 a duplicate that remains after the round is an anomaly; verify.duplicate_exports false skips the check", async () => {
|
|
5193
5518
|
let w = hworld(); w.io.duplicateExports = async () => DUP;
|
|
5194
5519
|
assert.equal((await run(w)).terminalReason, "merged");
|
|
@@ -5838,6 +6163,17 @@ describe("e2e merge gate (#4403)", () => {
|
|
|
5838
6163
|
assert.equal(r2.terminalReason, "needs-human"); assert.equal(w2.merges.length, 0);
|
|
5839
6164
|
});
|
|
5840
6165
|
|
|
6166
|
+
it("#4624 verify.flaky: a flaky-listed own e2e failure is reported as an anomaly, never reaches the builder, and the lane merges; a non-listed one still does", async () => {
|
|
6167
|
+
const w = withE2e(world({ changed: SPECS }), ["failure"]);
|
|
6168
|
+
const fixes = []; const inner = w.io.runner; w.io.runner = async (c) => { if (/CI failed on the pushed branch/.test(c.prompt)) fixes.push(c.prompt); return inner(c); };
|
|
6169
|
+
const r = await go(w, { verify: { ...cfg(), flaky: ["e2e"] } });
|
|
6170
|
+
assert.equal(r.terminalReason, "merged"); assert.equal(fixes.length, 0);
|
|
6171
|
+
assert.ok(traj(w).some((t) => /Known-flaky e2e failure\(s\) reported, not blocking \(verify\.flaky\): e2e/.test(t)));
|
|
6172
|
+
const w2 = withE2e(world({ changed: SPECS }), ["failure"]);
|
|
6173
|
+
const r2 = await go(w2, { verify: { ...cfg(), flaky: ["other"], maxRounds: 0 } });
|
|
6174
|
+
assert.equal(r2.code, OUTCOME_CODES.CI_RED); assert.equal(w2.merges.length, 0);
|
|
6175
|
+
});
|
|
6176
|
+
|
|
5841
6177
|
it("verify.e2e: never (or no label/workflow configured) keeps today's behaviour: no label, no e2e wait, no event", async () => {
|
|
5842
6178
|
for (const verify of [cfg({ e2e: "never" }), parseVerify({})]) {
|
|
5843
6179
|
const w = withE2e(world({ changed: SPECS }), ["failure"]);
|
|
@@ -6174,7 +6510,7 @@ describe("#4395 lane profile", () => {
|
|
|
6174
6510
|
assert.match(p, /Issue independent read-only calls \(reads, greps, git log\/blame, gh\) in the same turn or one shell call\./);
|
|
6175
6511
|
assert.match(p, /Read each file's ranges once .* and keep notes; don't re-read it\./);
|
|
6176
6512
|
}
|
|
6177
|
-
assert.ok(approxTokens(builderImplementPrompt()) <=
|
|
6513
|
+
assert.ok(approxTokens(builderImplementPrompt()) <= 1650 && approxTokens(builderPlanPrompt(7)) <= 1950);
|
|
6178
6514
|
});
|
|
6179
6515
|
|
|
6180
6516
|
it("runCliBackendAsync reports the first stream turn's context as baselineTokens; makeLeanRunner drops it on resume", async () => {
|