forgedock 1.29.1 → 1.31.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 +5 -2
- package/bin/engine/phases.mjs +20 -3
- 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 +453 -66
- package/bin/engine.mjs +50 -38
- package/bin/forgedock.mjs +35 -3
- package/bin/gc.mjs +50 -17
- 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 +356 -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 +278 -22
- package/bin/lean/conformance.test.mjs +251 -5
- package/bin/lean/context.mjs +133 -55
- package/bin/lean/context.test.mjs +175 -9
- 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 +55 -19
- package/bin/lean/enumerating.mjs +189 -0
- package/bin/lean/enumerating.test.mjs +97 -0
- 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 +799 -127
- package/bin/lean/lean.test.mjs +1749 -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/limits.test.mjs +36 -1
- package/bin/lean/orchestrate.mjs +341 -99
- 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 +180 -0
- package/bin/lean/precheck.test.mjs +205 -0
- package/bin/lean/prefilter.mjs +180 -9
- package/bin/lean/prefilter.test.mjs +175 -3
- package/bin/lean/preflight.mjs +238 -0
- package/bin/lean/preflight.test.mjs +108 -0
- package/bin/lean/probe.mjs +297 -0
- package/bin/lean/probe.test.mjs +210 -0
- package/bin/lean/prompts.mjs +134 -12
- package/bin/lean/recovery.test.mjs +75 -0
- 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.test.mjs +127 -1
- package/bin/lean/schedd.mjs +55 -9
- package/bin/lean/schedd.test.mjs +149 -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 +385 -0
- package/bin/lean/sequence.test.mjs +373 -0
- package/bin/lean/testmap.mjs +43 -6
- package/bin/lean/testmap.test.mjs +38 -1
- 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 +77 -10
- package/bin/lean/verify.test.mjs +74 -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 +319 -0
- package/bin/stats/efficiency.test.mjs +233 -0
- package/bin/stats/github-source.mjs +8 -3
- package/bin/stats/index.mjs +69 -6
- package/bin/stats/local-source.mjs +86 -3
- package/bin/stats/query.mjs +48 -0
- package/bin/stats/render.mjs +22 -0
- package/bin/stats/stats.test.mjs +224 -5
- package/commands/orchestrate.md +6 -2
- 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 +88 -6
- package/docs/OUTCOME-CODES.md +7 -4
- package/docs/RELEASE-NOTES.md +10 -0
- package/docs/STATS.md +53 -1
- package/docs/TELEMETRY.md +1 -1
- package/docs/spec/review-protocol.md +1 -1
- package/package.json +1 -1
- package/scripts/acceptance-red-green.sh +13 -4
- package/scripts/acceptance-red-green.test.sh +14 -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/test-map.sh +63 -17
- package/scripts/test-map.test.sh +55 -0
- package/scripts/trusted-comments.test.sh +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
3
|
import { describe, it } from "node:test";
|
|
4
4
|
import assert from "node:assert/strict";
|
|
5
|
-
import { validateContract, applyAmendments, precheck, hotspotGrowth, reproFailures, isBugIssue, parseReview, checkCompleteness, decide, closePlan, followupItems, inIssue, isTestPath, normalizeEvidencePath, mergeReviewRows, reviewSkeleton, missingIds, resolveEvidencePath, movingBaseDiff, scopeText, explainFailures } from "./conformance.mjs";
|
|
5
|
+
import { validateContract, applyAmendments, precheck, hotspotGrowth, reproFailures, isBugIssue, parseReview, checkCompleteness, decide, closePlan, isInvestigation, formatShadowSummary, followupItems, inIssue, isTestPath, normalizeEvidencePath, mergeReviewRows, reviewSkeleton, missingIds, resolveEvidencePath, movingBaseDiff, scopeText, evidencePathLine, evidencePathOnly, explainFailures, missingTool, repoTracks, tmpdirInputs, isScopeCheck, isPureScopeCheck, dropScopeValidates, tagOutOfBand, outOfBandRequirements, isOutOfBand, followupMap, VERIFY_MODES, OUT_OF_BAND_RE } from "./conformance.mjs";
|
|
6
6
|
|
|
7
7
|
const ISSUE = "## Requirements\n1. The scrape route must release the spend reservation on 429.\n2. The search route must release the spend reservation on 429.\n\n- [ ] Billing pricing is unchanged.";
|
|
8
8
|
const Q1 = "The scrape route must release the spend reservation on 429.";
|
|
@@ -62,7 +62,7 @@ describe("amendments", () => {
|
|
|
62
62
|
|
|
63
63
|
describe("precheck (deterministic, no reviewer)", () => {
|
|
64
64
|
const res = { requirements: [{ id: "R1", status: "implemented" }, { id: "R2", status: "implemented" }], validation: [{ id: "V1", exit: 0 }] };
|
|
65
|
-
it("passes a complete, validated, in-scope change", () => assert.deepEqual(precheck({ contract: C, result: res, changedFiles: files }), { ok: true, failures: [] }));
|
|
65
|
+
it("passes a complete, validated, in-scope change", () => assert.deepEqual(precheck({ contract: C, result: res, changedFiles: files }), { ok: true, failures: [], superseded: [] }));
|
|
66
66
|
it("unauthorized file -> scope_creep; tests and ignored paths are always allowed", () => {
|
|
67
67
|
assert.deepEqual(codes(precheck({ contract: C, result: res, changedFiles: [...files, "src/billing.js", ".forge-lean/x", "tests/test_x.py", "pkg/foo_test.go"], ignore: [".forge-lean/"] })), ["conformance.scope_creep"]);
|
|
68
68
|
for (const t of ["test/a.js", "src/a.test.ts", "tests/test_a.py", "a_test.go", "spec/a.spec.js"]) assert.ok(isTestPath(t), t);
|
|
@@ -72,6 +72,31 @@ describe("precheck (deterministic, no reviewer)", () => {
|
|
|
72
72
|
assert.deepEqual(codes(precheck({ contract: C, result: { ...res, validation: [{ id: "V1", exit: 1 }] }, changedFiles: files })), ["conformance.unimplemented"]);
|
|
73
73
|
assert.deepEqual(codes(precheck({ contract: C, result: { ...res, validation: [] }, changedFiles: files })), ["conformance.unimplemented"]);
|
|
74
74
|
});
|
|
75
|
+
it("#4323 a failed validate whose rerun could not find a host tool or module is env.tool_missing naming it, not conformance", () => {
|
|
76
|
+
const failed = { ...res, validation: [{ id: "V1", exit: 1 }] };
|
|
77
|
+
const pc = precheck({ contract: C, result: failed, changedFiles: files, validateRuns: { V1: { rc: 1, output: "Traceback...\nModuleNotFoundError: No module named 'yaml'" } } });
|
|
78
|
+
assert.deepEqual(codes(pc), ["env.tool_missing"]);
|
|
79
|
+
assert.equal(pc.failures[0].kind, "validate_env"); assert.equal(pc.failures[0].tool, "yaml");
|
|
80
|
+
assert.match(pc.failures[0].detail, /could not run: the host is missing Python module 'yaml'/);
|
|
81
|
+
assert.deepEqual(codes(precheck({ contract: C, result: failed, changedFiles: files, validateRuns: { V1: { rc: 127, output: "bash: line 1: pytest: command not found" } } })), ["env.tool_missing"]);
|
|
82
|
+
assert.deepEqual(codes(precheck({ contract: C, result: failed, changedFiles: files, ciVerified: true, validateRuns: { V1: { rc: 127, output: "bash: pytest: command not found" } } })), [], "CI-verified head: like a validate left to CI");
|
|
83
|
+
assert.deepEqual(codes(precheck({ contract: C, result: failed, changedFiles: files, validateRuns: { V1: { rc: 0, output: "" } } })), [], "a passing rerun counts as passed");
|
|
84
|
+
assert.deepEqual(codes(precheck({ contract: C, result: failed, changedFiles: files, validateRuns: { V1: { rc: 1, output: "AssertionError: 1 !== 2" } } })), ["conformance.unimplemented"]);
|
|
85
|
+
const local = precheck({ contract: C, result: failed, changedFiles: files, repoHas: repoTracks(["src/leak/__init__.py"]), validateRuns: { V1: { rc: 1, output: "ModuleNotFoundError: No module named 'leak.core'" } } });
|
|
86
|
+
assert.deepEqual(codes(local), ["conformance.unimplemented"], "a repo module the change did not create is a code defect");
|
|
87
|
+
});
|
|
88
|
+
it("#4323 missingTool: commands, python and node modules; paths, relative and repo-tracked names are not the environment", () => {
|
|
89
|
+
assert.deepEqual(missingTool({ rc: 127, output: "sh: 1: jq: not found" }), { tool: "jq", kind: "command" });
|
|
90
|
+
assert.deepEqual(missingTool({ rc: 127, output: "" }, { command: "uv run pytest -q" }), { tool: "uv", kind: "command" });
|
|
91
|
+
assert.deepEqual(missingTool({ rc: 1, output: "ImportError: No module named yaml.constructor" }), { tool: "yaml", kind: "python-module" });
|
|
92
|
+
assert.deepEqual(missingTool({ rc: 1, output: "Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'zod' imported from /x/a.mjs" }), { tool: "zod", kind: "node-module" });
|
|
93
|
+
assert.deepEqual(missingTool({ rc: 1, output: "Error: Cannot find module '@scope/pkg/sub'" }), { tool: "@scope/pkg", kind: "node-module" });
|
|
94
|
+
assert.equal(missingTool({ rc: 1, output: "Error: Cannot find module './lib/new.js'" }), null);
|
|
95
|
+
assert.equal(missingTool({ rc: 1, output: "Error: Cannot find module '/repo/src/x.mjs'" }), null);
|
|
96
|
+
assert.equal(missingTool({ rc: 127, output: "bash: ./scripts/check.sh: No such file or directory" }, { command: "./scripts/check.sh" }), null);
|
|
97
|
+
assert.equal(missingTool({ rc: 1, output: "ModuleNotFoundError: No module named 'app'" }, { repoHas: repoTracks(["app.py"]) }), null);
|
|
98
|
+
assert.equal(missingTool({ rc: 1, output: "1 test failed: expected not found" }), null, "rc != 127 needs command not found");
|
|
99
|
+
});
|
|
75
100
|
it("a deliver requirement reported deferred without an amendment -> untracked_deferral; partial -> unimplemented", () => {
|
|
76
101
|
const r = { ...res, requirements: [{ id: "R1", status: "deferred" }, { id: "R2", status: "partial" }] };
|
|
77
102
|
assert.deepEqual(codes(precheck({ contract: C, result: r, changedFiles: files })), ["conformance.untracked_deferral", "conformance.unimplemented"]);
|
|
@@ -117,6 +142,30 @@ describe("conformance review: the engine computes the verdict from complete rows
|
|
|
117
142
|
assert.equal(normalizeEvidencePath("b/route.ts", { changedFiles: ["app/a/route.ts", "app/b/route.ts"] }), "app/b/route.ts");
|
|
118
143
|
assert.equal(decide(row("R1", { evidence: "scrape.js:7" }), ctx).review.requirements[0].evidence, "src/scrape.js:7");
|
|
119
144
|
});
|
|
145
|
+
it("#4327 evidence paths with route-group, dynamic and catch-all segments are kept verbatim", () => {
|
|
146
|
+
const repo = ["apps/admin/app/(console)/layout.tsx", "apps/web/app/blog/[slug]/page.tsx", "apps/web/app/docs/[...slug]/page.tsx"];
|
|
147
|
+
const c = { ...ctx, repoFiles: repo, pathExists: (p) => repo.includes(p) };
|
|
148
|
+
for (const [ev, want] of [
|
|
149
|
+
["`(console)/layout.tsx:12` + nav (sidebar)", "apps/admin/app/(console)/layout.tsx:12"],
|
|
150
|
+
["`apps/admin/app/(console)/layout.tsx`:12", "apps/admin/app/(console)/layout.tsx:12"],
|
|
151
|
+
["see apps/web/app/blog/[slug]/page.tsx:4", "apps/web/app/blog/[slug]/page.tsx:4"],
|
|
152
|
+
["(apps/web/app/docs/[...slug]/page.tsx:9)", "apps/web/app/docs/[...slug]/page.tsx:9"],
|
|
153
|
+
["[page](apps/web/app/docs/[...slug]/page.tsx:9)", "apps/web/app/docs/[...slug]/page.tsx:9"],
|
|
154
|
+
]) {
|
|
155
|
+
const d = decide(row("R1", { evidence: ev }), c);
|
|
156
|
+
assert.equal(d.state, "conforms", ev); assert.equal(d.review.requirements[0].evidence, want, ev);
|
|
157
|
+
}
|
|
158
|
+
assert.deepEqual(evidencePathLine("(src/scrape.js:7), fixed"), { path: "src/scrape.js", line: "7" });
|
|
159
|
+
});
|
|
160
|
+
it("#4327 a missing route-group path is still missing; only path problems qualify for the extra re-ask", () => {
|
|
161
|
+
const c = { ...ctx, repoFiles: ["apps/admin/app/(console)/layout.tsx"], pathExists: (p) => p === "apps/admin/app/(console)/layout.tsx" };
|
|
162
|
+
const d = decide(row("R1", { evidence: "`apps/admin/app/(admin)/layout.tsx:12`" }), c);
|
|
163
|
+
assert.equal(d.state, "review.incomplete"); assert.deepEqual(d.missing, ["R1"]);
|
|
164
|
+
assert.match(d.failures[0].detail, /evidence path apps\/admin\/app\/\(admin\)\/layout\.tsx does not exist/);
|
|
165
|
+
assert.equal(evidencePathOnly(d.failures.map((f) => f.detail)), true);
|
|
166
|
+
assert.equal(evidencePathOnly(decide({ verdict: "PASS" }, ctx).failures.map((f) => f.detail)), false);
|
|
167
|
+
assert.equal(evidencePathOnly([]), false);
|
|
168
|
+
});
|
|
120
169
|
it("#4160 a targeted re-ask merges by id: the repair rows complete the first reply", () => {
|
|
121
170
|
const first = { ...good, requirements: [good.requirements[1]], preserve: [] };
|
|
122
171
|
const d1 = decide(first, ctx);
|
|
@@ -176,6 +225,21 @@ describe("conformance review: the engine computes the verdict from complete rows
|
|
|
176
225
|
assert.equal(movingBaseDiff("git diff HEAD~1"), null);
|
|
177
226
|
assert.equal(movingBaseDiff(""), null);
|
|
178
227
|
});
|
|
228
|
+
it("#4325 a validate or repro_test command reading a $TMPDIR file it does not create is rejected at freeze", () => {
|
|
229
|
+
const bad = validateContract({ ...C, validate: [{ id: "V4", command: "uv run --with-requirements $TMPDIR/api-req.txt pytest tests/x.py" }] }, ISSUE);
|
|
230
|
+
assert.equal(bad.ok, false);
|
|
231
|
+
assert.ok(bad.problems.some((x) => x.startsWith("V4: reads $TMPDIR/api-req.txt")), bad.problems.join("; "));
|
|
232
|
+
const rt = validateContract({ ...C, repro_test: { path: "t/x.py", command: "pytest -r ${TMPDIR}/req.txt t/x.py" } }, ISSUE, { repro: true });
|
|
233
|
+
assert.ok(rt.problems.some((x) => x.startsWith("repro_test: reads $TMPDIR/req.txt")), rt.problems.join("; "));
|
|
234
|
+
for (const ok of ["uv export > $TMPDIR/r.txt && uv run --with-requirements $TMPDIR/r.txt pytest", "pytest --basetemp=$TMPDIR/pt --junitxml $TMPDIR/j.xml", "mkdir -p \"$TMPDIR/d\" && cp a $TMPDIR/d/a && cat $TMPDIR/d/a", "X=$TMPDIR/cache npm test", "echo hi | tee $TMPDIR/l; grep hi $TMPDIR/l", "node --test test/leak.test.js"]) {
|
|
235
|
+
assert.equal(validateContract({ ...C, validate: [{ id: "V4", command: ok }] }, ISSUE).ok, true, ok);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
it("#4325 tmpdirInputs lists read-before-create $TMPDIR paths once, in first-use order", () => {
|
|
239
|
+
assert.deepEqual(tmpdirInputs("cat ${TMPDIR:-/tmp}/a.json | jq . && cp $TMPDIR/b.txt out && cat $TMPDIR/a.json"), ["$TMPDIR/a.json", "$TMPDIR/b.txt"]);
|
|
240
|
+
assert.deepEqual(tmpdirInputs("cp src.txt $TMPDIR/b.txt && cat $TMPDIR/b.txt"), []);
|
|
241
|
+
assert.deepEqual(tmpdirInputs(""), []);
|
|
242
|
+
});
|
|
179
243
|
it("#4160: reviewSkeleton (object form) lists every required row, scope rows pre-filled with the engine's authorization", () => {
|
|
180
244
|
const k = { ...C, requirements: [...C.requirements, { id: "R9", quote: "Billing pricing is unchanged.", disposition: "defer", why: "w" }] };
|
|
181
245
|
const sk = reviewSkeleton({ contract: k, changedFiles: [...files, "src/billing.js", ".forge-lean/x"], ignore: [".forge-lean/"] });
|
|
@@ -245,10 +309,44 @@ describe("close plan and follow-up items", () => {
|
|
|
245
309
|
const k = { requirements: [{ id: "R1", disposition: "deliver" }, { id: "R2", disposition: "defer", tracked: "#9" }, { id: "R3", disposition: "post-deploy", tracked: "#9" }] };
|
|
246
310
|
assert.deepEqual(closePlan(k).tracked, ["#9"]);
|
|
247
311
|
});
|
|
312
|
+
it("#4398 isInvestigation reads the investigate( / investigate: title type only", () => {
|
|
313
|
+
for (const t of ["investigate(stealth): which signal", "Investigate: why", " investigate(x)"]) assert.equal(isInvestigation(t), true, t);
|
|
314
|
+
for (const t of ["fix(investigate): x", "feat: investigate later", "investigation(x)", "", null]) assert.equal(isInvestigation(t), false, String(t));
|
|
315
|
+
});
|
|
316
|
+
it("#4398 an investigation closes partial when a requirement is deferred or its answer is not implemented; otherwise completed", () => {
|
|
317
|
+
const title = "investigate(stealth): which device signal is rejected";
|
|
318
|
+
const k = { requirements: [{ id: "R1", disposition: "deliver" }, { id: "R2", disposition: "defer", tracked: "#9" }, { id: "R3", disposition: "not-applicable" }] };
|
|
319
|
+
const p = closePlan(k, { title });
|
|
320
|
+
assert.equal(p.reason, "partial"); assert.deepEqual(p.open.map((r) => r.id), ["R2"]); assert.deepEqual(p.tracked, ["#9"]);
|
|
321
|
+
assert.deepEqual(closePlan(k, { title: "fix: foo" }), { reason: "completed", tracked: ["#9"] }, "only investigations change");
|
|
322
|
+
const d = { requirements: [{ id: "R1", disposition: "deliver" }, { id: "R2", disposition: "deliver" }] };
|
|
323
|
+
const rows = (s1, s2) => ({ requirements: [{ id: "R1", status: s1 }, { id: "R2", status: s2 }] });
|
|
324
|
+
assert.deepEqual(closePlan(d, { title, review: rows("implemented", "implemented") }), { reason: "completed", tracked: [] });
|
|
325
|
+
assert.deepEqual(closePlan(d, { title, review: rows("implemented", "deferred") }).open.map((r) => r.id), ["R2"]);
|
|
326
|
+
assert.deepEqual(closePlan(d, { title, review: { requirements: [{ id: "R1", status: "implemented" }] } }).open.map((r) => r.id), ["R2"], "a missing row is not an answer");
|
|
327
|
+
assert.equal(closePlan(d, { title, review: null }).reason, "completed", "a skipped review judges dispositions only");
|
|
328
|
+
});
|
|
329
|
+
it("#4398 formatShadowSummary: shadow would-block and false-block shares, risk-enforced lanes; null without conformance rows", () => {
|
|
330
|
+
assert.equal(formatShadowSummary([{ issue: 1 }]), null); assert.equal(formatShadowSummary(undefined), null);
|
|
331
|
+
const rows = [{ conformance: { mode: "shadow", would: "conforms", risk: false } }, { conformance: { mode: "shadow", would: "nonconforming", risk: false } },
|
|
332
|
+
{ conformance: { mode: "shadow", would: "review.incomplete", risk: false } }, { conformance: { mode: "shadow", would: null, risk: false } },
|
|
333
|
+
{ conformance: { mode: "enforce", would: null, risk: true } }, { issue: 9 }];
|
|
334
|
+
assert.equal(formatShadowSummary(rows), "conformance: 4 shadow lanes, would block 2 (50%), false-block 1 (25%: would-block on an incomplete/inconsistent/unparseable review); risk-path enforced: 1");
|
|
335
|
+
assert.match(formatShadowSummary([{ conformance: { mode: "enforce", would: null, risk: false } }]), /0 shadow lanes, would block 0 \(n\/a\)/);
|
|
336
|
+
});
|
|
248
337
|
it("follow-up keys are single-line, tab-free and stable", () => {
|
|
249
338
|
const [i] = followupItems([{ id: "R2", quote: "a\tb\n`c`", disposition: "defer", why: "x\ny" }]);
|
|
250
339
|
assert.equal(i.key, "R2: a b c"); assert.equal(i.why, "defer: x y"); assert.doesNotMatch(i.key + i.why, /[\t\n`]/);
|
|
251
340
|
});
|
|
341
|
+
it("a follow-up key whose 240-char cut ends on a space has no trailing whitespace (#4388)", () => {
|
|
342
|
+
const quote = `${"x".repeat(234)} tail of the quote past the cut`; // "R12: " + 234 chars puts the space at index 239
|
|
343
|
+
assert.equal(`R12: ${quote}`[239], " ");
|
|
344
|
+
const [i] = followupItems([{ id: "R12", quote, disposition: "defer", why: "w" }]);
|
|
345
|
+
assert.equal(i.key, i.key.trim()); assert.equal(i.key.length, 239); assert.doesNotMatch(i.key, /\s$/);
|
|
346
|
+
});
|
|
347
|
+
it("followupMap resolves untrimmed item keys from the script's trimmed echo (#4388)", () => {
|
|
348
|
+
assert.deepEqual(followupMap("R1: a\t42\nnoise\nR2: b\tx\n", [{ key: "R1: a " }, { key: "R2: b" }]), { "R1: a ": 42 });
|
|
349
|
+
});
|
|
252
350
|
});
|
|
253
351
|
|
|
254
352
|
describe("hotspot growth precheck (forge#4195)", () => {
|
|
@@ -300,13 +398,23 @@ describe("repro_test on bug lanes (#4271)", () => {
|
|
|
300
398
|
});
|
|
301
399
|
it("red on base and green on head passes; anything else fails the precheck as conformance.unimplemented", () => {
|
|
302
400
|
const k = { ...C, repro_test: RT };
|
|
303
|
-
assert.deepEqual(precheck({ contract: k, result: res, changedFiles: files, repro: { base: "FAILED", head: "RAN" } }), { ok: true, failures: [] });
|
|
304
|
-
for (const repro of [{ base: "RAN", head: "RAN" }, { base: "SKIPPED", head: "FAILED" }, { base: "UNRUNNABLE", head: "RAN" }, { base: "
|
|
401
|
+
assert.deepEqual(precheck({ contract: k, result: res, changedFiles: files, repro: { base: "FAILED", head: "RAN" } }), { ok: true, failures: [], superseded: [] });
|
|
402
|
+
for (const repro of [{ base: "RAN", head: "RAN" }, { base: "SKIPPED", head: "FAILED" }, { base: "UNRUNNABLE", head: "RAN" }, { base: "SKIPPED", head: "RAN_NOTHING" }]) {
|
|
305
403
|
const pc = precheck({ contract: k, result: res, changedFiles: files, repro });
|
|
306
404
|
assert.deepEqual(pc.failures.map((f) => [f.code, f.kind]), [["conformance.unimplemented", "repro_red_green"]], JSON.stringify(repro));
|
|
307
405
|
assert.match(pc.failures[0].detail, new RegExp(`BASE=${repro.base} HEAD=${repro.head}`));
|
|
308
406
|
}
|
|
309
|
-
assert.match(reproFailures(k, {})[0].detail, /
|
|
407
|
+
assert.match(reproFailures(k, { base: "SKIPPED", head: "FAILED" })[0].detail, /HEAD=FAILED \(base not run: head did not pass\)$/);
|
|
408
|
+
assert.doesNotMatch(reproFailures(k, { base: "RAN", head: "RAN" })[0].detail, /base not run/);
|
|
409
|
+
});
|
|
410
|
+
it("a harness UNRUNNABLE on head is env.repro_unrunnable, not conformance.unimplemented (#4334)", () => {
|
|
411
|
+
const k = { ...C, repro_test: RT };
|
|
412
|
+
for (const repro of [{ base: "SKIPPED", head: "UNRUNNABLE" }, { base: "UNRUNNABLE", head: "UNRUNNABLE" }, {}]) {
|
|
413
|
+
const pc = precheck({ contract: k, result: res, changedFiles: files, repro });
|
|
414
|
+
assert.deepEqual(pc.failures.map((f) => [f.code, f.kind]), [["env.repro_unrunnable", "repro_env"]], JSON.stringify(repro));
|
|
415
|
+
assert.match(pc.failures[0].detail, /test\/leak\.test\.js releases on 429 \(`node --test test\/leak\.test\.js`\): the harness could not run the test on head \(environment, not the change\)/);
|
|
416
|
+
assert.match(pc.failures[0].detail, new RegExp(`BASE=${repro.base ?? "UNRUNNABLE"} HEAD=UNRUNNABLE$`));
|
|
417
|
+
}
|
|
310
418
|
});
|
|
311
419
|
it("off (no repro option) is today's contract check and precheck exactly", () => {
|
|
312
420
|
assert.deepEqual(validateContract(C, ISSUE), validateContract(C, ISSUE, { repro: false }));
|
|
@@ -351,3 +459,141 @@ describe("#4265 issue scope and failure explanations", () => {
|
|
|
351
459
|
assert.equal(r.failures[0].detail, "validate V1 (`npm test`) failed (exit 2)");
|
|
352
460
|
});
|
|
353
461
|
});
|
|
462
|
+
|
|
463
|
+
describe("#4350 the engine's scope gate is the authority over a builder's scope-check validate", () => {
|
|
464
|
+
const ALLOW = 'test -z "$(git diff --name-only origin/main...HEAD | grep -v -x -e src/tool.js -e test/tool.test.js)"';
|
|
465
|
+
const frozen = {
|
|
466
|
+
requirements: [{ id: "R1", quote: Q1, disposition: "deliver" }],
|
|
467
|
+
deliverables: [{ id: "D1", path: "src/tool.js" }],
|
|
468
|
+
validate: [{ id: "V1", command: "node --test test/tool.test.js" }, { id: "V2", command: ALLOW }],
|
|
469
|
+
};
|
|
470
|
+
const amendments = [{ kind: "add-deliverable", path: "docs/tool.md", why: "the docs-freshness rule needs the module doc" }];
|
|
471
|
+
const effective = applyAmendments(frozen, amendments).contract;
|
|
472
|
+
const changed = ["src/tool.js", "test/tool.test.js", "docs/tool.md", ".forge-lean/result.json"];
|
|
473
|
+
const result = { requirements: [{ id: "R1", status: "implemented" }], validation: [{ id: "V1", exit: 0 }, { id: "V2", exit: 1 }], amendments };
|
|
474
|
+
const run = (o = {}) => precheck({ contract: effective, result, changedFiles: changed, ignore: [".forge-lean/"], validateRuns: { V2: { rc: 1, output: "" } }, frozen, ...o });
|
|
475
|
+
it("#4350 a scope-allowlist validate that fails only for an amended deliverable is superseded, not conformance.unimplemented", () => {
|
|
476
|
+
const pc = run();
|
|
477
|
+
assert.deepEqual(pc.failures, []);
|
|
478
|
+
assert.equal(pc.ok, true, "the lane is not ended by the stale check");
|
|
479
|
+
assert.deepEqual(pc.superseded, [{ id: "V2", files: ["docs/tool.md"] }]);
|
|
480
|
+
});
|
|
481
|
+
it("#4350 a real out-of-scope edit is still scope creep, and the stale validate failure stands", () => {
|
|
482
|
+
const pc = run({ changedFiles: [...changed, "src/other.js"] });
|
|
483
|
+
assert.deepEqual(codes(pc), ["conformance.scope_creep", "conformance.unimplemented"]);
|
|
484
|
+
assert.match(pc.failures[0].detail, /src\/other\.js/);
|
|
485
|
+
assert.deepEqual(pc.superseded, []);
|
|
486
|
+
});
|
|
487
|
+
it("#4350 a failing non-scope validate, a scope check failing with no amended file, and callers without frozen are unchanged", () => {
|
|
488
|
+
const testFail = { ...result, validation: [{ id: "V1", exit: 1 }, { id: "V2", exit: 0 }] };
|
|
489
|
+
assert.deepEqual(codes(run({ result: testFail, validateRuns: { V1: { rc: 1, output: "AssertionError" } } })), ["conformance.unimplemented"]);
|
|
490
|
+
assert.deepEqual(codes(run({ changedFiles: ["src/tool.js", "test/tool.test.js"] })), ["conformance.unimplemented"], "nothing amended in the diff: the validate failed for its own reason");
|
|
491
|
+
const legacy = run({ frozen: undefined });
|
|
492
|
+
assert.deepEqual(codes(legacy), ["conformance.unimplemented"]);
|
|
493
|
+
assert.deepEqual(legacy.superseded, []);
|
|
494
|
+
});
|
|
495
|
+
it("#4350 isScopeCheck recognises file-listing git commands only", () => {
|
|
496
|
+
for (const c of [ALLOW, "git diff --name-status origin/x...HEAD | grep -c .", "git -C pkg diff --stat $(git merge-base HEAD origin/x)", "git log --name-only -1", "test -z \"$(git status --porcelain)\""]) assert.ok(isScopeCheck(c), c);
|
|
497
|
+
for (const c of ["npm test", "git diff --check origin/x...HEAD", "pytest -k name_only", "git diff --exit-code -- src/a.js"]) assert.ok(!isScopeCheck(c), c);
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
describe("#4353 the engine's scope gate is the only scope authority: pure scope validates are dropped at freeze", () => {
|
|
502
|
+
const ALLOW = 'test -z "$(git diff --name-only origin/main...HEAD | grep -v -x -e src/tool.js -e test/tool.test.js)"';
|
|
503
|
+
it("#4353 isPureScopeCheck: an allowlist or denylist made only of git listing and filter commands; anything that also runs code is kept", () => {
|
|
504
|
+
for (const c of [ALLOW, "git diff --name-status origin/x...HEAD | grep -c .", "git -C pkg diff --stat $(git merge-base HEAD origin/x)", "! git diff --name-only origin/x...HEAD | grep -qv '^src/'", "test -z \"$(git status --porcelain)\"", "[ \"$(git diff --name-only origin/x...HEAD | wc -l)\" -le 3 ]"]) assert.ok(isPureScopeCheck(c), c);
|
|
505
|
+
for (const c of ["npm test", "node --test x && git diff --stat origin/x...HEAD", "git diff --name-only origin/x...HEAD | xargs node --check", "pytest -k name_only", "git diff --exit-code -- src/a.js", "git diff --name-only HEAD~1 | sh"]) assert.ok(!isPureScopeCheck(c), c);
|
|
506
|
+
});
|
|
507
|
+
it("#4353 dropScopeValidates removes only pure scope validates and reports them; other contracts are unchanged", () => {
|
|
508
|
+
const c = { requirements: [{ id: "R1", quote: Q1, disposition: "deliver" }], deliverables: [{ id: "D1", path: "src/tool.js" }],
|
|
509
|
+
validate: [{ id: "V1", command: "node --test test/tool.test.js" }, { id: "V2", command: ALLOW }, { id: "V3", command: "node --test x && git diff --stat" }] };
|
|
510
|
+
const d = dropScopeValidates(c);
|
|
511
|
+
assert.deepEqual(d.dropped, [{ id: "V2", command: ALLOW }]);
|
|
512
|
+
assert.deepEqual(d.contract.validate.map((v) => v.id), ["V1", "V3"]);
|
|
513
|
+
assert.equal(c.validate.length, 3, "the input is not mutated");
|
|
514
|
+
assert.equal(validateContract(d.contract, `${Q1}`).ok, true, "the dropped contract still freezes");
|
|
515
|
+
const plain = { ...c, validate: [c.validate[0]] };
|
|
516
|
+
assert.equal(dropScopeValidates(plain).contract, plain); assert.deepEqual(dropScopeValidates(plain).dropped, []);
|
|
517
|
+
for (const x of [null, [], "x", { requirements: [] }]) assert.deepEqual(dropScopeValidates(x), { contract: x, dropped: [] });
|
|
518
|
+
});
|
|
519
|
+
it("#4353 a legacy frozen contract that still carries the allowlist keeps the #4350 superseded path", () => {
|
|
520
|
+
const frozen = { requirements: [{ id: "R1", quote: Q1, disposition: "deliver" }], deliverables: [{ id: "D1", path: "src/tool.js" }], validate: [{ id: "V1", command: "node --test test/tool.test.js" }, { id: "V2", command: ALLOW }] };
|
|
521
|
+
const amendments = [{ kind: "add-deliverable", path: "docs/tool.md", why: "module doc" }];
|
|
522
|
+
const pc = precheck({ contract: applyAmendments(frozen, amendments).contract, frozen, changedFiles: ["src/tool.js", "docs/tool.md"], validateRuns: { V2: { rc: 1, output: "" } },
|
|
523
|
+
result: { requirements: [{ id: "R1", status: "implemented" }], validation: [{ id: "V1", exit: 0 }, { id: "V2", exit: 1 }], amendments } });
|
|
524
|
+
assert.equal(pc.ok, true); assert.deepEqual(pc.superseded, [{ id: "V2", files: ["docs/tool.md"] }]);
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
describe("#4404 verified out of band: a requirement measured outside the lane is judged by its instrumentation", () => {
|
|
529
|
+
const LH = "Lighthouse performance score >= 95 on the landing page.";
|
|
530
|
+
const TTI = "Interactive < 1 s on a throttled mid-range Android profile.";
|
|
531
|
+
const NIGHTLY = "The nightly perf suite reports the 1,000-event render time.";
|
|
532
|
+
const OOB_ISSUE = `${ISSUE}\n- [ ] ${LH}\n- [ ] ${TTI}\n- [ ] ${NIGHTLY}\n- [ ] Field perf stays within budget.`;
|
|
533
|
+
const k = { ...C, requirements: [...C.requirements, { id: "R3", quote: LH, disposition: "deliver", evidence: "lighthouse-nightly" }] };
|
|
534
|
+
const tagged = tagOutOfBand(k).contract;
|
|
535
|
+
const oobRow = { id: "R3", status: "partial", evidence: "web/perf/budget.test.js:4", validated_by: "V1", verify: "out_of_band", job: "lighthouse-nightly", instrumented: true };
|
|
536
|
+
const oobCtx = { ...ctx, contract: tagged, changedFiles: [...files, "web/perf/budget.test.js"], issueText: OOB_ISSUE };
|
|
537
|
+
const withRow = (r) => ({ ...good, requirements: [...good.requirements, r], issue_coverage: [...good.issue_coverage, { quote: LH, status: "covered" }] });
|
|
538
|
+
|
|
539
|
+
it("a contract requirement mentioning Lighthouse or field perf is tagged out_of_band at plan time", () => {
|
|
540
|
+
const c = { ...C, requirements: [...C.requirements,
|
|
541
|
+
{ id: "R3", quote: LH, disposition: "deliver" }, { id: "R4", quote: "Field perf stays within budget.", disposition: "deliver" },
|
|
542
|
+
{ id: "R5", quote: TTI, disposition: "deliver" }, { id: "R6", quote: NIGHTLY, disposition: "deliver" },
|
|
543
|
+
{ id: "R7", quote: LH, disposition: "deliver", verify: "in_lane" }, { id: "R8", quote: LH, disposition: "post-deploy", why: "w" }] };
|
|
544
|
+
const t = tagOutOfBand(c);
|
|
545
|
+
assert.deepEqual(t.tagged, ["R3", "R4", "R5", "R6"]);
|
|
546
|
+
assert.deepEqual(t.contract.requirements.filter(isOutOfBand).map((r) => r.id), ["R3", "R4", "R5", "R6"]);
|
|
547
|
+
assert.equal(t.contract.requirements.find((r) => r.id === "R7").verify, "in_lane"); // an explicit verify wins
|
|
548
|
+
assert.equal(t.contract.requirements.find((r) => r.id === "R8").verify, undefined); // only deliver requirements
|
|
549
|
+
assert.equal(c.requirements[2].verify, undefined); // the input contract is not mutated
|
|
550
|
+
assert.equal(tagOutOfBand(C).contract, C); assert.deepEqual(tagOutOfBand(C).tagged, []);
|
|
551
|
+
for (const x of [null, [], "x", {}]) assert.deepEqual(tagOutOfBand(x), { contract: x, tagged: [] });
|
|
552
|
+
assert.deepEqual(VERIFY_MODES, ["in_lane", "out_of_band"]);
|
|
553
|
+
for (const q of [LH, TTI, NIGHTLY, "Field performance holds.", "Web vitals pass.", "A report-only bench workflow tracks it."]) assert.match(q, OUT_OF_BAND_RE, q);
|
|
554
|
+
for (const q of ["The nightly build. Then a suite.", "Add a unit test for the parser.", "Run the job locally."]) assert.doesNotMatch(q, OUT_OF_BAND_RE, q);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it("an add-requirement amendment is tagged the same way", () => {
|
|
558
|
+
const a = applyAmendments(C, [{ kind: "add-requirement", requirement: { id: "R9", quote: LH }, why: "missed" }], OOB_ISSUE);
|
|
559
|
+
assert.equal(a.contract.requirements.find((r) => r.id === "R9").verify, "out_of_band");
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it("validateContract: an out-of-band requirement names its job; verify takes only known modes and deliver", () => {
|
|
563
|
+
assert.deepEqual(validateContract(tagged, OOB_ISSUE), { ok: true, problems: [] });
|
|
564
|
+
const noJob = tagOutOfBand({ ...C, requirements: [...C.requirements, { id: "R3", quote: LH, disposition: "deliver" }] }).contract;
|
|
565
|
+
assert.match(validateContract(noJob, OOB_ISSUE).problems.join("\n"), /R3: verify out_of_band needs evidence naming the job/);
|
|
566
|
+
const bad = { ...C, requirements: [{ ...C.requirements[0], verify: "later" }, { ...C.requirements[1], disposition: "defer", why: "w", verify: "out_of_band", evidence: "j" }] };
|
|
567
|
+
const p = validateContract(bad, OOB_ISSUE).problems.join("\n");
|
|
568
|
+
assert.match(p, /R1: verify must be one of in_lane\|out_of_band/); assert.match(p, /R2: verify out_of_band applies only to a deliver requirement/);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it("with the instrumentation present the reviewer passes the requirement; without it it is still unimplemented", () => {
|
|
572
|
+
assert.equal(decide(withRow(oobRow), oobCtx).state, "conforms");
|
|
573
|
+
const missing = decide(withRow({ ...oobRow, status: "missing", instrumented: false, evidence: "" }), oobCtx);
|
|
574
|
+
assert.equal(missing.state, "nonconforming"); assert.deepEqual(codes(missing), ["conformance.unimplemented"]);
|
|
575
|
+
assert.match(missing.failures[0].detail, /R3 verified out of band by lighthouse-nightly: instrumentation missing/);
|
|
576
|
+
// instrumented is required on an out-of-band row, and instrumented true needs path:line evidence
|
|
577
|
+
assert.deepEqual(checkCompleteness(withRow({ ...oobRow, instrumented: null }), oobCtx).missing, ["R3"]);
|
|
578
|
+
assert.deepEqual(checkCompleteness(withRow({ ...oobRow, evidence: "" }), oobCtx).missing, ["R3"]);
|
|
579
|
+
// the same partial row on an in-lane requirement still fails (unchanged behaviour)
|
|
580
|
+
const inLane = { ...oobCtx, contract: { ...tagged, requirements: tagged.requirements.map((r) => (r.id === "R3" ? { ...r, verify: "in_lane" } : r)) } };
|
|
581
|
+
assert.deepEqual(codes(decide(withRow(oobRow), inLane)), ["conformance.unimplemented"]);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it("precheck: the builder's instrumented out-of-band row is not unimplemented; a bare partial one is", () => {
|
|
585
|
+
const res = { requirements: [{ id: "R1", status: "implemented" }, { id: "R2", status: "implemented" }], validation: [{ id: "V1", exit: 0 }] };
|
|
586
|
+
const ok = { requirements: [...res.requirements, { id: "R3", status: "partial", instrumented: true }], validation: res.validation };
|
|
587
|
+
assert.equal(precheck({ contract: tagged, result: ok, changedFiles: files }).ok, true);
|
|
588
|
+
const no = { requirements: [...res.requirements, { id: "R3", status: "partial" }], validation: res.validation };
|
|
589
|
+
const f = precheck({ contract: tagged, result: no, changedFiles: files }).failures;
|
|
590
|
+
assert.deepEqual(f.map((x) => x.kind), ["requirement_unimplemented"]); assert.match(f[0].detail, /R3 is verified out of band by lighthouse-nightly/);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it("the skeleton row carries the job and an empty instrumented; outOfBandRequirements lists the job", () => {
|
|
594
|
+
const sk = reviewSkeleton(tagged, files);
|
|
595
|
+
assert.deepEqual(sk.requirements.find((r) => r.id === "R3"), { id: "R3", status: "", evidence: "", validated_by: "", verify: "out_of_band", job: "lighthouse-nightly", instrumented: null, note: "" });
|
|
596
|
+
assert.equal("instrumented" in sk.requirements[0], false);
|
|
597
|
+
assert.deepEqual(outOfBandRequirements(tagged), [{ id: "R3", quote: LH, job: "lighthouse-nightly" }]);
|
|
598
|
+
});
|
|
599
|
+
});
|