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,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PluginRegistry Unit Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
6
|
+
import { PluginRegistry } from "../../../plugin/registry";
|
|
7
|
+
import type { ManagedProvider, PluginDefinition } from "../../../plugin/types";
|
|
8
|
+
import type { Provider } from "../../../providers";
|
|
9
|
+
import type { CodingEvent, ProviderErrorContext } from "../../../types";
|
|
10
|
+
|
|
11
|
+
// Mock provider for testing
|
|
12
|
+
class MockProvider implements Provider {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly displayName: string;
|
|
15
|
+
readonly supportsStreaming = false;
|
|
16
|
+
readonly prefersJson = false;
|
|
17
|
+
initCalled = false;
|
|
18
|
+
shutdownCalled = false;
|
|
19
|
+
|
|
20
|
+
constructor(config: { id: string }) {
|
|
21
|
+
this.id = config.id;
|
|
22
|
+
this.displayName = `Mock ${config.id}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async runOnce() {
|
|
26
|
+
return { text: "mock response" };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async *runStream(): AsyncGenerator<CodingEvent> {
|
|
30
|
+
yield { type: "start", provider: this.id, requestId: "req-1" };
|
|
31
|
+
yield { type: "complete", provider: this.id, text: "mock" };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
classifyError(): "UNKNOWN" {
|
|
35
|
+
return "UNKNOWN";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getInfo() {
|
|
39
|
+
return {
|
|
40
|
+
id: this.id,
|
|
41
|
+
displayName: this.displayName,
|
|
42
|
+
supportsStreaming: false,
|
|
43
|
+
prefersJson: false,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async init() {
|
|
48
|
+
this.initCalled = true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async shutdown() {
|
|
52
|
+
this.shutdownCalled = true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function createMockPlugin(
|
|
57
|
+
type: string,
|
|
58
|
+
options?: {
|
|
59
|
+
onRegister?: () => Promise<void>;
|
|
60
|
+
onUnregister?: () => Promise<void>;
|
|
61
|
+
},
|
|
62
|
+
): PluginDefinition {
|
|
63
|
+
return {
|
|
64
|
+
type,
|
|
65
|
+
displayName: `Mock ${type}`,
|
|
66
|
+
hasLifecycle: false,
|
|
67
|
+
factory: (config: any) => new MockProvider({ id: type, ...config }),
|
|
68
|
+
onRegister: options?.onRegister,
|
|
69
|
+
onUnregister: options?.onUnregister,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe("PluginRegistry", () => {
|
|
74
|
+
let registry: PluginRegistry;
|
|
75
|
+
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
PluginRegistry.resetInstance();
|
|
78
|
+
registry = PluginRegistry.getInstance();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
afterEach(async () => {
|
|
82
|
+
await registry.clear();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("singleton pattern", () => {
|
|
86
|
+
test("should return same instance", () => {
|
|
87
|
+
const instance1 = PluginRegistry.getInstance();
|
|
88
|
+
const instance2 = PluginRegistry.getInstance();
|
|
89
|
+
expect(instance1).toBe(instance2);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("should reset instance", () => {
|
|
93
|
+
const instance1 = PluginRegistry.getInstance();
|
|
94
|
+
PluginRegistry.resetInstance();
|
|
95
|
+
const instance2 = PluginRegistry.getInstance();
|
|
96
|
+
expect(instance1).not.toBe(instance2);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("registration", () => {
|
|
101
|
+
test("should register a plugin", async () => {
|
|
102
|
+
const plugin = createMockPlugin("test");
|
|
103
|
+
const result = await registry.register(plugin);
|
|
104
|
+
|
|
105
|
+
expect(result.success).toBe(true);
|
|
106
|
+
expect(result.type).toBe("test");
|
|
107
|
+
expect(registry.has("test")).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("should reject duplicate registration", async () => {
|
|
111
|
+
const plugin = createMockPlugin("test");
|
|
112
|
+
await registry.register(plugin);
|
|
113
|
+
const result = await registry.register(plugin);
|
|
114
|
+
|
|
115
|
+
expect(result.success).toBe(false);
|
|
116
|
+
expect(result.message).toContain("already registered");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("should allow overwrite", async () => {
|
|
120
|
+
const plugin1 = createMockPlugin("test");
|
|
121
|
+
const plugin2 = createMockPlugin("test");
|
|
122
|
+
plugin2.displayName = "New Test";
|
|
123
|
+
|
|
124
|
+
await registry.register(plugin1);
|
|
125
|
+
const result = await registry.register(plugin2, { overwrite: true });
|
|
126
|
+
|
|
127
|
+
expect(result.success).toBe(true);
|
|
128
|
+
expect(registry.get("test")?.displayName).toBe("New Test");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("should call onRegister hook", async () => {
|
|
132
|
+
let registerCalled = false;
|
|
133
|
+
const plugin = createMockPlugin("test", {
|
|
134
|
+
onRegister: async () => {
|
|
135
|
+
registerCalled = true;
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await registry.register(plugin);
|
|
140
|
+
expect(registerCalled).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("should rollback on onRegister failure", async () => {
|
|
144
|
+
const plugin = createMockPlugin("test", {
|
|
145
|
+
onRegister: async () => {
|
|
146
|
+
throw new Error("Registration failed");
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const result = await registry.register(plugin);
|
|
151
|
+
|
|
152
|
+
expect(result.success).toBe(false);
|
|
153
|
+
expect(result.message).toContain("Registration failed");
|
|
154
|
+
expect(registry.has("test")).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("sync registration", () => {
|
|
159
|
+
test("should register synchronously", () => {
|
|
160
|
+
const plugin = createMockPlugin("test");
|
|
161
|
+
const result = registry.registerSync(plugin);
|
|
162
|
+
|
|
163
|
+
expect(result.success).toBe(true);
|
|
164
|
+
expect(registry.has("test")).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("should not call lifecycle hooks for sync registration", () => {
|
|
168
|
+
let hookCalled = false;
|
|
169
|
+
const plugin = createMockPlugin("test", {
|
|
170
|
+
onRegister: async () => {
|
|
171
|
+
hookCalled = true;
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
registry.registerSync(plugin);
|
|
176
|
+
expect(hookCalled).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("unregistration", () => {
|
|
181
|
+
test("should unregister a plugin", async () => {
|
|
182
|
+
const plugin = createMockPlugin("test");
|
|
183
|
+
await registry.register(plugin);
|
|
184
|
+
|
|
185
|
+
const result = await registry.unregister("test");
|
|
186
|
+
|
|
187
|
+
expect(result).toBe(true);
|
|
188
|
+
expect(registry.has("test")).toBe(false);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("should return false for non-existent plugin", async () => {
|
|
192
|
+
const result = await registry.unregister("non-existent");
|
|
193
|
+
expect(result).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("should call onUnregister hook", async () => {
|
|
197
|
+
let unregisterCalled = false;
|
|
198
|
+
const plugin = createMockPlugin("test", {
|
|
199
|
+
onUnregister: async () => {
|
|
200
|
+
unregisterCalled = true;
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
await registry.register(plugin);
|
|
205
|
+
await registry.unregister("test");
|
|
206
|
+
|
|
207
|
+
expect(unregisterCalled).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("queries", () => {
|
|
212
|
+
test("should get all types", async () => {
|
|
213
|
+
await registry.register(createMockPlugin("a"));
|
|
214
|
+
await registry.register(createMockPlugin("b"));
|
|
215
|
+
await registry.register(createMockPlugin("c"));
|
|
216
|
+
|
|
217
|
+
const types = registry.getTypes();
|
|
218
|
+
|
|
219
|
+
expect(types).toHaveLength(3);
|
|
220
|
+
expect(types).toContain("a");
|
|
221
|
+
expect(types).toContain("b");
|
|
222
|
+
expect(types).toContain("c");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("should get plugin definition", async () => {
|
|
226
|
+
const plugin = createMockPlugin("test");
|
|
227
|
+
plugin.description = "Test plugin";
|
|
228
|
+
|
|
229
|
+
await registry.register(plugin);
|
|
230
|
+
|
|
231
|
+
const retrieved = registry.get("test");
|
|
232
|
+
expect(retrieved?.displayName).toBe("Mock test");
|
|
233
|
+
expect(retrieved?.description).toBe("Test plugin");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("should list plugins", async () => {
|
|
237
|
+
await registry.register(createMockPlugin("a"));
|
|
238
|
+
await registry.register(createMockPlugin("b"));
|
|
239
|
+
|
|
240
|
+
const plugins = registry.listPlugins();
|
|
241
|
+
|
|
242
|
+
expect(plugins).toHaveLength(2);
|
|
243
|
+
expect(plugins[0].type).toBe("a");
|
|
244
|
+
expect(plugins[1].type).toBe("b");
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe("provider creation", () => {
|
|
249
|
+
test("should create provider from plugin", async () => {
|
|
250
|
+
const plugin = createMockPlugin("test");
|
|
251
|
+
await registry.register(plugin);
|
|
252
|
+
|
|
253
|
+
const provider = registry.createProvider("test", { id: "test" });
|
|
254
|
+
|
|
255
|
+
expect(provider.id).toBe("test");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("should throw for unknown type", () => {
|
|
259
|
+
expect(() => registry.createProvider("unknown", {})).toThrow(
|
|
260
|
+
"No plugin registered for type 'unknown'",
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("should pass config to factory", async () => {
|
|
265
|
+
const plugin: PluginDefinition = {
|
|
266
|
+
type: "test",
|
|
267
|
+
displayName: "Test",
|
|
268
|
+
hasLifecycle: false,
|
|
269
|
+
factory: (config: any) => new MockProvider({ id: config.customId }),
|
|
270
|
+
};
|
|
271
|
+
await registry.register(plugin);
|
|
272
|
+
|
|
273
|
+
const provider = registry.createProvider("test", { customId: "custom" });
|
|
274
|
+
|
|
275
|
+
expect(provider.id).toBe("custom");
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe("lifecycle management", () => {
|
|
280
|
+
test("should initialize and track provider", async () => {
|
|
281
|
+
const plugin: PluginDefinition = {
|
|
282
|
+
type: "test",
|
|
283
|
+
displayName: "Test",
|
|
284
|
+
hasLifecycle: true,
|
|
285
|
+
factory: () => new MockProvider({ id: "test" }),
|
|
286
|
+
};
|
|
287
|
+
await registry.register(plugin);
|
|
288
|
+
|
|
289
|
+
const provider = (await registry.createAndInitProvider("test", {})) as MockProvider;
|
|
290
|
+
|
|
291
|
+
expect(provider.initCalled).toBe(true);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("should shutdown all providers", async () => {
|
|
295
|
+
const provider1 = new MockProvider({ id: "a" });
|
|
296
|
+
const provider2 = new MockProvider({ id: "b" });
|
|
297
|
+
|
|
298
|
+
const plugin1: PluginDefinition = {
|
|
299
|
+
type: "a",
|
|
300
|
+
displayName: "A",
|
|
301
|
+
hasLifecycle: true,
|
|
302
|
+
factory: () => provider1,
|
|
303
|
+
};
|
|
304
|
+
const plugin2: PluginDefinition = {
|
|
305
|
+
type: "b",
|
|
306
|
+
displayName: "B",
|
|
307
|
+
hasLifecycle: true,
|
|
308
|
+
factory: () => provider2,
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
await registry.register(plugin1);
|
|
312
|
+
await registry.register(plugin2);
|
|
313
|
+
|
|
314
|
+
await registry.createAndInitProvider("a", {});
|
|
315
|
+
await registry.createAndInitProvider("b", {});
|
|
316
|
+
|
|
317
|
+
await registry.shutdownAll();
|
|
318
|
+
|
|
319
|
+
expect(provider1.shutdownCalled).toBe(true);
|
|
320
|
+
expect(provider2.shutdownCalled).toBe(true);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe("built-in tracking", () => {
|
|
325
|
+
test("should track built-in plugins", async () => {
|
|
326
|
+
const plugin = createMockPlugin("builtin");
|
|
327
|
+
await registry.register(plugin, { builtIn: true });
|
|
328
|
+
|
|
329
|
+
const info = registry.listPlugins().find((p) => p.type === "builtin");
|
|
330
|
+
expect(info?.isBuiltIn).toBe(true);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("should not mark regular plugins as built-in", async () => {
|
|
334
|
+
const plugin = createMockPlugin("regular");
|
|
335
|
+
await registry.register(plugin);
|
|
336
|
+
|
|
337
|
+
const info = registry.listPlugins().find((p) => p.type === "regular");
|
|
338
|
+
expect(info?.isBuiltIn).toBe(false);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
describe("size", () => {
|
|
343
|
+
test("should return number of registered plugins", async () => {
|
|
344
|
+
expect(registry.size).toBe(0);
|
|
345
|
+
|
|
346
|
+
await registry.register(createMockPlugin("a"));
|
|
347
|
+
expect(registry.size).toBe(1);
|
|
348
|
+
|
|
349
|
+
await registry.register(createMockPlugin("b"));
|
|
350
|
+
expect(registry.size).toBe(2);
|
|
351
|
+
|
|
352
|
+
await registry.unregister("a");
|
|
353
|
+
expect(registry.size).toBe(1);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { CodexProvider } from "../../../providers/codex";
|
|
3
|
+
import type { CodingRequest, ProviderConfig } from "../../../types";
|
|
4
|
+
|
|
5
|
+
describe("CodexProvider", () => {
|
|
6
|
+
let provider: CodexProvider;
|
|
7
|
+
let config: ProviderConfig;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
config = {
|
|
11
|
+
binary: "codex",
|
|
12
|
+
args: [],
|
|
13
|
+
jsonMode: "none",
|
|
14
|
+
streamingMode: "line",
|
|
15
|
+
capabilities: ["generate", "edit", "test"],
|
|
16
|
+
};
|
|
17
|
+
provider = new CodexProvider(config);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("constructor", () => {
|
|
21
|
+
test("should initialize with correct id and display name", () => {
|
|
22
|
+
expect(provider.id).toBe("codex");
|
|
23
|
+
expect(provider.displayName).toBe("Codex CLI");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("should set binary path from config", () => {
|
|
27
|
+
const customConfig = { ...config, binary: "/custom/codex" };
|
|
28
|
+
const customProvider = new CodexProvider(customConfig);
|
|
29
|
+
expect(customProvider.binaryPath).toBe("/custom/codex");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("should set default binary if not provided", () => {
|
|
33
|
+
const defaultConfig = { ...config, binary: "" };
|
|
34
|
+
const defaultProvider = new CodexProvider(defaultConfig);
|
|
35
|
+
expect(defaultProvider.binaryPath).toBe("codex");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("should set streaming support based on config", () => {
|
|
39
|
+
expect(provider.supportsStreaming).toBe(true);
|
|
40
|
+
|
|
41
|
+
const noStreamConfig = { ...config, streamingMode: "none" as const };
|
|
42
|
+
const noStreamProvider = new CodexProvider(noStreamConfig);
|
|
43
|
+
expect(noStreamProvider.supportsStreaming).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("buildArgs", () => {
|
|
48
|
+
test("should use exec subcommand with prompt", () => {
|
|
49
|
+
const req: CodingRequest = { prompt: "Write hello world" };
|
|
50
|
+
const args = provider.buildArgs(req, {});
|
|
51
|
+
|
|
52
|
+
expect(args[0]).toBe("exec");
|
|
53
|
+
expect(args).toContain("Write hello world");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("should include config args", () => {
|
|
57
|
+
const configWithArgs = { ...config, args: ["--model", "gpt-4"] };
|
|
58
|
+
const providerWithArgs = new CodexProvider(configWithArgs);
|
|
59
|
+
const req: CodingRequest = { prompt: "test" };
|
|
60
|
+
const args = providerWithArgs.buildArgs(req, {});
|
|
61
|
+
|
|
62
|
+
expect(args).toContain("--model");
|
|
63
|
+
expect(args).toContain("gpt-4");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("should place prompt after config args", () => {
|
|
67
|
+
const configWithArgs = { ...config, args: ["--verbose"] };
|
|
68
|
+
const providerWithArgs = new CodexProvider(configWithArgs);
|
|
69
|
+
const req: CodingRequest = { prompt: "test prompt" };
|
|
70
|
+
const args = providerWithArgs.buildArgs(req, {});
|
|
71
|
+
|
|
72
|
+
// exec, --verbose, test prompt
|
|
73
|
+
expect(args[0]).toBe("exec");
|
|
74
|
+
expect(args[1]).toBe("--verbose");
|
|
75
|
+
expect(args[2]).toBe("test prompt");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("getStdinInput", () => {
|
|
80
|
+
test("should return undefined for Codex (uses positional args)", () => {
|
|
81
|
+
const result = provider.getStdinInput();
|
|
82
|
+
expect(result).toBeUndefined();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("classifyError", () => {
|
|
87
|
+
test("classifies 'plan limit' as OUT_OF_CREDITS", () => {
|
|
88
|
+
const error = provider.classifyError({ stderr: "You've hit your plan limit" });
|
|
89
|
+
expect(error).toBe("OUT_OF_CREDITS");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("classifies 'subscription required' as OUT_OF_CREDITS", () => {
|
|
93
|
+
const error = provider.classifyError({ stderr: "Subscription required for this feature" });
|
|
94
|
+
expect(error).toBe("OUT_OF_CREDITS");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("classifies 'rate limit' as RATE_LIMIT", () => {
|
|
98
|
+
const error = provider.classifyError({ stderr: "Rate limit exceeded" });
|
|
99
|
+
expect(error).toBe("RATE_LIMIT");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("classifies '401 unauthorized' as UNAUTHORIZED", () => {
|
|
103
|
+
const error = provider.classifyError({ stderr: "401 unauthorized" });
|
|
104
|
+
expect(error).toBe("UNAUTHORIZED");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("classifies '500 internal error' as INTERNAL", () => {
|
|
108
|
+
const error = provider.classifyError({ stderr: "500 internal server error" });
|
|
109
|
+
expect(error).toBe("INTERNAL");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("classifies 'timeout' as TIMEOUT", () => {
|
|
113
|
+
const error = provider.classifyError({ stderr: "Request timeout" });
|
|
114
|
+
expect(error).toBe("TIMEOUT");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("classifies unknown errors as TRANSIENT", () => {
|
|
118
|
+
const error = provider.classifyError({ stderr: "Something unexpected happened" });
|
|
119
|
+
expect(error).toBe("TRANSIENT");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("should combine stdout and stderr for classification", () => {
|
|
123
|
+
const result = provider.classifyError({
|
|
124
|
+
stderr: "some stderr",
|
|
125
|
+
stdout: "plan limit reached",
|
|
126
|
+
});
|
|
127
|
+
expect(result).toBe("OUT_OF_CREDITS");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("getInfo", () => {
|
|
132
|
+
test("should return provider information", () => {
|
|
133
|
+
const info = provider.getInfo();
|
|
134
|
+
|
|
135
|
+
expect(info).toEqual({
|
|
136
|
+
id: "codex",
|
|
137
|
+
displayName: "Codex CLI",
|
|
138
|
+
supportsStreaming: true,
|
|
139
|
+
prefersJson: false,
|
|
140
|
+
capabilities: ["generate", "edit", "test"],
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("runOnce (integration)", () => {
|
|
146
|
+
test("should throw when binary not found", async () => {
|
|
147
|
+
// Use a non-existent binary path
|
|
148
|
+
const nonExistentConfig = { ...config, binary: "/nonexistent/path/to/codex-xyz-999" };
|
|
149
|
+
const nonExistentProvider = new CodexProvider(nonExistentConfig);
|
|
150
|
+
const req: CodingRequest = { prompt: "test" };
|
|
151
|
+
await expect(nonExistentProvider.runOnce(req, {})).rejects.toThrow(/Binary not found/);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("runStream (integration)", () => {
|
|
156
|
+
test("should throw when binary not found", async () => {
|
|
157
|
+
// Use a non-existent binary path
|
|
158
|
+
const nonExistentConfig = { ...config, binary: "/nonexistent/path/to/codex-xyz-999" };
|
|
159
|
+
const nonExistentProvider = new CodexProvider(nonExistentConfig);
|
|
160
|
+
const req: CodingRequest = { prompt: "test" };
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
for await (const _event of nonExistentProvider.runStream(req, {})) {
|
|
164
|
+
// Should not get here
|
|
165
|
+
}
|
|
166
|
+
expect(true).toBe(false); // Fail if we get here
|
|
167
|
+
} catch (error) {
|
|
168
|
+
expect(error).toBeDefined();
|
|
169
|
+
expect((error as Error).message).toMatch(/Binary not found/);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|