@kirrosh/apitool 0.4.3 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/APITOOL.md +48 -2
- package/package.json +1 -1
- package/scripts/run-mocked-tests.ts +0 -2
- package/src/cli/commands/ci-init.ts +48 -11
- package/src/cli/commands/compare.ts +129 -0
- package/src/cli/commands/coverage.ts +4 -0
- package/src/cli/commands/run.ts +28 -9
- package/src/cli/index.ts +53 -6
- package/src/core/generator/openapi-reader.ts +1 -1
- package/src/core/parser/types.ts +2 -0
- package/src/core/parser/yaml-parser.ts +1 -1
- package/src/core/runner/execute-run.ts +26 -3
- package/src/core/runner/executor.ts +17 -3
- package/src/db/schema.ts +6 -0
- package/src/mcp/server.ts +6 -1
- package/src/mcp/tools/coverage-analysis.ts +4 -1
- package/src/mcp/tools/describe-endpoint.ts +159 -0
- package/src/mcp/tools/generate-missing-tests.ts +9 -2
- package/src/mcp/tools/generate-tests-guide.ts +42 -2
- package/src/mcp/tools/query-db.ts +71 -3
- package/src/mcp/tools/run-tests.ts +5 -1
- package/src/mcp/tools/save-test-suite.ts +183 -127
- package/src/mcp/tools/set-work-dir.ts +38 -0
- package/src/mcp/tools/setup-api.ts +26 -0
- package/src/web/routes/dashboard.ts +4 -4
- package/src/web/server.ts +1 -1
- package/tests/agent/tools/manage-environment.test.ts +2 -2
- package/tests/core/generator/schema-utils.test.ts +1 -1
- package/tests/core/runner/root-body-assertions.test.ts +1 -1
- package/tests/db/chat-schema.test.ts +1 -1
- package/tests/db/schema.test.ts +1 -1
- package/tests/integration/auth-flow.test.ts +3 -58
- package/tests/mcp/setup-api.test.ts +1 -1
|
@@ -5,6 +5,147 @@ import { join, dirname } from "node:path";
|
|
|
5
5
|
import { existsSync, mkdirSync } from "node:fs";
|
|
6
6
|
import YAML from "yaml";
|
|
7
7
|
|
|
8
|
+
export interface SaveResult {
|
|
9
|
+
saved: boolean;
|
|
10
|
+
filePath?: string;
|
|
11
|
+
suite?: { name: unknown; tests: number; base_url: unknown };
|
|
12
|
+
hint?: string;
|
|
13
|
+
coverage?: Record<string, unknown>;
|
|
14
|
+
error?: string;
|
|
15
|
+
detected?: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function validateAndSave(
|
|
19
|
+
filePath: string,
|
|
20
|
+
content: string,
|
|
21
|
+
overwrite: boolean | undefined,
|
|
22
|
+
dbPath?: string,
|
|
23
|
+
): Promise<{ result: SaveResult; isError: boolean }> {
|
|
24
|
+
// Parse YAML
|
|
25
|
+
let parsed: unknown;
|
|
26
|
+
try {
|
|
27
|
+
parsed = YAML.parse(content);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
return {
|
|
30
|
+
result: {
|
|
31
|
+
saved: false,
|
|
32
|
+
error: `YAML parse error: ${(err as Error).message}`,
|
|
33
|
+
hint: "Check YAML syntax — indentation, colons, and quoting",
|
|
34
|
+
},
|
|
35
|
+
isError: true,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Validate against test suite schema
|
|
40
|
+
try {
|
|
41
|
+
validateSuite(parsed);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
const message = (err as Error).message;
|
|
44
|
+
let hint = "Check the test suite structure matches the expected format";
|
|
45
|
+
if (message.includes("status")) {
|
|
46
|
+
hint = "Status codes must be numbers, not strings (e.g. status: 200, not status: \"200\")";
|
|
47
|
+
} else if (message.includes("exists")) {
|
|
48
|
+
hint = "exists must be boolean true/false, not string \"true\"/\"false\"";
|
|
49
|
+
} else if (message.includes("tests")) {
|
|
50
|
+
hint = "Suite must have a 'tests' array with at least one test step";
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
result: { saved: false, error: `Validation: ${message}`, hint },
|
|
54
|
+
isError: true,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Detect hardcoded credentials — long opaque strings in auth headers
|
|
59
|
+
const credentialPattern = /Authorization\s*:\s*["']?(Basic|Bearer)\s+([A-Za-z0-9+/=_\-]{20,})["']?/g;
|
|
60
|
+
const credMatches = [...content.matchAll(credentialPattern)];
|
|
61
|
+
const suspiciousCredentials = credMatches.filter(m => {
|
|
62
|
+
const value = m[2]!;
|
|
63
|
+
return !value.startsWith("{{") && !value.endsWith("}}");
|
|
64
|
+
});
|
|
65
|
+
if (suspiciousCredentials.length > 0) {
|
|
66
|
+
return {
|
|
67
|
+
result: {
|
|
68
|
+
saved: false,
|
|
69
|
+
error: "Hardcoded credentials detected in Authorization header(s)",
|
|
70
|
+
hint: "Never put literal API keys or tokens in YAML files. Store them in the environment instead: use manage_environment(action: \"set\", name: \"default\", collectionName: \"...\", variables: {\"api_key\": \"...\"}) and reference as {{api_key}} in headers.",
|
|
71
|
+
detected: suspiciousCredentials.map(m => `${m[1]} <redacted>`),
|
|
72
|
+
},
|
|
73
|
+
isError: true,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Resolve path
|
|
78
|
+
const resolvedPath = filePath.startsWith("/") || /^[a-zA-Z]:/.test(filePath)
|
|
79
|
+
? filePath
|
|
80
|
+
: join(process.cwd(), filePath);
|
|
81
|
+
|
|
82
|
+
// Check existing file
|
|
83
|
+
if (!overwrite && existsSync(resolvedPath)) {
|
|
84
|
+
return {
|
|
85
|
+
result: {
|
|
86
|
+
saved: false,
|
|
87
|
+
error: `File already exists: ${resolvedPath}`,
|
|
88
|
+
hint: "Use overwrite: true to replace the existing file",
|
|
89
|
+
},
|
|
90
|
+
isError: true,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Create directories
|
|
95
|
+
const dir = dirname(resolvedPath);
|
|
96
|
+
if (!existsSync(dir)) {
|
|
97
|
+
mkdirSync(dir, { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Write original YAML content (preserve formatting/comments)
|
|
101
|
+
await Bun.write(resolvedPath, content);
|
|
102
|
+
|
|
103
|
+
// Extract summary info
|
|
104
|
+
const suite = parsed as Record<string, unknown>;
|
|
105
|
+
const tests = (suite.tests as unknown[]) ?? [];
|
|
106
|
+
|
|
107
|
+
const result: SaveResult = {
|
|
108
|
+
saved: true,
|
|
109
|
+
filePath: resolvedPath,
|
|
110
|
+
suite: {
|
|
111
|
+
name: suite.name,
|
|
112
|
+
tests: tests.length,
|
|
113
|
+
base_url: suite.base_url ?? null,
|
|
114
|
+
},
|
|
115
|
+
hint: "After tests are ready, ask the user if they want to set up CI/CD with ci_init to run tests automatically on push.",
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Attempt to compute coverage hint
|
|
119
|
+
try {
|
|
120
|
+
const testDir = dirname(resolvedPath);
|
|
121
|
+
const { findCollectionByTestPath } = await import("../../db/queries.ts");
|
|
122
|
+
const { getDb } = await import("../../db/schema.ts");
|
|
123
|
+
getDb(dbPath);
|
|
124
|
+
const collection = findCollectionByTestPath(testDir);
|
|
125
|
+
if (collection?.openapi_spec) {
|
|
126
|
+
const { readOpenApiSpec, extractEndpoints } = await import("../../core/generator/openapi-reader.ts");
|
|
127
|
+
const { scanCoveredEndpoints, filterUncoveredEndpoints } = await import("../../core/generator/coverage-scanner.ts");
|
|
128
|
+
|
|
129
|
+
const doc = await readOpenApiSpec(collection.openapi_spec);
|
|
130
|
+
const allEndpoints = extractEndpoints(doc);
|
|
131
|
+
const covered = await scanCoveredEndpoints(testDir);
|
|
132
|
+
const uncovered = filterUncoveredEndpoints(allEndpoints, covered);
|
|
133
|
+
|
|
134
|
+
const total = allEndpoints.length;
|
|
135
|
+
const coveredCount = total - uncovered.length;
|
|
136
|
+
const percentage = total > 0 ? Math.round((coveredCount / total) * 100) : 0;
|
|
137
|
+
|
|
138
|
+
const coverage: Record<string, unknown> = { percentage, covered: coveredCount, total, uncoveredCount: uncovered.length };
|
|
139
|
+
if (percentage < 80 && uncovered.length > 0) {
|
|
140
|
+
coverage.suggestion = `Use generate_missing_tests to cover ${uncovered.length} remaining endpoint${uncovered.length > 1 ? "s" : ""}`;
|
|
141
|
+
}
|
|
142
|
+
result.coverage = coverage;
|
|
143
|
+
}
|
|
144
|
+
} catch { /* silently skip coverage if unavailable */ }
|
|
145
|
+
|
|
146
|
+
return { result, isError: false };
|
|
147
|
+
}
|
|
148
|
+
|
|
8
149
|
export function registerSaveTestSuiteTool(server: McpServer, dbPath?: string) {
|
|
9
150
|
server.registerTool("save_test_suite", {
|
|
10
151
|
description: "Save a YAML test suite file with validation. Parses and validates the YAML content " +
|
|
@@ -17,144 +158,59 @@ export function registerSaveTestSuiteTool(server: McpServer, dbPath?: string) {
|
|
|
17
158
|
},
|
|
18
159
|
}, async ({ filePath, content, overwrite }) => {
|
|
19
160
|
try {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
validateSuite(parsed);
|
|
38
|
-
} catch (err) {
|
|
39
|
-
const message = (err as Error).message;
|
|
40
|
-
// Extract Zod error details
|
|
41
|
-
let hint = "Check the test suite structure matches the expected format";
|
|
42
|
-
if (message.includes("status")) {
|
|
43
|
-
hint = "Status codes must be numbers, not strings (e.g. status: 200, not status: \"200\")";
|
|
44
|
-
} else if (message.includes("exists")) {
|
|
45
|
-
hint = "exists must be boolean true/false, not string \"true\"/\"false\"";
|
|
46
|
-
} else if (message.includes("tests")) {
|
|
47
|
-
hint = "Suite must have a 'tests' array with at least one test step";
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
content: [{ type: "text" as const, text: JSON.stringify({
|
|
52
|
-
saved: false,
|
|
53
|
-
error: `Validation: ${message}`,
|
|
54
|
-
hint,
|
|
55
|
-
}, null, 2) }],
|
|
56
|
-
isError: true,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Detect hardcoded credentials — long opaque strings in auth headers
|
|
61
|
-
const credentialPattern = /Authorization\s*:\s*["']?(Basic|Bearer)\s+([A-Za-z0-9+/=_\-]{20,})["']?/g;
|
|
62
|
-
const credMatches = [...content.matchAll(credentialPattern)];
|
|
63
|
-
const suspiciousCredentials = credMatches.filter(m => {
|
|
64
|
-
const value = m[2]!;
|
|
65
|
-
// Allow {{variable}} references — flag only literal tokens
|
|
66
|
-
return !value.startsWith("{{") && !value.endsWith("}}");
|
|
67
|
-
});
|
|
68
|
-
if (suspiciousCredentials.length > 0) {
|
|
69
|
-
return {
|
|
70
|
-
content: [{ type: "text" as const, text: JSON.stringify({
|
|
71
|
-
saved: false,
|
|
72
|
-
error: "Hardcoded credentials detected in Authorization header(s)",
|
|
73
|
-
hint: "Never put literal API keys or tokens in YAML files. Store them in the environment instead: use manage_environment(action: \"set\", name: \"default\", collectionName: \"...\", variables: {\"api_key\": \"...\"}) and reference as {{api_key}} in headers.",
|
|
74
|
-
detected: suspiciousCredentials.map(m => `${m[1]} <redacted>`),
|
|
75
|
-
}, null, 2) }],
|
|
76
|
-
isError: true,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
161
|
+
const { result, isError } = await validateAndSave(filePath, content, overwrite, dbPath);
|
|
162
|
+
return {
|
|
163
|
+
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
|
|
164
|
+
...(isError ? { isError: true } : {}),
|
|
165
|
+
};
|
|
166
|
+
} catch (err) {
|
|
167
|
+
return {
|
|
168
|
+
content: [{ type: "text" as const, text: JSON.stringify({
|
|
169
|
+
saved: false,
|
|
170
|
+
error: (err as Error).message,
|
|
171
|
+
}, null, 2) }],
|
|
172
|
+
isError: true,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
79
177
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
178
|
+
export function registerSaveTestSuitesTool(server: McpServer, dbPath?: string) {
|
|
179
|
+
server.registerTool("save_test_suites", {
|
|
180
|
+
description: "Save multiple YAML test suite files in a single call. Each file is validated before writing. " +
|
|
181
|
+
"Returns per-file results. Use when you have generated multiple suites at once.",
|
|
182
|
+
inputSchema: {
|
|
183
|
+
files: z.array(z.object({
|
|
184
|
+
filePath: z.string().describe("Path for saving the YAML test file"),
|
|
185
|
+
content: z.string().describe("YAML content of the test suite"),
|
|
186
|
+
})).describe("Array of files to save"),
|
|
187
|
+
overwrite: z.optional(z.boolean()).describe("Overwrite existing files (default: false)"),
|
|
188
|
+
},
|
|
189
|
+
}, async ({ files, overwrite }) => {
|
|
190
|
+
try {
|
|
191
|
+
const results: Array<SaveResult & { filePath: string; inputPath: string }> = [];
|
|
192
|
+
let hasErrors = false;
|
|
96
193
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
194
|
+
for (const file of files) {
|
|
195
|
+
const { result, isError } = await validateAndSave(file.filePath, file.content, overwrite, dbPath);
|
|
196
|
+
results.push({ ...result, inputPath: file.filePath, filePath: result.filePath ?? file.filePath });
|
|
197
|
+
if (isError) hasErrors = true;
|
|
101
198
|
}
|
|
102
199
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const tests = (suite.tests as unknown[]) ?? [];
|
|
109
|
-
|
|
110
|
-
const result: Record<string, unknown> = {
|
|
111
|
-
saved: true,
|
|
112
|
-
filePath: resolvedPath,
|
|
113
|
-
suite: {
|
|
114
|
-
name: suite.name,
|
|
115
|
-
tests: tests.length,
|
|
116
|
-
base_url: suite.base_url ?? null,
|
|
117
|
-
},
|
|
200
|
+
const summary = {
|
|
201
|
+
total: files.length,
|
|
202
|
+
saved: results.filter(r => r.saved).length,
|
|
203
|
+
failed: results.filter(r => !r.saved).length,
|
|
204
|
+
files: results,
|
|
118
205
|
};
|
|
119
206
|
|
|
120
|
-
// CI hint
|
|
121
|
-
result.hint = "After tests are ready, ask the user if they want to set up CI/CD with ci_init to run tests automatically on push.";
|
|
122
|
-
|
|
123
|
-
// Attempt to compute coverage hint
|
|
124
|
-
try {
|
|
125
|
-
const testDir = dirname(resolvedPath);
|
|
126
|
-
const { findCollectionByTestPath } = await import("../../db/queries.ts");
|
|
127
|
-
const { getDb } = await import("../../db/schema.ts");
|
|
128
|
-
getDb(dbPath);
|
|
129
|
-
const collection = findCollectionByTestPath(testDir);
|
|
130
|
-
if (collection?.openapi_spec) {
|
|
131
|
-
const { readOpenApiSpec, extractEndpoints } = await import("../../core/generator/openapi-reader.ts");
|
|
132
|
-
const { scanCoveredEndpoints, filterUncoveredEndpoints } = await import("../../core/generator/coverage-scanner.ts");
|
|
133
|
-
|
|
134
|
-
const doc = await readOpenApiSpec(collection.openapi_spec);
|
|
135
|
-
const allEndpoints = extractEndpoints(doc);
|
|
136
|
-
const covered = await scanCoveredEndpoints(testDir);
|
|
137
|
-
const uncovered = filterUncoveredEndpoints(allEndpoints, covered);
|
|
138
|
-
|
|
139
|
-
const total = allEndpoints.length;
|
|
140
|
-
const coveredCount = total - uncovered.length;
|
|
141
|
-
const percentage = total > 0 ? Math.round((coveredCount / total) * 100) : 0;
|
|
142
|
-
|
|
143
|
-
const coverage: Record<string, unknown> = { percentage, covered: coveredCount, total, uncoveredCount: uncovered.length };
|
|
144
|
-
if (percentage < 80 && uncovered.length > 0) {
|
|
145
|
-
coverage.suggestion = `Use generate_missing_tests to cover ${uncovered.length} remaining endpoint${uncovered.length > 1 ? "s" : ""}`;
|
|
146
|
-
}
|
|
147
|
-
result.coverage = coverage;
|
|
148
|
-
}
|
|
149
|
-
} catch { /* silently skip coverage if unavailable */ }
|
|
150
|
-
|
|
151
207
|
return {
|
|
152
|
-
content: [{ type: "text" as const, text: JSON.stringify(
|
|
208
|
+
content: [{ type: "text" as const, text: JSON.stringify(summary, null, 2) }],
|
|
209
|
+
...(hasErrors ? { isError: true } : {}),
|
|
153
210
|
};
|
|
154
211
|
} catch (err) {
|
|
155
212
|
return {
|
|
156
213
|
content: [{ type: "text" as const, text: JSON.stringify({
|
|
157
|
-
saved: false,
|
|
158
214
|
error: (err as Error).message,
|
|
159
215
|
}, null, 2) }],
|
|
160
216
|
isError: true,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { resolve, join } from "node:path";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { resetDb } from "../../db/schema.ts";
|
|
6
|
+
|
|
7
|
+
export function registerSetWorkDirTool(server: McpServer) {
|
|
8
|
+
server.registerTool("set_work_dir", {
|
|
9
|
+
description:
|
|
10
|
+
"Set the working directory for this MCP session. " +
|
|
11
|
+
"Call this FIRST before any other tool when using a shared MCP server (npx). " +
|
|
12
|
+
"Determines where apitool.db and relative test paths resolve to. " +
|
|
13
|
+
"Pass the absolute path to your project root (same as workspace root in your editor).",
|
|
14
|
+
inputSchema: {
|
|
15
|
+
workDir: z.string().describe(
|
|
16
|
+
"Absolute path to project root (e.g. /home/user/myproject or C:/Users/user/myproject)"
|
|
17
|
+
),
|
|
18
|
+
},
|
|
19
|
+
}, async ({ workDir }) => {
|
|
20
|
+
const resolved = resolve(workDir);
|
|
21
|
+
if (!existsSync(resolved)) {
|
|
22
|
+
return {
|
|
23
|
+
content: [{ type: "text" as const, text: JSON.stringify({ error: `Directory not found: ${resolved}` }, null, 2) }],
|
|
24
|
+
isError: true,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
process.chdir(resolved);
|
|
28
|
+
resetDb();
|
|
29
|
+
const dbPath = join(resolved, "apitool.db");
|
|
30
|
+
return {
|
|
31
|
+
content: [{ type: "text" as const, text: JSON.stringify({
|
|
32
|
+
workDir: resolved,
|
|
33
|
+
apitool_db: dbPath,
|
|
34
|
+
hint: "Working directory set. All relative paths and apitool.db will now resolve from this directory.",
|
|
35
|
+
}, null, 2) }],
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
3
5
|
import { setupApi } from "../../core/setup-api.ts";
|
|
6
|
+
import { resetDb } from "../../db/schema.ts";
|
|
7
|
+
|
|
8
|
+
function findProjectRoot(fromPath: string): string | null {
|
|
9
|
+
let current = existsSync(fromPath) ? fromPath : dirname(fromPath);
|
|
10
|
+
while (true) {
|
|
11
|
+
if (existsSync(join(current, ".git")) || existsSync(join(current, "package.json"))) {
|
|
12
|
+
return current;
|
|
13
|
+
}
|
|
14
|
+
const parent = dirname(current);
|
|
15
|
+
if (parent === current) return null;
|
|
16
|
+
current = parent;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
4
19
|
|
|
5
20
|
export function registerSetupApiTool(server: McpServer, dbPath?: string) {
|
|
6
21
|
server.registerTool("setup_api", {
|
|
@@ -27,6 +42,17 @@ export function registerSetupApiTool(server: McpServer, dbPath?: string) {
|
|
|
27
42
|
};
|
|
28
43
|
}
|
|
29
44
|
}
|
|
45
|
+
|
|
46
|
+
// Auto-chdir to project root when dir is an absolute path
|
|
47
|
+
if (dir && isAbsolute(resolve(dir))) {
|
|
48
|
+
const resolvedDir = resolve(dir);
|
|
49
|
+
const root = findProjectRoot(resolvedDir);
|
|
50
|
+
if (root && root !== process.cwd()) {
|
|
51
|
+
process.chdir(root);
|
|
52
|
+
resetDb();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
30
56
|
const result = await setupApi({
|
|
31
57
|
name,
|
|
32
58
|
spec: specPath,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
getRunById,
|
|
12
12
|
getCollectionById,
|
|
13
13
|
} from "../../db/queries.ts";
|
|
14
|
-
import type { CollectionSummary } from "../../db/queries.ts";
|
|
14
|
+
import type { CollectionRecord, CollectionSummary } from "../../db/queries.ts";
|
|
15
15
|
|
|
16
16
|
const dashboard = new Hono();
|
|
17
17
|
|
|
@@ -79,7 +79,7 @@ dashboard.get("/panels/coverage", async (c) => {
|
|
|
79
79
|
const collection = getCollectionById(collectionId);
|
|
80
80
|
if (!collection?.openapi_spec) return c.html("");
|
|
81
81
|
|
|
82
|
-
return c.html(await renderCoveragePanel(collection));
|
|
82
|
+
return c.html(await renderCoveragePanel(collection as CollectionRecord & { openapi_spec: string }));
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
dashboard.get("/panels/history", (c) => {
|
|
@@ -134,7 +134,7 @@ function renderPage(collections: CollectionSummary[], selectedId: number | null)
|
|
|
134
134
|
</div>`;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
function renderCollectionContent(collection:
|
|
137
|
+
function renderCollectionContent(collection: CollectionRecord, envRecords: { id: number; name: string; collection_id: number | null }[]): string {
|
|
138
138
|
// Auto-select: prefer first scoped env, then first env if only one
|
|
139
139
|
const defaultEnv = envRecords.find(e => e.collection_id !== null)?.name
|
|
140
140
|
?? (envRecords.length === 1 ? envRecords[0]!.name : null);
|
|
@@ -237,7 +237,7 @@ async function renderRunResults(runId: number): Promise<string> {
|
|
|
237
237
|
return header + suitesHtml + autoExpandFailedScript();
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
async function renderCoveragePanel(collection:
|
|
240
|
+
async function renderCoveragePanel(collection: CollectionRecord & { openapi_spec: string }): Promise<string> {
|
|
241
241
|
try {
|
|
242
242
|
const { readOpenApiSpec, extractEndpoints } = await import("../../core/generator/openapi-reader.ts");
|
|
243
243
|
const { scanCoveredEndpoints, filterUncoveredEndpoints } = await import("../../core/generator/coverage-scanner.ts");
|
package/src/web/server.ts
CHANGED
|
@@ -58,7 +58,7 @@ export function createApp(options?: { dev?: boolean }) {
|
|
|
58
58
|
return c.body(content);
|
|
59
59
|
}
|
|
60
60
|
if (file === "htmx.min.js") {
|
|
61
|
-
const content = await Bun.file(htmxJsPath).text();
|
|
61
|
+
const content = await Bun.file(htmxJsPath as unknown as string).text();
|
|
62
62
|
c.header("Content-Type", "application/javascript; charset=utf-8");
|
|
63
63
|
c.header("Cache-Control", "public, max-age=86400");
|
|
64
64
|
return c.body(content);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, test, expect, mock, beforeEach, afterAll } from "bun:test";
|
|
2
2
|
|
|
3
3
|
const mockListEnvRecords = mock(() => [
|
|
4
|
-
{ id: 1, name: "staging", variables: { base_url: "https://staging.api.com" } },
|
|
4
|
+
{ id: 1, name: "staging", collection_id: null, variables: { base_url: "https://staging.api.com" } },
|
|
5
5
|
]);
|
|
6
6
|
const mockGetEnv = mock((): unknown => ({ base_url: "https://staging.api.com", api_key: "sk-123" }));
|
|
7
7
|
const mockUpsertEnv = mock(() => {});
|
|
@@ -37,7 +37,7 @@ describe("manageEnvironmentTool", () => {
|
|
|
37
37
|
test("list action returns environments", async () => {
|
|
38
38
|
const result = await manageEnvironmentTool.execute!({ action: "list" }, toolOpts);
|
|
39
39
|
expect(result).toEqual({
|
|
40
|
-
environments: [{ id: 1, name: "staging", variables: { base_url: "https://staging.api.com" } }],
|
|
40
|
+
environments: [{ id: 1, name: "staging", collection_id: null, variables: { base_url: "https://staging.api.com" } }],
|
|
41
41
|
});
|
|
42
42
|
});
|
|
43
43
|
|
|
@@ -70,7 +70,7 @@ describe("compressSchema", () => {
|
|
|
70
70
|
});
|
|
71
71
|
|
|
72
72
|
test("handles array without items", () => {
|
|
73
|
-
expect(compressSchema({ type: "array" })).toBe("[]");
|
|
73
|
+
expect(compressSchema({ type: "array" } as any)).toBe("[]");
|
|
74
74
|
});
|
|
75
75
|
|
|
76
76
|
test("handles schema without type", () => {
|
|
@@ -5,7 +5,7 @@ describe("DB schema v3 — chat tables", () => {
|
|
|
5
5
|
test("migration sets user_version to 3", () => {
|
|
6
6
|
const db = getDb();
|
|
7
7
|
const row = db.query("PRAGMA user_version").get() as { user_version: number };
|
|
8
|
-
expect(row.user_version).toBe(
|
|
8
|
+
expect(row.user_version).toBe(6);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
test("chat_sessions table exists", () => {
|
package/tests/db/schema.test.ts
CHANGED
|
@@ -97,7 +97,7 @@ describe("getDb / schema", () => {
|
|
|
97
97
|
dbPath = tmpDb();
|
|
98
98
|
const db = getDb(dbPath);
|
|
99
99
|
const row = db.query("PRAGMA user_version").get() as { user_version: number };
|
|
100
|
-
expect(row.user_version).toBe(
|
|
100
|
+
expect(row.user_version).toBe(6);
|
|
101
101
|
});
|
|
102
102
|
|
|
103
103
|
test("closeDb resets singleton so next call opens fresh db", () => {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
2
2
|
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
|
|
3
3
|
import { readOpenApiSpec, extractEndpoints, extractSecuritySchemes } from "../../src/core/generator/openapi-reader.ts";
|
|
4
|
-
import { generateSuites, writeSuites } from "../../src/core/generator/skeleton.ts";
|
|
5
4
|
import { parseFile } from "../../src/core/parser/yaml-parser.ts";
|
|
6
5
|
import { runSuite } from "../../src/core/runner/executor.ts";
|
|
7
6
|
import { tmpdir } from "os";
|
|
@@ -157,61 +156,7 @@ describe("Auth flow integration", () => {
|
|
|
157
156
|
expect(spec.components.securitySchemes.bearerAuth).toBeDefined();
|
|
158
157
|
});
|
|
159
158
|
|
|
160
|
-
test("generate auth-aware tests from live spec, then run them",
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
const spec = await res.json();
|
|
164
|
-
const specPath = join(tmpDir, "spec.json");
|
|
165
|
-
await writeFile(specPath, JSON.stringify(spec));
|
|
166
|
-
|
|
167
|
-
// Generate skeleton
|
|
168
|
-
const doc = await readOpenApiSpec(specPath);
|
|
169
|
-
const endpoints = extractEndpoints(doc);
|
|
170
|
-
const securitySchemes = extractSecuritySchemes(doc);
|
|
171
|
-
const suites = generateSuites(endpoints, TEST_BASE, securitySchemes);
|
|
172
|
-
|
|
173
|
-
const outputDir = join(tmpDir, "generated");
|
|
174
|
-
const { written } = await writeSuites(suites, outputDir);
|
|
175
|
-
|
|
176
|
-
// Find the pets suite (has auth + CRUD)
|
|
177
|
-
const petsFile = written.find((f) => f.includes("pets"))!;
|
|
178
|
-
expect(petsFile).toBeDefined();
|
|
179
|
-
|
|
180
|
-
// Write env file with auth credentials
|
|
181
|
-
const envPath = join(outputDir, ".env.yaml");
|
|
182
|
-
await writeFile(envPath, "auth_username: admin\nauth_password: admin\n");
|
|
183
|
-
|
|
184
|
-
// Parse and run the generated tests
|
|
185
|
-
const suite = await parseFile(petsFile);
|
|
186
|
-
|
|
187
|
-
// Login step should be generated first
|
|
188
|
-
expect(suite.tests[0]!.name).toBe("Auth: Login");
|
|
189
|
-
|
|
190
|
-
// Suite headers should include Bearer auth
|
|
191
|
-
expect(suite.headers?.Authorization).toBe("Bearer {{auth_token}}");
|
|
192
|
-
|
|
193
|
-
// Load env vars
|
|
194
|
-
const envText = await Bun.file(envPath).text();
|
|
195
|
-
const env = Bun.YAML.parse(envText) as Record<string, string>;
|
|
196
|
-
|
|
197
|
-
const result = await runSuite(suite, env);
|
|
198
|
-
|
|
199
|
-
// Login should pass and capture token
|
|
200
|
-
const loginResult = result.steps[0]!;
|
|
201
|
-
expect(loginResult.status).toBe("pass");
|
|
202
|
-
expect(loginResult.captures.auth_token).toBeDefined();
|
|
203
|
-
expect(typeof loginResult.captures.auth_token).toBe("string");
|
|
204
|
-
|
|
205
|
-
// Create pet should pass (uses captured token)
|
|
206
|
-
const createResult = result.steps.find((s) => s.name === "Create a pet");
|
|
207
|
-
expect(createResult?.status).toBe("pass");
|
|
208
|
-
expect(createResult?.response?.status).toBe(201);
|
|
209
|
-
|
|
210
|
-
// List pets should pass
|
|
211
|
-
const listResult = result.steps.find((s) => s.name === "List all pets");
|
|
212
|
-
expect(listResult?.status).toBe("pass");
|
|
213
|
-
expect(listResult?.response?.status).toBe(200);
|
|
214
|
-
|
|
215
|
-
expect(result.passed).toBeGreaterThanOrEqual(3);
|
|
216
|
-
}, 30000);
|
|
159
|
+
test.skip("generate auth-aware tests from live spec, then run them", () => {
|
|
160
|
+
// skeleton.ts was removed; this test needs to be rewritten
|
|
161
|
+
});
|
|
217
162
|
});
|
|
@@ -69,7 +69,7 @@ describe("setupApi", () => {
|
|
|
69
69
|
});
|
|
70
70
|
|
|
71
71
|
test("duplicate name throws error", async () => {
|
|
72
|
-
mockFindCollectionByNameOrId.mockImplementation(() => ({ id: 1, name: "petstore" }));
|
|
72
|
+
mockFindCollectionByNameOrId.mockImplementation(() => ({ id: 1, name: "petstore" }) as any);
|
|
73
73
|
|
|
74
74
|
await expect(setupApi({
|
|
75
75
|
name: "petstore",
|