@skyramp/mcp 0.3.2-rc.pom-2 → 0.3.2-rc.pom-3

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 (97) hide show
  1. package/build/adapters/jestAdapter.d.ts +14 -0
  2. package/build/adapters/jestAdapter.js +113 -0
  3. package/build/adapters/jestAdapter.test.d.ts +1 -0
  4. package/build/adapters/jestAdapter.test.js +93 -0
  5. package/build/adapters/mochaAdapter.d.ts +13 -0
  6. package/build/adapters/mochaAdapter.js +87 -0
  7. package/build/adapters/mochaAdapter.test.d.ts +1 -0
  8. package/build/adapters/mochaAdapter.test.js +63 -0
  9. package/build/adapters/playwrightAdapter.d.ts +17 -0
  10. package/build/adapters/playwrightAdapter.js +182 -0
  11. package/build/adapters/playwrightAdapter.test.d.ts +1 -0
  12. package/build/adapters/playwrightAdapter.test.js +169 -0
  13. package/build/adapters/pytestAdapter.d.ts +15 -0
  14. package/build/adapters/pytestAdapter.js +108 -0
  15. package/build/adapters/pytestAdapter.test.d.ts +1 -0
  16. package/build/adapters/pytestAdapter.test.js +90 -0
  17. package/build/index.js +2 -0
  18. package/build/prompts/code-reuse.d.ts +2 -6
  19. package/build/prompts/pom-aware-code-reuse.d.ts +2 -6
  20. package/build/prompts/pom-aware-code-reuse.js +4 -2
  21. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +46 -6
  22. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +1 -1
  23. package/build/prompts/sut-setup/shared.js +10 -12
  24. package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +10 -1
  25. package/build/prompts/test-maintenance/drift-analysis-prompt.js +54 -1
  26. package/build/prompts/test-maintenance/drift-analysis-prompt.test.d.ts +1 -0
  27. package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +51 -0
  28. package/build/prompts/testbot/testbot-prompts.d.ts +1 -1
  29. package/build/prompts/testbot/testbot-prompts.js +33 -12
  30. package/build/prompts/testbot/testbot-prompts.test.js +35 -8
  31. package/build/recommendation/budgeters/diversityBalancedBudgeter.test.js +63 -0
  32. package/build/recommendation/budgeters/shared.js +30 -6
  33. package/build/resources/testbotResource.js +1 -1
  34. package/build/resources/testbotResource.test.d.ts +1 -0
  35. package/build/resources/testbotResource.test.js +44 -0
  36. package/build/services/TestExecutionService.d.ts +1 -1
  37. package/build/tool-phases.js +1 -0
  38. package/build/tools/code-refactor/codeReuseTool.d.ts +0 -7
  39. package/build/tools/code-refactor/codeReuseTool.js +3 -75
  40. package/build/tools/code-refactor/codeReuseTool.test.js +288 -6
  41. package/build/tools/code-refactor/verify-gates.d.ts +43 -0
  42. package/build/tools/code-refactor/verify-gates.js +131 -0
  43. package/build/tools/generate-tests/generateContractRestTool.js +8 -0
  44. package/build/tools/generate-tests/generateContractRestTool.test.d.ts +1 -0
  45. package/build/tools/generate-tests/generateContractRestTool.test.js +142 -0
  46. package/build/tools/generate-tests/generateUIRestTool.d.ts +17 -1
  47. package/build/tools/generate-tests/generateUIRestTool.js +15 -3
  48. package/build/tools/generate-tests/generateUIRestTool.test.d.ts +1 -0
  49. package/build/tools/generate-tests/generateUIRestTool.test.js +32 -0
  50. package/build/tools/runExistingTestsTool.d.ts +107 -0
  51. package/build/tools/runExistingTestsTool.js +443 -0
  52. package/build/tools/runExistingTestsTool.test.d.ts +1 -0
  53. package/build/tools/runExistingTestsTool.test.js +379 -0
  54. package/build/tools/test-management/analyzeTestHealthTool.d.ts +11 -0
  55. package/build/tools/test-management/analyzeTestHealthTool.js +63 -1
  56. package/build/tools/test-management/analyzeTestHealthTool.test.js +117 -0
  57. package/build/tools/test-management/registerTestPlanTool.js +3 -1
  58. package/build/types/ExternalTestExecution.d.ts +67 -0
  59. package/build/types/ExternalTestExecution.js +8 -0
  60. package/build/types/TestTypes.js +3 -1
  61. package/build/utils/AnalysisStateManager.d.ts +7 -0
  62. package/build/utils/AnalysisStateManager.js +5 -1
  63. package/build/utils/docker.test.js +1 -1
  64. package/build/utils/planMatchKeys.d.ts +2 -1
  65. package/build/utils/planMatchKeys.js +43 -13
  66. package/build/utils/planMatchKeys.test.d.ts +1 -0
  67. package/build/utils/planMatchKeys.test.js +123 -0
  68. package/build/utils/pom-scope/index.d.ts +8 -4
  69. package/build/utils/pom-scope/index.js +19 -10
  70. package/build/utils/pom-scope/index.test.js +40 -1
  71. package/build/utils/pom-scope/scoring.d.ts +2 -1
  72. package/build/utils/pom-scope/scoring.js +5 -4
  73. package/build/utils/pom-scope/selector-extractor.js +1 -1
  74. package/build/utils/pom-verify/bindings.d.ts +2 -0
  75. package/build/utils/pom-verify/bindings.js +6 -4
  76. package/build/utils/pom-verify/calls.js +2 -4
  77. package/build/utils/pom-verify/resolve.d.ts +4 -3
  78. package/build/utils/pom-verify/resolve.js +29 -9
  79. package/build/utils/pom-verify/resolve.test.js +46 -0
  80. package/build/utils/pom-verify/verify.js +14 -7
  81. package/build/utils/pom-verify/verify.test.js +49 -0
  82. package/build/utils/regex.d.ts +2 -0
  83. package/build/utils/regex.js +4 -0
  84. package/build/utils/versions.d.ts +3 -3
  85. package/build/utils/versions.js +1 -1
  86. package/build/workspace/frameworks.d.ts +11 -0
  87. package/build/workspace/frameworks.js +22 -0
  88. package/build/workspace/testSuites.d.ts +19 -0
  89. package/build/workspace/testSuites.js +17 -0
  90. package/build/workspace/testSuites.test.d.ts +1 -0
  91. package/build/workspace/testSuites.test.js +23 -0
  92. package/build/workspace/workspace.d.ts +116 -10
  93. package/build/workspace/workspace.js +23 -1
  94. package/build/workspace/workspace.test.js +141 -1
  95. package/package.json +3 -3
  96. /package/build/utils/{pom-verify → pom-scope}/strip.d.ts +0 -0
  97. /package/build/utils/{pom-verify → pom-scope}/strip.js +0 -0
@@ -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 @@
1
+ export {};
@@ -0,0 +1,93 @@
1
+ import path from "path";
2
+ import { fileURLToPath } from "url";
3
+ import { readFileSync } from "fs";
4
+ import { parseJestJson, buildJestArgs, buildVitestArgs } from "./jestAdapter.js";
5
+ const here = path.dirname(fileURLToPath(import.meta.url));
6
+ function loadFixture(name) {
7
+ return JSON.parse(readFileSync(path.join(here, "__fixtures__", `${name}.json`), "utf8"));
8
+ }
9
+ describe("parseJestJson", () => {
10
+ describe("jest healthy variety report", () => {
11
+ const parsed = parseJestJson(loadFixture("jest-healthy"));
12
+ const byId = (s) => parsed.results.find((r) => r.testId.includes(s));
13
+ it("keeps the environment healthy (a broken test file is PR signal, not env)", () => {
14
+ expect(parsed.environmentHealthy).toBe(true);
15
+ });
16
+ it("maps passed/failed/todo and a file testExecError to pass/fail/skipped/error", () => {
17
+ expect(byId("adds numbers").status).toBe("pass");
18
+ expect(byId("detects a regression").status).toBe("fail");
19
+ expect(byId("todo later").status).toBe("skipped");
20
+ expect(byId("broken.test.js").status).toBe("error");
21
+ });
22
+ it("builds a testId from file + ancestorTitles + title", () => {
23
+ expect(byId("adds numbers").testId).toBe("/repo/src/math.test.js › math › adds numbers");
24
+ expect(byId("adds numbers").file).toBe("/repo/src/math.test.js");
25
+ });
26
+ it("captures a failure message for fail and file-exec error", () => {
27
+ expect(byId("detects a regression").message).toMatch(/Expected: 2/);
28
+ expect(byId("broken.test.js").message).toMatch(/SyntaxError/);
29
+ });
30
+ it("summarizes ran/failed/errored/skipped (skipped not in ran)", () => {
31
+ expect(parsed.summary).toMatchObject({ failed: 1, errored: 1, skipped: 1 });
32
+ expect(parsed.summary.ran).toBe(3);
33
+ });
34
+ });
35
+ describe("real jest syntax-error shape (no testExecError, top-level message)", () => {
36
+ // Confirmed against a REAL jest run (Step 7 smoke): a syntax error produces
37
+ // `status: "failed"`, `assertionResults: []`, and a top-level `message` —
38
+ // no `testExecError` object at all. Must map to "error" same as the
39
+ // testExecError shape.
40
+ const parsed = parseJestJson({
41
+ testResults: [
42
+ {
43
+ name: "/repo/src/broken2.test.js",
44
+ status: "failed",
45
+ message: "SyntaxError: Unexpected token, expected \",\"",
46
+ assertionResults: [],
47
+ },
48
+ ],
49
+ });
50
+ it("maps a message-only file failure to a single error result", () => {
51
+ expect(parsed.results).toHaveLength(1);
52
+ expect(parsed.results[0].status).toBe("error");
53
+ expect(parsed.results[0].message).toMatch(/SyntaxError/);
54
+ expect(parsed.summary).toMatchObject({ errored: 1, failed: 0, skipped: 0 });
55
+ });
56
+ });
57
+ describe("vitest report through the shared reader", () => {
58
+ const parsed = parseJestJson(loadFixture("vitest-healthy"));
59
+ it("parses vitest's jest-compatible json identically", () => {
60
+ expect(parsed.summary).toMatchObject({ failed: 0, errored: 0, skipped: 1 });
61
+ expect(parsed.summary.ran).toBe(1);
62
+ expect(parsed.results.find((r) => r.testId.includes("works")).status).toBe("pass");
63
+ });
64
+ });
65
+ it("honors an explicit truncated flag", () => {
66
+ expect(parseJestJson(loadFixture("jest-healthy"), { truncated: true }).summary.truncated).toBe(true);
67
+ });
68
+ });
69
+ describe("buildJestArgs", () => {
70
+ it("forces --json + --outputFile and passes files", () => {
71
+ const args = buildJestArgs({ selectors: ["src/a.test.js"], mode: "confirm", reportPath: "/tmp/r.json" });
72
+ expect(args).toContain("--json");
73
+ expect(args).toContain("--outputFile=/tmp/r.json");
74
+ expect(args).toContain("src/a.test.js");
75
+ expect(args).not.toContain("--bail=1");
76
+ });
77
+ it("bails at first failure in probe mode and filters by name via -t", () => {
78
+ const args = buildJestArgs({ selectors: ["src/a.test.js::renders header"], mode: "probe", reportPath: "/tmp/r.json" });
79
+ expect(args).toContain("--bail=1");
80
+ const ti = args.indexOf("-t");
81
+ expect(ti).toBeGreaterThanOrEqual(0);
82
+ expect(args[ti + 1]).toContain("renders header");
83
+ expect(args).toContain("src/a.test.js");
84
+ });
85
+ });
86
+ describe("buildVitestArgs", () => {
87
+ it("forces --reporter=json + --outputFile (vitest form)", () => {
88
+ const args = buildVitestArgs({ selectors: ["src/a.test.ts"], mode: "confirm", reportPath: "/tmp/r.json" });
89
+ expect(args).toContain("--reporter=json");
90
+ expect(args).toContain("--outputFile=/tmp/r.json");
91
+ expect(args).not.toContain("--json");
92
+ });
93
+ });
@@ -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 @@
1
+ export {};
@@ -0,0 +1,63 @@
1
+ import path from "path";
2
+ import { fileURLToPath } from "url";
3
+ import { readFileSync } from "fs";
4
+ import { parseMochaJson, buildMochaArgs } from "./mochaAdapter.js";
5
+ const here = path.dirname(fileURLToPath(import.meta.url));
6
+ function loadFixture(name) {
7
+ return JSON.parse(readFileSync(path.join(here, "__fixtures__", `mocha-${name}.json`), "utf8"));
8
+ }
9
+ describe("parseMochaJson", () => {
10
+ const parsed = parseMochaJson(loadFixture("healthy"));
11
+ const byId = (s) => parsed.results.find((r) => r.testId.includes(s));
12
+ it("keeps the environment healthy for a normal run", () => {
13
+ expect(parsed.environmentHealthy).toBe(true);
14
+ });
15
+ it("maps passes/pending/assertion-failure/hook-error to pass/skipped/fail/error", () => {
16
+ expect(byId("math adds").status).toBe("pass");
17
+ expect(byId("math later").status).toBe("skipped");
18
+ expect(byId("detects regression").status).toBe("fail");
19
+ expect(byId("before each").status).toBe("error");
20
+ });
21
+ it("builds testId from file + fullTitle", () => {
22
+ expect(byId("math adds").testId).toBe("/repo/test/math.spec.js › math adds");
23
+ expect(byId("math adds").file).toBe("/repo/test/math.spec.js");
24
+ });
25
+ it("captures a message for fail and error", () => {
26
+ expect(byId("detects regression").message).toMatch(/expected 2 to equal 3/);
27
+ expect(byId("before each").message).toMatch(/Cannot read properties/);
28
+ });
29
+ it("summarizes ran/failed/errored/skipped (skipped not in ran)", () => {
30
+ expect(parsed.summary).toMatchObject({ failed: 1, errored: 1, skipped: 1 });
31
+ expect(parsed.summary.ran).toBe(3);
32
+ });
33
+ it("classifies an assertion failure in a test merely titled with 'hook' as fail, not error", () => {
34
+ const parsed = parseMochaJson({
35
+ failures: [
36
+ {
37
+ title: "renders the hook menu",
38
+ fullTitle: "ui renders the hook menu",
39
+ file: "/repo/test/ui.spec.js",
40
+ duration: 2,
41
+ err: { name: "AssertionError", message: "expected menu open" },
42
+ },
43
+ ],
44
+ });
45
+ expect(parsed.results[0].status).toBe("fail");
46
+ });
47
+ });
48
+ describe("buildMochaArgs", () => {
49
+ it("forces the json reporter (to stdout) and passes files", () => {
50
+ const args = buildMochaArgs({ selectors: ["test/math.spec.js"], mode: "confirm" });
51
+ expect(args).toContain("--reporter");
52
+ expect(args).toContain("json");
53
+ expect(args).toContain("test/math.spec.js");
54
+ expect(args).not.toContain("--bail");
55
+ });
56
+ it("bails in probe mode and filters by name via --grep", () => {
57
+ const args = buildMochaArgs({ selectors: ["test/math.spec.js::adds"], mode: "probe" });
58
+ expect(args).toContain("--bail");
59
+ const gi = args.indexOf("--grep");
60
+ expect(gi).toBeGreaterThanOrEqual(0);
61
+ expect(args[gi + 1]).toContain("adds");
62
+ });
63
+ });
@@ -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 @@
1
+ export {};