@skyramp/mcp 0.2.9 → 0.2.10-rc.1

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 (76) hide show
  1. package/build/index.js +1 -1
  2. package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +28 -1
  3. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +1 -0
  4. package/build/prompts/personas.d.ts +2 -2
  5. package/build/prompts/personas.js +2 -2
  6. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +3 -0
  7. package/build/prompts/sut-setup/shared.js +1 -3
  8. package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +1 -1
  9. package/build/prompts/test-maintenance/drift-analysis-prompt.js +10 -5
  10. package/build/prompts/test-maintenance/driftAnalysisSections.js +1 -4
  11. package/build/prompts/test-maintenance/driftAnalysisShared.d.ts +7 -1
  12. package/build/prompts/test-maintenance/driftAnalysisShared.js +19 -1
  13. package/build/prompts/test-maintenance/uiDriftAnalysisSections.d.ts +0 -6
  14. package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +0 -16
  15. package/build/prompts/test-recommendation/recommendationShared.js +1 -1
  16. package/build/prompts/testbot/testbot-prompts.js +8 -8
  17. package/build/resources/testbotResource.js +1 -1
  18. package/build/services/TestDiscoveryService.d.ts +0 -1
  19. package/build/services/TestDiscoveryService.js +13 -33
  20. package/build/services/TestDiscoveryService.test.js +136 -1
  21. package/build/services/TestExecutionService.d.ts +1 -1
  22. package/build/tools/executeSkyrampTestTool.js +40 -71
  23. package/build/tools/submitReportTool.d.ts +20 -20
  24. package/build/tools/submitReportTool.js +113 -31
  25. package/build/tools/submitReportTool.test.js +411 -94
  26. package/build/tools/test-management/actionsTool.js +135 -53
  27. package/build/tools/test-management/actionsTool.test.d.ts +1 -0
  28. package/build/tools/test-management/actionsTool.test.js +297 -0
  29. package/build/tools/test-management/analyzeChangesTool.d.ts +9 -0
  30. package/build/tools/test-management/analyzeChangesTool.js +63 -152
  31. package/build/tools/test-management/analyzeChangesTool.test.js +34 -3
  32. package/build/tools/test-management/analyzeTestHealthTool.js +24 -157
  33. package/build/tools/test-management/analyzeTestHealthTool.test.js +15 -215
  34. package/build/tools/workspace/initializeWorkspaceTool.js +35 -15
  35. package/build/types/FrontendIntegration.d.ts +25 -0
  36. package/build/types/FrontendIntegration.js +19 -0
  37. package/build/types/RepositoryAnalysis.d.ts +8 -8
  38. package/build/types/TestAnalysis.d.ts +16 -50
  39. package/build/types/TestAnalysis.js +0 -28
  40. package/build/types/TestTypes.d.ts +3 -1
  41. package/build/types/TestTypes.js +3 -1
  42. package/build/utils/AnalysisStateManager.d.ts +13 -1
  43. package/build/utils/docker.test.js +1 -1
  44. package/build/utils/featureFlags.d.ts +1 -1
  45. package/build/utils/featureFlags.js +1 -1
  46. package/build/utils/frontendIntegration.d.ts +9 -0
  47. package/build/utils/frontendIntegration.js +237 -0
  48. package/build/utils/frontendIntegration.test.d.ts +1 -0
  49. package/build/utils/frontendIntegration.test.js +229 -0
  50. package/build/utils/pr-comment-parser.d.ts +3 -3
  51. package/build/utils/pr-comment-parser.js +6 -6
  52. package/build/utils/pr-comment-parser.test.js +3 -3
  53. package/build/utils/repoScanner.d.ts +7 -0
  54. package/build/utils/repoScanner.js +14 -6
  55. package/build/utils/repoScanner.test.js +9 -9
  56. package/build/utils/routeParsers.d.ts +32 -0
  57. package/build/utils/routeParsers.js +205 -2
  58. package/build/utils/routeParsers.test.js +279 -29
  59. package/build/utils/sourceRouteExtractor.js +174 -5
  60. package/build/utils/sourceRouteExtractor.test.js +173 -0
  61. package/build/utils/utils.d.ts +11 -0
  62. package/build/utils/utils.js +19 -0
  63. package/build/utils/utils.test.js +23 -1
  64. package/build/utils/versions.d.ts +3 -3
  65. package/build/utils/versions.js +1 -1
  66. package/node_modules/playwright/lib/mcp/browser/tools/files.js +8 -3
  67. package/node_modules/playwright/lib/mcp/browser/tools/wait.js +1 -1
  68. package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +10 -1
  69. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +33 -9
  70. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +7 -1
  71. package/node_modules/playwright/lib/mcp/test/skyRampExport.test.js +44 -0
  72. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.B_7ywgmr.js → index.d4gkVSou.js} +1 -1
  73. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
  74. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  75. package/node_modules/playwright/package.json +1 -1
  76. package/package.json +3 -3
@@ -6,6 +6,7 @@ import { TestExecutionStatus } from "../types/TestExecution.js";
6
6
  import * as fs from "fs/promises";
7
7
  import * as path from "path";
8
8
  import * as os from "os";
9
+ import { z } from "zod";
9
10
  jest.unstable_mockModule("../utils/logger.js", () => ({
10
11
  logger: { info: jest.fn(), error: jest.fn(), warning: jest.fn(), debug: jest.fn() },
11
12
  }));
@@ -18,17 +19,45 @@ const { AnalyticsService } = await import("../services/AnalyticsService.js");
18
19
  const pushMCPToolEventMock = AnalyticsService.pushMCPToolEvent;
19
20
  function captureToolHandler() {
20
21
  let handler;
22
+ let inputSchema;
21
23
  const fakeServer = {
22
- registerTool: (_name, _opts, fn) => {
24
+ registerTool: (_name, opts, fn) => {
23
25
  handler = fn;
26
+ inputSchema = opts.inputSchema;
24
27
  },
25
28
  };
26
29
  registerSubmitReportTool(fakeServer);
27
- return handler;
30
+ // Mirrors the real MCP SDK's registerTool: validates args against inputSchema via Zod
31
+ // before invoking the handler, so tests exercise the real tool contract (refinements
32
+ // included) instead of bypassing validation entirely.
33
+ return async (params) => {
34
+ const parsed = z.object(inputSchema).safeParse(params);
35
+ if (!parsed.success) {
36
+ return { isError: true, content: [{ type: "text", text: parsed.error.message }] };
37
+ }
38
+ return handler(parsed.data);
39
+ };
28
40
  }
29
41
  async function writeStateFile(dir, existingTests) {
30
42
  const stateFile = path.join(dir, "analyze-changes-state.json");
31
- await fs.writeFile(stateFile, JSON.stringify({ existingTests }), "utf-8");
43
+ // maintenanceVerdicts: [] simulates skyramp_actions having already run with nothing to
44
+ // do — the realistic state for tests not specifically exercising the maintenance flow.
45
+ await fs.writeFile(stateFile, JSON.stringify({ existingTests, maintenanceVerdicts: [], metadata: sampleMetadata(dir) }), "utf-8");
46
+ return stateFile;
47
+ }
48
+ function sampleMetadata(dir) {
49
+ return {
50
+ sessionId: "test-session",
51
+ stateType: "unified",
52
+ repositoryPath: dir,
53
+ createdAt: "2026-01-01T00:00:00.000Z",
54
+ updatedAt: "2026-01-01T00:00:00.000Z",
55
+ };
56
+ }
57
+ // SKYR-3906: state written by skyramp_actions after drift-analysis reconciliation.
58
+ async function writeReconciledStateFile(dir, existingTests, maintenanceVerdicts, ignoredTestFiles = []) {
59
+ const stateFile = path.join(dir, "analyze-changes-state.json");
60
+ await fs.writeFile(stateFile, JSON.stringify({ existingTests, maintenanceVerdicts, ignoredTestFiles, metadata: sampleMetadata(dir) }), "utf-8");
32
61
  return stateFile;
33
62
  }
34
63
  function sampleReportParams(outputFile, stateFile) {
@@ -37,9 +66,8 @@ function sampleReportParams(outputFile, stateFile) {
37
66
  stateFile,
38
67
  businessCaseAnalysis: "This PR adds product search. Tests needed for filtering.",
39
68
  newTestsCreated: [
40
- { testId: "smoke-get-products-search", testType: TestType.SMOKE, category: "workflow", endpoint: "GET /api/v1/products/search", fileName: "search_smoke_test.py", reasoning: "Validates product search returns correct results for category filter" },
69
+ { testId: "smoke-get-products-search", testType: TestType.SMOKE, category: "workflow", endpoint: "GET /api/v1/products/search", fileName: "search_smoke_test.py", description: "Searches products by category filter and asserts matching results are returned", reasoning: "Validates product search returns correct results for category filter" },
41
70
  ],
42
- testMaintenance: [{ action: "UPDATE", testType: TestType.CONTRACT, endpoint: "GET /api/v1/products", fileName: "products_contract_test.py", description: "Updated auth flow in existing tests", beforeDetails: "", afterDetails: "" }],
43
71
  testResults: [
44
72
  { testType: TestType.SMOKE, endpoint: "GET /api/v1/products", status: "Pass", details: "2.1s, products_smoke_test.py" },
45
73
  { testType: TestType.SMOKE, endpoint: "GET /api/v1/products/search", status: "Fail", details: "3.4s, search_smoke_test.py" },
@@ -83,7 +111,7 @@ describe("registerSubmitReportTool", () => {
83
111
  expect(written.newTestsCreated).toHaveLength(1);
84
112
  expect(written.testResults).toHaveLength(2);
85
113
  expect(written.issuesFound).toHaveLength(1);
86
- expect(written.testMaintenance).toHaveLength(1);
114
+ expect(written.testMaintenance).toBeUndefined();
87
115
  });
88
116
  it("accepts and persists the per-item `repository` field", async () => {
89
117
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
@@ -373,12 +401,12 @@ describe("registerSubmitReportTool", () => {
373
401
  expect(result.success).toBe(true);
374
402
  });
375
403
  it("rejects targetElement missing accessibleName", () => {
376
- const { accessibleName, ...withoutName } = validTarget;
404
+ const { accessibleName: _accessibleName, ...withoutName } = validTarget;
377
405
  const result = targetElementSchema.safeParse(withoutName);
378
406
  expect(result.success).toBe(false);
379
407
  });
380
408
  it("rejects targetElement missing role", () => {
381
- const { role, ...withoutRole } = validTarget;
409
+ const { role: _role, ...withoutRole } = validTarget;
382
410
  const result = targetElementSchema.safeParse(withoutRole);
383
411
  expect(result.success).toBe(false);
384
412
  });
@@ -560,9 +588,11 @@ describe("registerSubmitReportTool", () => {
560
588
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
561
589
  tmpDirs.push(tmpDir);
562
590
  const outputFile = path.join(tmpDir, "report.json");
563
- // sampleReportParams: 1 new test, 1 maintenance (no matching stateFile entry), 2 results
564
- // (1 Pass + 1 Fail), 1 issue (no severity).
565
- const stateFile = await writeStateFile(tmpDir, []);
591
+ // sampleReportParams: 1 new test, 2 results (1 Pass + 1 Fail), 1 issue (no severity).
592
+ // Plus 1 maintenance verdict (no matching existingTests entry) from state.
593
+ const stateFile = await writeReconciledStateFile(tmpDir, [], [
594
+ { testFilePath: "/repo/products_contract_test.py", action: DriftAction.Update, rationale: "Updated auth flow in existing tests", testType: TestType.CONTRACT, endpoint: "GET /api/v1/products" },
595
+ ]);
566
596
  await handler(sampleReportParams(outputFile, stateFile));
567
597
  const params = lastAnalyticsParams();
568
598
  expect(params.testsGenerated).toBe("1");
@@ -606,7 +636,6 @@ describe("registerSubmitReportTool", () => {
606
636
  stateFile,
607
637
  businessCaseAnalysis: "Config-only change.",
608
638
  newTestsCreated: [],
609
- testMaintenance: [],
610
639
  testResults: [],
611
640
  issuesFound: [],
612
641
  });
@@ -621,18 +650,15 @@ describe("registerSubmitReportTool", () => {
621
650
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
622
651
  tmpDirs.push(tmpDir);
623
652
  const outputFile = path.join(tmpDir, "report.json");
624
- const stateFile = await writeStateFile(tmpDir, []);
625
- await handler({
626
- ...sampleReportParams(outputFile, stateFile),
627
- testMaintenance: [
628
- // Real change
629
- { action: "UPDATE", testType: TestType.CONTRACT, endpoint: "GET /api/v1/products", fileName: "products_contract_test.py", description: "Patched auth", beforeDetails: "", afterDetails: "" },
630
- { action: "REGENERATE", testType: TestType.SMOKE, endpoint: "GET /api/v1/orders", fileName: "orders_smoke_test.py", description: "Rewrote for new shape", beforeDetails: "", afterDetails: "" },
631
- // No-ops — assessed but not modified
632
- { action: "IGNORE", testType: TestType.SMOKE, endpoint: "GET /api/v1/reviews", fileName: "reviews_smoke_test.py", description: "No action required", beforeDetails: "", afterDetails: "" },
633
- { action: "VERIFY", testType: TestType.SMOKE, endpoint: "GET /api/v1/coupons", fileName: "coupons_smoke_test.py", description: "Verified still valid", beforeDetails: "", afterDetails: "" },
634
- ],
635
- });
653
+ const stateFile = await writeReconciledStateFile(tmpDir, [], [
654
+ // Real change
655
+ { testFilePath: "/repo/products_contract_test.py", action: DriftAction.Update, rationale: "Patched auth", testType: TestType.CONTRACT, endpoint: "GET /api/v1/products" },
656
+ { testFilePath: "/repo/orders_smoke_test.py", action: DriftAction.Regenerate, rationale: "Rewrote for new shape", testType: TestType.SMOKE, endpoint: "GET /api/v1/orders" },
657
+ // No-ops — assessed but not modified
658
+ { testFilePath: "/repo/reviews_smoke_test.py", action: DriftAction.Ignore, rationale: "No action required", testType: TestType.SMOKE, endpoint: "GET /api/v1/reviews" },
659
+ { testFilePath: "/repo/coupons_smoke_test.py", action: DriftAction.Verify, rationale: "Verified still valid", testType: TestType.SMOKE, endpoint: "GET /api/v1/coupons" },
660
+ ]);
661
+ await handler(sampleReportParams(outputFile, stateFile));
636
662
  const params = lastAnalyticsParams();
637
663
  // 2 change actions (UPDATE + REGENERATE), not 4
638
664
  expect(params.testsMaintained).toBe("2");
@@ -647,89 +673,106 @@ describe("registerSubmitReportTool", () => {
647
673
  // when toOutput or the stateFile enrichment path changes.
648
674
  // ─────────────────────────────────────────────────────────────────────────────
649
675
  describe("testMaintenance output shapes", () => {
650
- const baseMaintenance = {
676
+ const baseVerdict = {
677
+ testFilePath: "/repo/backend/tests/orders_contract_test.py",
678
+ rationale: "Updated discount fields in assertions",
651
679
  testType: TestType.CONTRACT,
652
680
  endpoint: "GET /api/v1/orders",
653
- fileName: "orders_contract_test.py",
654
- description: "Updated discount fields in assertions",
655
- beforeDetails: "",
656
- afterDetails: "",
657
681
  };
658
- async function writeAndRead(tmpDir, maintenance, extra) {
682
+ async function submitAndReadMaintenance(tmpDir, stateFile, testMaintenanceDetails = []) {
659
683
  const outputFile = path.join(tmpDir, "report.json");
660
- await handler({
684
+ const result = await handler({
661
685
  summaryOutputFile: outputFile,
686
+ stateFile,
662
687
  businessCaseAnalysis: "Test",
663
688
  newTestsCreated: [],
664
- testMaintenance: maintenance,
689
+ testMaintenanceDetails,
665
690
  testResults: [],
666
691
  issuesFound: [],
667
- ...extra,
668
692
  });
693
+ if (result.isError)
694
+ throw new Error(`handler rejected: ${result.content?.[0]?.text}`);
669
695
  const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
670
696
  return written.testMaintenance;
671
697
  }
672
- // ── No matching stateFile entry ───────────────────────────────────────────
673
- it("UPDATE with no matching stateFile entry: beforeStatus=Unknown, afterStatus=Unknown", async () => {
698
+ // ── No matching existingTests entry ───────────────────────────────────────
699
+ it("UPDATE with no matching existingTests entry: beforeStatus=Unknown, afterStatus=Unknown", async () => {
674
700
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
675
701
  tmpDirs.push(tmpDir);
676
- const stateFile = await writeStateFile(tmpDir, []);
677
- const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Update }], { stateFile });
702
+ const stateFile = await writeReconciledStateFile(tmpDir, [], [{ ...baseVerdict, action: DriftAction.Update }]);
703
+ const result = await submitAndReadMaintenance(tmpDir, stateFile);
678
704
  expect(result[0].beforeStatus).toBe("Unknown");
679
705
  expect(result[0].afterStatus).toBe("Unknown");
680
706
  expect(result[0].beforeDetails).toBe("");
681
707
  expect(result[0].afterDetails).toBe("");
682
708
  });
683
- it("IGNORE with no matching stateFile entry: both Skipped", async () => {
709
+ it("IGNORE with no matching existingTests entry: both Skipped", async () => {
684
710
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
685
711
  tmpDirs.push(tmpDir);
686
- const stateFile = await writeStateFile(tmpDir, []);
687
- const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Ignore }], { stateFile });
712
+ const stateFile = await writeReconciledStateFile(tmpDir, [], [{ ...baseVerdict, action: DriftAction.Ignore }]);
713
+ const result = await submitAndReadMaintenance(tmpDir, stateFile);
688
714
  expect(result[0].beforeStatus).toBe("Skipped");
689
715
  expect(result[0].afterStatus).toBe("Skipped");
690
716
  expect(result[0].beforeDetails).toBe("");
691
717
  expect(result[0].afterDetails).toBe("");
692
718
  });
693
- it("DELETE with no matching stateFile entry: beforeStatus=Unknown, afterStatus=Skipped", async () => {
719
+ it("DELETE with no matching existingTests entry: beforeStatus=Unknown, afterStatus=Skipped", async () => {
694
720
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
695
721
  tmpDirs.push(tmpDir);
696
- const stateFile = await writeStateFile(tmpDir, []);
697
- const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Delete }], { stateFile });
722
+ const stateFile = await writeReconciledStateFile(tmpDir, [], [{ ...baseVerdict, action: DriftAction.Delete }]);
723
+ const result = await submitAndReadMaintenance(tmpDir, stateFile);
698
724
  expect(result[0].beforeStatus).toBe("Unknown");
699
725
  expect(result[0].afterStatus).toBe("Skipped");
700
726
  });
701
- // ── With matching stateFile entry ─────────────────────────────────────────
702
- it("UPDATE with before=Pass after=Pass: status from stateFile, details from LLM", async () => {
727
+ // ── With matching existingTests entry ─────────────────────────────────────
728
+ it("UPDATE with before=Pass after=Pass: status from stateFile, details from the LLM-drafted testMaintenanceDetails", async () => {
703
729
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
704
730
  tmpDirs.push(tmpDir);
705
- const stateFile = await writeStateFile(tmpDir, [{
706
- testFile: "/repo/backend/tests/orders_contract_test.py",
731
+ const stateFile = await writeReconciledStateFile(tmpDir, [{
732
+ testFile: baseVerdict.testFilePath,
707
733
  executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 15090 },
708
734
  executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 12880 },
709
- }]);
710
- const result = await writeAndRead(tmpDir, [{
711
- ...baseMaintenance, action: DriftAction.Update,
712
- beforeDetails: "4 passed in 15.09s",
713
- afterDetails: "5 passed in 12.88s",
714
- }], { stateFile });
735
+ }], [{ ...baseVerdict, action: DriftAction.Update }]);
736
+ const result = await submitAndReadMaintenance(tmpDir, stateFile, [
737
+ { testFilePath: baseVerdict.testFilePath, beforeDetails: "4 passed in 15.09s", afterDetails: "5 passed in 12.88s" },
738
+ ]);
715
739
  expect(result[0].beforeStatus).toBe("Pass");
716
740
  expect(result[0].beforeDetails).toBe("4 passed in 15.09s");
717
741
  expect(result[0].afterStatus).toBe("Pass");
718
742
  expect(result[0].afterDetails).toBe("5 passed in 12.88s");
719
743
  });
720
- it("UPDATE with before=Pass after=Fail: status from stateFile, details from LLM", async () => {
744
+ it("UPDATE with before=Pass after=Fail: rejects when afterDetails is missing for a recorded execution", async () => {
745
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
746
+ tmpDirs.push(tmpDir);
747
+ const stateFile = await writeReconciledStateFile(tmpDir, [{
748
+ testFile: baseVerdict.testFilePath,
749
+ executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 15090 },
750
+ executionAfter: { status: TestExecutionStatus.Fail, errors: ["check_schema fails, test isolation bug"], duration: 10940 },
751
+ }], [{ ...baseVerdict, action: DriftAction.Update }]);
752
+ const outputFile = path.join(tmpDir, "report.json");
753
+ const result = await handler({
754
+ summaryOutputFile: outputFile,
755
+ stateFile,
756
+ businessCaseAnalysis: "Test",
757
+ newTestsCreated: [],
758
+ testMaintenanceDetails: [{ testFilePath: baseVerdict.testFilePath, beforeDetails: "4 passed in 15.09s", afterDetails: "" }], // afterDetails left blank
759
+ testResults: [],
760
+ issuesFound: [],
761
+ });
762
+ expect(result.isError).toBe(true);
763
+ expect(result.content[0].text).toContain("orders_contract_test.py (afterDetails)");
764
+ });
765
+ it("UPDATE with before=Pass after=Fail: afterDetails carries the LLM's drafted root cause when supplied", async () => {
721
766
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
722
767
  tmpDirs.push(tmpDir);
723
- const stateFile = await writeStateFile(tmpDir, [{
724
- testFile: "/repo/backend/tests/orders_contract_test.py",
768
+ const stateFile = await writeReconciledStateFile(tmpDir, [{
769
+ testFile: baseVerdict.testFilePath,
725
770
  executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 15090 },
726
- executionAfter: { status: TestExecutionStatus.Fail, errors: [], duration: 10940 },
727
- }]);
728
- const result = await writeAndRead(tmpDir, [{
729
- ...baseMaintenance, action: DriftAction.Update,
730
- beforeDetails: "4 passed in 15.09s",
731
- afterDetails: "FAILED test_orders_get — check_schema fails, test isolation bug",
732
- }], { stateFile });
771
+ executionAfter: { status: TestExecutionStatus.Fail, errors: ["check_schema fails, test isolation bug"], duration: 10940 },
772
+ }], [{ ...baseVerdict, action: DriftAction.Update }]);
773
+ const result = await submitAndReadMaintenance(tmpDir, stateFile, [
774
+ { testFilePath: baseVerdict.testFilePath, beforeDetails: "4 passed in 15.09s", afterDetails: "FAILED test_orders_get — check_schema fails, test isolation bug" },
775
+ ]);
733
776
  expect(result[0].beforeStatus).toBe("Pass");
734
777
  expect(result[0].afterStatus).toBe("Fail");
735
778
  expect(result[0].afterDetails).toBe("FAILED test_orders_get — check_schema fails, test isolation bug");
@@ -737,67 +780,135 @@ describe("registerSubmitReportTool", () => {
737
780
  it("UPDATE with crashed execution: status is Error", async () => {
738
781
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
739
782
  tmpDirs.push(tmpDir);
740
- const stateFile = await writeStateFile(tmpDir, [{
741
- testFile: "/repo/backend/tests/orders_contract_test.py",
783
+ const stateFile = await writeReconciledStateFile(tmpDir, [{
784
+ testFile: baseVerdict.testFilePath,
742
785
  executionBefore: { status: TestExecutionStatus.Error, errors: ["Connection refused"], duration: 500 },
743
786
  executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
744
- }]);
745
- const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Update }], { stateFile });
787
+ }], [{ ...baseVerdict, action: DriftAction.Update }]);
788
+ const result = await submitAndReadMaintenance(tmpDir, stateFile, [
789
+ { testFilePath: baseVerdict.testFilePath, beforeDetails: "Error — Connection refused", afterDetails: "5 passed in 2.00s" },
790
+ ]);
746
791
  expect(result[0].beforeStatus).toBe("Error");
747
792
  expect(result[0].afterStatus).toBe("Pass");
748
793
  });
749
794
  it("UPDATE Fail→Pass: counted as maintenanceRecovered in analytics", async () => {
750
795
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
751
796
  tmpDirs.push(tmpDir);
752
- const stateFile = await writeStateFile(tmpDir, [{
753
- testFile: "/repo/backend/tests/orders_contract_test.py",
797
+ const stateFile = await writeReconciledStateFile(tmpDir, [{
798
+ testFile: baseVerdict.testFilePath,
754
799
  executionBefore: { status: TestExecutionStatus.Fail, errors: ["FAILED test_foo - assert False"], duration: 5000 },
755
800
  executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 5000 },
756
- }]);
757
- await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Update }], { stateFile });
801
+ }], [{ ...baseVerdict, action: DriftAction.Update }]);
802
+ await submitAndReadMaintenance(tmpDir, stateFile, [
803
+ { testFilePath: baseVerdict.testFilePath, beforeDetails: "FAILED test_foo - assert False", afterDetails: "5 passed in 5.00s" },
804
+ ]);
758
805
  expect(pushMCPToolEventMock.mock.calls.at(-1)?.[2].maintenanceRecovered).toBe("1");
759
806
  });
760
- it("UPDATE with no before-execution recorded: beforeStatus=Unknown, afterStatus=Pass", async () => {
807
+ it("UPDATE with no before-execution recorded: beforeStatus=Unknown, afterStatus=Pass, no beforeDetails required", async () => {
761
808
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
762
809
  tmpDirs.push(tmpDir);
763
- const stateFile = await writeStateFile(tmpDir, [{
764
- testFile: "/repo/backend/tests/orders_contract_test.py",
810
+ const stateFile = await writeReconciledStateFile(tmpDir, [{
811
+ testFile: baseVerdict.testFilePath,
765
812
  // executionBefore absent — agent skipped before-phase
766
813
  executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
767
- }]);
768
- const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Update }], { stateFile });
814
+ }], [{ ...baseVerdict, action: DriftAction.Update }]);
815
+ // Only afterDetails is meaningful there's no recorded before-execution to summarize,
816
+ // so beforeDetails is the empty-string sentinel (the field is required, not omittable).
817
+ const result = await submitAndReadMaintenance(tmpDir, stateFile, [
818
+ { testFilePath: baseVerdict.testFilePath, beforeDetails: "", afterDetails: "5 passed in 2.00s" },
819
+ ]);
769
820
  expect(result[0].beforeStatus).toBe("Unknown");
770
821
  expect(result[0].afterStatus).toBe("Pass");
771
822
  });
772
- it("DELETE with before-execution, no after (file deleted): beforeStatus=Pass, afterStatus=Skipped", async () => {
823
+ it("DELETE with before-execution, no after (file deleted): beforeStatus=Pass, afterStatus=Skipped, no afterDetails required", async () => {
773
824
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
774
825
  tmpDirs.push(tmpDir);
775
- const stateFile = await writeStateFile(tmpDir, [{
776
- testFile: "/repo/backend/tests/orders_contract_test.py",
826
+ const stateFile = await writeReconciledStateFile(tmpDir, [{
827
+ testFile: baseVerdict.testFilePath,
777
828
  executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
778
829
  // executionAfter absent — file was deleted, nothing to run
779
- }]);
780
- const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Delete }], { stateFile });
830
+ }], [{ ...baseVerdict, action: DriftAction.Delete }]);
831
+ // Only beforeDetails is meaningful there's no recorded after-execution to summarize,
832
+ // so afterDetails is the empty-string sentinel (the field is required, not omittable).
833
+ const result = await submitAndReadMaintenance(tmpDir, stateFile, [
834
+ { testFilePath: baseVerdict.testFilePath, beforeDetails: "4 passed in 2.00s", afterDetails: "" },
835
+ ]);
781
836
  expect(result[0].beforeStatus).toBe("Pass");
782
837
  expect(result[0].afterStatus).toBe("Skipped");
783
838
  });
784
- it("fileName matched by suffix: absolute stateFile path vs relative fileName", async () => {
839
+ it("REGENERATE with before=Fail after=Pass: requires both details, rejects when either is missing", async () => {
785
840
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
786
841
  tmpDirs.push(tmpDir);
787
- const stateFile = await writeStateFile(tmpDir, [{
788
- testFile: "/repo/backend/tests/python/orders_contract_test.py",
789
- executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 15000 },
790
- executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 12000 },
791
- }]);
792
- // fileName is relative — should still match via endsWith
793
- const result = await writeAndRead(tmpDir, [{
794
- ...baseMaintenance,
795
- action: DriftAction.Update,
796
- fileName: "orders_contract_test.py",
797
- }], { stateFile });
798
- expect(result[0].beforeStatus).toBe("Pass");
842
+ const stateFile = await writeReconciledStateFile(tmpDir, [{
843
+ testFile: baseVerdict.testFilePath,
844
+ executionBefore: { status: TestExecutionStatus.Fail, errors: ["schema drift"], duration: 4000 },
845
+ executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 3000 },
846
+ }], [{ ...baseVerdict, action: DriftAction.Regenerate }]);
847
+ const outputFile = path.join(tmpDir, "report.json");
848
+ const rejected = await handler({
849
+ summaryOutputFile: outputFile,
850
+ stateFile,
851
+ businessCaseAnalysis: "Test",
852
+ newTestsCreated: [],
853
+ testMaintenanceDetails: [{ testFilePath: baseVerdict.testFilePath, beforeDetails: "FAILED — schema drift", afterDetails: "" }], // afterDetails left blank
854
+ testResults: [],
855
+ issuesFound: [],
856
+ });
857
+ expect(rejected.isError).toBe(true);
858
+ expect(rejected.content[0].text).toContain("orders_contract_test.py (afterDetails)");
859
+ const result = await submitAndReadMaintenance(tmpDir, stateFile, [
860
+ { testFilePath: baseVerdict.testFilePath, beforeDetails: "FAILED — schema drift", afterDetails: "regenerated test now passes" },
861
+ ]);
862
+ expect(result[0].beforeStatus).toBe("Fail");
799
863
  expect(result[0].afterStatus).toBe("Pass");
800
864
  });
865
+ it("VERIFY with a recorded execution: requires details even though VERIFY is not a change action", async () => {
866
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
867
+ tmpDirs.push(tmpDir);
868
+ const stateFile = await writeReconciledStateFile(tmpDir, [{
869
+ testFile: baseVerdict.testFilePath,
870
+ executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 1500 },
871
+ }], [{ ...baseVerdict, action: DriftAction.Verify }]);
872
+ const outputFile = path.join(tmpDir, "report.json");
873
+ const rejected = await handler({
874
+ summaryOutputFile: outputFile,
875
+ stateFile,
876
+ businessCaseAnalysis: "Test",
877
+ newTestsCreated: [],
878
+ testMaintenanceDetails: [],
879
+ testResults: [],
880
+ issuesFound: [],
881
+ });
882
+ expect(rejected.isError).toBe(true);
883
+ expect(rejected.content[0].text).toContain("orders_contract_test.py (beforeDetails)");
884
+ const result = await submitAndReadMaintenance(tmpDir, stateFile, [
885
+ { testFilePath: baseVerdict.testFilePath, beforeDetails: "confirmed still passes, no changes needed", afterDetails: "" },
886
+ ]);
887
+ expect(result[0].beforeStatus).toBe("Pass");
888
+ });
889
+ it("DELETE with a spurious executionAfter recorded: afterDetails is still required despite DELETE's default afterStatus being Skipped", async () => {
890
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
891
+ tmpDirs.push(tmpDir);
892
+ const stateFile = await writeReconciledStateFile(tmpDir, [{
893
+ testFile: baseVerdict.testFilePath,
894
+ executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
895
+ // executionAfter present even though the file should have been deleted — documents
896
+ // that the required-details gate keys off recorded executions, not the action type.
897
+ executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 1800 },
898
+ }], [{ ...baseVerdict, action: DriftAction.Delete }]);
899
+ const outputFile = path.join(tmpDir, "report.json");
900
+ const rejected = await handler({
901
+ summaryOutputFile: outputFile,
902
+ stateFile,
903
+ businessCaseAnalysis: "Test",
904
+ newTestsCreated: [],
905
+ testMaintenanceDetails: [{ testFilePath: baseVerdict.testFilePath, beforeDetails: "4 passed in 2.00s", afterDetails: "" }], // afterDetails left blank
906
+ testResults: [],
907
+ issuesFound: [],
908
+ });
909
+ expect(rejected.isError).toBe(true);
910
+ expect(rejected.content[0].text).toContain("orders_contract_test.py (afterDetails)");
911
+ });
801
912
  });
802
913
  it("testResult accepts videoPath field (optional)", async () => {
803
914
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
@@ -853,4 +964,210 @@ describe("registerSubmitReportTool", () => {
853
964
  expect(written.testResults).toHaveLength(1);
854
965
  expect(written.testResults[0].videoPath).toBeUndefined();
855
966
  });
967
+ describe("testMaintenance derived from state (SKYR-3906)", () => {
968
+ it("derives testMaintenance from stateData.maintenanceVerdicts when testMaintenance is omitted", async () => {
969
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
970
+ tmpDirs.push(tmpDir);
971
+ const outputFile = path.join(tmpDir, "report.json");
972
+ const stateFile = await writeReconciledStateFile(tmpDir, [{ testFile: "/repo/tests/orders_contract_test.py" }], [{ testFilePath: "/repo/tests/orders_contract_test.py", action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }]);
973
+ const result = await handler({
974
+ summaryOutputFile: outputFile,
975
+ stateFile,
976
+ businessCaseAnalysis: "Adds a field to orders",
977
+ newTestsCreated: [],
978
+ // testMaintenance intentionally omitted — should be derived from state.
979
+ testResults: [],
980
+ issuesFound: [],
981
+ });
982
+ expect(result.isError).toBeUndefined();
983
+ const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
984
+ expect(written.testMaintenance).toEqual([
985
+ expect.objectContaining({
986
+ fileName: "orders_contract_test.py",
987
+ testType: TestType.CONTRACT,
988
+ endpoint: "GET /api/v1/orders",
989
+ action: DriftAction.Update,
990
+ description: "Field added",
991
+ }),
992
+ ]);
993
+ });
994
+ it("rejects when maintenanceVerdicts is undefined and existing tests were present — skyramp_actions was never called", async () => {
995
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
996
+ tmpDirs.push(tmpDir);
997
+ const outputFile = path.join(tmpDir, "report.json");
998
+ // Plain writeStateFile with no maintenanceVerdicts override — undefined, distinct
999
+ // from skyramp_actions having run and recorded an empty array.
1000
+ const stateFile = path.join(tmpDir, "analyze-changes-state.json");
1001
+ await fs.writeFile(stateFile, JSON.stringify({
1002
+ existingTests: [{ testFile: "/repo/tests/orders_contract_test.py" }],
1003
+ metadata: { sessionId: "test-session", stateType: "unified", repositoryPath: tmpDir, createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z" },
1004
+ }), "utf-8");
1005
+ const result = await handler({
1006
+ summaryOutputFile: outputFile,
1007
+ stateFile,
1008
+ businessCaseAnalysis: "Adds a field to orders",
1009
+ newTestsCreated: [],
1010
+ testResults: [],
1011
+ issuesFound: [],
1012
+ });
1013
+ expect(result.isError).toBe(true);
1014
+ expect(result.content[0].text).toContain("skyramp_actions was not called");
1015
+ });
1016
+ it("allows maintenanceVerdicts to be undefined when there were no existing tests to assess", async () => {
1017
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
1018
+ tmpDirs.push(tmpDir);
1019
+ const outputFile = path.join(tmpDir, "report.json");
1020
+ // No existing tests at all — nothing skyramp_actions could have lost, so skipping
1021
+ // it entirely (never writing maintenanceVerdicts) is a valid, not an error, state.
1022
+ const stateFile = path.join(tmpDir, "analyze-changes-state.json");
1023
+ await fs.writeFile(stateFile, JSON.stringify({
1024
+ existingTests: [],
1025
+ metadata: { sessionId: "test-session", stateType: "unified", repositoryPath: tmpDir, createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z" },
1026
+ }), "utf-8");
1027
+ const result = await handler({
1028
+ summaryOutputFile: outputFile,
1029
+ stateFile,
1030
+ businessCaseAnalysis: "Adds a new endpoint with no prior coverage",
1031
+ newTestsCreated: [],
1032
+ testResults: [],
1033
+ issuesFound: [],
1034
+ });
1035
+ expect(result.isError).toBeUndefined();
1036
+ const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
1037
+ expect(written.testMaintenance).toBeUndefined();
1038
+ });
1039
+ it("rejects when a related repo has reconciled maintenanceVerdicts — testMaintenance has no repository field to carry them", async () => {
1040
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
1041
+ tmpDirs.push(tmpDir);
1042
+ const outputFile = path.join(tmpDir, "report.json");
1043
+ const stateFile = path.join(tmpDir, "analyze-changes-state.json");
1044
+ const now = "2026-01-01T00:00:00.000Z";
1045
+ await fs.writeFile(stateFile, JSON.stringify({
1046
+ existingTests: [],
1047
+ maintenanceVerdicts: [],
1048
+ metadata: { sessionId: "test-session", stateType: "unified", repositoryPath: tmpDir, createdAt: now, updatedAt: now },
1049
+ relatedRepos: {
1050
+ "acme/backend": {
1051
+ repositoryPath: "/repo/backend",
1052
+ data: {
1053
+ existingTests: [],
1054
+ maintenanceVerdicts: [
1055
+ { testFilePath: "/repo/backend/tests/orders_test.py", action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" },
1056
+ ],
1057
+ },
1058
+ },
1059
+ },
1060
+ }), "utf-8");
1061
+ const result = await handler({
1062
+ summaryOutputFile: outputFile,
1063
+ stateFile,
1064
+ businessCaseAnalysis: "Test",
1065
+ newTestsCreated: [],
1066
+ testResults: [],
1067
+ issuesFound: [],
1068
+ });
1069
+ expect(result.isError).toBe(true);
1070
+ expect(result.content[0].text).toContain("acme/backend");
1071
+ });
1072
+ it("rejects when a recorded execution has no matching testMaintenanceDetails entry at all", async () => {
1073
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
1074
+ tmpDirs.push(tmpDir);
1075
+ const outputFile = path.join(tmpDir, "report.json");
1076
+ const stateFile = await writeReconciledStateFile(tmpDir, [{
1077
+ testFile: "/repo/tests/orders_contract_test.py",
1078
+ executionBefore: { status: TestExecutionStatus.Fail, errors: ["assert 403 got 200"], duration: 4200 },
1079
+ executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 5090 },
1080
+ }], [{ testFilePath: "/repo/tests/orders_contract_test.py", action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }]);
1081
+ const result = await handler({
1082
+ summaryOutputFile: outputFile,
1083
+ stateFile,
1084
+ businessCaseAnalysis: "Adds a field to orders",
1085
+ newTestsCreated: [],
1086
+ // testMaintenanceDetails omitted entirely, despite both executions being recorded.
1087
+ testResults: [],
1088
+ issuesFound: [],
1089
+ });
1090
+ expect(result.isError).toBe(true);
1091
+ expect(result.content[0].text).toContain("orders_contract_test.py (beforeDetails)");
1092
+ expect(result.content[0].text).toContain("orders_contract_test.py (afterDetails)");
1093
+ });
1094
+ it("rejects a whitespace-only testMaintenanceDetails summary as if it were missing", async () => {
1095
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
1096
+ tmpDirs.push(tmpDir);
1097
+ const outputFile = path.join(tmpDir, "report.json");
1098
+ const stateFile = await writeReconciledStateFile(tmpDir, [{
1099
+ testFile: "/repo/tests/orders_contract_test.py",
1100
+ executionBefore: { status: TestExecutionStatus.Fail, errors: ["assert 403 got 200"], duration: 4200 },
1101
+ executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 5090 },
1102
+ }], [{ testFilePath: "/repo/tests/orders_contract_test.py", action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }]);
1103
+ const result = await handler({
1104
+ summaryOutputFile: outputFile,
1105
+ stateFile,
1106
+ businessCaseAnalysis: "Adds a field to orders",
1107
+ newTestsCreated: [],
1108
+ testMaintenanceDetails: [{
1109
+ testFilePath: "/repo/tests/orders_contract_test.py",
1110
+ beforeDetails: " ",
1111
+ afterDetails: "5 passed in 5.09s",
1112
+ }],
1113
+ testResults: [],
1114
+ issuesFound: [],
1115
+ });
1116
+ expect(result.isError).toBe(true);
1117
+ expect(result.content[0].text).toContain("orders_contract_test.py (beforeDetails)");
1118
+ });
1119
+ it("prefers the LLM-drafted testMaintenanceDetails summary when supplied", async () => {
1120
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
1121
+ tmpDirs.push(tmpDir);
1122
+ const outputFile = path.join(tmpDir, "report.json");
1123
+ const stateFile = await writeReconciledStateFile(tmpDir, [{
1124
+ testFile: "/repo/tests/orders_contract_test.py",
1125
+ executionBefore: { status: TestExecutionStatus.Fail, errors: ["assert 403 got 200"], duration: 4200 },
1126
+ executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 5090 },
1127
+ }], [{ testFilePath: "/repo/tests/orders_contract_test.py", action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }]);
1128
+ const result = await handler({
1129
+ summaryOutputFile: outputFile,
1130
+ stateFile,
1131
+ businessCaseAnalysis: "Adds a field to orders",
1132
+ newTestsCreated: [],
1133
+ testMaintenanceDetails: [{
1134
+ testFilePath: "/repo/tests/orders_contract_test.py",
1135
+ beforeDetails: "FAILED test_orders_get — auth middleware not enforced for the new field",
1136
+ afterDetails: "5 passed in 5.09s",
1137
+ }],
1138
+ testResults: [],
1139
+ issuesFound: [],
1140
+ });
1141
+ expect(result.isError).toBeUndefined();
1142
+ const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
1143
+ expect(written.testMaintenance[0].beforeDetails).toBe("FAILED test_orders_get — auth middleware not enforced for the new field");
1144
+ expect(written.testMaintenance[0].afterDetails).toBe("5 passed in 5.09s");
1145
+ });
1146
+ it("rejects when only one of beforeDetails/afterDetails is supplied but both executions were recorded", async () => {
1147
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
1148
+ tmpDirs.push(tmpDir);
1149
+ const outputFile = path.join(tmpDir, "report.json");
1150
+ const stateFile = await writeReconciledStateFile(tmpDir, [{
1151
+ testFile: "/repo/tests/orders_contract_test.py",
1152
+ executionBefore: { status: TestExecutionStatus.Fail, errors: ["assert 403 got 200"], duration: 4200 },
1153
+ executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 5090 },
1154
+ }], [{ testFilePath: "/repo/tests/orders_contract_test.py", action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }]);
1155
+ const result = await handler({
1156
+ summaryOutputFile: outputFile,
1157
+ stateFile,
1158
+ businessCaseAnalysis: "Adds a field to orders",
1159
+ newTestsCreated: [],
1160
+ testMaintenanceDetails: [{
1161
+ testFilePath: "/repo/tests/orders_contract_test.py",
1162
+ beforeDetails: "FAILED test_orders_get — auth middleware not enforced for the new field",
1163
+ afterDetails: "", // left blank, but an after-execution was recorded — must be rejected.
1164
+ }],
1165
+ testResults: [],
1166
+ issuesFound: [],
1167
+ });
1168
+ expect(result.isError).toBe(true);
1169
+ expect(result.content[0].text).toContain("orders_contract_test.py (afterDetails)");
1170
+ expect(result.content[0].text).not.toContain("beforeDetails");
1171
+ });
1172
+ });
856
1173
  });