@skyramp/mcp 0.3.6 → 0.3.8
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/prompts/test-recommendation/diffExecutionPlan.js +40 -9
- package/build/prompts/test-recommendation/fullRepoCatalog.js +3 -2
- package/build/prompts/test-recommendation/recommendationSections.js +3 -3
- package/build/prompts/test-recommendation/scopeAssessment.d.ts +2 -2
- package/build/prompts/test-recommendation/scopeAssessment.js +58 -76
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +14 -1
- package/build/prompts/testbot/testbot-prompts.js +25 -8
- package/build/recommendation/budgeters/diversityBalancedBudgeter.js +39 -5
- package/build/recommendation/budgeters/shared.d.ts +21 -8
- package/build/recommendation/budgeters/shared.js +134 -56
- package/build/recommendation/planRanker.d.ts +5 -3
- package/build/recommendation/planRanker.js +1 -1
- 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 +87 -9
- package/build/tools/test-management/registerTestPlanTool.d.ts +3 -3
- package/build/tools/test-management/registerTestPlanTool.js +29 -8
- package/build/types/AssertionOutcome.d.ts +68 -0
- package/build/types/AssertionOutcome.js +1 -0
- package/build/types/RepositoryAnalysis.d.ts +10 -10
- package/build/types/ReuseOutcome.d.ts +16 -0
- package/build/types/TestRecommendation.d.ts +11 -1
- package/build/types/TestRecommendation.js +34 -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/pathSignatures.d.ts +4 -1
- package/build/utils/pathSignatures.js +14 -2
- package/build/utils/planOnlyMode.d.ts +33 -0
- package/build/utils/planOnlyMode.js +40 -0
- package/build/utils/reportVerification.d.ts +9 -0
- package/build/utils/reportVerification.js +39 -6
- package/build/utils/scenarioDrafting.d.ts +1 -1
- package/build/utils/scenarioDrafting.js +1 -1
- 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,375 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { realpathSync } from "fs";
|
|
3
|
+
import { mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
4
|
+
import { logger } from "../../utils/logger.js";
|
|
5
|
+
import { resolveRunStatePath, StateManager, } from "../../utils/AnalysisStateManager.js";
|
|
6
|
+
import { computeAssertionMetrics, detectAssertionLanguage, formatAssertionVerifyReport, sha256Of, verifyAssertionEnhancement, } from "../../utils/assertion-verify/index.js";
|
|
7
|
+
/**
|
|
8
|
+
* Persistence for the assertion-enhancement verification loop, keyed by the
|
|
9
|
+
* spec's CANONICAL absolute path in `UnifiedAnalysisState.assertionOutcomes`.
|
|
10
|
+
* Path keying (not basename, unlike reuseOutcomes — nothing here needs to
|
|
11
|
+
* match report rows) removes the multi-repo basename-collision class outright,
|
|
12
|
+
* and canonicalization closes the alias bypass: `dir//x.spec.ts` and
|
|
13
|
+
* `dir/./x.spec.ts` must hit the same record as `dir/x.spec.ts`.
|
|
14
|
+
*
|
|
15
|
+
* Every write is best-effort: verification must never be able to fail an
|
|
16
|
+
* enhancement that otherwise succeeded, so a missing/unreadable state file
|
|
17
|
+
* degrades to stateless checks and open gates — always with a warning.
|
|
18
|
+
*/
|
|
19
|
+
/** Canonical identity of a test file — all keying and ownership comparisons
|
|
20
|
+
* go through this so a non-normalized agent-supplied path cannot fork or
|
|
21
|
+
* bypass a record. realpath also collapses symlinked directories; a path
|
|
22
|
+
* that does not (yet) exist falls back to plain resolution. */
|
|
23
|
+
export function canonicalTestPath(testFile) {
|
|
24
|
+
const resolved = path.resolve(testFile);
|
|
25
|
+
try {
|
|
26
|
+
return realpathSync(resolved);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return resolved;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Read-modify-write one spec's record. The patch sees the existing record
|
|
33
|
+
* (or undefined) and returns the COMPLETE next record — or undefined to skip
|
|
34
|
+
* the write (e.g. a verdict for a record that does not exist). Returns true
|
|
35
|
+
* only when the record was actually written. */
|
|
36
|
+
async function updateAssertionOutcome(testFile, patch, explicitStateFile,
|
|
37
|
+
// State the caller already read for THIS file — skips the re-read (each
|
|
38
|
+
// gate call was re-parsing the same run-state JSON up to 8 times).
|
|
39
|
+
preloaded) {
|
|
40
|
+
const stateFile = resolveRunStatePath(explicitStateFile);
|
|
41
|
+
if (!stateFile)
|
|
42
|
+
return false;
|
|
43
|
+
try {
|
|
44
|
+
const manager = StateManager.fromStatePath(stateFile);
|
|
45
|
+
const data = preloaded ?? (await manager.readData());
|
|
46
|
+
if (!data) {
|
|
47
|
+
// A resolvable path is not proof of a run (this repo's own CI sets
|
|
48
|
+
// RUNNER_TEMP): the state file simply doesn't exist. Say so — silently
|
|
49
|
+
// returning here disables the whole gate for the run with no trace.
|
|
50
|
+
logger.warning("Run state file not found — assertion-enhancement state not recorded, verification gate inactive", { stateFile, testFile });
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const key = canonicalTestPath(testFile);
|
|
54
|
+
const next = patch(data.assertionOutcomes?.[key]);
|
|
55
|
+
if (next === undefined)
|
|
56
|
+
return false;
|
|
57
|
+
await manager.writeData({
|
|
58
|
+
...data,
|
|
59
|
+
assertionOutcomes: {
|
|
60
|
+
...data.assertionOutcomes,
|
|
61
|
+
[key]: next,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
logger.warning("Could not record assertion-enhancement state", {
|
|
68
|
+
testFile,
|
|
69
|
+
error: String(err),
|
|
70
|
+
});
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** The state files a record may live in: the explicitly supplied one first,
|
|
75
|
+
* then the run-default path. Callers thread `stateFile` inconsistently across
|
|
76
|
+
* tools (enhance may record to the default while execute checks an explicit
|
|
77
|
+
* path, or vice versa) — reading both closes the asymmetry that otherwise
|
|
78
|
+
* fails the gate open on a mismatched `stateFile`. */
|
|
79
|
+
function candidateStateFiles(explicitStateFile) {
|
|
80
|
+
const explicit = resolveRunStatePath(explicitStateFile);
|
|
81
|
+
const fallback = resolveRunStatePath(undefined);
|
|
82
|
+
return [...new Set([explicit, fallback])].filter((p) => p !== undefined);
|
|
83
|
+
}
|
|
84
|
+
/** The record and the state file it actually lives in, or undefined. */
|
|
85
|
+
async function locateAssertionRecord(testFile, explicitStateFile) {
|
|
86
|
+
for (const stateFile of candidateStateFiles(explicitStateFile)) {
|
|
87
|
+
try {
|
|
88
|
+
const data = await StateManager.fromStatePath(stateFile).readData();
|
|
89
|
+
const record = data?.assertionOutcomes?.[canonicalTestPath(testFile)];
|
|
90
|
+
if (data && record !== undefined)
|
|
91
|
+
return { record, stateFile, data };
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
logger.warning("Could not read a candidate run state file for the assertion record — trying the next", { stateFile, testFile, error: String(err) });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
/** A record only counts for `testFile` when its stored path canonicalizes to
|
|
100
|
+
* the same spec — the single home for the ownership invariant (previously
|
|
101
|
+
* copied in baselineFrom, recordAssertionVerdict, and the enhance tool). */
|
|
102
|
+
export function trustedRecord(record, testFile) {
|
|
103
|
+
return record !== undefined &&
|
|
104
|
+
canonicalTestPath(record.testFilePath) === canonicalTestPath(testFile)
|
|
105
|
+
? record
|
|
106
|
+
: undefined;
|
|
107
|
+
}
|
|
108
|
+
/** One spec's record, or undefined when there is no run state / no record. */
|
|
109
|
+
export async function readAssertionRecord(testFile, explicitStateFile) {
|
|
110
|
+
return (await locateAssertionRecord(testFile, explicitStateFile))?.record;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Snapshot the file at instruction hand-out time — the baseline verify measures
|
|
114
|
+
* against. Keep-first rule: while no Passed verdict exists for the same file and
|
|
115
|
+
* enhanceType, a repeat hand-out does NOT re-baseline (re-calling the tool after
|
|
116
|
+
* half-editing must not shrink the required delta). A hand-out after a Passed
|
|
117
|
+
* verdict, or with a different enhanceType, starts a new enhancement round.
|
|
118
|
+
*/
|
|
119
|
+
export async function recordAssertionBaseline(testFile, testType, enhanceType, explicitStateFile) {
|
|
120
|
+
const stateFile = resolveRunStatePath(explicitStateFile);
|
|
121
|
+
if (!stateFile)
|
|
122
|
+
return;
|
|
123
|
+
try {
|
|
124
|
+
// Probe the state file BEFORE writing the baseline copy: a resolvable path
|
|
125
|
+
// with no state file (RUNNER_TEMP set, no analyze-changes run) must not
|
|
126
|
+
// leave an orphan copy behind while the record write silently no-ops.
|
|
127
|
+
const data = await StateManager.fromStatePath(stateFile).readData();
|
|
128
|
+
if (!data) {
|
|
129
|
+
logger.warning("Run state file not found — assertion baseline not recorded, verification gate inactive for this file", { stateFile, testFile });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const canonical = canonicalTestPath(testFile);
|
|
133
|
+
const existing = data.assertionOutcomes?.[canonical];
|
|
134
|
+
if (existing?.fileSha256 !== undefined &&
|
|
135
|
+
existing.verification !== "passed" &&
|
|
136
|
+
existing.enhanceType === enhanceType) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const content = await readFile(testFile, "utf8");
|
|
140
|
+
const sha = sha256Of(content);
|
|
141
|
+
const language = detectAssertionLanguage(testFile);
|
|
142
|
+
const metrics = language
|
|
143
|
+
? computeAssertionMetrics(content, language)
|
|
144
|
+
: undefined;
|
|
145
|
+
const baselineFilePath = await saveBaselineCopy(stateFile, canonical, sha, content);
|
|
146
|
+
const written = await updateAssertionOutcome(testFile,
|
|
147
|
+
// Full replacement — a new enhancement round resets any earlier verdict.
|
|
148
|
+
// Only executionCount survives: executions already happened regardless
|
|
149
|
+
// of which round's instructions were in force.
|
|
150
|
+
(previous) => ({
|
|
151
|
+
testFilePath: canonical,
|
|
152
|
+
testType,
|
|
153
|
+
executionCount: previous?.executionCount ?? 0,
|
|
154
|
+
enhanceType,
|
|
155
|
+
fileSha256: sha,
|
|
156
|
+
baselineFilePath,
|
|
157
|
+
baselineCount: metrics?.count,
|
|
158
|
+
baselineStrength: metrics?.strengthScore,
|
|
159
|
+
baselineFingerprints: metrics?.fingerprints,
|
|
160
|
+
issuedAt: new Date().toISOString(),
|
|
161
|
+
}), explicitStateFile, data);
|
|
162
|
+
// A failed record write must not strand an orphan copy nobody references.
|
|
163
|
+
if (!written && baselineFilePath !== undefined) {
|
|
164
|
+
await rm(baselineFilePath, { force: true }).catch(() => { });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
logger.warning("Could not record assertion baseline — verification will run stateless checks only", { testFile, error: String(err) });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Preserve the generated file as delivered at hand-out time, beside the run
|
|
173
|
+
* state (never in the repo — a repo sidecar could leak into the customer
|
|
174
|
+
* commit and would need its own cleanup step, like the reuse flow's .raw.bak).
|
|
175
|
+
* The sha prefix disambiguates basename collisions across repos and rounds.
|
|
176
|
+
* Best-effort: an unsaved copy costs the diff/restore aid, not the baseline.
|
|
177
|
+
*/
|
|
178
|
+
async function saveBaselineCopy(stateFile, testFile, sha, content) {
|
|
179
|
+
try {
|
|
180
|
+
const dir = path.join(path.dirname(stateFile), "assertion-baselines");
|
|
181
|
+
await mkdir(dir, { recursive: true });
|
|
182
|
+
const copyPath = path.join(dir, `${sha.slice(0, 8)}-${path.basename(testFile)}`);
|
|
183
|
+
await writeFile(copyPath, content, "utf8");
|
|
184
|
+
return copyPath;
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
logger.warning("Could not save the assertion baseline copy", {
|
|
188
|
+
testFile,
|
|
189
|
+
error: String(err),
|
|
190
|
+
});
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** The baseline verify measures against, from a record; undefined when the
|
|
195
|
+
* record carries no snapshot or belongs to a different canonical path. */
|
|
196
|
+
export function baselineFrom(record, testFile) {
|
|
197
|
+
if (record?.fileSha256 === undefined)
|
|
198
|
+
return undefined;
|
|
199
|
+
if (trustedRecord(record, testFile) === undefined)
|
|
200
|
+
return undefined;
|
|
201
|
+
return {
|
|
202
|
+
fileSha256: record.fileSha256,
|
|
203
|
+
count: record.baselineCount,
|
|
204
|
+
strengthScore: record.baselineStrength,
|
|
205
|
+
fingerprints: record.baselineFingerprints,
|
|
206
|
+
baselineFilePath: record.baselineFilePath,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/** Record what one verify pass established. The verdict is written to the
|
|
210
|
+
* state file the record actually LIVES in (not blindly to the explicit path —
|
|
211
|
+
* a mismatched `stateFile` would otherwise split verdict from baseline).
|
|
212
|
+
* Never writes onto a record owned by a different canonical path, and never
|
|
213
|
+
* fails silently. */
|
|
214
|
+
export async function recordAssertionVerdict(testFile, result, explicitStateFile) {
|
|
215
|
+
let home;
|
|
216
|
+
let homeData;
|
|
217
|
+
try {
|
|
218
|
+
const located = await locateAssertionRecord(testFile, explicitStateFile);
|
|
219
|
+
home = located?.stateFile;
|
|
220
|
+
homeData = located?.data;
|
|
221
|
+
if (located !== undefined &&
|
|
222
|
+
trustedRecord(located.record, testFile) === undefined) {
|
|
223
|
+
logger.warning("Assertion verdict not recorded — the record is owned by a different spec path", { testFile, recordedPath: located.record.testFilePath });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
logger.warning("Assertion verdict not recorded — could not read the run state", { testFile, error: String(err) });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
await updateAssertionOutcome(testFile,
|
|
232
|
+
// A verdict amends an existing record; without one there is nothing the
|
|
233
|
+
// verdict is ABOUT (verify-with-no-baseline records a baseline instead,
|
|
234
|
+
// never a verdict), so skip the write rather than invent a partial record.
|
|
235
|
+
(existing) => existing === undefined
|
|
236
|
+
? undefined
|
|
237
|
+
: {
|
|
238
|
+
...existing,
|
|
239
|
+
verification: result.ok ? "passed" : "failed",
|
|
240
|
+
declinedReason: result.markerReason,
|
|
241
|
+
}, home ?? explicitStateFile, homeData);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Deterministic execute-time verification: skyramp_execute_test calls this
|
|
245
|
+
* itself, so the check runs whether or not the agent ever called `verify: true`
|
|
246
|
+
* — a prose instruction can be ignored, a server-side call cannot. Returns the
|
|
247
|
+
* feedback text when the assertions are insufficient (the caller returns it
|
|
248
|
+
* instead of executing), or undefined to proceed.
|
|
249
|
+
*
|
|
250
|
+
* Self-clearing: the checks run on the file's CURRENT content, so fixing the
|
|
251
|
+
* assertions and re-executing passes — no separate verify call is required.
|
|
252
|
+
* Skipped once a verdict has PASSED: sanctioned later rewrites (a `.raw.bak`
|
|
253
|
+
* restore during execution fix-up) would otherwise false-fail the differential
|
|
254
|
+
* gates. Modularization needs no special handling: the shared-helper policy
|
|
255
|
+
* keeps every value assertion in the test body, so the baseline comparison
|
|
256
|
+
* stays valid across it. Fails open on no baseline and unreadable state/file.
|
|
257
|
+
*/
|
|
258
|
+
export async function assertionFeedbackForExecution(testFile, explicitStateFile) {
|
|
259
|
+
try {
|
|
260
|
+
const record = await readAssertionRecord(testFile, explicitStateFile);
|
|
261
|
+
if (record === undefined)
|
|
262
|
+
return undefined;
|
|
263
|
+
const baseline = baselineFrom(record, testFile);
|
|
264
|
+
if (baseline === undefined)
|
|
265
|
+
return undefined;
|
|
266
|
+
if (record.verification === "passed")
|
|
267
|
+
return undefined;
|
|
268
|
+
const result = await verifyAssertionEnhancement({
|
|
269
|
+
testFile,
|
|
270
|
+
testType: record.testType,
|
|
271
|
+
// The baseline writer sets enhanceType with the snapshot, so a record
|
|
272
|
+
// with a baseline always carries it; a hand-truncated state file
|
|
273
|
+
// defaults to the STRICTER mode rather than deferring execution.
|
|
274
|
+
enhanceType: record.enhanceType ?? "generation",
|
|
275
|
+
baseline,
|
|
276
|
+
});
|
|
277
|
+
await recordAssertionVerdict(testFile, result, explicitStateFile);
|
|
278
|
+
if (result.ok)
|
|
279
|
+
return undefined;
|
|
280
|
+
return (`Execution deferred — the assertion enhancement of ${path.basename(testFile)} is insufficient. ` +
|
|
281
|
+
`Fix the violations below, then call skyramp_execute_test again (it re-checks the file automatically; ` +
|
|
282
|
+
`you can also get early feedback anytime via skyramp_enhance_assertions with verify: true).\n\n` +
|
|
283
|
+
formatAssertionVerifyReport(result, testFile));
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
logger.warning("Could not verify assertion enhancement at execution time — proceeding", { testFile, error: String(err) });
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Count one execution of this spec (called by skyramp_execute_test after its
|
|
292
|
+
* gates pass, best-effort). Creates a minimal record when none exists so an
|
|
293
|
+
* executed-but-never-enhanced spec is still visible to the report cross-check.
|
|
294
|
+
*/
|
|
295
|
+
export async function recordAssertionExecution(testFile, testType, explicitStateFile) {
|
|
296
|
+
const located = await locateAssertionRecord(testFile, explicitStateFile).catch(() => undefined);
|
|
297
|
+
await updateAssertionOutcome(testFile, (existing) => existing === undefined
|
|
298
|
+
? {
|
|
299
|
+
testFilePath: canonicalTestPath(testFile),
|
|
300
|
+
testType,
|
|
301
|
+
executionCount: 1,
|
|
302
|
+
}
|
|
303
|
+
: { ...existing, executionCount: existing.executionCount + 1 }, located?.stateFile ?? explicitStateFile, located?.data);
|
|
304
|
+
}
|
|
305
|
+
/** The record for a report row, matched by the canonical-path keys' basenames
|
|
306
|
+
* and then GUARDED: the record's testType must match the row's, and when the
|
|
307
|
+
* run's checkouts are known, the record's path must live in the row's repo
|
|
308
|
+
* (primary when the row names none). Without the guards, a basename shared
|
|
309
|
+
* across repos published one spec's proof-of-work under another's name — and
|
|
310
|
+
* the victim was exactly the never-enhanced spec the field exists to expose.
|
|
311
|
+
* Ambiguity after filtering still fails open: wrong attribution is worse
|
|
312
|
+
* than omission. */
|
|
313
|
+
export function findAssertionRecordByFileName(outcomes, row, checkouts) {
|
|
314
|
+
const base = path.basename(row.fileName);
|
|
315
|
+
const matches = Object.entries(outcomes).filter(([key, record]) => {
|
|
316
|
+
if (path.basename(key) !== base)
|
|
317
|
+
return false;
|
|
318
|
+
if (row.testType !== undefined && record.testType !== row.testType) {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
if (checkouts.length > 0) {
|
|
322
|
+
const home = checkouts.find((c) => record.testFilePath.startsWith(c.root + path.sep));
|
|
323
|
+
// A record living in a KNOWN checkout must belong to the row's repo
|
|
324
|
+
// (rows without a repository belong to the primary). A record outside
|
|
325
|
+
// every known checkout stays eligible — fail open on the guard itself.
|
|
326
|
+
if (home) {
|
|
327
|
+
const rowIsPrimary = row.repository === undefined;
|
|
328
|
+
if (rowIsPrimary ? !home.primary : home.repository !== row.repository) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return true;
|
|
334
|
+
});
|
|
335
|
+
return matches.length === 1 ? matches[0][1] : undefined;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Report-facing proof-of-work summary re-derived from the spec as delivered
|
|
339
|
+
* NOW — final metrics come from the file, not from verify-time state, so
|
|
340
|
+
* post-verify edits cannot publish stale counts. Always returns the status
|
|
341
|
+
* fields (verification, declinedReason, executionCount — they come from the
|
|
342
|
+
* record); the metric fields are omitted when the file cannot supply them.
|
|
343
|
+
*/
|
|
344
|
+
export async function rederiveAssertionOutcome(record) {
|
|
345
|
+
const { testFilePath, verification, declinedReason } = record;
|
|
346
|
+
const base = {
|
|
347
|
+
verification: verification ?? "never-verified",
|
|
348
|
+
declinedReason,
|
|
349
|
+
executionCount: record.executionCount,
|
|
350
|
+
};
|
|
351
|
+
try {
|
|
352
|
+
// Metrics exist only for the languages the analyzer supports (python, ts,
|
|
353
|
+
// js, java). Maintained rows can point at other languages the discovery
|
|
354
|
+
// patterns accept (go, ruby, php) — those report status without metrics.
|
|
355
|
+
const language = detectAssertionLanguage(testFilePath);
|
|
356
|
+
if (language === undefined)
|
|
357
|
+
return base;
|
|
358
|
+
const metrics = computeAssertionMetrics(await readFile(testFilePath, "utf8"), language);
|
|
359
|
+
return {
|
|
360
|
+
...base,
|
|
361
|
+
assertionsBaseline: record.baselineCount,
|
|
362
|
+
assertionsFinal: metrics.count,
|
|
363
|
+
strengthBaseline: record.baselineStrength,
|
|
364
|
+
strengthFinal: metrics.strengthScore,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
catch (err) {
|
|
368
|
+
// readFile is the only throw here: the delivered spec is gone or
|
|
369
|
+
// unreadable at report time (deleted after execution, permissions).
|
|
370
|
+
// Deterministic — a retry would re-read the same missing file — so
|
|
371
|
+
// report the status fields and omit the metrics.
|
|
372
|
+
logger.warning("Could not re-derive the assertion outcome from the delivered spec — omitting metrics from the report", { testFile: testFilePath, error: String(err) });
|
|
373
|
+
return base;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stageAndRecordRetrofits } from "./retrofit-state.js";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import * as path from "path";
|
|
3
4
|
import { logger } from "../../utils/logger.js";
|
|
@@ -7,9 +8,9 @@ import { isPomAwareTarget } from "../../utils/reuseRouting.js";
|
|
|
7
8
|
import { selectScopedPoms, } from "../../utils/pom-scope/index.js";
|
|
8
9
|
import { verifyReuse } from "../../utils/pom-verify/index.js";
|
|
9
10
|
import { infraGateFailure, zeroReuseGateFailure, composeVerifyText, } from "./verify-gates.js";
|
|
10
|
-
import { recordCandidates, recordNoPomLayer, recordVerifyOutcome, recordUtilsReuseIssued, recordUtilsVerifyOutcome, recordUtilsVerifyError, pendingModularization, } from "./reuse-state.js";
|
|
11
|
+
import { recordCandidates, recordNoPomLayer, recordVerifyOutcome, recordUtilsReuseIssued, recordUtilsVerifyOutcome, recordUtilsVerifyError, pendingModularization, utilsBaselineFor, } from "./reuse-state.js";
|
|
11
12
|
import { formatUtilsVerifyText } from "./utils-verify-gates.js";
|
|
12
|
-
import {
|
|
13
|
+
import { utilsSpecFor, verifyUtils } from "../../utils/utils-verify/index.js";
|
|
13
14
|
import { codeRefactoringSchema, languageSchema, TestType, } from "../../types/TestTypes.js";
|
|
14
15
|
import { SKYRAMP_UTILS_HEADER } from "../../utils/utils.js";
|
|
15
16
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
@@ -30,7 +31,7 @@ const codeReuseSchema = z.object({
|
|
|
30
31
|
verify: z
|
|
31
32
|
.boolean()
|
|
32
33
|
.default(false)
|
|
33
|
-
.describe("Verify a previously refactored test instead of returning the reuse prompt. POM path (browser tests): checks the test's POM calls against source; requires the discovery pass — this tool called for the same testFile without `verify` — to have run first. SkyrampUtils path (every other test type): stages the shared utils file for the output commit
|
|
34
|
+
.describe("Verify a previously refactored test instead of returning the reuse prompt. POM path (browser tests): checks the test's POM calls against source; requires the discovery pass — this tool called for the same testFile without `verify` — to have run first. SkyrampUtils path (every other test type): stages the shared utils file for the output commit, checks its invariants, and — when skyramp_modularization recorded a baseline for this spec — compares the delivered assertion count and page.on('pageerror') guard against that hand-out, rejecting a net loss (API helpers: one helper per method+path, status-code-only assertions, method+resource names; browser helpers: actions and structural waits only, intent names). Call it after the reuse edits are written; skyramp_enhance_assertions and skyramp_execute_test refuse until it has passed."),
|
|
34
35
|
testType: z
|
|
35
36
|
.nativeEnum(TestType)
|
|
36
37
|
.optional()
|
|
@@ -131,11 +132,12 @@ async function runUtilsVerify(params) {
|
|
|
131
132
|
};
|
|
132
133
|
}
|
|
133
134
|
try {
|
|
134
|
-
const staging = await
|
|
135
|
+
const staging = await stageAndRecordRetrofits(params.testFile, params.language);
|
|
135
136
|
const r = await verifyUtils({
|
|
136
137
|
testFile: params.testFile,
|
|
137
138
|
language: params.language,
|
|
138
139
|
testType: params.testType,
|
|
140
|
+
baseline: await utilsBaselineFor(params.testFile),
|
|
139
141
|
});
|
|
140
142
|
// A utils file git refused to stage ships a test importing a module the output
|
|
141
143
|
// commit lacks — the exact failure this gate exists for — so it fails the pass
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { pendingReuseDebt } from "./reuse-state.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { baselineFrom, readAssertionRecord, recordAssertionBaseline, recordAssertionVerdict, trustedRecord, } from "./assertion-state.js";
|
|
4
|
+
import { formatAssertionVerifyReport, verifyAssertionEnhancement, } from "../../utils/assertion-verify/index.js";
|
|
5
|
+
import { stageAndRecordRetrofits } from "./retrofit-state.js";
|
|
6
|
+
import * as path from "path";
|
|
5
7
|
import { toolError } from "../../utils/utils.js";
|
|
6
|
-
import { TestType } from "../../types/TestTypes.js";
|
|
8
|
+
import { ASSERTION_ENHANCEABLE_TEST_TYPES, TestType, } from "../../types/TestTypes.js";
|
|
7
9
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
8
10
|
import { getContractProviderAssertionsPrompt } from "../../prompts/enhance-assertions/contractProviderAssertionsPrompt.js";
|
|
9
11
|
import { getIntegrationAssertionsPrompt } from "../../prompts/enhance-assertions/integrationAssertionsPrompt.js";
|
|
@@ -53,7 +55,7 @@ const enhanceAssertionsSchema = {
|
|
|
53
55
|
.string()
|
|
54
56
|
.describe("Absolute path to the generated or updated test file whose assertions must be enhanced. Pass the exact file you just created or modified."),
|
|
55
57
|
testType: z
|
|
56
|
-
.enum(
|
|
58
|
+
.enum(ASSERTION_ENHANCEABLE_TEST_TYPES)
|
|
57
59
|
.describe("The type of test whose assertions should be enhanced. Contract applies to provider contract tests only, not consumer contract tests."),
|
|
58
60
|
enhanceType: z
|
|
59
61
|
.enum(["generation", "maintenance"])
|
|
@@ -66,6 +68,18 @@ const enhanceAssertionsSchema = {
|
|
|
66
68
|
.describe("When true, returns a compact instruction set (10 lines) that tells the agent to " +
|
|
67
69
|
"read the test file, enhance assertions per standard rules, and write it back directly. " +
|
|
68
70
|
"When false (default), returns the full verbose instruction set."),
|
|
71
|
+
verify: z
|
|
72
|
+
.boolean()
|
|
73
|
+
.default(false)
|
|
74
|
+
.describe("Set true AFTER applying the enhancement instructions to deterministically verify " +
|
|
75
|
+
"they were applied (assertions added or strengthened, rubric rules followed). " +
|
|
76
|
+
"Returns VERIFICATION PASSED or FAILED with per-violation remediations. " +
|
|
77
|
+
"Use the same testFile, testType, and enhanceType as the instruction call."),
|
|
78
|
+
stateFile: z
|
|
79
|
+
.string()
|
|
80
|
+
.optional()
|
|
81
|
+
.describe("Absolute path to the run state file, when one was provided by the workflow. " +
|
|
82
|
+
"Optional — in CI the run state is resolved automatically."),
|
|
69
83
|
};
|
|
70
84
|
export function registerEnhanceAssertionsTool(server) {
|
|
71
85
|
server.registerTool(TOOL_NAME, {
|
|
@@ -73,36 +87,77 @@ export function registerEnhanceAssertionsTool(server) {
|
|
|
73
87
|
|
|
74
88
|
### When to call
|
|
75
89
|
- After generating a new supported test file
|
|
76
|
-
- After updating an existing supported test file during maintenance
|
|
90
|
+
- After updating an existing supported test file during maintenance
|
|
91
|
+
- Optionally with \`verify: true\` for early feedback — the same deterministic checks run automatically inside skyramp_execute_test, which returns per-violation feedback instead of executing until they pass.`,
|
|
77
92
|
inputSchema: enhanceAssertionsSchema,
|
|
78
93
|
}, async (params) => {
|
|
79
|
-
const { testFile, testType, enhanceType, autoApply } = params;
|
|
94
|
+
const { testFile, testType, enhanceType, autoApply, verify, stateFile } = params;
|
|
95
|
+
// Everything downstream keys state by this path; a relative spelling
|
|
96
|
+
// would fork records and fail gates open (review finding on SKYR-4262).
|
|
97
|
+
if (!path.isAbsolute(testFile)) {
|
|
98
|
+
return toolError(`testFile must be an absolute path, got: ${testFile}`);
|
|
99
|
+
}
|
|
80
100
|
// SKYR-4115: the reuse tool hands out POM mapping instructions and then relies on
|
|
81
101
|
// the agent to come back with `verify: true` — the call that runs the zero-reuse
|
|
82
102
|
// gate. This is the first tool called after it on the same spec, so it is where a
|
|
83
103
|
// skipped verify is caught cheapest. Enforcing the call (not the outcome) keeps
|
|
84
104
|
// the policy in the reuse phase where it belongs.
|
|
85
|
-
//
|
|
86
|
-
// No stateFile is passed because this tool's schema has no such field, so outside
|
|
87
|
-
// CI — where nothing anchors the run state path — this checkpoint fails open. The
|
|
88
|
-
// execute_test backstop does take params.stateFile and covers that caller.
|
|
89
105
|
// SKYR-4220: stage the shared utils file this spec wrote or imports BEFORE the
|
|
90
106
|
// debt check, unconditionally — so the file reaches the output commit even on a
|
|
91
107
|
// path where the agent never calls verify. Located by header, not by name.
|
|
92
|
-
await
|
|
93
|
-
const owed = await pendingReuseDebt(testFile,
|
|
108
|
+
await stageAndRecordRetrofits(testFile);
|
|
109
|
+
const owed = await pendingReuseDebt(testFile, stateFile, testType, {
|
|
94
110
|
chain: false,
|
|
95
111
|
});
|
|
96
112
|
if (owed)
|
|
97
113
|
return toolError(owed);
|
|
98
114
|
// Stage so testbot includes the generated files in its output commit.
|
|
99
115
|
await stageGeneratedPaths(testFile);
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
116
|
+
// Verify pass: deterministically check the hand-out instructions were applied
|
|
117
|
+
// (hash delta, no removed coverage, rubric lints, strength increase) and give
|
|
118
|
+
// per-violation feedback. isError on FAILED forces the fix-and-retry loop.
|
|
119
|
+
if (verify) {
|
|
120
|
+
try {
|
|
121
|
+
const record = await readAssertionRecord(testFile, stateFile);
|
|
122
|
+
// Discriminators come from the persisted record when it matches this
|
|
123
|
+
// canonical path — params are agent-supplied, and flipping enhanceType
|
|
124
|
+
// to "maintenance" would disarm the strength gate and structural lints
|
|
125
|
+
// for a permanent PASS. Canonical comparison closes the alias bypass
|
|
126
|
+
// (dir//x.spec.ts). Falls back to params only when stateless.
|
|
127
|
+
const trusted = trustedRecord(record, testFile);
|
|
128
|
+
const result = await verifyAssertionEnhancement({
|
|
129
|
+
testFile,
|
|
130
|
+
testType: trusted?.testType ?? testType,
|
|
131
|
+
enhanceType: (trusted?.enhanceType ??
|
|
132
|
+
enhanceType),
|
|
133
|
+
baseline: baselineFrom(record, testFile),
|
|
134
|
+
});
|
|
135
|
+
await recordAssertionVerdict(testFile, result, stateFile);
|
|
136
|
+
AnalyticsService.pushMCPToolEvent(TOOL_NAME, undefined, {
|
|
137
|
+
testFile,
|
|
138
|
+
testType,
|
|
139
|
+
enhanceType,
|
|
140
|
+
verify: true,
|
|
141
|
+
verifyPassed: result.ok,
|
|
142
|
+
}).catch(() => { });
|
|
143
|
+
return {
|
|
144
|
+
content: [
|
|
145
|
+
{
|
|
146
|
+
type: "text",
|
|
147
|
+
text: formatAssertionVerifyReport(result, testFile),
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
isError: !result.ok,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
// Not necessarily the test file — state reads can throw here too.
|
|
155
|
+
return toolError(`${TOOL_NAME} verification failed for ${testFile}: ${err.message}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Hand-out: snapshot the file as generated so the verify pass has a baseline
|
|
159
|
+
// to measure against. Best-effort — no run state means stateless verify later.
|
|
160
|
+
await recordAssertionBaseline(testFile, testType, enhanceType, stateFile);
|
|
106
161
|
if (autoApply) {
|
|
107
162
|
const compactInstructions = [
|
|
108
163
|
`Enhance response body assertions in: \`${testFile}\``,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type UnifiedAnalysisState } from "../../utils/AnalysisStateManager.js";
|
|
2
|
+
import { type StagingResult } from "../../utils/utils-verify/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Retrofit tracking (SKYR-4276 A4): a RETROFIT is a pre-existing Skyramp-generated
|
|
5
|
+
* test the reuse pass edited to import from the shared utils file (STEP 5c / the
|
|
6
|
+
* sibling-inline advisory). Staging carries the edit into the output commit, so the
|
|
7
|
+
* report must disclose it — with an execution that ran the edited bytes.
|
|
8
|
+
*
|
|
9
|
+
* Whether an execution counts is answered by TIME, not by a stored hash: the
|
|
10
|
+
* execution record already carries `executedAt`, and the file's mtime says when it
|
|
11
|
+
* was last edited — an execution older than the last edit ran bytes that no longer
|
|
12
|
+
* exist. That also covers a bare editor write with no tool call in between, which a
|
|
13
|
+
* record-time hash cannot see. (Same identity pattern as VideoRecord's
|
|
14
|
+
* status/executedAt.)
|
|
15
|
+
*/
|
|
16
|
+
/** Stage the utils artifacts for `testFile` and record any retrofits staging found.
|
|
17
|
+
* The one call every checkpoint makes (reuse verify, enhance, execute). */
|
|
18
|
+
export declare function stageAndRecordRetrofits(testFile: string, language?: string, explicitStateFile?: string): Promise<StagingResult>;
|
|
19
|
+
/**
|
|
20
|
+
* Record the pre-existing generated tests the reuse pass edited (found by staging).
|
|
21
|
+
* Idempotent; a retrofit already recorded keeps its first attribution. Fails open
|
|
22
|
+
* like every other reuse-state write.
|
|
23
|
+
*/
|
|
24
|
+
export declare function recordReuseRetrofits(retrofits: string[], utilsFiles: string[], testFile: string, explicitStateFile?: string): Promise<void>;
|
|
25
|
+
/** One retrofit as the report sees it. */
|
|
26
|
+
export interface RetrofitView {
|
|
27
|
+
/** Absolute path. */
|
|
28
|
+
file: string;
|
|
29
|
+
utilsFile: string;
|
|
30
|
+
/** The spec whose reuse pass caused the edit (absolute). */
|
|
31
|
+
testFile: string;
|
|
32
|
+
/** Status of an execution that ran the CURRENT bytes (executedAt ≥ the file's
|
|
33
|
+
* mtime), if any. */
|
|
34
|
+
execution?: import("../../types/TestExecution.js").TestExecutionStatus;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The retrofits that still stand in the working tree, with the execution that ran
|
|
38
|
+
* their current bytes. `changedFilesAbs` is ABSOLUTE paths across every checkout (see
|
|
39
|
+
* listChangedFilesAbs) — a retrofit the agent reverted is not among them and drops
|
|
40
|
+
* out. An execution older than the file's mtime ran bytes that no longer exist and
|
|
41
|
+
* does not count; an unreadable mtime fails open (the execution counts), matching
|
|
42
|
+
* the surrounding SKYR-3883 infrastructure-failure semantics.
|
|
43
|
+
*/
|
|
44
|
+
export declare function standingRetrofits(state: Pick<UnifiedAnalysisState, "reuseRetrofits" | "existingTests">, changedFilesAbs: string[]): Promise<RetrofitView[]>;
|
|
45
|
+
/**
|
|
46
|
+
* The report gate over the standing retrofits: every one needs an execution of its
|
|
47
|
+
* current bytes, or the report is refused. Returns the views (for the report rows)
|
|
48
|
+
* and, when any are unexecuted, the refusal text.
|
|
49
|
+
*/
|
|
50
|
+
export declare function retrofitGate(state: Pick<UnifiedAnalysisState, "reuseRetrofits" | "existingTests">, changedFilesAbs: string[], repoRoot: string): Promise<{
|
|
51
|
+
views: RetrofitView[];
|
|
52
|
+
refusal?: string;
|
|
53
|
+
}>;
|