@skyramp/mcp 0.2.8 → 0.2.150-rc.local.de

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 (93) hide show
  1. package/build/commands/commandLibrary.d.ts +1 -0
  2. package/build/commands/commandLibrary.js +18 -17
  3. package/build/commands/localDevTestChangesCommand.d.ts +15 -0
  4. package/build/commands/localDevTestChangesCommand.js +175 -0
  5. package/build/index.js +74 -13
  6. package/build/prompts/enhance-assertions/sharedAssertionRules.js +1 -0
  7. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +7 -6
  8. package/build/prompts/local-dev/local-dev-plan.d.ts +25 -0
  9. package/build/prompts/local-dev/local-dev-plan.js +414 -0
  10. package/build/prompts/local-dev/local-dev-prompts.d.ts +4 -0
  11. package/build/prompts/local-dev/local-dev-prompts.js +152 -0
  12. package/build/prompts/prompt-utils.d.ts +8 -0
  13. package/build/prompts/prompt-utils.js +33 -0
  14. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +3 -1
  15. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +3 -1
  16. package/build/prompts/sut-setup/shared.d.ts +20 -0
  17. package/build/prompts/sut-setup/shared.js +69 -7
  18. package/build/prompts/test-recommendation/analysisOutputPrompt.js +21 -29
  19. package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +79 -16
  20. package/build/prompts/test-recommendation/test-recommendation-prompt.js +41 -4
  21. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +34 -1
  22. package/build/prompts/testbot/testbot-prompts.d.ts +0 -5
  23. package/build/prompts/testbot/testbot-prompts.js +4 -34
  24. package/build/resources/testbotResource.js +2 -1
  25. package/build/services/TestExecutionService.d.ts +10 -2
  26. package/build/services/TestExecutionService.js +220 -28
  27. package/build/services/TestExecutionService.test.js +447 -16
  28. package/build/services/TestGenerationService.d.ts +2 -2
  29. package/build/tool-phases.js +4 -0
  30. package/build/tools/code-refactor/enhanceAssertionsTool.js +34 -2
  31. package/build/tools/enrichTestWithMocksTool.d.ts +15 -0
  32. package/build/tools/enrichTestWithMocksTool.js +414 -0
  33. package/build/tools/enrichTestWithMocksTool.test.d.ts +1 -0
  34. package/build/tools/enrichTestWithMocksTool.test.js +397 -0
  35. package/build/tools/executeSkyrampTestTool.d.ts +3 -0
  36. package/build/tools/executeSkyrampTestTool.js +73 -18
  37. package/build/tools/executeSkyrampTestTool.test.d.ts +1 -0
  38. package/build/tools/executeSkyrampTestTool.test.js +111 -0
  39. package/build/tools/generate-tests/batchMockGenerationTool.d.ts +72 -0
  40. package/build/tools/generate-tests/batchMockGenerationTool.js +382 -0
  41. package/build/tools/generate-tests/batchMockGenerationTool.test.d.ts +1 -0
  42. package/build/tools/generate-tests/batchMockGenerationTool.test.js +514 -0
  43. package/build/tools/generate-tests/generateContractRestTool.js +2 -2
  44. package/build/tools/generate-tests/generateMockRestTool.d.ts +56 -6
  45. package/build/tools/generate-tests/generateMockRestTool.js +193 -21
  46. package/build/tools/generate-tests/generateMockRestTool.test.d.ts +1 -0
  47. package/build/tools/generate-tests/generateMockRestTool.test.js +340 -0
  48. package/build/tools/generateEnrichedIntegrationTestTool.d.ts +24 -0
  49. package/build/tools/generateEnrichedIntegrationTestTool.js +209 -0
  50. package/build/tools/generateEnrichedIntegrationTestTool.test.d.ts +1 -0
  51. package/build/tools/generateEnrichedIntegrationTestTool.test.js +199 -0
  52. package/build/tools/one-click/oneClickTool.d.ts +10 -0
  53. package/build/tools/one-click/oneClickTool.js +182 -40
  54. package/build/tools/one-click/oneClickTool.test.d.ts +1 -0
  55. package/build/tools/one-click/oneClickTool.test.js +204 -0
  56. package/build/tools/preflightMockCheckTool.d.ts +2 -0
  57. package/build/tools/preflightMockCheckTool.js +91 -0
  58. package/build/tools/test-management/analyzeChangesTool.d.ts +2 -1
  59. package/build/tools/test-management/analyzeChangesTool.js +59 -38
  60. package/build/tools/workspace/initializeWorkspaceTool.js +2 -9
  61. package/build/tools/workspace/initializeWorkspaceTool.test.js +9 -4
  62. package/build/types/OneClickCommands.d.ts +1 -1
  63. package/build/types/RepositoryAnalysis.d.ts +117 -0
  64. package/build/types/RepositoryAnalysis.js +16 -2
  65. package/build/types/TestTypes.d.ts +11 -2
  66. package/build/types/TestTypes.js +11 -1
  67. package/build/utils/analyze-openapi.js +18 -1
  68. package/build/utils/analyze-openapi.test.d.ts +1 -0
  69. package/build/utils/analyze-openapi.test.js +19 -0
  70. package/build/utils/branchDiff.d.ts +17 -1
  71. package/build/utils/branchDiff.js +96 -14
  72. package/build/utils/branchDiff.test.d.ts +1 -0
  73. package/build/utils/branchDiff.test.js +107 -0
  74. package/build/utils/docker.test.js +1 -1
  75. package/build/utils/featureFlags.d.ts +15 -1
  76. package/build/utils/featureFlags.js +19 -2
  77. package/build/utils/featureFlags.test.js +57 -2
  78. package/build/utils/grpcMockValidation.d.ts +1 -0
  79. package/build/utils/grpcMockValidation.js +49 -0
  80. package/build/utils/grpcMockValidation.test.d.ts +1 -0
  81. package/build/utils/grpcMockValidation.test.js +41 -0
  82. package/build/utils/httpMethodValidation.d.ts +4 -0
  83. package/build/utils/httpMethodValidation.js +13 -0
  84. package/build/utils/mockCompatibility.d.ts +49 -0
  85. package/build/utils/mockCompatibility.js +80 -0
  86. package/build/utils/mockCompatibility.test.d.ts +1 -0
  87. package/build/utils/mockCompatibility.test.js +88 -0
  88. package/build/utils/versions.d.ts +3 -3
  89. package/build/utils/versions.js +1 -1
  90. package/build/workspace/workspace.d.ts +35 -21
  91. package/build/workspace/workspace.js +8 -4
  92. package/build/workspace/workspace.test.js +65 -6
  93. package/package.json +2 -2
@@ -1,22 +1,41 @@
1
1
  // @ts-nocheck - Jest ESM type inference issues
2
2
  import { jest } from "@jest/globals";
3
- import { buildContainerEnv, rewriteLocalhostForDocker } from "./containerEnv.js";
3
+ import { buildContainerEnv, rewriteLocalhostForDocker, } from "./containerEnv.js";
4
4
  // Mock dockerode before importing TestExecutionService
5
5
  const mockRun = jest.fn();
6
6
  const mockListImages = jest.fn();
7
+ const mockListContainers = jest.fn();
8
+ const mockGetContainer = jest.fn();
7
9
  jest.unstable_mockModule("dockerode", () => ({
8
10
  default: jest.fn().mockImplementation(() => ({
9
11
  run: mockRun,
10
12
  listImages: mockListImages,
13
+ listContainers: mockListContainers,
14
+ getContainer: mockGetContainer,
11
15
  })),
12
16
  }));
17
+ const mockExec = jest.fn((_command, _options, callback) => {
18
+ callback(null);
19
+ return { pid: 123 };
20
+ });
21
+ jest.unstable_mockModule("child_process", () => ({
22
+ exec: mockExec,
23
+ }));
13
24
  // Get actual fs for spreading, then mock specific methods
14
25
  const actualFs = await import("fs");
15
26
  const mockAccessSync = jest.fn();
16
27
  const mockExistsSync = jest.fn().mockReturnValue(true);
17
- const mockReaddirSync = jest.fn().mockImplementation((_path, options) => {
28
+ const mockReaddirSync = jest
29
+ .fn()
30
+ .mockImplementation((_path, options) => {
18
31
  if (options?.withFileTypes) {
19
- return [{ name: "test_file.py", isFile: () => true, isDirectory: () => false }];
32
+ return [
33
+ {
34
+ name: "test_file.py",
35
+ isFile: () => true,
36
+ isDirectory: () => false,
37
+ },
38
+ ];
20
39
  }
21
40
  return ["test_file.py"];
22
41
  });
@@ -45,6 +64,10 @@ jest.unstable_mockModule("fs", () => ({
45
64
  mkdirSync: mockMkdirSync,
46
65
  rmSync: mockRmSync,
47
66
  }));
67
+ const mockReadWorkspaceConfigRaw = jest.fn();
68
+ jest.unstable_mockModule("../utils/workspaceAuth.js", () => ({
69
+ readWorkspaceConfigRaw: mockReadWorkspaceConfigRaw,
70
+ }));
48
71
  // Mock logger
49
72
  jest.unstable_mockModule("../utils/logger.js", () => ({
50
73
  logger: {
@@ -293,9 +316,12 @@ describe("TestExecutionService.executeTest - Docker env forwarding", () => {
293
316
  beforeEach(() => {
294
317
  jest.clearAllMocks();
295
318
  // Simulate image already cached
296
- mockListImages.mockResolvedValue([
297
- { RepoTags: [EXECUTOR_DOCKER_IMAGE] },
298
- ]);
319
+ mockListImages.mockResolvedValue([{ RepoTags: [EXECUTOR_DOCKER_IMAGE] }]);
320
+ mockListContainers.mockResolvedValue([]);
321
+ mockGetContainer.mockReturnValue({
322
+ remove: jest.fn().mockResolvedValue(undefined),
323
+ });
324
+ mockReadWorkspaceConfigRaw.mockResolvedValue(null);
299
325
  });
300
326
  afterEach(() => {
301
327
  // Clean up env vars we set during tests
@@ -318,6 +344,21 @@ describe("TestExecutionService.executeTest - Docker env forwarding", () => {
318
344
  const dockerOptions = mockRun.mock.calls[0][3];
319
345
  expect(dockerOptions.Env).toContain("SKYRAMP_TEST_BASE_URL=http://external-host:8000");
320
346
  });
347
+ it("labels executor containers as owned by this tool", async () => {
348
+ const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
349
+ mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
350
+ const service = new TestExecutionService();
351
+ await service.executeTest({
352
+ testFile: "/workspace/test_file.py",
353
+ workspacePath: "/workspace",
354
+ language: "python",
355
+ testType: "smoke",
356
+ });
357
+ const dockerOptions = mockRun.mock.calls[0][3];
358
+ expect(dockerOptions.Labels).toEqual({
359
+ "dev.skyramp.mcp.execution": "true",
360
+ });
361
+ });
321
362
  it("passes multiple SKYRAMP_TEST_SERVICE_URL_* to Docker container Env", async () => {
322
363
  process.env.SKYRAMP_TEST_SERVICE_URL_BACKEND = "http://host1:8000";
323
364
  process.env.SKYRAMP_TEST_SERVICE_URL_FRONTEND = "http://host2:5173";
@@ -349,6 +390,52 @@ describe("TestExecutionService.executeTest - Docker env forwarding", () => {
349
390
  e.startsWith("SKYRAMP_TEST_SERVICE_URL_"));
350
391
  expect(envWithBaseUrl).toHaveLength(0);
351
392
  });
393
+ it("removes only stopped execution containers owned by this tool", async () => {
394
+ const removeOwned = jest.fn().mockResolvedValue(undefined);
395
+ const removeUnowned = jest.fn().mockResolvedValue(undefined);
396
+ mockListContainers.mockResolvedValue([
397
+ {
398
+ Id: "owned-stopped-1",
399
+ Image: EXECUTOR_DOCKER_IMAGE,
400
+ Names: ["/skyramp_executor_1"],
401
+ State: "exited",
402
+ Labels: { "dev.skyramp.mcp.execution": "true" },
403
+ },
404
+ {
405
+ Id: "owned-running-1",
406
+ Image: EXECUTOR_DOCKER_IMAGE,
407
+ Names: ["/skyramp_executor_2"],
408
+ State: "running",
409
+ Labels: { "dev.skyramp.mcp.execution": "true" },
410
+ },
411
+ {
412
+ Id: "unowned-worker-1",
413
+ Image: "skyramp/worker:local-mockfix",
414
+ Names: ["/skyramp_worker"],
415
+ State: "exited",
416
+ },
417
+ { Id: "postgres-1", Image: "postgres:15-alpine", Names: ["/db"] },
418
+ ]);
419
+ mockGetContainer.mockImplementation((id) => ({
420
+ remove: id === "owned-stopped-1" ? removeOwned : removeUnowned,
421
+ }));
422
+ const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
423
+ mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
424
+ const service = new TestExecutionService();
425
+ await service.executeTest({
426
+ testFile: "/workspace/test_file.py",
427
+ workspacePath: "/workspace",
428
+ language: "python",
429
+ testType: "smoke",
430
+ });
431
+ expect(mockGetContainer).toHaveBeenCalledWith("owned-stopped-1");
432
+ expect(mockGetContainer).not.toHaveBeenCalledWith("owned-running-1");
433
+ expect(mockGetContainer).not.toHaveBeenCalledWith("unowned-worker-1");
434
+ expect(mockGetContainer).not.toHaveBeenCalledWith("postgres-1");
435
+ expect(removeOwned).toHaveBeenCalledWith();
436
+ expect(removeUnowned).not.toHaveBeenCalled();
437
+ expect(mockRun).toHaveBeenCalledTimes(1);
438
+ });
352
439
  // Approach B: every workspace mount is mirrored at the host-absolute path so
353
440
  // tests that embed absolute references (storageState, fixtures, snapshots)
354
441
  // resolve correctly inside the executor regardless of which path-shape the
@@ -357,8 +444,16 @@ describe("TestExecutionService.executeTest - Docker env forwarding", () => {
357
444
  mockReaddirSync.mockImplementation((_path, options) => {
358
445
  if (options?.withFileTypes) {
359
446
  return [
360
- { name: "xdr_dashboard.spec.ts", isFile: () => true, isDirectory: () => false },
361
- { name: "skyramp_session_storage.json", isFile: () => true, isDirectory: () => false },
447
+ {
448
+ name: "xdr_dashboard.spec.ts",
449
+ isFile: () => true,
450
+ isDirectory: () => false,
451
+ },
452
+ {
453
+ name: "skyramp_session_storage.json",
454
+ isFile: () => true,
455
+ isDirectory: () => false,
456
+ },
362
457
  ];
363
458
  }
364
459
  return ["xdr_dashboard.spec.ts", "skyramp_session_storage.json"];
@@ -384,7 +479,13 @@ describe("TestExecutionService.executeTest - Docker env forwarding", () => {
384
479
  it("does not double-mount when workspacePath equals /home/user", async () => {
385
480
  mockReaddirSync.mockImplementation((_path, options) => {
386
481
  if (options?.withFileTypes) {
387
- return [{ name: "test_file.py", isFile: () => true, isDirectory: () => false }];
482
+ return [
483
+ {
484
+ name: "test_file.py",
485
+ isFile: () => true,
486
+ isDirectory: () => false,
487
+ },
488
+ ];
388
489
  }
389
490
  return ["test_file.py"];
390
491
  });
@@ -408,6 +509,304 @@ describe("TestExecutionService.executeTest - Docker env forwarding", () => {
408
509
  }
409
510
  });
410
511
  });
512
+ describe("TestExecutionService.executeBatch", () => {
513
+ let TestExecutionService;
514
+ beforeAll(async () => {
515
+ const mod = await import("./TestExecutionService.js");
516
+ TestExecutionService = mod.TestExecutionService;
517
+ });
518
+ it("executes tests serially", async () => {
519
+ const service = new TestExecutionService();
520
+ const order = [];
521
+ jest.spyOn(service, "executeTest").mockImplementation(async (options) => {
522
+ order.push(`start:${options.testFile}`);
523
+ await Promise.resolve();
524
+ order.push(`end:${options.testFile}`);
525
+ return {
526
+ testFile: options.testFile,
527
+ status: "pass",
528
+ executedAt: new Date().toISOString(),
529
+ duration: 1,
530
+ errors: [],
531
+ warnings: [],
532
+ };
533
+ });
534
+ await service.executeBatch([
535
+ { testFile: "one.py", workspacePath: "/workspace", language: "python", testType: "smoke" },
536
+ { testFile: "two.py", workspacePath: "/workspace", language: "python", testType: "smoke" },
537
+ ]);
538
+ expect(order).toEqual(["start:one.py", "end:one.py", "start:two.py", "end:two.py"]);
539
+ });
540
+ });
541
+ describe("TestExecutionService.executeTest - SUT preparation", () => {
542
+ let TestExecutionService;
543
+ let EXECUTOR_DOCKER_IMAGE;
544
+ const originalFetch = global.fetch;
545
+ beforeAll(async () => {
546
+ const mod = await import("./TestExecutionService.js");
547
+ TestExecutionService = mod.TestExecutionService;
548
+ EXECUTOR_DOCKER_IMAGE = mod.EXECUTOR_DOCKER_IMAGE;
549
+ });
550
+ beforeEach(() => {
551
+ jest.clearAllMocks();
552
+ global.fetch = originalFetch;
553
+ mockListImages.mockResolvedValue([{ RepoTags: [EXECUTOR_DOCKER_IMAGE] }]);
554
+ mockListContainers.mockResolvedValue([]);
555
+ mockGetContainer.mockReturnValue({
556
+ remove: jest.fn().mockResolvedValue(undefined),
557
+ });
558
+ mockReadWorkspaceConfigRaw.mockResolvedValue({
559
+ services: [
560
+ {
561
+ testDirectory: "tests",
562
+ language: "python",
563
+ api: { baseUrl: "" },
564
+ runtimeDetails: {
565
+ runtime: "docker",
566
+ serverStartCommand: "docker compose up -d app",
567
+ },
568
+ },
569
+ ],
570
+ });
571
+ });
572
+ afterEach(() => {
573
+ jest.useRealTimers();
574
+ global.fetch = originalFetch;
575
+ });
576
+ it("runs serverStartCommand before non-contract test execution", async () => {
577
+ mockExec.mockImplementationOnce((_command, _options, callback) => {
578
+ callback(new Error("exit 13"));
579
+ return { pid: 123 };
580
+ });
581
+ const service = new TestExecutionService();
582
+ await expect(service.executeTest({
583
+ testFile: "/workspace/tests/test_file.py",
584
+ workspacePath: "/workspace",
585
+ language: "python",
586
+ testType: "smoke",
587
+ })).rejects.toThrow("SUT setup command failed before test execution");
588
+ expect(mockExec.mock.calls.map(([command]) => command)).toEqual([
589
+ "docker compose up -d app",
590
+ ]);
591
+ expect(mockRun).not.toHaveBeenCalled();
592
+ });
593
+ it("rejects non-Docker serverStartCommand for docker runtime services", async () => {
594
+ mockReadWorkspaceConfigRaw.mockResolvedValue({
595
+ services: [
596
+ {
597
+ testDirectory: "tests",
598
+ language: "python",
599
+ api: { baseUrl: "" },
600
+ runtimeDetails: {
601
+ runtime: "docker",
602
+ serverStartCommand: "node server.js",
603
+ },
604
+ },
605
+ ],
606
+ });
607
+ const service = new TestExecutionService();
608
+ await expect(service.executeTest({
609
+ testFile: "/workspace/tests/test_file.py",
610
+ workspacePath: "/workspace",
611
+ language: "python",
612
+ testType: "smoke",
613
+ })).rejects.toThrow("serverStartCommand must be a Docker command");
614
+ expect(mockExec).not.toHaveBeenCalled();
615
+ expect(mockRun).not.toHaveBeenCalled();
616
+ });
617
+ it("rejects newline-separated shell commands for docker runtime services", async () => {
618
+ mockReadWorkspaceConfigRaw.mockResolvedValue({
619
+ services: [
620
+ {
621
+ testDirectory: "tests",
622
+ language: "python",
623
+ api: { baseUrl: "" },
624
+ runtimeDetails: {
625
+ runtime: "docker",
626
+ serverStartCommand: "docker compose up -d app\nnode server.js",
627
+ },
628
+ },
629
+ ],
630
+ });
631
+ const service = new TestExecutionService();
632
+ await expect(service.executeTest({
633
+ testFile: "/workspace/tests/test_file.py",
634
+ workspacePath: "/workspace",
635
+ language: "python",
636
+ testType: "smoke",
637
+ })).rejects.toThrow("serverStartCommand must be a Docker command");
638
+ expect(mockExec).not.toHaveBeenCalled();
639
+ expect(mockRun).not.toHaveBeenCalled();
640
+ });
641
+ it("probes the service base URL before running the test", async () => {
642
+ const mockFetch = jest.fn().mockResolvedValue({ ok: true });
643
+ global.fetch = mockFetch;
644
+ mockReadWorkspaceConfigRaw.mockResolvedValue({
645
+ services: [
646
+ {
647
+ testDirectory: "tests",
648
+ language: "python",
649
+ api: { baseUrl: "http://127.0.0.1:8080/health" },
650
+ runtimeDetails: {
651
+ runtime: "docker",
652
+ },
653
+ },
654
+ ],
655
+ });
656
+ const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
657
+ mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
658
+ const service = new TestExecutionService();
659
+ await service.executeTest({
660
+ testFile: "/workspace/tests/test_file.py",
661
+ workspacePath: "/workspace",
662
+ language: "python",
663
+ testType: "smoke",
664
+ });
665
+ expect(mockFetch).toHaveBeenCalledWith("http://127.0.0.1:8080/health", expect.objectContaining({ method: "GET" }));
666
+ expect(mockRun).toHaveBeenCalledTimes(1);
667
+ });
668
+ it("runs serverStopCommand after test execution when configured", async () => {
669
+ mockReadWorkspaceConfigRaw.mockResolvedValue({
670
+ services: [
671
+ {
672
+ testDirectory: "tests",
673
+ language: "python",
674
+ api: { baseUrl: "" },
675
+ runtimeDetails: {
676
+ runtime: "docker",
677
+ serverStartCommand: "docker compose up -d app",
678
+ serverStopCommand: "docker compose stop app",
679
+ },
680
+ },
681
+ ],
682
+ });
683
+ const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
684
+ mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
685
+ const service = new TestExecutionService();
686
+ await service.executeTest({
687
+ testFile: "/workspace/tests/test_file.py",
688
+ workspacePath: "/workspace",
689
+ language: "python",
690
+ testType: "smoke",
691
+ });
692
+ expect(mockExec.mock.calls.map(([command]) => command)).toEqual([
693
+ "docker compose up -d app",
694
+ "docker compose stop app",
695
+ ]);
696
+ expect(mockRun).toHaveBeenCalledTimes(1);
697
+ });
698
+ it("treats serverStopCommand failure as non-fatal after test execution", async () => {
699
+ mockReadWorkspaceConfigRaw.mockResolvedValue({
700
+ services: [
701
+ {
702
+ testDirectory: "tests",
703
+ language: "python",
704
+ api: { baseUrl: "" },
705
+ runtimeDetails: {
706
+ runtime: "docker",
707
+ serverStartCommand: "docker compose up -d app",
708
+ serverStopCommand: "docker compose stop app",
709
+ },
710
+ },
711
+ ],
712
+ });
713
+ mockExec
714
+ .mockImplementationOnce((_command, _options, callback) => {
715
+ callback(null);
716
+ return { pid: 123 };
717
+ })
718
+ .mockImplementationOnce((_command, _options, callback) => {
719
+ callback(new Error("stop failed"));
720
+ return { pid: 123 };
721
+ });
722
+ const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
723
+ mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
724
+ const service = new TestExecutionService();
725
+ await service.executeTest({
726
+ testFile: "/workspace/tests/test_file.py",
727
+ workspacePath: "/workspace",
728
+ language: "python",
729
+ testType: "smoke",
730
+ });
731
+ expect(mockExec.mock.calls.map(([command]) => command)).toEqual([
732
+ "docker compose up -d app",
733
+ "docker compose stop app",
734
+ ]);
735
+ expect(mockRun).toHaveBeenCalledTimes(1);
736
+ });
737
+ it("does not run serverStopCommand when serverStartCommand is absent", async () => {
738
+ mockReadWorkspaceConfigRaw.mockResolvedValue({
739
+ services: [
740
+ {
741
+ testDirectory: "tests",
742
+ language: "python",
743
+ api: { baseUrl: "" },
744
+ runtimeDetails: {
745
+ runtime: "docker",
746
+ serverStopCommand: "docker compose stop app",
747
+ },
748
+ },
749
+ ],
750
+ });
751
+ const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
752
+ mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
753
+ const service = new TestExecutionService();
754
+ await service.executeTest({
755
+ testFile: "/workspace/tests/test_file.py",
756
+ workspacePath: "/workspace",
757
+ language: "python",
758
+ testType: "smoke",
759
+ });
760
+ expect(mockExec).not.toHaveBeenCalled();
761
+ expect(mockRun).toHaveBeenCalledTimes(1);
762
+ });
763
+ it("runs serverStopCommand when readiness probing times out", async () => {
764
+ jest.useFakeTimers();
765
+ const mockFetch = jest.fn().mockRejectedValue(new Error("not ready"));
766
+ global.fetch = mockFetch;
767
+ mockReadWorkspaceConfigRaw.mockResolvedValue({
768
+ services: [
769
+ {
770
+ testDirectory: "tests",
771
+ language: "python",
772
+ api: { baseUrl: "http://127.0.0.1:8080/health" },
773
+ runtimeDetails: {
774
+ runtime: "docker",
775
+ serverStartCommand: "docker compose up -d app",
776
+ serverStopCommand: "docker compose stop app",
777
+ },
778
+ },
779
+ ],
780
+ });
781
+ const service = new TestExecutionService();
782
+ const resultPromise = expect(service.executeTest({
783
+ testFile: "/workspace/tests/test_file.py",
784
+ workspacePath: "/workspace",
785
+ language: "python",
786
+ testType: "smoke",
787
+ })).rejects.toThrow("SUT did not become reachable");
788
+ await jest.advanceTimersByTimeAsync(16000);
789
+ await resultPromise;
790
+ expect(mockExec.mock.calls.map(([command]) => command)).toEqual([
791
+ "docker compose up -d app",
792
+ "docker compose stop app",
793
+ ]);
794
+ expect(mockRun).not.toHaveBeenCalled();
795
+ });
796
+ it("skips serverStartCommand for contract test execution", async () => {
797
+ const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
798
+ mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
799
+ const service = new TestExecutionService();
800
+ await service.executeTest({
801
+ testFile: "/workspace/tests/test_file.py",
802
+ workspacePath: "/workspace",
803
+ language: "python",
804
+ testType: "contract",
805
+ });
806
+ expect(mockExec).not.toHaveBeenCalled();
807
+ expect(mockRun).toHaveBeenCalledTimes(1);
808
+ });
809
+ });
411
810
  describe("TestExecutionService.executeTest - Video capture for browser tests", () => {
412
811
  let TestExecutionService;
413
812
  let getVideoSubdir;
@@ -421,11 +820,20 @@ describe("TestExecutionService.executeTest - Video capture for browser tests", (
421
820
  beforeEach(() => {
422
821
  jest.clearAllMocks();
423
822
  mockListImages.mockResolvedValue([{ RepoTags: [EXECUTOR_DOCKER_IMAGE] }]);
823
+ mockListContainers.mockResolvedValue([]);
824
+ mockGetContainer.mockReturnValue({
825
+ remove: jest.fn().mockResolvedValue(undefined),
826
+ });
827
+ mockReadWorkspaceConfigRaw.mockResolvedValue(null);
424
828
  mockExistsSync.mockReturnValue(true);
425
829
  mockReaddirSync.mockImplementation((_path, options) => {
426
830
  if (options?.withFileTypes) {
427
831
  return [
428
- { name: "test.spec.ts", isFile: () => true, isDirectory: () => false },
832
+ {
833
+ name: "test.spec.ts",
834
+ isFile: () => true,
835
+ isDirectory: () => false,
836
+ },
429
837
  { name: "test.py", isFile: () => true, isDirectory: () => false },
430
838
  ];
431
839
  }
@@ -457,7 +865,8 @@ describe("TestExecutionService.executeTest - Video capture for browser tests", (
457
865
  mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
458
866
  let capturedConfigContent = "";
459
867
  mockWriteFileSync.mockImplementation((filePath, content) => {
460
- if (filePath.includes("skyramp-playwright-") && filePath.endsWith(".config.ts")) {
868
+ if (filePath.includes("skyramp-playwright-") &&
869
+ filePath.endsWith(".config.ts")) {
461
870
  capturedConfigContent = content;
462
871
  }
463
872
  });
@@ -521,7 +930,10 @@ describe("TestExecutionService.executeTest - Video capture for browser tests", (
521
930
  });
522
931
  const dockerOptions = mockRun.mock.calls[0][3];
523
932
  const videoSubdir = getVideoSubdir("/workspace/tests/login.spec.ts");
524
- const videoMount = dockerOptions.HostConfig.Mounts.find((m) => m.Target === `/home/user/.skyramp/videos/${videoSubdir}`);
933
+ // Each execution gets a unique per-run dir: `${videoSubdir}-<timestamp>`,
934
+ // so match by prefix rather than exact equality.
935
+ const videoMountPrefix = `/home/user/.skyramp/videos/${videoSubdir}`;
936
+ const videoMount = dockerOptions.HostConfig.Mounts.find((m) => m.Target.startsWith(videoMountPrefix));
525
937
  expect(videoMount).toBeDefined();
526
938
  expect(videoMount.Type).toBe("bind");
527
939
  expect(videoMount.ReadOnly).toBe(false);
@@ -547,7 +959,8 @@ describe("TestExecutionService.executeTest - Video capture for browser tests", (
547
959
  mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
548
960
  let usedGeneratedConfig = false;
549
961
  mockWriteFileSync.mockImplementation((filePath, content) => {
550
- if (filePath.includes("skyramp-playwright-") && content.includes('video: "on"')) {
962
+ if (filePath.includes("skyramp-playwright-") &&
963
+ content.includes('video: "on"')) {
551
964
  usedGeneratedConfig = true;
552
965
  }
553
966
  });
@@ -569,13 +982,25 @@ describe("TestExecutionService.executeTest - Video capture for browser tests", (
569
982
  // Mock video directory scan
570
983
  if (dir.includes(".skyramp/videos") && dir.includes(videoSubdir)) {
571
984
  if (options?.withFileTypes) {
572
- return [{ name: "video.webm", isFile: () => true, isDirectory: () => false }];
985
+ return [
986
+ {
987
+ name: "video.webm",
988
+ isFile: () => true,
989
+ isDirectory: () => false,
990
+ },
991
+ ];
573
992
  }
574
993
  return ["video.webm"];
575
994
  }
576
995
  // Default workspace files
577
996
  if (options?.withFileTypes) {
578
- return [{ name: "test.spec.ts", isFile: () => true, isDirectory: () => false }];
997
+ return [
998
+ {
999
+ name: "test.spec.ts",
1000
+ isFile: () => true,
1001
+ isDirectory: () => false,
1002
+ },
1003
+ ];
579
1004
  }
580
1005
  return ["test.spec.ts"];
581
1006
  });
@@ -604,7 +1029,13 @@ describe("TestExecutionService.executeTest - Video capture for browser tests", (
604
1029
  }
605
1030
  // Default workspace files
606
1031
  if (options?.withFileTypes) {
607
- return [{ name: "test.spec.ts", isFile: () => true, isDirectory: () => false }];
1032
+ return [
1033
+ {
1034
+ name: "test.spec.ts",
1035
+ isFile: () => true,
1036
+ isDirectory: () => false,
1037
+ },
1038
+ ];
608
1039
  }
609
1040
  return ["test.spec.ts"];
610
1041
  });
@@ -1,6 +1,6 @@
1
1
  import { SkyrampClient } from "@skyramp/skyramp";
2
2
  import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3
- import { TestType } from "../types/TestTypes.js";
3
+ import { TestType, MOCK_TYPE } from "../types/TestTypes.js";
4
4
  export interface BaseTestParams {
5
5
  endpointURL?: string;
6
6
  method?: string;
@@ -42,7 +42,7 @@ export declare abstract class TestGenerationService {
42
42
  generateTest(params: BaseTestParams & Record<string, any>): Promise<CallToolResult>;
43
43
  protected validateInputs(params: BaseTestParams): CallToolResult;
44
44
  protected abstract buildGenerationOptions(params: BaseTestParams & Record<string, any>): any;
45
- protected abstract getTestType(): TestType;
45
+ protected abstract getTestType(): TestType | typeof MOCK_TYPE;
46
46
  protected handleApiAnalysis(params: BaseTestParams): Promise<CallToolResult | null>;
47
47
  private static readonly STANDARD_HEADERS;
48
48
  private static simpleWildcardMatch;
@@ -10,6 +10,9 @@ export const TOOL_PHASE_MAP = {
10
10
  skyramp_ui_test_generation: "generating",
11
11
  skyramp_batch_scenario_test_generation: "generating",
12
12
  skyramp_mock_generation: "generating",
13
+ skyramp_batch_mock_generation: "generating",
14
+ skyramp_generate_enriched_integration_test: "generating",
15
+ skyramp_enrich_test_with_mocks: "generating",
13
16
  skyramp_execute_test: { before: "maintaining", after: "executing" },
14
17
  skyramp_analyze_test_health: "maintaining",
15
18
  skyramp_submit_report: "reporting",
@@ -36,4 +39,5 @@ export const TOOLS_WITHOUT_PHASE = new Set([
36
39
  "skyramp_modularization",
37
40
  "skyramp_reuse_code",
38
41
  "skyramp_enhance_assertions",
42
+ "skyramp_preflight_mock_check",
39
43
  ]);
@@ -24,6 +24,12 @@ const enhanceAssertionsSchema = {
24
24
  .describe("The context of the enhancement. " +
25
25
  "Use 'generation' after generating a new test file (applies to every test function). " +
26
26
  "Use 'maintenance' during drift UPDATE (applies only to new or diff-affected functions)."),
27
+ autoApply: z
28
+ .boolean()
29
+ .default(false)
30
+ .describe("When true, returns a compact instruction set (10 lines) that tells the agent to " +
31
+ "read the test file, enhance assertions per standard rules, and write it back directly. " +
32
+ "When false (default), returns the full verbose instruction set."),
27
33
  };
28
34
  export function registerEnhanceAssertionsTool(server) {
29
35
  server.registerTool(TOOL_NAME, {
@@ -34,9 +40,35 @@ export function registerEnhanceAssertionsTool(server) {
34
40
  - After updating an existing supported test file during maintenance`,
35
41
  inputSchema: enhanceAssertionsSchema,
36
42
  }, async (params) => {
37
- const { testFile, testType, enhanceType } = params;
43
+ const { testFile, testType, enhanceType, autoApply } = params;
38
44
  // Stage so testbot includes the generated files in its output commit.
39
45
  await stageGeneratedPaths(testFile);
46
+ if (autoApply) {
47
+ const compactInstructions = [
48
+ `Enhance response body assertions in: \`${testFile}\``,
49
+ `Test type: ${testType} | Context: ${enhanceType}`,
50
+ ``,
51
+ `Read the file, add assertions after each send_request/sendRequest status-code assertion, write it back.`,
52
+ `Use SDK helper: Python \`skyramp.get_response_value(response, "json.path")\`, JS \`getValue(response, "json.path")\`.`,
53
+ ``,
54
+ `Rules (apply all that fit):`,
55
+ `1. Echo-back: assert exact request values returned unchanged (not null-checks)`,
56
+ `2. Error responses: assert every body field (error code, message, detail)`,
57
+ `3. Server-generated: assert format (UUID regex, acc_ prefix, ISO timestamp)`,
58
+ `4. Known values: assert exact enum/status for this flow`,
59
+ `5. Arrays: assert length, item fields, next-index undefined`,
60
+ `6. Computed: assert with formula if derivable from request/response`,
61
+ `7. Chained: use prior response IDs, never hardcode`,
62
+ ``,
63
+ `Do NOT restructure, reformat, add comments, or change imports. Only add assertion lines.`,
64
+ ].join("\n");
65
+ const result = {
66
+ content: [{ type: "text", text: compactInstructions }],
67
+ isError: false,
68
+ };
69
+ AnalyticsService.pushMCPToolEvent(TOOL_NAME, undefined, { testFile: params.testFile, testType: params.testType, enhanceType: params.enhanceType, autoApply: String(params.autoApply) }).catch(() => { });
70
+ return result;
71
+ }
40
72
  const enhanceCtx = enhanceType;
41
73
  let instructions;
42
74
  if (testType === TestType.UI) {
@@ -63,7 +95,7 @@ export function registerEnhanceAssertionsTool(server) {
63
95
  ],
64
96
  isError: false,
65
97
  };
66
- AnalyticsService.pushMCPToolEvent(TOOL_NAME, undefined, params).catch(() => { });
98
+ AnalyticsService.pushMCPToolEvent(TOOL_NAME, undefined, { testFile: params.testFile, testType: params.testType, enhanceType: params.enhanceType, autoApply: String(params.autoApply) }).catch(() => { });
67
99
  return result;
68
100
  });
69
101
  }