lastlight-evals 0.10.0 → 0.12.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/dashboard/dist/assets/index-B4TAaIUR.css +1 -0
- package/dashboard/dist/assets/index-LFdFc6Z9.js +109 -0
- package/dashboard/dist/index.html +2 -2
- package/datasets/aacr-bench/README.md +146 -0
- package/datasets/pr-review/README.md +41 -0
- package/datasets/pr-review/anchors.json +3709 -0
- package/dist/add-case.js.map +1 -1
- package/dist/arm.js +14 -5
- package/dist/arm.js.map +1 -1
- package/dist/bootstrap.js +14 -0
- package/dist/bootstrap.js.map +1 -1
- package/dist/clean.js.map +1 -1
- package/dist/config.js +59 -20
- package/dist/config.js.map +1 -1
- package/dist/fake-github.js +72 -0
- package/dist/fake-github.js.map +1 -1
- package/dist/grade.js +308 -64
- package/dist/grade.js.map +1 -1
- package/dist/init.js.map +1 -1
- package/dist/judge.js +19 -0
- package/dist/judge.js.map +1 -1
- package/dist/mechanism.test.js +303 -13
- package/dist/mechanism.test.js.map +1 -1
- package/dist/metrics.js +88 -9
- package/dist/metrics.js.map +1 -1
- package/dist/paths.js +135 -1
- package/dist/paths.js.map +1 -1
- package/dist/phase-models.js +56 -0
- package/dist/phase-models.js.map +1 -0
- package/dist/pool.js +32 -0
- package/dist/pool.js.map +1 -0
- package/dist/pool.test.js +180 -0
- package/dist/pool.test.js.map +1 -0
- package/dist/pr-context.js +77 -6
- package/dist/pr-context.js.map +1 -1
- package/dist/repeats.test.js +317 -0
- package/dist/repeats.test.js.map +1 -0
- package/dist/report.js +99 -5
- package/dist/report.js.map +1 -1
- package/dist/review-metrics.js +571 -0
- package/dist/review-metrics.js.map +1 -0
- package/dist/review-metrics.test.js +634 -0
- package/dist/review-metrics.test.js.map +1 -0
- package/dist/review-pipeline-stats.js +455 -0
- package/dist/review-pipeline-stats.js.map +1 -0
- package/dist/review-pipeline-stats.test.js +413 -0
- package/dist/review-pipeline-stats.test.js.map +1 -0
- package/dist/run-instance.js +176 -45
- package/dist/run-instance.js.map +1 -1
- package/dist/run.js +637 -122
- package/dist/run.js.map +1 -1
- package/dist/schema.js +22 -1
- package/dist/schema.js.map +1 -1
- package/package.json +9 -8
- package/dashboard/dist/assets/index-B1jNgf2P.js +0 -313
- package/dashboard/dist/assets/index-Sic0jI7y.css +0 -1
package/dist/judge.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"judge.js","sourceRoot":"","sources":["../src/judge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAaH;2CAC2C;AAC3C,SAAS,SAAS,CAAC,EAAU;IAC3B,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC1B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACvC,CAAC;AAED,SAAS,gBAAgB,CAAC,GAAW,EAAE,MAAc;IACnD,OAAO;QACL,GAAG;QACH,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,MAAM,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAClF,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,CAAC,CAAC;YAC3C,KAAK;YACL,QAAQ,EAAE;gBACR,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE;gBACnC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE;aAChC;YACD,GAAG,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"judge.js","sourceRoot":"","sources":["../src/judge.ts"],"names":[],"mappings":"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"}
|
package/dist/mechanism.test.js
CHANGED
|
@@ -13,16 +13,17 @@ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, rmSync
|
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { tmpdir } from "node:os";
|
|
16
|
-
import { startFakeGitHub } from "./fake-github.js";
|
|
16
|
+
import { startFakeGitHub, closingIssueNumbers } from "./fake-github.js";
|
|
17
17
|
import { appliedRepoConfigKeys, loadRepoConfigFixture, resolveEvalRepoConfig, } from "./repo-config.js";
|
|
18
18
|
import { seedWorkspace, seedWorkspaceFromGit, prFilesFromGit, injectRepoContext } from "./seed.js";
|
|
19
19
|
import { gitDiffAgainstBase } from "./run-instance.js";
|
|
20
20
|
import { execFileSync } from "node:child_process";
|
|
21
21
|
import { gradeExecution, gradeBehavioral, gradeTriage, gradeReview, gradeMarkers, fBeta } from "./grade.js";
|
|
22
22
|
import { prContextPatch } from "./pr-context.js";
|
|
23
|
-
import { defaultFixConfig } from "lastlight-core/evals";
|
|
23
|
+
import { defaultFixConfig, resolveReviewGitHubClient } from "lastlight-core/evals";
|
|
24
24
|
import { computeMartianRanking } from "./report.js";
|
|
25
25
|
import { loadMergedConfig, resolvePhaseModel } from "./config.js";
|
|
26
|
+
import { modelTemplateForRow } from "./phase-models.js";
|
|
26
27
|
import { modelsArm, configArm, releaseOverlayGuard } from "./arm.js";
|
|
27
28
|
import { collectMetrics } from "./metrics.js";
|
|
28
29
|
const staticAuth = { getToken: async () => "fake-token", expiresAt: null, canRefresh: false };
|
|
@@ -625,6 +626,74 @@ describe("config run type — per-step model resolution (config.ts)", () => {
|
|
|
625
626
|
// 5. Template referencing an unset key → falls through to phase/default.
|
|
626
627
|
expect(resolvePhaseModel("{{models.missing}}", "executor", models)).toBe("m-default");
|
|
627
628
|
});
|
|
629
|
+
it("renders {{#if}} conditional templates via core's engine (the adjudicate fallback pair)", () => {
|
|
630
|
+
// pr-review.yaml's `adjudicate` model — an {{#if}}/{{#if !x}} pair that
|
|
631
|
+
// falls back to models.review when models.review-adjudicate is unset. The
|
|
632
|
+
// old bare-variable regex left the {{#if}} blocks un-rendered, so the
|
|
633
|
+
// recorded PhaseMetric.model was literal template residue, not a model id.
|
|
634
|
+
const tpl = "{{#if models.review-adjudicate}}{{models.review-adjudicate}}{{/if}}" +
|
|
635
|
+
"{{#if !models.review-adjudicate}}{{models.review}}{{/if}}";
|
|
636
|
+
const models = { default: "m-default", review: "m-review" };
|
|
637
|
+
// Unset → the {{#if !x}} branch renders models.review.
|
|
638
|
+
expect(resolvePhaseModel(tpl, "adjudicate", models)).toBe("m-review");
|
|
639
|
+
// Set → the pinned value wins and the negated branch renders empty.
|
|
640
|
+
expect(resolvePhaseModel(tpl, "adjudicate", { ...models, "review-adjudicate": "m-pinned" })).toBe("m-pinned");
|
|
641
|
+
// Both unset → both branches render empty → falls through to the default
|
|
642
|
+
// (matching core: an empty render is falsy, resolver → fallback).
|
|
643
|
+
expect(resolvePhaseModel(tpl, "adjudicate", { default: "m-default" })).toBe("m-default");
|
|
644
|
+
});
|
|
645
|
+
it("resolvePhaseModel branch precedence: template → models[label] → models[parent] → default", () => {
|
|
646
|
+
const models = {
|
|
647
|
+
default: "m-default",
|
|
648
|
+
survey: "m-survey",
|
|
649
|
+
survey_branch_contract: "m-contract-pin",
|
|
650
|
+
};
|
|
651
|
+
// Core's fanout resolver (`fanout.ts resolveModelVariant`) tries the branch
|
|
652
|
+
// LABEL as the task name, then the parent phase as fallbackTask.
|
|
653
|
+
expect(resolvePhaseModel(undefined, "survey_branch_contract", models, "survey")).toBe("m-contract-pin");
|
|
654
|
+
expect(resolvePhaseModel(undefined, "survey_branch_tests", models, "survey")).toBe("m-survey");
|
|
655
|
+
expect(resolvePhaseModel(undefined, "other_branch_x", models, "other")).toBe("m-default");
|
|
656
|
+
// A rendered template beats both map lookups.
|
|
657
|
+
expect(resolvePhaseModel("{{models.survey}}", "survey_branch_contract", models, "survey")).toBe("m-survey");
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
describe("modelTemplateForRow — ledger label → YAML model template (phase-models.ts)", () => {
|
|
661
|
+
// A pr-review-shaped def: a fanout phase whose `model:` template must govern
|
|
662
|
+
// every `<parent>_branch_<name>` row, plus one branch with its own override.
|
|
663
|
+
const phases = [
|
|
664
|
+
{ name: "seed" },
|
|
665
|
+
{
|
|
666
|
+
name: "survey",
|
|
667
|
+
model: "{{models.review-survey}}",
|
|
668
|
+
branches: [{ name: "contract" }, { name: "tests", model: "{{models.survey-tests}}" }],
|
|
669
|
+
},
|
|
670
|
+
{ name: "review", model: "{{models.review}}" },
|
|
671
|
+
];
|
|
672
|
+
it("maps declared phase names to their own template (old behaviour)", () => {
|
|
673
|
+
expect(modelTemplateForRow(phases, "survey")).toEqual({
|
|
674
|
+
template: "{{models.review-survey}}",
|
|
675
|
+
fallbackPhase: undefined,
|
|
676
|
+
});
|
|
677
|
+
expect(modelTemplateForRow(phases, "seed")).toEqual({ template: undefined, fallbackPhase: undefined });
|
|
678
|
+
});
|
|
679
|
+
it("maps branch rows to the parent's template with the parent as fallback task", () => {
|
|
680
|
+
for (const label of ["survey_branch_contract", "survey_branch_contract_retry", "survey_branch_contract_check"]) {
|
|
681
|
+
expect(modelTemplateForRow(phases, label)).toEqual({
|
|
682
|
+
template: "{{models.review-survey}}",
|
|
683
|
+
fallbackPhase: "survey",
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
it("a branch-level model: override beats the parent template (forward-looking)", () => {
|
|
688
|
+
expect(modelTemplateForRow(phases, "survey_branch_tests")).toEqual({
|
|
689
|
+
template: "{{models.survey-tests}}",
|
|
690
|
+
fallbackPhase: "survey",
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
it("loop-derived and unknown labels keep the old no-template behaviour", () => {
|
|
694
|
+
expect(modelTemplateForRow(phases, "review_fix_1")).toEqual({ template: undefined, fallbackPhase: undefined });
|
|
695
|
+
expect(modelTemplateForRow(phases, "nonexistent")).toEqual({ template: undefined, fallbackPhase: undefined });
|
|
696
|
+
});
|
|
628
697
|
});
|
|
629
698
|
describe("Arm seam — model-selection adapters (arm.ts)", () => {
|
|
630
699
|
// A stand-in core root (just the one file loadMergedConfig reads) + an overlay
|
|
@@ -662,6 +731,26 @@ describe("Arm seam — model-selection adapters (arm.ts)", () => {
|
|
|
662
731
|
it("activate() is a no-op (no overlay to switch)", () => {
|
|
663
732
|
expect(() => modelsArm("m", "f").activate()).not.toThrow();
|
|
664
733
|
});
|
|
734
|
+
// A forced model is not the whole arm: the overlay's `review:` policy is a
|
|
735
|
+
// deployment fact `--model` says nothing about, and `review.analysis.enabled`
|
|
736
|
+
// is what switches the review evidence pipeline on. A `models` run with an
|
|
737
|
+
// overlay must carry it, or `--model X --overlay wp3` silently runs baseline.
|
|
738
|
+
it("carries the overlay's review policy; no overlay ⇒ undefined", () => {
|
|
739
|
+
const { root, overlay } = makeRoots();
|
|
740
|
+
try {
|
|
741
|
+
// The overlay `makeRoots` writes has no `review:` — absent, not empty.
|
|
742
|
+
expect(modelsArm("m", "f", overlay).review).toBeUndefined();
|
|
743
|
+
expect(modelsArm("m", "f").review).toBeUndefined();
|
|
744
|
+
writeFileSync(join(overlay, "config.yaml"), "models:\n default: openai/gpt-5.4-mini\nreview:\n postsCheck: true\n analysis:\n enabled: true\n surveyPasses: 6\n");
|
|
745
|
+
const arm = modelsArm("m", "f", overlay);
|
|
746
|
+
expect(arm.review).toEqual({ postsCheck: true, analysis: { enabled: true, surveyPasses: 6 } });
|
|
747
|
+
// Config arms read the same block from the same file.
|
|
748
|
+
expect(configArm(root, overlay).review).toEqual(arm.review);
|
|
749
|
+
}
|
|
750
|
+
finally {
|
|
751
|
+
rmSync(root, { recursive: true, force: true });
|
|
752
|
+
}
|
|
753
|
+
});
|
|
665
754
|
});
|
|
666
755
|
describe("configArm — a deployment's per-step config drives selection", () => {
|
|
667
756
|
it("prepare() patches ctx.models/variants and returns the merged maps + default", () => {
|
|
@@ -713,6 +802,68 @@ describe("Arm seam — model-selection adapters (arm.ts)", () => {
|
|
|
713
802
|
rmSync(root, { recursive: true, force: true });
|
|
714
803
|
}
|
|
715
804
|
});
|
|
805
|
+
it("records fan-out BRANCH rows with the survey model, not models.default (money trap 12, deterministic-pr-levers.md)", () => {
|
|
806
|
+
const { root, overlay } = makeRoots();
|
|
807
|
+
try {
|
|
808
|
+
// A wp-shaped overlay: surveys pinned to haiku, review to sonnet.
|
|
809
|
+
writeFileSync(join(overlay, "config.yaml"), [
|
|
810
|
+
"models:",
|
|
811
|
+
" default: anthropic/claude-sonnet-4-6",
|
|
812
|
+
" review-survey: anthropic/claude-haiku-4-5",
|
|
813
|
+
" survey-tests: openai/gpt-5.5-mini",
|
|
814
|
+
" review: anthropic/claude-sonnet-4-6",
|
|
815
|
+
].join("\n") + "\n");
|
|
816
|
+
const arm = configArm(root, overlay);
|
|
817
|
+
const phases = [
|
|
818
|
+
{
|
|
819
|
+
name: "survey",
|
|
820
|
+
model: "{{models.review-survey}}",
|
|
821
|
+
branches: [{ name: "contract" }, { name: "tests", model: "{{models.survey-tests}}" }],
|
|
822
|
+
},
|
|
823
|
+
{ name: "adjudicate", model: "{{models.review-survey}}" },
|
|
824
|
+
];
|
|
825
|
+
// The exact recording path run-instance.ts takes for a wf.phases row:
|
|
826
|
+
// ledger label → modelTemplateForRow → arm.recordPhaseModel.
|
|
827
|
+
const record = (label) => {
|
|
828
|
+
const { template, fallbackPhase } = modelTemplateForRow(phases, label);
|
|
829
|
+
return arm.recordPhaseModel(template, label, fallbackPhase);
|
|
830
|
+
};
|
|
831
|
+
// Branch rows inherit the parent's template — the measured bug recorded
|
|
832
|
+
// these as models.default (sonnet) while the sessions ran haiku.
|
|
833
|
+
expect(record("survey_branch_contract")).toBe("anthropic/claude-haiku-4-5");
|
|
834
|
+
expect(record("survey_branch_contract_retry")).toBe("anthropic/claude-haiku-4-5");
|
|
835
|
+
// A branch-level `model:` override wins over the parent's (forward-looking).
|
|
836
|
+
expect(record("survey_branch_tests")).toBe("openai/gpt-5.5-mini");
|
|
837
|
+
// The parent row itself is unchanged.
|
|
838
|
+
expect(record("survey")).toBe("anthropic/claude-haiku-4-5");
|
|
839
|
+
// A generic-loop iteration re-runs ITS OWN phase — `adjudicate_iter_1`
|
|
840
|
+
// is the adjudicate phase, template and all. The measured bug (say-side
|
|
841
|
+
// ladder, 2026-08-25): the row stamped models.default while the session
|
|
842
|
+
// envelope ran the template's answer.
|
|
843
|
+
expect(record("adjudicate_iter_1")).toBe("anthropic/claude-haiku-4-5");
|
|
844
|
+
expect(record("adjudicate_iter_1_check")).toBe("anthropic/claude-haiku-4-5");
|
|
845
|
+
}
|
|
846
|
+
finally {
|
|
847
|
+
rmSync(root, { recursive: true, force: true });
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
it("records the adjudicate {{#if}} fallback pair as a real model id, never template residue", () => {
|
|
851
|
+
const { root, overlay } = makeRoots();
|
|
852
|
+
try {
|
|
853
|
+
// The literal template from apps/server/workflows/pr-review.yaml.
|
|
854
|
+
const tpl = "{{#if models.review-adjudicate}}{{models.review-adjudicate}}{{/if}}" +
|
|
855
|
+
"{{#if !models.review-adjudicate}}{{models.review}}{{/if}}";
|
|
856
|
+
// review-adjudicate UNSET → falls back to models.review.
|
|
857
|
+
writeFileSync(join(overlay, "config.yaml"), "models:\n default: openai/gpt-5.4-mini\n review: anthropic/claude-sonnet-4-6\n");
|
|
858
|
+
expect(configArm(root, overlay).recordPhaseModel(tpl, "adjudicate")).toBe("anthropic/claude-sonnet-4-6");
|
|
859
|
+
// review-adjudicate SET → the pinned value.
|
|
860
|
+
writeFileSync(join(overlay, "config.yaml"), "models:\n default: openai/gpt-5.4-mini\n review: anthropic/claude-sonnet-4-6\n review-adjudicate: openai/gpt-5.5\n");
|
|
861
|
+
expect(configArm(root, overlay).recordPhaseModel(tpl, "adjudicate")).toBe("openai/gpt-5.5");
|
|
862
|
+
}
|
|
863
|
+
finally {
|
|
864
|
+
rmSync(root, { recursive: true, force: true });
|
|
865
|
+
}
|
|
866
|
+
});
|
|
716
867
|
});
|
|
717
868
|
describe("overlay guard — the process-global asset root (ADR 0001)", () => {
|
|
718
869
|
it("throws when a second, different overlay activates while one is in use; release clears it", () => {
|
|
@@ -974,8 +1125,8 @@ describe("PR context — core's own projection, not a copy", () => {
|
|
|
974
1125
|
body: "Bumps tiny-case.",
|
|
975
1126
|
branch: "dependabot/npm_and_yarn/tiny-case-3",
|
|
976
1127
|
};
|
|
977
|
-
it("projects the CI evidence the fix prompts render", () => {
|
|
978
|
-
const ctx = prContextPatch({
|
|
1128
|
+
it("projects the CI evidence the fix prompts render", async () => {
|
|
1129
|
+
const ctx = await prContextPatch({
|
|
979
1130
|
...args,
|
|
980
1131
|
seed: {
|
|
981
1132
|
checks_state: "failing",
|
|
@@ -1001,8 +1152,8 @@ describe("PR context — core's own projection, not a copy", () => {
|
|
|
1001
1152
|
expect(String(ctx.verifyScript)).toContain(".git/");
|
|
1002
1153
|
expect(String(ctx.notesFile)).toContain(".git/");
|
|
1003
1154
|
});
|
|
1004
|
-
it("carries the flaky promotion the third attempt turns on", () => {
|
|
1005
|
-
const ctx = prContextPatch({
|
|
1155
|
+
it("carries the flaky promotion the third attempt turns on", async () => {
|
|
1156
|
+
const ctx = await prContextPatch({
|
|
1006
1157
|
...args,
|
|
1007
1158
|
seed: { attempt: 3, flaky_deferrals: 2, checks_state: "failing" },
|
|
1008
1159
|
});
|
|
@@ -1012,32 +1163,171 @@ describe("PR context — core's own projection, not a copy", () => {
|
|
|
1012
1163
|
expect(ctx.flakyDeferrals).toBe(2);
|
|
1013
1164
|
expect(ctx.maxFlakyDeferrals).toBe(defaultFixConfig().maxFlakyDeferrals);
|
|
1014
1165
|
});
|
|
1015
|
-
it("projects the merge gate's verdict AND its reason, from one decision", () => {
|
|
1016
|
-
const green = prContextPatch({
|
|
1166
|
+
it("projects the merge gate's verdict AND its reason, from one decision", async () => {
|
|
1167
|
+
const green = await prContextPatch({
|
|
1017
1168
|
...args,
|
|
1018
1169
|
seed: { checks_state: "passing", settled_check_count: 3 },
|
|
1019
1170
|
});
|
|
1020
1171
|
expect(green.mayMerge).toBe(true);
|
|
1021
1172
|
expect(typeof green.mayMergeReason).toBe("string");
|
|
1022
|
-
const pending = prContextPatch({ ...args, seed: { checks_state: "pending" } });
|
|
1173
|
+
const pending = await prContextPatch({ ...args, seed: { checks_state: "pending" } });
|
|
1023
1174
|
expect(pending.mayMerge).toBe(false);
|
|
1024
1175
|
// The reason is produced BY the decision — the panel, the log line and the
|
|
1025
1176
|
// prompt are three renderings of it, never three prose variants.
|
|
1026
1177
|
expect(String(pending.mayMergeReason).length).toBeGreaterThan(0);
|
|
1027
1178
|
});
|
|
1028
|
-
it("seeds the policy blocks a prompt renders, under the case's overrides", () => {
|
|
1029
|
-
const ctx = prContextPatch({ ...args, seed: { dependencies: { autoMergeMaxImpact: "low" } } });
|
|
1179
|
+
it("seeds the policy blocks a prompt renders, under the case's overrides", async () => {
|
|
1180
|
+
const ctx = await prContextPatch({ ...args, seed: { dependencies: { autoMergeMaxImpact: "low" } } });
|
|
1030
1181
|
expect(ctx.dependencies.autoMergeMaxImpact).toBe("low");
|
|
1031
1182
|
// Everything the case did NOT override is the shipped default, so a case
|
|
1032
1183
|
// states only what it is actually testing.
|
|
1033
1184
|
expect(ctx.fix.maxAttempts).toBe(defaultFixConfig().maxAttempts);
|
|
1034
1185
|
});
|
|
1035
|
-
it("a case with no seed still gets a coherent snapshot", () => {
|
|
1036
|
-
const ctx = prContextPatch(args);
|
|
1186
|
+
it("a case with no seed still gets a coherent snapshot", async () => {
|
|
1187
|
+
const ctx = await prContextPatch(args);
|
|
1037
1188
|
expect(ctx.attempt).toBe(1);
|
|
1038
1189
|
expect(ctx.ciSection).toBe("");
|
|
1039
1190
|
expect(ctx.prNumber).toBe(412);
|
|
1040
1191
|
});
|
|
1192
|
+
// The review evidence pipeline's ONE switch. `analysisEnabled` is what every
|
|
1193
|
+
// WP3 phase in `pr-review.yaml` gates on (`skip_if: "analysisEnabled != true"`),
|
|
1194
|
+
// and it comes from the ARM's overlay `config.yaml`, never from gold — so this
|
|
1195
|
+
// pair of assertions IS the two-arm comparison in miniature.
|
|
1196
|
+
it("the arm's review policy switches the evidence pipeline on, and its absence leaves it off", async () => {
|
|
1197
|
+
// baseline/config.yaml: a `review:` block with no `analysis` — the pipeline
|
|
1198
|
+
// stays off, with no per-case special-casing.
|
|
1199
|
+
const off = await prContextPatch({ ...args, review: { postsCheck: true } });
|
|
1200
|
+
expect(off.analysisEnabled).toBeUndefined();
|
|
1201
|
+
expect(off.prBody).toBeUndefined();
|
|
1202
|
+
// wp3/config.yaml: the same block plus `analysis.enabled`.
|
|
1203
|
+
const on = await prContextPatch({
|
|
1204
|
+
...args,
|
|
1205
|
+
review: { postsCheck: true, analysis: { enabled: true, maxObligations: 40, surveyPasses: 6 } },
|
|
1206
|
+
});
|
|
1207
|
+
expect(on.analysisEnabled).toBe("true");
|
|
1208
|
+
expect(on.prBody).toBe(args.body);
|
|
1209
|
+
// No arm policy at all is byte-identical to a policy that names no analysis.
|
|
1210
|
+
expect((await prContextPatch(args)).analysisEnabled).toBeUndefined();
|
|
1211
|
+
});
|
|
1212
|
+
// The `spec` family's SECOND END. It went inert twice: once when `pr-review`
|
|
1213
|
+
// was excluded from `prContextPatch` entirely, and again after that exclusion
|
|
1214
|
+
// was lifted — because production reads the changed-file list from
|
|
1215
|
+
// `listPullRequestFilePaths` in `resolveSpecContext`, which the eval never
|
|
1216
|
+
// calls. `buildSpecObligations` then correctly refuses to emit a one-ended
|
|
1217
|
+
// seed (IRIS: a half mechanism measured WORSE than no seed), so all six
|
|
1218
|
+
// obligations vanish and the branch spends a model call saying it cannot work.
|
|
1219
|
+
//
|
|
1220
|
+
// Both failures were silent in the sense that mattered: the run went green.
|
|
1221
|
+
const withAnalysis = { enabled: true, maxObligations: 40, surveyPasses: 6 };
|
|
1222
|
+
const specBody = "### What & why\n\nCloses #1586.\n\n### Acceptance criteria\n\n- [ ] Silent login must not show the Google popup on a returning session\n";
|
|
1223
|
+
it("harness-derived changed files give the spec axis its second end", async () => {
|
|
1224
|
+
const ctx = await prContextPatch({
|
|
1225
|
+
...args,
|
|
1226
|
+
body: specBody,
|
|
1227
|
+
review: { analysis: withAnalysis },
|
|
1228
|
+
changedFiles: ["src/auth/redirect-sign-in.ts", "src/auth/session.ts"],
|
|
1229
|
+
});
|
|
1230
|
+
const block = ctx.specObligations;
|
|
1231
|
+
expect(block).toBeDefined();
|
|
1232
|
+
// The obligation names both ends: the criterion verbatim, and a changed file.
|
|
1233
|
+
expect(block).toContain("Silent login must not show the Google popup");
|
|
1234
|
+
expect(block).toContain("src/auth/redirect-sign-in.ts");
|
|
1235
|
+
expect(block).not.toContain("changed-file list could not be read");
|
|
1236
|
+
});
|
|
1237
|
+
it("without them the axis degrades LOUDLY rather than silently passing", async () => {
|
|
1238
|
+
const ctx = await prContextPatch({ ...args, body: specBody, review: { analysis: withAnalysis } });
|
|
1239
|
+
// Locked decision 6: "could not look" and "looked and it is fine" are
|
|
1240
|
+
// different facts, so the block still renders and says which.
|
|
1241
|
+
expect(ctx.specObligations).toContain("changed-file list could not be read");
|
|
1242
|
+
expect(ctx.specObligations).toContain("That is NOT a pass");
|
|
1243
|
+
});
|
|
1244
|
+
it("a case seeding [] means 'changes nothing', not 'could not read'", async () => {
|
|
1245
|
+
const ctx = await prContextPatch({
|
|
1246
|
+
...args,
|
|
1247
|
+
body: specBody,
|
|
1248
|
+
review: { analysis: withAnalysis },
|
|
1249
|
+
seed: { changed_files: [] },
|
|
1250
|
+
changedFiles: ["src/auth/session.ts"],
|
|
1251
|
+
});
|
|
1252
|
+
// The seed wins over the harness-derived set, and its degraded message is
|
|
1253
|
+
// the other one — a `||` fallback here would silently swap the two.
|
|
1254
|
+
expect(ctx.specObligations).toContain("changes no files");
|
|
1255
|
+
expect(ctx.specObligations).not.toContain("src/auth/session.ts");
|
|
1256
|
+
});
|
|
1257
|
+
it("the arm wins over a case's own review seed — gold can never flip an arm", async () => {
|
|
1258
|
+
const ctx = await prContextPatch({
|
|
1259
|
+
...args,
|
|
1260
|
+
seed: { review: { analysis: { enabled: true } } },
|
|
1261
|
+
review: { analysis: { enabled: false } },
|
|
1262
|
+
});
|
|
1263
|
+
expect(ctx.analysisEnabled).toBeUndefined();
|
|
1264
|
+
});
|
|
1265
|
+
// BOTH ends, from the path production uses. The two above cover the
|
|
1266
|
+
// harness-derived fallback; this one covers what actually ships: a real
|
|
1267
|
+
// `GitHubClient` pointed at the fake, with core's own `resolveSpecContext`
|
|
1268
|
+
// doing the GraphQL + REST reads. Each end went missing separately, so the
|
|
1269
|
+
// assertion is deliberately on the CONJUNCTION — a criterion traceable to the
|
|
1270
|
+
// linked ISSUE (not merely the PR body) and a changed file, in one block.
|
|
1271
|
+
it("routes both ends through core's resolver against the fake (the production path)", async () => {
|
|
1272
|
+
const fake = await startFakeGitHub({
|
|
1273
|
+
owner: "acme",
|
|
1274
|
+
repo: "widgets",
|
|
1275
|
+
issues: [
|
|
1276
|
+
{
|
|
1277
|
+
number: 1586,
|
|
1278
|
+
title: "Authentication standardisation: silent sign-in",
|
|
1279
|
+
// The criterion lives ONLY here, never in the PR body — so a run that
|
|
1280
|
+
// read the body alone cannot produce it, and this test would fail.
|
|
1281
|
+
body: "Acceptance criteria\n\n- [ ] Backend enforces the nearform.com domain server-side (non-nearform accounts get 403)\n",
|
|
1282
|
+
},
|
|
1283
|
+
],
|
|
1284
|
+
pulls: [
|
|
1285
|
+
{
|
|
1286
|
+
number: 412,
|
|
1287
|
+
title: "feat: silent sign-in",
|
|
1288
|
+
body: "### What & why\n\nCloses #1586. Adds the silent transport.\n",
|
|
1289
|
+
base_ref: "main",
|
|
1290
|
+
head_ref: "feat/sso",
|
|
1291
|
+
base_commit: "a".repeat(40),
|
|
1292
|
+
head_commit: "b".repeat(40),
|
|
1293
|
+
},
|
|
1294
|
+
],
|
|
1295
|
+
});
|
|
1296
|
+
try {
|
|
1297
|
+
fake.setPullFiles(412, [
|
|
1298
|
+
{ filename: "src/auth/session.ts", status: "modified", additions: 12, deletions: 2, changes: 14, sha: "c".repeat(40) },
|
|
1299
|
+
]);
|
|
1300
|
+
const ctx = await prContextPatch({
|
|
1301
|
+
repo: "acme/widgets",
|
|
1302
|
+
prNumber: 412,
|
|
1303
|
+
title: "feat: silent sign-in",
|
|
1304
|
+
body: "### What & why\n\nCloses #1586. Adds the silent transport.\n",
|
|
1305
|
+
branch: "feat/sso",
|
|
1306
|
+
review: { analysis: withAnalysis },
|
|
1307
|
+
github: resolveReviewGitHubClient({ githubApiBaseUrl: fake.url }),
|
|
1308
|
+
});
|
|
1309
|
+
const block = ctx.specObligations;
|
|
1310
|
+
expect(block).toBeDefined();
|
|
1311
|
+
expect(block).toContain("nearform.com domain server-side");
|
|
1312
|
+
expect(block).toContain("src/auth/session.ts");
|
|
1313
|
+
expect(block).not.toContain("changed-file list could not be read");
|
|
1314
|
+
}
|
|
1315
|
+
finally {
|
|
1316
|
+
await fake.close();
|
|
1317
|
+
}
|
|
1318
|
+
});
|
|
1319
|
+
// GitHub's own boundary: a closing keyword links, a bare reference does not.
|
|
1320
|
+
// Getting this wrong would seed the axis with criteria from an issue the PR
|
|
1321
|
+
// never promised to satisfy — a confident obligation about the wrong "what
|
|
1322
|
+
// was asked", which is worse than no obligation.
|
|
1323
|
+
it("resolves closing keywords only, not bare references", () => {
|
|
1324
|
+
expect(closingIssueNumbers("Closes #12")).toEqual([12]);
|
|
1325
|
+
expect(closingIssueNumbers("fixes #7 and resolved #8")).toEqual([7, 8]);
|
|
1326
|
+
expect(closingIssueNumbers("Closes https://github.com/o/r/issues/99")).toEqual([99]);
|
|
1327
|
+
expect(closingIssueNumbers("Closes #5. Also closes #5.")).toEqual([5]);
|
|
1328
|
+
expect(closingIssueNumbers("See #12, part of #13, related to #14")).toEqual([]);
|
|
1329
|
+
expect(closingIssueNumbers("no links here")).toEqual([]);
|
|
1330
|
+
});
|
|
1041
1331
|
});
|
|
1042
1332
|
// ── Marker grading (fix / dependency-merge) ─────────────────────────────────
|
|
1043
1333
|
describe("gradeMarkers", () => {
|