@skyramp/mcp 0.3.6 → 0.3.7
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/build/prompts/code-reuse.js +17 -2
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +1 -1
- package/build/prompts/modularization/ui-test-modularization.js +9 -6
- package/build/prompts/pom-aware-code-reuse.js +1 -1
- package/build/prompts/shared-helper-policy.js +5 -5
- package/build/services/TestGenerationService.js +15 -1
- package/build/tools/code-refactor/assertion-state.d.ts +91 -0
- package/build/tools/code-refactor/assertion-state.js +375 -0
- package/build/tools/code-refactor/codeReuseTool.js +6 -4
- package/build/tools/code-refactor/enhanceAssertionsTool.js +73 -18
- package/build/tools/code-refactor/retrofit-state.d.ts +53 -0
- package/build/tools/code-refactor/retrofit-state.js +162 -0
- package/build/tools/code-refactor/reuse-outcome.d.ts +7 -0
- package/build/tools/code-refactor/reuse-state.d.ts +9 -0
- package/build/tools/code-refactor/reuse-state.js +42 -4
- package/build/tools/code-refactor/utils-verify-gates.js +69 -15
- package/build/tools/executeSkyrampTestTool.js +19 -14
- package/build/tools/submitReportTool.js +81 -8
- package/build/types/AssertionOutcome.d.ts +68 -0
- package/build/types/AssertionOutcome.js +1 -0
- package/build/types/ReuseOutcome.d.ts +16 -0
- package/build/types/TestTypes.d.ts +4 -0
- package/build/types/TestTypes.js +8 -0
- package/build/types/TestbotReport.d.ts +13 -0
- package/build/types/index.d.ts +1 -1
- package/build/utils/AnalysisStateManager.d.ts +20 -7
- package/build/utils/assertion-verify/api-shared-lints.d.ts +5 -0
- package/build/utils/assertion-verify/api-shared-lints.js +315 -0
- package/build/utils/assertion-verify/contract-lints.d.ts +3 -0
- package/build/utils/assertion-verify/contract-lints.js +87 -0
- package/build/utils/assertion-verify/format.d.ts +5 -0
- package/build/utils/assertion-verify/format.js +65 -0
- package/build/utils/assertion-verify/helper-imports.d.ts +6 -0
- package/build/utils/assertion-verify/helper-imports.js +178 -0
- package/build/utils/assertion-verify/index.d.ts +3 -0
- package/build/utils/assertion-verify/index.js +7 -0
- package/build/utils/assertion-verify/integration-lints.d.ts +3 -0
- package/build/utils/assertion-verify/integration-lints.js +36 -0
- package/build/utils/assertion-verify/js-regex-blank.d.ts +1 -0
- package/build/utils/assertion-verify/js-regex-blank.js +153 -0
- package/build/utils/assertion-verify/lint-types.d.ts +33 -0
- package/build/utils/assertion-verify/lint-types.js +57 -0
- package/build/utils/assertion-verify/marker.d.ts +27 -0
- package/build/utils/assertion-verify/marker.js +61 -0
- package/build/utils/assertion-verify/metrics.d.ts +30 -0
- package/build/utils/assertion-verify/metrics.js +341 -0
- package/build/utils/assertion-verify/python-strip.d.ts +6 -0
- package/build/utils/assertion-verify/python-strip.js +75 -0
- package/build/utils/assertion-verify/strip-dispatch.d.ts +19 -0
- package/build/utils/assertion-verify/strip-dispatch.js +42 -0
- package/build/utils/assertion-verify/ui-lints.d.ts +8 -0
- package/build/utils/assertion-verify/ui-lints.js +244 -0
- package/build/utils/assertion-verify/verify.d.ts +61 -0
- package/build/utils/assertion-verify/verify.js +215 -0
- package/build/utils/featureFlags.d.ts +12 -2
- package/build/utils/featureFlags.js +33 -3
- package/build/utils/reportVerification.d.ts +4 -0
- package/build/utils/reportVerification.js +32 -4
- package/build/utils/utils-verify/allow.d.ts +22 -4
- package/build/utils/utils-verify/allow.js +8 -2
- package/build/utils/utils-verify/call-sites.d.ts +40 -1
- package/build/utils/utils-verify/call-sites.js +196 -30
- package/build/utils/utils-verify/importers.d.ts +31 -0
- package/build/utils/utils-verify/importers.js +78 -0
- package/build/utils/utils-verify/index.d.ts +1 -0
- package/build/utils/utils-verify/index.js +1 -0
- package/build/utils/utils-verify/language-spec.d.ts +13 -2
- package/build/utils/utils-verify/language-spec.js +12 -2
- package/build/utils/utils-verify/parse.d.ts +31 -3
- package/build/utils/utils-verify/parse.js +190 -9
- package/build/utils/utils-verify/retrofit-equivalence.d.ts +43 -0
- package/build/utils/utils-verify/retrofit-equivalence.js +218 -0
- package/build/utils/utils-verify/stage.d.ts +6 -0
- package/build/utils/utils-verify/stage.js +12 -2
- package/build/utils/utils-verify/verify.d.ts +54 -4
- package/build/utils/utils-verify/verify.js +224 -12
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-CZfp96qZ.js → codeMirrorModule-LNgEKtdV.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-gpLo02E0.js → defaultSettingsView-Bwr1eMKC.js} +135 -135
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.Bq1r1URj.js → index.-Id052Lr.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.VEfqi1qN.js → uiMode.BPopbasy.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +2 -2
- package/build/tools/code-refactor/enhance-state.d.ts +0 -49
- package/build/tools/code-refactor/enhance-state.js +0 -109
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { readFile, stat } from "fs/promises";
|
|
3
|
+
import { logger } from "../../utils/logger.js";
|
|
4
|
+
import { resolveRunStatePath, StateManager, } from "../../utils/AnalysisStateManager.js";
|
|
5
|
+
import { stageUtilsArtifacts, } from "../../utils/utils-verify/index.js";
|
|
6
|
+
import { importFormViolations, locateUtilsFiles, retrofitMismatches, utilsSpecForFile, } from "../../utils/utils-verify/index.js";
|
|
7
|
+
import { realpath } from "../../utils/utils-verify/index.js";
|
|
8
|
+
import { canonPath, samePath, utilsFileLabel } from "./reuse-state.js";
|
|
9
|
+
/**
|
|
10
|
+
* Retrofit tracking (SKYR-4276 A4): a RETROFIT is a pre-existing Skyramp-generated
|
|
11
|
+
* test the reuse pass edited to import from the shared utils file (STEP 5c / the
|
|
12
|
+
* sibling-inline advisory). Staging carries the edit into the output commit, so the
|
|
13
|
+
* report must disclose it — with an execution that ran the edited bytes.
|
|
14
|
+
*
|
|
15
|
+
* Whether an execution counts is answered by TIME, not by a stored hash: the
|
|
16
|
+
* execution record already carries `executedAt`, and the file's mtime says when it
|
|
17
|
+
* was last edited — an execution older than the last edit ran bytes that no longer
|
|
18
|
+
* exist. That also covers a bare editor write with no tool call in between, which a
|
|
19
|
+
* record-time hash cannot see. (Same identity pattern as VideoRecord's
|
|
20
|
+
* status/executedAt.)
|
|
21
|
+
*/
|
|
22
|
+
/** Stage the utils artifacts for `testFile` and record any retrofits staging found.
|
|
23
|
+
* The one call every checkpoint makes (reuse verify, enhance, execute). */
|
|
24
|
+
export async function stageAndRecordRetrofits(testFile, language, explicitStateFile) {
|
|
25
|
+
const staging = await stageUtilsArtifacts(testFile, language);
|
|
26
|
+
await recordReuseRetrofits(staging.retrofits, staging.utilsFiles, testFile, explicitStateFile);
|
|
27
|
+
return staging;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Record the pre-existing generated tests the reuse pass edited (found by staging).
|
|
31
|
+
* Idempotent; a retrofit already recorded keeps its first attribution. Fails open
|
|
32
|
+
* like every other reuse-state write.
|
|
33
|
+
*/
|
|
34
|
+
export async function recordReuseRetrofits(retrofits, utilsFiles, testFile, explicitStateFile) {
|
|
35
|
+
if (retrofits.length === 0)
|
|
36
|
+
return;
|
|
37
|
+
const stateFile = resolveRunStatePath(explicitStateFile);
|
|
38
|
+
if (!stateFile)
|
|
39
|
+
return;
|
|
40
|
+
try {
|
|
41
|
+
const manager = StateManager.fromStatePath(stateFile);
|
|
42
|
+
const data = await manager.readData();
|
|
43
|
+
if (!data)
|
|
44
|
+
return;
|
|
45
|
+
const utilsFile = utilsFileLabel(utilsFiles);
|
|
46
|
+
const entries = { ...data.reuseRetrofits };
|
|
47
|
+
for (const f of retrofits) {
|
|
48
|
+
const key = await realpath(f);
|
|
49
|
+
if (!entries[key])
|
|
50
|
+
entries[key] = { utilsFile, testFile: await realpath(testFile) };
|
|
51
|
+
}
|
|
52
|
+
await manager.writeData({ ...data, reuseRetrofits: entries });
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
logger.warning("Could not record the reuse retrofits — the report will not be checked for them", { retrofits, error: String(err) });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The retrofits that still stand in the working tree, with the execution that ran
|
|
60
|
+
* their current bytes. `changedFilesAbs` is ABSOLUTE paths across every checkout (see
|
|
61
|
+
* listChangedFilesAbs) — a retrofit the agent reverted is not among them and drops
|
|
62
|
+
* out. An execution older than the file's mtime ran bytes that no longer exist and
|
|
63
|
+
* does not count; an unreadable mtime fails open (the execution counts), matching
|
|
64
|
+
* the surrounding SKYR-3883 infrastructure-failure semantics.
|
|
65
|
+
*/
|
|
66
|
+
export async function standingRetrofits(state, changedFilesAbs) {
|
|
67
|
+
const changed = new Set(changedFilesAbs.map((f) => canonPath(f)));
|
|
68
|
+
const out = [];
|
|
69
|
+
for (const [file, r] of Object.entries(state.reuseRetrofits ?? {})) {
|
|
70
|
+
if (!changed.has(canonPath(file)))
|
|
71
|
+
continue;
|
|
72
|
+
const recorded = state.existingTests?.find((t) => samePath(t.testFile, file))?.executionAfter;
|
|
73
|
+
let execution = recorded?.status;
|
|
74
|
+
if (recorded) {
|
|
75
|
+
try {
|
|
76
|
+
const mtime = (await stat(file)).mtimeMs;
|
|
77
|
+
if (Date.parse(recorded.executedAt) < mtime)
|
|
78
|
+
execution = undefined;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
/* fail open: an unreadable mtime must not demand a re-run */
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
out.push({ file, utilsFile: r.utilsFile, testFile: r.testFile, execution });
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The report gate over the standing retrofits: every one needs an execution of its
|
|
90
|
+
* current bytes, or the report is refused. Returns the views (for the report rows)
|
|
91
|
+
* and, when any are unexecuted, the refusal text.
|
|
92
|
+
*/
|
|
93
|
+
export async function retrofitGate(state, changedFilesAbs, repoRoot) {
|
|
94
|
+
const views = await standingRetrofits(state, changedFilesAbs);
|
|
95
|
+
// A retrofit that changed the request is refused whether or not it ran: an
|
|
96
|
+
// execution proves the edited file was exercised, not that it still exercises the
|
|
97
|
+
// same thing. Checked here rather than where the edit happens because the agent
|
|
98
|
+
// performs the substitution itself (STEP 5c) — this is the deterministic check
|
|
99
|
+
// over what it actually did.
|
|
100
|
+
const checked = (await Promise.all(views.map(async (v) => {
|
|
101
|
+
const spec = utilsSpecForFile(v.file);
|
|
102
|
+
if (!spec)
|
|
103
|
+
return { mismatches: [], imports: [] };
|
|
104
|
+
try {
|
|
105
|
+
// The rewired file's own imports resolve the utils file(s) it now uses —
|
|
106
|
+
// RetrofitView carries only a basename label for the report row.
|
|
107
|
+
const utilsFiles = await locateUtilsFiles({
|
|
108
|
+
testFile: v.file,
|
|
109
|
+
language: spec.language,
|
|
110
|
+
// The root the report is written against — without it every retrofit
|
|
111
|
+
// re-detects the git root.
|
|
112
|
+
cwd: repoRoot,
|
|
113
|
+
});
|
|
114
|
+
if (utilsFiles.length === 0)
|
|
115
|
+
return { mismatches: [], imports: [] };
|
|
116
|
+
// STEP 5c adds an import to the SIBLING, and siblings share the spec's
|
|
117
|
+
// directory — so if that directory is a package, the sibling's new import
|
|
118
|
+
// needs the same fallback pair the spec's does. verifyUtils only ever sees
|
|
119
|
+
// the spec, so without this the gate covered one of the two files it should.
|
|
120
|
+
const imports = spec.language === "python"
|
|
121
|
+
? importFormViolations(v.file, await readFile(v.file, "utf8"), utilsFiles)
|
|
122
|
+
: [];
|
|
123
|
+
return {
|
|
124
|
+
mismatches: await retrofitMismatches(v.file, utilsFiles, spec),
|
|
125
|
+
imports,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return { mismatches: [], imports: [] }; // never fail a report on the check
|
|
130
|
+
}
|
|
131
|
+
}))).flat();
|
|
132
|
+
const mismatches = checked.flatMap((c) => c.mismatches);
|
|
133
|
+
const badImports = checked.flatMap((c) => c.imports);
|
|
134
|
+
if (badImports.length > 0) {
|
|
135
|
+
const refusal = `${badImports.length} pre-existing generated test(s) were rewired onto the shared helper with an import that will NOT resolve when the customer runs pytest — the file dies at collection.\n` +
|
|
136
|
+
badImports
|
|
137
|
+
.map((v) => ` - ${path.relative(repoRoot, v.file)}:${v.line} · ${v.detail}`)
|
|
138
|
+
.join("\n");
|
|
139
|
+
return { views, refusal };
|
|
140
|
+
}
|
|
141
|
+
if (mismatches.length > 0) {
|
|
142
|
+
const refusal = `${mismatches.length} pre-existing generated test(s) were rewired onto a shared helper that does NOT send the same request they sent before — ` +
|
|
143
|
+
`the edit changed what the customer's test exercises, which this flow must never do.\n` +
|
|
144
|
+
`For each one: either restore that file's own request block, or extend the helper with a parameter DEFAULTED to the value the helper already uses, so every existing call site keeps its behaviour and this file passes its own values.\n` +
|
|
145
|
+
mismatches
|
|
146
|
+
.map((m) => ` - ${path.relative(repoRoot, m.file)} · ${m.method} ${m.path} → ${m.helper}: ${m.detail}`)
|
|
147
|
+
.join("\n");
|
|
148
|
+
return { views, refusal };
|
|
149
|
+
}
|
|
150
|
+
const unexecuted = views.filter((v) => !v.execution);
|
|
151
|
+
if (unexecuted.length === 0)
|
|
152
|
+
return { views };
|
|
153
|
+
const refusal = `${unexecuted.length} pre-existing generated test(s) were edited by the code-reuse pass to import from a shared utils file, but have no recorded execution of the edited file — ` +
|
|
154
|
+
`Testbot will not report a rewired test it has not run.\n` +
|
|
155
|
+
`Run each file below with skyramp_execute_test (it is a pre-existing test, so pass its path and phase "after"); that records the execution and clears this.\n` +
|
|
156
|
+
`Do NOT remove imports from any test, inline helpers back, or restore files from HEAD to satisfy this check — un-sharing the helpers is the tamper path, not the fix.\n` +
|
|
157
|
+
`Only if the edit was unintended, restore that ONE file from HEAD and re-verify the tests that import the utils file:\n` +
|
|
158
|
+
unexecuted
|
|
159
|
+
.map((v) => ` - ${path.relative(repoRoot, v.file)} (now imports from ${v.utilsFile})`)
|
|
160
|
+
.join("\n");
|
|
161
|
+
return { views, refusal };
|
|
162
|
+
}
|
|
@@ -60,6 +60,13 @@ export interface ReuseRecord {
|
|
|
60
60
|
* enforceable unit, as with every other debt here). Read by the reuse tool on a
|
|
61
61
|
* hand-off target: modularize-first flows owe modularization BEFORE reuse. */
|
|
62
62
|
modularized?: true;
|
|
63
|
+
/** The spec as it stood when `skyramp_modularization` handed out its instructions —
|
|
64
|
+
* after the enhancer, before any code moved (SKYR-4276 A7/A8). The verify pass
|
|
65
|
+
* compares the delivered spec plus the helpers it imports against this. */
|
|
66
|
+
baseline?: {
|
|
67
|
+
assertions: number;
|
|
68
|
+
pageErrorGuard: boolean;
|
|
69
|
+
};
|
|
63
70
|
}
|
|
64
71
|
export interface UtilsReuseRecord {
|
|
65
72
|
/** True once the utils path handed out reuse instructions; a bare verify without a
|
|
@@ -76,12 +76,21 @@ export declare function recordUtilsReuseIssued(testFile: string, language: strin
|
|
|
76
76
|
/** A verify pass that threw. Recorded so the debt FAILS OPEN — a verifier that cannot
|
|
77
77
|
* run must not leave a debt nothing can pay — and cleared by the next successful pass. */
|
|
78
78
|
export declare function recordUtilsVerifyError(testFile: string, explicitStateFile?: string): Promise<void>;
|
|
79
|
+
/** Canonical comparison: a record written through a symlinked path (macOS /var →
|
|
80
|
+
* /private/var) must still be recognised as this spec's when checked by realpath. */
|
|
81
|
+
export declare function samePath(a: string, b: string): boolean;
|
|
82
|
+
/** Exported for retrofit-state: the same canonicalisation the records use. */
|
|
83
|
+
export declare function canonPath(p: string): string;
|
|
84
|
+
/** The basename label a report row shows for a set of utils files. */
|
|
85
|
+
export declare function utilsFileLabel(files: string[]): string;
|
|
79
86
|
/** Verdict of one utils verify pass. The verdict alone is stored — counts and the
|
|
80
87
|
* file list are re-derived from the delivered files at report time. */
|
|
81
88
|
export declare function recordUtilsVerifyOutcome(testFile: string, r: UtilsVerifyResult, language: string, framework: string, testType: string | undefined, explicitStateFile?: string): Promise<boolean>;
|
|
82
89
|
/** `skyramp_modularization` was called for this spec. Ownership on the write, like
|
|
83
90
|
* the utils debt: a same-basename spec elsewhere does not inherit the mark. */
|
|
84
91
|
export declare function recordModularized(testFile: string, explicitStateFile?: string): Promise<void>;
|
|
92
|
+
/** The modularization-hand-out baseline for this spec, if one was recorded for it. */
|
|
93
|
+
export declare function utilsBaselineFor(testFile: string, explicitStateFile?: string): Promise<ReuseRecord["baseline"] | undefined>;
|
|
85
94
|
/**
|
|
86
95
|
* Does this spec owe `skyramp_modularization` before reuse may be issued? Only a spec
|
|
87
96
|
* generation handed off (modularize-first flows: the hand-off names both calls, in
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as fsp from "fs/promises";
|
|
1
2
|
import * as path from "path";
|
|
2
3
|
import { readFile, stat, access, constants as fsConstants } from "fs/promises";
|
|
3
4
|
import { realpathSync } from "fs";
|
|
@@ -7,7 +8,7 @@ import { logger } from "../../utils/logger.js";
|
|
|
7
8
|
import { resolveRunStatePath, StateManager, } from "../../utils/AnalysisStateManager.js";
|
|
8
9
|
import { verifyReuse, } from "../../utils/pom-verify/index.js";
|
|
9
10
|
import { buildSkipped, findUnparsableKeptInline, flaggedFrom, mergeFlagged, verificationFrom, ReuseVerificationOutcome, HelperVerificationOutcome, } from "./reuse-outcome.js";
|
|
10
|
-
import { verifyUtils, realpath, utilsSpecFor, } from "../../utils/utils-verify/index.js";
|
|
11
|
+
import { verifyUtils, realpath, utilsSpecFor, countAssertions, hasPageErrorGuard, utilsSpecForFile, } from "../../utils/utils-verify/index.js";
|
|
11
12
|
import { detectGitRoot } from "../../utils/gitStaging.js";
|
|
12
13
|
const execFileAsync = promisify(execFile);
|
|
13
14
|
import { resolvePomCatalogPath } from "../../utils/pom-catalog.js";
|
|
@@ -238,9 +239,17 @@ function owns(record, testFile) {
|
|
|
238
239
|
}
|
|
239
240
|
/** Canonical comparison: a record written through a symlinked path (macOS /var →
|
|
240
241
|
* /private/var) must still be recognised as this spec's when checked by realpath. */
|
|
241
|
-
function samePath(a, b) {
|
|
242
|
+
export function samePath(a, b) {
|
|
242
243
|
return canon(a) === canon(b);
|
|
243
244
|
}
|
|
245
|
+
/** Exported for retrofit-state: the same canonicalisation the records use. */
|
|
246
|
+
export function canonPath(p) {
|
|
247
|
+
return canon(p);
|
|
248
|
+
}
|
|
249
|
+
/** The basename label a report row shows for a set of utils files. */
|
|
250
|
+
export function utilsFileLabel(files) {
|
|
251
|
+
return files.map((f) => path.basename(f)).join(", ");
|
|
252
|
+
}
|
|
244
253
|
function canon(p) {
|
|
245
254
|
try {
|
|
246
255
|
return realpathSync(p);
|
|
@@ -297,17 +306,43 @@ async function runStateWritable(explicitStateFile) {
|
|
|
297
306
|
/** `skyramp_modularization` was called for this spec. Ownership on the write, like
|
|
298
307
|
* the utils debt: a same-basename spec elsewhere does not inherit the mark. */
|
|
299
308
|
export async function recordModularized(testFile, explicitStateFile) {
|
|
309
|
+
// The spec as handed out — after the enhancer, before any code moves. Taken once;
|
|
310
|
+
// a second modularization call must not overwrite the true "before".
|
|
311
|
+
let baseline;
|
|
312
|
+
const spec = utilsSpecForFile(testFile);
|
|
313
|
+
if (spec) {
|
|
314
|
+
try {
|
|
315
|
+
const content = await fsp.readFile(testFile, "utf8");
|
|
316
|
+
baseline = {
|
|
317
|
+
assertions: countAssertions(content, spec),
|
|
318
|
+
pageErrorGuard: hasPageErrorGuard(content, spec),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
/* fail open: no baseline, no assertion check */
|
|
323
|
+
}
|
|
324
|
+
}
|
|
300
325
|
await updateReuseOutcome(testFile, (existing) => owns(existing, testFile)
|
|
301
|
-
? {
|
|
326
|
+
? {
|
|
327
|
+
testFilePath: existing.testFilePath ?? testFile,
|
|
328
|
+
modularized: true,
|
|
329
|
+
baseline: existing.baseline ?? baseline,
|
|
330
|
+
}
|
|
302
331
|
: {
|
|
303
332
|
testFilePath: testFile,
|
|
304
333
|
modularized: true,
|
|
334
|
+
baseline,
|
|
305
335
|
utils: undefined,
|
|
306
336
|
verification: undefined,
|
|
307
337
|
candidatesDetected: undefined,
|
|
308
338
|
flagged: undefined,
|
|
309
339
|
}, explicitStateFile);
|
|
310
340
|
}
|
|
341
|
+
/** The modularization-hand-out baseline for this spec, if one was recorded for it. */
|
|
342
|
+
export async function utilsBaselineFor(testFile, explicitStateFile) {
|
|
343
|
+
const record = await readReuseRecord(testFile, explicitStateFile);
|
|
344
|
+
return record && owns(record, testFile) ? record.baseline : undefined;
|
|
345
|
+
}
|
|
311
346
|
/**
|
|
312
347
|
* Does this spec owe `skyramp_modularization` before reuse may be issued? Only a spec
|
|
313
348
|
* generation handed off (modularize-first flows: the hand-off names both calls, in
|
|
@@ -606,13 +641,16 @@ async function rederiveHelperOutcome(record) {
|
|
|
606
641
|
testFile: testFilePath,
|
|
607
642
|
language,
|
|
608
643
|
testType: record.utils?.testType,
|
|
644
|
+
baseline: record.baseline,
|
|
609
645
|
});
|
|
610
646
|
if (r.utilsFiles.length === 0)
|
|
611
647
|
return {};
|
|
648
|
+
const lost = r.violations.some((v) => v.kind === "assertion-loss" || v.kind === "guard-removed");
|
|
612
649
|
return {
|
|
613
650
|
helpers: {
|
|
614
|
-
utilsFile: r.utilsFiles
|
|
651
|
+
utilsFile: utilsFileLabel(r.utilsFiles),
|
|
615
652
|
helpersImported: r.imported,
|
|
653
|
+
...(record.baseline ? { assertionsPreserved: !lost } : {}),
|
|
616
654
|
...(r.inlineCallSites.length > 0
|
|
617
655
|
? { siblingInlineCallSites: r.inlineCallSites.length }
|
|
618
656
|
: {}),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as path from "path";
|
|
2
|
-
import { allowMarkerLine, } from "../../utils/utils-verify/index.js";
|
|
2
|
+
import { allowMarkerLine, MARKERABLE_UTILS_KINDS, HELPER_FAMILIES, } from "../../utils/utils-verify/index.js";
|
|
3
3
|
/** How many verify rounds the agent may spend before it must document what remains.
|
|
4
4
|
* SKYR-4059: an unbounded repair loop made six execution attempts against a cap of
|
|
5
5
|
* two and destroyed verified reuse in between. The bound is stated in the text the
|
|
@@ -15,9 +15,12 @@ function show(file, r) {
|
|
|
15
15
|
: file;
|
|
16
16
|
}
|
|
17
17
|
export function formatUtilsVerifyText(r, ctx = {}) {
|
|
18
|
-
const tail = verdictTail(ctx) +
|
|
18
|
+
const tail = verdictTail(ctx) +
|
|
19
|
+
unstagedTail(r, ctx) +
|
|
20
|
+
namingAdvisory(r) +
|
|
21
|
+
typingAdvisory(r);
|
|
19
22
|
const spec = r.spec;
|
|
20
|
-
if (r.utilsFiles.length === 0) {
|
|
23
|
+
if (r.utilsFiles.length === 0 && r.violations.length === 0) {
|
|
21
24
|
return ("VERIFICATION PASSED — no shared utils file is written or imported for this test; nothing to verify. " +
|
|
22
25
|
"(If you did create one, it carries no `Generated by Skyramp on …` header line, so it cannot be recognised or staged — add the header and re-verify.)" +
|
|
23
26
|
tail);
|
|
@@ -30,7 +33,8 @@ export function formatUtilsVerifyText(r, ctx = {}) {
|
|
|
30
33
|
return (`VERIFICATION PASSED — ${files}: ${r.helpers} helper${r.helpers === 1 ? "" : "s"}, ${r.imported} imported by this test.${allowed} ` +
|
|
31
34
|
`${ctx.staged && ctx.staged.length > 0 ? "The utils file is staged for the output commit. " : ""}Continue.` +
|
|
32
35
|
tail +
|
|
33
|
-
inlineCallSitesAdvisory(r)
|
|
36
|
+
inlineCallSitesAdvisory(r) +
|
|
37
|
+
singleImporterAdvisory(r));
|
|
34
38
|
}
|
|
35
39
|
const lines = [];
|
|
36
40
|
if (r.violations.length > 0) {
|
|
@@ -38,19 +42,36 @@ export function formatUtilsVerifyText(r, ctx = {}) {
|
|
|
38
42
|
for (const v of r.violations) {
|
|
39
43
|
lines.push(`- ${v.kind} · ${show(v.file, r)}:${v.line} · ${v.helper}: ${v.detail}`);
|
|
40
44
|
}
|
|
41
|
-
lines.push("",
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
lines.push("",
|
|
46
|
+
// The fix rules come from the policy objects, not restated here; the browser
|
|
47
|
+
// clause reads as a continuation of the api one for mixed files.
|
|
48
|
+
`Fix each shared-helper violation in the utils file (${r.fallbackFamily.family === "browser"
|
|
49
|
+
? `${HELPER_FAMILIES.api.fixRule}; ${HELPER_FAMILIES.browser.fixRule}`
|
|
50
|
+
: HELPER_FAMILIES.api.fixRule}), update any call site whose signature changed, then call skyramp_reuse_code again with the SAME arguments and \`verify: true\`. Re-verify after every fix pass; finish only on PASSED.`);
|
|
51
|
+
// Marker guidance only for kinds a marker can document: a guard-removed-only
|
|
52
|
+
// failure must not be told "every deliberate violation may be documented".
|
|
53
|
+
const markerable = r.violations.filter((v) => v.kind !== "guard-removed" && v.kind !== "import-form");
|
|
54
|
+
if (markerable.length > 0) {
|
|
55
|
+
lines.push(`A violation that is deliberate may be documented instead of fixed. Copy the matching line below into the utils file as a comment, replacing only the <reason…> part — nothing else satisfies the gate:`);
|
|
56
|
+
if (spec)
|
|
57
|
+
for (const v of markerable)
|
|
58
|
+
lines.push(` ${allowMarkerLine(spec, v.kind, v.helper)}`);
|
|
59
|
+
lines.push(`Do not spend more than ${UTILS_VERIFY_MAX_ROUNDS} verify rounds on this file: if violations remain after round ${UTILS_VERIFY_MAX_ROUNDS}, document each remaining one with its line above and re-verify once more.`);
|
|
45
60
|
}
|
|
46
|
-
|
|
61
|
+
if (r.violations.some((v) => v.kind === "import-form"))
|
|
62
|
+
lines.push("An `import-form` violation is in the TEST file, not the utils file: replace that import with the try/except pair shown on the line above, then re-verify. Nothing in the utils file needs to change for it.");
|
|
63
|
+
if (r.violations.some((v) => v.kind === "guard-removed"))
|
|
64
|
+
lines.push("A removed page-error guard cannot be documented away — restore the registration (in the spec, or in a shared helper the spec imports and calls) and its final expect, then re-verify.");
|
|
47
65
|
}
|
|
48
66
|
if (r.malformedAllows.length > 0) {
|
|
49
67
|
lines.push(r.violations.length > 0
|
|
50
68
|
? ""
|
|
51
|
-
: "VERIFICATION FAILED — every violation is documented, but not every marker can be read back.", "Fix these `reuse-verify: allow` lines — the report cannot read them, so the declines they document would be dropped silently:", ...r.malformedAllows.map((m) => ` ${show(m.file, r)}: ${m.line}`), `The form is \`${spec?.commentPrefix ?? "#"} reuse-verify: allow <kind> <helper> — <reason>\` with the reason separated by an em dash or a hyphen with a space on BOTH sides, the kind one of
|
|
69
|
+
: "VERIFICATION FAILED — every violation is documented, but not every marker can be read back.", "Fix these `reuse-verify: allow` lines — the report cannot read them, so the declines they document would be dropped silently:", ...r.malformedAllows.map((m) => ` ${show(m.file, r)}: ${m.line}`), `The form is \`${spec?.commentPrefix ?? "#"} reuse-verify: allow <kind> <helper> — <reason>\` with the reason separated by an em dash or a hyphen with a space on BOTH sides, the kind one of ${MARKERABLE_UTILS_KINDS.join(" / ")}, and the reason written out (not the <reason…> placeholder).`);
|
|
52
70
|
}
|
|
53
|
-
return lines.join("\n") +
|
|
71
|
+
return (lines.join("\n") +
|
|
72
|
+
tail +
|
|
73
|
+
inlineCallSitesAdvisory(r) +
|
|
74
|
+
singleImporterAdvisory(r));
|
|
54
75
|
}
|
|
55
76
|
/** SKYR-4219: the sibling call-site pass the prompt asks for and the agent skips (0/2
|
|
56
77
|
* seeded eval runs). Stated here because the verify call is the one point every run
|
|
@@ -68,9 +89,26 @@ function inlineCallSitesAdvisory(r) {
|
|
|
68
89
|
"",
|
|
69
90
|
`ADVISORY — ${n} inline request call${n === 1 ? "" : "s"} in other Skyramp-generated tests that a shared helper already wraps:`,
|
|
70
91
|
...r.inlineCallSites.map((c) => `- ${show(c.file, r)}:${c.line} · ${c.method} ${c.path} · ${utils}.${c.helper}`),
|
|
71
|
-
`Each is a candidate: the request's method, path and call shape match the helper's. Replace the block with a call to the helper only if the remaining differences are literals (they become the arguments; keep the block's response variable; import the helper into that file; change nothing else there), then re-verify. This does not block execution.`,
|
|
92
|
+
`Each is a candidate: the request's method, path and call shape match the helper's. Replace the block with a call to the helper only if the remaining differences are literals (they become the arguments; keep the block's response variable; import the helper into that file; change nothing else there), then re-verify AND run the edited file with skyramp_execute_test — an edited pre-existing test without a recorded execution cannot be reported. This does not block execution.`,
|
|
72
93
|
].join("\n");
|
|
73
94
|
}
|
|
95
|
+
/** Helpers only this test (or no test) imports. Advisory: on a first run every helper
|
|
96
|
+
* is single-use by design — it seeds the utils file for the tests that follow — so
|
|
97
|
+
* this never blocks; it names the helpers whose sharing has not (yet) happened. */
|
|
98
|
+
function singleImporterAdvisory(r) {
|
|
99
|
+
const single = r.singleImporters;
|
|
100
|
+
if (single.length === 0 || r.helpers === 0)
|
|
101
|
+
return "";
|
|
102
|
+
const dead = single.filter((h) => h.importers === 0);
|
|
103
|
+
const lines = ["", ""];
|
|
104
|
+
if (dead.length > 0)
|
|
105
|
+
lines.push(`ADVISORY — ${dead.length} helper${dead.length === 1 ? " is" : "s are"} imported by NOTHING, not even the test that wrote ${dead.length === 1 ? "it" : "them"} (does not block):`, ...dead.map((h) => `- ${show(h.file, r)}:${h.line} · ${h.helper}`), "A dead helper is always actionable: call it from the test it belongs to, or delete it.");
|
|
106
|
+
const once = single.filter((h) => h.importers === 1);
|
|
107
|
+
if (once.length === 0)
|
|
108
|
+
return lines.join("\n");
|
|
109
|
+
lines.push(`ADVISORY — ${once.length} of ${r.helpers} shared helper${r.helpers === 1 ? "" : "s"} ${once.length === 1 ? "is" : "are"} imported by exactly one test (does not block):`, ...once.map((h) => `- ${show(h.file, r)}:${h.line} · ${h.helper}`), "Other delivered tests exist and do not call these. Inline a helper back into its test only if it wraps a step specific to this scenario's narrative that no other test would perform as-is; a request wrapper or a cross-test action (sign-in, open a form, apply a filter) stays shared.");
|
|
110
|
+
return lines.join("\n");
|
|
111
|
+
}
|
|
74
112
|
function unstagedTail(r, ctx) {
|
|
75
113
|
if (!ctx.unstaged || ctx.unstaged.length === 0)
|
|
76
114
|
return "";
|
|
@@ -81,17 +119,33 @@ function unstagedTail(r, ctx) {
|
|
|
81
119
|
/** Naming is advised, never enforced: a heuristic that blocked execution on
|
|
82
120
|
* `create_test_case` or `list_expired_subscriptions` would be the worse outcome. */
|
|
83
121
|
function namingAdvisory(r) {
|
|
84
|
-
|
|
122
|
+
const names = r.advisories.filter((v) => v.kind === "scenario-name");
|
|
123
|
+
if (names.length === 0)
|
|
85
124
|
return "";
|
|
86
|
-
const n =
|
|
125
|
+
const n = names.length;
|
|
87
126
|
return [
|
|
88
127
|
"",
|
|
89
128
|
"",
|
|
90
129
|
`ADVISORY — ${n} helper name${n === 1 ? "" : "s"} look${n === 1 ? "s" : ""} scenario-flavoured (does not block):`,
|
|
91
|
-
...
|
|
130
|
+
...names.map((v) => `- ${show(v.file, r)}:${v.line} · ${v.helper}: ${v.detail}`),
|
|
92
131
|
"Shared helpers are named by method + resource (API) or user intent (browser); scenario values and adjectives belong in arguments. Rename if the name would stop another test from reusing the helper; otherwise continue.",
|
|
93
132
|
].join("\n");
|
|
94
133
|
}
|
|
134
|
+
/** TypeScript only. Whether an untyped parameter breaks the customer's build depends
|
|
135
|
+
* on their tsconfig reaching the tests directory, so this advises and never blocks. */
|
|
136
|
+
function typingAdvisory(r) {
|
|
137
|
+
const typed = r.advisories.filter((v) => v.kind === "untyped-parameter");
|
|
138
|
+
if (typed.length === 0)
|
|
139
|
+
return "";
|
|
140
|
+
const n = typed.length;
|
|
141
|
+
return [
|
|
142
|
+
"",
|
|
143
|
+
"",
|
|
144
|
+
`ADVISORY — ${n} helper${n === 1 ? " has" : "s have"} untyped parameters (does not block):`,
|
|
145
|
+
...typed.map((v) => `- ${show(v.file, r)}:${v.line} · ${v.helper}: ${v.detail}`),
|
|
146
|
+
"Annotate each with its actual type — a Playwright page as `page: Page` (add `import type { Page } from '@playwright/test'` once, only if a page parameter is among them); API values by their shape (`client: SkyrampClient`, `headers: Record<string, string>`); never `any`. Advisory only: continue without re-verifying, and do NOT write a `reuse-verify` marker for it.",
|
|
147
|
+
].join("\n");
|
|
148
|
+
}
|
|
95
149
|
function verdictTail(ctx) {
|
|
96
150
|
if (ctx.recorded !== false)
|
|
97
151
|
return "";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { pendingReuseDebt } from "./code-refactor/reuse-state.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { assertionFeedbackForExecution, recordAssertionExecution, } from "./code-refactor/assertion-state.js";
|
|
4
|
+
import { stageAndRecordRetrofits } from "./code-refactor/retrofit-state.js";
|
|
5
5
|
import path from "path";
|
|
6
6
|
import { stripVTControlCharacters } from "util";
|
|
7
7
|
import { TestExecutionService } from "../services/TestExecutionService.js";
|
|
@@ -227,17 +227,6 @@ export function registerExecuteSkyrampTestTool(server) {
|
|
|
227
227
|
errorResult = toolError(`testFile must be an absolute path, got: ${params.testFile}`);
|
|
228
228
|
return errorResult;
|
|
229
229
|
}
|
|
230
|
-
// SKYR-4262: block execution when skyramp_enhance_assertions handed out
|
|
231
|
-
// instructions for this spec and the file has not changed since — the
|
|
232
|
-
// agent acknowledged the enhancement step without acting on it. The debt
|
|
233
|
-
// clears through the file itself (any edit, including a documented
|
|
234
|
-
// `// assertions reviewed:` no-change decision), so the agent can never
|
|
235
|
-
// be stuck here. Same enforcement shape as the reuse checkpoint above.
|
|
236
|
-
const owedEnhancement = await pendingAssertionEnhancement(params.testFile, params.stateFile);
|
|
237
|
-
if (owedEnhancement) {
|
|
238
|
-
errorResult = toolError(owedEnhancement);
|
|
239
|
-
return errorResult;
|
|
240
|
-
}
|
|
241
230
|
// Deterministic external-test guard (SKYR-3924): this tool runs Skyramp-generated
|
|
242
231
|
// tests in the executor and cannot run a repo's native (user-written) suite, so a
|
|
243
232
|
// run on an external test only errors (e.g. pytest import/collection failure).
|
|
@@ -270,7 +259,7 @@ export function registerExecuteSkyrampTestTool(server) {
|
|
|
270
259
|
// the last step before delivery, and a utils file left unstaged here ships a
|
|
271
260
|
// test importing a module the PR lacks. After the external-test skip: a native
|
|
272
261
|
// suite this tool will not run gets no scan and no staging.
|
|
273
|
-
await
|
|
262
|
+
await stageAndRecordRetrofits(params.testFile, undefined, params.stateFile);
|
|
274
263
|
// SKYR-4115 backstop: enhance_assertions carries the same check, but nothing
|
|
275
264
|
// guarantees the agent calls it, and execution is the last step that still
|
|
276
265
|
// precedes reporting. See pendingReuseVerification for why enforcing the verify
|
|
@@ -285,6 +274,22 @@ export function registerExecuteSkyrampTestTool(server) {
|
|
|
285
274
|
errorResult = toolError(owedReuseVerification);
|
|
286
275
|
return errorResult;
|
|
287
276
|
}
|
|
277
|
+
// Deterministic assertion-enhancement check: the server verifies the file
|
|
278
|
+
// itself here (never relying on the agent to call `verify: true` — prose
|
|
279
|
+
// can be ignored, this cannot). Insufficient assertions return feedback
|
|
280
|
+
// instead of executing; a fixed file passes on the next execute call.
|
|
281
|
+
// Sits BELOW the external-test guard: an external test is skipped, not
|
|
282
|
+
// executed, so deferring it for assertion work would demand fixes to a
|
|
283
|
+
// file this tool will never run.
|
|
284
|
+
const assertionFeedback = await assertionFeedbackForExecution(params.testFile, params.stateFile);
|
|
285
|
+
if (assertionFeedback) {
|
|
286
|
+
errorResult = toolError(assertionFeedback);
|
|
287
|
+
return errorResult;
|
|
288
|
+
}
|
|
289
|
+
// Proof-of-work substrate (SKYR-4262 follow-up): count this execution
|
|
290
|
+
// server-side so the report can cross-check generated vs executed —
|
|
291
|
+
// the narrative is LLM-authored, this number is not. Best-effort.
|
|
292
|
+
await recordAssertionExecution(params.testFile, params.testType, params.stateFile);
|
|
288
293
|
// Send initial progress
|
|
289
294
|
await sendProgress(5, 100, "Starting test execution...");
|
|
290
295
|
// Cross-repo run (SKYR-3819): tests delivered under the run's testsRepoDir
|
|
@@ -12,9 +12,11 @@ import { StateManager, runArtifactDir, getTestsRepoDir, } from "../utils/Analysi
|
|
|
12
12
|
import { toolError, testFileMatches } from "../utils/utils.js";
|
|
13
13
|
import { matchesApprovedPlan } from "../utils/planMatchKeys.js";
|
|
14
14
|
import { isTestbotEnabled } from "../utils/featureFlags.js";
|
|
15
|
-
import { findInvalidSourceCitations, findUnchangedFileClaims, listChangedFiles, listChangedFilesAcross, } from "../utils/reportVerification.js";
|
|
15
|
+
import { findInvalidSourceCitations, findUnchangedFileClaims, listChangedFiles, listChangedFilesAcross, listChangedFilesAbs, } from "../utils/reportVerification.js";
|
|
16
16
|
import { getReportLanguage, isEnforcedReportLanguage, findLanguageViolations, findLanguageNearMisses, reportLanguageDisplayName, } from "../utils/reportLanguage.js";
|
|
17
|
-
import {
|
|
17
|
+
import { canonicalTestPath, findAssertionRecordByFileName, rederiveAssertionOutcome, } from "./code-refactor/assertion-state.js";
|
|
18
|
+
import { rederiveReuseOutcome, reuseChainSkipped, samePath, } from "./code-refactor/reuse-state.js";
|
|
19
|
+
import { retrofitGate, } from "./code-refactor/retrofit-state.js";
|
|
18
20
|
// SKYR-3879 Path B: which testTypes the register-plan checkpoint gates. Mirrors
|
|
19
21
|
// the generation tools actually wired to planGuard (batch-scenario/integration,
|
|
20
22
|
// contract) — UI and E2E are on a separate blueprint-grounded pipeline and are
|
|
@@ -641,7 +643,26 @@ function computeReportMetrics(params) {
|
|
|
641
643
|
* gets `chainSkipped: true` (SKYR-4220), the one claim that is exactly about the
|
|
642
644
|
* reuse tool NOT having run. Applies to every test type: UI tests carry the POM
|
|
643
645
|
* fields, API tests the shared-helper ones. */
|
|
644
|
-
|
|
646
|
+
/** Attach the assertion-enhancement summary to a report row.
|
|
647
|
+
*
|
|
648
|
+
* How the row finds its record: records are keyed by the test file's full
|
|
649
|
+
* path, but a report row only carries a file NAME. So the match compares
|
|
650
|
+
* basenames, then checks testType and repository. When more than one record
|
|
651
|
+
* still matches, the row gets NO summary — attaching the wrong spec's
|
|
652
|
+
* numbers is worse than attaching none.
|
|
653
|
+
*
|
|
654
|
+
* The numbers are server-derived and re-computed from the delivered file at
|
|
655
|
+
* report time — the report narrative is LLM-authored, these numbers are not.
|
|
656
|
+
* What a reader can conclude: `executionCount: 0` = generated but never
|
|
657
|
+
* executed; no `assertions` field at all = the enhance tool never ran for
|
|
658
|
+
* the file and it never executed. */
|
|
659
|
+
async function attachAssertionOutcome(test, outcomes, checkouts) {
|
|
660
|
+
const record = findAssertionRecordByFileName(outcomes, test, checkouts);
|
|
661
|
+
if (!record)
|
|
662
|
+
return test;
|
|
663
|
+
return { ...test, assertions: await rederiveAssertionOutcome(record) };
|
|
664
|
+
}
|
|
665
|
+
async function attachReuseOutcome(test, outcomes, handOffs, retrofits = []) {
|
|
645
666
|
// POM records describe browser specs; a basename collision with an API test's
|
|
646
667
|
// fileName must not attach them there. A utils-path record is attachable anywhere.
|
|
647
668
|
const record = outcomes?.[path.basename(test.fileName)];
|
|
@@ -652,6 +673,18 @@ async function attachReuseOutcome(test, outcomes, handOffs) {
|
|
|
652
673
|
? record
|
|
653
674
|
: undefined;
|
|
654
675
|
const derived = found ? await rederiveReuseOutcome(found) : undefined;
|
|
676
|
+
// Pre-existing generated tests this spec's reuse pass rewired (SKYR-4276 A4): the
|
|
677
|
+
// report names each with its recorded execution, so a reviewer sees that the
|
|
678
|
+
// module became a dependency of code they already owned — and that it still runs.
|
|
679
|
+
if (derived?.helpers && found?.testFilePath) {
|
|
680
|
+
const specPath = found.testFilePath;
|
|
681
|
+
const mine = retrofits.filter((r) => samePath(r.testFile, specPath));
|
|
682
|
+
if (mine.length > 0)
|
|
683
|
+
derived.helpers.retrofits = mine.map((r) => ({
|
|
684
|
+
file: path.basename(r.file),
|
|
685
|
+
...(r.execution ? { execution: r.execution } : {}),
|
|
686
|
+
}));
|
|
687
|
+
}
|
|
655
688
|
// The RAW record: a colliding record of any kind means reuse ran for this basename,
|
|
656
689
|
// and the chain claim must not be made on the filtered view.
|
|
657
690
|
const chainSkipped = await reuseChainSkipped(test.fileName, test.testType, record, handOffs);
|
|
@@ -1036,6 +1069,11 @@ export function registerSubmitReportTool(server) {
|
|
|
1036
1069
|
"Add a testMaintenanceDetails entry with the missing field(s), drafted from the execution output you already saw.");
|
|
1037
1070
|
return errorResult;
|
|
1038
1071
|
}
|
|
1072
|
+
// SKYR-4276 A4: retrofits (pre-existing generated tests the reuse pass edited)
|
|
1073
|
+
// that still stand in the working tree, resolved once here and attached to the
|
|
1074
|
+
// rows below; an unexecuted one refuses the report (inside the SKYR-3883 block,
|
|
1075
|
+
// which already enumerates the working tree).
|
|
1076
|
+
let retrofitViews = [];
|
|
1039
1077
|
// SKYR-3883: in a testbot run, refuse to ship a report that claims file work
|
|
1040
1078
|
// the working tree doesn't reflect. The delivery step can only commit what the
|
|
1041
1079
|
// agent actually created/edited, so a report claiming otherwise erodes trust
|
|
@@ -1096,6 +1134,17 @@ export function registerSubmitReportTool(server) {
|
|
|
1096
1134
|
`Files with actual working-tree changes:\n${changedList || " (none)"}`);
|
|
1097
1135
|
return errorResult;
|
|
1098
1136
|
}
|
|
1137
|
+
const changedFilesAbs = await listChangedFilesAbs([
|
|
1138
|
+
repoRoot,
|
|
1139
|
+
getTestsRepoDir(),
|
|
1140
|
+
...Object.values(fullState?.relatedRepos ?? {}).map((section) => section.repositoryPath),
|
|
1141
|
+
]);
|
|
1142
|
+
const gate = await retrofitGate(stateData, changedFilesAbs, repoRoot);
|
|
1143
|
+
retrofitViews = gate.views;
|
|
1144
|
+
if (gate.refusal) {
|
|
1145
|
+
errorResult = toolError(gate.refusal);
|
|
1146
|
+
return errorResult;
|
|
1147
|
+
}
|
|
1099
1148
|
}
|
|
1100
1149
|
catch (err) {
|
|
1101
1150
|
// Never block a valid report because verification itself failed (path not a
|
|
@@ -1133,7 +1182,16 @@ export function registerSubmitReportTool(server) {
|
|
|
1133
1182
|
// generation — downstream scoring scripts don't expect them and fail if
|
|
1134
1183
|
// they encounter these string fields while traversing the object.
|
|
1135
1184
|
// Also normalize each item's `repository` (blank → undefined).
|
|
1136
|
-
|
|
1185
|
+
// Checkout roots let the assertion-record matcher verify a candidate
|
|
1186
|
+
// record actually lives in the row's repo (basename collisions across
|
|
1187
|
+
// repos must not publish one spec's proof-of-work under another's name).
|
|
1188
|
+
const assertionCheckouts = await stateManager
|
|
1189
|
+
.listRepoCheckouts()
|
|
1190
|
+
.catch(() => []);
|
|
1191
|
+
const sanitizedNewTests = await Promise.all(dedupedNewTests.map(async ({ scenarioFile: _sf, traceFile: _tf, frontendTrace: _ft, ...rest }) => {
|
|
1192
|
+
const row = await attachReuseOutcome(normalizeRepository(rest), stateData.reuseOutcomes, stateData.reuseHandOffs, retrofitViews);
|
|
1193
|
+
return attachAssertionOutcome(row, stateData.assertionOutcomes ?? {}, assertionCheckouts);
|
|
1194
|
+
}));
|
|
1137
1195
|
const report = {
|
|
1138
1196
|
businessCaseAnalysis: params.businessCaseAnalysis,
|
|
1139
1197
|
newTestsCreated: sanitizedNewTests,
|
|
@@ -1142,10 +1200,25 @@ export function registerSubmitReportTool(server) {
|
|
|
1142
1200
|
// an internal-only field, needed for matching but never meant to reach the report.
|
|
1143
1201
|
// TODO(multi-repo maintenance): map(normalizeRepository) once testMaintenanceSchema
|
|
1144
1202
|
// has a repository field (see TODO above).
|
|
1145
|
-
testMaintenance: testMaintenance
|
|
1146
|
-
...row
|
|
1147
|
-
|
|
1148
|
-
|
|
1203
|
+
testMaintenance: testMaintenance
|
|
1204
|
+
? await Promise.all(testMaintenance.map(async ({ testFilePath, ...row }) => {
|
|
1205
|
+
// Maintenance rows still carry the absolute path here, so the
|
|
1206
|
+
// assertion summary uses an EXACT canonical-path lookup — no
|
|
1207
|
+
// basename ambiguity. This is what carries the maintenance
|
|
1208
|
+
// honesty labels (nothing-to-verify vs verified) into the
|
|
1209
|
+
// report instead of leaving them as tool text the agent can
|
|
1210
|
+
// paraphrase.
|
|
1211
|
+
const record = stateData.assertionOutcomes?.[canonicalTestPath(testFilePath)];
|
|
1212
|
+
const assertions = record
|
|
1213
|
+
? await rederiveAssertionOutcome(record)
|
|
1214
|
+
: undefined;
|
|
1215
|
+
return {
|
|
1216
|
+
...row,
|
|
1217
|
+
fileName: path.basename(testFilePath),
|
|
1218
|
+
...(assertions ? { assertions } : {}),
|
|
1219
|
+
};
|
|
1220
|
+
}))
|
|
1221
|
+
: undefined,
|
|
1149
1222
|
// videoPath is filled from the run's execution records; testFilePath is the
|
|
1150
1223
|
// match-only key and is stripped from the wire format, the same line drawn for
|
|
1151
1224
|
// testMaintenance's own testFilePath above (downstream scoring scripts traverse
|