wraptc 1.0.2 → 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.2.tgz +0 -0
@@ -0,0 +1,241 @@
1
+ import { describe, expect, mock, test } from "bun:test";
2
+ import { mkdir, unlink, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { ConfigLoader } from "../../config";
5
+ import { Router } from "../../router";
6
+ import { StateManager } from "../../state";
7
+ import type { CodingRequest } from "../../types";
8
+
9
+ // Mock Provider for testing
10
+ class MockProvider {
11
+ readonly id: string;
12
+ readonly displayName: string;
13
+ readonly supportsStreaming: boolean = false;
14
+ readonly prefersJson: boolean = false;
15
+ readonly capabilities?: string[] = ["generate", "edit", "explain", "test"];
16
+
17
+ private responseText: string;
18
+ private shouldFail = false;
19
+
20
+ constructor(id: string, displayName: string, responseText: string) {
21
+ this.id = id;
22
+ this.displayName = displayName;
23
+ this.responseText = responseText;
24
+ }
25
+
26
+ setShouldFail(fail: boolean) {
27
+ this.shouldFail = fail;
28
+ }
29
+
30
+ async runOnce(req: CodingRequest, opts: any): Promise<{ text: string; usage?: any }> {
31
+ if (this.shouldFail) {
32
+ throw new Error(`${this.id} failed`);
33
+ }
34
+ return {
35
+ text: this.responseText,
36
+ usage: { inputTokens: 10, outputTokens: 20 },
37
+ };
38
+ }
39
+
40
+ async *runStream(req: CodingRequest, opts: any): AsyncGenerator<any> {
41
+ if (this.shouldFail) {
42
+ throw new Error(`${this.id} failed`);
43
+ }
44
+ yield {
45
+ type: "complete",
46
+ text: this.responseText,
47
+ usage: { inputTokens: 10, outputTokens: 20 },
48
+ };
49
+ }
50
+
51
+ classifyError(error: any): import("../../types").ProviderErrorKind {
52
+ return "TRANSIENT";
53
+ }
54
+
55
+ getInfo(): import("../../types").ProviderInfo {
56
+ return {
57
+ id: this.id,
58
+ displayName: this.displayName,
59
+ supportsStreaming: this.supportsStreaming,
60
+ prefersJson: this.prefersJson,
61
+ capabilities: this.capabilities,
62
+ };
63
+ }
64
+ }
65
+
66
+ describe("Integration Tests", () => {
67
+ test("Router and StateManager integration", async () => {
68
+ // Create components with proper integration
69
+ const providers = new Map([
70
+ ["provider1", new MockProvider("provider1", "Provider 1", "Response from provider1")],
71
+ ["provider2", new MockProvider("provider2", "Provider 2", "Response from provider2")],
72
+ ]);
73
+
74
+ const config = {
75
+ routing: { defaultOrder: ["provider1", "provider2"] },
76
+ providers: {},
77
+ credits: { providers: {} },
78
+ };
79
+
80
+ const stateManager = new StateManager();
81
+ await stateManager.initialize();
82
+
83
+ const router = new Router(providers, {
84
+ config,
85
+ stateManager,
86
+ });
87
+
88
+ // Test basic routing
89
+ const request: CodingRequest = {
90
+ prompt: "Test prompt",
91
+ mode: "generate",
92
+ stream: false,
93
+ };
94
+
95
+ const response = await router.route(request);
96
+
97
+ expect(response.provider).toBe("provider1");
98
+ expect(response.text).toBe("Response from provider1");
99
+ expect(response.usage).toEqual({ inputTokens: 10, outputTokens: 20 });
100
+ });
101
+
102
+ test("Provider fallback on failure", async () => {
103
+ // Create providers where first one fails
104
+ const provider1 = new MockProvider("provider1", "Provider 1", "Response from provider1");
105
+ provider1.setShouldFail(true);
106
+
107
+ const provider2 = new MockProvider("provider2", "Provider 2", "Response from provider2");
108
+
109
+ const providers = new Map([
110
+ ["provider1", provider1],
111
+ ["provider2", provider2],
112
+ ]);
113
+
114
+ const config = {
115
+ routing: { defaultOrder: ["provider1", "provider2"] },
116
+ providers: {},
117
+ credits: { providers: {} },
118
+ };
119
+
120
+ const stateManager = new StateManager();
121
+ await stateManager.initialize();
122
+
123
+ const router = new Router(providers, {
124
+ config,
125
+ stateManager,
126
+ });
127
+
128
+ const request: CodingRequest = {
129
+ prompt: "Test prompt",
130
+ mode: "generate",
131
+ stream: false,
132
+ };
133
+
134
+ const response = await router.route(request);
135
+
136
+ // Should fallback to provider2
137
+ expect(response.provider).toBe("provider2");
138
+ expect(response.text).toBe("Response from provider2");
139
+ });
140
+
141
+ test("Config loading and provider configuration", async () => {
142
+ // Create temporary config file
143
+ const tempDir = `/tmp/test-${Date.now()}`;
144
+ await mkdir(tempDir, { recursive: true });
145
+ const configPath = join(tempDir, "config.json");
146
+
147
+ const testConfig = {
148
+ routing: { defaultOrder: ["test-provider"] },
149
+ providers: {
150
+ "test-provider": {
151
+ binary: "test-binary",
152
+ args: ["--test"],
153
+ jsonMode: "none",
154
+ streamingMode: "line",
155
+ capabilities: ["generate"],
156
+ },
157
+ },
158
+ credits: { providers: {} },
159
+ };
160
+
161
+ await writeFile(configPath, JSON.stringify(testConfig, null, 2));
162
+
163
+ // Load config
164
+ const configLoader = new ConfigLoader({ projectConfigPath: configPath });
165
+ const config = await configLoader.loadConfig();
166
+
167
+ // Verify config loaded correctly
168
+ expect(config.routing.defaultOrder).toEqual(["test-provider"]);
169
+ expect(config.providers["test-provider"]).toBeDefined();
170
+ expect(config.providers["test-provider"].binary).toBe("test-binary");
171
+
172
+ // Cleanup
173
+ await unlink(configPath);
174
+ await unlink(tempDir).catch(() => {});
175
+ });
176
+
177
+ test.skip("State persistence across sessions - SKIP: timing issues", async () => {
178
+ const tempDir = `/tmp/test-${Date.now()}`;
179
+ await mkdir(tempDir, { recursive: true });
180
+ const statePath = join(tempDir, "state.json");
181
+
182
+ // First session - record success
183
+ const stateManager1 = new StateManager({ statePath });
184
+ await stateManager1.initialize();
185
+ await stateManager1.recordSuccess("test-provider");
186
+ await stateManager1.save(); // Force immediate save
187
+
188
+ // Second session - load state
189
+ const stateManager2 = new StateManager({ statePath });
190
+ await stateManager2.initialize();
191
+ const providerState = await stateManager2.getProviderState("test-provider");
192
+
193
+ // Verify state persisted (should be 1 since we recorded in first session)
194
+ expect(providerState.requestsToday).toBe(1);
195
+
196
+ // Cleanup
197
+ await unlink(statePath);
198
+ await unlink(tempDir).catch(() => {});
199
+ });
200
+
201
+ test("Full routing with real components", async () => {
202
+ const providers = new Map([
203
+ ["primary", new MockProvider("primary", "Primary Provider", "Primary response")],
204
+ ["secondary", new MockProvider("secondary", "Secondary Provider", "Secondary response")],
205
+ ]);
206
+
207
+ const config = {
208
+ routing: { defaultOrder: ["primary", "secondary"] },
209
+ providers: {},
210
+ credits: {
211
+ providers: {
212
+ primary: { dailyRequestLimit: 100, resetHourUtc: 0 },
213
+ secondary: { dailyRequestLimit: 50, resetHourUtc: 0 },
214
+ },
215
+ },
216
+ };
217
+
218
+ const stateManager = new StateManager();
219
+ await stateManager.initialize();
220
+
221
+ const router = new Router(providers, {
222
+ config,
223
+ stateManager,
224
+ });
225
+
226
+ // Test multiple requests
227
+ const requests: CodingRequest[] = [
228
+ { prompt: "Request 1", mode: "generate", stream: false },
229
+ { prompt: "Request 2", mode: "edit", stream: false },
230
+ { prompt: "Request 3", mode: "explain", stream: false },
231
+ ];
232
+
233
+ const responses = await Promise.all(requests.map((req) => router.route(req)));
234
+
235
+ // All should use primary provider
236
+ responses.forEach((response) => {
237
+ expect(response.provider).toBe("primary");
238
+ expect(response.text).toBe("Primary response");
239
+ });
240
+ });
241
+ });
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Mock Coder Adapter Integration Tests
3
+ *
4
+ * Tests the full adapter system end-to-end using a bash script mock coder.
5
+ * This validates that the adapter wrapper can properly:
6
+ * - Execute the underlying binary
7
+ * - Pass input via stdin, positional args, or flags
8
+ * - Parse output (text or JSON)
9
+ * - Handle streaming output
10
+ * - Classify errors correctly
11
+ */
12
+
13
+ import { beforeAll, describe, expect, test } from "bun:test";
14
+ import {
15
+ mockCoderFlag,
16
+ mockCoderJson,
17
+ mockCoderPositional,
18
+ mockCoderRateLimit,
19
+ mockCoderStdin,
20
+ mockCoderStreamJson,
21
+ mockCoderStreamText,
22
+ mockCoderUnauthorized,
23
+ } from "../../adapters/builtin/mock-coder";
24
+ import { AdapterProviderBridge } from "../../adapters/provider-bridge";
25
+ import { AdapterError, AdapterRunner } from "../../adapters/runner";
26
+
27
+ // Ensure mock-coder.sh is executable
28
+ beforeAll(async () => {
29
+ const mockCoderPath = new URL("../../../test-fixtures/mock-coder.sh", import.meta.url).pathname;
30
+
31
+ // Make executable if not already
32
+ const proc = Bun.spawn(["chmod", "+x", mockCoderPath]);
33
+ await proc.exited;
34
+ });
35
+
36
+ describe("Mock Coder Adapter - Input Methods", () => {
37
+ test("should handle stdin input", async () => {
38
+ const runner = new AdapterRunner(mockCoderStdin);
39
+ const result = await runner.run("hello world", {});
40
+
41
+ expect(result.text).toContain("Mock Coder");
42
+ expect(result.text).toContain("Hello");
43
+ });
44
+
45
+ test("should handle positional input", async () => {
46
+ const runner = new AdapterRunner(mockCoderPositional);
47
+ const result = await runner.run("hello", {});
48
+
49
+ expect(result.text).toContain("Mock Coder");
50
+ });
51
+
52
+ test("should handle flag input", async () => {
53
+ const runner = new AdapterRunner(mockCoderFlag);
54
+ const result = await runner.run("hello", {});
55
+
56
+ expect(result.text).toContain("Mock Coder");
57
+ });
58
+ });
59
+
60
+ describe("Mock Coder Adapter - Output Formats", () => {
61
+ test("should parse text output", async () => {
62
+ const runner = new AdapterRunner(mockCoderPositional);
63
+ const result = await runner.run("explain something", {});
64
+
65
+ expect(result.text).toBeTruthy();
66
+ expect(typeof result.text).toBe("string");
67
+ });
68
+
69
+ test("should parse JSON output", async () => {
70
+ const runner = new AdapterRunner(mockCoderJson);
71
+ const result = await runner.run("hello", {});
72
+
73
+ expect(result.text).toContain("Mock Coder");
74
+ expect(result.usage).toBeDefined();
75
+ expect(result.usage?.inputTokens).toBeGreaterThan(0);
76
+ expect(result.usage?.outputTokens).toBeGreaterThan(0);
77
+ });
78
+ });
79
+
80
+ describe("Mock Coder Adapter - Streaming", () => {
81
+ test("should stream text output line by line", async () => {
82
+ const runner = new AdapterRunner(mockCoderStreamText);
83
+ const chunks: string[] = [];
84
+
85
+ for await (const chunk of runner.runStream("hello", {})) {
86
+ if (chunk.type === "text") {
87
+ chunks.push(chunk.content);
88
+ }
89
+ }
90
+
91
+ expect(chunks.length).toBeGreaterThan(0);
92
+ const fullText = chunks.join("");
93
+ expect(fullText).toContain("Mock Coder");
94
+ });
95
+
96
+ test("should stream JSON output as JSONL", async () => {
97
+ const runner = new AdapterRunner(mockCoderStreamJson);
98
+ const chunks: unknown[] = [];
99
+
100
+ for await (const chunk of runner.runStream("hello", {})) {
101
+ if (chunk.type === "json") {
102
+ chunks.push(chunk.data);
103
+ }
104
+ }
105
+
106
+ expect(chunks.length).toBeGreaterThan(0);
107
+ });
108
+ });
109
+
110
+ describe("Mock Coder Adapter - Error Handling", () => {
111
+ test("should classify rate limit error", async () => {
112
+ const runner = new AdapterRunner(mockCoderRateLimit);
113
+
114
+ try {
115
+ await runner.run("test", {});
116
+ expect.unreachable("Should have thrown");
117
+ } catch (error) {
118
+ expect(error).toBeInstanceOf(AdapterError);
119
+ const adapterError = error as AdapterError;
120
+ expect(adapterError.kind).toBe("RATE_LIMIT");
121
+ expect(adapterError.isRetryable).toBe(true);
122
+ }
123
+ });
124
+
125
+ test("should classify unauthorized error", async () => {
126
+ const runner = new AdapterRunner(mockCoderUnauthorized);
127
+
128
+ try {
129
+ await runner.run("test", {});
130
+ expect.unreachable("Should have thrown");
131
+ } catch (error) {
132
+ expect(error).toBeInstanceOf(AdapterError);
133
+ const adapterError = error as AdapterError;
134
+ expect(adapterError.kind).toBe("UNAUTHORIZED");
135
+ expect(adapterError.isRetryable).toBe(false);
136
+ expect(adapterError.isUserError).toBe(true);
137
+ }
138
+ });
139
+ });
140
+
141
+ describe("Mock Coder Adapter - Provider Bridge", () => {
142
+ test("should work through provider bridge (runOnce)", async () => {
143
+ const bridge = new AdapterProviderBridge(mockCoderPositional);
144
+
145
+ expect(bridge.id).toBe("mock-coder-positional");
146
+ expect(bridge.displayName).toBe("Mock Coder (positional)");
147
+
148
+ const result = await bridge.runOnce({ prompt: "hello", mode: "generate" }, {});
149
+
150
+ expect(result.text).toContain("Mock Coder");
151
+ });
152
+
153
+ test("should work through provider bridge (runStream)", async () => {
154
+ const bridge = new AdapterProviderBridge(mockCoderStreamText);
155
+
156
+ expect(bridge.supportsStreaming).toBe(true);
157
+
158
+ const events: string[] = [];
159
+ for await (const event of bridge.runStream({ prompt: "hello", mode: "generate" }, {})) {
160
+ events.push(event.type);
161
+ if (event.type === "text_delta") {
162
+ expect(event.text).toBeTruthy();
163
+ }
164
+ }
165
+
166
+ expect(events).toContain("start");
167
+ expect(events).toContain("text_delta");
168
+ expect(events).toContain("complete");
169
+ });
170
+
171
+ test("should classify errors through bridge", () => {
172
+ const bridge = new AdapterProviderBridge(mockCoderStdin);
173
+
174
+ expect(
175
+ bridge.classifyError({
176
+ error: new Error("Rate limit exceeded"),
177
+ stderr: "Error 429: Rate limit exceeded",
178
+ exitCode: 1,
179
+ }),
180
+ ).toBe("RATE_LIMIT");
181
+
182
+ expect(
183
+ bridge.classifyError({
184
+ error: new Error("Auth failed"),
185
+ stderr: "Error 401: Unauthorized",
186
+ exitCode: 1,
187
+ }),
188
+ ).toBe("UNAUTHORIZED");
189
+ });
190
+
191
+ test("should report capabilities through getInfo", () => {
192
+ const bridge = new AdapterProviderBridge(mockCoderStdin);
193
+ const info = bridge.getInfo();
194
+
195
+ expect(info.capabilities).toContain("generate");
196
+ expect(info.capabilities).toContain("edit");
197
+ expect(info.capabilities).toContain("explain");
198
+ expect(info.supportsStreaming).toBe(false);
199
+ });
200
+ });
201
+
202
+ describe("Mock Coder Adapter - Different Prompts", () => {
203
+ test("should handle code generation prompt", async () => {
204
+ const runner = new AdapterRunner(mockCoderPositional);
205
+ const result = await runner.run("write a function to greet", {});
206
+
207
+ expect(result.text).toContain("function");
208
+ expect(result.text).toContain("greet");
209
+ });
210
+
211
+ test("should handle explanation prompt", async () => {
212
+ const runner = new AdapterRunner(mockCoderPositional);
213
+ const result = await runner.run("explain this code", {});
214
+
215
+ expect(result.text).toContain("code does the following");
216
+ });
217
+
218
+ test("should handle generic prompt", async () => {
219
+ const runner = new AdapterRunner(mockCoderPositional);
220
+ const result = await runner.run("do something random", {});
221
+
222
+ expect(result.text).toContain("I received your prompt");
223
+ });
224
+ });
225
+
226
+ describe("Mock Coder Adapter - Runner Info", () => {
227
+ test("should expose adapter info", () => {
228
+ const runner = new AdapterRunner(mockCoderStdin);
229
+ const info = runner.getInfo();
230
+
231
+ expect(info.id).toBe("mock-coder-stdin");
232
+ expect(info.displayName).toBe("Mock Coder (stdin)");
233
+ expect(info.supportsStreaming).toBe(false);
234
+ expect(info.capabilities).toContain("generate");
235
+ });
236
+
237
+ test("should report streaming capability", () => {
238
+ const runner = new AdapterRunner(mockCoderStreamText);
239
+ const info = runner.getInfo();
240
+
241
+ expect(info.supportsStreaming).toBe(true);
242
+ });
243
+ });
@@ -0,0 +1,136 @@
1
+ import { mock } from "bun:test";
2
+ // Test utilities for reliable and consistent testing
3
+ import { mkdir, rm } from "node:fs/promises";
4
+ import type { CodingRequest, Config } from "../types";
5
+
6
+ export interface TestEnvironment {
7
+ tempDir: string;
8
+ cleanup: () => Promise<void>;
9
+ }
10
+
11
+ /**
12
+ * Create a unique temporary directory for testing
13
+ */
14
+ export async function createTestEnvironment(prefix = "test"): Promise<TestEnvironment> {
15
+ const testId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
16
+ const tempDir = `/tmp/${prefix}-${testId}`;
17
+
18
+ await mkdir(tempDir, { recursive: true });
19
+
20
+ return {
21
+ tempDir,
22
+ cleanup: async () => {
23
+ try {
24
+ await rm(tempDir, { recursive: true, force: true });
25
+ } catch (error) {
26
+ // Ignore cleanup errors in tests
27
+ }
28
+ },
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Create mock providers for testing
34
+ */
35
+ export function createMockProvider(
36
+ id: string,
37
+ displayName: string,
38
+ options: {
39
+ shouldFail?: boolean;
40
+ failWith?: Error;
41
+ supportsStreaming?: boolean;
42
+ } = {},
43
+ ) {
44
+ const { shouldFail = false, failWith, supportsStreaming = false } = options;
45
+
46
+ let currentShouldFail = shouldFail;
47
+ let currentFailWith = failWith;
48
+
49
+ return {
50
+ id,
51
+ displayName,
52
+ supportsStreaming,
53
+ prefersJson: true,
54
+ capabilities: ["generate", "edit", "explain", "test"],
55
+
56
+ setShouldFail: (fail: boolean, error?: Error) => {
57
+ currentShouldFail = fail;
58
+ currentFailWith = error || new Error("Mock failure");
59
+ },
60
+
61
+ runOnce: mock(async (_req: CodingRequest) => {
62
+ if (currentShouldFail && currentFailWith) {
63
+ throw currentFailWith;
64
+ }
65
+ return {
66
+ text: `Response from ${id}`,
67
+ usage: { inputTokens: 10, outputTokens: 20 },
68
+ };
69
+ }),
70
+
71
+ runStream: mock(async function* (_req: CodingRequest) {
72
+ if (currentShouldFail && currentFailWith) {
73
+ throw currentFailWith;
74
+ }
75
+ yield {
76
+ type: "complete" as const,
77
+ provider: id,
78
+ text: `Response from ${id}`,
79
+ usage: { inputTokens: 10, outputTokens: 20 },
80
+ };
81
+ }),
82
+
83
+ classifyError: mock(() => "TRANSIENT"),
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Create mock state manager for testing
89
+ */
90
+ export function createMockStateManager() {
91
+ return {
92
+ initialize: mock(async () => {}),
93
+ getProviderState: mock(async () => ({
94
+ requestsToday: 0,
95
+ lastErrors: [],
96
+ outOfCreditsUntil: undefined,
97
+ lastUsedAt: undefined,
98
+ lastReset: undefined,
99
+ })),
100
+ recordSuccess: mock(async () => {}),
101
+ recordError: mock(async () => {}),
102
+ markOutOfCredits: mock(async () => {}),
103
+ resetProvider: mock(async () => {}),
104
+ resetAll: mock(async () => {}),
105
+ getState: mock(() => ({ version: "1.0.0", providers: {} })),
106
+ getStatePath: mock(() => "/tmp/mock-state.json"),
107
+ save: mock(async () => {}),
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Create mock config for testing
113
+ */
114
+ export function createMockConfig(overrides: Partial<Config> = {}) {
115
+ return {
116
+ routing: {
117
+ defaultOrder: ["provider1", "provider2", "provider3"],
118
+ perModeOverride: {},
119
+ ...overrides.routing,
120
+ },
121
+ providers: {
122
+ provider1: {
123
+ binary: "provider1",
124
+ args: [],
125
+ jsonMode: "none",
126
+ streamingMode: "line",
127
+ capabilities: ["generate"],
128
+ },
129
+ ...overrides.providers,
130
+ },
131
+ credits: {
132
+ providers: {},
133
+ ...overrides.credits,
134
+ },
135
+ };
136
+ }