@skyramp/mcp 0.3.6-rc.2.ac20 → 0.3.7

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.
Files changed (103) hide show
  1. package/build/adapters/jestAdapter.js +0 -3
  2. package/build/adapters/mochaAdapter.js +0 -2
  3. package/build/adapters/playwrightAdapter.js +0 -3
  4. package/build/adapters/pytestAdapter.js +0 -12
  5. package/build/prompts/code-reuse.js +17 -2
  6. package/build/prompts/enhance-assertions/sharedAssertionRules.js +1 -1
  7. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +2 -1
  8. package/build/prompts/modularization/ui-test-modularization.js +9 -6
  9. package/build/prompts/pom-aware-code-reuse.js +1 -1
  10. package/build/prompts/shared-helper-policy.js +5 -5
  11. package/build/prompts/testbot/testbot-prompts.js +1 -1
  12. package/build/services/TestGenerationService.js +28 -1
  13. package/build/tools/code-refactor/assertion-state.d.ts +91 -0
  14. package/build/tools/code-refactor/assertion-state.js +375 -0
  15. package/build/tools/code-refactor/codeReuseTool.js +6 -4
  16. package/build/tools/code-refactor/enhanceAssertionsTool.js +73 -18
  17. package/build/tools/code-refactor/retrofit-state.d.ts +53 -0
  18. package/build/tools/code-refactor/retrofit-state.js +162 -0
  19. package/build/tools/code-refactor/reuse-outcome.d.ts +7 -0
  20. package/build/tools/code-refactor/reuse-state.d.ts +9 -0
  21. package/build/tools/code-refactor/reuse-state.js +42 -4
  22. package/build/tools/code-refactor/utils-verify-gates.js +69 -15
  23. package/build/tools/executeSkyrampTestTool.js +19 -14
  24. package/build/tools/generate-tests/batchMockGenerationTool.js +25 -0
  25. package/build/tools/generateEnrichedIntegrationTestTool.js +10 -0
  26. package/build/tools/runExistingTestsTool.d.ts +2 -34
  27. package/build/tools/runExistingTestsTool.js +4 -104
  28. package/build/tools/submitReportTool.js +87 -134
  29. package/build/tools/workspace/initializeWorkspaceTool.js +99 -27
  30. package/build/types/AssertionOutcome.d.ts +68 -0
  31. package/build/types/AssertionOutcome.js +1 -0
  32. package/build/types/ExternalTestExecution.d.ts +1 -67
  33. package/build/types/ReuseOutcome.d.ts +16 -0
  34. package/build/types/TestTypes.d.ts +4 -0
  35. package/build/types/TestTypes.js +8 -0
  36. package/build/types/TestbotReport.d.ts +13 -0
  37. package/build/types/index.d.ts +1 -1
  38. package/build/utils/AnalysisStateManager.d.ts +20 -7
  39. package/build/utils/assertion-verify/api-shared-lints.d.ts +5 -0
  40. package/build/utils/assertion-verify/api-shared-lints.js +315 -0
  41. package/build/utils/assertion-verify/contract-lints.d.ts +3 -0
  42. package/build/utils/assertion-verify/contract-lints.js +87 -0
  43. package/build/utils/assertion-verify/format.d.ts +5 -0
  44. package/build/utils/assertion-verify/format.js +65 -0
  45. package/build/utils/assertion-verify/helper-imports.d.ts +6 -0
  46. package/build/utils/assertion-verify/helper-imports.js +178 -0
  47. package/build/utils/assertion-verify/index.d.ts +3 -0
  48. package/build/utils/assertion-verify/index.js +7 -0
  49. package/build/utils/assertion-verify/integration-lints.d.ts +3 -0
  50. package/build/utils/assertion-verify/integration-lints.js +36 -0
  51. package/build/utils/assertion-verify/js-regex-blank.d.ts +1 -0
  52. package/build/utils/assertion-verify/js-regex-blank.js +153 -0
  53. package/build/utils/assertion-verify/lint-types.d.ts +33 -0
  54. package/build/utils/assertion-verify/lint-types.js +57 -0
  55. package/build/utils/assertion-verify/marker.d.ts +27 -0
  56. package/build/utils/assertion-verify/marker.js +61 -0
  57. package/build/utils/assertion-verify/metrics.d.ts +30 -0
  58. package/build/utils/assertion-verify/metrics.js +341 -0
  59. package/build/utils/assertion-verify/python-strip.d.ts +6 -0
  60. package/build/utils/assertion-verify/python-strip.js +75 -0
  61. package/build/utils/assertion-verify/strip-dispatch.d.ts +19 -0
  62. package/build/utils/assertion-verify/strip-dispatch.js +42 -0
  63. package/build/utils/assertion-verify/ui-lints.d.ts +8 -0
  64. package/build/utils/assertion-verify/ui-lints.js +244 -0
  65. package/build/utils/assertion-verify/verify.d.ts +61 -0
  66. package/build/utils/assertion-verify/verify.js +215 -0
  67. package/build/utils/executorWorkDir.d.ts +36 -0
  68. package/build/utils/executorWorkDir.js +77 -0
  69. package/build/utils/featureFlags.d.ts +12 -2
  70. package/build/utils/featureFlags.js +33 -3
  71. package/build/utils/reportVerification.d.ts +4 -0
  72. package/build/utils/reportVerification.js +32 -4
  73. package/build/utils/utils-verify/allow.d.ts +22 -4
  74. package/build/utils/utils-verify/allow.js +8 -2
  75. package/build/utils/utils-verify/call-sites.d.ts +40 -1
  76. package/build/utils/utils-verify/call-sites.js +196 -30
  77. package/build/utils/utils-verify/importers.d.ts +31 -0
  78. package/build/utils/utils-verify/importers.js +78 -0
  79. package/build/utils/utils-verify/index.d.ts +1 -0
  80. package/build/utils/utils-verify/index.js +1 -0
  81. package/build/utils/utils-verify/language-spec.d.ts +13 -2
  82. package/build/utils/utils-verify/language-spec.js +12 -2
  83. package/build/utils/utils-verify/parse.d.ts +31 -3
  84. package/build/utils/utils-verify/parse.js +190 -9
  85. package/build/utils/utils-verify/retrofit-equivalence.d.ts +43 -0
  86. package/build/utils/utils-verify/retrofit-equivalence.js +218 -0
  87. package/build/utils/utils-verify/stage.d.ts +6 -0
  88. package/build/utils/utils-verify/stage.js +12 -2
  89. package/build/utils/utils-verify/verify.d.ts +54 -4
  90. package/build/utils/utils-verify/verify.js +224 -12
  91. package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
  92. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-CZfp96qZ.js → codeMirrorModule-LNgEKtdV.js} +1 -1
  93. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-gpLo02E0.js → defaultSettingsView-Bwr1eMKC.js} +135 -135
  94. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.Bq1r1URj.js → index.-Id052Lr.js} +1 -1
  95. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  96. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.VEfqi1qN.js → uiMode.BPopbasy.js} +1 -1
  97. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  98. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  99. package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
  100. package/node_modules/playwright/package.json +1 -1
  101. package/package.json +2 -2
  102. package/build/tools/code-refactor/enhance-state.d.ts +0 -49
  103. package/build/tools/code-refactor/enhance-state.js +0 -109
@@ -1,4 +1,3 @@
1
- import * as path from "path";
2
1
  /**
3
2
  * Shared jest + vitest adapter for skyramp_run_existing_tests. vitest's json
4
3
  * reporter is jest-compatible, so both use ONE parser; only the reporter flag
@@ -41,7 +40,6 @@ export function parseJestJson(report, opts) {
41
40
  results.push({
42
41
  testId: `${name} › (file failed to run)`,
43
42
  file: name,
44
- ...(path.isAbsolute(name) ? { absoluteFile: name } : {}),
45
43
  status: "error",
46
44
  message: stripVTControlCharacters(raw).trim() || undefined,
47
45
  durationMs: 0,
@@ -57,7 +55,6 @@ export function parseJestJson(report, opts) {
57
55
  results.push({
58
56
  testId: `${name} › ${titlePath}`,
59
57
  file: name,
60
- ...(path.isAbsolute(name) ? { absoluteFile: name } : {}),
61
58
  status,
62
59
  message,
63
60
  durationMs: Math.round(a.duration ?? 0),
@@ -1,4 +1,3 @@
1
- import * as path from "path";
2
1
  /**
3
2
  * Mocha adapter for skyramp_run_existing_tests. Normalizes `mocha --reporter
4
3
  * json` (emitted to stdout) into the neutral shape. Mocha does not tag
@@ -30,7 +29,6 @@ function toResult(t, status) {
30
29
  return {
31
30
  testId: `${file} › ${full}`,
32
31
  file,
33
- ...(path.isAbsolute(file) ? { absoluteFile: file } : {}),
34
32
  status,
35
33
  message: status === "fail" || status === "error" ? messageOf(t) : undefined,
36
34
  durationMs: Math.round(t.duration ?? 0),
@@ -12,7 +12,6 @@
12
12
  * builds the flags appended to the suite's `testRunCommand`. The IO shell that
13
13
  * actually spawns the run lives in `runExistingTestsTool`.
14
14
  */
15
- import * as path from "path";
16
15
  import { stripVTControlCharacters } from "util";
17
16
  /**
18
17
  * A Playwright project (or spec file) is "infra" when it exists to bring up /
@@ -68,7 +67,6 @@ function messageOf(test) {
68
67
  }
69
68
  export function parsePlaywrightJson(report, opts) {
70
69
  const rep = (report ?? {});
71
- const rootDir = rep.config?.rootDir;
72
70
  const collected = [];
73
71
  for (const fileSuite of rep.suites ?? []) {
74
72
  collectSpecs(fileSuite, fileSuite.file ?? "", [], collected);
@@ -120,7 +118,6 @@ export function parsePlaywrightJson(report, opts) {
120
118
  results.push({
121
119
  testId,
122
120
  file,
123
- ...(rootDir ? { absoluteFile: path.resolve(rootDir, file) } : {}),
124
121
  status,
125
122
  message: status === "fail" || status === "error" ? messageOf(test) : undefined,
126
123
  durationMs,
@@ -1,4 +1,3 @@
1
- import * as path from "path";
2
1
  /**
3
2
  * pytest adapter for skyramp_run_existing_tests. Normalizes the
4
3
  * `pytest-json-report` (`--json-report`) output into the neutral result shape.
@@ -40,8 +39,6 @@ function durationMsOf(test) {
40
39
  export function parsePytestJson(report, opts) {
41
40
  const rep = (report ?? {});
42
41
  const tests = rep.tests ?? [];
43
- const root = rep.root;
44
- const abs = (f) => (root ? { absoluteFile: path.resolve(root, f) } : {});
45
42
  // Real test outcomes.
46
43
  const testResults = tests.map((t) => {
47
44
  const status = mapOutcome(t.outcome);
@@ -49,7 +46,6 @@ export function parsePytestJson(report, opts) {
49
46
  return {
50
47
  testId: nodeid,
51
48
  file: fileOf(nodeid),
52
- ...abs(fileOf(nodeid)),
53
49
  status,
54
50
  message: status === "fail" || status === "error" ? messageOf(t) : undefined,
55
51
  durationMs: durationMsOf(t),
@@ -65,7 +61,6 @@ export function parsePytestJson(report, opts) {
65
61
  return {
66
62
  testId: `${nodeid} › (collection error)`,
67
63
  file: fileOf(nodeid),
68
- ...abs(fileOf(nodeid)),
69
64
  status: "error",
70
65
  message: lastLine(c.longrepr),
71
66
  durationMs: 0,
@@ -77,13 +72,6 @@ export function parsePytestJson(report, opts) {
77
72
  // When collection failed AND no real test ran, the suite as a whole could not be
78
73
  // collected — environmental (a wall of red), not PR signal. When some real tests
79
74
  // DID run, the collection errors above stand as ordinary PR-signal error results.
80
- // NOTE: a collection error names the file that failed to import, but says nothing
81
- // about WHY. A missing settings module, a database that is down or any shared
82
- // dependency failure surfaces per-module, landing on exactly the files the run
83
- // selected — indistinguishable here from a file the PR itself broke. So a run that
84
- // collected nothing stays unhealthy even when scoped, and the file keeps its
85
- // Unknown baseline. Treating it as PR signal would let an environment outage be
86
- // reported as a test the change broke, and then repaired.
87
75
  if (collectionErrors.length > 0 && testResults.length === 0) {
88
76
  environmentHealthy = false;
89
77
  const first = collectionErrors[0];
@@ -55,7 +55,7 @@ export async function uploadFileToRFI(page, fileName, tabName) {
55
55
  These ARE near-duplicates. Same primitive sequence; the only material difference is the \`.first()\` qualifier on the locator. \`tabName\` is unused in the body — recorder noise, drop it. Correct merge:
56
56
 
57
57
  \`\`\`ts
58
- export async function uploadFileViaChooser(page, fileName) {
58
+ export async function uploadFileViaChooser(page: Page, fileName: string) {
59
59
  await page.waitForTimeout(1500);
60
60
  const responsePromise = page.waitForResponse("**/api/files/upload**");
61
61
  const fileUploadPromise = page.waitForEvent("filechooser");
@@ -69,6 +69,15 @@ export async function uploadFileViaChooser(page, fileName) {
69
69
 
70
70
  Both test files import and call \`uploadFileViaChooser(page, fileName)\`.`;
71
71
  }
72
+ /** Every step that authors an import of the shared module states this: STEP 4b
73
+ * merges, STEP 5b seeds, STEP 5c rewires a sibling — and a run with no siblings
74
+ * jumps straight to 5b, so a rule living in one step only is skippable. The
75
+ * verifier is deterministic, so a skipped rule is a failed run. */
76
+ const pyImportRule = (fileName) => {
77
+ const stem = fileName.replace(/\.[^.]+$/, "");
78
+ return `**Import form (Python)**: if the test's directory contains \`__init__.py\`, a bare \`from ${stem} import …\` will NOT resolve when the customer runs pytest from their repo root — the test dies at collection. Write the fallback pair, which works there AND in the Skyramp executor: \`try:\` / \` from .${stem} import …\` / \`except ImportError:\` / \` from ${stem} import …\` — with \`…\` the names this test actually uses, unchanged between the two arms. Without \`__init__.py\` the bare form alone is correct — do not add the fallback.`;
79
+ };
80
+ const pyImportRuleLine = (fileName) => `- ${pyImportRule(fileName)}`;
72
81
  export function getCodeReusePrompt(testFile, language, framework, scopedPoms, testType) {
73
82
  const lang = language.toLowerCase();
74
83
  if (isPomAwareTarget(lang, framework, testType)) {
@@ -111,6 +120,8 @@ export function getCodeReusePrompt(testFile, language, framework, scopedPoms, te
111
120
 
112
121
  skyramp_modularization already ran on ${testFile} and extracted its steps into named helper functions. Those helpers belong in the shared \`${fileName}\` so later tests import them instead of re-defining them.
113
122
 
123
+ ${pyImportRuleLine(fileName)}
124
+
114
125
  For each helper function still defined locally in ${testFile} (i.e. not already merged by STEP 4b):
115
126
  1. **CREATE** \`${fileName}\` if it does not exist, with the standard header:
116
127
  \`\`\`${ext}
@@ -148,6 +159,8 @@ This step MOVES helpers that skyramp_modularization already created — it does
148
159
 
149
160
  **Run this step EVEN IF STEP 3 already satisfied ${testFile} and STEPS 4–5b did nothing.** ${inlineRequestRule}
150
161
 
162
+ ${pyImportRuleLine(fileName)} (this applies to the import you add to the SIBLING file too — its directory is what decides the form.)
163
+
151
164
  1. Grep for the other Skyramp-generated test files exactly as in STEP 4 (codegen marker \`${codegenMarker}\`, excluding ${testFile} and \`${fileName}\`).
152
165
  2. In each, find every inline request block whose method+path matches a helper now defined in \`${fileName}\`.
153
166
  3. For each match: import the helper into that file and replace the block with the call, passing the block's literals (or expressions) as arguments; lift a literal the helper lacks to a defaulted parameter in \`${fileName}\`. Change NOTHING else in that file — its other steps, assertions and order stay verbatim.
@@ -286,7 +299,8 @@ For each helper defined locally in ${testFile}:
286
299
 
287
300
  - **Conservative bias**: a wrong merge breaks both tests; a missed merge leaves a duplicate. When in doubt, do NOT merge.
288
301
  - **No "cleanup"**: the merged body must mirror the original primitive sequence — same waits, same correlation patterns, same order. This is not refactoring.
289
- - **Preserve typing style**: if originals used \`page: any\` (or untyped \`page\`), keep that convention.
302
+ ${pyImportRuleLine(fileName)}
303
+ - **Typed parameters (TypeScript)**: the merged helper types every parameter (\`page: Page\`, values by their type); add \`import type { Page } from '@playwright/test'\` to the utils file if it is missing. Never \`page: any\` or an untyped \`page\` — that fails a repo that type-checks its tests under \`noImplicitAny\`.
290
304
  - **Don't merge with helpers already in \`${fileName}\`**: those are STEP 3's territory. STEP 4b is strictly cross-test merge between sibling Skyramp-generated test files.
291
305
 
292
306
  ---
@@ -336,6 +350,7 @@ ${seedStepBlock}${siblingCallSitesStep}## STEP 6: VERIFY AND VALIDATE
336
350
  8. **REMOVE** any helper functions that are not being used after refactoring
337
351
  9. **NEVER** refactor, reorganize, or restructure existing source test files beyond moving helpers${inlineRequestRule ? " — and, in this flow, replacing an inline request block with a call to the shared helper it duplicates (STEP 3 / STEP 5c)" : ""}
338
352
  10. **RUN TESTS** to ensure functionality is preserved after refactoring
353
+ 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
339
354
 
340
355
  **FINAL REMINDER: DO NOT CREATE HELPER FUNCTIONS FROM SCRATCH**
341
356
  ${finalReminderScope}
@@ -206,7 +206,7 @@ Before editing the given file, you must output a \`<thinking>\` block. The aim o
206
206
  1. Scan the given test file and expected responses based on the test recommendations for the code change tested.
207
207
  2. Classify each response first by its response status type and then assign the applicable assertion rules to the response.
208
208
  1. Success with body (2xx with a response body): all assertion rules below may apply — echo-back of request fields, computed response fields, array / items validation, and chained values across steps.
209
- 2. Success with no body (204, or 202 with empty body): assert the status code only. Also apply chained-values rules if a follow-up step uses this response's ID.
209
+ 2. Success with no body (200/202/204 with an empty body — e.g. logout/cancel/submit action endpoints): assert the status code only. Also apply chained-values rules if a follow-up step uses this response's ID.
210
210
  3. Error response (4xx/5xx with a body): assert every error body field with its exact value plus array / items validation on the \`errors[]\` array (exact length + per-item fields + next index undefined). Status code alone is never sufficient when a body is present — for example, also assert \`errors.0.extensions.code == 'INVALID_PAYLOAD'\` and that \`errors.1\` is undefined.
211
211
  3. For each in-scope response, output one JSON object using the template below. The output is an array — one object per in-scope response.
212
212
  - \`step\`: the HTTP method, path, and response variable name for this request (e.g. \`POST /products → products_POST_response\`).
@@ -91,6 +91,7 @@ Create one service entry per deployable unit. You MUST include every backend/API
91
91
  - Single service: set testDirectory to tests/skyramp.
92
92
  - Multiple services or monorepos: set testDirectory to tests/skyramp/<serviceDirName>, where <serviceDirName> is the service directory name with path separators and whitespace replaced by hyphens.
93
93
  Framework config takes precedence. Use the Skyramp deterministic fallback only when no framework-configured test directory is available.
94
+ NEVER set testDirectory to \`.skyramp\`, to any path inside it, or to \`.\` or the repo root, whatever the framework config or existing test files say. \`.skyramp\` is the executor's own working area: it holds the run videos and executor artefacts. skyramp_init_workspace rejects all of these, and the generation tools refuse to write there.
94
95
  </basic_fields>
95
96
 
96
97
  ### API fields
@@ -160,7 +161,7 @@ Before calling skyramp_init_workspace, confirm all of the following:
160
161
  4. Every service has api.baseUrl set to a valid, discoverable URL. Use localhost for local services or the actual deployment URL for cloud or external services. Never fabricate a URL.
161
162
  5. Every service with authType apiKey has authHeader explicitly set to the actual custom header name (such as "X-API-Key" or "X-Admin-Key"). If you cannot find the header name in the source code, env vars, or README, do NOT use authType apiKey. Use authType none instead and add a YAML comment explaining auth is unresolved.
162
163
  6. framework matches language (python uses pytest or robot, typescript or javascript uses playwright, java uses junit).
163
- 7. testDirectory follows the stable resolution rules above: framework config file when present (Playwright testDir in playwright.config.ts, pytest testpaths in pytest.ini or pyproject.toml, JUnit test source dir in pom.xml or build.gradle); otherwise the deterministic default (tests/skyramp for a single service, tests/skyramp/<serviceDirName> for multiple services).
164
+ 7. testDirectory follows the stable resolution rules above: framework config file when present (Playwright testDir in playwright.config.ts, pytest testpaths in pytest.ini or pyproject.toml, JUnit test source dir in pom.xml or build.gradle); otherwise the deterministic default (tests/skyramp for a single service, tests/skyramp/<serviceDirName> for multiple services). No service uses \`.skyramp\`, \`.\` or the repo root.
164
165
  8. If serverStartCommand is provided, it matches the runtime. If serverStopCommand is provided, runtime is "docker" and the command is a Docker command.
165
166
  9. For services in docker-compose.yml: runtime MUST be "docker" and the command MUST be a docker command such as "docker compose up -d --build <service-name>" when the service has a build context. Always include it since it is derivable from the service name.
166
167
  10. NEVER use application-level commands (uvicorn, npm, node, python, java, etc.) with runtime "docker".
@@ -41,13 +41,13 @@ ${p.utilsReuse ? "6" : "5"}. **SKIP to Step ${_plan.labels.VERIFY}** for verific
41
41
  - Examples: "create product", "fill form", "verify result"
42
42
  - Must be cohesive - related operations that achieve one goal${p.utilsReuse
43
43
  ? `
44
- - This project consolidates test helpers into a shared utils file, so EVERY logical section that performs a user action (open a form, fill and submit it, add or remove an item, apply an option) becomes a named helper the section does NOT need to repeat. Another scenario's test will call the same helper with its own values.`
44
+ - This project consolidates test helpers into a shared utils file. Extract a user-action section as a helper when another scenario's test would perform it AS-IS with its own values — sign in, open an entity's form or dialog, add or remove an item, apply a filter or option, dismiss a bannereven if it appears once here. Do NOT extract a section that exists only in this scenario's narrative (a specific multi-step data entry, a check sequence over this test's own values) just because the utils file exists: a single-use helper adds code and hides this scenario's locators behind a name, and the shared file then carries it for every later run.`
45
45
  : ""}
46
46
 
47
47
  **Don't extract:**
48
48
  - Code less than 5 lines (unless highly repetitive${p.utilsReuse
49
- ? ", or a cohesive user-action section — those become helpers regardless of length, per B above"
50
- : ""})
49
+ ? ", or a cross-test user action per B above — those become helpers regardless of length"
50
+ : ""})${p.utilsReuse ? "\n- A section only this scenario performs AND that appears once in this file — keep it inline (see B above; if it repeats 2+ times in this file, scenario A still applies and it is extracted)" : ""}
51
51
  - Navigation sequences (\`navbar-\` clicks, \`page.goto\`)`)
52
52
  .step("EXTRACT", "EXTRACT INTO HELPERS", (p) => `**GOLDEN RULE: Copy the original code exactly, only add parameters for values that differ**
53
53
 
@@ -123,8 +123,10 @@ async function createOrderWithItems(...) { }
123
123
 
124
124
  **RIGHT - Good practices:**
125
125
  \`\`\`typescript
126
+ import type { Page } from '@playwright/test';
127
+
126
128
  // CORRECT - Extract with parameter, no new logic
127
- async function addItem(page, name: string) {
129
+ async function addItem(page: Page, name: string) {
128
130
  await page.fill("#name", name); // Replace literal with parameter
129
131
  }
130
132
 
@@ -136,8 +138,8 @@ await addItem(page, "item3");
136
138
  \`\`\``)
137
139
  .step("USE", "CALL HELPERS WITH EXACT SAME VALUES", () => `Use the EXACT same values from the original test when calling helpers.`)
138
140
  .step("VERIFY", "VERIFY - CRITICAL CHECKS", (p) => `**BUGS TO AVOID:**
139
- - [ ] **NO TYPE ANNOTATIONS FOR \`page\` IN FUNCTION SIGNATURES** - Parameters must be untyped (e.g., \`page\`, not \`page: Page\`)
140
- - [ ] **NO RETURN TYPES** - Do not add \`: Promise<void>\` or any return type annotations
141
+ - [ ] **EVERY PARAMETER IS TYPED (TypeScript)** - \`page: Page\` with \`import type { Page } from '@playwright/test'\` once at the top of the file; other parameters by their value type (\`name: string\`, \`quantity: number\`). An untyped parameter fails a repo that type-checks its tests under \`noImplicitAny\`. JavaScript files carry no annotations.
142
+ - [ ] **Return types are optional** - inferred returns are fine under strict mode; if you write one, it is the real type, never \`any\`
141
143
  - [ ] **ALL field mappings verified** - EVERY \`.fill()\` uses the correct parameter. Check field gets quantity parameter, NOT hardcoded
142
144
  - [ ] **No duplicate helpers** - Consolidate similar helpers (e.g., one createOrder vs createSimpleOrder + createOrderWithItems)
143
145
  - [ ] **No nested helpers** - Helpers don't call other helpers unnecessarily
@@ -150,6 +152,7 @@ ${p.utilsReuse
150
152
  - [ ] Every helper name is a canonical intent (\`openEditOrderForm\`, \`applyDiscount\`) with no scenario value or adjective in it
151
153
  - [ ] No helper contains a value assertion (${BROWSER_VALUE_ASSERTIONS}) — each sits in the test body, right after the helper call it verifies
152
154
  - [ ] Every selector inside a helper is byte-identical to the original test
155
+ - [ ] The assertion COUNT is unchanged: every \`expect(\` from the original is still in this file or inside a helper it calls, and the \`page.on('pageerror'...)\` guard and its final \`expect\` over the collected errors are still here — cross-cutting guards are never removed (the verify step counts both)
153
156
  `
154
157
  : ""}
155
158
  **DATA INTEGRITY:**
@@ -559,7 +559,7 @@ For each helper defined locally in ${testFile}:
559
559
 
560
560
  - **Conservative bias**: a wrong merge breaks both tests; a missed merge leaves a duplicate. When in doubt, do NOT merge.
561
561
  - **No "cleanup"**: the merged body must mirror the original primitive sequence — same waits, same correlation patterns, same order. This is not refactoring.
562
- - **Preserve typing style**: if originals used \`page: any\` (or untyped \`page\`), keep that convention.
562
+ - **Typed parameters (TypeScript)**: the merged helper types every parameter (\`page: Page\`, values by their type); add \`import type { Page } from '@playwright/test'\` if the file lacks it. Never \`page: any\` or an untyped \`page\`.
563
563
  - **Don't merge with helpers already in \`${utilsFile}\`**: those are STEP 3's territory. STEP 4b is strictly cross-test merge between sibling Skyramp-generated test files.
564
564
 
565
565
  ---
@@ -45,11 +45,11 @@ export const API_HELPERS = {
45
45
  sameStep: "the SAME method+path",
46
46
  liftableLiterals: API_LIFTABLE,
47
47
  structuralNoise: "",
48
- namingRule: "**NAME** each helper by method + resource ONLY: `create_product`, `update_order`, `get_order`, `get_orders_list`. Scenario adjectives in names are FORBIDDEN (`create_discount_test_product`, `update_order_50_discount`) — other tests define the same step, and a shared utils file can hold only one canonical name per step. Scenario data belongs in ARGUMENTS, never in the name.",
48
+ namingRule: "**NAME** each helper by method + resource ONLY, written in the naming convention of the language it is emitted in — Python `create_product`, `update_order`, `get_order`, `get_orders_list`; TypeScript/JavaScript `createProduct`, `updateOrder`, `getOrder`, `getOrdersList`. Match the surrounding test file: snake_case helpers in a camelCase TS suite read as foreign code the repo did not write. Scenario adjectives in names are FORBIDDEN (`create_discount_test_product`, `updateOrder50Discount`) — other tests define the same step, and a shared utils file can hold only one canonical name per step. Scenario data belongs in ARGUMENTS, never in the name.",
49
49
  assertionRule: `**THE ONLY ASSERTION INSIDE A SHARED HELPER IS THE STATUS CODE**, written in the test's own assertion syntax (Python \`assert response.status_code == expected_status\`; TypeScript/JavaScript \`expect(response.status).toBe(expectedStatus)\`), with the expected status a parameter defaulted to this scenario's expected code. EVERY response-body assertion (echo-backs, computed totals, field checks) stays in the test body, applied to the response the helper returns. ${SHARED_HELPER_WHY}`,
50
50
  relocationTarget: "the test body, applied to the response the helper returns",
51
51
  moveRule: "",
52
- namingExample: "E.g. `update_order`, not `update_order_discount` or `update_order_50_discount`.",
52
+ namingExample: "E.g. `update_order` (Python) / `updateOrder` (TypeScript), not `update_order_discount` or `updateOrder50Discount`.",
53
53
  oneHelperRule: (fileName) => `One method+path = one helper in \`${fileName}\`.`,
54
54
  // The API section below carries its own worked example; the generic
55
55
  // Playwright one would teach intent-naming and locator merges to a Python prompt.
@@ -81,7 +81,7 @@ export const BROWSER_HELPERS = {
81
81
  kind: "browser",
82
82
  sameStep: "the SAME action sequence (same Playwright primitives in the same order, selectors identical)",
83
83
  liftableLiterals: BROWSER_LIFTABLE,
84
- structuralNoise: "`waitForTimeout(...)` lines and the `getValue(playwrightRequestN, ...)` value wrapper around a fill are RECORDER NOISE, not structure — two bodies that differ only by them perform the same step; keep the utils version (it carries the waits) and import it",
84
+ structuralNoise: "`waitForTimeout(...)` lines and the `getValue(playwrightRequestN, ...)` value wrapper around a fill are RECORDER NOISE, not structure — two bodies that differ only by them perform the same step, so merge them; keep the body WITHOUT the `waitForTimeout` lines. A fixed sleep is not a structural wait: it is the one line of the pair that carries no meaning, and in a shared helper every importer pays it on every run. Where a step genuinely needs to wait, wait on the element the next line touches (`toBeVisible`, `toBeEnabled`)",
85
85
  namingRule: "**NAME** each helper by the user INTENT it performs, shared across tests: `openEditOrderForm`, `applyDiscount`, `submitProductForm`, `removeOrderItem`. Scenario values or adjectives in names are FORBIDDEN (`applyFiftyPercentDiscount`, `editOrder1Discount`, `createExpensiveProduct`) — another test performs the same action with different data, and a shared utils file holds ONE canonical name per action. Scenario data belongs in ARGUMENTS, never in the name.",
86
86
  assertionRule: `**A SHARED HELPER CONTAINS ACTIONS AND STRUCTURAL WAITS ONLY**: clicks, fills, selects, ${BROWSER_STRUCTURAL_WAITS}. EVERY VALUE ASSERTION (${BROWSER_VALUE_ASSERTIONS}) stays in the test body, placed immediately after the helper call it verifies, with its selector verbatim and its expected value unchanged. ${SHARED_HELPER_WHY}`,
87
87
  relocationTarget: "the test body, immediately after the helper call, selector verbatim",
@@ -100,7 +100,7 @@ export async function applyDiscount(page) {
100
100
 
101
101
  \`${testFile}\` (current) has:
102
102
  \`\`\`ts
103
- export async function applyTwentyPercentDiscount(page) {
103
+ export async function applyTwentyPercentDiscount(page: Page) {
104
104
  await page.getByTestId("edit-order-input-discount").fill("20");
105
105
  await page.getByTestId("edit-order-submit-btn").click();
106
106
  }
@@ -109,7 +109,7 @@ export async function applyTwentyPercentDiscount(page) {
109
109
  These ARE near-duplicates: same primitive sequence, selectors byte-identical, the only difference is the fill literal. Correct merge (name = intent, the literal becomes a parameter passed exactly as it was written — no conversion or other logic added, selectors untouched):
110
110
 
111
111
  \`\`\`ts
112
- export async function applyDiscount(page, discountPercent) {
112
+ export async function applyDiscount(page: Page, discountPercent: string) {
113
113
  await page.getByTestId("edit-order-input-discount").fill(discountPercent);
114
114
  await page.getByTestId("edit-order-submit-btn").click();
115
115
  }
@@ -212,7 +212,7 @@ ${maintenanceBeforeExecStep}
212
212
 
213
213
  e. Call \`skyramp_actions\` with \`stateFile\` (from \`skyramp_analyze_changes\` output) and apply the edits it returns.
214
214
 
215
- f. Verify external-test fixes. **This step is not optional and it is the easiest one to forget — you have just edited files in step 2(e), so come back here before you move on to anything else.** It applies whenever step 2(a) reported a real pass/fail result for a file you then edited. It does NOT apply when step 2(a) returned \`skipped: true\` or \`ran: 0\` for every suite — there is no baseline to compare against, so say so in your report instead of re-running. When it applies: re-run those \`[external]\` files with \`skyramp_run_existing_tests\` (\`mode: "verify"\`, \`stateFile\`) the server reads each file's result back as its \`afterStatus\`. Editing an \`[external]\` file that step 2(a) confirmed failing and NOT re-running it leaves your own fix unverified — you would be reporting a repair you never saw work. A still-failing verify is surfaced in the report — do not loop.
215
+ f. Verify external-test fixes. **This step is not optional and it is the easiest one to forget — you have just edited files in step 2(e), so come back here before you move on to anything else.** It applies whenever step 2(a) reported a real pass/fail result for a file you then edited. It does NOT apply when step 2(a) returned \`skipped: true\` or \`ran: 0\` for every suite — there is no baseline to compare against, so say so in your report instead of re-running. When it applies: re-run those \`[external]\` files with \`skyramp_run_existing_tests\` (\`mode: "verify"\`, \`stateFile\`) and record each file's result as its \`afterStatus\`. Editing an \`[external]\` file that step 2(a) confirmed failing and NOT re-running it leaves your own fix unverified — you would be reporting a repair you never saw work. A still-failing verify is surfaced in the report — do not loop.
216
216
 
217
217
  3. **Code review:** Find the logic bugs in the code that this change touches. Read the implementation of each changed endpoint: the route handler, and the functions that it calls to read or write data. For a changed screen, read the component and the functions that it calls. Read these files even when the diff does not contain them — a defect often sits in the code that the change depends on. Report each finding in \`issuesFound\` with a severity, and say which file and line holds it. Common patterns to flag:
218
218
  - Computed fields not recalculated after mutation (e.g. \`total_amount\` unchanged after items are added/removed)
@@ -7,12 +7,14 @@ import { extractPathname, resolveQueryParamsForPath, contributesNothing, } from
7
7
  import { getPathParameterValidationError, OUTPUT_DIR_FIELD_NAME, PATH_PARAMS_FIELD_NAME, QUERY_PARAMS_FIELD_NAME, FORM_PARAMS_FIELD_NAME, validateParams, validatePath, validateRequestData, } from "../utils/utils.js";
8
8
  import { getEntryPoint } from "../utils/telemetry.js";
9
9
  import { getLanguageSteps } from "../utils/language-helper.js";
10
- import { TestType } from "../types/TestTypes.js";
10
+ import { ASSERTION_ENHANCEABLE_TEST_TYPES, TestType, MOCK_TYPE, } from "../types/TestTypes.js";
11
11
  import { logger } from "../utils/logger.js";
12
12
  import { normalizeLanguageParams } from "../utils/normalizeParams.js";
13
13
  import { stageGeneratedPaths, resolveOutputDir } from "../utils/gitStaging.js";
14
+ import { isInsideExecutorWorkDir, executorWorkDirRefusal, generationTargets, } from "../utils/executorWorkDir.js";
14
15
  import { getTestsRepoDir } from "../utils/AnalysisStateManager.js";
15
16
  import { recordReuseHandOff } from "../tools/code-refactor/reuse-state.js";
17
+ import { recordAssertionBaseline } from "../tools/code-refactor/assertion-state.js";
16
18
  import { isModularizeFirstTarget } from "../utils/reuseRouting.js";
17
19
  /**
18
20
  * Derive the framework actually used for generation from the (normalized)
@@ -73,6 +75,18 @@ export class TestGenerationService {
73
75
  });
74
76
  params.outputDir = resolved;
75
77
  }
78
+ // Every generation tool reaches the filesystem through this method, so one
79
+ // check here covers them all. Nothing written under `.skyramp` is delivered
80
+ // as a test — an observed run put 7 specs there because the only absolute
81
+ // path the prompt gave it was the trace-zip directory. The error names the
82
+ // field and the replacement so the agent can correct itself in one round trip.
83
+ const badTarget = generationTargets(params.outputDir, params.output).find(isInsideExecutorWorkDir);
84
+ if (badTarget) {
85
+ return {
86
+ content: [{ type: "text", text: executorWorkDirRefusal(badTarget) }],
87
+ isError: true,
88
+ };
89
+ }
76
90
  // Log prompt parameter using reusable utility
77
91
  logger.info("Generating test", {
78
92
  prompt: params.prompt,
@@ -93,7 +107,20 @@ export class TestGenerationService {
93
107
  // The files this call wrote, as codegen reports them — the reuse hand-off is
94
108
  // recorded per FILE (SKYR-4220).
95
109
  const written = generatedFilesFrom(result);
110
+ // Seed the assertion-enhancement baseline SERVER-SIDE at generation time
111
+ // (same pattern as recordReuseHandOff below): the execute-time
112
+ // verification gate arms even if the agent never calls
113
+ // skyramp_enhance_assertions at all — the last prompt-mediated entry
114
+ // point. Best-effort; no run state means the gate stays open as before.
96
115
  const testType = this.getTestType();
116
+ if (params.enhanceAssertions &&
117
+ testType !== MOCK_TYPE &&
118
+ ASSERTION_ENHANCEABLE_TEST_TYPES.includes(testType) &&
119
+ (testType !== TestType.CONTRACT || params.providerMode !== false)) {
120
+ for (const file of written) {
121
+ await recordAssertionBaseline(file, testType, "generation");
122
+ }
123
+ }
97
124
  const languageSteps = getLanguageSteps({
98
125
  language: params.language || "",
99
126
  testType: testType,
@@ -0,0 +1,91 @@
1
+ import { type AssertionBaseline, type AssertionEnhanceType, type AssertionVerifyResult } from "../../utils/assertion-verify/index.js";
2
+ import type { AssertionOutcome, AssertionRecord } from "../../types/AssertionOutcome.js";
3
+ import type { TestType } from "../../types/TestTypes.js";
4
+ import type { RepoCheckout } from "../../utils/reportVerification.js";
5
+ /**
6
+ * Persistence for the assertion-enhancement verification loop, keyed by the
7
+ * spec's CANONICAL absolute path in `UnifiedAnalysisState.assertionOutcomes`.
8
+ * Path keying (not basename, unlike reuseOutcomes — nothing here needs to
9
+ * match report rows) removes the multi-repo basename-collision class outright,
10
+ * and canonicalization closes the alias bypass: `dir//x.spec.ts` and
11
+ * `dir/./x.spec.ts` must hit the same record as `dir/x.spec.ts`.
12
+ *
13
+ * Every write is best-effort: verification must never be able to fail an
14
+ * enhancement that otherwise succeeded, so a missing/unreadable state file
15
+ * degrades to stateless checks and open gates — always with a warning.
16
+ */
17
+ /** Canonical identity of a test file — all keying and ownership comparisons
18
+ * go through this so a non-normalized agent-supplied path cannot fork or
19
+ * bypass a record. realpath also collapses symlinked directories; a path
20
+ * that does not (yet) exist falls back to plain resolution. */
21
+ export declare function canonicalTestPath(testFile: string): string;
22
+ /** A record only counts for `testFile` when its stored path canonicalizes to
23
+ * the same spec — the single home for the ownership invariant (previously
24
+ * copied in baselineFrom, recordAssertionVerdict, and the enhance tool). */
25
+ export declare function trustedRecord(record: AssertionRecord | undefined, testFile: string): AssertionRecord | undefined;
26
+ /** One spec's record, or undefined when there is no run state / no record. */
27
+ export declare function readAssertionRecord(testFile: string, explicitStateFile?: string): Promise<AssertionRecord | undefined>;
28
+ /**
29
+ * Snapshot the file at instruction hand-out time — the baseline verify measures
30
+ * against. Keep-first rule: while no Passed verdict exists for the same file and
31
+ * enhanceType, a repeat hand-out does NOT re-baseline (re-calling the tool after
32
+ * half-editing must not shrink the required delta). A hand-out after a Passed
33
+ * verdict, or with a different enhanceType, starts a new enhancement round.
34
+ */
35
+ export declare function recordAssertionBaseline(testFile: string, testType: TestType, enhanceType: AssertionEnhanceType, explicitStateFile?: string): Promise<void>;
36
+ /** The baseline verify measures against, from a record; undefined when the
37
+ * record carries no snapshot or belongs to a different canonical path. */
38
+ export declare function baselineFrom(record: AssertionRecord | undefined, testFile: string): AssertionBaseline | undefined;
39
+ /** Record what one verify pass established. The verdict is written to the
40
+ * state file the record actually LIVES in (not blindly to the explicit path —
41
+ * a mismatched `stateFile` would otherwise split verdict from baseline).
42
+ * Never writes onto a record owned by a different canonical path, and never
43
+ * fails silently. */
44
+ export declare function recordAssertionVerdict(testFile: string, result: AssertionVerifyResult, explicitStateFile?: string): Promise<void>;
45
+ /**
46
+ * Deterministic execute-time verification: skyramp_execute_test calls this
47
+ * itself, so the check runs whether or not the agent ever called `verify: true`
48
+ * — a prose instruction can be ignored, a server-side call cannot. Returns the
49
+ * feedback text when the assertions are insufficient (the caller returns it
50
+ * instead of executing), or undefined to proceed.
51
+ *
52
+ * Self-clearing: the checks run on the file's CURRENT content, so fixing the
53
+ * assertions and re-executing passes — no separate verify call is required.
54
+ * Skipped once a verdict has PASSED: sanctioned later rewrites (a `.raw.bak`
55
+ * restore during execution fix-up) would otherwise false-fail the differential
56
+ * gates. Modularization needs no special handling: the shared-helper policy
57
+ * keeps every value assertion in the test body, so the baseline comparison
58
+ * stays valid across it. Fails open on no baseline and unreadable state/file.
59
+ */
60
+ export declare function assertionFeedbackForExecution(testFile: string, explicitStateFile?: string): Promise<string | undefined>;
61
+ /**
62
+ * Count one execution of this spec (called by skyramp_execute_test after its
63
+ * gates pass, best-effort). Creates a minimal record when none exists so an
64
+ * executed-but-never-enhanced spec is still visible to the report cross-check.
65
+ */
66
+ export declare function recordAssertionExecution(testFile: string, testType: TestType, explicitStateFile?: string): Promise<void>;
67
+ /** A repo checkout as listRepoCheckouts returns it — the canonical type from
68
+ * reportVerification, re-exported (a local re-declaration had drifted:
69
+ * `root` weakened to optional, forcing a redundant guard at each use). */
70
+ export type RepoCheckoutRef = RepoCheckout;
71
+ /** The record for a report row, matched by the canonical-path keys' basenames
72
+ * and then GUARDED: the record's testType must match the row's, and when the
73
+ * run's checkouts are known, the record's path must live in the row's repo
74
+ * (primary when the row names none). Without the guards, a basename shared
75
+ * across repos published one spec's proof-of-work under another's name — and
76
+ * the victim was exactly the never-enhanced spec the field exists to expose.
77
+ * Ambiguity after filtering still fails open: wrong attribution is worse
78
+ * than omission. */
79
+ export declare function findAssertionRecordByFileName(outcomes: Record<string, AssertionRecord>, row: {
80
+ fileName: string;
81
+ testType?: string;
82
+ repository?: string;
83
+ }, checkouts: RepoCheckoutRef[]): AssertionRecord | undefined;
84
+ /**
85
+ * Report-facing proof-of-work summary re-derived from the spec as delivered
86
+ * NOW — final metrics come from the file, not from verify-time state, so
87
+ * post-verify edits cannot publish stale counts. Always returns the status
88
+ * fields (verification, declinedReason, executionCount — they come from the
89
+ * record); the metric fields are omitted when the file cannot supply them.
90
+ */
91
+ export declare function rederiveAssertionOutcome(record: AssertionRecord): Promise<AssertionOutcome>;