@skyramp/mcp 0.4.2-rc.1 → 0.4.2-rc.2
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/commands/localDevTestChangesCommand.js +1 -1
- package/build/commands/recommendTestsAndExecuteCommand.js +10 -1
- package/build/commands/testThisEndpointCommand.js +19 -2
- package/build/execution/wrapperConfig.d.ts +56 -0
- package/build/execution/wrapperConfig.js +155 -0
- package/build/index.js +6 -6
- package/build/playwright/registerPlaywrightTools.js +14 -0
- package/build/playwright/traceExportStore.d.ts +22 -0
- package/build/playwright/traceExportStore.js +81 -0
- package/build/playwright/traceRecordingPrompt.js +2 -1
- package/build/prompts/code-reuse.js +24 -21
- package/build/prompts/local-dev/local-dev-plan.js +6 -23
- package/build/prompts/local-dev/local-dev-prompts.js +1 -1
- package/build/prompts/shared-helper-policy.d.ts +36 -0
- package/build/prompts/shared-helper-policy.js +33 -1
- package/build/prompts/startTraceCollectionPrompts.js +1 -1
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +6 -7
- package/build/prompts/sut-setup/shared.d.ts +1 -1
- package/build/prompts/sut-setup/shared.js +5 -3
- package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +16 -8
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +90 -36
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +1 -1
- package/build/prompts/test-recommendation/recommendationShared.d.ts +1 -1
- package/build/prompts/test-recommendation/recommendationShared.js +0 -1
- package/build/prompts/testbot/testbot-prompts.js +11 -9
- package/build/services/TestDiscoveryService.js +32 -4
- package/build/skills/runTestSkill.d.ts +6 -0
- package/build/skills/runTestSkill.js +17 -0
- package/build/tool-phases.js +0 -1
- package/build/tools/budgetExcuse.d.ts +15 -0
- package/build/tools/budgetExcuse.js +113 -0
- package/build/tools/code-refactor/utils-verify-gates.js +17 -3
- package/build/tools/executeSkyrampTestTool.d.ts +97 -48
- package/build/tools/executeSkyrampTestTool.js +775 -449
- package/build/tools/submitReportTool.js +128 -0
- package/build/tools/test-management/actionsTool.js +31 -0
- package/build/tools/test-management/analyzeChangesTool.d.ts +4 -4
- package/build/tools/test-management/analyzeTestHealthTool.d.ts +0 -11
- package/build/tools/test-management/analyzeTestHealthTool.js +7 -63
- package/build/tools/test-management/testsOwedBeforeRun.d.ts +28 -0
- package/build/tools/test-management/testsOwedBeforeRun.js +53 -0
- package/build/tools/trace/stopTraceCollectionTool.js +1 -1
- package/build/types/RepositoryAnalysis.d.ts +32 -32
- package/build/types/ReuseOutcome.d.ts +4 -3
- package/build/types/TestExecution.d.ts +2 -2
- package/build/types/TestTypes.d.ts +3 -0
- package/build/types/TestTypes.js +6 -0
- package/build/utils/AnalysisStateManager.d.ts +0 -7
- package/build/utils/AnalysisStateManager.js +1 -1
- package/build/utils/connectionErrors.d.ts +10 -0
- package/build/utils/connectionErrors.js +10 -0
- package/build/utils/language-helper.js +24 -3
- package/build/utils/progress.d.ts +1 -1
- package/build/utils/progress.js +1 -1
- package/build/utils/rebaselineSnapshots.d.ts +1 -1
- package/build/utils/rebaselineSnapshots.js +6 -16
- package/build/utils/reuseRouting.d.ts +10 -0
- package/build/utils/reuseRouting.js +15 -0
- package/build/utils/runContextGauge.d.ts +27 -0
- package/build/utils/runContextGauge.js +181 -0
- package/build/utils/skyrampMdContent.d.ts +1 -1
- package/build/utils/skyrampMdContent.js +1 -1
- package/build/utils/skyrampSdkVersion.d.ts +9 -0
- package/build/utils/skyrampSdkVersion.js +16 -0
- package/build/utils/testDependencyPolicy.js +21 -0
- package/build/utils/testExecutionRecord.d.ts +5 -1
- package/build/utils/testExecutionRecord.js +3 -1
- package/build/utils/testFileClassification.d.ts +8 -0
- package/build/utils/testFileClassification.js +36 -3
- package/build/utils/utils-verify/action-key.d.ts +42 -0
- package/build/utils/utils-verify/action-key.js +118 -36
- package/build/utils/utils-verify/action-sites.d.ts +32 -0
- package/build/utils/utils-verify/action-sites.js +202 -0
- package/build/utils/utils-verify/body-reach.js +2 -4
- package/build/utils/utils-verify/call-sites.d.ts +25 -6
- package/build/utils/utils-verify/call-sites.js +8 -5
- 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 +25 -0
- package/build/utils/utils-verify/language-spec.js +16 -2
- package/build/utils/utils-verify/parse.d.ts +10 -1
- package/build/utils/utils-verify/parse.js +19 -2
- package/build/utils/utils-verify/verify.d.ts +3 -2
- package/build/utils/utils-verify/verify.js +16 -18
- package/build/workspace/workspace.d.ts +72 -52
- package/build/workspace/workspace.js +12 -8
- package/package.json +1 -1
- package/plugin/prompts/testbot-task1.md +0 -2
- package/plugin/skills/fix-test-import-errors/SKILL.md +2 -1
- package/plugin/skills/run-test/SKILL.md +16 -0
- package/build/adapters/jestAdapter.d.ts +0 -14
- package/build/adapters/jestAdapter.js +0 -131
- package/build/adapters/mochaAdapter.d.ts +0 -13
- package/build/adapters/mochaAdapter.js +0 -93
- package/build/adapters/playwrightAdapter.d.ts +0 -17
- package/build/adapters/playwrightAdapter.js +0 -184
- package/build/adapters/pytestAdapter.d.ts +0 -15
- package/build/adapters/pytestAdapter.js +0 -119
- package/build/tools/runExistingTestsTool.d.ts +0 -138
- package/build/tools/runExistingTestsTool.js +0 -666
- package/build/types/ExternalTestExecution.d.ts +0 -67
- package/build/types/ExternalTestExecution.js +0 -8
- package/build/workspace/testSuites.d.ts +0 -20
- package/build/workspace/testSuites.js +0 -17
|
@@ -111,7 +111,7 @@ Fallback service routing inputs from the one-click invocation:
|
|
|
111
111
|
Three rules apply in fallback mode at all times:
|
|
112
112
|
1. **No hand-written tests or mocks**: do NOT use Write, Edit, or any file-writing tool to create test or mock files manually. ALL test and mock files MUST be produced by Skyramp MCP generation tools (\`skyramp_batch_scenario_test_generation\`, \`skyramp_integration_test_generation\`, \`skyramp_generate_enriched_integration_test\`, \`skyramp_contract_test_generation\`, \`skyramp_mock_generation\`, \`skyramp_batch_mock_generation\`). If a generation tool fails, fix the inputs and retry — do not fall back to writing tests by hand.
|
|
113
113
|
2. **Mock scope**: if the user said "mock all downstream services", "mock all", "mock everything", or "mock all except X", treat all discovered downstream dependencies as selected for mocking (same as passing \`servicesToMock: "all"\`). Do not silently default to the third-party-only policy when the user expressed broad mock intent.
|
|
114
|
-
3. **
|
|
114
|
+
3. **Prefer skyramp_execute_test**: run tests through \`skyramp_execute_test\` rather than the repository's test runner directly: the server records the run, and a run it did not record cannot appear in the report.
|
|
115
115
|
|
|
116
116
|
---
|
|
117
117
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RUN_TEST_COMMAND_RULES } from "../skills/runTestSkill.js";
|
|
1
2
|
/**
|
|
2
3
|
* Predefined One-Click Command: Recommend tests and generate and execute top recommended tests
|
|
3
4
|
*
|
|
@@ -65,7 +66,11 @@ const fullRepoRecommendGenerateExecuteSteps = [
|
|
|
65
66
|
toolName: "skyramp_execute_test",
|
|
66
67
|
description: "Execute each generated test; resolve token before calling. For each generated test file path returned directly by the generation tools in step 2, invoke this tool with testFile set to that path.",
|
|
67
68
|
inputs: {
|
|
68
|
-
|
|
69
|
+
commandOverride: {
|
|
70
|
+
source: "literal",
|
|
71
|
+
value: `the shell command that runs this one test file with the repository's own test runner (${RUN_TEST_COMMAND_RULES})`,
|
|
72
|
+
},
|
|
73
|
+
cwd: { source: "user", paramKey: "repositoryPath" },
|
|
69
74
|
language: { source: "literal", value: "from workspace" },
|
|
70
75
|
testType: {
|
|
71
76
|
source: "literal",
|
|
@@ -75,6 +80,10 @@ const fullRepoRecommendGenerateExecuteSteps = [
|
|
|
75
80
|
source: "literal",
|
|
76
81
|
value: "path to a single generated test file; for each test path returned by generation tools in step 2, call skyramp_execute_test separately with testFile set to that path",
|
|
77
82
|
},
|
|
83
|
+
repository: {
|
|
84
|
+
source: "literal",
|
|
85
|
+
value: "owner/repo of the checkout, from its origin remote",
|
|
86
|
+
},
|
|
78
87
|
token: { source: "user", paramKey: "token" },
|
|
79
88
|
},
|
|
80
89
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RUN_TEST_COMMAND_RULES } from "../skills/runTestSkill.js";
|
|
1
2
|
/**
|
|
2
3
|
* Predefined One-Click Command: Test this endpoint
|
|
3
4
|
*
|
|
@@ -73,7 +74,11 @@ const comprehensivelyTestGivenEndpointSteps = [
|
|
|
73
74
|
toolName: "skyramp_execute_test",
|
|
74
75
|
description: "Execute each generated test; resolve token before calling. For each generated test file path returned directly by the generation tools in step 3, invoke this tool with testFile set to that path.",
|
|
75
76
|
inputs: {
|
|
76
|
-
|
|
77
|
+
commandOverride: {
|
|
78
|
+
source: "literal",
|
|
79
|
+
value: `the shell command that runs this one test file with the repository's own test runner (${RUN_TEST_COMMAND_RULES})`,
|
|
80
|
+
},
|
|
81
|
+
cwd: { source: "user", paramKey: "repositoryPath" },
|
|
77
82
|
language: { source: "literal", value: "from workspace" },
|
|
78
83
|
testType: {
|
|
79
84
|
source: "literal",
|
|
@@ -83,6 +88,10 @@ const comprehensivelyTestGivenEndpointSteps = [
|
|
|
83
88
|
source: "literal",
|
|
84
89
|
value: "path to a single generated test file; for each test path returned by generation tools in step 3, call skyramp_execute_test separately with testFile set to that path",
|
|
85
90
|
},
|
|
91
|
+
repository: {
|
|
92
|
+
source: "literal",
|
|
93
|
+
value: "owner/repo of the checkout, from its origin remote",
|
|
94
|
+
},
|
|
86
95
|
token: { source: "user", paramKey: "token" },
|
|
87
96
|
},
|
|
88
97
|
},
|
|
@@ -110,7 +119,11 @@ const comprehensivelyTestGivenEndpointSteps = [
|
|
|
110
119
|
toolName: "skyramp_execute_test",
|
|
111
120
|
description: "Optionally run existing tests individually; iterate over tests from stateFile and write results back",
|
|
112
121
|
inputs: {
|
|
113
|
-
|
|
122
|
+
commandOverride: {
|
|
123
|
+
source: "literal",
|
|
124
|
+
value: `the shell command that runs this one test file with the repository's own test runner (${RUN_TEST_COMMAND_RULES})`,
|
|
125
|
+
},
|
|
126
|
+
cwd: { source: "user", paramKey: "repositoryPath" },
|
|
114
127
|
testFile: {
|
|
115
128
|
source: "literal",
|
|
116
129
|
value: "path from stateFile existingTests array",
|
|
@@ -123,6 +136,10 @@ const comprehensivelyTestGivenEndpointSteps = [
|
|
|
123
136
|
source: "literal",
|
|
124
137
|
value: "testType from stateFile existingTests array",
|
|
125
138
|
},
|
|
139
|
+
repository: {
|
|
140
|
+
source: "literal",
|
|
141
|
+
value: "owner/repo of the checkout, from its origin remote",
|
|
142
|
+
},
|
|
126
143
|
token: { source: "user", paramKey: "token" },
|
|
127
144
|
stateFile: { source: "step", stepIndex: 1, outputKey: "stateFile" },
|
|
128
145
|
},
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Playwright's `--browser` names the implicit project itself, so the wrapper must not
|
|
2
|
+
* declare one: it refuses the flag when the config defines projects. */
|
|
3
|
+
export declare function commandPassesBrowserFlag(command: string): boolean;
|
|
4
|
+
/** The nearest Playwright config at or above the test file, up to the git root of `cwd`. */
|
|
5
|
+
export declare function findRepoPlaywrightConfig(testFile: string, cwd: string): string | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* A config that imports the repo's own and overlays only:
|
|
8
|
+
* - `video: "on"`, top level and per project — Playwright has no `--video` flag, and a
|
|
9
|
+
* project's `use` overrides the top level.
|
|
10
|
+
* - a per-run `outputDir`, so two runs in one checkout keep their videos apart.
|
|
11
|
+
* - `reuseExistingServer: true` on every `webServer` — the app is already running, and
|
|
12
|
+
* `!process.env.CI` would refuse its port.
|
|
13
|
+
* - the file under test appended to a declared `testMatch`. A repository that names its
|
|
14
|
+
* tests `*.test.ts` collects no `*.spec.ts`, so the file Skyramp generated is not a
|
|
15
|
+
* test to it and Playwright answers "No tests found" whatever the config or filter.
|
|
16
|
+
* Only the top level is widened: a project that declares its own `testMatch` keeps it,
|
|
17
|
+
* so the file runs once.
|
|
18
|
+
* - a project named `chromium`, ONLY when the repository declares no projects and the
|
|
19
|
+
* command passes no `--browser`. The Skyramp executor always runs
|
|
20
|
+
* `--browser=chromium`, which Playwright turns into a project of that name, so every
|
|
21
|
+
* baseline it has written is `<stem>-chromium-linux.png`. With neither a project nor
|
|
22
|
+
* the flag the host resolves `<stem>-linux.png`, finds no baseline, and writes a fresh
|
|
23
|
+
* one instead of comparing -- a visual regression then passes silently.
|
|
24
|
+
* The flag case needs nothing: `--browser=firefox` names the project `firefox` by
|
|
25
|
+
* itself. Declaring a project there would not help but throw, because Playwright
|
|
26
|
+
* refuses `--browser` when the config defines projects (`common/config.js`:
|
|
27
|
+
* "Cannot use --browser option when configuration file defines projects").
|
|
28
|
+
* See `utils/rebaselineSnapshots.ts` for the layout.
|
|
29
|
+
* No reporter: the verdict is the exit code.
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildWrapperConfig(opts: {
|
|
32
|
+
repoConfigBasename?: string;
|
|
33
|
+
outputDir: string;
|
|
34
|
+
/** Absolute path of the file this run executes. */
|
|
35
|
+
testFile?: string;
|
|
36
|
+
/** True when the command already passes `--browser`, which names the project itself. */
|
|
37
|
+
commandPassesBrowserFlag: boolean;
|
|
38
|
+
}): string;
|
|
39
|
+
/**
|
|
40
|
+
* Writes the wrapper beside the config it imports, because Playwright resolves `testDir`,
|
|
41
|
+
* `globalSetup` and `webServer.cwd` from the config file's directory. A dotfile with a
|
|
42
|
+
* random suffix: runs cannot collide and `testMatch` cannot collect it.
|
|
43
|
+
*/
|
|
44
|
+
export declare function writeWrapperConfig(opts: {
|
|
45
|
+
repoConfigPath?: string;
|
|
46
|
+
/** Where to write when there is no repo config. */
|
|
47
|
+
fallbackDir: string;
|
|
48
|
+
outputDir: string;
|
|
49
|
+
/** Absolute path of the file this run executes. */
|
|
50
|
+
testFile?: string;
|
|
51
|
+
/** True when the command already passes `--browser`, which names the project itself. */
|
|
52
|
+
commandPassesBrowserFlag: boolean;
|
|
53
|
+
}): {
|
|
54
|
+
path: string;
|
|
55
|
+
cleanup: () => void;
|
|
56
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { detectGitRoot } from "../utils/gitStaging.js";
|
|
5
|
+
import { samePath } from "../tools/code-refactor/reuse-state.js";
|
|
6
|
+
/** Playwright's own config names, in its probe order (1.59.1 `configLoader.js:314`).
|
|
7
|
+
* A missing name silently drops the repo's config. */
|
|
8
|
+
const CONFIG_NAMES = [
|
|
9
|
+
"playwright.config.ts",
|
|
10
|
+
"playwright.config.js",
|
|
11
|
+
"playwright.config.mts",
|
|
12
|
+
"playwright.config.mjs",
|
|
13
|
+
"playwright.config.cts",
|
|
14
|
+
"playwright.config.cjs",
|
|
15
|
+
];
|
|
16
|
+
/** Playwright's `--browser` names the implicit project itself, so the wrapper must not
|
|
17
|
+
* declare one: it refuses the flag when the config defines projects. */
|
|
18
|
+
export function commandPassesBrowserFlag(command) {
|
|
19
|
+
return /(^|\s)--browser(=|\s)/.test(command);
|
|
20
|
+
}
|
|
21
|
+
/** The nearest Playwright config at or above the test file, up to the git root of `cwd`. */
|
|
22
|
+
export function findRepoPlaywrightConfig(testFile, cwd) {
|
|
23
|
+
const stop = detectGitRoot(cwd);
|
|
24
|
+
let dir = path.dirname(path.resolve(testFile));
|
|
25
|
+
for (;;) {
|
|
26
|
+
for (const name of CONFIG_NAMES) {
|
|
27
|
+
const candidate = path.join(dir, name);
|
|
28
|
+
if (fs.existsSync(candidate))
|
|
29
|
+
return candidate;
|
|
30
|
+
}
|
|
31
|
+
const parent = path.dirname(dir);
|
|
32
|
+
if (parent === dir || (stop && samePath(dir, stop)))
|
|
33
|
+
return undefined;
|
|
34
|
+
dir = parent;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A config that imports the repo's own and overlays only:
|
|
39
|
+
* - `video: "on"`, top level and per project — Playwright has no `--video` flag, and a
|
|
40
|
+
* project's `use` overrides the top level.
|
|
41
|
+
* - a per-run `outputDir`, so two runs in one checkout keep their videos apart.
|
|
42
|
+
* - `reuseExistingServer: true` on every `webServer` — the app is already running, and
|
|
43
|
+
* `!process.env.CI` would refuse its port.
|
|
44
|
+
* - the file under test appended to a declared `testMatch`. A repository that names its
|
|
45
|
+
* tests `*.test.ts` collects no `*.spec.ts`, so the file Skyramp generated is not a
|
|
46
|
+
* test to it and Playwright answers "No tests found" whatever the config or filter.
|
|
47
|
+
* Only the top level is widened: a project that declares its own `testMatch` keeps it,
|
|
48
|
+
* so the file runs once.
|
|
49
|
+
* - a project named `chromium`, ONLY when the repository declares no projects and the
|
|
50
|
+
* command passes no `--browser`. The Skyramp executor always runs
|
|
51
|
+
* `--browser=chromium`, which Playwright turns into a project of that name, so every
|
|
52
|
+
* baseline it has written is `<stem>-chromium-linux.png`. With neither a project nor
|
|
53
|
+
* the flag the host resolves `<stem>-linux.png`, finds no baseline, and writes a fresh
|
|
54
|
+
* one instead of comparing -- a visual regression then passes silently.
|
|
55
|
+
* The flag case needs nothing: `--browser=firefox` names the project `firefox` by
|
|
56
|
+
* itself. Declaring a project there would not help but throw, because Playwright
|
|
57
|
+
* refuses `--browser` when the config defines projects (`common/config.js`:
|
|
58
|
+
* "Cannot use --browser option when configuration file defines projects").
|
|
59
|
+
* See `utils/rebaselineSnapshots.ts` for the layout.
|
|
60
|
+
* No reporter: the verdict is the exit code.
|
|
61
|
+
*/
|
|
62
|
+
export function buildWrapperConfig(opts) {
|
|
63
|
+
const outputDir = JSON.stringify(opts.outputDir);
|
|
64
|
+
const overlay = `outputDir: ${outputDir},`;
|
|
65
|
+
// Widen only what the repository declared: with no `testMatch`, Playwright's own
|
|
66
|
+
// default already collects both spellings, and setting one would narrow it.
|
|
67
|
+
// One chromium project, used by both branches below. The Skyramp executor runs
|
|
68
|
+
// --browser=chromium, so every baseline it has written is <stem>-chromium-linux.png;
|
|
69
|
+
// a wrapper with no project resolves <stem>-linux.png and compares against nothing.
|
|
70
|
+
// A command that passes --browser names the project itself, and Playwright refuses
|
|
71
|
+
// the flag when the config declares projects, so there it must stay empty.
|
|
72
|
+
const chromiumProject = (useExpr) => opts.commandPassesBrowserFlag
|
|
73
|
+
? ""
|
|
74
|
+
: `{ name: "chromium", outputDir: ${outputDir}, use: { ...${useExpr}, browserName: "chromium" } }`;
|
|
75
|
+
const testMatchOverlay = opts.testFile
|
|
76
|
+
? `...(base.testMatch ? { testMatch: [...asList(base.testMatch), ${JSON.stringify(opts.testFile)}] } : {}),`
|
|
77
|
+
: "";
|
|
78
|
+
if (!opts.repoConfigBasename) {
|
|
79
|
+
// No repo config: a minimal one, so video still records.
|
|
80
|
+
return `const config = {
|
|
81
|
+
testDir: ".",
|
|
82
|
+
${overlay}
|
|
83
|
+
use: { video: "on" },${chromiumProject('{ video: "on" }')
|
|
84
|
+
? `\n projects: [${chromiumProject('{ video: "on" }')}],`
|
|
85
|
+
: ""}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export default config;
|
|
89
|
+
`;
|
|
90
|
+
}
|
|
91
|
+
return `import base from ${JSON.stringify(`./${opts.repoConfigBasename}`)};
|
|
92
|
+
|
|
93
|
+
const withVideo = (use) => ({ ...(use ?? {}), video: "on" });
|
|
94
|
+
const reuseServer = (server) => ({ ...server, reuseExistingServer: true });
|
|
95
|
+
|
|
96
|
+
const asList = (m) => (Array.isArray(m) ? m : [m]);
|
|
97
|
+
|
|
98
|
+
const config = {
|
|
99
|
+
...base,
|
|
100
|
+
${overlay}
|
|
101
|
+
${testMatchOverlay}
|
|
102
|
+
use: withVideo(base.use),
|
|
103
|
+
...(base.webServer
|
|
104
|
+
? {
|
|
105
|
+
webServer: Array.isArray(base.webServer)
|
|
106
|
+
? base.webServer.map(reuseServer)
|
|
107
|
+
: reuseServer(base.webServer),
|
|
108
|
+
}
|
|
109
|
+
: {}),
|
|
110
|
+
...(Array.isArray(base.projects) && base.projects.length > 0
|
|
111
|
+
? {
|
|
112
|
+
projects: base.projects.map((p) => ({
|
|
113
|
+
...p,
|
|
114
|
+
outputDir: ${outputDir},
|
|
115
|
+
use: withVideo(p.use),
|
|
116
|
+
})),
|
|
117
|
+
}
|
|
118
|
+
: ${chromiumProject("withVideo(base.use)")
|
|
119
|
+
? `{ projects: [${chromiumProject("withVideo(base.use)")}] }`
|
|
120
|
+
: "{}"}),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export default config;
|
|
124
|
+
`;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Writes the wrapper beside the config it imports, because Playwright resolves `testDir`,
|
|
128
|
+
* `globalSetup` and `webServer.cwd` from the config file's directory. A dotfile with a
|
|
129
|
+
* random suffix: runs cannot collide and `testMatch` cannot collect it.
|
|
130
|
+
*/
|
|
131
|
+
export function writeWrapperConfig(opts) {
|
|
132
|
+
const dir = opts.repoConfigPath
|
|
133
|
+
? path.dirname(opts.repoConfigPath)
|
|
134
|
+
: opts.fallbackDir;
|
|
135
|
+
const wrapperPath = path.join(dir, `.skyramp-sut-${crypto.randomBytes(6).toString("hex")}.config.ts`);
|
|
136
|
+
fs.writeFileSync(wrapperPath, buildWrapperConfig({
|
|
137
|
+
repoConfigBasename: opts.repoConfigPath
|
|
138
|
+
? path.basename(opts.repoConfigPath)
|
|
139
|
+
: undefined,
|
|
140
|
+
outputDir: opts.outputDir,
|
|
141
|
+
testFile: opts.testFile,
|
|
142
|
+
commandPassesBrowserFlag: opts.commandPassesBrowserFlag,
|
|
143
|
+
}));
|
|
144
|
+
return {
|
|
145
|
+
path: wrapperPath,
|
|
146
|
+
cleanup: () => {
|
|
147
|
+
try {
|
|
148
|
+
fs.rmSync(wrapperPath, { force: true });
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// Best effort: never fail the run over cleanup.
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
package/build/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { runnableLanguagesText } from "./types/TestTypes.js";
|
|
3
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
5
|
import { registerStartTraceCollectionPrompt } from "./prompts/startTraceCollectionPrompts.js";
|
|
5
6
|
import { registerTraceTool } from "./tools/trace/startTraceCollectionTool.js";
|
|
6
7
|
import { registerTraceStopTool } from "./tools/trace/stopTraceCollectionTool.js";
|
|
7
8
|
import { registerExecuteSkyrampTestTool } from "./tools/executeSkyrampTestTool.js";
|
|
8
|
-
import { registerRunExistingTestsTool } from "./tools/runExistingTestsTool.js";
|
|
9
9
|
import { AUTH_PLACEHOLDER_TOKEN } from "./types/TestTypes.js";
|
|
10
10
|
import { AUTH_CONFLICT_ERROR_MSG } from "./prompts/test-recommendation/recommendationSections.js";
|
|
11
11
|
import { logger } from "./utils/logger.js";
|
|
@@ -114,11 +114,12 @@ Use \`skyramp_analyze_changes\` as the single entry point for both test recommen
|
|
|
114
114
|
1. Call \`skyramp_analyze_changes\` with \`repositoryPath\` and \`scope\` → scans endpoints, computes branch diff, discovers existing tests → returns a \`stateFile\` **and** inline ranked test recommendations.
|
|
115
115
|
2. (Optional) Use the \`skyramp_recommend_tests\` prompt with \`stateFile\` in Cursor/IDE chat for a focused recommendation view.
|
|
116
116
|
|
|
117
|
-
### Health Analysis (
|
|
117
|
+
### Health Analysis (5-step)
|
|
118
118
|
1. Call \`skyramp_analyze_changes\` with \`repositoryPath\` and \`scope\` → returns a \`stateFile\`.
|
|
119
|
-
2.
|
|
120
|
-
3.
|
|
121
|
-
4.
|
|
119
|
+
2. Run each external test in ${runnableLanguagesText()} with \`skyramp_execute_test\`, \`phase: "before"\`, and \`stateFile\` → the server runs your command and writes the verdict back to stateFile. The tool takes no other language, and the gate in step 3 does not ask for those.
|
|
120
|
+
3. Call \`skyramp_analyze_test_health\` with \`stateFile\` → runs drift analysis + health scoring + LLM semantic assessment. It refuses until step 2 has run.
|
|
121
|
+
4. (Optional) Run Skyramp-generated tests with \`skyramp_execute_test\` and \`stateFile\` → writes each verdict back for health scoring.
|
|
122
|
+
5. Call \`skyramp_actions\` with \`stateFile\` → executes UPDATE/REGENERATE/ADD recommendations (with execution-aware prioritization if step 4 ran).
|
|
122
123
|
|
|
123
124
|
After \`skyramp_analyze_changes\`, inspect enriched data via MCP Resources (use the \`sessionId\` returned in the output):
|
|
124
125
|
- \`skyramp://analysis/{sessionId}/summary\` — high-level overview
|
|
@@ -241,7 +242,6 @@ if (isLocalDevEnabled()) {
|
|
|
241
242
|
}
|
|
242
243
|
if (isTestbotEnabled()) {
|
|
243
244
|
infrastructureTools.push(registerSubmitReportTool);
|
|
244
|
-
infrastructureTools.push(registerRunExistingTestsTool);
|
|
245
245
|
logger.info("Testbot tools enabled via SKYRAMP_FEATURE_TESTBOT");
|
|
246
246
|
}
|
|
247
247
|
infrastructureTools.forEach((registerTool) => registerTool(server));
|
|
@@ -7,6 +7,7 @@ import { logger } from "../utils/logger.js";
|
|
|
7
7
|
import { PlaywrightTraceService, } from "./PlaywrightTraceService.js";
|
|
8
8
|
import { blueprintJsonSpan } from "./blueprintDigest.js";
|
|
9
9
|
import { recordBlueprintCapture, recordBrowserNavigation, } from "./blueprintDigestStore.js";
|
|
10
|
+
import { recordTraceExport } from "./traceExportStore.js";
|
|
10
11
|
let _service = null;
|
|
11
12
|
export function getPlaywrightTraceService() {
|
|
12
13
|
return _service;
|
|
@@ -21,6 +22,14 @@ const BLUEPRINT_TOOL = "browser_blueprint";
|
|
|
21
22
|
* withAssertReminder.
|
|
22
23
|
*/
|
|
23
24
|
const NAVIGATE_TOOL = "browser_navigate";
|
|
25
|
+
/**
|
|
26
|
+
* The one browser tool whose CALL is counted — see traceExportStore.
|
|
27
|
+
*
|
|
28
|
+
* The count is what lets `skyramp_submit_report` tell "recorded, and it did not
|
|
29
|
+
* work" apart from "never recorded". Those two deserve opposite answers, and
|
|
30
|
+
* without the count the report check can only see the second as the first.
|
|
31
|
+
*/
|
|
32
|
+
const EXPORT_TOOL = "skyramp_export_zip";
|
|
24
33
|
/**
|
|
25
34
|
* The line the agent reads the moment it has reached a page.
|
|
26
35
|
*
|
|
@@ -225,6 +234,11 @@ export async function registerPlaywrightTools(server, options) {
|
|
|
225
234
|
};
|
|
226
235
|
}
|
|
227
236
|
const result = await _service.callTool(tool.name, params);
|
|
237
|
+
// Counted BEFORE any early return and REGARDLESS of isError: an export
|
|
238
|
+
// that failed still cost the agent a recording, and the report gate that
|
|
239
|
+
// reads this has to be satisfiable by trying. Returns nothing, never throws.
|
|
240
|
+
if (tool.name === EXPORT_TOOL)
|
|
241
|
+
recordTraceExport();
|
|
228
242
|
if (isNavigate) {
|
|
229
243
|
// The destination this server forwarded, so `skyramp_resolve_screen`
|
|
230
244
|
// can say where the browser actually is without asking the agent.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** A run recording more traces than this has a problem no counter fixes; the cap is
|
|
2
|
+
* here so a runaway loop cannot grow the number without bound. */
|
|
3
|
+
export declare const MAX_COUNTED_EXPORTS = 100;
|
|
4
|
+
/** How many times the report may be refused for an unrecorded planned UI test.
|
|
5
|
+
* The refusal is meant to cost the agent a recording, not the whole run: a
|
|
6
|
+
* resubmission that records nothing still advances nothing, and without this cap
|
|
7
|
+
* an agent that will not record and a job timeout are the same outcome. */
|
|
8
|
+
export declare const MAX_UI_DELIVERY_REFUSALS = 3;
|
|
9
|
+
/** Record one `skyramp_export_zip` call, or do nothing. NOTHING HERE MAY COST THE
|
|
10
|
+
* EXPORT: the payload the agent receives is untouched and every failure is
|
|
11
|
+
* swallowed with a log line. */
|
|
12
|
+
export declare function recordTraceExport(): void;
|
|
13
|
+
/** Recording attempts this run. */
|
|
14
|
+
export declare function traceExportAttempts(): number;
|
|
15
|
+
/** Count one refusal and return the new total. */
|
|
16
|
+
export declare function countUiDeliveryRefusal(): number;
|
|
17
|
+
/** Refusals so far, without counting one. */
|
|
18
|
+
export declare function uiDeliveryRefusals(): number;
|
|
19
|
+
/** Test seam. The store is module state keyed by the run; a suite that writes a
|
|
20
|
+
* fresh state file per test reseats it anyway, but an explicit reset keeps a test
|
|
21
|
+
* that reuses one honest. */
|
|
22
|
+
export declare function resetTraceExportStoreForTests(): void;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { currentRunStateFile } from "../utils/AnalysisStateManager.js";
|
|
2
|
+
import { logger } from "../utils/logger.js";
|
|
3
|
+
/** How many times this run called `skyramp_export_zip`, and how many times the
|
|
4
|
+
* report was refused for a planned UI test nobody recorded. IN PROCESS, not in the
|
|
5
|
+
* state file, for the same reason as the blueprint digests: the only reader shares
|
|
6
|
+
* a process with the browser tools, and a read-modify-write per export buys
|
|
7
|
+
* nothing. KEYED BY THE RUN, one slot — a new key means the previous run is over.
|
|
8
|
+
*
|
|
9
|
+
* ATTEMPTS, never successes. An export that errored still cost the agent a
|
|
10
|
+
* recording, and the gate reading this has to be satisfiable by TRYING: counting
|
|
11
|
+
* only successes lets one screen that cannot be recorded refuse the report for the
|
|
12
|
+
* rest of the run. */
|
|
13
|
+
let countedRunStateFile;
|
|
14
|
+
let exportAttempts = 0;
|
|
15
|
+
let deliveryRefusals = 0;
|
|
16
|
+
/** A run recording more traces than this has a problem no counter fixes; the cap is
|
|
17
|
+
* here so a runaway loop cannot grow the number without bound. */
|
|
18
|
+
export const MAX_COUNTED_EXPORTS = 100;
|
|
19
|
+
/** How many times the report may be refused for an unrecorded planned UI test.
|
|
20
|
+
* The refusal is meant to cost the agent a recording, not the whole run: a
|
|
21
|
+
* resubmission that records nothing still advances nothing, and without this cap
|
|
22
|
+
* an agent that will not record and a job timeout are the same outcome. */
|
|
23
|
+
export const MAX_UI_DELIVERY_REFUSALS = 3;
|
|
24
|
+
/** The slot this run writes to. NOT gated on a resolvable state file: a run whose
|
|
25
|
+
* path cannot be resolved still records and still reports, and a counter that
|
|
26
|
+
* stayed at zero there made the gate below refuse a report it could never let
|
|
27
|
+
* through. `NO_RUN` is one slot like any other — a new key still clears it. */
|
|
28
|
+
const NO_RUN = "<no-run>";
|
|
29
|
+
function reseatRun() {
|
|
30
|
+
let runStateFile;
|
|
31
|
+
try {
|
|
32
|
+
runStateFile = currentRunStateFile() ?? NO_RUN;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
runStateFile = NO_RUN;
|
|
36
|
+
}
|
|
37
|
+
if (runStateFile !== countedRunStateFile) {
|
|
38
|
+
countedRunStateFile = runStateFile;
|
|
39
|
+
exportAttempts = 0;
|
|
40
|
+
deliveryRefusals = 0;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Record one `skyramp_export_zip` call, or do nothing. NOTHING HERE MAY COST THE
|
|
44
|
+
* EXPORT: the payload the agent receives is untouched and every failure is
|
|
45
|
+
* swallowed with a log line. */
|
|
46
|
+
export function recordTraceExport() {
|
|
47
|
+
try {
|
|
48
|
+
reseatRun();
|
|
49
|
+
if (exportAttempts >= MAX_COUNTED_EXPORTS)
|
|
50
|
+
return;
|
|
51
|
+
exportAttempts += 1;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
55
|
+
logger.warning(`recordTraceExport failed: ${detail}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Recording attempts this run. */
|
|
59
|
+
export function traceExportAttempts() {
|
|
60
|
+
reseatRun();
|
|
61
|
+
return exportAttempts;
|
|
62
|
+
}
|
|
63
|
+
/** Count one refusal and return the new total. */
|
|
64
|
+
export function countUiDeliveryRefusal() {
|
|
65
|
+
reseatRun();
|
|
66
|
+
deliveryRefusals += 1;
|
|
67
|
+
return deliveryRefusals;
|
|
68
|
+
}
|
|
69
|
+
/** Refusals so far, without counting one. */
|
|
70
|
+
export function uiDeliveryRefusals() {
|
|
71
|
+
reseatRun();
|
|
72
|
+
return deliveryRefusals;
|
|
73
|
+
}
|
|
74
|
+
/** Test seam. The store is module state keyed by the run; a suite that writes a
|
|
75
|
+
* fresh state file per test reseats it anyway, but an explicit reset keeps a test
|
|
76
|
+
* that reuses one honest. */
|
|
77
|
+
export function resetTraceExportStoreForTests() {
|
|
78
|
+
countedRunStateFile = undefined;
|
|
79
|
+
exportAttempts = 0;
|
|
80
|
+
deliveryRefusals = 0;
|
|
81
|
+
}
|
|
@@ -43,7 +43,8 @@ Then execute in strict order:
|
|
|
43
43
|
|
|
44
44
|
- **After every action that changes the page**, call \`browser_snapshot\` before the next interaction — refs become stale after navigation, clicks that trigger page updates, and form submissions.
|
|
45
45
|
- **Iframe content** appears inline in the snapshot — interact with those elements using their refs normally.
|
|
46
|
-
- **
|
|
46
|
+
- **One recorder, and the export drains it**: every \`browser_*\` call accumulates into ONE buffer, exploration included — whatever you did before the flow is in the trace with it. A successful \`skyramp_export_zip\` writes that buffer, EMPTIES it and closes the browser, so the next recording starts clean. A trace that came out polluted is therefore a reason to record again, not a reason to stop, and the retry costs you one navigation and the flow. Record each test in its own pass: \`browser_navigate\` to the start URL, the flow, export.
|
|
47
|
+
- **Trace deduplication is narrower than it sounds**: a repeated \`browser_navigate\` drops the attempt before it only when it repeats the MOST RECENT \`browser_navigate\`. Clicks and types in between are fine; another navigation is not — a detour through a sign-out page, say, and BOTH attempts are exported. Never rely on it to clean up an exploratory session: export that session to a throwaway path and discard the zip, which leaves the recorder empty for the real one.
|
|
47
48
|
- **No Docker required**: the \`browser_*\` tools run a local browser session managed by the MCP server. Docker is ONLY used by \`skyramp_start_trace_collection\` (manual recording mode). Never suggest or check for Docker when using AI-driven recording.
|
|
48
49
|
${modularizeNote}
|
|
49
50
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { generateSkyrampHeader, SKYRAMP_UTILS_HEADER } from "../utils/utils.js";
|
|
2
2
|
import { isUtilsReuseEnabled } from "../utils/featureFlags.js";
|
|
3
|
-
import { isModularizeFirstTarget, isPomAwareTarget, } from "../utils/reuseRouting.js";
|
|
3
|
+
import { inlineCallSiteFamily, isModularizeFirstTarget, isPomAwareTarget, } from "../utils/reuseRouting.js";
|
|
4
4
|
import { getPomAwareCodeReusePrompt } from "./pom-aware-code-reuse.js";
|
|
5
5
|
import { NO_SHADOWING_RULE, SELECTOR_OWNING_MODULE_RULE, selectHelperPolicy, } from "./shared-helper-policy.js";
|
|
6
6
|
import { UTILS_LANGUAGE_SPECS } from "../utils/utils-verify/language-spec.js";
|
|
@@ -159,31 +159,34 @@ This step MOVES helpers that skyramp_modularization already created — it does
|
|
|
159
159
|
**IF \`${fileName}\` ALREADY CONTAINS A HELPER FOR ${policy.sameStep}** that differs from what ${testFile} needs only by ${policy.liftableLiterals}: do NOT define a second helper — EXTEND the utils helper by lifting the differing literals to parameters whose defaults are the utils version's current literals (existing call sites stay valid unchanged), then import and call it with this test's values. ${policy.oneHelperRule(fileName)}${policy.structuralNoise ? ` ${policy.structuralNoise}.` : ""} ${NO_SHADOWING_RULE}
|
|
160
160
|
`
|
|
161
161
|
: "";
|
|
162
|
-
// SKYR-4219: an inline
|
|
162
|
+
// SKYR-4219: an inline copy of a step is a CALL SITE for an existing helper, not a
|
|
163
163
|
// helper to move. Without this, a direct-mode sibling (plain literals, no
|
|
164
|
-
//
|
|
165
|
-
// same request,
|
|
166
|
-
// helper
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
164
|
+
// `data_override`/`uniqueSuffix`) never meets the scenario-mode helper for the
|
|
165
|
+
// same request, a step still inline in the current test never meets the utils
|
|
166
|
+
// helper, and a browser spec that repeats a helper's action sequence keeps its copy
|
|
167
|
+
// — reuse degrades to a no-op in any mixed directory. The text is the policy's, in
|
|
168
|
+
// the kind's own terms; the gate is inlineCallSiteFamily, which the verify pass runs
|
|
169
|
+
// its sibling scan on, so the rule and the scan cannot drift apart.
|
|
170
|
+
const inlineCallSiteRule = inlineCallSiteFamily(testType, lang)
|
|
171
|
+
? policy.inlineCallSiteRule
|
|
170
172
|
: "";
|
|
173
|
+
const site = policy.inlineSite;
|
|
171
174
|
// Its own step, placed after 5b: the agent routinely satisfies ${testFile} in STEP 3
|
|
172
175
|
// and jumps straight to verification, never entering STEP 4 — so a sibling rule parked
|
|
173
176
|
// there is never read (measured: eval run u4219b). This is where the shared helper is
|
|
174
177
|
// guaranteed to exist and the agent is guaranteed to still be running.
|
|
175
|
-
const siblingCallSitesStep =
|
|
176
|
-
? `## STEP 5c: REPLACE
|
|
178
|
+
const siblingCallSitesStep = inlineCallSiteRule
|
|
179
|
+
? `## STEP 5c: REPLACE ${site.nouns.toUpperCase()} IN OTHER SKYRAMP-GENERATED TESTS WITH ${site.target(fileName)}
|
|
177
180
|
|
|
178
|
-
**Run this step EVEN IF STEP 3 already satisfied ${testFile} and STEPS 4–5b did nothing.** ${
|
|
181
|
+
**Run this step EVEN IF STEP 3 already satisfied ${testFile} and STEPS 4–5b did nothing.** ${inlineCallSiteRule}
|
|
179
182
|
|
|
180
183
|
${pyImportRuleLine(fileName)} (this applies to the import you add to the SIBLING file too — its directory is what decides the form.)
|
|
181
184
|
|
|
182
185
|
1. Grep for the other Skyramp-generated test files exactly as in STEP 4 (codegen marker \`${codegenMarker}\`, excluding ${testFile} and \`${fileName}\`).
|
|
183
|
-
2. In each, find every
|
|
184
|
-
3. For each match: import the helper into that file and replace the
|
|
185
|
-
4. A
|
|
186
|
-
|
|
186
|
+
2. In each, find every ${site.noun} ${site.match(fileName)}.
|
|
187
|
+
3. For each match: import the helper into that file and replace the ${site.unit} with the call, passing the ${site.unit}'s literals (or expressions) as arguments; ${site.lift(fileName)}. Change NOTHING else in that file — ${site.keeps}.
|
|
188
|
+
4. A ${site.unit} with no matching helper, or one that differs structurally, stays as it is. Conservative bias: a wrong replacement breaks a passing test; a missed one leaves a duplicate.
|
|
189
|
+
${site.afterSteps}
|
|
187
190
|
`
|
|
188
191
|
: "";
|
|
189
192
|
const finalReminderScope = seedsUtils
|
|
@@ -234,11 +237,11 @@ Use the Grep tool to search for files containing "${SKYRAMP_UTILS_HEADER}":
|
|
|
234
237
|
Read the matching non-test files and check if any helpers can be reused in ${testFile}.
|
|
235
238
|
${inHouseDiscoveryStep}
|
|
236
239
|
## STEP 3: USE EXISTING HELPERS FROM UTILS SOURCE FILES FOUND IN STEP 2
|
|
237
|
-
If helpers exist in util file that can be reused in ${testFile} without modifying them${
|
|
240
|
+
If helpers exist in util file that can be reused in ${testFile} without modifying them${inlineCallSiteRule ? site.extendClause : ""}:
|
|
238
241
|
- Import them into ${testFile}
|
|
239
242
|
- Remove any duplicate code in ${testFile}
|
|
240
|
-
${
|
|
241
|
-
? `- ${
|
|
243
|
+
${inlineCallSiteRule
|
|
244
|
+
? `- ${inlineCallSiteRule} This applies to every ${site.stepNoun} still inline in ${testFile} that matches a utils helper.
|
|
242
245
|
`
|
|
243
246
|
: ""}- Test that ${testFile} still works without any errors and logical is same as original test file.
|
|
244
247
|
${utilsMergeRule}${inHouseMatchStep}
|
|
@@ -272,8 +275,8 @@ NOT A HELPER FUNCTION (do not extract):
|
|
|
272
275
|
- No function definition/signature
|
|
273
276
|
- Example: Multiple \`await page.getByTestId("xyz").click()\` directly in test
|
|
274
277
|
|
|
275
|
-
${
|
|
276
|
-
? `(
|
|
278
|
+
${inlineCallSiteRule
|
|
279
|
+
? `(${site.nouns} in OTHER test files are not helpers to move — STEP 5c replaces them with calls to the shared helpers.)
|
|
277
280
|
|
|
278
281
|
`
|
|
279
282
|
: ""}**IF OTHER TEST FILES ONLY CONTAIN REPETITIVE PATTERNS (NO ACTUAL HELPER FUNCTIONS), SKIP TO ${skipTarget}**
|
|
@@ -365,7 +368,7 @@ ${seedStepBlock}${siblingCallSitesStep}## STEP 6: VERIFY AND VALIDATE
|
|
|
365
368
|
6. **VERIFY** that no unnecessary helper functions were created (functions that duplicate existing functionality)
|
|
366
369
|
7. **VERIFY** that all helper functions in utils are actually imported and used in the test files
|
|
367
370
|
8. **REMOVE** any helper functions that are not being used after refactoring
|
|
368
|
-
9. **NEVER** refactor, reorganize, or restructure existing source test files beyond moving helpers${
|
|
371
|
+
9. **NEVER** refactor, reorganize, or restructure existing source test files beyond moving helpers${inlineCallSiteRule ? ` — and, in this flow, replacing an ${site.noun} with a call to the shared helper it duplicates (STEP 3 / STEP 5c)` : ""}
|
|
369
372
|
10. **RUN TESTS** to ensure functionality is preserved after refactoring
|
|
370
373
|
11. **NEVER** drop an assertion or a cross-cutting guard (\`page.on('pageerror'...)\`, error collectors and their final \`expect\`) while moving code — the verify step compares the assertion count against the modularization hand-out and fails on a loss
|
|
371
374
|
|