@skyramp/mcp 0.3.5 → 0.3.6-rc.2.ac20
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/adapters/jestAdapter.js +3 -0
- package/build/adapters/mochaAdapter.js +2 -0
- package/build/adapters/playwrightAdapter.js +3 -0
- package/build/adapters/pytestAdapter.js +12 -0
- package/build/playwright/registerPlaywrightTools.js +92 -30
- package/build/playwright/traceRecordingPrompt.d.ts +6 -0
- package/build/playwright/traceRecordingPrompt.js +6 -2
- package/build/prompts/code-reuse.d.ts +1 -2
- package/build/prompts/code-reuse.js +182 -77
- package/build/prompts/modularization/integration-test-modularization.d.ts +2 -0
- package/build/prompts/modularization/integration-test-modularization.js +83 -41
- package/build/prompts/modularization/render.d.ts +18 -0
- package/build/prompts/modularization/render.js +12 -0
- package/build/prompts/modularization/ui-test-modularization.d.ts +3 -1
- package/build/prompts/modularization/ui-test-modularization.js +89 -47
- package/build/prompts/pom-aware-code-reuse.js +3 -1
- package/build/prompts/shared-helper-policy.d.ts +57 -0
- package/build/prompts/shared-helper-policy.js +135 -0
- package/build/prompts/test-recommendation/diffExecutionPlan.js +62 -56
- package/build/prompts/test-recommendation/fullRepoCatalog.js +19 -8
- package/build/prompts/test-recommendation/recommendationShared.d.ts +28 -6
- package/build/prompts/test-recommendation/recommendationShared.js +90 -16
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +22 -0
- package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +2 -2
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +3 -3
- package/build/prompts/testbot/testbot-prompts.js +81 -35
- package/build/recommendation/budgeters/shared.js +105 -27
- package/build/recommendation/discriminators.js +13 -2
- package/build/recommendation/planRanker.d.ts +6 -6
- package/build/recommendation/planRanker.js +6 -61
- package/build/services/AnalyticsService.d.ts +7 -0
- package/build/services/AnalyticsService.js +7 -1
- package/build/services/ModularizationService.js +1 -3
- package/build/services/TestDiscoveryService.d.ts +0 -2
- package/build/services/TestDiscoveryService.js +2 -37
- package/build/services/TestGenerationService.d.ts +16 -0
- package/build/services/TestGenerationService.js +86 -10
- package/build/services/containerEnv.js +13 -12
- package/build/tools/code-refactor/codeReuseTool.js +279 -93
- package/build/tools/code-refactor/enhance-state.d.ts +49 -0
- package/build/tools/code-refactor/enhance-state.js +109 -0
- package/build/tools/code-refactor/enhanceAssertionsTool.js +34 -1
- package/build/tools/code-refactor/modularizationTool.js +9 -2
- package/build/tools/code-refactor/reuse-outcome.d.ts +23 -1
- package/build/tools/code-refactor/reuse-outcome.js +14 -4
- package/build/tools/code-refactor/reuse-state.d.ts +127 -5
- package/build/tools/code-refactor/reuse-state.js +628 -16
- package/build/tools/code-refactor/utils-verify-gates.d.ts +26 -0
- package/build/tools/code-refactor/utils-verify-gates.js +100 -0
- package/build/tools/code-refactor/verify-gates.d.ts +2 -1
- package/build/tools/code-refactor/verify-gates.js +90 -25
- package/build/tools/executeSkyrampTestTool.d.ts +19 -0
- package/build/tools/executeSkyrampTestTool.js +158 -8
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +2 -2
- package/build/tools/generate-tests/generateE2ERestTool.js +16 -0
- package/build/tools/generate-tests/generateUIRestTool.d.ts +1 -0
- package/build/tools/generate-tests/generateUIRestTool.js +22 -0
- package/build/tools/generate-tests/scenarioLint.d.ts +2 -0
- package/build/tools/generate-tests/scenarioLint.js +127 -19
- package/build/tools/generate-tests/trace-reuse-guard.d.ts +20 -0
- package/build/tools/generate-tests/trace-reuse-guard.js +93 -0
- package/build/tools/runExistingTestsTool.d.ts +34 -2
- package/build/tools/runExistingTestsTool.js +104 -4
- package/build/tools/submitReportTool.d.ts +38 -38
- package/build/tools/submitReportTool.js +537 -120
- package/build/tools/test-management/analyzeChangesTool.d.ts +24 -1
- package/build/tools/test-management/analyzeChangesTool.js +71 -10
- package/build/tools/test-management/analyzeTestHealthTool.js +7 -7
- package/build/tools/test-management/registerTestPlanTool.d.ts +203 -0
- package/build/tools/test-management/registerTestPlanTool.js +70 -12
- package/build/types/ExternalTestExecution.d.ts +67 -1
- package/build/types/Recommendation.d.ts +34 -5
- package/build/types/RepositoryAnalysis.d.ts +133 -114
- package/build/types/RepositoryAnalysis.js +1 -1
- package/build/types/ReuseOutcome.d.ts +102 -6
- package/build/types/ReuseOutcome.js +16 -2
- package/build/types/TestRecommendation.js +21 -3
- package/build/types/TestTypes.js +14 -8
- package/build/types/TestbotReport.d.ts +10 -1
- package/build/types/index.d.ts +2 -2
- package/build/types/index.js +1 -1
- package/build/utils/AnalysisStateManager.d.ts +57 -1
- package/build/utils/AnalysisStateManager.js +54 -5
- package/build/utils/branchDiff.d.ts +10 -0
- package/build/utils/branchDiff.js +28 -0
- package/build/utils/changedRoutes.d.ts +29 -0
- package/build/utils/changedRoutes.js +87 -0
- package/build/utils/featureFlags.d.ts +21 -0
- package/build/utils/featureFlags.js +23 -0
- package/build/utils/frontendIntegration.js +34 -4
- package/build/utils/importerHop.d.ts +2 -8
- package/build/utils/importerHop.js +15 -53
- package/build/utils/pathMatching.d.ts +38 -0
- package/build/utils/pathMatching.js +71 -0
- package/build/utils/pathSignatures.d.ts +22 -0
- package/build/utils/pathSignatures.js +57 -0
- package/build/utils/planMatchKeys.d.ts +16 -3
- package/build/utils/planMatchKeys.js +26 -10
- package/build/utils/pluralization.d.ts +10 -0
- package/build/utils/pluralization.js +18 -0
- package/build/utils/pom-catalog-parse.d.ts +52 -0
- package/build/utils/pom-catalog-parse.js +141 -0
- package/build/utils/pom-scope/selector-extractor.d.ts +12 -0
- package/build/utils/pom-scope/selector-extractor.js +34 -8
- package/build/utils/pom-verify/verify.d.ts +6 -5
- package/build/utils/pom-verify/verify.js +8 -6
- package/build/utils/reportVerification.d.ts +64 -4
- package/build/utils/reportVerification.js +228 -3
- package/build/utils/reuseRouting.d.ts +3 -0
- package/build/utils/reuseRouting.js +50 -0
- package/build/utils/routeParsers.d.ts +2 -0
- package/build/utils/routeParsers.js +65 -8
- package/build/utils/scenarioDrafting.d.ts +1 -1
- package/build/utils/scenarioDrafting.js +57 -45
- package/build/utils/subjectEndpoints.d.ts +19 -0
- package/build/utils/subjectEndpoints.js +98 -0
- package/build/utils/testFileClassification.d.ts +11 -0
- package/build/utils/testFileClassification.js +47 -0
- package/build/utils/uiPageEnumerator.d.ts +45 -19
- package/build/utils/uiPageEnumerator.js +95 -51
- package/build/utils/utils-verify/allow.d.ts +16 -0
- package/build/utils/utils-verify/allow.js +68 -0
- package/build/utils/utils-verify/call-sites.d.ts +34 -0
- package/build/utils/utils-verify/call-sites.js +154 -0
- package/build/utils/utils-verify/index.d.ts +7 -0
- package/build/utils/utils-verify/index.js +7 -0
- package/build/utils/utils-verify/language-spec.d.ts +91 -0
- package/build/utils/utils-verify/language-spec.js +210 -0
- package/build/utils/utils-verify/locate.d.ts +39 -0
- package/build/utils/utils-verify/locate.js +199 -0
- package/build/utils/utils-verify/parse.d.ts +34 -0
- package/build/utils/utils-verify/parse.js +177 -0
- package/build/utils/utils-verify/stage.d.ts +24 -0
- package/build/utils/utils-verify/stage.js +107 -0
- package/build/utils/utils-verify/verify.d.ts +63 -0
- package/build/utils/utils-verify/verify.js +168 -0
- package/build/utils/utils.d.ts +3 -1
- package/build/utils/utils.js +3 -1
- package/build/workspace/workspace.d.ts +32 -32
- package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +9 -5
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +16 -0
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +2 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +115 -14
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +13 -1
- package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/htmlReport/index.html +27 -253
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-DtudTj_v.js → codeMirrorModule-DJMC4zNo.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BW82eAUI.js +196 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/index.html +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-FNMuBzX1.js → codeMirrorModule-CZfp96qZ.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-gpLo02E0.js +809 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.Bq1r1URj.js +2 -0
- 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 +5 -0
- 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/server/codegen/skyramp/jsonlReader.ts +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BpDwp16L.js +0 -422
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-Co9upU5h.js +0 -1035
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.DXNIQ_dx.js +0 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CIKB3XSv.js +0 -5
|
@@ -24,6 +24,7 @@ export declare const uiTestSchema: {
|
|
|
24
24
|
force: z.ZodDefault<z.ZodBoolean>;
|
|
25
25
|
codeReuse: z.ZodDefault<z.ZodBoolean>;
|
|
26
26
|
modularizeCode: z.ZodDefault<z.ZodDefault<z.ZodBoolean>>;
|
|
27
|
+
allowTraceReuse: z.ZodDefault<z.ZodBoolean>;
|
|
27
28
|
language: z.ZodNativeEnum<typeof import("../../types/TestTypes.js").ProgrammingLanguage>;
|
|
28
29
|
framework: z.ZodString;
|
|
29
30
|
};
|
|
@@ -5,6 +5,8 @@ import { TestGenerationService, } from "../../services/TestGenerationService.js"
|
|
|
5
5
|
import { normalizeLanguageParams, resolveParamAliases, } from "../../utils/normalizeParams.js";
|
|
6
6
|
import { getPersonaPrefix } from "../../prompts/personas.js";
|
|
7
7
|
import { isTestbotEnabled } from "../../utils/featureFlags.js";
|
|
8
|
+
import { toolError } from "../../utils/utils.js";
|
|
9
|
+
import { checkTraceReuse, recordTraceOutput } from "./trace-reuse-guard.js";
|
|
8
10
|
const TOOL_NAME = "skyramp_ui_test_generation";
|
|
9
11
|
export class UITestService extends TestGenerationService {
|
|
10
12
|
getTestType() {
|
|
@@ -32,6 +34,11 @@ export class UITestService extends TestGenerationService {
|
|
|
32
34
|
});
|
|
33
35
|
if (result.isError)
|
|
34
36
|
return result;
|
|
37
|
+
// On modularize-first flows the base hand-off sequences the whole post-gen
|
|
38
|
+
// chain (enhance → modularize → reuse); a second block here would instruct
|
|
39
|
+
// enhance twice and re-extract the consolidated helpers.
|
|
40
|
+
if (this.emitsModularizeFirstHandOff(params))
|
|
41
|
+
return result;
|
|
35
42
|
if (!params.enhanceAssertions && !params.modularizeCode)
|
|
36
43
|
return result;
|
|
37
44
|
const content = [...result.content];
|
|
@@ -83,6 +90,13 @@ export const uiTestSchema = {
|
|
|
83
90
|
force: baseSchema.shape.force,
|
|
84
91
|
codeReuse: codeRefactoringSchema.shape.codeReuse,
|
|
85
92
|
modularizeCode: codeRefactoringSchema.shape.modularizeCode.default(true),
|
|
93
|
+
allowTraceReuse: z
|
|
94
|
+
.boolean()
|
|
95
|
+
.default(false)
|
|
96
|
+
.describe("Explicit override for the trace-reuse guard. By default, generating a second differently-named spec " +
|
|
97
|
+
"from a trace zip that already produced one is rejected — one recording covers one scenario, and reusing " +
|
|
98
|
+
"it manufactures identical tests under names the recording never covered. Set true only when the same " +
|
|
99
|
+
"recording genuinely covers the new scenario end-to-end."),
|
|
86
100
|
};
|
|
87
101
|
export function registerUITestTool(server) {
|
|
88
102
|
server.registerTool(TOOL_NAME, {
|
|
@@ -123,8 +137,16 @@ This tells you exactly which frontend files changed so you record traces for the
|
|
|
123
137
|
zip: "playwrightInput",
|
|
124
138
|
});
|
|
125
139
|
normalizeLanguageParams(params);
|
|
140
|
+
const traceReuse = await checkTraceReuse(params.playwrightInput, params.output, params.outputDir, !!params.allowTraceReuse, "ui");
|
|
141
|
+
if (traceReuse.rejection)
|
|
142
|
+
return toolError(traceReuse.rejection);
|
|
126
143
|
const service = new UITestService();
|
|
127
144
|
const result = await service.generateTest(params);
|
|
145
|
+
// Register only what generation actually produced: recording at check
|
|
146
|
+
// time poisoned the registry on failures (name-validation rejections,
|
|
147
|
+
// codegen errors), refusing the corrected retry for reusing its own trace.
|
|
148
|
+
if (!result.isError && traceReuse.key && traceReuse.identity)
|
|
149
|
+
recordTraceOutput(traceReuse.key, traceReuse.identity);
|
|
128
150
|
AnalyticsService.pushTestGenerationToolEvent(TOOL_NAME, result, params).catch(() => {
|
|
129
151
|
// Silently ignore analytics errors
|
|
130
152
|
});
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* failure family. This lint checks the bait at write time:
|
|
10
10
|
*
|
|
11
11
|
* - a referenced value produced ONLY by a later step → error (step order)
|
|
12
|
+
* (with ONE remedy: reorder when that later step creates the referenced
|
|
13
|
+
* resource, else raise the entropy of its fabricated id — see below)
|
|
12
14
|
* - a reference produced by no prior step at all → warning (hardcode)
|
|
13
15
|
* - a reference produced by multiple prior steps → warning (ambiguous)
|
|
14
16
|
*
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* failure family. This lint checks the bait at write time:
|
|
10
10
|
*
|
|
11
11
|
* - a referenced value produced ONLY by a later step → error (step order)
|
|
12
|
+
* (with ONE remedy: reorder when that later step creates the referenced
|
|
13
|
+
* resource, else raise the entropy of its fabricated id — see below)
|
|
12
14
|
* - a reference produced by no prior step at all → warning (hardcode)
|
|
13
15
|
* - a reference produced by multiple prior steps → warning (ambiguous)
|
|
14
16
|
*
|
|
@@ -16,6 +18,7 @@
|
|
|
16
18
|
* fields with id-like keys, or keys resource-affine to a producing step's
|
|
17
19
|
* collection path) to keep noise down — mirroring the reader's own matching.
|
|
18
20
|
*/
|
|
21
|
+
import { singularize } from "../../utils/pluralization.js";
|
|
19
22
|
const UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
20
23
|
function isIdLikeKey(key) {
|
|
21
24
|
return /^(id|pk|uuid)$/i.test(key) || /_id$/i.test(key) || /[a-z]Id$/.test(key);
|
|
@@ -23,6 +26,14 @@ function isIdLikeKey(key) {
|
|
|
23
26
|
function isIdishSegment(seg) {
|
|
24
27
|
return /^\d+$/.test(seg) || UUID_RE.test(seg);
|
|
25
28
|
}
|
|
29
|
+
/** The collection an id path-segment hangs off: "orders" in /orders/1/confirm. */
|
|
30
|
+
function owningSegment(segs, idIndex) {
|
|
31
|
+
for (let i = idIndex - 1; i >= 0; i--) {
|
|
32
|
+
if (!isIdishSegment(segs[i]))
|
|
33
|
+
return segs[i].toLowerCase();
|
|
34
|
+
}
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
26
37
|
function lastStaticSegment(path) {
|
|
27
38
|
const segs = path.split("/").filter((s) => s !== "");
|
|
28
39
|
for (let i = segs.length - 1; i >= 0; i--) {
|
|
@@ -31,13 +42,45 @@ function lastStaticSegment(path) {
|
|
|
31
42
|
}
|
|
32
43
|
return "";
|
|
33
44
|
}
|
|
45
|
+
/** Strip the id suffix from a body key, accepting exactly the two spellings
|
|
46
|
+
* `isIdLikeKey` accepts and no others: `_id` case-insensitively (it reads
|
|
47
|
+
* `/_id$/i`, so `provider_ID` is a reference too) and a camel `Id` after a
|
|
48
|
+
* lower-case letter. Words that merely end in those letters (`uuid`, `valid`)
|
|
49
|
+
* keep their spelling — a blanket case-insensitive strip would turn `uuid`
|
|
50
|
+
* into `uu` and match any 4-character collection. */
|
|
51
|
+
function stripIdSuffix(key) {
|
|
52
|
+
if (/_id$/i.test(key))
|
|
53
|
+
return key.slice(0, -3);
|
|
54
|
+
if (/[a-z]Id$/.test(key))
|
|
55
|
+
return key.slice(0, -2);
|
|
56
|
+
return key;
|
|
57
|
+
}
|
|
34
58
|
/** "provider" ↔ "providers" style prefix affinity between a body key and a
|
|
35
59
|
* producing step's collection segment. Minimum overlap of 4 chars keeps
|
|
36
|
-
* short generic keys from matching everything.
|
|
60
|
+
* short generic keys from matching everything.
|
|
61
|
+
*
|
|
62
|
+
* The id suffix comes off first: `product_id` and `products` are prefixes of
|
|
63
|
+
* neither direction, and an FK is spelled with that suffix far more often than
|
|
64
|
+
* without, so leaving it on made the affinity fail on the commonest shape it
|
|
65
|
+
* exists to recognise — and a mis-ordered create then received the remedy
|
|
66
|
+
* meant for a fixed-position sub-action. Both sides are then singularized,
|
|
67
|
+
* because a prefix test cannot bridge an irregular plural: "category" is a
|
|
68
|
+
* prefix of neither "categories" nor the reverse. */
|
|
37
69
|
function keyResourceAffinity(key, segment) {
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
70
|
+
const rawKey = stripIdSuffix(key).toLowerCase();
|
|
71
|
+
const rawSegment = segment.toLowerCase();
|
|
72
|
+
const k = singularize(rawKey);
|
|
73
|
+
const s = singularize(rawSegment);
|
|
74
|
+
// Equal singular forms are the strongest affinity there is, so they do not
|
|
75
|
+
// need the length floor — and they must skip it, because singularizing first
|
|
76
|
+
// collapses a 4-letter plural under it (`tags` -> `tag`, and `jobs`, `orgs`,
|
|
77
|
+
// `keys`, `pets` alike). `tag_id` with a later `POST /tags` then read as a
|
|
78
|
+
// sub-action and took the remedy that ships a hardcoded value.
|
|
79
|
+
if (k === s)
|
|
80
|
+
return true;
|
|
81
|
+
// The floor reads the RAW words for the same reason: `cases` -> `cas` is a
|
|
82
|
+
// 3-character over-strip that would fail a comparison the raw pair passes.
|
|
83
|
+
if (rawKey.length < 4 || rawSegment.length < 4)
|
|
41
84
|
return false;
|
|
42
85
|
return k.startsWith(s) || s.startsWith(k);
|
|
43
86
|
}
|
|
@@ -85,27 +128,40 @@ export function lintScenarioChaining(steps) {
|
|
|
85
128
|
if (body === undefined)
|
|
86
129
|
return;
|
|
87
130
|
const segment = typeof step.path === "string" ? lastStaticSegment(step.path) : "";
|
|
131
|
+
const creates = (step.method ?? "").toUpperCase() === "POST";
|
|
132
|
+
const collectionCreate = creates &&
|
|
133
|
+
typeof step.path === "string" &&
|
|
134
|
+
!step.path.split("/").some((seg) => seg !== "" && isIdishSegment(seg));
|
|
88
135
|
walkPrimitives(body, "", (key, value) => {
|
|
89
136
|
if (!isIdLikeKey(key))
|
|
90
137
|
return;
|
|
91
138
|
const list = producers.get(value) ?? [];
|
|
92
|
-
|
|
93
|
-
|
|
139
|
+
const existing = list.find((p) => p.step === idx);
|
|
140
|
+
if (existing) {
|
|
141
|
+
if (!existing.keys.includes(key))
|
|
142
|
+
existing.keys.push(key);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
list.push({ step: idx, segment, keys: [key], creates, collectionCreate });
|
|
146
|
+
}
|
|
94
147
|
producers.set(value, list);
|
|
95
148
|
});
|
|
96
149
|
});
|
|
97
150
|
steps.forEach((step, idx) => {
|
|
98
151
|
// Collect this step's id-like references, one per distinct value, so a
|
|
99
152
|
// value referenced in both the path and the body (or repeated in an
|
|
100
|
-
// array) yields a single classification and a single message.
|
|
101
|
-
|
|
153
|
+
// array) yields a single classification and a single message. `resource`
|
|
154
|
+
// names the thing the reference points at ("provider", "orders") and only
|
|
155
|
+
// picks the remedy wording below — it never changes what is flagged.
|
|
156
|
+
const refs = new Map();
|
|
102
157
|
// Path segments that look like ids are always FK-ish (path params are ids).
|
|
103
158
|
if (typeof step.path === "string") {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
159
|
+
const segs = step.path.split("/").filter((s) => s !== "");
|
|
160
|
+
segs.forEach((seg, i) => {
|
|
161
|
+
if (!isIdishSegment(seg) || refs.has(seg))
|
|
162
|
+
return;
|
|
163
|
+
refs.set(seg, { desc: "path segment", resource: owningSegment(segs, i) });
|
|
164
|
+
});
|
|
109
165
|
}
|
|
110
166
|
// Body values: only id-like keys, or keys resource-affine to a producer.
|
|
111
167
|
const body = parseJsonObject(step.requestBody);
|
|
@@ -117,16 +173,44 @@ export function lintScenarioChaining(steps) {
|
|
|
117
173
|
const relevant = isIdLikeKey(key) || producerList.some((p) => keyResourceAffinity(key, p.segment));
|
|
118
174
|
if (!relevant)
|
|
119
175
|
return;
|
|
120
|
-
refs.set(value, `body field "${key}"
|
|
176
|
+
refs.set(value, { desc: `body field "${key}"`, resource: key, key });
|
|
121
177
|
});
|
|
122
178
|
}
|
|
123
|
-
for (const [value,
|
|
179
|
+
for (const [value, ref] of refs) {
|
|
180
|
+
const refDesc = ref.desc;
|
|
124
181
|
const all = producers.get(value) ?? [];
|
|
125
182
|
const earlier = all.filter((p) => p.step < idx);
|
|
126
183
|
// Strictly-later producers are a fixable ordering mistake; a producer in
|
|
127
184
|
// the SAME step (e.g. a create whose own response echoes the id) is just
|
|
128
185
|
// unchainable — Go only chains from strictly-prior steps.
|
|
129
186
|
const later = all.filter((p) => p.step > idx);
|
|
187
|
+
// An echo of the referencing field is not a producer. A sub-action
|
|
188
|
+
// response that mirrors the parent's child collection re-emits the very
|
|
189
|
+
// field the earlier step SENT — `items[].product_id` comes back on every
|
|
190
|
+
// confirm/ship/deliver — so no value of that field can clear the error,
|
|
191
|
+
// and the only escape is to delete the array from the fabricated
|
|
192
|
+
// response. Measured on run 32395388709: the agent did exactly that, and
|
|
193
|
+
// the assertion judge then docked 10 points for the missing array
|
|
194
|
+
// coverage. A DIFFERENT id key carrying the same value is still an error:
|
|
195
|
+
// that is the genuine `product_id: 1` vs `order_id: 1` ambiguity, where
|
|
196
|
+
// raising the fabricated id is both followable and correct.
|
|
197
|
+
// A later step that creates the very resource this reference points at.
|
|
198
|
+
// Found BEFORE the echo test, because a create is a genuine producer: a
|
|
199
|
+
// conventional `POST /orders` answering `{ order_id }` reuses the FK's own
|
|
200
|
+
// spelling and would otherwise satisfy the echo condition below, silently
|
|
201
|
+
// downgrading a real ordering mistake to a hardcoding warning.
|
|
202
|
+
// The second lookup catches an ALIASED foreign key. `customer_id` created
|
|
203
|
+
// by `POST /clients` shares no prefix with "clients", so affinity cannot
|
|
204
|
+
// see it, and the echo test below then downgraded a genuine ordering
|
|
205
|
+
// mistake to a warning. A collection create planting the referencing key
|
|
206
|
+
// is a producer whatever the key is called; a sub-action POST is excluded
|
|
207
|
+
// by `collectionCreate`, so the measured `/orders/{id}/confirm` echo is
|
|
208
|
+
// untouched.
|
|
209
|
+
const creator = later.find((p) => p.creates && keyResourceAffinity(ref.resource, p.segment)) ??
|
|
210
|
+
later.find((p) => p.collectionCreate && ref.key !== undefined && p.keys.includes(ref.key));
|
|
211
|
+
const echoesReferencingField = ref.key !== undefined &&
|
|
212
|
+
creator === undefined &&
|
|
213
|
+
later.every((p) => p.keys.every((k) => k === ref.key));
|
|
130
214
|
if (earlier.length > 0) {
|
|
131
215
|
if (earlier.length > 1) {
|
|
132
216
|
warnings.push(`Value "${value}" referenced by ${stepLabel(steps, idx)} is produced by multiple prior steps ` +
|
|
@@ -134,10 +218,34 @@ export function lintScenarioChaining(steps) {
|
|
|
134
218
|
`use distinct, high-entropy ids (e.g. fresh UUIDs) in fabricated responses.`);
|
|
135
219
|
}
|
|
136
220
|
}
|
|
137
|
-
else if (later.length > 0) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
221
|
+
else if (later.length > 0 && !echoesReferencingField) {
|
|
222
|
+
// Two causes need OPPOSITE fixes, and only one of them can be
|
|
223
|
+
// reordered. Naming both is worse than naming one: renumbering the
|
|
224
|
+
// later id ALWAYS clears the error because it deletes the producer, so
|
|
225
|
+
// an agent offered both picks it and the mis-ordered batch goes green
|
|
226
|
+
// while still hardcoding the value. Pick the remedy here instead.
|
|
227
|
+
if (creator) {
|
|
228
|
+
// The later step creates the very resource being referenced, so it
|
|
229
|
+
// is a real ordering mistake and the value is already correct.
|
|
230
|
+
errors.push(`${stepLabel(steps, idx)} references value "${value}" (${refDesc}) that is only produced by a ` +
|
|
231
|
+
`later ${stepLabel(steps, creator.step)} — that step creates the "${creator.segment}" this ` +
|
|
232
|
+
`reference points at, so reorder the batch to put step ${creator.step + 1} before ` +
|
|
233
|
+
`step ${idx + 1} and keep the value unchanged (the generated test can only chain from PRIOR steps).`);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
// The later step is a sub-action on a fixed rung of a state machine
|
|
237
|
+
// (confirm/ship/deliver). It cannot move, so the collision is the
|
|
238
|
+
// fabricated id being too guessable — raise its entropy.
|
|
239
|
+
const producer = later[0];
|
|
240
|
+
const shape = producer.segment
|
|
241
|
+
? `is a "${producer.segment}" sub-action, not a create of what this reference points at`
|
|
242
|
+
: `does not create what this reference points at`;
|
|
243
|
+
errors.push(`${stepLabel(steps, idx)} references value "${value}" (${refDesc}) that is only produced by a ` +
|
|
244
|
+
`later ${stepLabel(steps, producer.step)} — that step ${shape}, so its position is fixed and ` +
|
|
245
|
+
`reordering cannot fix this. Change the id in step ${producer.step + 1}'s fabricated ` +
|
|
246
|
+
`responseBody to a distinct high-entropy value (e.g. a fresh UUID or a large random integer) ` +
|
|
247
|
+
`so it stops colliding with "${value}" here.`);
|
|
248
|
+
}
|
|
141
249
|
}
|
|
142
250
|
else {
|
|
143
251
|
warnings.push(`${stepLabel(steps, idx)} ${refDesc} value "${value}" does not appear in any prior step's ` +
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Test-only: clear the in-process trace→outputs registry. */
|
|
2
|
+
export declare function resetTraceReuseGuard(): void;
|
|
3
|
+
export interface TraceReuseCheck {
|
|
4
|
+
/** Set when the call must be rejected (same zip, different output). */
|
|
5
|
+
rejection?: string;
|
|
6
|
+
/** Content hash of the zip — pass to recordTraceOutput on success. */
|
|
7
|
+
key?: string;
|
|
8
|
+
/** The resolved output identity — pass to recordTraceOutput on success. */
|
|
9
|
+
identity?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Read-only reuse check. Returns a rejection when this zip's content already
|
|
13
|
+
* generated a DIFFERENT output identity and `allowTraceReuse` is not set; on a
|
|
14
|
+
* clean check, returns the hash/identity for the caller to record after
|
|
15
|
+
* generation succeeds. Fails open (empty result) when the zip is unreadable —
|
|
16
|
+
* generation will surface its own error for that.
|
|
17
|
+
*/
|
|
18
|
+
export declare function checkTraceReuse(playwrightInput: string | undefined, output: string | undefined, outputDir: string | undefined, allowTraceReuse: boolean, scope: "ui" | "e2e"): Promise<TraceReuseCheck>;
|
|
19
|
+
/** Register a generated output for its trace. Call ONLY after generation succeeded. */
|
|
20
|
+
export declare function recordTraceOutput(key: string, identity: string): void;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import { createReadStream } from "fs";
|
|
3
|
+
import { pipeline } from "stream/promises";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
/**
|
|
6
|
+
* SKYR-4262 — trace-reuse guard. One recording captures one flow; generating
|
|
7
|
+
* several differently-named specs from the same trace zip produces byte-identical
|
|
8
|
+
* tests whose names claim scenarios the recording never covered (observed in
|
|
9
|
+
* production: one trace fanned into 4 specs, none asserting its named behavior).
|
|
10
|
+
* Keyed by CONTENT hash, not path, so re-exporting a genuinely new recording to
|
|
11
|
+
* the same zip path never trips the guard.
|
|
12
|
+
*
|
|
13
|
+
* Scope: this registry is per MCP-server process. That is narrower than "one
|
|
14
|
+
* testbot run" — testbot retries an agent up to 3 times, each attempt spawning a
|
|
15
|
+
* fresh agent CLI and therefore a fresh server (a retry also deletes the run's
|
|
16
|
+
* state files and restarts the flow, so the reset is coherent) — and broader
|
|
17
|
+
* than one recording session in a long-lived IDE/stdio server. Both are
|
|
18
|
+
* acceptable for this guard's job: reusing an IDENTICAL zip under a new name is
|
|
19
|
+
* the failure mode wherever it happens, and a fresh recording always hashes
|
|
20
|
+
* differently.
|
|
21
|
+
*
|
|
22
|
+
* Check/record are split: `checkTraceReuse` is read-only and runs BEFORE
|
|
23
|
+
* generation; `recordTraceOutput` runs only after generation SUCCEEDS.
|
|
24
|
+
* Registering at check time poisoned the registry on any generation failure —
|
|
25
|
+
* e.g. output-name validation rejects `a.ts` with "rename to a.spec.ts", and the
|
|
26
|
+
* corrected retry was then refused for reusing its own trace.
|
|
27
|
+
*/
|
|
28
|
+
const generatedFromTrace = new Map();
|
|
29
|
+
/** Test-only: clear the in-process trace→outputs registry. */
|
|
30
|
+
export function resetTraceReuseGuard() {
|
|
31
|
+
generatedFromTrace.clear();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The registered identity of one generated spec: the RESOLVED output path, so
|
|
35
|
+
* the same filename into two different outputDirs still counts as a second,
|
|
36
|
+
* differently-named spec. An omitted `output` (Skyramp picks a default name)
|
|
37
|
+
* maps to a per-directory sentinel: two default-named generations from the same
|
|
38
|
+
* zip into the same directory overwrite each other and are allowed, while
|
|
39
|
+
* mixing a named and an unnamed generation of the same zip is a fan-out and is
|
|
40
|
+
* not.
|
|
41
|
+
*/
|
|
42
|
+
function outputIdentity(output, outputDir) {
|
|
43
|
+
return path.resolve(outputDir ?? process.cwd(), output ?? "(default output name)");
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Read-only reuse check. Returns a rejection when this zip's content already
|
|
47
|
+
* generated a DIFFERENT output identity and `allowTraceReuse` is not set; on a
|
|
48
|
+
* clean check, returns the hash/identity for the caller to record after
|
|
49
|
+
* generation succeeds. Fails open (empty result) when the zip is unreadable —
|
|
50
|
+
* generation will surface its own error for that.
|
|
51
|
+
*/
|
|
52
|
+
export async function checkTraceReuse(playwrightInput, output, outputDir, allowTraceReuse, scope) {
|
|
53
|
+
if (!playwrightInput)
|
|
54
|
+
return {};
|
|
55
|
+
let key;
|
|
56
|
+
try {
|
|
57
|
+
// Stream the zip through the hash — trace zips carry full HARs and can be
|
|
58
|
+
// large, so buffering the whole file would spike the server's memory.
|
|
59
|
+
const hasher = createHash("sha256");
|
|
60
|
+
await pipeline(createReadStream(playwrightInput), hasher);
|
|
61
|
+
// Namespace by generator: a UI test and an E2E test from ONE recording are
|
|
62
|
+
// different artifacts (the e2e generator adds API correlation), not the
|
|
63
|
+
// byte-identical fan-out this guard exists to stop — only same-generator
|
|
64
|
+
// reuse of a zip is a duplicate.
|
|
65
|
+
key = `${scope}:${hasher.digest("hex")}`;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Unreadable zip fails in generation with its own error; the guard stays out of the way.
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
const identity = outputIdentity(output, outputDir);
|
|
72
|
+
const outputs = generatedFromTrace.get(key);
|
|
73
|
+
if (!allowTraceReuse && outputs && outputs.size > 0 && !outputs.has(identity)) {
|
|
74
|
+
const prior = [...outputs].map(p => path.basename(p)).join(", ");
|
|
75
|
+
return {
|
|
76
|
+
key,
|
|
77
|
+
identity,
|
|
78
|
+
rejection: `This trace zip already generated ${prior}. A recording captures ONE flow — generating a second, ` +
|
|
79
|
+
`differently-named spec (${path.basename(identity)}) from the same trace produces an identical test whose name claims a ` +
|
|
80
|
+
`scenario the recording never covered. Record a dedicated trace for this scenario with the browser_* tools ` +
|
|
81
|
+
`(including browser_assert on the behavior the test is named for) and skyramp_export_zip, then generate from ` +
|
|
82
|
+
`that. If this recording genuinely covers the scenario end-to-end, pass allowTraceReuse: true and be prepared ` +
|
|
83
|
+
`to differentiate the spec's assertions afterward via skyramp_enhance_assertions.`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return { key, identity };
|
|
87
|
+
}
|
|
88
|
+
/** Register a generated output for its trace. Call ONLY after generation succeeded. */
|
|
89
|
+
export function recordTraceOutput(key, identity) {
|
|
90
|
+
const outputs = generatedFromTrace.get(key) ?? new Set();
|
|
91
|
+
outputs.add(identity);
|
|
92
|
+
generatedFromTrace.set(key, outputs);
|
|
93
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { WorkspaceConfig } from "../workspace/workspace.js";
|
|
3
|
-
import { ParsedExternalRun } from "../types/ExternalTestExecution.js";
|
|
3
|
+
import { ExternalFileOutcome, ExternalTestResult, ParsedExternalRun } from "../types/ExternalTestExecution.js";
|
|
4
4
|
import { SUPPORTED_FRAMEWORKS } from "../workspace/frameworks.js";
|
|
5
5
|
/** Test-run lifecycle resolved from a service's workspace.yml runtimeDetails. */
|
|
6
6
|
export interface ResolvedRunConfig {
|
|
@@ -42,7 +42,9 @@ export interface RunExistingTestsParams {
|
|
|
42
42
|
workspacePath: string;
|
|
43
43
|
service?: string;
|
|
44
44
|
testSelectors?: string[];
|
|
45
|
-
|
|
45
|
+
/** Required: the only thing that marks a record as the pre-edit baseline or the
|
|
46
|
+
* post-edit re-run. A wrong label is read as fact by the report. */
|
|
47
|
+
mode: "confirm" | "verify";
|
|
46
48
|
project?: string;
|
|
47
49
|
stateFile?: string;
|
|
48
50
|
repository?: string;
|
|
@@ -128,6 +130,36 @@ export declare function aggregateRuns(parsed: ParsedExternalRun[]): ParsedExtern
|
|
|
128
130
|
*/
|
|
129
131
|
export declare function runExternalSuite(spec: ReaderSpec, cfg: ResolvedRunConfig, opts: PlaywrightRunOpts): Promise<ParsedExternalRun>;
|
|
130
132
|
export declare function runPlaywright(cfg: ResolvedRunConfig, opts: PlaywrightRunOpts): Promise<ParsedExternalRun>;
|
|
133
|
+
/**
|
|
134
|
+
* Reduce a run's flat result list to one outcome per spec file.
|
|
135
|
+
*
|
|
136
|
+
* This is the only place that knows BOTH what the framework reported and what the
|
|
137
|
+
* run asked it for, which is what `complete` needs. Deriving it later from the
|
|
138
|
+
* results alone is not possible: a file re-run for one test looks identical to a
|
|
139
|
+
* file whose other tests silently vanished.
|
|
140
|
+
*
|
|
141
|
+
* `partialFiles` are the files a `file::test title` selector narrowed to a subset of
|
|
142
|
+
* their tests — the one narrowing this tool applies itself, and so the only one it
|
|
143
|
+
* can state with certainty.
|
|
144
|
+
*/
|
|
145
|
+
export declare function summarizeFilesInRun(results: ExternalTestResult[], opts: {
|
|
146
|
+
partialFiles: Set<string>;
|
|
147
|
+
}): ExternalFileOutcome[];
|
|
148
|
+
/** Placeholder identity for a record that names no suite because none resolved.
|
|
149
|
+
* Deliberately not a valid suiteIdOf output, so it can never collide with one. */
|
|
150
|
+
export declare const NO_RUNNABLE_SUITE = "(no runnable suite)";
|
|
151
|
+
/** Stable identity for ONE configured suite. A service can declare several
|
|
152
|
+
* (workspace.yml testSuites), so its name alone collides — two suites under one
|
|
153
|
+
* service would share a slot and the later would mask the other instead of both
|
|
154
|
+
* counting. */
|
|
155
|
+
export declare function suiteIdOf(cfg: {
|
|
156
|
+
serviceName: string;
|
|
157
|
+
framework: string;
|
|
158
|
+
testRunCommand: string;
|
|
159
|
+
}): string;
|
|
160
|
+
/** The files a selector narrowed to individual tests. A bare `file` selector runs
|
|
161
|
+
* the whole file; `file::test title` does not. */
|
|
162
|
+
export declare function partialFilesFrom(selectors: string[]): Set<string>;
|
|
131
163
|
/**
|
|
132
164
|
* Orchestrates an external test run: resolve config → apply maxTests cap → run →
|
|
133
165
|
* persist a record to the state file. Pure of MCP/registration concerns and
|
|
@@ -440,6 +440,95 @@ const defaultDeps = {
|
|
|
440
440
|
},
|
|
441
441
|
now: () => new Date().toISOString(),
|
|
442
442
|
};
|
|
443
|
+
/** Worst outcome wins for a file. `error` (never reached its assertions) outranks
|
|
444
|
+
* `fail` so a collection failure is never reported as an assertion failure, and
|
|
445
|
+
* `skipped` sits below everything: it only wins when nothing else ran. */
|
|
446
|
+
const STATUS_RANK = {
|
|
447
|
+
skipped: 0,
|
|
448
|
+
pass: 1,
|
|
449
|
+
fail: 2,
|
|
450
|
+
error: 3,
|
|
451
|
+
};
|
|
452
|
+
/**
|
|
453
|
+
* Reduce a run's flat result list to one outcome per spec file.
|
|
454
|
+
*
|
|
455
|
+
* This is the only place that knows BOTH what the framework reported and what the
|
|
456
|
+
* run asked it for, which is what `complete` needs. Deriving it later from the
|
|
457
|
+
* results alone is not possible: a file re-run for one test looks identical to a
|
|
458
|
+
* file whose other tests silently vanished.
|
|
459
|
+
*
|
|
460
|
+
* `partialFiles` are the files a `file::test title` selector narrowed to a subset of
|
|
461
|
+
* their tests — the one narrowing this tool applies itself, and so the only one it
|
|
462
|
+
* can state with certainty.
|
|
463
|
+
*/
|
|
464
|
+
export function summarizeFilesInRun(results, opts) {
|
|
465
|
+
const byFile = new Map();
|
|
466
|
+
for (const r of results) {
|
|
467
|
+
if (typeof r.file !== "string" || r.file === "")
|
|
468
|
+
continue;
|
|
469
|
+
const key = r.absoluteFile ?? r.file;
|
|
470
|
+
byFile.set(key, [...(byFile.get(key) ?? []), r]);
|
|
471
|
+
}
|
|
472
|
+
return [...byFile.values()].map((group) => {
|
|
473
|
+
const counts = { pass: 0, fail: 0, error: 0, skipped: 0 };
|
|
474
|
+
for (const r of group)
|
|
475
|
+
counts[r.status] = (counts[r.status] ?? 0) + 1;
|
|
476
|
+
const status = group.reduce((a, b) => STATUS_RANK[b.status] > STATUS_RANK[a.status] ? b : a).status;
|
|
477
|
+
const first = group[0];
|
|
478
|
+
return {
|
|
479
|
+
file: first.file,
|
|
480
|
+
...(first.absoluteFile ? { absoluteFile: first.absoluteFile } : {}),
|
|
481
|
+
status,
|
|
482
|
+
counts,
|
|
483
|
+
complete: !isPartial(first, opts.partialFiles),
|
|
484
|
+
errors: group
|
|
485
|
+
.filter((r) => r.status === "fail" || r.status === "error")
|
|
486
|
+
.map((r) => `${r.testId}: ${r.message ?? r.status}`),
|
|
487
|
+
durationMs: group.reduce((sum, r) => sum + (r.durationMs ?? 0), 0),
|
|
488
|
+
};
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
/** A selector spelling and a result spelling need not agree — the selector is
|
|
492
|
+
* repo-relative, the result may be absolute or framework-root-relative — so match
|
|
493
|
+
* on a path-segment suffix in either direction. */
|
|
494
|
+
function isPartial(r, partialFiles) {
|
|
495
|
+
if (partialFiles.size === 0)
|
|
496
|
+
return false;
|
|
497
|
+
const candidates = [r.file, r.absoluteFile].filter((c) => typeof c === "string" && c !== "");
|
|
498
|
+
for (const sel of partialFiles) {
|
|
499
|
+
for (const c of candidates) {
|
|
500
|
+
if (c === sel || c.endsWith("/" + sel) || sel.endsWith("/" + c))
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
/** Placeholder identity for a record that names no suite because none resolved.
|
|
507
|
+
* Deliberately not a valid suiteIdOf output, so it can never collide with one. */
|
|
508
|
+
export const NO_RUNNABLE_SUITE = "(no runnable suite)";
|
|
509
|
+
/** Stable identity for ONE configured suite. A service can declare several
|
|
510
|
+
* (workspace.yml testSuites), so its name alone collides — two suites under one
|
|
511
|
+
* service would share a slot and the later would mask the other instead of both
|
|
512
|
+
* counting. */
|
|
513
|
+
export function suiteIdOf(cfg) {
|
|
514
|
+
return `${cfg.serviceName}::${cfg.framework}::${cfg.testRunCommand}`;
|
|
515
|
+
}
|
|
516
|
+
/** The files a selector narrowed to individual tests. A bare `file` selector runs
|
|
517
|
+
* the whole file; `file::test title` does not. */
|
|
518
|
+
export function partialFilesFrom(selectors) {
|
|
519
|
+
const out = new Set();
|
|
520
|
+
for (const sel of selectors) {
|
|
521
|
+
const [file, ...rest] = sel.split("::");
|
|
522
|
+
// A trailing `::` names no title, so every builder drops it and the whole file
|
|
523
|
+
// runs. Treating it as partial would throw away a real status.
|
|
524
|
+
if (rest.join("::").trim() === "")
|
|
525
|
+
continue;
|
|
526
|
+
const trimmed = file.trim();
|
|
527
|
+
if (trimmed)
|
|
528
|
+
out.add(trimmed);
|
|
529
|
+
}
|
|
530
|
+
return out;
|
|
531
|
+
}
|
|
443
532
|
/** Appends a run record into UnifiedAnalysisState.externalTestResults for the
|
|
444
533
|
* given repo section. Best-effort: a state failure never fails the run. */
|
|
445
534
|
async function persistExternalRun(stateFile, repository, record) {
|
|
@@ -463,7 +552,7 @@ async function persistExternalRun(stateFile, repository, record) {
|
|
|
463
552
|
* fully injectable for tests.
|
|
464
553
|
*/
|
|
465
554
|
export async function runExternalTests(params, deps = defaultDeps) {
|
|
466
|
-
const mode = params.mode
|
|
555
|
+
const mode = params.mode;
|
|
467
556
|
const selectors = params.testSelectors ?? [];
|
|
468
557
|
let effective = selectors;
|
|
469
558
|
let truncated = false;
|
|
@@ -486,6 +575,11 @@ export async function runExternalTests(params, deps = defaultDeps) {
|
|
|
486
575
|
if (params.stateFile) {
|
|
487
576
|
await persistExternalRun(params.stateFile, params.repository, {
|
|
488
577
|
...skippedRun,
|
|
578
|
+
files: [],
|
|
579
|
+
// No run config resolved, so there is no suite to identify — and a service
|
|
580
|
+
// name here would read as one. The reader never keys on it (the record is
|
|
581
|
+
// skipped, and skipped records are dropped first), so name it for what it is.
|
|
582
|
+
suite: NO_RUNNABLE_SUITE,
|
|
489
583
|
mode,
|
|
490
584
|
ranAt: deps.now(),
|
|
491
585
|
repository: params.repository,
|
|
@@ -518,6 +612,8 @@ export async function runExternalTests(params, deps = defaultDeps) {
|
|
|
518
612
|
if (params.stateFile) {
|
|
519
613
|
await persistExternalRun(params.stateFile, params.repository, {
|
|
520
614
|
...routedOut,
|
|
615
|
+
files: [],
|
|
616
|
+
suite: suiteIdOf(cfg),
|
|
521
617
|
mode,
|
|
522
618
|
ranAt: deps.now(),
|
|
523
619
|
repository: params.repository,
|
|
@@ -542,8 +638,13 @@ export async function runExternalTests(params, deps = defaultDeps) {
|
|
|
542
638
|
};
|
|
543
639
|
perSuite.push(suiteResult);
|
|
544
640
|
if (params.stateFile) {
|
|
641
|
+
const files = summarizeFilesInRun(suiteResult.results, {
|
|
642
|
+
partialFiles: partialFilesFrom(routing.kept),
|
|
643
|
+
});
|
|
545
644
|
await persistExternalRun(params.stateFile, params.repository, {
|
|
546
645
|
...suiteResult,
|
|
646
|
+
files,
|
|
647
|
+
suite: suiteIdOf(cfg),
|
|
547
648
|
mode,
|
|
548
649
|
ranAt: deps.now(),
|
|
549
650
|
repository: params.repository,
|
|
@@ -575,8 +676,7 @@ export function registerRunExistingTestsTool(server) {
|
|
|
575
676
|
.describe("Spec files to run (paths relative to the run command's cwd), optionally `file::test title` to also filter by name. Omit to run the whole configured suite."),
|
|
576
677
|
mode: z
|
|
577
678
|
.enum(["confirm", "verify"])
|
|
578
|
-
.
|
|
579
|
-
.describe("confirm (default): scoped run to establish which tests a change breaks; disables the suite's fail-fast cap (e.g. Playwright's maxFailures) so ALL breakages are counted. verify: re-run after an edit."),
|
|
679
|
+
.describe("REQUIRED, and the only thing that marks a run as before or after the edit: confirm = the pre-edit baseline (a scoped run establishing which tests the change breaks; disables the suite's fail-fast cap, e.g. Playwright's maxFailures, so ALL breakages are counted), verify = the re-run after you edited. Passing confirm for a post-edit run overwrites the baseline with the fixed result, and the report then shows a repair as having never been broken."),
|
|
580
680
|
project: z
|
|
581
681
|
.string()
|
|
582
682
|
.optional()
|
|
@@ -635,7 +735,7 @@ export function registerRunExistingTestsTool(server) {
|
|
|
635
735
|
AnalyticsService.pushMCPToolEvent(TOOL_NAME, errorResult, {
|
|
636
736
|
workspacePath: params.workspacePath,
|
|
637
737
|
service: params.service ?? "",
|
|
638
|
-
mode: params.mode
|
|
738
|
+
mode: params.mode,
|
|
639
739
|
}).catch((err) => {
|
|
640
740
|
logger.warning("Analytics event failed", { error: String(err) });
|
|
641
741
|
});
|