@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.
- package/build/commands/commandLibrary.d.ts +1 -0
- package/build/commands/commandLibrary.js +18 -17
- package/build/commands/localDevTestChangesCommand.d.ts +15 -0
- package/build/commands/localDevTestChangesCommand.js +175 -0
- package/build/index.js +74 -13
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +1 -0
- package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +7 -6
- package/build/prompts/local-dev/local-dev-plan.d.ts +25 -0
- package/build/prompts/local-dev/local-dev-plan.js +414 -0
- package/build/prompts/local-dev/local-dev-prompts.d.ts +4 -0
- package/build/prompts/local-dev/local-dev-prompts.js +152 -0
- package/build/prompts/prompt-utils.d.ts +8 -0
- package/build/prompts/prompt-utils.js +33 -0
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +3 -1
- package/build/prompts/sut-setup/modes/dockerComposePrompt.js +3 -1
- package/build/prompts/sut-setup/shared.d.ts +20 -0
- package/build/prompts/sut-setup/shared.js +69 -7
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +21 -29
- package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +79 -16
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +41 -4
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +34 -1
- package/build/prompts/testbot/testbot-prompts.d.ts +0 -5
- package/build/prompts/testbot/testbot-prompts.js +4 -34
- package/build/resources/testbotResource.js +2 -1
- package/build/services/TestExecutionService.d.ts +10 -2
- package/build/services/TestExecutionService.js +220 -28
- package/build/services/TestExecutionService.test.js +447 -16
- package/build/services/TestGenerationService.d.ts +2 -2
- package/build/tool-phases.js +4 -0
- package/build/tools/code-refactor/enhanceAssertionsTool.js +34 -2
- package/build/tools/enrichTestWithMocksTool.d.ts +15 -0
- package/build/tools/enrichTestWithMocksTool.js +414 -0
- package/build/tools/enrichTestWithMocksTool.test.d.ts +1 -0
- package/build/tools/enrichTestWithMocksTool.test.js +397 -0
- package/build/tools/executeSkyrampTestTool.d.ts +3 -0
- package/build/tools/executeSkyrampTestTool.js +73 -18
- package/build/tools/executeSkyrampTestTool.test.d.ts +1 -0
- package/build/tools/executeSkyrampTestTool.test.js +111 -0
- package/build/tools/generate-tests/batchMockGenerationTool.d.ts +72 -0
- package/build/tools/generate-tests/batchMockGenerationTool.js +382 -0
- package/build/tools/generate-tests/batchMockGenerationTool.test.d.ts +1 -0
- package/build/tools/generate-tests/batchMockGenerationTool.test.js +514 -0
- package/build/tools/generate-tests/generateContractRestTool.js +2 -2
- package/build/tools/generate-tests/generateMockRestTool.d.ts +56 -6
- package/build/tools/generate-tests/generateMockRestTool.js +193 -21
- package/build/tools/generate-tests/generateMockRestTool.test.d.ts +1 -0
- package/build/tools/generate-tests/generateMockRestTool.test.js +340 -0
- package/build/tools/generateEnrichedIntegrationTestTool.d.ts +24 -0
- package/build/tools/generateEnrichedIntegrationTestTool.js +209 -0
- package/build/tools/generateEnrichedIntegrationTestTool.test.d.ts +1 -0
- package/build/tools/generateEnrichedIntegrationTestTool.test.js +199 -0
- package/build/tools/one-click/oneClickTool.d.ts +10 -0
- package/build/tools/one-click/oneClickTool.js +182 -40
- package/build/tools/one-click/oneClickTool.test.d.ts +1 -0
- package/build/tools/one-click/oneClickTool.test.js +204 -0
- package/build/tools/preflightMockCheckTool.d.ts +2 -0
- package/build/tools/preflightMockCheckTool.js +91 -0
- package/build/tools/test-management/analyzeChangesTool.d.ts +2 -1
- package/build/tools/test-management/analyzeChangesTool.js +59 -38
- package/build/tools/workspace/initializeWorkspaceTool.js +2 -9
- package/build/tools/workspace/initializeWorkspaceTool.test.js +9 -4
- package/build/types/OneClickCommands.d.ts +1 -1
- package/build/types/RepositoryAnalysis.d.ts +117 -0
- package/build/types/RepositoryAnalysis.js +16 -2
- package/build/types/TestTypes.d.ts +11 -2
- package/build/types/TestTypes.js +11 -1
- package/build/utils/analyze-openapi.js +18 -1
- package/build/utils/analyze-openapi.test.d.ts +1 -0
- package/build/utils/analyze-openapi.test.js +19 -0
- package/build/utils/branchDiff.d.ts +17 -1
- package/build/utils/branchDiff.js +96 -14
- package/build/utils/branchDiff.test.d.ts +1 -0
- package/build/utils/branchDiff.test.js +107 -0
- package/build/utils/docker.test.js +1 -1
- package/build/utils/featureFlags.d.ts +15 -1
- package/build/utils/featureFlags.js +19 -2
- package/build/utils/featureFlags.test.js +57 -2
- package/build/utils/grpcMockValidation.d.ts +1 -0
- package/build/utils/grpcMockValidation.js +49 -0
- package/build/utils/grpcMockValidation.test.d.ts +1 -0
- package/build/utils/grpcMockValidation.test.js +41 -0
- package/build/utils/httpMethodValidation.d.ts +4 -0
- package/build/utils/httpMethodValidation.js +13 -0
- package/build/utils/mockCompatibility.d.ts +49 -0
- package/build/utils/mockCompatibility.js +80 -0
- package/build/utils/mockCompatibility.test.d.ts +1 -0
- package/build/utils/mockCompatibility.test.js +88 -0
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/workspace/workspace.d.ts +35 -21
- package/build/workspace/workspace.js +8 -4
- package/build/workspace/workspace.test.js +65 -6
- 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
|
|
28
|
+
const mockReaddirSync = jest
|
|
29
|
+
.fn()
|
|
30
|
+
.mockImplementation((_path, options) => {
|
|
18
31
|
if (options?.withFileTypes) {
|
|
19
|
-
return [
|
|
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
|
-
|
|
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
|
-
{
|
|
361
|
-
|
|
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 [
|
|
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
|
-
{
|
|
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-") &&
|
|
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
|
-
|
|
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-") &&
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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;
|
package/build/tool-phases.js
CHANGED
|
@@ -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
|
}
|