@skyramp/mcp 0.3.2-rc.pom-4 → 0.3.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.
Files changed (132) hide show
  1. package/build/adapters/jestAdapter.d.ts +14 -0
  2. package/build/adapters/jestAdapter.js +113 -0
  3. package/build/adapters/mochaAdapter.d.ts +13 -0
  4. package/build/adapters/mochaAdapter.js +87 -0
  5. package/build/adapters/playwrightAdapter.d.ts +17 -0
  6. package/build/adapters/playwrightAdapter.js +182 -0
  7. package/build/adapters/pytestAdapter.d.ts +15 -0
  8. package/build/adapters/pytestAdapter.js +108 -0
  9. package/build/commands/commandLibrary.d.ts +1 -0
  10. package/build/commands/commandLibrary.js +19 -13
  11. package/build/commands/localDevTestChangesCommand.d.ts +15 -0
  12. package/build/commands/localDevTestChangesCommand.js +201 -0
  13. package/build/index.js +82 -6
  14. package/build/prompts/code-reuse.js +3 -0
  15. package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +1 -0
  16. package/build/prompts/enhance-assertions/sharedAssertionRules.js +17 -0
  17. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +11 -9
  18. package/build/prompts/local-dev/local-dev-plan.d.ts +35 -0
  19. package/build/prompts/local-dev/local-dev-plan.js +429 -0
  20. package/build/prompts/local-dev/local-dev-prompts.d.ts +4 -0
  21. package/build/prompts/local-dev/local-dev-prompts.js +190 -0
  22. package/build/prompts/prompt-utils.d.ts +8 -0
  23. package/build/prompts/prompt-utils.js +33 -0
  24. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +70 -7
  25. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +1 -1
  26. package/build/prompts/sut-setup/shared.js +19 -17
  27. package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +10 -1
  28. package/build/prompts/test-maintenance/drift-analysis-prompt.js +54 -1
  29. package/build/prompts/test-recommendation/analysisOutputPrompt.js +21 -29
  30. package/build/prompts/test-recommendation/scopeAssessment.d.ts +5 -2
  31. package/build/prompts/test-recommendation/scopeAssessment.js +78 -6
  32. package/build/prompts/test-recommendation/test-recommendation-prompt.js +41 -4
  33. package/build/prompts/testbot/testbot-prompts.d.ts +0 -5
  34. package/build/prompts/testbot/testbot-prompts.js +39 -55
  35. package/build/recommendation/planRanker.d.ts +15 -2
  36. package/build/recommendation/planRanker.js +76 -5
  37. package/build/resources/testbotResource.js +2 -1
  38. package/build/services/AnalyticsService.d.ts +1 -1
  39. package/build/services/TestExecutionService.d.ts +2 -1
  40. package/build/services/TestExecutionService.js +8 -3
  41. package/build/services/TestGenerationService.d.ts +2 -2
  42. package/build/services/TestGenerationService.js +39 -21
  43. package/build/services/containerEnv.js +3 -1
  44. package/build/tool-phases.js +7 -0
  45. package/build/tools/code-refactor/codeReuseTool.js +43 -4
  46. package/build/tools/code-refactor/enhanceAssertionsTool.js +68 -18
  47. package/build/tools/code-refactor/reuse-outcome.d.ts +109 -0
  48. package/build/tools/code-refactor/reuse-outcome.js +158 -0
  49. package/build/tools/code-refactor/reuse-state.d.ts +45 -0
  50. package/build/tools/code-refactor/reuse-state.js +140 -0
  51. package/build/tools/enrichTestWithMocksTool.d.ts +28 -0
  52. package/build/tools/enrichTestWithMocksTool.js +726 -0
  53. package/build/tools/executeSkyrampTestTool.d.ts +11 -0
  54. package/build/tools/executeSkyrampTestTool.js +62 -21
  55. package/build/tools/generate-tests/batchMockGenerationTool.d.ts +106 -0
  56. package/build/tools/generate-tests/batchMockGenerationTool.js +545 -0
  57. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +25 -23
  58. package/build/tools/generate-tests/generateContractRestTool.js +2 -2
  59. package/build/tools/generate-tests/generateE2ERestTool.d.ts +1 -1
  60. package/build/tools/generate-tests/generateE2ERestTool.js +1 -1
  61. package/build/tools/generate-tests/generateLoadRestTool.d.ts +1 -1
  62. package/build/tools/generate-tests/generateLoadRestTool.js +1 -1
  63. package/build/tools/generate-tests/generateMockRestTool.d.ts +179 -6
  64. package/build/tools/generate-tests/generateMockRestTool.js +391 -22
  65. package/build/tools/generate-tests/loadTestSchema.d.ts +1 -1
  66. package/build/tools/generate-tests/planGuard.js +2 -22
  67. package/build/tools/generateEnrichedIntegrationTestTool.d.ts +25 -0
  68. package/build/tools/generateEnrichedIntegrationTestTool.js +235 -0
  69. package/build/tools/localDevWorkerComposeTool.d.ts +24 -0
  70. package/build/tools/localDevWorkerComposeTool.js +264 -0
  71. package/build/tools/one-click/oneClickTool.d.ts +13 -0
  72. package/build/tools/one-click/oneClickTool.js +195 -24
  73. package/build/tools/preflightMockCheckTool.d.ts +2 -0
  74. package/build/tools/preflightMockCheckTool.js +96 -0
  75. package/build/tools/queryProxyMocksTool.d.ts +70 -0
  76. package/build/tools/queryProxyMocksTool.js +522 -0
  77. package/build/tools/runExistingTestsTool.d.ts +138 -0
  78. package/build/tools/runExistingTestsTool.js +644 -0
  79. package/build/tools/submitReportTool.js +30 -1
  80. package/build/tools/test-management/analyzeChangesTool.d.ts +2 -1
  81. package/build/tools/test-management/analyzeChangesTool.js +63 -40
  82. package/build/tools/test-management/analyzeTestHealthTool.d.ts +11 -0
  83. package/build/tools/test-management/analyzeTestHealthTool.js +63 -1
  84. package/build/tools/test-management/registerTestPlanTool.js +55 -7
  85. package/build/tools/trace/startTraceCollectionTool.js +3 -3
  86. package/build/types/ExternalTestExecution.d.ts +67 -0
  87. package/build/types/ExternalTestExecution.js +8 -0
  88. package/build/types/OneClickCommands.d.ts +1 -1
  89. package/build/types/Recommendation.d.ts +20 -0
  90. package/build/types/Recommendation.js +32 -6
  91. package/build/types/RepositoryAnalysis.d.ts +131 -14
  92. package/build/types/RepositoryAnalysis.js +16 -2
  93. package/build/types/ReuseOutcome.d.ts +63 -0
  94. package/build/types/ReuseOutcome.js +33 -0
  95. package/build/types/TestExecution.d.ts +1 -0
  96. package/build/types/TestTypes.d.ts +25 -7
  97. package/build/types/TestTypes.js +25 -7
  98. package/build/types/TestbotReport.d.ts +7 -0
  99. package/build/types/index.d.ts +2 -0
  100. package/build/types/index.js +1 -0
  101. package/build/utils/AnalysisStateManager.d.ts +33 -0
  102. package/build/utils/AnalysisStateManager.js +36 -2
  103. package/build/utils/analyze-openapi.js +18 -1
  104. package/build/utils/branchDiff.d.ts +17 -1
  105. package/build/utils/branchDiff.js +99 -14
  106. package/build/utils/featureFlags.d.ts +31 -0
  107. package/build/utils/featureFlags.js +37 -0
  108. package/build/utils/grpcMockValidation.d.ts +1 -0
  109. package/build/utils/grpcMockValidation.js +49 -0
  110. package/build/utils/httpMethodValidation.d.ts +4 -0
  111. package/build/utils/httpMethodValidation.js +15 -0
  112. package/build/utils/logger.js +1 -1
  113. package/build/utils/mockCompatibility.d.ts +49 -0
  114. package/build/utils/mockCompatibility.js +82 -0
  115. package/build/utils/pom-verify/verify.d.ts +5 -0
  116. package/build/utils/pom-verify/verify.js +1 -0
  117. package/build/utils/progress.js +10 -5
  118. package/build/utils/proxy-terminal.js +3 -3
  119. package/build/utils/routeParsers.d.ts +3 -9
  120. package/build/utils/routeParsers.js +79 -4
  121. package/build/utils/utils.js +2 -2
  122. package/build/utils/versions.d.ts +4 -3
  123. package/build/utils/versions.js +3 -1
  124. package/build/utils/workspaceAuth.d.ts +46 -0
  125. package/build/utils/workspaceAuth.js +156 -1
  126. package/build/workspace/frameworks.d.ts +11 -0
  127. package/build/workspace/frameworks.js +22 -0
  128. package/build/workspace/testSuites.d.ts +20 -0
  129. package/build/workspace/testSuites.js +17 -0
  130. package/build/workspace/workspace.d.ts +206 -24
  131. package/build/workspace/workspace.js +52 -2
  132. package/package.json +5 -2
@@ -0,0 +1,14 @@
1
+ import { ParsedExternalRun } from "../types/ExternalTestExecution.js";
2
+ export declare function parseJestJson(report: unknown, opts?: {
3
+ truncated?: boolean;
4
+ }): ParsedExternalRun;
5
+ export declare function buildJestArgs(input: {
6
+ selectors: string[];
7
+ mode: "confirm" | "verify" | "probe";
8
+ reportPath: string;
9
+ }): string[];
10
+ export declare function buildVitestArgs(input: {
11
+ selectors: string[];
12
+ mode: "confirm" | "verify" | "probe";
13
+ reportPath: string;
14
+ }): string[];
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Shared jest + vitest adapter for skyramp_run_existing_tests. vitest's json
3
+ * reporter is jest-compatible, so both use ONE parser; only the reporter flag
4
+ * differs (`--json` vs `--reporter=json`), hence two buildArgs. Native JSON
5
+ * (not junit): preserves the fail signal and file-level testExecError (mapped to
6
+ * `error`).
7
+ */
8
+ import { stripVTControlCharacters } from "util";
9
+ function mapAssertion(status) {
10
+ switch (status) {
11
+ case "passed":
12
+ return "pass";
13
+ case "failed":
14
+ return "fail";
15
+ case "pending":
16
+ case "skipped":
17
+ case "todo":
18
+ case "disabled":
19
+ return "skipped";
20
+ default:
21
+ return status ? "error" : "skipped";
22
+ }
23
+ }
24
+ export function parseJestJson(report, opts) {
25
+ const rep = (report ?? {});
26
+ const results = [];
27
+ for (const file of rep.testResults ?? []) {
28
+ const name = file.name ?? "(unknown)";
29
+ // A test file that failed to compile/run has no assertionResults. Jest
30
+ // surfaces the cause two ways depending on failure type: a structured
31
+ // `testExecError` (e.g. environment setup errors), OR — confirmed against a
32
+ // REAL jest run with a syntax error — `status: "failed"` plus a plain
33
+ // top-level `message` and no `testExecError` at all. Treat either as one
34
+ // synthetic `error` result (a broken test file is PR signal, unlike a
35
+ // Playwright global-setup failure).
36
+ const noAssertions = (file.assertionResults ?? []).length === 0;
37
+ const isExecFailure = noAssertions && (file.testExecError || file.status === "failed");
38
+ if (isExecFailure) {
39
+ const raw = file.testExecError?.message ?? file.message ?? "test file failed to run";
40
+ results.push({
41
+ testId: `${name} › (file failed to run)`,
42
+ file: name,
43
+ status: "error",
44
+ message: stripVTControlCharacters(raw).trim() || undefined,
45
+ durationMs: 0,
46
+ });
47
+ continue;
48
+ }
49
+ for (const a of file.assertionResults ?? []) {
50
+ const status = mapAssertion(a.status);
51
+ const titlePath = [...(a.ancestorTitles ?? []), a.title ?? ""].filter(Boolean).join(" › ");
52
+ const message = status === "fail" || status === "error"
53
+ ? stripVTControlCharacters((a.failureMessages ?? [])[0] ?? "").trim() || undefined
54
+ : undefined;
55
+ results.push({
56
+ testId: `${name} › ${titlePath}`,
57
+ file: name,
58
+ status,
59
+ message,
60
+ durationMs: Math.round(a.duration ?? 0),
61
+ });
62
+ }
63
+ }
64
+ const failed = results.filter((r) => r.status === "fail").length;
65
+ const errored = results.filter((r) => r.status === "error").length;
66
+ const skipped = results.filter((r) => r.status === "skipped").length;
67
+ const passed = results.filter((r) => r.status === "pass").length;
68
+ return {
69
+ results,
70
+ summary: { ran: passed + failed + errored, failed, errored, skipped, truncated: opts?.truncated ?? false },
71
+ environmentHealthy: true,
72
+ };
73
+ }
74
+ function escapeRegex(s) {
75
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
76
+ }
77
+ function splitSelectors(selectors) {
78
+ const files = [];
79
+ const titles = [];
80
+ for (const sel of selectors) {
81
+ const idx = sel.indexOf("::");
82
+ if (idx >= 0) {
83
+ const file = sel.slice(0, idx).trim();
84
+ const title = sel.slice(idx + 2).trim();
85
+ if (file && !files.includes(file))
86
+ files.push(file);
87
+ if (title)
88
+ titles.push(title);
89
+ }
90
+ else if (sel.trim() && !files.includes(sel.trim())) {
91
+ files.push(sel.trim());
92
+ }
93
+ }
94
+ return { files, titles };
95
+ }
96
+ export function buildJestArgs(input) {
97
+ const { files, titles } = splitSelectors(input.selectors);
98
+ const args = [...files, "--json", `--outputFile=${input.reportPath}`];
99
+ if (input.mode === "probe")
100
+ args.push("--bail=1");
101
+ if (titles.length > 0)
102
+ args.push("-t", titles.map(escapeRegex).join("|"));
103
+ return args;
104
+ }
105
+ export function buildVitestArgs(input) {
106
+ const { files, titles } = splitSelectors(input.selectors);
107
+ const args = [...files, "--reporter=json", `--outputFile=${input.reportPath}`];
108
+ if (input.mode === "probe")
109
+ args.push("--bail=1");
110
+ if (titles.length > 0)
111
+ args.push("-t", titles.map(escapeRegex).join("|"));
112
+ return args;
113
+ }
@@ -0,0 +1,13 @@
1
+ import { ParsedExternalRun } from "../types/ExternalTestExecution.js";
2
+ export declare function parseMochaJson(report: unknown, opts?: {
3
+ truncated?: boolean;
4
+ }): ParsedExternalRun;
5
+ /**
6
+ * Builds the args appended to the suite's `testRunCommand` (e.g. `npx mocha`).
7
+ * `--reporter json` prints the report to stdout (the generic runner captures
8
+ * it). Selectors are spec files, optionally `file::title` → `--grep`.
9
+ */
10
+ export declare function buildMochaArgs(input: {
11
+ selectors: string[];
12
+ mode: "confirm" | "verify" | "probe";
13
+ }): string[];
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Mocha adapter for skyramp_run_existing_tests. Normalizes `mocha --reporter
3
+ * json` (emitted to stdout) into the neutral shape. Mocha does not tag
4
+ * fail-vs-error, so we classify: an AssertionError is a test FAILURE; a hook
5
+ * failure or any other thrown error is an ERROR.
6
+ */
7
+ import { stripVTControlCharacters } from "util";
8
+ /** Assertion failure → fail; hook failure or any other throw → error. */
9
+ function classifyFailure(t) {
10
+ const title = `${t.fullTitle ?? ""} ${t.title ?? ""}`;
11
+ // Mocha reports a hook failure with a title like `"before each" hook for "x"`.
12
+ // Match that specific shape (not a loose /hook/) so a normal test merely titled
13
+ // "...hook..." is not mislabeled as an environment error.
14
+ if (/"(before|after) (each|all)" hook\b/i.test(title))
15
+ return "error";
16
+ if (/assertion/i.test(t.err?.name ?? ""))
17
+ return "fail";
18
+ return "error";
19
+ }
20
+ function messageOf(t) {
21
+ const raw = t.err?.message ?? t.err?.stack;
22
+ if (!raw)
23
+ return undefined;
24
+ return stripVTControlCharacters(raw).split("\n")[0].trim() || undefined;
25
+ }
26
+ function toResult(t, status) {
27
+ const file = t.file ?? "(unknown)";
28
+ const full = t.fullTitle ?? t.title ?? "(unknown)";
29
+ return {
30
+ testId: `${file} › ${full}`,
31
+ file,
32
+ status,
33
+ message: status === "fail" || status === "error" ? messageOf(t) : undefined,
34
+ durationMs: Math.round(t.duration ?? 0),
35
+ };
36
+ }
37
+ export function parseMochaJson(report, opts) {
38
+ const rep = (report ?? {});
39
+ const results = [];
40
+ for (const t of rep.passes ?? [])
41
+ results.push(toResult(t, "pass"));
42
+ for (const t of rep.pending ?? [])
43
+ results.push(toResult(t, "skipped"));
44
+ for (const t of rep.failures ?? [])
45
+ results.push(toResult(t, classifyFailure(t)));
46
+ const failed = results.filter((r) => r.status === "fail").length;
47
+ const errored = results.filter((r) => r.status === "error").length;
48
+ const skipped = results.filter((r) => r.status === "skipped").length;
49
+ const passed = results.filter((r) => r.status === "pass").length;
50
+ return {
51
+ results,
52
+ summary: { ran: passed + failed + errored, failed, errored, skipped, truncated: opts?.truncated ?? false },
53
+ environmentHealthy: true,
54
+ };
55
+ }
56
+ function escapeRegex(s) {
57
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
58
+ }
59
+ /**
60
+ * Builds the args appended to the suite's `testRunCommand` (e.g. `npx mocha`).
61
+ * `--reporter json` prints the report to stdout (the generic runner captures
62
+ * it). Selectors are spec files, optionally `file::title` → `--grep`.
63
+ */
64
+ export function buildMochaArgs(input) {
65
+ const files = [];
66
+ const titles = [];
67
+ for (const sel of input.selectors) {
68
+ const idx = sel.indexOf("::");
69
+ if (idx >= 0) {
70
+ const file = sel.slice(0, idx).trim();
71
+ const title = sel.slice(idx + 2).trim();
72
+ if (file && !files.includes(file))
73
+ files.push(file);
74
+ if (title)
75
+ titles.push(title);
76
+ }
77
+ else if (sel.trim() && !files.includes(sel.trim())) {
78
+ files.push(sel.trim());
79
+ }
80
+ }
81
+ const args = [...files, "--reporter", "json"];
82
+ if (input.mode === "probe")
83
+ args.push("--bail");
84
+ if (titles.length > 0)
85
+ args.push("--grep", titles.map(escapeRegex).join("|"));
86
+ return args;
87
+ }
@@ -0,0 +1,17 @@
1
+ import { ParsedExternalRun } from "../types/ExternalTestExecution.js";
2
+ export declare function parsePlaywrightJson(report: unknown, opts?: {
3
+ truncated?: boolean;
4
+ }): ParsedExternalRun;
5
+ /**
6
+ * Builds the argument list appended to a suite's `testRunCommand` (e.g.
7
+ * `npx playwright test`). Selectors are spec files, optionally `file::title`
8
+ * to also filter by test name. Forces the JSON reporter for machine-readable
9
+ * results and, in confirm mode, disables the failure cap so ALL breakages are
10
+ * counted (Ghost's config sets `maxFailures: 1`, which would otherwise stop at
11
+ * the first break).
12
+ */
13
+ export declare function buildPlaywrightArgs(input: {
14
+ selectors: string[];
15
+ mode: "confirm" | "verify";
16
+ project?: string;
17
+ }): string[];
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Playwright adapter for skyramp_run_existing_tests.
3
+ *
4
+ * Runs a repo's OWN Playwright suite (external, user-authored tests) and turns
5
+ * the `@playwright/test` JSON reporter output into the tool's neutral result
6
+ * shape. This is the DELEGATE-mode path (eng doc §7 / design spec §7): the
7
+ * suite owns environment bring-up (via a Playwright `global-setup` dependency
8
+ * and/or a wrapper entrypoint), and this adapter only drives the run with
9
+ * scoped selectors + config overrides and reads back structured results.
10
+ *
11
+ * The pure `parsePlaywrightJson` is the tested core; `buildPlaywrightArgs`
12
+ * builds the flags appended to the suite's `testRunCommand`. The IO shell that
13
+ * actually spawns the run lives in `runExistingTestsTool`.
14
+ */
15
+ import { stripVTControlCharacters } from "util";
16
+ /**
17
+ * A Playwright project (or spec file) is "infra" when it exists to bring up /
18
+ * tear down the environment rather than to assert product behavior. Its
19
+ * failures are environment health signals, not PR signals.
20
+ */
21
+ function isInfraSpec(projectName, file) {
22
+ if (/setup|teardown/i.test(projectName ?? ""))
23
+ return true;
24
+ return /(^|\/)global[.-]?(setup|teardown)|\.(setup|teardown)\.[cm]?[jt]s$/i.test(file);
25
+ }
26
+ /** Depth-first walk collecting every spec with its file + describe-title path.
27
+ * A file-level suite's own title is the filename, so it is NOT added to the
28
+ * path; nested (describe) suite titles are. */
29
+ function collectSpecs(suite, file, describePath, out) {
30
+ const suiteFile = suite.file ?? file;
31
+ for (const spec of suite.specs ?? []) {
32
+ out.push({ spec, file: spec.file ?? suiteFile, describePath });
33
+ }
34
+ for (const child of suite.suites ?? []) {
35
+ collectSpecs(child, suiteFile, [...describePath, child.title ?? ""], out);
36
+ }
37
+ }
38
+ /** Maps a single Playwright test (one project's run of a spec) to a neutral status. */
39
+ function mapStatus(test) {
40
+ if (test.status === "skipped")
41
+ return "skipped";
42
+ const results = test.results ?? [];
43
+ if (results.length === 0)
44
+ return "skipped"; // never executed (e.g. dependency failed)
45
+ const final = results[results.length - 1];
46
+ switch (final.status) {
47
+ case "passed":
48
+ return "pass";
49
+ case "skipped":
50
+ return "skipped";
51
+ case "timedOut":
52
+ case "interrupted":
53
+ return "error";
54
+ case "failed":
55
+ return "fail";
56
+ default:
57
+ return final.status ? "error" : "skipped";
58
+ }
59
+ }
60
+ function messageOf(test) {
61
+ const results = test.results ?? [];
62
+ const final = results[results.length - 1];
63
+ const raw = final?.error?.message ?? final?.errors?.[0]?.message;
64
+ if (!raw)
65
+ return undefined;
66
+ return stripVTControlCharacters(raw).trim() || undefined;
67
+ }
68
+ export function parsePlaywrightJson(report, opts) {
69
+ const rep = (report ?? {});
70
+ const collected = [];
71
+ for (const fileSuite of rep.suites ?? []) {
72
+ collectSpecs(fileSuite, fileSuite.file ?? "", [], collected);
73
+ }
74
+ const results = [];
75
+ let environmentHealthy = true;
76
+ let healthDetail;
77
+ let infraSpecFailed = false;
78
+ const runLevelErrors = (rep.errors ?? []).map((e) => stripVTControlCharacters(e?.message ?? "unknown error").split("\n")[0]);
79
+ const isMaxFailuresBail = (m) => /maximum allowed failures|stopped early/i.test(m);
80
+ const maxFailuresBail = runLevelErrors.some(isMaxFailuresBail);
81
+ // A top-level error normally means the run never really started (import/config
82
+ // error) → broken environment. The exception is a --max-failures bail ("stopped
83
+ // early after N maximum allowed failures"): the run DID start and these are real
84
+ // product-test failures (still PR signal), so a bail must NOT flip
85
+ // environmentHealthy — otherwise downstream env-health logic would discard the
86
+ // failures it stands for. Only a non-bail run-level error marks the env unhealthy.
87
+ const firstFatalError = runLevelErrors.find((m) => !isMaxFailuresBail(m));
88
+ if (firstFatalError) {
89
+ environmentHealthy = false;
90
+ healthDetail =
91
+ `Playwright reported a run-level error; the suite could not be collected or configured. ` +
92
+ `First: ${firstFatalError}`;
93
+ }
94
+ for (const { spec, file, describePath } of collected) {
95
+ const tests = spec.tests ?? [];
96
+ const multiProject = tests.length > 1;
97
+ for (const test of tests) {
98
+ const status = mapStatus(test);
99
+ const infra = isInfraSpec(test.projectName, file);
100
+ if (infra) {
101
+ // Setup/teardown failure = broken environment, not a product failure, so
102
+ // the first failing infra spec flips environmentHealthy and its cause
103
+ // ("global-setup failed") — more actionable than any generic run-level
104
+ // line — wins the healthDetail.
105
+ if (status === "fail" || status === "error") {
106
+ if (!infraSpecFailed) {
107
+ environmentHealthy = false;
108
+ healthDetail = `Environment setup failed: "${spec.title}" (${file}) reported ${status}. Skipped tests are not PR signal.`;
109
+ }
110
+ infraSpecFailed = true;
111
+ }
112
+ continue; // never reported as a test result
113
+ }
114
+ const titlePath = [...describePath, spec.title].filter(Boolean).join(" › ");
115
+ const testId = `${file} › ${titlePath}` +
116
+ (multiProject && test.projectName ? ` [${test.projectName}]` : "");
117
+ const durationMs = test.results?.[test.results.length - 1]?.duration ?? 0;
118
+ results.push({
119
+ testId,
120
+ file,
121
+ status,
122
+ message: status === "fail" || status === "error" ? messageOf(test) : undefined,
123
+ durationMs,
124
+ });
125
+ }
126
+ }
127
+ const failed = results.filter((r) => r.status === "fail").length;
128
+ const errored = results.filter((r) => r.status === "error").length;
129
+ const skipped = results.filter((r) => r.status === "skipped").length;
130
+ const passed = results.filter((r) => r.status === "pass").length;
131
+ return {
132
+ results,
133
+ summary: {
134
+ ran: passed + failed + errored,
135
+ failed,
136
+ errored,
137
+ skipped,
138
+ truncated: opts?.truncated ?? false,
139
+ },
140
+ environmentHealthy,
141
+ healthDetail,
142
+ diagnostics: { runLevelErrors, infraSpecFailed, maxFailuresBail },
143
+ };
144
+ }
145
+ function escapeRegex(s) {
146
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
147
+ }
148
+ /**
149
+ * Builds the argument list appended to a suite's `testRunCommand` (e.g.
150
+ * `npx playwright test`). Selectors are spec files, optionally `file::title`
151
+ * to also filter by test name. Forces the JSON reporter for machine-readable
152
+ * results and, in confirm mode, disables the failure cap so ALL breakages are
153
+ * counted (Ghost's config sets `maxFailures: 1`, which would otherwise stop at
154
+ * the first break).
155
+ */
156
+ export function buildPlaywrightArgs(input) {
157
+ const files = [];
158
+ const titles = [];
159
+ for (const sel of input.selectors) {
160
+ const idx = sel.indexOf("::");
161
+ if (idx >= 0) {
162
+ const file = sel.slice(0, idx).trim();
163
+ const title = sel.slice(idx + 2).trim();
164
+ if (file && !files.includes(file))
165
+ files.push(file);
166
+ if (title)
167
+ titles.push(title);
168
+ }
169
+ else if (sel.trim() && !files.includes(sel.trim())) {
170
+ files.push(sel.trim());
171
+ }
172
+ }
173
+ const args = [...files, "--reporter=json"];
174
+ if (input.mode === "confirm")
175
+ args.push("--max-failures=0");
176
+ if (input.project)
177
+ args.push(`--project=${input.project}`);
178
+ if (titles.length > 0) {
179
+ args.push("--grep", titles.map(escapeRegex).join("|"));
180
+ }
181
+ return args;
182
+ }
@@ -0,0 +1,15 @@
1
+ import { ParsedExternalRun } from "../types/ExternalTestExecution.js";
2
+ export declare function parsePytestJson(report: unknown, opts?: {
3
+ truncated?: boolean;
4
+ }): ParsedExternalRun;
5
+ /**
6
+ * Builds the args appended to the suite's `testRunCommand` (e.g. `pytest`).
7
+ * Selectors are pytest nodeids (`file.py::Class::test`) and pass through as
8
+ * positional args — `file::test` IS a nodeid, so no separate name filter is
9
+ * needed. Forces the json-report reporter to the given path.
10
+ */
11
+ export declare function buildPytestArgs(input: {
12
+ selectors: string[];
13
+ mode: "confirm" | "verify" | "probe";
14
+ reportPath: string;
15
+ }): string[];
@@ -0,0 +1,108 @@
1
+ /**
2
+ * pytest adapter for skyramp_run_existing_tests. Normalizes the
3
+ * `pytest-json-report` (`--json-report`) output into the neutral result shape.
4
+ * Native JSON (not junit): preserves the real nodeid and pytest's explicit
5
+ * passed/failed/error/skipped, so a fixture/collection error stays distinct from
6
+ * an assertion failure.
7
+ */
8
+ import { stripVTControlCharacters } from "util";
9
+ function mapOutcome(outcome) {
10
+ switch (outcome) {
11
+ case "passed":
12
+ case "xpassed":
13
+ return "pass";
14
+ case "failed":
15
+ return "fail";
16
+ case "error":
17
+ return "error";
18
+ case "skipped":
19
+ case "xfailed":
20
+ return "skipped";
21
+ default:
22
+ return outcome ? "error" : "skipped";
23
+ }
24
+ }
25
+ function fileOf(nodeid) {
26
+ const idx = nodeid.indexOf("::");
27
+ return idx >= 0 ? nodeid.slice(0, idx) : nodeid;
28
+ }
29
+ function messageOf(test) {
30
+ const raw = test.call?.longrepr ?? test.setup?.longrepr ?? test.teardown?.longrepr;
31
+ if (!raw)
32
+ return undefined;
33
+ return stripVTControlCharacters(raw).trim() || undefined;
34
+ }
35
+ function durationMsOf(test) {
36
+ const secs = (test.setup?.duration ?? 0) + (test.call?.duration ?? 0) + (test.teardown?.duration ?? 0);
37
+ return Math.round(secs * 1000);
38
+ }
39
+ export function parsePytestJson(report, opts) {
40
+ const rep = (report ?? {});
41
+ const tests = rep.tests ?? [];
42
+ // Real test outcomes.
43
+ const testResults = tests.map((t) => {
44
+ const status = mapOutcome(t.outcome);
45
+ const nodeid = t.nodeid ?? "(unknown)";
46
+ return {
47
+ testId: nodeid,
48
+ file: fileOf(nodeid),
49
+ status,
50
+ message: status === "fail" || status === "error" ? messageOf(t) : undefined,
51
+ durationMs: durationMsOf(t),
52
+ };
53
+ });
54
+ const collectionErrors = (rep.collectors ?? []).filter((c) => c.outcome === "error" || c.outcome === "failed");
55
+ const lastLine = (raw) => stripVTControlCharacters(raw ?? "collection error").split("\n").filter(Boolean).slice(-1)[0] ?? "collection error";
56
+ // Surface each collection/import error as an errored result so a broken file is
57
+ // never invisible — regardless of whether other selected files ran. A collection
58
+ // error IS a real breakage (PR signal), counted in `errored`/`ran`.
59
+ const collectionResults = collectionErrors.map((c) => {
60
+ const nodeid = c.nodeid ?? "(collection)";
61
+ return {
62
+ testId: `${nodeid} › (collection error)`,
63
+ file: fileOf(nodeid),
64
+ status: "error",
65
+ message: lastLine(c.longrepr),
66
+ durationMs: 0,
67
+ };
68
+ });
69
+ const results = [...testResults, ...collectionResults];
70
+ let environmentHealthy = true;
71
+ let healthDetail;
72
+ // When collection failed AND no real test ran, the suite as a whole could not be
73
+ // collected — environmental (a wall of red), not PR signal. When some real tests
74
+ // DID run, the collection errors above stand as ordinary PR-signal error results.
75
+ if (collectionErrors.length > 0 && testResults.length === 0) {
76
+ environmentHealthy = false;
77
+ const first = collectionErrors[0];
78
+ healthDetail = `pytest could not collect the suite: ${collectionErrors.length} collection error(s). First (${first.nodeid ?? "?"}): ${lastLine(first.longrepr)}`;
79
+ }
80
+ const failed = results.filter((r) => r.status === "fail").length;
81
+ const errored = results.filter((r) => r.status === "error").length;
82
+ const skipped = results.filter((r) => r.status === "skipped").length;
83
+ const passed = results.filter((r) => r.status === "pass").length;
84
+ return {
85
+ results,
86
+ summary: { ran: passed + failed + errored, failed, errored, skipped, truncated: opts?.truncated ?? false },
87
+ environmentHealthy,
88
+ healthDetail,
89
+ diagnostics: {
90
+ runLevelErrors: collectionErrors.map((c) => lastLine(c.longrepr)),
91
+ infraSpecFailed: false,
92
+ maxFailuresBail: false,
93
+ },
94
+ };
95
+ }
96
+ /**
97
+ * Builds the args appended to the suite's `testRunCommand` (e.g. `pytest`).
98
+ * Selectors are pytest nodeids (`file.py::Class::test`) and pass through as
99
+ * positional args — `file::test` IS a nodeid, so no separate name filter is
100
+ * needed. Forces the json-report reporter to the given path.
101
+ */
102
+ export function buildPytestArgs(input) {
103
+ const args = input.selectors.map((s) => s.trim()).filter(Boolean);
104
+ args.push("--json-report", `--json-report-file=${input.reportPath}`);
105
+ if (input.mode === "probe")
106
+ args.push("--maxfail=1");
107
+ return args;
108
+ }
@@ -9,3 +9,4 @@ export declare function getCommandIds(): OneClickCommandId[];
9
9
  export declare function lookupCommand(id: string): OneClickCommandDef;
10
10
  export { TEST_GIVEN_ENDPOINT_COMPREHENSIVELY_COMMAND } from "./testThisEndpointCommand.js";
11
11
  export { FULLREPO_RECOMMEND_GENERATE_EXECUTE_TOPN_TESTS_COMMAND } from "./recommendTestsAndExecuteCommand.js";
12
+ export { LOCAL_DEV_TEST_CHANGES_COMMAND } from "./localDevTestChangesCommand.js";
@@ -1,23 +1,28 @@
1
+ import { isLocalDevEnabled, isOneClickEnabled } from "../utils/featureFlags.js";
1
2
  import { TEST_GIVEN_ENDPOINT_COMPREHENSIVELY_COMMAND } from "./testThisEndpointCommand.js";
2
3
  import { FULLREPO_RECOMMEND_GENERATE_EXECUTE_TOPN_TESTS_COMMAND } from "./recommendTestsAndExecuteCommand.js";
3
- /**
4
- * To add a new predefined workflow:
5
- * 1. Add a new id to OneClickCommandId in types/OneClickCommands.ts
6
- * 2. Create a new file in commands/ (e.g. myWorkflowCommand.ts) with steps and export the command
7
- * 3. Import the command here and add it to COMMAND_LIBRARY
8
- * 4. The generic buildWorkflowFromCommand() will render detailed instructions from your steps
9
- * (no schema changes needed in oneClickTool.ts — the workflow param description is generated dynamically)
10
- */
11
- /** All predefined one-click commands */
12
- const COMMAND_LIBRARY = {
4
+ import { LOCAL_DEV_TEST_CHANGES_COMMAND } from "./localDevTestChangesCommand.js";
5
+ /** Base predefined one-click commands */
6
+ const BASE_COMMAND_LIBRARY = {
13
7
  test_given_endpoint_comprehensively: TEST_GIVEN_ENDPOINT_COMPREHENSIVELY_COMMAND,
14
8
  full_repo_scan_recommend_generate_and_execute_top_n_tests: FULLREPO_RECOMMEND_GENERATE_EXECUTE_TOPN_TESTS_COMMAND,
15
9
  };
10
+ /** All enabled predefined one-click commands */
11
+ function getCommandLibrary() {
12
+ const commands = {};
13
+ if (isOneClickEnabled()) {
14
+ Object.assign(commands, BASE_COMMAND_LIBRARY);
15
+ }
16
+ if (isLocalDevEnabled()) {
17
+ commands.local_dev_test_changes = LOCAL_DEV_TEST_CHANGES_COMMAND;
18
+ }
19
+ return commands;
20
+ }
16
21
  export function getAllCommands() {
17
- return Object.values(COMMAND_LIBRARY);
22
+ return Object.values(getCommandLibrary()).filter((cmd) => Boolean(cmd));
18
23
  }
19
24
  export function getCommandIds() {
20
- return Object.keys(COMMAND_LIBRARY);
25
+ return getAllCommands().map((cmd) => cmd.id);
21
26
  }
22
27
  /**
23
28
  * Look up a command by its string ID. Accepts any string so the tool handler
@@ -25,7 +30,7 @@ export function getCommandIds() {
25
30
  * the ID is not found.
26
31
  */
27
32
  export function lookupCommand(id) {
28
- const cmd = COMMAND_LIBRARY[id];
33
+ const cmd = getCommandLibrary()[id];
29
34
  if (!cmd) {
30
35
  const available = getCommandIds().join(", ");
31
36
  throw new Error(`Unknown workflow: "${id}". Valid workflow IDs: ${available}.`);
@@ -35,3 +40,4 @@ export function lookupCommand(id) {
35
40
  // Re-export commands for consumers that import from commandLibrary
36
41
  export { TEST_GIVEN_ENDPOINT_COMPREHENSIVELY_COMMAND } from "./testThisEndpointCommand.js";
37
42
  export { FULLREPO_RECOMMEND_GENERATE_EXECUTE_TOPN_TESTS_COMMAND } from "./recommendTestsAndExecuteCommand.js";
43
+ export { LOCAL_DEV_TEST_CHANGES_COMMAND } from "./localDevTestChangesCommand.js";
@@ -0,0 +1,15 @@
1
+ import type { OneClickCommandDef } from "../types/OneClickCommands.js";
2
+ /**
3
+ * Predefined One-Click Command: Local Dev — Test Changes
4
+ *
5
+ * This is the BOOTSTRAPPER / ENTRY POINT for local-dev testing.
6
+ * It handles first-time setup (skills + workspace), then delegates
7
+ * the actual test workflow to the `skyramp_local_dev` MCP prompt.
8
+ *
9
+ * Flow:
10
+ * Step 0: Install IDE skill into repo for the calling agent
11
+ * Step 1: Ensure .skyramp/workspace.yml exists
12
+ * Step 2: Invoke skyramp_local_dev prompt (which runs the full workflow)
13
+ */
14
+ export declare const LOCAL_DEV_SKILL_CONTENT = "---\nname: local-dev\ndescription: Run the Skyramp MCP local-dev workflow to test local code changes against a base branch. Use when the user asks to test changes, mock downstream services, validate a feature branch, run local-dev, or verify a PR before merge.\nuser_invocable: true\n---\n\n# local-dev\n\nUse the Skyramp MCP local-dev workflow as the source of truth. Do not recreate the workflow steps from this skill.\n\n## Usage\n\n`/local-dev` \u2014 diff against main (default)\n`/local-dev main --mock stripe,billing --real postgres,redis`\n\n## Invocation\n\nCall the `skyramp_local_dev` MCP prompt with the user's intent preserved:\n- `baseCommit`: branch/SHA to diff against (default: main)\n- `repositoryPath`: absolute path to repo\n- `servicesToMock`: comma-separated services the user explicitly wants mocked\n- `realServices`: comma-separated services that should receive real traffic\n- `mockProtocols`: comma-separated protocols the user explicitly wants mocked (for example, `rest`)\n\nBy default, mock only true third-party downstream services. Keep the service under test, infrastructure, and local first-party services real unless the user explicitly asks to mock them.\n";
15
+ export declare const LOCAL_DEV_TEST_CHANGES_COMMAND: OneClickCommandDef;