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.
- package/bin/wraptc +4 -4
- package/package.json +2 -2
- package/src/cli/__tests__/cli.test.ts +337 -0
- package/src/cli/index.ts +149 -0
- package/src/core/__tests__/fixtures/configs/project-config.json +14 -0
- package/src/core/__tests__/fixtures/configs/system-config.json +14 -0
- package/src/core/__tests__/fixtures/configs/user-config.json +15 -0
- package/src/core/__tests__/integration/integration.test.ts +241 -0
- package/src/core/__tests__/integration/mock-coder-adapter.test.ts +243 -0
- package/src/core/__tests__/test-utils.ts +136 -0
- package/src/core/__tests__/unit/adapters/runner.test.ts +302 -0
- package/src/core/__tests__/unit/basic-test.test.ts +44 -0
- package/src/core/__tests__/unit/basic.test.ts +12 -0
- package/src/core/__tests__/unit/config.test.ts +244 -0
- package/src/core/__tests__/unit/error-patterns.test.ts +181 -0
- package/src/core/__tests__/unit/memory-monitor.test.ts +354 -0
- package/src/core/__tests__/unit/plugin/registry.test.ts +356 -0
- package/src/core/__tests__/unit/providers/codex.test.ts +173 -0
- package/src/core/__tests__/unit/providers/configurable.test.ts +429 -0
- package/src/core/__tests__/unit/providers/gemini.test.ts +251 -0
- package/src/core/__tests__/unit/providers/opencode.test.ts +258 -0
- package/src/core/__tests__/unit/providers/qwen-code.test.ts +195 -0
- package/src/core/__tests__/unit/providers/simple-codex.test.ts +18 -0
- package/src/core/__tests__/unit/router.test.ts +967 -0
- package/src/core/__tests__/unit/state.test.ts +1079 -0
- package/src/core/__tests__/unit/unified/capabilities.test.ts +186 -0
- package/src/core/__tests__/unit/wrap-terminalcoder.test.ts +32 -0
- package/src/core/adapters/builtin/codex.ts +35 -0
- package/src/core/adapters/builtin/gemini.ts +34 -0
- package/src/core/adapters/builtin/index.ts +31 -0
- package/src/core/adapters/builtin/mock-coder.ts +148 -0
- package/src/core/adapters/builtin/qwen.ts +34 -0
- package/src/core/adapters/define.ts +48 -0
- package/src/core/adapters/index.ts +43 -0
- package/src/core/adapters/loader.ts +143 -0
- package/src/core/adapters/provider-bridge.ts +190 -0
- package/src/core/adapters/runner.ts +437 -0
- package/src/core/adapters/types.ts +172 -0
- package/src/core/config.ts +290 -0
- package/src/core/define-provider.ts +212 -0
- package/src/core/error-patterns.ts +147 -0
- package/src/core/index.ts +130 -0
- package/src/core/memory-monitor.ts +171 -0
- package/src/core/plugin/builtin.ts +87 -0
- package/src/core/plugin/index.ts +34 -0
- package/src/core/plugin/registry.ts +350 -0
- package/src/core/plugin/types.ts +209 -0
- package/src/core/provider-factory.ts +397 -0
- package/src/core/provider-loader.ts +171 -0
- package/src/core/providers/codex.ts +56 -0
- package/src/core/providers/configurable.ts +637 -0
- package/src/core/providers/custom.ts +261 -0
- package/src/core/providers/gemini.ts +41 -0
- package/src/core/providers/index.ts +383 -0
- package/src/core/providers/opencode.ts +168 -0
- package/src/core/providers/qwen-code.ts +41 -0
- package/src/core/router.ts +370 -0
- package/src/core/state.ts +258 -0
- package/src/core/types.ts +206 -0
- package/src/core/unified/capabilities.ts +184 -0
- package/src/core/unified/errors.ts +141 -0
- package/src/core/unified/index.ts +29 -0
- package/src/core/unified/output.ts +189 -0
- package/src/core/wrap-terminalcoder.ts +245 -0
- package/src/mcp/__tests__/server.test.ts +295 -0
- package/src/mcp/server.ts +284 -0
- package/src/test-fixtures/mock-coder.sh +194 -0
- package/dist/cli/index.js +0 -16501
- package/dist/core/index.js +0 -7531
- package/dist/mcp/server.js +0 -14568
- 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
|
+
}
|