@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.
@@ -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
- // Parse YAML
21
- let parsed: unknown;
22
- try {
23
- parsed = YAML.parse(content);
24
- } catch (err) {
25
- return {
26
- content: [{ type: "text" as const, text: JSON.stringify({
27
- saved: false,
28
- error: `YAML parse error: ${(err as Error).message}`,
29
- hint: "Check YAML syntax — indentation, colons, and quoting",
30
- }, null, 2) }],
31
- isError: true,
32
- };
33
- }
34
-
35
- // Validate against test suite schema
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
- // Resolve path
81
- const resolvedPath = filePath.startsWith("/") || /^[a-zA-Z]:/.test(filePath)
82
- ? filePath
83
- : join(process.cwd(), filePath);
84
-
85
- // Check existing file
86
- if (!overwrite && existsSync(resolvedPath)) {
87
- return {
88
- content: [{ type: "text" as const, text: JSON.stringify({
89
- saved: false,
90
- error: `File already exists: ${resolvedPath}`,
91
- hint: "Use overwrite: true to replace the existing file",
92
- }, null, 2) }],
93
- isError: true,
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
- // Create directories
98
- const dir = dirname(resolvedPath);
99
- if (!existsSync(dir)) {
100
- mkdirSync(dir, { recursive: true });
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
- // Write original YAML content (preserve formatting/comments)
104
- await Bun.write(resolvedPath, content);
105
-
106
- // Extract summary info
107
- const suite = parsed as Record<string, unknown>;
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(result, null, 2) }],
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: CollectionSummary, envRecords: { id: number; name: string; collection_id: number | null }[]): string {
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: CollectionSummary & { openapi_spec: string }): Promise<string> {
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", () => {
@@ -6,7 +6,7 @@ function makeResponse(body: unknown, status = 200): HttpResponse {
6
6
  return {
7
7
  status,
8
8
  headers: { "content-type": "application/json" },
9
- body_raw: JSON.stringify(body),
9
+ body: JSON.stringify(body),
10
10
  body_parsed: body,
11
11
  duration_ms: 50,
12
12
  };
@@ -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(5);
8
+ expect(row.user_version).toBe(6);
9
9
  });
10
10
 
11
11
  test("chat_sessions table exists", () => {
@@ -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(5);
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", async () => {
161
- // Fetch and save spec
162
- const res = await fetch(`${TEST_BASE}/doc`);
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",