@kirrosh/apitool 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +27 -0
- package/.github/workflows/release.yml +97 -0
- package/.mcp.json +9 -0
- package/APITOOL.md +195 -0
- package/BACKLOG.md +62 -0
- package/CHANGELOG.md +88 -0
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/bun.lock +291 -0
- package/docs/GLOSSARY.md +182 -0
- package/docs/INDEX.md +21 -0
- package/docs/agent.md +135 -0
- package/docs/archive/APITOOL-pre-M22.md +831 -0
- package/docs/archive/BACKLOG-AI-NATIVE.md +56 -0
- package/docs/archive/M1-M2-parser-runner.md +216 -0
- package/docs/archive/M4-M7-reporter-cli.md +179 -0
- package/docs/archive/M5-M7-storage-junit.md +300 -0
- package/docs/archive/M6-webui.md +339 -0
- package/docs/ci.md +274 -0
- package/docs/generation-issues.md +67 -0
- package/generated/.env.yaml +3 -0
- package/install.ps1 +80 -0
- package/install.sh +113 -0
- package/package.json +46 -0
- package/scripts/run-mocked-tests.ts +45 -0
- package/seed-demo.ts +53 -0
- package/self-tests/auth.yaml +18 -0
- package/self-tests/collections-crud.yaml +46 -0
- package/self-tests/environments-crud.yaml +48 -0
- package/self-tests/export.yaml +32 -0
- package/self-tests/runs.yaml +16 -0
- package/src/bun-types.d.ts +5 -0
- package/src/cli/commands/add-api.ts +51 -0
- package/src/cli/commands/ai-generate.ts +106 -0
- package/src/cli/commands/chat.ts +43 -0
- package/src/cli/commands/ci-init.ts +126 -0
- package/src/cli/commands/collections.ts +41 -0
- package/src/cli/commands/coverage.ts +65 -0
- package/src/cli/commands/doctor.ts +127 -0
- package/src/cli/commands/envs.ts +218 -0
- package/src/cli/commands/init.ts +84 -0
- package/src/cli/commands/mcp.ts +16 -0
- package/src/cli/commands/run.ts +137 -0
- package/src/cli/commands/runs.ts +108 -0
- package/src/cli/commands/serve.ts +22 -0
- package/src/cli/commands/update.ts +142 -0
- package/src/cli/commands/validate.ts +18 -0
- package/src/cli/index.ts +500 -0
- package/src/cli/output.ts +24 -0
- package/src/cli/runtime.ts +7 -0
- package/src/core/agent/agent-loop.ts +116 -0
- package/src/core/agent/context-manager.ts +41 -0
- package/src/core/agent/system-prompt.ts +33 -0
- package/src/core/agent/tools/diagnose-failure.ts +51 -0
- package/src/core/agent/tools/explore-api.ts +40 -0
- package/src/core/agent/tools/index.ts +48 -0
- package/src/core/agent/tools/manage-environment.ts +40 -0
- package/src/core/agent/tools/query-results.ts +40 -0
- package/src/core/agent/tools/run-tests.ts +38 -0
- package/src/core/agent/tools/send-request.ts +44 -0
- package/src/core/agent/tools/validate-tests.ts +23 -0
- package/src/core/agent/types.ts +22 -0
- package/src/core/generator/ai/ai-generator.ts +61 -0
- package/src/core/generator/ai/llm-client.ts +159 -0
- package/src/core/generator/ai/output-parser.ts +307 -0
- package/src/core/generator/ai/prompt-builder.ts +153 -0
- package/src/core/generator/ai/types.ts +56 -0
- package/src/core/generator/coverage-scanner.ts +87 -0
- package/src/core/generator/data-factory.ts +115 -0
- package/src/core/generator/index.ts +10 -0
- package/src/core/generator/openapi-reader.ts +142 -0
- package/src/core/generator/schema-utils.ts +52 -0
- package/src/core/generator/serializer.ts +189 -0
- package/src/core/generator/types.ts +47 -0
- package/src/core/parser/filter.ts +14 -0
- package/src/core/parser/index.ts +21 -0
- package/src/core/parser/schema.ts +175 -0
- package/src/core/parser/types.ts +50 -0
- package/src/core/parser/variables.ts +146 -0
- package/src/core/parser/yaml-parser.ts +85 -0
- package/src/core/reporter/console.ts +175 -0
- package/src/core/reporter/index.ts +23 -0
- package/src/core/reporter/json.ts +9 -0
- package/src/core/reporter/junit.ts +78 -0
- package/src/core/reporter/types.ts +12 -0
- package/src/core/runner/assertions.ts +172 -0
- package/src/core/runner/execute-run.ts +75 -0
- package/src/core/runner/executor.ts +150 -0
- package/src/core/runner/http-client.ts +69 -0
- package/src/core/runner/index.ts +12 -0
- package/src/core/runner/types.ts +48 -0
- package/src/core/setup-api.ts +97 -0
- package/src/core/utils.ts +9 -0
- package/src/db/queries.ts +868 -0
- package/src/db/schema.ts +215 -0
- package/src/mcp/server.ts +47 -0
- package/src/mcp/tools/ci-init.ts +57 -0
- package/src/mcp/tools/coverage-analysis.ts +58 -0
- package/src/mcp/tools/explore-api.ts +84 -0
- package/src/mcp/tools/generate-missing-tests.ts +80 -0
- package/src/mcp/tools/generate-tests-guide.ts +353 -0
- package/src/mcp/tools/manage-environment.ts +123 -0
- package/src/mcp/tools/manage-server.ts +87 -0
- package/src/mcp/tools/query-db.ts +141 -0
- package/src/mcp/tools/run-tests.ts +66 -0
- package/src/mcp/tools/save-test-suite.ts +164 -0
- package/src/mcp/tools/send-request.ts +53 -0
- package/src/mcp/tools/setup-api.ts +49 -0
- package/src/mcp/tools/validate-tests.ts +42 -0
- package/src/tui/chat-ui.ts +150 -0
- package/src/web/routes/api.ts +234 -0
- package/src/web/routes/dashboard.ts +348 -0
- package/src/web/routes/runs.ts +64 -0
- package/src/web/schemas.ts +121 -0
- package/src/web/server.ts +134 -0
- package/src/web/static/htmx.min.js +1 -0
- package/src/web/static/style.css +265 -0
- package/src/web/views/layout.ts +46 -0
- package/src/web/views/results.ts +209 -0
- package/tests/agent/agent-loop.test.ts +61 -0
- package/tests/agent/context-manager.test.ts +59 -0
- package/tests/agent/system-prompt.test.ts +42 -0
- package/tests/agent/tools/diagnose-failure.test.ts +85 -0
- package/tests/agent/tools/explore-api.test.ts +59 -0
- package/tests/agent/tools/manage-environment.test.ts +78 -0
- package/tests/agent/tools/query-results.test.ts +77 -0
- package/tests/agent/tools/run-tests.test.ts +89 -0
- package/tests/agent/tools/send-request.test.ts +78 -0
- package/tests/agent/tools/validate-tests.test.ts +59 -0
- package/tests/ai/ai-generator.integration.test.ts +131 -0
- package/tests/ai/llm-client.test.ts +145 -0
- package/tests/ai/output-parser.test.ts +132 -0
- package/tests/ai/prompt-builder.test.ts +67 -0
- package/tests/ai/types.test.ts +55 -0
- package/tests/cli/args.test.ts +63 -0
- package/tests/cli/chat.test.ts +38 -0
- package/tests/cli/ci-init.test.ts +112 -0
- package/tests/cli/commands.test.ts +316 -0
- package/tests/cli/coverage.test.ts +58 -0
- package/tests/cli/doctor.test.ts +39 -0
- package/tests/cli/envs.test.ts +181 -0
- package/tests/cli/init.test.ts +80 -0
- package/tests/cli/runs.test.ts +94 -0
- package/tests/cli/safe-run.test.ts +103 -0
- package/tests/cli/update.test.ts +32 -0
- package/tests/core/generator/schema-utils.test.ts +108 -0
- package/tests/core/parser/nested-assertions.test.ts +80 -0
- package/tests/core/runner/root-body-assertions.test.ts +70 -0
- package/tests/db/chat-queries.test.ts +88 -0
- package/tests/db/chat-schema.test.ts +37 -0
- package/tests/db/environments.test.ts +131 -0
- package/tests/db/queries.test.ts +409 -0
- package/tests/db/schema.test.ts +141 -0
- package/tests/fixtures/.env.yaml +3 -0
- package/tests/fixtures/auth-token-test.yaml +8 -0
- package/tests/fixtures/bail/suite-a.yaml +6 -0
- package/tests/fixtures/bail/suite-b.yaml +6 -0
- package/tests/fixtures/crud.yaml +35 -0
- package/tests/fixtures/invalid-missing-name.yaml +5 -0
- package/tests/fixtures/invalid-no-method.yaml +6 -0
- package/tests/fixtures/petstore-auth.json +295 -0
- package/tests/fixtures/petstore-simple.json +151 -0
- package/tests/fixtures/post-only.yaml +12 -0
- package/tests/fixtures/simple.yaml +6 -0
- package/tests/fixtures/valid/.env.yaml +1 -0
- package/tests/fixtures/valid/a.yaml +5 -0
- package/tests/fixtures/valid/b.yml +5 -0
- package/tests/generator/coverage-scanner.test.ts +129 -0
- package/tests/generator/data-factory.test.ts +133 -0
- package/tests/generator/openapi-reader.test.ts +131 -0
- package/tests/integration/auth-flow.test.ts +217 -0
- package/tests/mcp/coverage-analysis.test.ts +64 -0
- package/tests/mcp/explore-api-schemas.test.ts +105 -0
- package/tests/mcp/explore-api.test.ts +49 -0
- package/tests/mcp/generate-missing-tests.test.ts +69 -0
- package/tests/mcp/manage-environment.test.ts +89 -0
- package/tests/mcp/save-test-suite.test.ts +116 -0
- package/tests/mcp/send-request.test.ts +79 -0
- package/tests/mcp/setup-api.test.ts +106 -0
- package/tests/mcp/tools.test.ts +248 -0
- package/tests/parser/schema.test.ts +134 -0
- package/tests/parser/variables.test.ts +227 -0
- package/tests/parser/yaml-parser.test.ts +69 -0
- package/tests/reporter/console.test.ts +256 -0
- package/tests/reporter/json.test.ts +98 -0
- package/tests/reporter/junit.test.ts +284 -0
- package/tests/runner/assertions.test.ts +262 -0
- package/tests/runner/executor.test.ts +310 -0
- package/tests/runner/http-client.test.ts +138 -0
- package/tests/web/routes.test.ts +160 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, test, expect, mock, beforeEach, afterAll } from "bun:test";
|
|
2
|
+
|
|
3
|
+
mock.module("../../src/core/generator/openapi-reader.ts", () => ({
|
|
4
|
+
readOpenApiSpec: mock(() => Promise.resolve({
|
|
5
|
+
info: { title: "Pet Store", version: "1.0.0" },
|
|
6
|
+
servers: [{ url: "https://petstore.io/v1" }],
|
|
7
|
+
paths: {},
|
|
8
|
+
})),
|
|
9
|
+
extractEndpoints: mock(() => [
|
|
10
|
+
{ method: "GET", path: "/pets", summary: "List pets", tags: ["pets"], parameters: [{ name: "limit", in: "query", required: false }], responses: [{ statusCode: 200, description: "OK" }], requestBodySchema: undefined },
|
|
11
|
+
{ method: "POST", path: "/pets", summary: "Create pet", tags: ["pets"], parameters: [], responses: [{ statusCode: 201, description: "Created" }], requestBodySchema: { type: "object" } },
|
|
12
|
+
{ method: "GET", path: "/users", summary: "List users", tags: ["users"], parameters: [], responses: [{ statusCode: 200, description: "OK" }], requestBodySchema: undefined },
|
|
13
|
+
]),
|
|
14
|
+
extractSecuritySchemes: mock(() => [
|
|
15
|
+
{ name: "bearerAuth", type: "http", scheme: "bearer" },
|
|
16
|
+
]),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
afterAll(() => { mock.restore(); });
|
|
20
|
+
|
|
21
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
22
|
+
import { registerExploreApiTool } from "../../src/mcp/tools/explore-api.ts";
|
|
23
|
+
|
|
24
|
+
describe("MCP explore_api", () => {
|
|
25
|
+
test("returns spec info and endpoints", async () => {
|
|
26
|
+
const server = new McpServer({ name: "test", version: "0.0.1" });
|
|
27
|
+
registerExploreApiTool(server);
|
|
28
|
+
|
|
29
|
+
const tool = (server as any)._registeredTools["explore_api"];
|
|
30
|
+
const result = await tool.handler({ specPath: "petstore.yaml" });
|
|
31
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
32
|
+
expect(parsed.title).toBe("Pet Store");
|
|
33
|
+
expect(parsed.totalEndpoints).toBe(3);
|
|
34
|
+
expect(parsed.endpoints).toHaveLength(3);
|
|
35
|
+
expect(parsed.securitySchemes).toHaveLength(1);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("filters by tag", async () => {
|
|
39
|
+
const server = new McpServer({ name: "test", version: "0.0.1" });
|
|
40
|
+
registerExploreApiTool(server);
|
|
41
|
+
|
|
42
|
+
const tool = (server as any)._registeredTools["explore_api"];
|
|
43
|
+
const result = await tool.handler({ specPath: "petstore.yaml", tag: "pets" });
|
|
44
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
45
|
+
expect(parsed.filteredByTag).toBe("pets");
|
|
46
|
+
expect(parsed.matchingEndpoints).toBe(2);
|
|
47
|
+
expect(parsed.endpoints).toHaveLength(2);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, test, expect, mock, afterAll } from "bun:test";
|
|
2
|
+
|
|
3
|
+
const mockEndpoints = [
|
|
4
|
+
{ method: "GET", path: "/pets", summary: "List pets", tags: ["pets"], parameters: [], responses: [{ statusCode: 200, description: "OK" }], security: [] },
|
|
5
|
+
{ method: "POST", path: "/pets", summary: "Create pet", tags: ["pets"], parameters: [], responses: [{ statusCode: 201, description: "Created" }], security: [] },
|
|
6
|
+
{ method: "GET", path: "/users", summary: "List users", tags: ["users"], parameters: [], responses: [{ statusCode: 200, description: "OK" }], security: [] },
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
mock.module("../../src/core/generator/openapi-reader.ts", () => ({
|
|
10
|
+
readOpenApiSpec: mock(() => Promise.resolve({
|
|
11
|
+
info: { title: "Pet Store", version: "1.0.0" },
|
|
12
|
+
servers: [{ url: "https://petstore.io/v1" }],
|
|
13
|
+
paths: {},
|
|
14
|
+
})),
|
|
15
|
+
extractEndpoints: mock(() => mockEndpoints),
|
|
16
|
+
extractSecuritySchemes: mock(() => []),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
const mockScanCovered = mock(() => Promise.resolve([
|
|
20
|
+
{ method: "GET", path: "/pets", file: "tests/pets.yaml" },
|
|
21
|
+
]));
|
|
22
|
+
|
|
23
|
+
mock.module("../../src/core/generator/coverage-scanner.ts", () => ({
|
|
24
|
+
scanCoveredEndpoints: mockScanCovered,
|
|
25
|
+
filterUncoveredEndpoints: mock((all: any[], covered: any[]) => {
|
|
26
|
+
const coveredSet = new Set(covered.map((c: any) => `${c.method} ${c.path}`));
|
|
27
|
+
return all.filter((ep: any) => !coveredSet.has(`${ep.method} ${ep.path}`));
|
|
28
|
+
}),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
afterAll(() => { mock.restore(); });
|
|
32
|
+
|
|
33
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
34
|
+
import { registerGenerateMissingTestsTool } from "../../src/mcp/tools/generate-missing-tests.ts";
|
|
35
|
+
|
|
36
|
+
describe("MCP generate_missing_tests", () => {
|
|
37
|
+
test("partially covered → guide with only uncovered endpoints", async () => {
|
|
38
|
+
const server = new McpServer({ name: "test", version: "0.0.1" });
|
|
39
|
+
registerGenerateMissingTestsTool(server);
|
|
40
|
+
|
|
41
|
+
const tool = (server as any)._registeredTools["generate_missing_tests"];
|
|
42
|
+
const result = await tool.handler({ specPath: "petstore.yaml", testsDir: "./tests/" });
|
|
43
|
+
|
|
44
|
+
expect(result.isError).toBeUndefined();
|
|
45
|
+
const text = result.content[0].text;
|
|
46
|
+
expect(text).toContain("Coverage: 1/3 endpoints covered (33%)");
|
|
47
|
+
expect(text).toContain("2 uncovered endpoints");
|
|
48
|
+
expect(text).toContain("POST /pets");
|
|
49
|
+
expect(text).toContain("GET /users");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("all covered → fullyCovered response", async () => {
|
|
53
|
+
mockScanCovered.mockImplementationOnce(() => Promise.resolve([
|
|
54
|
+
{ method: "GET", path: "/pets", file: "a.yaml" },
|
|
55
|
+
{ method: "POST", path: "/pets", file: "a.yaml" },
|
|
56
|
+
{ method: "GET", path: "/users", file: "b.yaml" },
|
|
57
|
+
]));
|
|
58
|
+
|
|
59
|
+
const server = new McpServer({ name: "test", version: "0.0.1" });
|
|
60
|
+
registerGenerateMissingTestsTool(server);
|
|
61
|
+
|
|
62
|
+
const tool = (server as any)._registeredTools["generate_missing_tests"];
|
|
63
|
+
const result = await tool.handler({ specPath: "petstore.yaml", testsDir: "./tests/" });
|
|
64
|
+
|
|
65
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
66
|
+
expect(parsed.fullyCovered).toBe(true);
|
|
67
|
+
expect(parsed.percentage).toBe(100);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { tmpdir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { unlinkSync } from "fs";
|
|
5
|
+
import { getDb, closeDb } from "../../src/db/schema.ts";
|
|
6
|
+
import { upsertEnvironment } from "../../src/db/queries.ts";
|
|
7
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
+
import { registerManageEnvironmentTool } from "../../src/mcp/tools/manage-environment.ts";
|
|
9
|
+
|
|
10
|
+
function tmpDb(): string {
|
|
11
|
+
return join(tmpdir(), `apitool-mcp-env-test-${Date.now()}-${Math.random().toString(36).slice(2)}.db`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function tryUnlink(path: string): void {
|
|
15
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
16
|
+
try { unlinkSync(path + suffix); } catch { /* ignore */ }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("MCP manage_environment", () => {
|
|
21
|
+
let dbPath: string;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
closeDb();
|
|
25
|
+
dbPath = tmpDb();
|
|
26
|
+
getDb(dbPath);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
closeDb();
|
|
31
|
+
tryUnlink(dbPath);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("list returns array", async () => {
|
|
35
|
+
const server = new McpServer({ name: "test", version: "0.0.1" });
|
|
36
|
+
registerManageEnvironmentTool(server, dbPath);
|
|
37
|
+
const tool = (server as any)._registeredTools["manage_environment"];
|
|
38
|
+
const result = await tool.handler({ action: "list" });
|
|
39
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
40
|
+
expect(Array.isArray(parsed)).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("set and get environment", async () => {
|
|
44
|
+
const server = new McpServer({ name: "test", version: "0.0.1" });
|
|
45
|
+
registerManageEnvironmentTool(server, dbPath);
|
|
46
|
+
const tool = (server as any)._registeredTools["manage_environment"];
|
|
47
|
+
|
|
48
|
+
// Set
|
|
49
|
+
const setResult = await tool.handler({ action: "set", name: "dev", variables: { base_url: "http://localhost" } });
|
|
50
|
+
const setParsed = JSON.parse(setResult.content[0].text);
|
|
51
|
+
expect(setParsed.success).toBe(true);
|
|
52
|
+
|
|
53
|
+
// Get
|
|
54
|
+
const getResult = await tool.handler({ action: "get", name: "dev" });
|
|
55
|
+
const getParsed = JSON.parse(getResult.content[0].text);
|
|
56
|
+
expect(getParsed.name).toBe("dev");
|
|
57
|
+
expect(getParsed.variables.base_url).toBe("http://localhost");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("delete environment", async () => {
|
|
61
|
+
upsertEnvironment("tmp", { key: "val" });
|
|
62
|
+
const server = new McpServer({ name: "test", version: "0.0.1" });
|
|
63
|
+
registerManageEnvironmentTool(server, dbPath);
|
|
64
|
+
const tool = (server as any)._registeredTools["manage_environment"];
|
|
65
|
+
|
|
66
|
+
const result = await tool.handler({ action: "delete", name: "tmp" });
|
|
67
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
68
|
+
expect(parsed.success).toBe(true);
|
|
69
|
+
expect(parsed.deleted).toBe("tmp");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("get missing env returns error", async () => {
|
|
73
|
+
const server = new McpServer({ name: "test", version: "0.0.1" });
|
|
74
|
+
registerManageEnvironmentTool(server, dbPath);
|
|
75
|
+
const tool = (server as any)._registeredTools["manage_environment"];
|
|
76
|
+
|
|
77
|
+
const result = await tool.handler({ action: "get", name: "ghost" });
|
|
78
|
+
expect(result.isError).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("delete missing env returns error", async () => {
|
|
82
|
+
const server = new McpServer({ name: "test", version: "0.0.1" });
|
|
83
|
+
registerManageEnvironmentTool(server, dbPath);
|
|
84
|
+
const tool = (server as any)._registeredTools["manage_environment"];
|
|
85
|
+
|
|
86
|
+
const result = await tool.handler({ action: "delete", name: "ghost" });
|
|
87
|
+
expect(result.isError).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { registerSaveTestSuiteTool } from "../../src/mcp/tools/save-test-suite.ts";
|
|
4
|
+
import { existsSync, rmSync, mkdirSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
const TEST_DIR = join(import.meta.dir, ".tmp-save-test");
|
|
8
|
+
|
|
9
|
+
describe("MCP save_test_suite", () => {
|
|
10
|
+
let handler: (args: Record<string, unknown>) => Promise<any>;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
const server = new McpServer({ name: "test", version: "0.0.1" });
|
|
14
|
+
registerSaveTestSuiteTool(server);
|
|
15
|
+
handler = (server as any)._registeredTools["save_test_suite"].handler;
|
|
16
|
+
if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const VALID_YAML = `name: "Test Suite"
|
|
24
|
+
base_url: "http://localhost:3000"
|
|
25
|
+
tests:
|
|
26
|
+
- name: "Get items"
|
|
27
|
+
GET: "/items"
|
|
28
|
+
expect:
|
|
29
|
+
status: 200
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
test("saves valid YAML and returns success", async () => {
|
|
33
|
+
const filePath = join(TEST_DIR, "test.yaml");
|
|
34
|
+
const result = await handler({ filePath, content: VALID_YAML });
|
|
35
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
36
|
+
|
|
37
|
+
expect(parsed.saved).toBe(true);
|
|
38
|
+
expect(parsed.suite.name).toBe("Test Suite");
|
|
39
|
+
expect(parsed.suite.tests).toBe(1);
|
|
40
|
+
expect(existsSync(filePath)).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("creates nested directories", async () => {
|
|
44
|
+
const filePath = join(TEST_DIR, "nested", "deep", "test.yaml");
|
|
45
|
+
const result = await handler({ filePath, content: VALID_YAML });
|
|
46
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
47
|
+
|
|
48
|
+
expect(parsed.saved).toBe(true);
|
|
49
|
+
expect(existsSync(filePath)).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("rejects invalid YAML syntax", async () => {
|
|
53
|
+
const filePath = join(TEST_DIR, "bad.yaml");
|
|
54
|
+
const result = await handler({ filePath, content: "name: [invalid yaml\n broken:" });
|
|
55
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
56
|
+
|
|
57
|
+
expect(parsed.saved).toBe(false);
|
|
58
|
+
expect(parsed.error).toContain("YAML parse error");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("rejects YAML that fails validation", async () => {
|
|
62
|
+
const filePath = join(TEST_DIR, "invalid.yaml");
|
|
63
|
+
const invalidYaml = `name: "Bad Suite"
|
|
64
|
+
tests: []
|
|
65
|
+
`;
|
|
66
|
+
const result = await handler({ filePath, content: invalidYaml });
|
|
67
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
68
|
+
|
|
69
|
+
expect(parsed.saved).toBe(false);
|
|
70
|
+
expect(parsed.error).toContain("Validation");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("refuses to overwrite existing file without flag", async () => {
|
|
74
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
75
|
+
const filePath = join(TEST_DIR, "existing.yaml");
|
|
76
|
+
await Bun.write(filePath, VALID_YAML);
|
|
77
|
+
|
|
78
|
+
const result = await handler({ filePath, content: VALID_YAML });
|
|
79
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
80
|
+
|
|
81
|
+
expect(parsed.saved).toBe(false);
|
|
82
|
+
expect(parsed.error).toContain("already exists");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("overwrites existing file with overwrite=true", async () => {
|
|
86
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
87
|
+
const filePath = join(TEST_DIR, "existing.yaml");
|
|
88
|
+
await Bun.write(filePath, "old content");
|
|
89
|
+
|
|
90
|
+
const result = await handler({ filePath, content: VALID_YAML, overwrite: true });
|
|
91
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
92
|
+
|
|
93
|
+
expect(parsed.saved).toBe(true);
|
|
94
|
+
const content = await Bun.file(filePath).text();
|
|
95
|
+
expect(content).toBe(VALID_YAML);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("preserves original YAML content (no re-serialization)", async () => {
|
|
99
|
+
const yamlWithComments = `# This is a test suite
|
|
100
|
+
name: "With Comments"
|
|
101
|
+
base_url: "http://localhost:3000"
|
|
102
|
+
tests:
|
|
103
|
+
- name: "Get items"
|
|
104
|
+
GET: "/items"
|
|
105
|
+
expect:
|
|
106
|
+
status: 200
|
|
107
|
+
`;
|
|
108
|
+
const filePath = join(TEST_DIR, "comments.yaml");
|
|
109
|
+
const result = await handler({ filePath, content: yamlWithComments });
|
|
110
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
111
|
+
|
|
112
|
+
expect(parsed.saved).toBe(true);
|
|
113
|
+
const written = await Bun.file(filePath).text();
|
|
114
|
+
expect(written).toBe(yamlWithComments);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, test, expect, mock, beforeEach, afterAll } from "bun:test";
|
|
2
|
+
|
|
3
|
+
mock.module("../../src/core/runner/http-client.ts", () => ({
|
|
4
|
+
executeRequest: mock(() => Promise.resolve({
|
|
5
|
+
status: 200,
|
|
6
|
+
headers: { "content-type": "application/json" },
|
|
7
|
+
body: '{"ok":true}',
|
|
8
|
+
body_parsed: { ok: true },
|
|
9
|
+
duration_ms: 55,
|
|
10
|
+
})),
|
|
11
|
+
DEFAULT_FETCH_OPTIONS: { timeout: 30000, retries: 0, retry_delay: 1000, follow_redirects: true },
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
mock.module("../../src/core/parser/variables.ts", () => ({
|
|
15
|
+
loadEnvironment: mock(() => Promise.resolve({ base_url: "https://api.test.com" })),
|
|
16
|
+
substituteString: mock((template: string, vars: Record<string, unknown>) => {
|
|
17
|
+
if (typeof template !== "string") return template;
|
|
18
|
+
return template.replace(/\{\{(.+?)\}\}/g, (_, key: string) => String(vars[key] ?? `{{${key}}}`));
|
|
19
|
+
}),
|
|
20
|
+
substituteDeep: mock((value: any, vars: Record<string, unknown>) => {
|
|
21
|
+
if (typeof value === "object" && value !== null) {
|
|
22
|
+
const result: Record<string, unknown> = {};
|
|
23
|
+
for (const [k, v] of Object.entries(value)) {
|
|
24
|
+
result[k] = typeof v === "string"
|
|
25
|
+
? v.replace(/\{\{(.+?)\}\}/g, (_, key: string) => String(vars[key] ?? `{{${key}}}`))
|
|
26
|
+
: v;
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}),
|
|
32
|
+
GENERATORS: {},
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
afterAll(() => { mock.restore(); });
|
|
36
|
+
|
|
37
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
38
|
+
import { registerSendRequestTool } from "../../src/mcp/tools/send-request.ts";
|
|
39
|
+
import { executeRequest } from "../../src/core/runner/http-client.ts";
|
|
40
|
+
|
|
41
|
+
describe("MCP send_request", () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
(executeRequest as ReturnType<typeof mock>).mockClear();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("registers and handles GET request", async () => {
|
|
47
|
+
const server = new McpServer({ name: "test", version: "0.0.1" });
|
|
48
|
+
registerSendRequestTool(server);
|
|
49
|
+
|
|
50
|
+
const tool = (server as any)._registeredTools["send_request"];
|
|
51
|
+
expect(tool).toBeDefined();
|
|
52
|
+
|
|
53
|
+
const result = await tool.handler({ method: "GET", url: "https://api.test.com/data" });
|
|
54
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
55
|
+
expect(parsed.status).toBe(200);
|
|
56
|
+
expect(parsed.duration_ms).toBe(55);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("interpolates variables from environment", async () => {
|
|
60
|
+
const server = new McpServer({ name: "test", version: "0.0.1" });
|
|
61
|
+
registerSendRequestTool(server);
|
|
62
|
+
|
|
63
|
+
const tool = (server as any)._registeredTools["send_request"];
|
|
64
|
+
await tool.handler({ method: "GET", url: "{{base_url}}/users", envName: "dev" });
|
|
65
|
+
expect(executeRequest).toHaveBeenCalledTimes(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("returns error on failure", async () => {
|
|
69
|
+
(executeRequest as ReturnType<typeof mock>).mockRejectedValueOnce(new Error("timeout"));
|
|
70
|
+
const server = new McpServer({ name: "test", version: "0.0.1" });
|
|
71
|
+
registerSendRequestTool(server);
|
|
72
|
+
|
|
73
|
+
const tool = (server as any)._registeredTools["send_request"];
|
|
74
|
+
const result = await tool.handler({ method: "GET", url: "https://bad.host/" });
|
|
75
|
+
expect(result.isError).toBe(true);
|
|
76
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
77
|
+
expect(parsed.error).toBe("timeout");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, test, expect, mock, afterAll, beforeEach } from "bun:test";
|
|
2
|
+
import { mkdirSync, existsSync, readFileSync, rmSync } from "fs";
|
|
3
|
+
import { join, resolve } from "path";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
|
|
6
|
+
// Mock the DB layer
|
|
7
|
+
const mockFindCollectionByNameOrId = mock(() => null);
|
|
8
|
+
const mockCreateCollection = mock(() => 42);
|
|
9
|
+
const mockUpsertEnvironment = mock(() => {});
|
|
10
|
+
const mockNormalizePath = mock((p: string) => p.replace(/\\/g, "/"));
|
|
11
|
+
|
|
12
|
+
mock.module("../../src/db/queries.ts", () => ({
|
|
13
|
+
findCollectionByNameOrId: mockFindCollectionByNameOrId,
|
|
14
|
+
createCollection: mockCreateCollection,
|
|
15
|
+
upsertEnvironment: mockUpsertEnvironment,
|
|
16
|
+
normalizePath: mockNormalizePath,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
mock.module("../../src/db/schema.ts", () => ({
|
|
20
|
+
getDb: mock(() => ({})),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
mock.module("../../src/core/generator/index.ts", () => ({
|
|
24
|
+
readOpenApiSpec: mock(() => Promise.resolve({
|
|
25
|
+
servers: [{ url: "https://petstore.io/v2" }],
|
|
26
|
+
})),
|
|
27
|
+
extractEndpoints: mock(() => [
|
|
28
|
+
{ method: "GET", path: "/pets" },
|
|
29
|
+
{ method: "POST", path: "/pets" },
|
|
30
|
+
]),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
mock.module("../../src/cli/commands/envs.ts", () => ({
|
|
34
|
+
toYaml: mock((vars: Record<string, string>) =>
|
|
35
|
+
Object.entries(vars).map(([k, v]) => `${k}: ${v}`).join("\n")
|
|
36
|
+
),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
afterAll(() => { mock.restore(); });
|
|
40
|
+
|
|
41
|
+
import { setupApi } from "../../src/core/setup-api.ts";
|
|
42
|
+
|
|
43
|
+
describe("setupApi", () => {
|
|
44
|
+
let tempDir: string;
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
tempDir = join(tmpdir(), `apitool-test-${Date.now()}`);
|
|
48
|
+
mockFindCollectionByNameOrId.mockImplementation(() => null);
|
|
49
|
+
mockCreateCollection.mockImplementation(() => 42);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("creates API with spec → collection created, dirs exist, env written", async () => {
|
|
53
|
+
const dir = join(tempDir, "petstore");
|
|
54
|
+
const result = await setupApi({
|
|
55
|
+
name: "petstore",
|
|
56
|
+
spec: "https://petstore.io/v2/swagger.json",
|
|
57
|
+
dir,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(result.created).toBe(true);
|
|
61
|
+
expect(result.collectionId).toBe(42);
|
|
62
|
+
expect(result.baseUrl).toBe("https://petstore.io/v2");
|
|
63
|
+
expect(result.specEndpoints).toBe(2);
|
|
64
|
+
expect(existsSync(join(dir, "tests"))).toBe(true);
|
|
65
|
+
expect(existsSync(join(dir, ".env.yaml"))).toBe(true);
|
|
66
|
+
|
|
67
|
+
// Cleanup
|
|
68
|
+
rmSync(dir, { recursive: true, force: true });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("duplicate name throws error", async () => {
|
|
72
|
+
mockFindCollectionByNameOrId.mockImplementation(() => ({ id: 1, name: "petstore" }));
|
|
73
|
+
|
|
74
|
+
await expect(setupApi({
|
|
75
|
+
name: "petstore",
|
|
76
|
+
dir: join(tempDir, "dup"),
|
|
77
|
+
})).rejects.toThrow("already exists");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("without spec → collection created without openapi_spec", async () => {
|
|
81
|
+
const dir = join(tempDir, "nospec");
|
|
82
|
+
const result = await setupApi({
|
|
83
|
+
name: "nospec-api",
|
|
84
|
+
dir,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(result.created).toBe(true);
|
|
88
|
+
expect(result.specEndpoints).toBe(0);
|
|
89
|
+
expect(result.baseUrl).toBe("");
|
|
90
|
+
|
|
91
|
+
rmSync(dir, { recursive: true, force: true });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("custom envVars are passed through", async () => {
|
|
95
|
+
const dir = join(tempDir, "withenv");
|
|
96
|
+
await setupApi({
|
|
97
|
+
name: "myapi",
|
|
98
|
+
dir,
|
|
99
|
+
envVars: { token: "abc123" },
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(mockUpsertEnvironment).toHaveBeenCalled();
|
|
103
|
+
|
|
104
|
+
rmSync(dir, { recursive: true, force: true });
|
|
105
|
+
});
|
|
106
|
+
});
|