Package not found. Please check the package name and try again.
@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,218 @@
|
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { promisify } from "util";
|
|
5
|
+
import { logger } from "../logger.js";
|
|
6
|
+
import { detectGitRoot } from "../gitStaging.js";
|
|
7
|
+
import { blankCommentsAndStrings, parseUtilsFile } from "./parse.js";
|
|
8
|
+
import { normalizePath } from "./language-spec.js";
|
|
9
|
+
import { callText, hasSchemaCheck, requestFields, REQUEST_CALL_RE, SCHEMA_SCAN_LINES, shapeKeys, } from "./call-sites.js";
|
|
10
|
+
import { escapeRegExp } from "../regex.js";
|
|
11
|
+
const exec = promisify(execFile);
|
|
12
|
+
/**
|
|
13
|
+
* Whether a retrofit changed the request the rewired test sends.
|
|
14
|
+
*
|
|
15
|
+
* STEP 5c edits a PRE-EXISTING test — code the customer already owns and whose
|
|
16
|
+
* behaviour nothing in this run chose — to call a shared helper instead of its own
|
|
17
|
+
* inline request block. That is only safe if the helper sends the same request. It
|
|
18
|
+
* usually does, because the helper was extracted from an identical block; but nothing
|
|
19
|
+
* checked it, and a helper that hardcodes a field the block never sent (or omits one
|
|
20
|
+
* it did) silently changes what the customer's test exercises. The failure is quiet:
|
|
21
|
+
* the test still runs, and a red result reads like any other red.
|
|
22
|
+
*
|
|
23
|
+
* The advisory path already refuses to OFFER such a site (see `substitutable` in
|
|
24
|
+
* call-sites.ts). This is the same question asked after the fact, about the edit that
|
|
25
|
+
* was actually made, and the rule is stricter: offering tolerates a site with an extra
|
|
26
|
+
* field, because extending the helper with a defaulted parameter is the sanctioned
|
|
27
|
+
* move; a completed retrofit must leave the request byte-equivalent in shape, so the
|
|
28
|
+
* field sets must match exactly in both directions.
|
|
29
|
+
*
|
|
30
|
+
* LIMIT: this compares the request's SHAPE — which fields and options it carries —
|
|
31
|
+
* not the values passed. A baseline sending `status: "draft"` rewired to
|
|
32
|
+
* `createPost(..., "published")` has the same field set and is not reported. Value
|
|
33
|
+
* equivalence needs argument-to-parameter binding, default resolution and expression
|
|
34
|
+
* normalisation; a wrong answer here REFUSES the report, so it is deliberately left
|
|
35
|
+
* out rather than approximated. The value case is also visible in the rewired file's
|
|
36
|
+
* own diff, where a hardcoded helper field is not.
|
|
37
|
+
*
|
|
38
|
+
* Compares against the file's committed baseline (`git show HEAD:<path>`), which is
|
|
39
|
+
* the only record of what the block sent before the edit. Fails OPEN — an untracked
|
|
40
|
+
* file, a detached worktree, or an unreadable baseline yields no mismatches rather
|
|
41
|
+
* than a refusal the agent cannot act on.
|
|
42
|
+
*/
|
|
43
|
+
export async function retrofitMismatches(retrofitFile, utilsFiles, spec) {
|
|
44
|
+
const baseline = await committedBaseline(retrofitFile);
|
|
45
|
+
if (baseline === undefined)
|
|
46
|
+
return [];
|
|
47
|
+
let delivered;
|
|
48
|
+
try {
|
|
49
|
+
delivered = await fs.promises.readFile(retrofitFile, "utf8");
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
const byRoute = new Map();
|
|
55
|
+
for (const utilsFile of utilsFiles) {
|
|
56
|
+
let content;
|
|
57
|
+
try {
|
|
58
|
+
content = await fs.promises.readFile(utilsFile, "utf8");
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const helpers = parseUtilsFile(content, spec);
|
|
64
|
+
// Positions come from a comment/string-blanked copy (offsets preserved), so a
|
|
65
|
+
// commented-out or quoted request is never read as a real one; the text itself
|
|
66
|
+
// is taken from the original.
|
|
67
|
+
const scan = blankCommentsAndStrings(content, spec);
|
|
68
|
+
for (const m of scan.matchAll(new RegExp(REQUEST_CALL_RE))) {
|
|
69
|
+
const start = m.index ?? 0;
|
|
70
|
+
const line = content.slice(0, start).split("\n").length;
|
|
71
|
+
const owner = helpers.find((h) => h.line <= line && line <= h.endLine);
|
|
72
|
+
if (!owner?.method || !owner.normalizedPath)
|
|
73
|
+
continue;
|
|
74
|
+
const call = callText(content, start + m[0].length - 1);
|
|
75
|
+
const route = `${owner.method} ${owner.normalizedPath}`;
|
|
76
|
+
const list = byRoute.get(route) ?? [];
|
|
77
|
+
// One helper may issue several requests; judge it by its first for this route.
|
|
78
|
+
if (!list.some((h) => h.helper === owner.name))
|
|
79
|
+
list.push({
|
|
80
|
+
helper: owner.name,
|
|
81
|
+
keys: shapeKeys(call),
|
|
82
|
+
fields: requestFields(content, call, start),
|
|
83
|
+
schemaChecked: hasSchemaCheck(content, owner.line, owner.endLine),
|
|
84
|
+
});
|
|
85
|
+
byRoute.set(route, list);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (byRoute.size === 0)
|
|
89
|
+
return [];
|
|
90
|
+
// The requests the file used to make itself, before the edit.
|
|
91
|
+
const baselineByRoute = new Map();
|
|
92
|
+
const ambiguous = new Set();
|
|
93
|
+
const baselineScan = blankCommentsAndStrings(baseline, spec);
|
|
94
|
+
for (const m of baselineScan.matchAll(new RegExp(REQUEST_CALL_RE))) {
|
|
95
|
+
const start = m.index ?? 0;
|
|
96
|
+
const line = baseline.slice(0, start).split("\n").length;
|
|
97
|
+
const call = callText(baseline, start + m[0].length - 1);
|
|
98
|
+
const method = spec.methodRe.exec(call)?.[1]?.toUpperCase();
|
|
99
|
+
const p = spec.pathRe.exec(call)?.[1];
|
|
100
|
+
if (!method || !p)
|
|
101
|
+
continue;
|
|
102
|
+
const key = `${method} ${normalizePath(p)}`;
|
|
103
|
+
const seen = baselineByRoute.get(key);
|
|
104
|
+
const here = {
|
|
105
|
+
keys: shapeKeys(call),
|
|
106
|
+
fields: requestFields(baseline, call, start),
|
|
107
|
+
schemaChecked: hasSchemaCheck(baseline, line, line + SCHEMA_SCAN_LINES),
|
|
108
|
+
};
|
|
109
|
+
if (!seen) {
|
|
110
|
+
baselineByRoute.set(key, here);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
// The same route more than once: only one of them was replaced, and nothing here
|
|
114
|
+
// says which. Identical blocks compare the same either way; DIFFERING ones are
|
|
115
|
+
// ambiguous, so the route is dropped rather than judged against a block that may
|
|
116
|
+
// not be the one the helper took over. Occurrence-level attribution would need to
|
|
117
|
+
// know which block disappeared, and getting that wrong refuses a safe edit.
|
|
118
|
+
if (!sameSet(seen.fields, here.fields) || !sameSet(seen.keys, here.keys))
|
|
119
|
+
ambiguous.add(key);
|
|
120
|
+
}
|
|
121
|
+
for (const key of ambiguous)
|
|
122
|
+
baselineByRoute.delete(key);
|
|
123
|
+
if (baselineByRoute.size === 0)
|
|
124
|
+
return [];
|
|
125
|
+
const calls = blankCommentsAndStrings(delivered, spec);
|
|
126
|
+
const out = [];
|
|
127
|
+
for (const [route, helpers] of byRoute) {
|
|
128
|
+
const before = baselineByRoute.get(route);
|
|
129
|
+
if (!before)
|
|
130
|
+
continue; // the file never made this request; nothing was replaced
|
|
131
|
+
for (const helper of helpers) {
|
|
132
|
+
// Only a helper this file actually calls now can have replaced anything —
|
|
133
|
+
// under whatever local name it was imported as (`import create_order as mk`).
|
|
134
|
+
const local = localBindings(calls, helper.helper);
|
|
135
|
+
if (!local.some((n) => new RegExp(`\\b${escapeRegExp(n)}\\s*\\(`).test(calls)))
|
|
136
|
+
continue;
|
|
137
|
+
const added = [...helper.fields].filter((f) => !before.fields.has(f));
|
|
138
|
+
const dropped = [...before.fields].filter((f) => !helper.fields.has(f));
|
|
139
|
+
const reasons = [];
|
|
140
|
+
// Non-liftable call options (query_params, timeout, …): the advisory compares
|
|
141
|
+
// these before offering a site, so the completed edit must be held to them too.
|
|
142
|
+
const optAdded = [...helper.keys].filter((k) => !before.keys.has(k));
|
|
143
|
+
const optDropped = [...before.keys].filter((k) => !helper.keys.has(k));
|
|
144
|
+
if (optAdded.length > 0 || optDropped.length > 0)
|
|
145
|
+
reasons.push(`the request options changed (${[
|
|
146
|
+
...optAdded.map((k) => `+\`${k}\``),
|
|
147
|
+
...optDropped.map((k) => `-\`${k}\``),
|
|
148
|
+
].join(", ")})`);
|
|
149
|
+
if (added.length > 0)
|
|
150
|
+
reasons.push(`it now also sends ${added.map((f) => `\`${f}\``).join(", ")}`);
|
|
151
|
+
if (dropped.length > 0)
|
|
152
|
+
reasons.push(`it no longer sends ${dropped.map((f) => `\`${f}\``).join(", ")}`);
|
|
153
|
+
if (before.schemaChecked && !helper.schemaChecked)
|
|
154
|
+
reasons.push("the response-schema check the block carried is gone");
|
|
155
|
+
if (reasons.length === 0)
|
|
156
|
+
continue;
|
|
157
|
+
const [method, ...rest] = route.split(" ");
|
|
158
|
+
out.push({
|
|
159
|
+
file: retrofitFile,
|
|
160
|
+
helper: helper.helper,
|
|
161
|
+
method,
|
|
162
|
+
path: rest.join(" "),
|
|
163
|
+
detail: reasons.join("; "),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The local names a module's export can be called by in this file: the export's own
|
|
171
|
+
* name, plus any `import X as Y` / `{ X as Y }` alias. A retrofit that imported the
|
|
172
|
+
* helper under an alias would otherwise look uncalled and fail open.
|
|
173
|
+
*/
|
|
174
|
+
function localBindings(content, exported) {
|
|
175
|
+
const esc = escapeRegExp(exported);
|
|
176
|
+
const names = new Set([exported]);
|
|
177
|
+
// Python `from M import x as y`; TS `import { x as y }` / `{ x: y } = require(...)`.
|
|
178
|
+
for (const re of [
|
|
179
|
+
new RegExp(`\\b${esc}\\s+as\\s+([A-Za-z_$][\\w$]*)`, "g"),
|
|
180
|
+
new RegExp(`\\b${esc}\\s*:\\s*([A-Za-z_$][\\w$]*)`, "g"),
|
|
181
|
+
])
|
|
182
|
+
for (const m of content.matchAll(re))
|
|
183
|
+
names.add(m[1]);
|
|
184
|
+
return [...names];
|
|
185
|
+
}
|
|
186
|
+
function sameSet(a, b) {
|
|
187
|
+
if (a.size !== b.size)
|
|
188
|
+
return false;
|
|
189
|
+
for (const v of a)
|
|
190
|
+
if (!b.has(v))
|
|
191
|
+
return false;
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
/** The file's content at HEAD, or undefined when there is nothing to compare against
|
|
195
|
+
* (untracked, no git root, or git unavailable). Never throws. */
|
|
196
|
+
async function committedBaseline(file) {
|
|
197
|
+
const root = detectGitRoot(file);
|
|
198
|
+
if (!root)
|
|
199
|
+
return undefined;
|
|
200
|
+
const rel = path.relative(root, file);
|
|
201
|
+
if (!rel || rel.startsWith(".."))
|
|
202
|
+
return undefined;
|
|
203
|
+
try {
|
|
204
|
+
const { stdout } = await exec("git", ["show", `HEAD:${rel}`], {
|
|
205
|
+
cwd: root,
|
|
206
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
207
|
+
});
|
|
208
|
+
return stdout;
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
// A file added by this run has no HEAD version — the common case, not an error.
|
|
212
|
+
logger.debug("No committed baseline for a retrofit; skipping equivalence", {
|
|
213
|
+
file,
|
|
214
|
+
error: String(err),
|
|
215
|
+
});
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
export interface StagingResult {
|
|
17
17
|
/** Files that reached the index. */
|
|
18
18
|
staged: string[];
|
|
19
|
+
/** Utils files located for the test (absolute), staged or not. */
|
|
20
|
+
utilsFiles: string[];
|
|
21
|
+
/** Pre-existing generated siblings this run edited to import from those files
|
|
22
|
+
* (absolute) — the caller records them as retrofits so the report must disclose
|
|
23
|
+
* them. */
|
|
24
|
+
retrofits: string[];
|
|
19
25
|
/** Utils files that `git add` refused (gitignored, outside the repo, git failure) —
|
|
20
26
|
* the tests importing them will fail in the output commit, so the verify pass
|
|
21
27
|
* treats a non-empty list as a failed verification. */
|
|
@@ -12,7 +12,9 @@ const execFileAsync = promisify(execFile);
|
|
|
12
12
|
export async function stageUtilsArtifacts(testFile, language) {
|
|
13
13
|
const staged = [];
|
|
14
14
|
const failed = [];
|
|
15
|
-
const
|
|
15
|
+
const utilsFilesOut = [];
|
|
16
|
+
const retrofits = [];
|
|
17
|
+
const result = { staged, failed, utilsFiles: utilsFilesOut, retrofits };
|
|
16
18
|
// Outside a testbot run stageGeneratedPaths is a no-op; report nothing as staged
|
|
17
19
|
// rather than a list of files nothing touched.
|
|
18
20
|
if (!isTestbotEnabled())
|
|
@@ -46,7 +48,9 @@ export async function stageUtilsArtifacts(testFile, language) {
|
|
|
46
48
|
// file, so without this the sibling's edit drops out of the output commit while the
|
|
47
49
|
// utils change lands — the same class of hole as the unstaged utils file.
|
|
48
50
|
if (files.length > 0) {
|
|
51
|
+
utilsFilesOut.push(...files);
|
|
49
52
|
for (const sibling of await modifiedSiblingsImporting(testFile, files, language)) {
|
|
53
|
+
retrofits.push(sibling);
|
|
50
54
|
try {
|
|
51
55
|
await stageGeneratedPaths(sibling);
|
|
52
56
|
staged.push(sibling);
|
|
@@ -69,7 +73,13 @@ async function modifiedSiblingsImporting(testFile, utilsFiles, language) {
|
|
|
69
73
|
let modified;
|
|
70
74
|
try {
|
|
71
75
|
// `-z`: unquoted, NUL-separated — git C-quotes non-ASCII paths otherwise.
|
|
72
|
-
const { stdout } = await execFileAsync("git",
|
|
76
|
+
const { stdout } = await execFileAsync("git",
|
|
77
|
+
// Against HEAD, modified only: a test THIS run created is staged at discovery, so
|
|
78
|
+
// its later STEP 5b edits show in a plain `git diff` and it would masquerade as a
|
|
79
|
+
// pre-existing sibling (observed: the report gate then demanded an execution no
|
|
80
|
+
// record could satisfy, and the agent un-shared the helpers to escape). Files
|
|
81
|
+
// added since HEAD are excluded; they are staged by their own tool calls.
|
|
82
|
+
["diff", "-z", "--name-only", "--diff-filter=M", "HEAD"], {
|
|
73
83
|
cwd: root,
|
|
74
84
|
encoding: "utf8",
|
|
75
85
|
});
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import { type InlineCallSite } from "./call-sites.js";
|
|
2
|
-
import { type UtilsViolationKind } from "./allow.js";
|
|
2
|
+
import { type UtilsAdvisoryKind, type UtilsViolationKind } from "./allow.js";
|
|
3
|
+
import { type SingleImporterHelper } from "./importers.js";
|
|
3
4
|
import { type HelperFamilySpec, type UtilsLanguageSpec } from "./language-spec.js";
|
|
4
5
|
export interface UtilsViolation {
|
|
5
6
|
kind: UtilsViolationKind;
|
|
6
7
|
/** Absolute path of the utils file. */
|
|
7
8
|
file: string;
|
|
9
|
+
/** The helper the finding is about — or, for a spec-level finding (assertion-loss,
|
|
10
|
+
* guard-removed), the spec's marker token: its basename with non-word characters
|
|
11
|
+
* as `_` (`orders.spec.ts` → `orders_spec_ts`), what the allow grammar accepts. */
|
|
8
12
|
helper: string;
|
|
9
13
|
line: number;
|
|
10
14
|
detail: string;
|
|
11
15
|
}
|
|
16
|
+
/** A violation-shaped finding whose kind may also be advisory-only. Only the
|
|
17
|
+
* `advisories` list carries these; `violations` never does. */
|
|
18
|
+
export type UtilsFinding = Omit<UtilsViolation, "kind"> & {
|
|
19
|
+
kind: UtilsViolationKind | UtilsAdvisoryKind;
|
|
20
|
+
};
|
|
12
21
|
export interface UtilsVerifyResult {
|
|
13
22
|
/** No unallowed violations and no malformed allow markers. */
|
|
14
23
|
ok: boolean;
|
|
@@ -24,18 +33,23 @@ export interface UtilsVerifyResult {
|
|
|
24
33
|
/** Findings that inform but never fail the pass. `scenario-name` lives here: a naming
|
|
25
34
|
* heuristic is a quality signal, not a broken shared helper, and blocking a
|
|
26
35
|
* customer's run on a false positive of it is the worse outcome. */
|
|
27
|
-
advisories:
|
|
36
|
+
advisories: UtilsFinding[];
|
|
28
37
|
/** Allow-marker lines the grammar cannot read, per file. */
|
|
29
38
|
malformedAllows: {
|
|
30
39
|
file: string;
|
|
31
40
|
line: string;
|
|
32
41
|
}[];
|
|
33
42
|
spec?: UtilsLanguageSpec;
|
|
34
|
-
/**
|
|
35
|
-
|
|
43
|
+
/** The test type's invariant set — the FALLBACK for helpers that wrap no request;
|
|
44
|
+
* a helper that issues an SDK request is judged by the api rules regardless. See
|
|
45
|
+
* helperFamilyOf. */
|
|
46
|
+
fallbackFamily: HelperFamilySpec;
|
|
36
47
|
/** Advisory (does not affect `ok`): inline request calls in sibling generated tests
|
|
37
48
|
* that a utils helper already wraps — see findSiblingInlineCallSites. API family only. */
|
|
38
49
|
inlineCallSites: InlineCallSite[];
|
|
50
|
+
/** Helpers imported by 0 or 1 delivered test (SKYR-4276 A6). Advisory: a first run
|
|
51
|
+
* seeds every helper single-use by design; the count is the trend to watch. */
|
|
52
|
+
singleImporters: SingleImporterHelper[];
|
|
39
53
|
}
|
|
40
54
|
export interface VerifyUtilsParams {
|
|
41
55
|
testFile: string;
|
|
@@ -43,6 +57,13 @@ export interface VerifyUtilsParams {
|
|
|
43
57
|
/** Selects the helper family (api vs browser). Absent → api. */
|
|
44
58
|
testType?: string;
|
|
45
59
|
cwd?: string;
|
|
60
|
+
/** The spec at the modularization hand-out (SKYR-4276 A7/A8). When present, the
|
|
61
|
+
* delivered spec plus its imported helpers must hold at least as many assertions,
|
|
62
|
+
* and a page-error guard that was there must still be. */
|
|
63
|
+
baseline?: {
|
|
64
|
+
assertions: number;
|
|
65
|
+
pageErrorGuard: boolean;
|
|
66
|
+
};
|
|
46
67
|
}
|
|
47
68
|
/**
|
|
48
69
|
* Deterministic post-reuse check of the shared utils file(s) a spec depends on.
|
|
@@ -61,3 +82,32 @@ export interface VerifyUtilsParams {
|
|
|
61
82
|
* except to count what the spec imports.
|
|
62
83
|
*/
|
|
63
84
|
export declare function verifyUtils(params: VerifyUtilsParams): Promise<UtilsVerifyResult>;
|
|
85
|
+
/**
|
|
86
|
+
* Python only. The import form that works here is not the one that works in the
|
|
87
|
+
* customer's repo, and the difference is invisible in this run.
|
|
88
|
+
*
|
|
89
|
+
* pytest imports a test inside a package directory (one carrying `__init__.py`) from
|
|
90
|
+
* that package's first NON-package ancestor, so a bare `from SkyrampUtils import …`
|
|
91
|
+
* resolves only when the runner puts the test's own directory on `sys.path`. The
|
|
92
|
+
* Skyramp executor flattens its bundle and drops the `__init__.py` files, so it does;
|
|
93
|
+
* the customer running `pytest` from their repo root does not, and the test dies at
|
|
94
|
+
* collection with `ModuleNotFoundError`, taking the whole file with it. Measured on
|
|
95
|
+
* prefect (`tests/`, `tests/server/`, `tests/server/api/` all packages) and present in
|
|
96
|
+
* five of the six real Python repos in the eval corpus — the majority shape, not an
|
|
97
|
+
* edge case.
|
|
98
|
+
*
|
|
99
|
+
* The relative form is not simply the answer: under the flattened bundle it fails with
|
|
100
|
+
* `attempted relative import with no known parent package`. The two layouts want
|
|
101
|
+
* opposite forms, so a package directory must carry BOTH — the relative import first,
|
|
102
|
+
* falling back to the bare one:
|
|
103
|
+
*
|
|
104
|
+
* try:
|
|
105
|
+
* from .SkyrampUtils import create_order
|
|
106
|
+
* except ImportError: # flattened test bundle (no package context)
|
|
107
|
+
* from SkyrampUtils import create_order
|
|
108
|
+
*
|
|
109
|
+
* Verified in both layouts. Outside a package the bare form alone is correct, and the
|
|
110
|
+
* fallback would be noise, so it is required only where `__init__.py` sits beside the
|
|
111
|
+
* spec — which is exactly where pytest changes its rule.
|
|
112
|
+
*/
|
|
113
|
+
export declare function importFormViolations(testFile: string, specContent: string, utilsFiles: string[]): UtilsViolation[];
|