@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.
Files changed (191) hide show
  1. package/.github/workflows/ci.yml +27 -0
  2. package/.github/workflows/release.yml +97 -0
  3. package/.mcp.json +9 -0
  4. package/APITOOL.md +195 -0
  5. package/BACKLOG.md +62 -0
  6. package/CHANGELOG.md +88 -0
  7. package/LICENSE +21 -0
  8. package/README.md +105 -0
  9. package/bun.lock +291 -0
  10. package/docs/GLOSSARY.md +182 -0
  11. package/docs/INDEX.md +21 -0
  12. package/docs/agent.md +135 -0
  13. package/docs/archive/APITOOL-pre-M22.md +831 -0
  14. package/docs/archive/BACKLOG-AI-NATIVE.md +56 -0
  15. package/docs/archive/M1-M2-parser-runner.md +216 -0
  16. package/docs/archive/M4-M7-reporter-cli.md +179 -0
  17. package/docs/archive/M5-M7-storage-junit.md +300 -0
  18. package/docs/archive/M6-webui.md +339 -0
  19. package/docs/ci.md +274 -0
  20. package/docs/generation-issues.md +67 -0
  21. package/generated/.env.yaml +3 -0
  22. package/install.ps1 +80 -0
  23. package/install.sh +113 -0
  24. package/package.json +46 -0
  25. package/scripts/run-mocked-tests.ts +45 -0
  26. package/seed-demo.ts +53 -0
  27. package/self-tests/auth.yaml +18 -0
  28. package/self-tests/collections-crud.yaml +46 -0
  29. package/self-tests/environments-crud.yaml +48 -0
  30. package/self-tests/export.yaml +32 -0
  31. package/self-tests/runs.yaml +16 -0
  32. package/src/bun-types.d.ts +5 -0
  33. package/src/cli/commands/add-api.ts +51 -0
  34. package/src/cli/commands/ai-generate.ts +106 -0
  35. package/src/cli/commands/chat.ts +43 -0
  36. package/src/cli/commands/ci-init.ts +126 -0
  37. package/src/cli/commands/collections.ts +41 -0
  38. package/src/cli/commands/coverage.ts +65 -0
  39. package/src/cli/commands/doctor.ts +127 -0
  40. package/src/cli/commands/envs.ts +218 -0
  41. package/src/cli/commands/init.ts +84 -0
  42. package/src/cli/commands/mcp.ts +16 -0
  43. package/src/cli/commands/run.ts +137 -0
  44. package/src/cli/commands/runs.ts +108 -0
  45. package/src/cli/commands/serve.ts +22 -0
  46. package/src/cli/commands/update.ts +142 -0
  47. package/src/cli/commands/validate.ts +18 -0
  48. package/src/cli/index.ts +500 -0
  49. package/src/cli/output.ts +24 -0
  50. package/src/cli/runtime.ts +7 -0
  51. package/src/core/agent/agent-loop.ts +116 -0
  52. package/src/core/agent/context-manager.ts +41 -0
  53. package/src/core/agent/system-prompt.ts +33 -0
  54. package/src/core/agent/tools/diagnose-failure.ts +51 -0
  55. package/src/core/agent/tools/explore-api.ts +40 -0
  56. package/src/core/agent/tools/index.ts +48 -0
  57. package/src/core/agent/tools/manage-environment.ts +40 -0
  58. package/src/core/agent/tools/query-results.ts +40 -0
  59. package/src/core/agent/tools/run-tests.ts +38 -0
  60. package/src/core/agent/tools/send-request.ts +44 -0
  61. package/src/core/agent/tools/validate-tests.ts +23 -0
  62. package/src/core/agent/types.ts +22 -0
  63. package/src/core/generator/ai/ai-generator.ts +61 -0
  64. package/src/core/generator/ai/llm-client.ts +159 -0
  65. package/src/core/generator/ai/output-parser.ts +307 -0
  66. package/src/core/generator/ai/prompt-builder.ts +153 -0
  67. package/src/core/generator/ai/types.ts +56 -0
  68. package/src/core/generator/coverage-scanner.ts +87 -0
  69. package/src/core/generator/data-factory.ts +115 -0
  70. package/src/core/generator/index.ts +10 -0
  71. package/src/core/generator/openapi-reader.ts +142 -0
  72. package/src/core/generator/schema-utils.ts +52 -0
  73. package/src/core/generator/serializer.ts +189 -0
  74. package/src/core/generator/types.ts +47 -0
  75. package/src/core/parser/filter.ts +14 -0
  76. package/src/core/parser/index.ts +21 -0
  77. package/src/core/parser/schema.ts +175 -0
  78. package/src/core/parser/types.ts +50 -0
  79. package/src/core/parser/variables.ts +146 -0
  80. package/src/core/parser/yaml-parser.ts +85 -0
  81. package/src/core/reporter/console.ts +175 -0
  82. package/src/core/reporter/index.ts +23 -0
  83. package/src/core/reporter/json.ts +9 -0
  84. package/src/core/reporter/junit.ts +78 -0
  85. package/src/core/reporter/types.ts +12 -0
  86. package/src/core/runner/assertions.ts +172 -0
  87. package/src/core/runner/execute-run.ts +75 -0
  88. package/src/core/runner/executor.ts +150 -0
  89. package/src/core/runner/http-client.ts +69 -0
  90. package/src/core/runner/index.ts +12 -0
  91. package/src/core/runner/types.ts +48 -0
  92. package/src/core/setup-api.ts +97 -0
  93. package/src/core/utils.ts +9 -0
  94. package/src/db/queries.ts +868 -0
  95. package/src/db/schema.ts +215 -0
  96. package/src/mcp/server.ts +47 -0
  97. package/src/mcp/tools/ci-init.ts +57 -0
  98. package/src/mcp/tools/coverage-analysis.ts +58 -0
  99. package/src/mcp/tools/explore-api.ts +84 -0
  100. package/src/mcp/tools/generate-missing-tests.ts +80 -0
  101. package/src/mcp/tools/generate-tests-guide.ts +353 -0
  102. package/src/mcp/tools/manage-environment.ts +123 -0
  103. package/src/mcp/tools/manage-server.ts +87 -0
  104. package/src/mcp/tools/query-db.ts +141 -0
  105. package/src/mcp/tools/run-tests.ts +66 -0
  106. package/src/mcp/tools/save-test-suite.ts +164 -0
  107. package/src/mcp/tools/send-request.ts +53 -0
  108. package/src/mcp/tools/setup-api.ts +49 -0
  109. package/src/mcp/tools/validate-tests.ts +42 -0
  110. package/src/tui/chat-ui.ts +150 -0
  111. package/src/web/routes/api.ts +234 -0
  112. package/src/web/routes/dashboard.ts +348 -0
  113. package/src/web/routes/runs.ts +64 -0
  114. package/src/web/schemas.ts +121 -0
  115. package/src/web/server.ts +134 -0
  116. package/src/web/static/htmx.min.js +1 -0
  117. package/src/web/static/style.css +265 -0
  118. package/src/web/views/layout.ts +46 -0
  119. package/src/web/views/results.ts +209 -0
  120. package/tests/agent/agent-loop.test.ts +61 -0
  121. package/tests/agent/context-manager.test.ts +59 -0
  122. package/tests/agent/system-prompt.test.ts +42 -0
  123. package/tests/agent/tools/diagnose-failure.test.ts +85 -0
  124. package/tests/agent/tools/explore-api.test.ts +59 -0
  125. package/tests/agent/tools/manage-environment.test.ts +78 -0
  126. package/tests/agent/tools/query-results.test.ts +77 -0
  127. package/tests/agent/tools/run-tests.test.ts +89 -0
  128. package/tests/agent/tools/send-request.test.ts +78 -0
  129. package/tests/agent/tools/validate-tests.test.ts +59 -0
  130. package/tests/ai/ai-generator.integration.test.ts +131 -0
  131. package/tests/ai/llm-client.test.ts +145 -0
  132. package/tests/ai/output-parser.test.ts +132 -0
  133. package/tests/ai/prompt-builder.test.ts +67 -0
  134. package/tests/ai/types.test.ts +55 -0
  135. package/tests/cli/args.test.ts +63 -0
  136. package/tests/cli/chat.test.ts +38 -0
  137. package/tests/cli/ci-init.test.ts +112 -0
  138. package/tests/cli/commands.test.ts +316 -0
  139. package/tests/cli/coverage.test.ts +58 -0
  140. package/tests/cli/doctor.test.ts +39 -0
  141. package/tests/cli/envs.test.ts +181 -0
  142. package/tests/cli/init.test.ts +80 -0
  143. package/tests/cli/runs.test.ts +94 -0
  144. package/tests/cli/safe-run.test.ts +103 -0
  145. package/tests/cli/update.test.ts +32 -0
  146. package/tests/core/generator/schema-utils.test.ts +108 -0
  147. package/tests/core/parser/nested-assertions.test.ts +80 -0
  148. package/tests/core/runner/root-body-assertions.test.ts +70 -0
  149. package/tests/db/chat-queries.test.ts +88 -0
  150. package/tests/db/chat-schema.test.ts +37 -0
  151. package/tests/db/environments.test.ts +131 -0
  152. package/tests/db/queries.test.ts +409 -0
  153. package/tests/db/schema.test.ts +141 -0
  154. package/tests/fixtures/.env.yaml +3 -0
  155. package/tests/fixtures/auth-token-test.yaml +8 -0
  156. package/tests/fixtures/bail/suite-a.yaml +6 -0
  157. package/tests/fixtures/bail/suite-b.yaml +6 -0
  158. package/tests/fixtures/crud.yaml +35 -0
  159. package/tests/fixtures/invalid-missing-name.yaml +5 -0
  160. package/tests/fixtures/invalid-no-method.yaml +6 -0
  161. package/tests/fixtures/petstore-auth.json +295 -0
  162. package/tests/fixtures/petstore-simple.json +151 -0
  163. package/tests/fixtures/post-only.yaml +12 -0
  164. package/tests/fixtures/simple.yaml +6 -0
  165. package/tests/fixtures/valid/.env.yaml +1 -0
  166. package/tests/fixtures/valid/a.yaml +5 -0
  167. package/tests/fixtures/valid/b.yml +5 -0
  168. package/tests/generator/coverage-scanner.test.ts +129 -0
  169. package/tests/generator/data-factory.test.ts +133 -0
  170. package/tests/generator/openapi-reader.test.ts +131 -0
  171. package/tests/integration/auth-flow.test.ts +217 -0
  172. package/tests/mcp/coverage-analysis.test.ts +64 -0
  173. package/tests/mcp/explore-api-schemas.test.ts +105 -0
  174. package/tests/mcp/explore-api.test.ts +49 -0
  175. package/tests/mcp/generate-missing-tests.test.ts +69 -0
  176. package/tests/mcp/manage-environment.test.ts +89 -0
  177. package/tests/mcp/save-test-suite.test.ts +116 -0
  178. package/tests/mcp/send-request.test.ts +79 -0
  179. package/tests/mcp/setup-api.test.ts +106 -0
  180. package/tests/mcp/tools.test.ts +248 -0
  181. package/tests/parser/schema.test.ts +134 -0
  182. package/tests/parser/variables.test.ts +227 -0
  183. package/tests/parser/yaml-parser.test.ts +69 -0
  184. package/tests/reporter/console.test.ts +256 -0
  185. package/tests/reporter/json.test.ts +98 -0
  186. package/tests/reporter/junit.test.ts +284 -0
  187. package/tests/runner/assertions.test.ts +262 -0
  188. package/tests/runner/executor.test.ts +310 -0
  189. package/tests/runner/http-client.test.ts +138 -0
  190. package/tests/web/routes.test.ts +160 -0
  191. package/tsconfig.json +31 -0
@@ -0,0 +1,46 @@
1
+ let _devMode = false;
2
+
3
+ export function setDevMode(enabled: boolean): void {
4
+ _devMode = enabled;
5
+ }
6
+
7
+ export function layout(title: string, content: string): string {
8
+ const devScript = _devMode
9
+ ? `<script>new EventSource('/dev/reload').onmessage = (e) => { if (e.data === 'reload') location.reload() }</script>`
10
+ : "";
11
+ return `<!DOCTYPE html>
12
+ <html lang="en">
13
+ <head>
14
+ <meta charset="UTF-8">
15
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
16
+ <title>${escapeHtml(title)} — apitool</title>
17
+ <link rel="stylesheet" href="/static/style.css">
18
+ <script src="/static/htmx.min.js"></script>
19
+ <script>htmx.config.refreshOnHistoryMiss = true;</script>
20
+ </head>
21
+ <body>
22
+ <nav class="navbar">
23
+ <a href="/" class="nav-brand" style="text-decoration:none;color:inherit;">apitool</a>
24
+ </nav>
25
+ <main class="container">
26
+ ${content}
27
+ </main>
28
+ <footer class="footer">
29
+ <div class="container">apitool v0.1.0</div>
30
+ </footer>
31
+ ${devScript}
32
+ </body>
33
+ </html>`;
34
+ }
35
+
36
+ export function fragment(content: string): string {
37
+ return content;
38
+ }
39
+
40
+ export function escapeHtml(str: string): string {
41
+ return str
42
+ .replace(/&/g, "&amp;")
43
+ .replace(/</g, "&lt;")
44
+ .replace(/>/g, "&gt;")
45
+ .replace(/"/g, "&quot;");
46
+ }
@@ -0,0 +1,209 @@
1
+ import { escapeHtml } from "./layout.ts";
2
+ import { formatDuration } from "../../core/reporter/console.ts";
3
+ import type { StoredStepResult } from "../../db/queries.ts";
4
+
5
+ export function statusBadge(total: number, passed: number, failed: number): string {
6
+ if (total === 0) return `<span class="badge badge-skip">empty</span>`;
7
+ if (failed > 0) return `<span class="badge badge-fail">fail</span>`;
8
+ return `<span class="badge badge-pass">pass</span>`;
9
+ }
10
+
11
+ export function stepStatusBadge(status: string): string {
12
+ switch (status) {
13
+ case "pass":
14
+ return `<span class="badge badge-pass">&#10003;</span>`;
15
+ case "fail":
16
+ return `<span class="badge badge-fail">&#10007;</span>`;
17
+ case "skip":
18
+ return `<span class="badge badge-skip">&#9675;</span>`;
19
+ case "error":
20
+ return `<span class="badge badge-error">&#10007;</span>`;
21
+ default:
22
+ return `<span class="badge">${escapeHtml(status)}</span>`;
23
+ }
24
+ }
25
+
26
+ export function methodBadge(method: string): string {
27
+ const m = method.toLowerCase();
28
+ return `<span class="badge-method method-${m}">${method}</span>`;
29
+ }
30
+
31
+ /**
32
+ * Render grouped suite results with step details, captures, and chain visualization.
33
+ * Used by both the dashboard panels and the /runs/:id detail page.
34
+ */
35
+ export function renderSuiteResults(
36
+ results: StoredStepResult[],
37
+ runId: number,
38
+ options?: { idPrefix?: string; suiteMetadata?: Map<string, { description?: string; tags?: string[] }> },
39
+ ): string {
40
+ const prefix = options?.idPrefix ?? `r${runId}`;
41
+
42
+ // Group by suite
43
+ const suites = new Map<string, StoredStepResult[]>();
44
+ for (const r of results) {
45
+ const list = suites.get(r.suite_name) ?? [];
46
+ list.push(r);
47
+ suites.set(r.suite_name, list);
48
+ }
49
+
50
+ // Build capture source map
51
+ const captureSourceMap = new Map<string, string>();
52
+ for (const [, steps] of suites) {
53
+ for (const step of steps) {
54
+ if (step.captures && typeof step.captures === "object") {
55
+ for (const varName of Object.keys(step.captures)) {
56
+ captureSourceMap.set(varName, step.test_name);
57
+ }
58
+ }
59
+ }
60
+ }
61
+
62
+ let suitesHtml = "";
63
+ for (const [suiteName, steps] of suites) {
64
+ const suiteHasCaptures = steps.some(s =>
65
+ s.captures && typeof s.captures === "object" && Object.keys(s.captures).length > 0,
66
+ );
67
+ const isChainSuite = suiteHasCaptures || suiteName.endsWith("CRUD");
68
+
69
+ const stepsHtml = steps
70
+ .map((step, i) => {
71
+ const detailId = `detail-${prefix}-${i}`;
72
+ const hasFailed = step.status === "fail" || step.status === "error";
73
+
74
+ let capturesHtml = "";
75
+ if (step.captures && typeof step.captures === "object") {
76
+ const captureEntries = Object.entries(step.captures);
77
+ if (captureEntries.length > 0) {
78
+ capturesHtml = captureEntries.map(([k, v]) =>
79
+ `<span class="capture-badge">${escapeHtml(k)} = ${escapeHtml(String(v))}</span>`,
80
+ ).join(" ");
81
+ }
82
+ }
83
+
84
+ let assertionsHtml = "";
85
+ if (step.assertions.length > 0) {
86
+ const items = step.assertions
87
+ .map(
88
+ (a) =>
89
+ `<li class="${a.passed ? "assertion-pass" : "assertion-fail"}">${escapeHtml(a.field)}: ${escapeHtml(a.rule)} (got ${escapeHtml(String(a.actual))})</li>`,
90
+ )
91
+ .join("");
92
+ assertionsHtml = `<ul class="assertion-list">${items}</ul>`;
93
+ }
94
+
95
+ let requestHtml = "";
96
+ if (step.request_method) {
97
+ requestHtml = `<div><strong>Request:</strong> ${escapeHtml(step.request_method)} ${escapeHtml(step.request_url ?? "")}</div>`;
98
+ }
99
+
100
+ let reqBodyHtml = "";
101
+ if (hasFailed && step.request_body) {
102
+ reqBodyHtml = `<details class="body-details"><summary>Request Body</summary><pre>${escapeHtml(step.request_body)}</pre></details>`;
103
+ }
104
+ let resBodyHtml = "";
105
+ if (hasFailed && step.response_body) {
106
+ resBodyHtml = `<details class="body-details"><summary>Response Body</summary><pre>${escapeHtml(step.response_body)}</pre></details>`;
107
+ }
108
+
109
+ let errorHtml = "";
110
+ if (step.error_message) {
111
+ errorHtml = `<div><strong>Error:</strong> ${escapeHtml(step.error_message)}</div>`;
112
+ }
113
+
114
+ let skipReasonHtml = "";
115
+ if (step.status === "skip" && step.error_message) {
116
+ const match = step.error_message.match(/Depends on missing capture: (\w+)/);
117
+ if (match) {
118
+ const depVar = match[1]!;
119
+ const sourceStep = captureSourceMap.get(depVar);
120
+ skipReasonHtml = sourceStep
121
+ ? `<div class="skip-reason">Skipped: depends on <code>${escapeHtml(depVar)}</code> (from step "${escapeHtml(sourceStep)}")</div>`
122
+ : `<div class="skip-reason">Skipped: depends on <code>${escapeHtml(depVar)}</code></div>`;
123
+ }
124
+ }
125
+
126
+ const detailPanel = (hasFailed || skipReasonHtml)
127
+ ? `<div class="detail-panel" id="${detailId}" style="display:none">
128
+ ${requestHtml}
129
+ ${errorHtml}
130
+ ${skipReasonHtml}
131
+ ${assertionsHtml}
132
+ ${reqBodyHtml}
133
+ ${resBodyHtml}
134
+ </div>`
135
+ : "";
136
+
137
+ const toggle = (hasFailed || skipReasonHtml)
138
+ ? `onclick="var d=document.getElementById('${detailId}');d.style.display=d.style.display==='none'?'block':'none'"`
139
+ : "";
140
+
141
+ const chainedClass = isChainSuite ? " chained" : "";
142
+ const statusClass = (step.status === "fail" || step.status === "error") ? ` step-${step.status}` : "";
143
+
144
+ return `
145
+ <div class="step-row${chainedClass}${statusClass}" ${toggle}>
146
+ <div>${stepStatusBadge(step.status)}</div>
147
+ <div class="step-name">${escapeHtml(step.test_name)}${capturesHtml ? ` ${capturesHtml}` : ""}</div>
148
+ <div class="step-duration">${formatDuration(step.duration_ms)}</div>
149
+ </div>
150
+ ${detailPanel}`;
151
+ })
152
+ .join("");
153
+
154
+ const chainClass = isChainSuite ? " chain-suite" : "";
155
+
156
+ const meta = options?.suiteMetadata?.get(suiteName);
157
+ const descriptionHtml = meta?.description
158
+ ? `<p class="suite-description">${escapeHtml(meta.description)}</p>`
159
+ : "";
160
+ const tagsHtml = meta?.tags?.length
161
+ ? `<div class="suite-tags">${meta.tags.map(t => `<span class="tag-badge">${escapeHtml(t)}</span>`).join(" ")}</div>`
162
+ : "";
163
+
164
+ suitesHtml += `
165
+ <div class="suite-section${chainClass}">
166
+ <h3>${escapeHtml(suiteName)}</h3>
167
+ ${descriptionHtml}
168
+ ${tagsHtml}
169
+ ${isChainSuite ? '<div class="chain-connector">' : ""}
170
+ ${stepsHtml}
171
+ ${isChainSuite ? "</div>" : ""}
172
+ </div>`;
173
+ }
174
+
175
+ return suitesHtml;
176
+ }
177
+
178
+ /**
179
+ * Render the "show only failed" toggle + auto-expand failed steps script.
180
+ */
181
+ export function failedFilterToggle(): string {
182
+ return `
183
+ <label class="failed-filter-toggle" style="display:inline-flex;align-items:center;gap:0.5rem;font-size:0.85rem;cursor:pointer;">
184
+ <input type="checkbox" id="failed-only-toggle" onchange="
185
+ var on = this.checked;
186
+ document.querySelectorAll('.step-row').forEach(function(el) {
187
+ if (on && !el.classList.contains('step-fail') && !el.classList.contains('step-error')) {
188
+ el.style.display = 'none';
189
+ var next = el.nextElementSibling;
190
+ if (next && next.classList.contains('detail-panel')) next.style.display = 'none';
191
+ } else {
192
+ el.style.display = '';
193
+ }
194
+ });
195
+ "> Show only failed
196
+ </label>`;
197
+ }
198
+
199
+ /**
200
+ * Script to auto-expand failed step detail panels on page load.
201
+ */
202
+ export function autoExpandFailedScript(): string {
203
+ return `<script>
204
+ document.querySelectorAll('.step-row.step-fail, .step-row.step-error').forEach(function(el) {
205
+ var next = el.nextElementSibling;
206
+ if (next && next.classList.contains('detail-panel')) next.style.display = 'block';
207
+ });
208
+ </script>`;
209
+ }
@@ -0,0 +1,61 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { buildProvider } from "../../src/core/agent/agent-loop.ts";
3
+ import type { AgentConfig } from "../../src/core/agent/types.ts";
4
+
5
+ function makeConfig(overrides: Partial<AgentConfig["provider"]> = {}): AgentConfig {
6
+ return {
7
+ provider: {
8
+ provider: "ollama",
9
+ baseUrl: "http://localhost:11434/v1",
10
+ model: "qwen3:4b",
11
+ ...overrides,
12
+ },
13
+ };
14
+ }
15
+
16
+ describe("buildProvider", () => {
17
+ test("returns an OpenAI-compatible provider for ollama", () => {
18
+ const config = makeConfig({ provider: "ollama" });
19
+ const provider = buildProvider(config);
20
+ expect(typeof provider).toBe("function");
21
+ });
22
+
23
+ test("returns an OpenAI-compatible provider for openai", () => {
24
+ const config = makeConfig({
25
+ provider: "openai",
26
+ baseUrl: "https://api.openai.com/v1",
27
+ model: "gpt-4o",
28
+ apiKey: "sk-test",
29
+ });
30
+ const provider = buildProvider(config);
31
+ expect(typeof provider).toBe("function");
32
+ });
33
+
34
+ test("returns an Anthropic provider for anthropic", () => {
35
+ const config = makeConfig({
36
+ provider: "anthropic",
37
+ baseUrl: "https://api.anthropic.com",
38
+ model: "claude-sonnet-4-20250514",
39
+ apiKey: "sk-ant-test",
40
+ });
41
+ const provider = buildProvider(config);
42
+ expect(typeof provider).toBe("function");
43
+ });
44
+
45
+ test("returns an OpenAI-compatible provider for custom", () => {
46
+ const config = makeConfig({
47
+ provider: "custom",
48
+ baseUrl: "http://custom:8080/v1",
49
+ model: "my-model",
50
+ });
51
+ const provider = buildProvider(config);
52
+ expect(typeof provider).toBe("function");
53
+ });
54
+ });
55
+
56
+ describe("runAgentTurn", () => {
57
+ test("module exports runAgentTurn function", async () => {
58
+ const mod = await import("../../src/core/agent/agent-loop.ts");
59
+ expect(typeof mod.runAgentTurn).toBe("function");
60
+ });
61
+ });
@@ -0,0 +1,59 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { trimContext } from "../../src/core/agent/context-manager.ts";
3
+ import type { CoreMessageFormat } from "../../src/db/queries.ts";
4
+
5
+ function makeMessages(count: number): CoreMessageFormat[] {
6
+ const messages: CoreMessageFormat[] = [];
7
+ for (let i = 0; i < count; i++) {
8
+ messages.push({
9
+ role: i % 2 === 0 ? "user" : "assistant",
10
+ content: `Message ${i + 1}`,
11
+ });
12
+ }
13
+ return messages;
14
+ }
15
+
16
+ describe("trimContext", () => {
17
+ test("messages.length <= 20 returned unchanged", () => {
18
+ const messages = makeMessages(10);
19
+ const result = trimContext(messages);
20
+ expect(result).toHaveLength(10);
21
+ expect(result).toEqual(messages);
22
+ });
23
+
24
+ test("exactly 20 messages returned unchanged", () => {
25
+ const messages = makeMessages(20);
26
+ const result = trimContext(messages);
27
+ expect(result).toHaveLength(20);
28
+ });
29
+
30
+ test("messages.length > 20 trimmed to summary + last 6 turns", () => {
31
+ const messages = makeMessages(30);
32
+ const result = trimContext(messages);
33
+
34
+ // Should have: 1 summary message + 12 messages (6 turns = 12 messages, user+assistant)
35
+ expect(result.length).toBeLessThanOrEqual(13);
36
+ expect(result.length).toBeGreaterThan(1);
37
+
38
+ // First message should be summary with role "user" (to satisfy APIs that require user-first)
39
+ expect(result[0]!.role).toBe("user");
40
+ expect(result[0]!.content).toContain("summary");
41
+ });
42
+
43
+ test("summary contains key info from old turns", () => {
44
+ const messages = makeMessages(30);
45
+ const result = trimContext(messages);
46
+ const summary = result[0]!.content;
47
+
48
+ // Summary should mention that earlier messages existed
49
+ expect(summary.toLowerCase()).toContain("summary");
50
+ });
51
+
52
+ test("last messages are preserved exactly", () => {
53
+ const messages = makeMessages(30);
54
+ const result = trimContext(messages);
55
+ const lastOriginal = messages[messages.length - 1]!;
56
+ const lastTrimmed = result[result.length - 1]!;
57
+ expect(lastTrimmed.content).toBe(lastOriginal.content);
58
+ });
59
+ });
@@ -0,0 +1,42 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { AGENT_SYSTEM_PROMPT } from "../../src/core/agent/system-prompt.ts";
3
+
4
+ describe("AGENT_SYSTEM_PROMPT", () => {
5
+ test("is a non-empty string", () => {
6
+ expect(typeof AGENT_SYSTEM_PROMPT).toBe("string");
7
+ expect(AGENT_SYSTEM_PROMPT.length).toBeGreaterThan(100);
8
+ });
9
+
10
+ test("contains API testing context", () => {
11
+ expect(AGENT_SYSTEM_PROMPT).toContain("API testing");
12
+ });
13
+
14
+ test("mentions all tool names", () => {
15
+ const toolNames = [
16
+ "run_tests",
17
+ "validate_tests",
18
+ "generate_tests",
19
+ "query_results",
20
+ "manage_environment",
21
+ "diagnose_failure",
22
+ ];
23
+ for (const name of toolNames) {
24
+ expect(AGENT_SYSTEM_PROMPT).toContain(name);
25
+ }
26
+ });
27
+
28
+ test("mentions safe mode", () => {
29
+ expect(AGENT_SYSTEM_PROMPT).toContain("safe mode");
30
+ });
31
+
32
+ test("contains tool usage examples", () => {
33
+ expect(AGENT_SYSTEM_PROMPT).toContain("Tool usage examples");
34
+ expect(AGENT_SYSTEM_PROMPT).toContain("list_runs");
35
+ expect(AGENT_SYSTEM_PROMPT).toContain("list_collections");
36
+ });
37
+
38
+ test("contains error recovery guidance", () => {
39
+ expect(AGENT_SYSTEM_PROMPT).toContain("validation error");
40
+ expect(AGENT_SYSTEM_PROMPT).toContain("retry");
41
+ });
42
+ });
@@ -0,0 +1,85 @@
1
+ import { describe, test, expect, mock, beforeEach, afterAll } from "bun:test";
2
+
3
+ const mockGetRunById = mock((): unknown => ({
4
+ id: 1, started_at: "2024-01-01", finished_at: "2024-01-01",
5
+ total: 3, passed: 1, failed: 2, skipped: 0,
6
+ trigger: "cli", environment: "staging", duration_ms: 500,
7
+ commit_sha: null, branch: null, collection_id: null,
8
+ }));
9
+ const mockGetResultsByRunId = mock(() => [
10
+ { suite_name: "API", test_name: "GET /pets", status: "pass", duration_ms: 100 },
11
+ {
12
+ suite_name: "API", test_name: "POST /pets", status: "fail", duration_ms: 200,
13
+ error_message: "Expected 201 got 404",
14
+ request_method: "POST", request_url: "https://api.com/pets",
15
+ response_status: 404,
16
+ assertions: [{ field: "status", expected: 201, actual: 404, pass: false }],
17
+ },
18
+ {
19
+ suite_name: "API", test_name: "DELETE /pets/1", status: "error", duration_ms: 50,
20
+ error_message: "Connection refused",
21
+ request_method: "DELETE", request_url: "https://api.com/pets/1",
22
+ response_status: null,
23
+ assertions: [],
24
+ },
25
+ ]);
26
+
27
+ mock.module("../../../src/db/queries.ts", () => ({
28
+ getRunById: mockGetRunById,
29
+ getResultsByRunId: mockGetResultsByRunId,
30
+ }));
31
+
32
+ mock.module("../../../src/db/schema.ts", () => ({
33
+ getDb: mock(() => ({})),
34
+ }));
35
+
36
+ afterAll(() => { mock.restore(); });
37
+
38
+ import { diagnoseFailureTool } from "../../../src/core/agent/tools/diagnose-failure.ts";
39
+
40
+ const toolOpts = { toolCallId: "test", messages: [] as any[] };
41
+
42
+ describe("diagnoseFailureTool", () => {
43
+ beforeEach(() => {
44
+ mockGetRunById.mockClear();
45
+ mockGetResultsByRunId.mockClear();
46
+ });
47
+
48
+ test("is an AI SDK v6 tool with inputSchema", () => {
49
+ expect(diagnoseFailureTool).toHaveProperty("inputSchema");
50
+ expect(diagnoseFailureTool).toHaveProperty("execute");
51
+ });
52
+
53
+ test("returns diagnosis with failed steps", async () => {
54
+ const result = await diagnoseFailureTool.execute!({ runId: 1 }, toolOpts) as any;
55
+ expect(result).toHaveProperty("run");
56
+ expect(result).toHaveProperty("failures");
57
+ expect(result.failures).toHaveLength(2);
58
+ expect(result.failures[0].test_name).toBe("POST /pets");
59
+ expect(result.failures[0].error_message).toBe("Expected 201 got 404");
60
+ expect(result.failures[1].test_name).toBe("DELETE /pets/1");
61
+ expect(result.summary).toEqual({ total: 3, passed: 1, failed: 2 });
62
+ });
63
+
64
+ test("returns error for missing run", async () => {
65
+ mockGetRunById.mockReturnValueOnce(null);
66
+ const result = await diagnoseFailureTool.execute!({ runId: 999 }, toolOpts);
67
+ expect(result).toEqual({ error: "Run 999 not found" });
68
+ });
69
+
70
+ test("returns no failures when all passed", async () => {
71
+ mockGetRunById.mockReturnValueOnce({
72
+ id: 2, started_at: "2024-01-01", finished_at: "2024-01-01",
73
+ total: 2, passed: 2, failed: 0, skipped: 0,
74
+ trigger: "cli", environment: null, duration_ms: 200,
75
+ commit_sha: null, branch: null, collection_id: null,
76
+ });
77
+ mockGetResultsByRunId.mockReturnValueOnce([
78
+ { suite_name: "API", test_name: "GET /pets", status: "pass", duration_ms: 100 },
79
+ { suite_name: "API", test_name: "GET /pets/1", status: "pass", duration_ms: 120 },
80
+ ]);
81
+ const result = await diagnoseFailureTool.execute!({ runId: 2 }, toolOpts) as any;
82
+ expect(result.failures).toHaveLength(0);
83
+ expect(result.summary).toEqual({ total: 2, passed: 2, failed: 0 });
84
+ });
85
+ });
@@ -0,0 +1,59 @@
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" }],
7
+ paths: {},
8
+ })),
9
+ extractEndpoints: mock(() => [
10
+ { method: "GET", path: "/pets", summary: "List pets", tags: ["pets"], parameters: [], responses: [] },
11
+ { method: "POST", path: "/pets", summary: "Create pet", tags: ["pets"], parameters: [], responses: [] },
12
+ { method: "GET", path: "/users", summary: "List users", tags: ["users"], parameters: [], responses: [] },
13
+ ]),
14
+ extractSecuritySchemes: mock(() => [
15
+ { name: "bearerAuth", type: "http", scheme: "bearer" },
16
+ ]),
17
+ }));
18
+
19
+ afterAll(() => { mock.restore(); });
20
+
21
+ import { exploreApiTool } from "../../../src/core/agent/tools/explore-api.ts";
22
+ import { readOpenApiSpec } from "../../../src/core/generator/openapi-reader.ts";
23
+
24
+ const toolOpts = { toolCallId: "test", messages: [] as any[] };
25
+
26
+ describe("exploreApiTool", () => {
27
+ beforeEach(() => {
28
+ (readOpenApiSpec as ReturnType<typeof mock>).mockClear();
29
+ });
30
+
31
+ test("is an AI SDK v6 tool with inputSchema", () => {
32
+ expect(exploreApiTool).toHaveProperty("inputSchema");
33
+ expect(exploreApiTool).toHaveProperty("execute");
34
+ expect(exploreApiTool).toHaveProperty("description");
35
+ });
36
+
37
+ test("returns compact spec info", async () => {
38
+ const result = await exploreApiTool.execute!({ specPath: "petstore.yaml" }, toolOpts) as any;
39
+ expect(result.title).toBe("Pet Store");
40
+ expect(result.totalEndpoints).toBe(3);
41
+ expect(result.endpoints).toHaveLength(3);
42
+ // Compact: servers as string array, securitySchemes as name array
43
+ expect(result.servers).toEqual(["https://petstore.io"]);
44
+ expect(result.securitySchemes).toEqual(["bearerAuth"]);
45
+ });
46
+
47
+ test("filters by tag", async () => {
48
+ const result = await exploreApiTool.execute!({ specPath: "petstore.yaml", tag: "pets" }, toolOpts) as any;
49
+ expect(result.filteredByTag).toBe("pets");
50
+ expect(result.matchingEndpoints).toBe(2);
51
+ expect(result.endpoints).toHaveLength(2);
52
+ });
53
+
54
+ test("returns error on failure", async () => {
55
+ (readOpenApiSpec as ReturnType<typeof mock>).mockRejectedValueOnce(new Error("spec not found"));
56
+ const result = await exploreApiTool.execute!({ specPath: "bad.yaml" }, toolOpts) as any;
57
+ expect(result.error).toBe("spec not found");
58
+ });
59
+ });
@@ -0,0 +1,78 @@
1
+ import { describe, test, expect, mock, beforeEach, afterAll } from "bun:test";
2
+
3
+ const mockListEnvRecords = mock(() => [
4
+ { id: 1, name: "staging", variables: { base_url: "https://staging.api.com" } },
5
+ ]);
6
+ const mockGetEnv = mock((): unknown => ({ base_url: "https://staging.api.com", api_key: "sk-123" }));
7
+ const mockUpsertEnv = mock(() => {});
8
+
9
+ mock.module("../../../src/db/queries.ts", () => ({
10
+ listEnvironmentRecords: mockListEnvRecords,
11
+ getEnvironment: mockGetEnv,
12
+ upsertEnvironment: mockUpsertEnv,
13
+ }));
14
+
15
+ mock.module("../../../src/db/schema.ts", () => ({
16
+ getDb: mock(() => ({})),
17
+ }));
18
+
19
+ afterAll(() => { mock.restore(); });
20
+
21
+ import { manageEnvironmentTool } from "../../../src/core/agent/tools/manage-environment.ts";
22
+
23
+ const toolOpts = { toolCallId: "test", messages: [] as any[] };
24
+
25
+ describe("manageEnvironmentTool", () => {
26
+ beforeEach(() => {
27
+ mockListEnvRecords.mockClear();
28
+ mockGetEnv.mockClear();
29
+ mockUpsertEnv.mockClear();
30
+ });
31
+
32
+ test("is an AI SDK v6 tool with inputSchema", () => {
33
+ expect(manageEnvironmentTool).toHaveProperty("inputSchema");
34
+ expect(manageEnvironmentTool).toHaveProperty("execute");
35
+ });
36
+
37
+ test("list action returns environments", async () => {
38
+ const result = await manageEnvironmentTool.execute!({ action: "list" }, toolOpts);
39
+ expect(result).toEqual({
40
+ environments: [{ id: 1, name: "staging", variables: { base_url: "https://staging.api.com" } }],
41
+ });
42
+ });
43
+
44
+ test("get action returns environment variables", async () => {
45
+ const result = await manageEnvironmentTool.execute!({ action: "get", name: "staging" }, toolOpts);
46
+ expect(result).toEqual({
47
+ name: "staging",
48
+ variables: { base_url: "https://staging.api.com", api_key: "sk-123" },
49
+ });
50
+ expect(mockGetEnv).toHaveBeenCalledWith("staging");
51
+ });
52
+
53
+ test("get action returns error for missing environment", async () => {
54
+ mockGetEnv.mockReturnValueOnce(null);
55
+ const result = await manageEnvironmentTool.execute!({ action: "get", name: "prod" }, toolOpts);
56
+ expect(result).toEqual({ error: "Environment 'prod' not found" });
57
+ });
58
+
59
+ test("set action upserts environment", async () => {
60
+ const result = await manageEnvironmentTool.execute!({
61
+ action: "set",
62
+ name: "prod",
63
+ variables: { base_url: "https://api.com" },
64
+ }, toolOpts);
65
+ expect(result).toEqual({ success: true, name: "prod" });
66
+ expect(mockUpsertEnv).toHaveBeenCalledWith("prod", { base_url: "https://api.com" });
67
+ });
68
+
69
+ test("set action without name returns error", async () => {
70
+ const result = await manageEnvironmentTool.execute!({ action: "set" } as any, toolOpts);
71
+ expect(result).toEqual({ error: "name and variables are required for set action" });
72
+ });
73
+
74
+ test("unknown action returns error", async () => {
75
+ const result = await manageEnvironmentTool.execute!({ action: "unknown" as any }, toolOpts);
76
+ expect(result).toEqual({ error: "Unknown action: unknown" });
77
+ });
78
+ });