wraptc 1.0.3 → 1.0.4

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 (71) hide show
  1. package/bin/wraptc +4 -4
  2. package/package.json +2 -2
  3. package/src/cli/__tests__/cli.test.ts +337 -0
  4. package/src/cli/index.ts +149 -0
  5. package/src/core/__tests__/fixtures/configs/project-config.json +14 -0
  6. package/src/core/__tests__/fixtures/configs/system-config.json +14 -0
  7. package/src/core/__tests__/fixtures/configs/user-config.json +15 -0
  8. package/src/core/__tests__/integration/integration.test.ts +241 -0
  9. package/src/core/__tests__/integration/mock-coder-adapter.test.ts +243 -0
  10. package/src/core/__tests__/test-utils.ts +136 -0
  11. package/src/core/__tests__/unit/adapters/runner.test.ts +302 -0
  12. package/src/core/__tests__/unit/basic-test.test.ts +44 -0
  13. package/src/core/__tests__/unit/basic.test.ts +12 -0
  14. package/src/core/__tests__/unit/config.test.ts +244 -0
  15. package/src/core/__tests__/unit/error-patterns.test.ts +181 -0
  16. package/src/core/__tests__/unit/memory-monitor.test.ts +354 -0
  17. package/src/core/__tests__/unit/plugin/registry.test.ts +356 -0
  18. package/src/core/__tests__/unit/providers/codex.test.ts +173 -0
  19. package/src/core/__tests__/unit/providers/configurable.test.ts +429 -0
  20. package/src/core/__tests__/unit/providers/gemini.test.ts +251 -0
  21. package/src/core/__tests__/unit/providers/opencode.test.ts +258 -0
  22. package/src/core/__tests__/unit/providers/qwen-code.test.ts +195 -0
  23. package/src/core/__tests__/unit/providers/simple-codex.test.ts +18 -0
  24. package/src/core/__tests__/unit/router.test.ts +967 -0
  25. package/src/core/__tests__/unit/state.test.ts +1079 -0
  26. package/src/core/__tests__/unit/unified/capabilities.test.ts +186 -0
  27. package/src/core/__tests__/unit/wrap-terminalcoder.test.ts +32 -0
  28. package/src/core/adapters/builtin/codex.ts +35 -0
  29. package/src/core/adapters/builtin/gemini.ts +34 -0
  30. package/src/core/adapters/builtin/index.ts +31 -0
  31. package/src/core/adapters/builtin/mock-coder.ts +148 -0
  32. package/src/core/adapters/builtin/qwen.ts +34 -0
  33. package/src/core/adapters/define.ts +48 -0
  34. package/src/core/adapters/index.ts +43 -0
  35. package/src/core/adapters/loader.ts +143 -0
  36. package/src/core/adapters/provider-bridge.ts +190 -0
  37. package/src/core/adapters/runner.ts +437 -0
  38. package/src/core/adapters/types.ts +172 -0
  39. package/src/core/config.ts +290 -0
  40. package/src/core/define-provider.ts +212 -0
  41. package/src/core/error-patterns.ts +147 -0
  42. package/src/core/index.ts +130 -0
  43. package/src/core/memory-monitor.ts +171 -0
  44. package/src/core/plugin/builtin.ts +87 -0
  45. package/src/core/plugin/index.ts +34 -0
  46. package/src/core/plugin/registry.ts +350 -0
  47. package/src/core/plugin/types.ts +209 -0
  48. package/src/core/provider-factory.ts +397 -0
  49. package/src/core/provider-loader.ts +171 -0
  50. package/src/core/providers/codex.ts +56 -0
  51. package/src/core/providers/configurable.ts +637 -0
  52. package/src/core/providers/custom.ts +261 -0
  53. package/src/core/providers/gemini.ts +41 -0
  54. package/src/core/providers/index.ts +383 -0
  55. package/src/core/providers/opencode.ts +168 -0
  56. package/src/core/providers/qwen-code.ts +41 -0
  57. package/src/core/router.ts +370 -0
  58. package/src/core/state.ts +258 -0
  59. package/src/core/types.ts +206 -0
  60. package/src/core/unified/capabilities.ts +184 -0
  61. package/src/core/unified/errors.ts +141 -0
  62. package/src/core/unified/index.ts +29 -0
  63. package/src/core/unified/output.ts +189 -0
  64. package/src/core/wrap-terminalcoder.ts +245 -0
  65. package/src/mcp/__tests__/server.test.ts +295 -0
  66. package/src/mcp/server.ts +284 -0
  67. package/src/test-fixtures/mock-coder.sh +194 -0
  68. package/dist/cli/index.js +0 -16501
  69. package/dist/core/index.js +0 -7531
  70. package/dist/mcp/server.js +0 -14568
  71. package/dist/wraptc-1.0.3.tgz +0 -0
@@ -0,0 +1,429 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+ import { defineProvider } from "../../../define-provider";
3
+ import { ConfigurableProvider, createConfigurableProvider } from "../../../providers/configurable";
4
+ import type { CodingRequest, ProviderDefinition } from "../../../types";
5
+
6
+ // Mock Bun.spawn for provider tests
7
+ const createMockProcess = (stdout: string, stderr = "", exitCode = 0) => {
8
+ const encoder = new TextEncoder();
9
+ const stdoutData = encoder.encode(stdout);
10
+ const stderrData = encoder.encode(stderr);
11
+
12
+ return {
13
+ stdin: {
14
+ write: mock(() => {}),
15
+ end: mock(() => {}),
16
+ },
17
+ stdout: {
18
+ getReader: () => {
19
+ let read = false;
20
+ return {
21
+ read: async () => {
22
+ if (!read) {
23
+ read = true;
24
+ return { done: false, value: stdoutData };
25
+ }
26
+ return { done: true, value: undefined };
27
+ },
28
+ releaseLock: () => {},
29
+ };
30
+ },
31
+ },
32
+ stderr: {
33
+ getReader: () => {
34
+ let read = false;
35
+ return {
36
+ read: async () => {
37
+ if (!read && stderr) {
38
+ read = true;
39
+ return { done: false, value: stderrData };
40
+ }
41
+ return { done: true, value: undefined };
42
+ },
43
+ releaseLock: () => {},
44
+ };
45
+ },
46
+ },
47
+ exited: Promise.resolve(exitCode),
48
+ kill: mock(() => {}),
49
+ };
50
+ };
51
+
52
+ let mockSpawn: ReturnType<typeof mock>;
53
+ let originalSpawn: typeof Bun.spawn;
54
+
55
+ describe("ConfigurableProvider - Phase 2 Features", () => {
56
+ beforeEach(() => {
57
+ originalSpawn = Bun.spawn;
58
+ mockSpawn = mock(() => createMockProcess("test output"));
59
+ (globalThis as any).Bun.spawn = mockSpawn;
60
+ });
61
+
62
+ afterEach(() => {
63
+ (globalThis as any).Bun.spawn = originalSpawn;
64
+ });
65
+
66
+ describe("buildArgs with subcommand", () => {
67
+ test("should prepend subcommand to arguments", async () => {
68
+ const definition = defineProvider({
69
+ id: "test-cli",
70
+ binary: "test-cli",
71
+ subcommand: "exec",
72
+ input: { method: "positional" },
73
+ });
74
+
75
+ const provider = new ConfigurableProvider(definition);
76
+ const req: CodingRequest = { prompt: "Hello world", stream: false };
77
+
78
+ await provider.runOnce(req, {});
79
+
80
+ expect(mockSpawn).toHaveBeenCalled();
81
+ const spawnArgs = mockSpawn.mock.calls[0][0];
82
+ expect(spawnArgs[0]).toBe("test-cli");
83
+ expect(spawnArgs[1]).toBe("exec"); // subcommand should be first
84
+ expect(spawnArgs).toContain("Hello world");
85
+ });
86
+ });
87
+
88
+ describe("buildArgs with system prompt", () => {
89
+ test("should add system prompt flag when configured", async () => {
90
+ const definition = defineProvider({
91
+ id: "test-cli",
92
+ binary: "test-cli",
93
+ input: { method: "positional" },
94
+ args: {
95
+ systemPromptFlag: "--system",
96
+ },
97
+ });
98
+
99
+ const provider = new ConfigurableProvider(definition);
100
+ const req: CodingRequest = {
101
+ prompt: "Hello world",
102
+ systemPrompt: "You are a helpful assistant",
103
+ stream: false,
104
+ };
105
+
106
+ await provider.runOnce(req, {});
107
+
108
+ const spawnArgs = mockSpawn.mock.calls[0][0];
109
+ expect(spawnArgs).toContain("--system");
110
+ expect(spawnArgs).toContain("You are a helpful assistant");
111
+ });
112
+
113
+ test("should combine system prompt with main prompt when method is combined", async () => {
114
+ const definition = defineProvider({
115
+ id: "test-cli",
116
+ binary: "test-cli",
117
+ input: { method: "positional" },
118
+ args: {
119
+ systemPromptMethod: "combined",
120
+ },
121
+ });
122
+
123
+ const provider = new ConfigurableProvider(definition);
124
+ const req: CodingRequest = {
125
+ prompt: "Hello world",
126
+ systemPrompt: "You are a helpful assistant",
127
+ stream: false,
128
+ };
129
+
130
+ await provider.runOnce(req, {});
131
+
132
+ const spawnArgs = mockSpawn.mock.calls[0][0];
133
+ // Should contain combined prompt
134
+ expect(
135
+ spawnArgs.some(
136
+ (arg: string) =>
137
+ arg.includes("You are a helpful assistant") && arg.includes("Hello world"),
138
+ ),
139
+ ).toBe(true);
140
+ });
141
+ });
142
+
143
+ describe("buildArgs with request parameters", () => {
144
+ test("should add maxTokens flag when configured", async () => {
145
+ const definition = defineProvider({
146
+ id: "test-cli",
147
+ binary: "test-cli",
148
+ input: { method: "positional" },
149
+ args: {
150
+ maxTokensFlag: "--max-tokens",
151
+ },
152
+ });
153
+
154
+ const provider = new ConfigurableProvider(definition);
155
+ const req: CodingRequest = {
156
+ prompt: "Hello",
157
+ maxTokens: 1000,
158
+ stream: false,
159
+ };
160
+
161
+ await provider.runOnce(req, {});
162
+
163
+ const spawnArgs = mockSpawn.mock.calls[0][0];
164
+ expect(spawnArgs).toContain("--max-tokens");
165
+ expect(spawnArgs).toContain("1000");
166
+ });
167
+
168
+ test("should add temperature flag when configured", async () => {
169
+ const definition = defineProvider({
170
+ id: "test-cli",
171
+ binary: "test-cli",
172
+ input: { method: "positional" },
173
+ args: {
174
+ temperatureFlag: "--temperature",
175
+ },
176
+ });
177
+
178
+ const provider = new ConfigurableProvider(definition);
179
+ const req: CodingRequest = {
180
+ prompt: "Hello",
181
+ temperature: 0.7,
182
+ stream: false,
183
+ };
184
+
185
+ await provider.runOnce(req, {});
186
+
187
+ const spawnArgs = mockSpawn.mock.calls[0][0];
188
+ expect(spawnArgs).toContain("--temperature");
189
+ expect(spawnArgs).toContain("0.7");
190
+ });
191
+
192
+ test("should add language flag when configured", async () => {
193
+ const definition = defineProvider({
194
+ id: "test-cli",
195
+ binary: "test-cli",
196
+ input: { method: "positional" },
197
+ args: {
198
+ languageFlag: "--language",
199
+ },
200
+ });
201
+
202
+ const provider = new ConfigurableProvider(definition);
203
+ const req: CodingRequest = {
204
+ prompt: "Hello",
205
+ language: "python",
206
+ stream: false,
207
+ };
208
+
209
+ await provider.runOnce(req, {});
210
+
211
+ const spawnArgs = mockSpawn.mock.calls[0][0];
212
+ expect(spawnArgs).toContain("--language");
213
+ expect(spawnArgs).toContain("python");
214
+ });
215
+ });
216
+
217
+ describe("environment variables", () => {
218
+ test("should pass environment variables to process", async () => {
219
+ const definition = defineProvider({
220
+ id: "test-cli",
221
+ binary: "test-cli",
222
+ input: { method: "positional" },
223
+ env: {
224
+ API_KEY: "test-key",
225
+ },
226
+ });
227
+
228
+ const provider = new ConfigurableProvider(definition);
229
+ const req: CodingRequest = { prompt: "Hello", stream: false };
230
+
231
+ await provider.runOnce(req, {});
232
+
233
+ const spawnOpts = mockSpawn.mock.calls[0][1];
234
+ expect(spawnOpts.env).toBeDefined();
235
+ expect(spawnOpts.env.API_KEY).toBe("test-key");
236
+ });
237
+
238
+ test("should interpolate ${VAR} syntax from process.env", async () => {
239
+ // Set up test environment variable
240
+ process.env.TEST_API_KEY = "interpolated-value";
241
+
242
+ const definition = defineProvider({
243
+ id: "test-cli",
244
+ binary: "test-cli",
245
+ input: { method: "positional" },
246
+ env: {
247
+ API_KEY: "${TEST_API_KEY}",
248
+ },
249
+ });
250
+
251
+ const provider = new ConfigurableProvider(definition);
252
+ const req: CodingRequest = { prompt: "Hello", stream: false };
253
+
254
+ await provider.runOnce(req, {});
255
+
256
+ const spawnOpts = mockSpawn.mock.calls[0][1];
257
+ expect(spawnOpts.env.API_KEY).toBe("interpolated-value");
258
+
259
+ // Clean up
260
+ process.env.TEST_API_KEY = undefined;
261
+ });
262
+
263
+ test("should allow opts.env to override definition env", async () => {
264
+ const definition = defineProvider({
265
+ id: "test-cli",
266
+ binary: "test-cli",
267
+ input: { method: "positional" },
268
+ env: {
269
+ API_KEY: "default-key",
270
+ },
271
+ });
272
+
273
+ const provider = new ConfigurableProvider(definition);
274
+ const req: CodingRequest = { prompt: "Hello", stream: false };
275
+
276
+ await provider.runOnce(req, { env: { API_KEY: "override-key" } });
277
+
278
+ const spawnOpts = mockSpawn.mock.calls[0][1];
279
+ expect(spawnOpts.env.API_KEY).toBe("override-key");
280
+ });
281
+ });
282
+
283
+ describe("defaultCwd", () => {
284
+ test("should use defaultCwd when no opts.cwd provided", async () => {
285
+ const definition = defineProvider({
286
+ id: "test-cli",
287
+ binary: "test-cli",
288
+ input: { method: "positional" },
289
+ defaultCwd: "/tmp/test",
290
+ });
291
+
292
+ const provider = new ConfigurableProvider(definition);
293
+ const req: CodingRequest = { prompt: "Hello", stream: false };
294
+
295
+ await provider.runOnce(req, {});
296
+
297
+ const spawnOpts = mockSpawn.mock.calls[0][1];
298
+ expect(spawnOpts.cwd).toBe("/tmp/test");
299
+ });
300
+
301
+ test("should allow opts.cwd to override defaultCwd", async () => {
302
+ const definition = defineProvider({
303
+ id: "test-cli",
304
+ binary: "test-cli",
305
+ input: { method: "positional" },
306
+ defaultCwd: "/tmp/test",
307
+ });
308
+
309
+ const provider = new ConfigurableProvider(definition);
310
+ const req: CodingRequest = { prompt: "Hello", stream: false };
311
+
312
+ await provider.runOnce(req, { cwd: "/tmp/override" });
313
+
314
+ const spawnOpts = mockSpawn.mock.calls[0][1];
315
+ expect(spawnOpts.cwd).toBe("/tmp/override");
316
+ });
317
+ });
318
+
319
+ describe("allowedExitCodes", () => {
320
+ test("should treat exit code 0 as success by default", async () => {
321
+ mockSpawn = mock(() => createMockProcess("success output", "", 0));
322
+ (globalThis as any).Bun.spawn = mockSpawn;
323
+
324
+ const definition = defineProvider({
325
+ id: "test-cli",
326
+ binary: "test-cli",
327
+ input: { method: "positional" },
328
+ });
329
+
330
+ const provider = new ConfigurableProvider(definition);
331
+ const req: CodingRequest = { prompt: "Hello", stream: false };
332
+
333
+ const result = await provider.runOnce(req, {});
334
+ expect(result.text).toBe("success output");
335
+ });
336
+
337
+ test("should treat exit code 1 as failure by default", async () => {
338
+ mockSpawn = mock(() => createMockProcess("", "error", 1));
339
+ (globalThis as any).Bun.spawn = mockSpawn;
340
+
341
+ const definition = defineProvider({
342
+ id: "test-cli",
343
+ binary: "test-cli",
344
+ input: { method: "positional" },
345
+ });
346
+
347
+ const provider = new ConfigurableProvider(definition);
348
+ const req: CodingRequest = { prompt: "Hello", stream: false };
349
+
350
+ await expect(provider.runOnce(req, {})).rejects.toThrow(/failed with code 1/);
351
+ });
352
+
353
+ test("should allow custom exit codes", async () => {
354
+ mockSpawn = mock(() => createMockProcess("warning output", "", 1));
355
+ (globalThis as any).Bun.spawn = mockSpawn;
356
+
357
+ const definition = defineProvider({
358
+ id: "test-cli",
359
+ binary: "test-cli",
360
+ input: { method: "positional" },
361
+ allowedExitCodes: [0, 1], // Allow warnings
362
+ });
363
+
364
+ const provider = new ConfigurableProvider(definition);
365
+ const req: CodingRequest = { prompt: "Hello", stream: false };
366
+
367
+ const result = await provider.runOnce(req, {});
368
+ expect(result.text).toBe("warning output");
369
+ });
370
+ });
371
+
372
+ describe("error classification", () => {
373
+ test("should classify rate limit errors", () => {
374
+ const definition = defineProvider({
375
+ id: "test-cli",
376
+ binary: "test-cli",
377
+ input: { method: "positional" },
378
+ });
379
+
380
+ const provider = new ConfigurableProvider(definition);
381
+
382
+ expect(provider.classifyError({ stderr: "rate limit exceeded" })).toBe("RATE_LIMIT");
383
+ expect(provider.classifyError({ stderr: "429 Too Many Requests" })).toBe("RATE_LIMIT");
384
+ });
385
+
386
+ test("should classify auth errors", () => {
387
+ const definition = defineProvider({
388
+ id: "test-cli",
389
+ binary: "test-cli",
390
+ input: { method: "positional" },
391
+ });
392
+
393
+ const provider = new ConfigurableProvider(definition);
394
+
395
+ expect(provider.classifyError({ stderr: "401 unauthorized" })).toBe("UNAUTHORIZED");
396
+ expect(provider.classifyError({ stderr: "invalid api key" })).toBe("UNAUTHORIZED");
397
+ });
398
+
399
+ test("should use provider-specific error patterns first", () => {
400
+ const definition = defineProvider({
401
+ id: "test-cli",
402
+ binary: "test-cli",
403
+ input: { method: "positional" },
404
+ errors: {
405
+ OUT_OF_CREDITS: ["custom quota message"],
406
+ },
407
+ });
408
+
409
+ const provider = new ConfigurableProvider(definition);
410
+
411
+ expect(provider.classifyError({ stderr: "custom quota message" })).toBe("OUT_OF_CREDITS");
412
+ });
413
+ });
414
+
415
+ describe("createConfigurableProvider helper", () => {
416
+ test("should create provider from definition", () => {
417
+ const definition = defineProvider({
418
+ id: "helper-test",
419
+ binary: "helper-cli",
420
+ input: { method: "stdin" },
421
+ });
422
+
423
+ const provider = createConfigurableProvider(definition);
424
+
425
+ expect(provider).toBeInstanceOf(ConfigurableProvider);
426
+ expect(provider.id).toBe("helper-test");
427
+ });
428
+ });
429
+ });
@@ -0,0 +1,251 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
2
+ import { GeminiProvider } from "../../../providers/gemini";
3
+ import type { CodingRequest, ProviderConfig } from "../../../types";
4
+
5
+ describe("GeminiProvider", () => {
6
+ let provider: GeminiProvider;
7
+ let config: ProviderConfig;
8
+ let originalSpawn: typeof Bun.spawn;
9
+
10
+ beforeEach(() => {
11
+ config = {
12
+ binary: "gemini",
13
+ args: [],
14
+ jsonMode: "flag",
15
+ jsonFlag: "--output-format",
16
+ streamingMode: "jsonl",
17
+ capabilities: ["generate", "edit", "explain", "test"],
18
+ };
19
+ provider = new GeminiProvider(config);
20
+ originalSpawn = Bun.spawn;
21
+ });
22
+
23
+ afterEach(() => {
24
+ Bun.spawn = originalSpawn;
25
+ });
26
+
27
+ describe("constructor", () => {
28
+ test("should initialize with correct id and display name", () => {
29
+ expect(provider.id).toBe("gemini");
30
+ expect(provider.displayName).toBe("Gemini CLI");
31
+ });
32
+
33
+ test("should set binary path from config", () => {
34
+ const customBinaryConfig = { ...config, binary: "/custom/gemini" };
35
+ const customProvider = new GeminiProvider(customBinaryConfig);
36
+ expect(customProvider.binaryPath).toBe("/custom/gemini");
37
+ });
38
+
39
+ test("should set default binary if not provided", () => {
40
+ const defaultConfig = { ...config, binary: "" };
41
+ const defaultProvider = new GeminiProvider(defaultConfig);
42
+ expect(defaultProvider.binaryPath).toBe("gemini");
43
+ });
44
+
45
+ test("should set streaming support based on config", () => {
46
+ expect(provider.supportsStreaming).toBe(true);
47
+
48
+ const noStreamConfig = { ...config, streamingMode: "none" as const };
49
+ const noStreamProvider = new GeminiProvider(noStreamConfig);
50
+ expect(noStreamProvider.supportsStreaming).toBe(false);
51
+ });
52
+
53
+ test("should set JSON preference based on config", () => {
54
+ expect(provider.prefersJson).toBe(true);
55
+
56
+ const noJsonConfig = { ...config, jsonMode: "none" as const };
57
+ const noJsonProvider = new GeminiProvider(noJsonConfig);
58
+ expect(noJsonProvider.prefersJson).toBe(false);
59
+ });
60
+ });
61
+
62
+ describe("buildArgs", () => {
63
+ test("should build args with JSON flag", () => {
64
+ const req: CodingRequest = { prompt: "test prompt" };
65
+ const args = provider.buildArgs(req, {});
66
+
67
+ expect(args).toContain("-o");
68
+ expect(args).toContain("json");
69
+ expect(args).toContain("test prompt");
70
+ });
71
+
72
+ test("should include config args", () => {
73
+ const configWithArgs = { ...config, args: ["--verbose", "--model=gpt4"] };
74
+ const providerWithArgs = new GeminiProvider(configWithArgs);
75
+ const req: CodingRequest = { prompt: "test" };
76
+ const args = providerWithArgs.buildArgs(req, {});
77
+
78
+ expect(args).toContain("--verbose");
79
+ expect(args).toContain("--model=gpt4");
80
+ });
81
+
82
+ test("should skip JSON flag when jsonMode is none", () => {
83
+ const noJsonConfig = { ...config, jsonMode: "none" as const };
84
+ const noJsonProvider = new GeminiProvider(noJsonConfig);
85
+ const req: CodingRequest = { prompt: "test" };
86
+ const args = noJsonProvider.buildArgs(req, {});
87
+
88
+ expect(args).not.toContain("-o");
89
+ expect(args).not.toContain("json");
90
+ });
91
+ });
92
+
93
+ describe("getStdinInput", () => {
94
+ test("should return undefined for Gemini (uses positional args)", () => {
95
+ const result = provider.getStdinInput();
96
+ expect(result).toBeUndefined();
97
+ });
98
+ });
99
+
100
+ describe("classifyError", () => {
101
+ test("should classify OUT_OF_CREDITS errors", () => {
102
+ const errors = [
103
+ "quota exceeded",
104
+ "out of quota",
105
+ "QUOTA EXCEEDED",
106
+ "out of credits",
107
+ "insufficient quota",
108
+ ];
109
+
110
+ for (const error of errors) {
111
+ const result = provider.classifyError({ stderr: error });
112
+ expect(result).toBe("OUT_OF_CREDITS");
113
+ }
114
+ });
115
+
116
+ test("should classify RATE_LIMIT errors", () => {
117
+ const errors = ["rate limit exceeded", "Rate limit exceeded", "429", "too many requests"];
118
+
119
+ for (const error of errors) {
120
+ const result = provider.classifyError({ stderr: error });
121
+ expect(result).toBe("RATE_LIMIT");
122
+ }
123
+ });
124
+
125
+ test("should classify BAD_REQUEST errors", () => {
126
+ const errors = ["400", "Bad request", "bad request", "invalid request"];
127
+
128
+ for (const error of errors) {
129
+ const result = provider.classifyError({ stderr: error });
130
+ expect(result).toBe("BAD_REQUEST");
131
+ }
132
+ });
133
+
134
+ test("should classify UNAUTHORIZED errors", () => {
135
+ const errors = ["401", "unauthorized", "invalid api key", "authentication failed"];
136
+
137
+ for (const error of errors) {
138
+ const result = provider.classifyError({ stderr: error });
139
+ expect(result).toBe("UNAUTHORIZED");
140
+ }
141
+ });
142
+
143
+ test("should classify TIMEOUT errors", () => {
144
+ const errors = ["timeout", "timed out", "deadline exceeded"];
145
+
146
+ for (const error of errors) {
147
+ const result = provider.classifyError({ stderr: error });
148
+ expect(result).toBe("TIMEOUT");
149
+ }
150
+ });
151
+
152
+ test("should classify INTERNAL errors", () => {
153
+ const errors = ["500", "Internal server error", "internal error", "server error"];
154
+
155
+ for (const error of errors) {
156
+ const result = provider.classifyError({ stderr: error });
157
+ expect(result).toBe("INTERNAL");
158
+ }
159
+ });
160
+
161
+ test("should classify TRANSIENT for unmatched errors", () => {
162
+ const errors = [
163
+ "connection refused",
164
+ "econnrefused",
165
+ "network error",
166
+ "some unknown error xyz",
167
+ ];
168
+
169
+ for (const error of errors) {
170
+ const result = provider.classifyError({ stderr: error });
171
+ expect(result).toBe("TRANSIENT");
172
+ }
173
+ });
174
+
175
+ test("should combine stdout and stderr for classification", () => {
176
+ const result = provider.classifyError({
177
+ stderr: "some stderr",
178
+ stdout: "rate limit exceeded",
179
+ });
180
+
181
+ expect(result).toBe("RATE_LIMIT");
182
+ });
183
+
184
+ test("should handle empty error context", () => {
185
+ const result = provider.classifyError({});
186
+ expect(result).toBe("TRANSIENT");
187
+ });
188
+ });
189
+
190
+ describe("getInfo", () => {
191
+ test("should return provider information", () => {
192
+ const info = provider.getInfo();
193
+
194
+ expect(info).toEqual({
195
+ id: "gemini",
196
+ displayName: "Gemini CLI",
197
+ supportsStreaming: true,
198
+ prefersJson: true,
199
+ capabilities: ["generate", "edit", "explain", "test"],
200
+ });
201
+ });
202
+
203
+ test("should handle different streaming modes", () => {
204
+ const noStreamConfig = { ...config, streamingMode: "none" as const };
205
+ const noStreamProvider = new GeminiProvider(noStreamConfig);
206
+
207
+ const info = noStreamProvider.getInfo();
208
+ expect(info.supportsStreaming).toBe(false);
209
+ });
210
+
211
+ test("should handle different JSON modes", () => {
212
+ const noJsonConfig = { ...config, jsonMode: "none" as const };
213
+ const noJsonProvider = new GeminiProvider(noJsonConfig);
214
+
215
+ const info = noJsonProvider.getInfo();
216
+ expect(info.prefersJson).toBe(false);
217
+ });
218
+ });
219
+
220
+ describe("runOnce (integration)", () => {
221
+ test("should throw when binary not found", async () => {
222
+ // Use a non-existent binary path
223
+ const nonExistentConfig = { ...config, binary: "/nonexistent/path/to/gemini-xyz-999" };
224
+ const nonExistentProvider = new GeminiProvider(nonExistentConfig);
225
+ const req: CodingRequest = { prompt: "test" };
226
+
227
+ await expect(nonExistentProvider.runOnce(req, {})).rejects.toThrow(/Binary not found/);
228
+ });
229
+ });
230
+
231
+ describe("runStream (integration)", () => {
232
+ test("should throw when binary not found", async () => {
233
+ // Use a non-existent binary path
234
+ const nonExistentConfig = { ...config, binary: "/nonexistent/path/to/gemini-xyz-999" };
235
+ const nonExistentProvider = new GeminiProvider(nonExistentConfig);
236
+ const req: CodingRequest = { prompt: "test" };
237
+
238
+ try {
239
+ for await (const _event of nonExistentProvider.runStream(req, {})) {
240
+ // Should not get here
241
+ }
242
+ // If we get here without error, fail the test
243
+ expect(true).toBe(false);
244
+ } catch (error) {
245
+ // Expected - binary not found
246
+ expect(error).toBeDefined();
247
+ expect((error as Error).message).toMatch(/Binary not found/);
248
+ }
249
+ });
250
+ });
251
+ });