@skyramp/mcp 0.4.2 → 0.4.3-rc.1
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/execution/wrapperConfig.d.ts +4 -0
- package/build/execution/wrapperConfig.js +14 -2
- package/build/prompts/code-reuse.d.ts +5 -1
- package/build/prompts/code-reuse.js +16 -10
- package/build/prompts/modularization/ui-test-modularization.js +9 -2
- package/build/prompts/pom-aware-code-reuse.d.ts +4 -1
- package/build/prompts/pom-aware-code-reuse.js +38 -6
- package/build/prompts/reuse-hand-off.d.ts +4 -0
- package/build/prompts/reuse-hand-off.js +10 -5
- package/build/prompts/shared-helper-policy.d.ts +5 -0
- package/build/prompts/shared-helper-policy.js +5 -0
- package/build/prompts/test-maintenance/driftAnalysisShared.js +2 -0
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +2 -1
- package/build/prompts/testbot/testbot-prompts.js +33 -16
- package/build/recommendation/fakeVerifyContext.d.ts +3 -0
- package/build/recommendation/fakeVerifyContext.js +16 -0
- package/build/recommendation/runVerifiers.js +2 -0
- package/build/recommendation/types.d.ts +12 -0
- package/build/recommendation/verifierContracts.d.ts +24 -2
- package/build/recommendation/verifierContracts.js +32 -4
- package/build/recommendation/verifiers/changedLinesClaimed.d.ts +17 -0
- package/build/recommendation/verifiers/changedLinesClaimed.js +88 -0
- package/build/recommendation/verifiers/expectedValueSourced.js +8 -0
- package/build/recommendation/verifiers/requirementSourced.js +141 -11
- package/build/services/TestGenerationService.js +4 -0
- package/build/tools/code-refactor/caller-gate.d.ts +105 -0
- package/build/tools/code-refactor/caller-gate.js +174 -0
- package/build/tools/code-refactor/changed-helper-report.d.ts +62 -0
- package/build/tools/code-refactor/changed-helper-report.js +114 -0
- package/build/tools/code-refactor/codeReuseTool.js +131 -62
- package/build/tools/code-refactor/enhanceAssertionsTool.js +2 -2
- package/build/tools/code-refactor/helper-callers.d.ts +65 -0
- package/build/tools/code-refactor/helper-callers.js +85 -0
- package/build/tools/code-refactor/modularizationTool.js +10 -0
- package/build/tools/code-refactor/pom-pass-state.d.ts +51 -0
- package/build/tools/code-refactor/pom-pass-state.js +115 -0
- package/build/tools/code-refactor/retrofit-state.d.ts +3 -2
- package/build/tools/code-refactor/retrofit-state.js +11 -12
- package/build/tools/code-refactor/reuse-outcome.d.ts +6 -0
- package/build/tools/code-refactor/reuse-record-store.d.ts +52 -0
- package/build/tools/code-refactor/reuse-record-store.js +126 -0
- package/build/tools/code-refactor/reuse-state.d.ts +18 -6
- package/build/tools/code-refactor/reuse-state.js +78 -131
- package/build/tools/code-refactor/utils-verify-gates.js +3 -3
- package/build/tools/code-refactor/verify-gates.d.ts +4 -0
- package/build/tools/code-refactor/verify-gates.js +8 -1
- package/build/tools/generate-tests/generateContractRestTool.js +7 -7
- package/build/tools/generate-tests/generateIntegrationRestTool.js +8 -8
- package/build/tools/generate-tests/generateUIRestTool.js +3 -3
- package/build/tools/submitReportTool.js +91 -29
- package/build/tools/test-management/actionsTool.js +7 -0
- package/build/tools/test-management/registerTestPlanTool.d.ts +1 -0
- package/build/tools/test-management/registerTestPlanTool.js +70 -22
- package/build/types/RepositoryAnalysis.d.ts +20 -20
- package/build/types/ReuseOutcome.d.ts +96 -0
- package/build/types/TestExecution.d.ts +11 -0
- package/build/types/TestExecution.js +19 -0
- package/build/types/index.d.ts +1 -1
- package/build/utils/branchDiff.d.ts +1 -1
- package/build/utils/branchDiff.js +1 -1
- package/build/utils/changedRuns.d.ts +13 -0
- package/build/utils/changedRuns.js +56 -0
- package/build/utils/featureFlags.d.ts +8 -10
- package/build/utils/featureFlags.js +23 -21
- package/build/utils/normalizeSkyrampImports.d.ts +4 -1
- package/build/utils/normalizeSkyrampImports.js +24 -17
- package/build/utils/pom-scope/ownership.d.ts +19 -0
- package/build/utils/pom-scope/ownership.js +31 -0
- package/build/utils/pom-scope/pom-files.d.ts +8 -0
- package/build/utils/pom-scope/pom-files.js +8 -0
- package/build/utils/pom-scope/scoring.js +13 -1
- package/build/utils/pom-scope/strip.d.ts +10 -0
- package/build/utils/pom-scope/strip.js +9 -0
- package/build/utils/pom-verify/verify.js +13 -2
- package/build/utils/reuseRouting.d.ts +30 -2
- package/build/utils/reuseRouting.js +36 -15
- package/build/utils/testDependencyPolicy.js +4 -16
- package/build/utils/utils-verify/action-sites.d.ts +2 -2
- package/build/utils/utils-verify/body-reach.d.ts +2 -2
- package/build/utils/utils-verify/body-reach.js +4 -1
- package/build/utils/utils-verify/call-sites.d.ts +16 -6
- package/build/utils/utils-verify/call-sites.js +12 -6
- package/build/utils/utils-verify/changed-helpers.d.ts +38 -0
- package/build/utils/utils-verify/changed-helpers.js +91 -0
- package/build/utils/utils-verify/head.d.ts +48 -0
- package/build/utils/utils-verify/head.js +115 -0
- package/build/utils/utils-verify/importers.d.ts +59 -10
- package/build/utils/utils-verify/importers.js +121 -31
- package/build/utils/utils-verify/in-house.js +26 -31
- package/build/utils/utils-verify/index.d.ts +3 -0
- package/build/utils/utils-verify/index.js +3 -0
- package/build/utils/utils-verify/language-spec.js +10 -2
- package/build/utils/utils-verify/locate.d.ts +3 -0
- package/build/utils/utils-verify/locate.js +6 -6
- package/build/utils/utils-verify/parse.d.ts +13 -1
- package/build/utils/utils-verify/parse.js +122 -49
- package/build/utils/utils-verify/retrofit-equivalence.d.ts +0 -5
- package/build/utils/utils-verify/retrofit-equivalence.js +2 -34
- package/build/utils/utils-verify/stage.js +4 -16
- package/build/utils/utils-verify/verify.d.ts +9 -3
- package/build/utils/utils-verify/verify.js +26 -12
- package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1 -0
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +1 -0
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +6 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +303 -150
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-LNgEKtdV.js → codeMirrorModule-Bou8kKzE.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-Bwr1eMKC.js → defaultSettingsView-DTp2-8Si.js} +3 -3
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.BAkLd5DX.js → index.ZYuEFHsi.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.BPopbasy.js → uiMode.GLqsx5cI.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/pollingRecorderSource.ts +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +2 -2
- package/plugin/prompts/code-reuse/hand-off.md +13 -1
- package/plugin/prompts/plan-tests.md +2 -2
- package/plugin/skills/enhance-assertions/reference/shared-rules.md +1 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as fsp from "fs/promises";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import { readFile, stat
|
|
4
|
-
import { realpathSync } from "fs";
|
|
3
|
+
import { readFile, stat } from "fs/promises";
|
|
5
4
|
import { execFile } from "child_process";
|
|
6
5
|
import { promisify } from "util";
|
|
7
6
|
import { logger } from "../../utils/logger.js";
|
|
8
|
-
import { resolveRunStatePath,
|
|
7
|
+
import { resolveRunStatePath, } from "../../utils/AnalysisStateManager.js";
|
|
8
|
+
import { isAbsentFromHead } from "../../utils/utils-verify/head.js";
|
|
9
9
|
import { verifyReuse, } from "../../utils/pom-verify/index.js";
|
|
10
10
|
import { buildSkipped, findUnparsableKeptInline, flaggedFrom, mergeFlagged, verificationFrom, ReuseVerificationOutcome, HelperVerificationOutcome, } from "./reuse-outcome.js";
|
|
11
11
|
import { verifyUtils, realpath, utilsSpecFor, countAssertions, hasPageErrorGuard, utilsSpecForFile, } from "../../utils/utils-verify/index.js";
|
|
@@ -18,6 +18,11 @@ const execFileAsync = promisify(execFile);
|
|
|
18
18
|
import { resolvePomCatalogPath } from "../../utils/pom-catalog.js";
|
|
19
19
|
import { parsePomCatalog, findMissedReuse, } from "../../utils/pom-catalog-parse.js";
|
|
20
20
|
import { ProgrammingLanguage } from "../../types/TestTypes.js";
|
|
21
|
+
import { canonPath, freshRecord, owns, readHandOff, readReuseRecord, readRunState, runStateWritable, samePath, updateReuseOutcome, } from "./reuse-record-store.js";
|
|
22
|
+
import { combineRederivations, pomFirstHandOff } from "./pom-pass-state.js";
|
|
23
|
+
/** The canonical path comparison the records use — declared in the store, re-exported
|
|
24
|
+
* here for the readers that always imported it from this module. */
|
|
25
|
+
export { samePath, canonPath } from "./reuse-record-store.js";
|
|
21
26
|
/**
|
|
22
27
|
* Persistence for the server-derived POM-reuse outcome, keyed by test-file
|
|
23
28
|
* basename (what `newTestsCreated[].fileName` carries, so
|
|
@@ -28,52 +33,6 @@ import { ProgrammingLanguage } from "../../types/TestTypes.js";
|
|
|
28
33
|
* file degrades to "no reuse summary in the report" — the same outcome as running
|
|
29
34
|
* outside a Testbot run, where there is no run state to record into.
|
|
30
35
|
*/
|
|
31
|
-
/**
|
|
32
|
-
* Read-modify-write one spec's outcome. `patch` receives the entry recorded so
|
|
33
|
-
* far, so a caller that needs prior state (the verify pass, which accumulates
|
|
34
|
-
* flagged members) uses the same path as one that does not.
|
|
35
|
-
*
|
|
36
|
-
* Fields the patch omits are preserved: the prompt-generation call knows the
|
|
37
|
-
* candidate count, each verify pass knows the counts and verdict.
|
|
38
|
-
*/
|
|
39
|
-
async function updateReuseOutcome(testFile, patch, explicitStateFile) {
|
|
40
|
-
const stateFile = resolveRunStatePath(explicitStateFile);
|
|
41
|
-
if (!stateFile)
|
|
42
|
-
return false;
|
|
43
|
-
try {
|
|
44
|
-
const manager = StateManager.fromStatePath(stateFile);
|
|
45
|
-
const data = await manager.readData();
|
|
46
|
-
if (!data)
|
|
47
|
-
return false;
|
|
48
|
-
const key = path.basename(testFile);
|
|
49
|
-
const existing = data.reuseOutcomes?.[key] ?? {};
|
|
50
|
-
await manager.appendData({
|
|
51
|
-
...data,
|
|
52
|
-
reuseOutcomes: {
|
|
53
|
-
...data.reuseOutcomes,
|
|
54
|
-
[key]: { ...existing, ...patch(existing) },
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
catch (err) {
|
|
60
|
-
logger.warning("Could not record POM-reuse outcome — report will omit the reuse summary", {
|
|
61
|
-
testFile,
|
|
62
|
-
error: String(err),
|
|
63
|
-
});
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
/** Read one spec's record, or `undefined` when there is no run state, no record, or the
|
|
68
|
-
* state file cannot be read. Shares `updateReuseOutcome`'s resolve-read-basename plumbing
|
|
69
|
-
* so the two cannot disagree about which key a spec maps to. */
|
|
70
|
-
async function readReuseRecord(testFile, explicitStateFile) {
|
|
71
|
-
const stateFile = resolveRunStatePath(explicitStateFile);
|
|
72
|
-
if (!stateFile)
|
|
73
|
-
return undefined;
|
|
74
|
-
const data = await StateManager.fromStatePath(stateFile).readData();
|
|
75
|
-
return data?.reuseOutcomes?.[path.basename(testFile)];
|
|
76
|
-
}
|
|
77
36
|
/**
|
|
78
37
|
* Record the tier-1 candidate count STEP 1 detected, plus the identity needed to
|
|
79
38
|
* re-derive the counts from the delivered spec later.
|
|
@@ -221,17 +180,6 @@ export async function recordUtilsReuseIssued(testFile, language, framework, test
|
|
|
221
180
|
},
|
|
222
181
|
}), explicitStateFile);
|
|
223
182
|
}
|
|
224
|
-
/** Every field cleared — the patch spread onto a record owned by another spec. */
|
|
225
|
-
function freshRecord() {
|
|
226
|
-
return {
|
|
227
|
-
candidatesDetected: undefined,
|
|
228
|
-
verification: undefined,
|
|
229
|
-
flagged: undefined,
|
|
230
|
-
language: undefined,
|
|
231
|
-
utils: undefined,
|
|
232
|
-
modularized: undefined,
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
183
|
/** A verify pass that threw. Recorded so the debt FAILS OPEN — a verifier that cannot
|
|
236
184
|
* run must not leave a debt nothing can pay — and cleared by the next successful pass. */
|
|
237
185
|
export async function recordUtilsVerifyError(testFile, explicitStateFile) {
|
|
@@ -239,20 +187,6 @@ export async function recordUtilsVerifyError(testFile, explicitStateFile) {
|
|
|
239
187
|
? { utils: { ...existing.utils, verifyError: true } }
|
|
240
188
|
: {}, explicitStateFile);
|
|
241
189
|
}
|
|
242
|
-
/** Whether `record` describes `testFile` (or nobody yet). Basename-keyed run state
|
|
243
|
-
* means two specs can share one record; only the owner's history carries over. */
|
|
244
|
-
function owns(record, testFile) {
|
|
245
|
-
return !record.testFilePath || samePath(record.testFilePath, testFile);
|
|
246
|
-
}
|
|
247
|
-
/** Canonical comparison: a record written through a symlinked path (macOS /var →
|
|
248
|
-
* /private/var) must still be recognised as this spec's when checked by realpath. */
|
|
249
|
-
export function samePath(a, b) {
|
|
250
|
-
return canon(a) === canon(b);
|
|
251
|
-
}
|
|
252
|
-
/** Exported for retrofit-state: the same canonicalisation the records use. */
|
|
253
|
-
export function canonPath(p) {
|
|
254
|
-
return canon(p);
|
|
255
|
-
}
|
|
256
190
|
/**
|
|
257
191
|
* The label a report row shows for a set of utils files: each file's path relative
|
|
258
192
|
* to its repository root, posix-separated, joined with `, `.
|
|
@@ -273,7 +207,7 @@ function repoRelativeLabel(file) {
|
|
|
273
207
|
return path.basename(file);
|
|
274
208
|
// Both sides canonical: git reports the REAL root, and a path through a symlink
|
|
275
209
|
// (macOS /tmp → /private/tmp) would otherwise relativise to `../../…`.
|
|
276
|
-
const rel = path.relative(
|
|
210
|
+
const rel = path.relative(canonPath(root), canonPath(file));
|
|
277
211
|
// Outside the root means a leading `..` SEGMENT — not any name starting with two
|
|
278
212
|
// dots (`..generated/SkyrampUtils.py` is a valid in-repository path).
|
|
279
213
|
const outside = rel === ".." || rel.startsWith(`..${path.sep}`);
|
|
@@ -281,14 +215,6 @@ function repoRelativeLabel(file) {
|
|
|
281
215
|
return path.basename(file);
|
|
282
216
|
return rel.split(path.sep).join("/");
|
|
283
217
|
}
|
|
284
|
-
function canon(p) {
|
|
285
|
-
try {
|
|
286
|
-
return realpathSync(p);
|
|
287
|
-
}
|
|
288
|
-
catch {
|
|
289
|
-
return path.resolve(p);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
218
|
/** Verdict of one utils verify pass. The verdict alone is stored — counts and the
|
|
293
219
|
* file list are re-derived from the delivered files at report time. */
|
|
294
220
|
export async function recordUtilsVerifyOutcome(testFile, r, language, framework, testType, explicitStateFile,
|
|
@@ -333,20 +259,6 @@ inHouseDeclared) {
|
|
|
333
259
|
};
|
|
334
260
|
}, explicitStateFile);
|
|
335
261
|
}
|
|
336
|
-
/** Whether the run state file can be written — the precondition for any debt this
|
|
337
|
-
* module holds, since only a write clears it. */
|
|
338
|
-
async function runStateWritable(explicitStateFile) {
|
|
339
|
-
const stateFile = resolveRunStatePath(explicitStateFile);
|
|
340
|
-
if (!stateFile)
|
|
341
|
-
return false;
|
|
342
|
-
try {
|
|
343
|
-
await access(stateFile, fsConstants.W_OK);
|
|
344
|
-
return true;
|
|
345
|
-
}
|
|
346
|
-
catch {
|
|
347
|
-
return false;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
262
|
/** `skyramp_modularization` was called for this spec. Ownership on the write, like
|
|
351
263
|
* the utils debt: a same-basename spec elsewhere does not inherit the mark. */
|
|
352
264
|
export async function recordModularized(testFile, explicitStateFile) {
|
|
@@ -399,14 +311,11 @@ export async function utilsBaselineFor(testFile, explicitStateFile) {
|
|
|
399
311
|
*/
|
|
400
312
|
export async function pendingModularization(testFile, explicitStateFile) {
|
|
401
313
|
try {
|
|
402
|
-
const
|
|
403
|
-
if (!
|
|
404
|
-
return undefined;
|
|
405
|
-
const data = await StateManager.fromStatePath(stateFile).readData();
|
|
406
|
-
const handOff = data?.reuseHandOffs?.[await realpath(testFile)];
|
|
407
|
-
if (!handOff)
|
|
314
|
+
const read = await readHandOff(testFile, explicitStateFile);
|
|
315
|
+
if (!read?.handOff)
|
|
408
316
|
return undefined;
|
|
409
|
-
const
|
|
317
|
+
const { data, handOff } = read;
|
|
318
|
+
const record = data.reuseOutcomes?.[path.basename(testFile)];
|
|
410
319
|
if (record && !owns(record, testFile))
|
|
411
320
|
return undefined;
|
|
412
321
|
if (record?.modularized)
|
|
@@ -506,14 +415,11 @@ export async function recordReuseHandOff(files, testType, language, framework, e
|
|
|
506
415
|
// nothing on the reuse side records for it — so no hand-off is owed either.
|
|
507
416
|
if (!utilsSpecFor(language))
|
|
508
417
|
return;
|
|
509
|
-
const stateFile = resolveRunStatePath(explicitStateFile);
|
|
510
|
-
if (!stateFile)
|
|
511
|
-
return;
|
|
512
418
|
try {
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
if (!data)
|
|
419
|
+
const state = await readRunState(explicitStateFile);
|
|
420
|
+
if (!state)
|
|
516
421
|
return;
|
|
422
|
+
const { manager, data } = state;
|
|
517
423
|
// A REGENERATE marks its file as maintenance before the generation call it
|
|
518
424
|
// prescribes records the same file as generation. The rewrite is still the
|
|
519
425
|
// maintenance of a file at HEAD, so the origin is never demoted.
|
|
@@ -567,9 +473,7 @@ async function isNewInRepo(file) {
|
|
|
567
473
|
// Only "absent from HEAD" (or no HEAD yet) means new. Anything else — git missing,
|
|
568
474
|
// a corrupt object store, a permission error — is git not answering, and the
|
|
569
475
|
// documented answer to that is false: never block, never accuse, on a malfunction.
|
|
570
|
-
|
|
571
|
-
const text = `${e.stderr ?? ""} ${String(err)}`;
|
|
572
|
-
if (/does not exist|Not a valid object name|bad revision|exists on disk, but not in/i.test(text))
|
|
476
|
+
if (isAbsentFromHead(err))
|
|
573
477
|
return true;
|
|
574
478
|
logger.warning("Could not determine whether a file is new in the repository — treating it as pre-existing", {
|
|
575
479
|
file,
|
|
@@ -596,17 +500,14 @@ async function isNewInRepo(file) {
|
|
|
596
500
|
*/
|
|
597
501
|
export async function pendingReuseChain(testFile, explicitStateFile, testType) {
|
|
598
502
|
try {
|
|
599
|
-
const stateFile = resolveRunStatePath(explicitStateFile);
|
|
600
|
-
if (!stateFile)
|
|
601
|
-
return undefined;
|
|
602
|
-
const data = await StateManager.fromStatePath(stateFile).readData();
|
|
603
503
|
// Keyed by the GENERATED FILE: generation snapshots its outputDir before and after
|
|
604
504
|
// the codegen call and records exactly the files it wrote, so a test generated
|
|
605
505
|
// without codeReuse into the same directory owes nothing, and neither does a
|
|
606
506
|
// pre-existing test beside it.
|
|
607
|
-
const
|
|
608
|
-
if (!handOff)
|
|
507
|
+
const read = await readHandOff(testFile, explicitStateFile);
|
|
508
|
+
if (!read?.handOff)
|
|
609
509
|
return undefined;
|
|
510
|
+
const { data, handOff } = read;
|
|
610
511
|
// A caller that knows the file's type and disagrees with the hand-off is exempt;
|
|
611
512
|
// an unknown type still owes (the hand-off is authoritative for this file).
|
|
612
513
|
if (testType && testType !== handOff.testType)
|
|
@@ -619,7 +520,7 @@ export async function pendingReuseChain(testFile, explicitStateFile, testType) {
|
|
|
619
520
|
// and execute.
|
|
620
521
|
if (record && !owns(record, testFile))
|
|
621
522
|
return undefined;
|
|
622
|
-
if (record && reuseRan(record))
|
|
523
|
+
if (record && reuseRan(record, pomFirstHandOff(handOff)))
|
|
623
524
|
return undefined;
|
|
624
525
|
const maintained = handOff.source === "maintenance";
|
|
625
526
|
if (!maintained && !(await isNewInRepo(testFile)))
|
|
@@ -674,7 +575,7 @@ handOffs) {
|
|
|
674
575
|
if (record) {
|
|
675
576
|
if (!owns(record, file))
|
|
676
577
|
continue; // foreign basename — fail open for this file
|
|
677
|
-
if (reuseRan(record))
|
|
578
|
+
if (reuseRan(record, pomFirstHandOff(h)))
|
|
678
579
|
return undefined;
|
|
679
580
|
}
|
|
680
581
|
try {
|
|
@@ -693,8 +594,12 @@ handOffs) {
|
|
|
693
594
|
}
|
|
694
595
|
/** Whether a record shows the reuse tool ran for its spec: the utils path issued its
|
|
695
596
|
* instructions, or the POM path recorded detection/verification. A modularization mark
|
|
696
|
-
* alone, or a verify verdict recorded without a prior discovery, is NOT reuse.
|
|
697
|
-
|
|
597
|
+
* alone, or a verify verdict recorded without a prior discovery, is NOT reuse. When the
|
|
598
|
+
* page-object pass ran first (`pomFirst`), its verdict is only the first half of the
|
|
599
|
+
* chain: the pair it owes is paid by the utils path's instructions alone. */
|
|
600
|
+
function reuseRan(record, pomFirst) {
|
|
601
|
+
if (pomFirst)
|
|
602
|
+
return record.utils?.issued === true;
|
|
698
603
|
return (record.utils?.issued === true ||
|
|
699
604
|
record.candidatesDetected !== undefined ||
|
|
700
605
|
record.verification !== undefined);
|
|
@@ -826,6 +731,13 @@ async function rederiveHelperOutcome(record, carriedAllows) {
|
|
|
826
731
|
...(r.inlineCallSites.length > 0
|
|
827
732
|
? { siblingInlineCallSites: r.inlineCallSites.length }
|
|
828
733
|
: {}),
|
|
734
|
+
// Helpers nothing imports — the code a deleted test leaves behind for the
|
|
735
|
+
// customer to maintain. Reported, never removed and never gated: the caller
|
|
736
|
+
// list behind it is a lower bound, and removing a helper that is still called
|
|
737
|
+
// is far worse than leaving a dead one. When the walk could not read every
|
|
738
|
+
// file beside the module it reports NO orphan and says so, rather than let an
|
|
739
|
+
// unreadable directory read as "nothing imports this".
|
|
740
|
+
...orphanFields(r),
|
|
829
741
|
verification: r.ok
|
|
830
742
|
? HelperVerificationOutcome.Passed
|
|
831
743
|
: HelperVerificationOutcome.Failed,
|
|
@@ -836,6 +748,22 @@ async function rederiveHelperOutcome(record, carriedAllows) {
|
|
|
836
748
|
blocking,
|
|
837
749
|
};
|
|
838
750
|
}
|
|
751
|
+
/** The orphan fields of a report row: the helpers nothing imports, or the statement
|
|
752
|
+
* that the walk could not tell. Exactly one of the two, and neither when the walk ran
|
|
753
|
+
* and found none. Exported for its tests — the derivation is the whole behaviour. */
|
|
754
|
+
export function orphanFields(r) {
|
|
755
|
+
if (!r.importersComplete)
|
|
756
|
+
return { orphanScanIncomplete: true };
|
|
757
|
+
const orphans = r.singleImporters.filter((h) => h.importers.length === 0);
|
|
758
|
+
if (orphans.length === 0)
|
|
759
|
+
return {};
|
|
760
|
+
return {
|
|
761
|
+
orphanedHelpers: orphans.map((h) => ({
|
|
762
|
+
utilsFile: utilsFileLabel([h.file]),
|
|
763
|
+
helper: h.helper,
|
|
764
|
+
})),
|
|
765
|
+
};
|
|
766
|
+
}
|
|
839
767
|
/**
|
|
840
768
|
* The reason behind a failed shared-helper verdict, one entry per blocking kind in
|
|
841
769
|
* the order the verifier found them. The verify pass had these in hand and dropped
|
|
@@ -948,18 +876,36 @@ export async function deriveReuseFromDelivered(testFile, language) {
|
|
|
948
876
|
};
|
|
949
877
|
}
|
|
950
878
|
export async function rederiveReuse(record, carriedAllows) {
|
|
951
|
-
|
|
952
|
-
//
|
|
953
|
-
//
|
|
879
|
+
// A SkyrampUtils-path record is re-derived from the utils file. With both reuse
|
|
880
|
+
// settings on, the same spec also went through the page-object pass first, and its
|
|
881
|
+
// row carries both halves: the page-object fields and `helpers`/`inHouse`.
|
|
954
882
|
if (record.utils) {
|
|
883
|
+
let helper;
|
|
955
884
|
try {
|
|
956
|
-
|
|
885
|
+
helper = await rederiveHelperOutcome(record, carriedAllows);
|
|
957
886
|
}
|
|
958
887
|
catch (err) {
|
|
959
|
-
logger.warning("Could not re-derive the shared-helper outcome from the delivered files — omitting it rather than reporting a stale verdict", { testFile: testFilePath, error: String(err) });
|
|
960
|
-
|
|
888
|
+
logger.warning("Could not re-derive the shared-helper outcome from the delivered files — omitting it rather than reporting a stale verdict", { testFile: record.testFilePath, error: String(err) });
|
|
889
|
+
helper = { outcome: undefined };
|
|
961
890
|
}
|
|
891
|
+
if (!record.pomPassFinished)
|
|
892
|
+
return helper;
|
|
893
|
+
return combineRederivations(await rederivePomOutcome(record, carriedAllows, true), helper, (carriedAllows ?? []).some((c) => c.keptInline !== undefined), record.testFilePath
|
|
894
|
+
? utilsVerifyCall(record, record.testFilePath)
|
|
895
|
+
: undefined);
|
|
962
896
|
}
|
|
897
|
+
return rederivePomOutcome(record, carriedAllows);
|
|
898
|
+
}
|
|
899
|
+
/** The page-object half of a row, re-derived from the delivered spec. `composed`
|
|
900
|
+
* says the spec also took the SkyrampUtils flow after this pass: the report-time
|
|
901
|
+
* gates then split candidates by owner exactly as the live ones did. */
|
|
902
|
+
async function rederivePomOutcome(record, carriedAllows, composed = false) {
|
|
903
|
+
const { candidatesDetected, verification, flagged, testFilePath, language } = record;
|
|
904
|
+
// No page-object layer was found: that detection-time fact is the whole half. A
|
|
905
|
+
// composed spec whose SkyrampUtils flow wrote its identity must not be measured
|
|
906
|
+
// into page-object counts it never had.
|
|
907
|
+
if (composed && verification === ReuseVerificationOutcome.SkippedNoPom)
|
|
908
|
+
return { outcome: { candidatesDetected, verification } };
|
|
963
909
|
// No spec identity was ever recorded, so there is nothing to measure — only the
|
|
964
910
|
// detection-time facts, which no spec change can invalidate. Reached by the
|
|
965
911
|
// no-POM-layer path; a run that detected candidates always records the identity
|
|
@@ -971,7 +917,7 @@ export async function rederiveReuse(record, carriedAllows) {
|
|
|
971
917
|
const specContent = await readFile(testFilePath, "utf8");
|
|
972
918
|
const carriedKeptInline = (carriedAllows ?? []).filter((c) => c.keptInline !== undefined);
|
|
973
919
|
const classification = { covered: new Set(), ran: false };
|
|
974
|
-
const blocking = await pomBlockingVerdict(testFilePath, language, r, carriedKeptInline.map((c) => c.keptInline), classification);
|
|
920
|
+
const blocking = await pomBlockingVerdict(testFilePath, language, r, carriedKeptInline.map((c) => c.keptInline), classification, composed);
|
|
975
921
|
// Diagnose the file the report is actually built from. A marker the zero-reuse
|
|
976
922
|
// gate accepts but this cannot parse satisfies the escape hatch while its
|
|
977
923
|
// decline never reaches the report — silent signal loss is the failure class
|
|
@@ -1071,7 +1017,7 @@ async function pomBlockingVerdict(testFile, language, r, carriedKeptInline,
|
|
|
1071
1017
|
/** Filled with what the zero-reuse gate did — see `zeroReuseGateFailure`. `ran`
|
|
1072
1018
|
* stays false on every path that returns before the candidate walk, including the
|
|
1073
1019
|
* unknown language and the infra-gate precedence below. */
|
|
1074
|
-
classification) {
|
|
1020
|
+
classification, composed) {
|
|
1075
1021
|
// The record's language was written from the reuse tool's validated `language`
|
|
1076
1022
|
// param, but the record type keeps it a string: a value outside the enum means a
|
|
1077
1023
|
// check that cannot run, and a check that cannot run does not block.
|
|
@@ -1084,6 +1030,7 @@ classification) {
|
|
|
1084
1030
|
testFile,
|
|
1085
1031
|
language: language,
|
|
1086
1032
|
framework: "playwright",
|
|
1033
|
+
composed,
|
|
1087
1034
|
};
|
|
1088
1035
|
let infra;
|
|
1089
1036
|
let zero;
|
|
@@ -36,7 +36,7 @@ export function formatUtilsVerifyText(r, ctx = {}) {
|
|
|
36
36
|
return ((unchecked.length > 0
|
|
37
37
|
? `VERIFICATION PASSED — no Skyramp utils file; this test imports ${unchecked.length} module${unchecked.length === 1 ? "" : "s"} the pass read and does not check: ${unchecked
|
|
38
38
|
.map((f) => path.basename(f))
|
|
39
|
-
.join(", ")} — the repository's own, tracked without a Skyramp header. The in-house resolve and additive checks did not run: ${r.inHouseChecksSkipped ?? "they run for a UI test with
|
|
39
|
+
.join(", ")} — the repository's own, tracked without a Skyramp header. The in-house resolve and additive checks did not run: ${r.inHouseChecksSkipped ?? "they run for a UI test in TypeScript or JavaScript with utils reuse on"}. Continue.`
|
|
40
40
|
: "VERIFICATION PASSED — no shared utils file is written or imported for this test; nothing to verify.") +
|
|
41
41
|
tail +
|
|
42
42
|
unextractedDuplicatesAdvisory(r));
|
|
@@ -286,11 +286,11 @@ function singleImporterAdvisory(r) {
|
|
|
286
286
|
const single = r.singleImporters;
|
|
287
287
|
if (single.length === 0 || r.helpers === 0)
|
|
288
288
|
return "";
|
|
289
|
-
const dead = single.filter((h) => h.importers === 0);
|
|
289
|
+
const dead = single.filter((h) => h.importers.length === 0);
|
|
290
290
|
const lines = ["", ""];
|
|
291
291
|
if (dead.length > 0)
|
|
292
292
|
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.");
|
|
293
|
-
const once = single.filter((h) => h.importers === 1);
|
|
293
|
+
const once = single.filter((h) => h.importers.length === 1);
|
|
294
294
|
if (once.length === 0)
|
|
295
295
|
return lines.join("\n");
|
|
296
296
|
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.");
|
|
@@ -14,6 +14,10 @@ import { languageSchema } from "../../types/TestTypes.js";
|
|
|
14
14
|
export type VerifyTarget = Pick<z.infer<typeof languageSchema>, "language" | "framework"> & {
|
|
15
15
|
testFile: string;
|
|
16
16
|
testType?: string;
|
|
17
|
+
/** Both reuse settings on for this target (the route's `composed`): the in-house
|
|
18
|
+
* path owns free-function modules, so only a class module is a candidate this
|
|
19
|
+
* pass skipped. Stated by the caller, which resolved the route; never inferred. */
|
|
20
|
+
composed: boolean;
|
|
17
21
|
};
|
|
18
22
|
/** Renders a `VerifyResult` (from `verifyReuse`) as the text report returned to the agent. */
|
|
19
23
|
export declare function formatVerifyReport(r: VerifyResult): string;
|
|
@@ -2,6 +2,7 @@ import { readFile } from "fs/promises";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { logger } from "../../utils/logger.js";
|
|
4
4
|
import { isPomAwareTarget } from "../../utils/reuseRouting.js";
|
|
5
|
+
import { pageObjectCandidates } from "../../utils/pom-scope/ownership.js";
|
|
5
6
|
import { selectScopedPoms, } from "../../utils/pom-scope/index.js";
|
|
6
7
|
import { stripComments, stripStrings } from "../../utils/pom-scope/strip.js";
|
|
7
8
|
import { escapeRegExp } from "../../utils/regex.js";
|
|
@@ -174,10 +175,16 @@ classification = {
|
|
|
174
175
|
return undefined;
|
|
175
176
|
try {
|
|
176
177
|
const specContent = await readFile(params.testFile, "utf8");
|
|
177
|
-
const
|
|
178
|
+
const scoped = await selectScopedPoms({
|
|
178
179
|
testFile: params.testFile,
|
|
179
180
|
specContent,
|
|
180
181
|
});
|
|
182
|
+
const { diagnostics } = scoped;
|
|
183
|
+
// Both settings on: a free-function module is the in-house path's, so it is not
|
|
184
|
+
// a candidate this pass skipped — the same split discovery applied.
|
|
185
|
+
const tier1 = params.composed
|
|
186
|
+
? await pageObjectCandidates(scoped.tier1)
|
|
187
|
+
: scoped.tier1;
|
|
181
188
|
// Only fire on candidates found via conventional POM dirs (globs) — the selector-grep
|
|
182
189
|
// discovery fallback also matches ordinary in-repo frontend source (app components
|
|
183
190
|
// legitimately contain the spec's test-ids by construction), which is not evidence of
|
|
@@ -399,14 +399,14 @@ const CHAINING_SECTION = CONSUMER_MODE_ENABLED
|
|
|
399
399
|
- \`parentStatusCode\`: expected HTTP status code for each parent provisioning call (e.g. \`{"id": "201"}\`). Not allowed with \`skipProvisionParents\`.`;
|
|
400
400
|
export function registerContractTestTool(server) {
|
|
401
401
|
server.registerTool(TOOL_NAME, {
|
|
402
|
-
description: `${getPersonaPrefix()}Before calling this tool,
|
|
402
|
+
description: `${getPersonaPrefix()}Before calling this tool, verify these test inputs and requirements against the codebase, workspace config, API schema, or user input:
|
|
403
403
|
1. The endpoint URL and HTTP method being tested
|
|
404
|
-
2.
|
|
405
|
-
3. Assertion
|
|
406
|
-
a. Echo-back fields:
|
|
407
|
-
b. Computed fields:
|
|
408
|
-
c. Test data isolation:
|
|
409
|
-
d. Edge cases and business logic bugs: if \`skyramp_analyze_changes\` already ran and returned recommendations for this endpoint,
|
|
404
|
+
2. For a nested resource (URL contains a path parameter like \`{id}\`, \`{flow_id}\`, etc.), use the request body to provision the parent, or use skipProvisionParents when it cannot be provisioned.
|
|
405
|
+
3. Assertion requirements:
|
|
406
|
+
a. Echo-back fields: assert response fields returned unchanged from the request body (e.g. name, email, status) with the exact sent value, not just not-null.
|
|
407
|
+
b. Computed fields: assert numeric fields derived from other values (e.g. total_amount, discount_amount) with the formula, never a hardcoded number.
|
|
408
|
+
c. Test data isolation: create the test's own resources via parentRequestData. All IDs must come from provisioned resources, never hardcoded.
|
|
409
|
+
d. Edge cases and business logic bugs: if \`skyramp_analyze_changes\` already ran and returned recommendations for this endpoint, map each flagged boundary condition, invalid-input behavior, or business logic risk to a targeted assertion in this test.
|
|
410
410
|
4. Each required parameter and what value it will take, with source (workspace config / diff / schema / user input)
|
|
411
411
|
NEVER use a hardcoded ID (UUID or integer) as a path parameter value. If a real resource ID is needed and cannot be provisioned, use skipProvisionParents instead.
|
|
412
412
|
|
|
@@ -89,15 +89,15 @@ ${enhanceAssertionsStep("integration", "generated test file")}
|
|
|
89
89
|
const TOOL_NAME = TOOL_INTEGRATION_TEST_GENERATION;
|
|
90
90
|
export function registerIntegrationTestTool(server) {
|
|
91
91
|
server.registerTool(TOOL_NAME, {
|
|
92
|
-
description: `${getPersonaPrefix()}Before calling this tool,
|
|
92
|
+
description: `${getPersonaPrefix()}Before calling this tool, verify these test inputs and requirements against the codebase, workspace config, API schema, or user input:
|
|
93
93
|
1. The endpoint URL(s) and HTTP method(s) involved in this multi-step workflow
|
|
94
|
-
2.
|
|
95
|
-
3. Assertion
|
|
96
|
-
a. Echo-back fields:
|
|
97
|
-
b. Computed fields:
|
|
98
|
-
c. Chaining:
|
|
99
|
-
d. Edge cases and business logic bugs: if \`skyramp_analyze_changes\` already ran and returned recommendations for these endpoints,
|
|
100
|
-
4. Test data isolation:
|
|
94
|
+
2. The requested test exercises a multi-step workflow.
|
|
95
|
+
3. Assertion requirements for each step:
|
|
96
|
+
a. Echo-back fields: assert response fields returned unchanged from the request body with the exact sent value, not just not-null.
|
|
97
|
+
b. Computed fields: assert derived numeric fields (e.g. total_amount = price × quantity) with the formula, never hardcoded numbers.
|
|
98
|
+
c. Chaining: use the relevant JSON path in step N's response for IDs in step N+1 (e.g. POST /orders response → order_id → GET /orders/{order_id}). Every ID must be dynamically chained — no hardcoded IDs anywhere in the test.
|
|
99
|
+
d. Edge cases and business logic bugs: if \`skyramp_analyze_changes\` already ran and returned recommendations for these endpoints, map each flagged boundary condition, invalid-input behavior, or business logic risk to a targeted assertion in this test.
|
|
100
|
+
4. Test data isolation: create all prerequisite resources within the test setup, without depending on pre-existing database state.
|
|
101
101
|
If any required parameter cannot be determined without guessing, STOP and ask the user before calling the tool.
|
|
102
102
|
|
|
103
103
|
---
|
|
@@ -101,10 +101,10 @@ export const uiTestSchema = {
|
|
|
101
101
|
};
|
|
102
102
|
export function registerUITestTool(server) {
|
|
103
103
|
server.registerTool(TOOL_NAME, {
|
|
104
|
-
description: `${getPersonaPrefix()}Before calling this tool,
|
|
104
|
+
description: `${getPersonaPrefix()}Before calling this tool, verify these test inputs against the Playwright trace, codebase, and filesystem:
|
|
105
105
|
1. The user-facing flow(s) captured in the Playwright trace (pages visited, actions taken)
|
|
106
|
-
2.
|
|
107
|
-
3.
|
|
106
|
+
2. The intended behavior is covered by the recorded Playwright interactions.
|
|
107
|
+
3. The expected page content, element state, or navigation results the test must assert
|
|
108
108
|
4. The absolute path to the trace zip and the output directory, with source confirmation
|
|
109
109
|
If the trace zip path cannot be confirmed as an absolute path to an existing file, STOP and re-export before calling this tool.
|
|
110
110
|
|