@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,132 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { parseAIResponse } from "../../src/core/generator/ai/output-parser.ts";
|
|
3
|
+
|
|
4
|
+
describe("output-parser", () => {
|
|
5
|
+
test("parses clean JSON with suites array", () => {
|
|
6
|
+
const raw = JSON.stringify({
|
|
7
|
+
suites: [
|
|
8
|
+
{
|
|
9
|
+
name: "Pet CRUD",
|
|
10
|
+
tests: [
|
|
11
|
+
{
|
|
12
|
+
name: "Create pet",
|
|
13
|
+
POST: "/pets",
|
|
14
|
+
json: { name: "Buddy", species: "dog" },
|
|
15
|
+
expect: { status: 201, body: { id: { type: "number", capture: "pet_id" } } },
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: "Get pet",
|
|
19
|
+
GET: "/pets/{{pet_id}}",
|
|
20
|
+
expect: { status: 200, body: { name: { type: "string" } } },
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const result = parseAIResponse(raw);
|
|
28
|
+
expect(result.suites.length).toBe(1);
|
|
29
|
+
expect(result.suites[0]!.name).toBe("Pet CRUD");
|
|
30
|
+
expect(result.suites[0]!.tests.length).toBe(2);
|
|
31
|
+
expect(result.yaml).toContain("name: Pet CRUD");
|
|
32
|
+
expect(result.yaml).toContain("POST: /pets");
|
|
33
|
+
expect(result.yaml).toContain("GET: /pets/{{pet_id}}");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("parses JSON wrapped in ```json fences", () => {
|
|
37
|
+
const raw = '```json\n{"suites":[{"name":"Test","tests":[{"name":"Get","GET":"/api","expect":{"status":200}}]}]}\n```';
|
|
38
|
+
const result = parseAIResponse(raw);
|
|
39
|
+
expect(result.suites.length).toBe(1);
|
|
40
|
+
expect(result.yaml).toContain("GET: /api");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("parses JSON wrapped in ``` fences (no json tag)", () => {
|
|
44
|
+
const raw = '```\n{"suites":[{"name":"Test","tests":[{"name":"Get","GET":"/api","expect":{"status":200}}]}]}\n```';
|
|
45
|
+
const result = parseAIResponse(raw);
|
|
46
|
+
expect(result.suites.length).toBe(1);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("parses JSON with leading text", () => {
|
|
50
|
+
const raw = 'Here is the generated test suite:\n\n{"suites":[{"name":"Test","tests":[{"name":"Get","GET":"/api","expect":{"status":200}}]}]}';
|
|
51
|
+
const result = parseAIResponse(raw);
|
|
52
|
+
expect(result.suites.length).toBe(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("parses single suite object (not wrapped in suites array)", () => {
|
|
56
|
+
const raw = JSON.stringify({
|
|
57
|
+
name: "Single Suite",
|
|
58
|
+
tests: [
|
|
59
|
+
{ name: "Get", GET: "/api", expect: { status: 200 } },
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
const result = parseAIResponse(raw);
|
|
63
|
+
expect(result.suites.length).toBe(1);
|
|
64
|
+
expect(result.suites[0]!.name).toBe("Single Suite");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("returns error for invalid JSON", () => {
|
|
68
|
+
const raw = "This is not JSON at all, just plain text response.";
|
|
69
|
+
const result = parseAIResponse(raw);
|
|
70
|
+
expect(result.suites.length).toBe(0);
|
|
71
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("skips suites without tests but includes valid ones", () => {
|
|
75
|
+
const raw = JSON.stringify({
|
|
76
|
+
suites: [
|
|
77
|
+
{ name: "Empty", base_url: "http://localhost" },
|
|
78
|
+
{
|
|
79
|
+
name: "Valid",
|
|
80
|
+
tests: [{ name: "Get", GET: "/api", expect: { status: 200 } }],
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
const result = parseAIResponse(raw);
|
|
85
|
+
// The empty suite is skipped, only the valid one is included
|
|
86
|
+
expect(result.suites.length).toBe(1);
|
|
87
|
+
expect(result.suites[0]!.name).toBe("Valid");
|
|
88
|
+
expect(result.errors.length).toBeGreaterThan(0); // skip message for empty suite
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("generates valid YAML with captures", () => {
|
|
92
|
+
const raw = JSON.stringify({
|
|
93
|
+
suites: [
|
|
94
|
+
{
|
|
95
|
+
name: "Chain Test",
|
|
96
|
+
base_url: "{{base_url}}",
|
|
97
|
+
tests: [
|
|
98
|
+
{
|
|
99
|
+
name: "Create",
|
|
100
|
+
POST: "/items",
|
|
101
|
+
json: { name: "test" },
|
|
102
|
+
expect: {
|
|
103
|
+
status: 201,
|
|
104
|
+
body: { id: { type: "number", capture: "item_id" } },
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "Verify",
|
|
109
|
+
GET: "/items/{{item_id}}",
|
|
110
|
+
expect: { status: 200 },
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
const result = parseAIResponse(raw);
|
|
117
|
+
expect(result.yaml).toContain("capture:");
|
|
118
|
+
expect(result.yaml).toContain("{{item_id}}");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("handles array of suites at top level", () => {
|
|
122
|
+
const raw = JSON.stringify([
|
|
123
|
+
{
|
|
124
|
+
name: "Suite A",
|
|
125
|
+
tests: [{ name: "Get", GET: "/a", expect: { status: 200 } }],
|
|
126
|
+
},
|
|
127
|
+
]);
|
|
128
|
+
const result = parseAIResponse(raw);
|
|
129
|
+
expect(result.suites.length).toBe(1);
|
|
130
|
+
expect(result.suites[0]!.name).toBe("Suite A");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { buildMessages } from "../../src/core/generator/ai/prompt-builder.ts";
|
|
3
|
+
import { readOpenApiSpec, extractEndpoints, extractSecuritySchemes } from "../../src/core/generator/openapi-reader.ts";
|
|
4
|
+
|
|
5
|
+
describe("prompt-builder", () => {
|
|
6
|
+
test("compresses petstore-simple spec to concise text", async () => {
|
|
7
|
+
const doc = await readOpenApiSpec("tests/fixtures/petstore-simple.json");
|
|
8
|
+
const endpoints = extractEndpoints(doc);
|
|
9
|
+
const securitySchemes = extractSecuritySchemes(doc);
|
|
10
|
+
|
|
11
|
+
const messages = buildMessages(endpoints, securitySchemes, "Create a pet and verify", "http://localhost:3000");
|
|
12
|
+
|
|
13
|
+
expect(messages.length).toBe(2);
|
|
14
|
+
expect(messages[0]!.role).toBe("system");
|
|
15
|
+
expect(messages[1]!.role).toBe("user");
|
|
16
|
+
|
|
17
|
+
const userMsg = messages[1]!.content;
|
|
18
|
+
|
|
19
|
+
// Should contain endpoint signatures
|
|
20
|
+
expect(userMsg).toContain("GET /pets");
|
|
21
|
+
expect(userMsg).toContain("POST /pets");
|
|
22
|
+
expect(userMsg).toContain("GET /pets/{petId}");
|
|
23
|
+
expect(userMsg).toContain("DELETE /pets/{petId}");
|
|
24
|
+
|
|
25
|
+
// Should contain response codes
|
|
26
|
+
expect(userMsg).toContain("201:");
|
|
27
|
+
expect(userMsg).toContain("409:");
|
|
28
|
+
expect(userMsg).toContain("200:");
|
|
29
|
+
|
|
30
|
+
// Should contain request body info
|
|
31
|
+
expect(userMsg).toContain("name: string");
|
|
32
|
+
expect(userMsg).toContain("species: string");
|
|
33
|
+
|
|
34
|
+
// Should contain the user prompt
|
|
35
|
+
expect(userMsg).toContain("Create a pet and verify");
|
|
36
|
+
|
|
37
|
+
// Should contain base URL
|
|
38
|
+
expect(userMsg).toContain("http://localhost:3000");
|
|
39
|
+
|
|
40
|
+
// Should be concise — well under 500 lines
|
|
41
|
+
const lines = userMsg.split("\n").length;
|
|
42
|
+
expect(lines).toBeLessThan(100);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("includes security info for auth spec", async () => {
|
|
46
|
+
const doc = await readOpenApiSpec("tests/fixtures/petstore-auth.json");
|
|
47
|
+
const endpoints = extractEndpoints(doc);
|
|
48
|
+
const securitySchemes = extractSecuritySchemes(doc);
|
|
49
|
+
|
|
50
|
+
const messages = buildMessages(endpoints, securitySchemes, "Test CRUD with auth");
|
|
51
|
+
const userMsg = messages[1]!.content;
|
|
52
|
+
|
|
53
|
+
expect(userMsg).toContain("SECURITY:");
|
|
54
|
+
expect(userMsg).toContain("bearerAuth");
|
|
55
|
+
expect(userMsg).toContain("bearer");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("system prompt defines JSON output format", async () => {
|
|
59
|
+
const messages = buildMessages([], [], "test");
|
|
60
|
+
const systemMsg = messages[0]!.content;
|
|
61
|
+
|
|
62
|
+
expect(systemMsg).toContain("suites");
|
|
63
|
+
expect(systemMsg).toContain("capture");
|
|
64
|
+
expect(systemMsg).toContain("expect");
|
|
65
|
+
expect(systemMsg).toContain("JSON");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { resolveProviderConfig, PROVIDER_DEFAULTS } from "../../src/core/generator/ai/types.ts";
|
|
3
|
+
|
|
4
|
+
describe("AI types", () => {
|
|
5
|
+
test("resolveProviderConfig fills ollama defaults", () => {
|
|
6
|
+
const config = resolveProviderConfig({ provider: "ollama" });
|
|
7
|
+
expect(config.baseUrl).toBe("http://localhost:11434/v1");
|
|
8
|
+
expect(config.model).toBe("qwen3:4b");
|
|
9
|
+
expect(config.temperature).toBe(0.2);
|
|
10
|
+
expect(config.maxTokens).toBe(4096);
|
|
11
|
+
expect(config.apiKey).toBeUndefined();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("resolveProviderConfig fills openai defaults", () => {
|
|
15
|
+
const config = resolveProviderConfig({ provider: "openai", apiKey: "sk-test" });
|
|
16
|
+
expect(config.baseUrl).toBe("https://api.openai.com/v1");
|
|
17
|
+
expect(config.model).toBe("gpt-4o");
|
|
18
|
+
expect(config.apiKey).toBe("sk-test");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("resolveProviderConfig fills anthropic defaults", () => {
|
|
22
|
+
const config = resolveProviderConfig({ provider: "anthropic" });
|
|
23
|
+
expect(config.baseUrl).toBe("https://api.anthropic.com");
|
|
24
|
+
expect(config.model).toBe("claude-sonnet-4-20250514");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("resolveProviderConfig allows overrides", () => {
|
|
28
|
+
const config = resolveProviderConfig({
|
|
29
|
+
provider: "ollama",
|
|
30
|
+
baseUrl: "http://custom:1234/v1",
|
|
31
|
+
model: "custom-model",
|
|
32
|
+
temperature: 0.8,
|
|
33
|
+
maxTokens: 2048,
|
|
34
|
+
});
|
|
35
|
+
expect(config.baseUrl).toBe("http://custom:1234/v1");
|
|
36
|
+
expect(config.model).toBe("custom-model");
|
|
37
|
+
expect(config.temperature).toBe(0.8);
|
|
38
|
+
expect(config.maxTokens).toBe(2048);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("custom provider uses empty defaults", () => {
|
|
42
|
+
const config = resolveProviderConfig({
|
|
43
|
+
provider: "custom",
|
|
44
|
+
baseUrl: "http://my-llm/v1",
|
|
45
|
+
model: "my-model",
|
|
46
|
+
});
|
|
47
|
+
expect(config.provider).toBe("custom");
|
|
48
|
+
expect(config.baseUrl).toBe("http://my-llm/v1");
|
|
49
|
+
expect(config.model).toBe("my-model");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("PROVIDER_DEFAULTS has expected keys", () => {
|
|
53
|
+
expect(Object.keys(PROVIDER_DEFAULTS)).toEqual(["ollama", "openai", "anthropic"]);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { parseArgs } from "../../src/cli/index.ts";
|
|
3
|
+
|
|
4
|
+
// parseArgs expects full argv: [bunPath, scriptPath, ...userArgs]
|
|
5
|
+
function parse(...userArgs: string[]) {
|
|
6
|
+
return parseArgs(["bun", "script.ts", ...userArgs]);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("parseArgs", () => {
|
|
10
|
+
test("run command with path", () => {
|
|
11
|
+
const result = parse("run", "tests/");
|
|
12
|
+
expect(result.command).toBe("run");
|
|
13
|
+
expect(result.positional).toEqual(["tests/"]);
|
|
14
|
+
expect(result.flags).toEqual({});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("run with all flags", () => {
|
|
18
|
+
const result = parse("run", "test.yaml", "--env", "staging", "--report", "json", "--timeout", "5000", "--bail");
|
|
19
|
+
expect(result.command).toBe("run");
|
|
20
|
+
expect(result.positional).toEqual(["test.yaml"]);
|
|
21
|
+
expect(result.flags).toEqual({
|
|
22
|
+
env: "staging",
|
|
23
|
+
report: "json",
|
|
24
|
+
timeout: "5000",
|
|
25
|
+
bail: true,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("validate command", () => {
|
|
30
|
+
const result = parse("validate", "test.yaml");
|
|
31
|
+
expect(result.command).toBe("validate");
|
|
32
|
+
expect(result.positional).toEqual(["test.yaml"]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("--help flag", () => {
|
|
36
|
+
const result = parse("--help");
|
|
37
|
+
expect(result.command).toBe(undefined);
|
|
38
|
+
expect(result.flags["help"]).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("-h short flag", () => {
|
|
42
|
+
const result = parse("-h");
|
|
43
|
+
expect(result.command).toBe(undefined);
|
|
44
|
+
expect(result.flags["h"]).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("no arguments", () => {
|
|
48
|
+
const result = parse();
|
|
49
|
+
expect(result.command).toBe(undefined);
|
|
50
|
+
expect(result.positional).toEqual([]);
|
|
51
|
+
expect(result.flags).toEqual({});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("--flag=value syntax", () => {
|
|
55
|
+
const result = parse("run", "tests/", "--report=json");
|
|
56
|
+
expect(result.flags["report"]).toBe("json");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("unknown command captured as command", () => {
|
|
60
|
+
const result = parse("foobar");
|
|
61
|
+
expect(result.command).toBe("foobar");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { parseArgs } from "../../src/cli/index.ts";
|
|
3
|
+
|
|
4
|
+
function parse(...userArgs: string[]) {
|
|
5
|
+
return parseArgs(["bun", "script.ts", ...userArgs]);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
describe("chat command parsing", () => {
|
|
9
|
+
test("basic chat command", () => {
|
|
10
|
+
const result = parse("chat");
|
|
11
|
+
expect(result.command).toBe("chat");
|
|
12
|
+
expect(result.positional).toEqual([]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("chat with --provider openai --model gpt-4o", () => {
|
|
16
|
+
const result = parse("chat", "--provider", "openai", "--model", "gpt-4o");
|
|
17
|
+
expect(result.command).toBe("chat");
|
|
18
|
+
expect(result.flags["provider"]).toBe("openai");
|
|
19
|
+
expect(result.flags["model"]).toBe("gpt-4o");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("chat with --safe flag", () => {
|
|
23
|
+
const result = parse("chat", "--safe");
|
|
24
|
+
expect(result.command).toBe("chat");
|
|
25
|
+
expect(result.flags["safe"]).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("chat with --api-key", () => {
|
|
29
|
+
const result = parse("chat", "--provider", "anthropic", "--api-key", "sk-ant-test");
|
|
30
|
+
expect(result.flags["provider"]).toBe("anthropic");
|
|
31
|
+
expect(result.flags["api-key"]).toBe("sk-ant-test");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("default provider is handled by command implementation", () => {
|
|
35
|
+
const result = parse("chat");
|
|
36
|
+
expect(result.flags["provider"]).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test";
|
|
2
|
+
import { mkdtempSync, existsSync, readFileSync, writeFileSync, mkdirSync, rmSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
import { ciInitCommand } from "../../src/cli/commands/ci-init.ts";
|
|
6
|
+
|
|
7
|
+
function suppressOutput() {
|
|
8
|
+
const origOut = process.stdout.write;
|
|
9
|
+
const origErr = process.stderr.write;
|
|
10
|
+
process.stdout.write = mock(() => true) as typeof process.stdout.write;
|
|
11
|
+
process.stderr.write = mock(() => true) as typeof process.stderr.write;
|
|
12
|
+
return () => {
|
|
13
|
+
process.stdout.write = origOut;
|
|
14
|
+
process.stderr.write = origErr;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("apitool ci init", () => {
|
|
19
|
+
let tmpDir: string;
|
|
20
|
+
let origCwd: string;
|
|
21
|
+
let restoreOutput: () => void;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
tmpDir = mkdtempSync(join(tmpdir(), "apitool-ci-init-"));
|
|
25
|
+
origCwd = process.cwd();
|
|
26
|
+
process.chdir(tmpDir);
|
|
27
|
+
restoreOutput = suppressOutput();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
process.chdir(origCwd);
|
|
32
|
+
restoreOutput();
|
|
33
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("--github creates GitHub Actions workflow", async () => {
|
|
37
|
+
const code = await ciInitCommand({ platform: "github", force: false });
|
|
38
|
+
expect(code).toBe(0);
|
|
39
|
+
|
|
40
|
+
const filePath = join(tmpDir, ".github/workflows/api-tests.yml");
|
|
41
|
+
expect(existsSync(filePath)).toBe(true);
|
|
42
|
+
|
|
43
|
+
const content = readFileSync(filePath, "utf-8");
|
|
44
|
+
expect(content).toContain("actions/checkout@v4");
|
|
45
|
+
expect(content).toContain("apitool run");
|
|
46
|
+
expect(content).toContain("--report junit");
|
|
47
|
+
expect(content).toContain("install.sh");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("--gitlab creates GitLab CI config", async () => {
|
|
51
|
+
const code = await ciInitCommand({ platform: "gitlab", force: false });
|
|
52
|
+
expect(code).toBe(0);
|
|
53
|
+
|
|
54
|
+
const filePath = join(tmpDir, ".gitlab-ci.yml");
|
|
55
|
+
expect(existsSync(filePath)).toBe(true);
|
|
56
|
+
|
|
57
|
+
const content = readFileSync(filePath, "utf-8");
|
|
58
|
+
expect(content).toContain("api-tests:");
|
|
59
|
+
expect(content).toContain("apitool run");
|
|
60
|
+
expect(content).toContain("junit: test-results/junit.xml");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("auto-detects GitHub when .github/ exists", async () => {
|
|
64
|
+
mkdirSync(join(tmpDir, ".github"));
|
|
65
|
+
|
|
66
|
+
const code = await ciInitCommand({ force: false });
|
|
67
|
+
expect(code).toBe(0);
|
|
68
|
+
|
|
69
|
+
expect(existsSync(join(tmpDir, ".github/workflows/api-tests.yml"))).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("auto-detects GitLab when .gitlab-ci.yml exists", async () => {
|
|
73
|
+
writeFileSync(join(tmpDir, ".gitlab-ci.yml"), "# existing\n");
|
|
74
|
+
|
|
75
|
+
const code = await ciInitCommand({ platform: "gitlab", force: true });
|
|
76
|
+
expect(code).toBe(0);
|
|
77
|
+
|
|
78
|
+
const content = readFileSync(join(tmpDir, ".gitlab-ci.yml"), "utf-8");
|
|
79
|
+
expect(content).toContain("api-tests:");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("defaults to GitHub when no platform detected", async () => {
|
|
83
|
+
const code = await ciInitCommand({ force: false });
|
|
84
|
+
expect(code).toBe(0);
|
|
85
|
+
|
|
86
|
+
expect(existsSync(join(tmpDir, ".github/workflows/api-tests.yml"))).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("skips existing file without --force", async () => {
|
|
90
|
+
const dir = join(tmpDir, ".github/workflows");
|
|
91
|
+
mkdirSync(dir, { recursive: true });
|
|
92
|
+
writeFileSync(join(dir, "api-tests.yml"), "original: true\n");
|
|
93
|
+
|
|
94
|
+
const code = await ciInitCommand({ platform: "github", force: false });
|
|
95
|
+
expect(code).toBe(0);
|
|
96
|
+
|
|
97
|
+
const content = readFileSync(join(dir, "api-tests.yml"), "utf-8");
|
|
98
|
+
expect(content).toBe("original: true\n");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("--force overwrites existing file", async () => {
|
|
102
|
+
const dir = join(tmpDir, ".github/workflows");
|
|
103
|
+
mkdirSync(dir, { recursive: true });
|
|
104
|
+
writeFileSync(join(dir, "api-tests.yml"), "original: true\n");
|
|
105
|
+
|
|
106
|
+
const code = await ciInitCommand({ platform: "github", force: true });
|
|
107
|
+
expect(code).toBe(0);
|
|
108
|
+
|
|
109
|
+
const content = readFileSync(join(dir, "api-tests.yml"), "utf-8");
|
|
110
|
+
expect(content).toContain("actions/checkout@v4");
|
|
111
|
+
});
|
|
112
|
+
});
|