tlc-claude-code 1.6.4 → 1.8.0

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 (105) hide show
  1. package/dashboard/dist/components/ContainerSecurityPane.d.ts +45 -0
  2. package/dashboard/dist/components/ContainerSecurityPane.js +44 -0
  3. package/dashboard/dist/components/ContainerSecurityPane.test.d.ts +1 -0
  4. package/dashboard/dist/components/ContainerSecurityPane.test.js +153 -0
  5. package/package.json +1 -1
  6. package/server/lib/access-control.test.js +1 -1
  7. package/server/lib/agents-cancel-command.test.js +1 -1
  8. package/server/lib/agents-get-command.test.js +1 -1
  9. package/server/lib/agents-list-command.test.js +1 -1
  10. package/server/lib/agents-logs-command.test.js +1 -1
  11. package/server/lib/agents-retry-command.test.js +1 -1
  12. package/server/lib/budget-limits.test.js +2 -2
  13. package/server/lib/code-gate/bypass-logger.js +129 -0
  14. package/server/lib/code-gate/bypass-logger.test.js +142 -0
  15. package/server/lib/code-gate/first-commit-audit.js +138 -0
  16. package/server/lib/code-gate/first-commit-audit.test.js +203 -0
  17. package/server/lib/code-gate/gate-command.js +114 -0
  18. package/server/lib/code-gate/gate-command.test.js +111 -0
  19. package/server/lib/code-gate/gate-config.js +163 -0
  20. package/server/lib/code-gate/gate-config.test.js +181 -0
  21. package/server/lib/code-gate/gate-engine.js +193 -0
  22. package/server/lib/code-gate/gate-engine.test.js +258 -0
  23. package/server/lib/code-gate/gate-reporter.js +123 -0
  24. package/server/lib/code-gate/gate-reporter.test.js +159 -0
  25. package/server/lib/code-gate/hooks-generator.js +149 -0
  26. package/server/lib/code-gate/hooks-generator.test.js +142 -0
  27. package/server/lib/code-gate/llm-reviewer.js +176 -0
  28. package/server/lib/code-gate/llm-reviewer.test.js +161 -0
  29. package/server/lib/code-gate/multi-model-reviewer.js +172 -0
  30. package/server/lib/code-gate/multi-model-reviewer.test.js +217 -0
  31. package/server/lib/code-gate/push-gate.js +133 -0
  32. package/server/lib/code-gate/push-gate.test.js +190 -0
  33. package/server/lib/code-gate/rules/architecture-rules.js +228 -0
  34. package/server/lib/code-gate/rules/architecture-rules.test.js +155 -0
  35. package/server/lib/code-gate/rules/client-rules.js +120 -0
  36. package/server/lib/code-gate/rules/client-rules.test.js +121 -0
  37. package/server/lib/code-gate/rules/config-rules.js +140 -0
  38. package/server/lib/code-gate/rules/config-rules.test.js +103 -0
  39. package/server/lib/code-gate/rules/database-rules.js +158 -0
  40. package/server/lib/code-gate/rules/database-rules.test.js +119 -0
  41. package/server/lib/code-gate/rules/docker-rules.js +201 -0
  42. package/server/lib/code-gate/rules/docker-rules.test.js +104 -0
  43. package/server/lib/code-gate/rules/quality-rules.js +304 -0
  44. package/server/lib/code-gate/rules/quality-rules.test.js +199 -0
  45. package/server/lib/code-gate/rules/security-rules.js +228 -0
  46. package/server/lib/code-gate/rules/security-rules.test.js +131 -0
  47. package/server/lib/code-gate/rules/structure-rules.js +155 -0
  48. package/server/lib/code-gate/rules/structure-rules.test.js +107 -0
  49. package/server/lib/code-gate/rules/test-rules.js +93 -0
  50. package/server/lib/code-gate/rules/test-rules.test.js +97 -0
  51. package/server/lib/code-gate/typescript-gate.js +128 -0
  52. package/server/lib/code-gate/typescript-gate.test.js +131 -0
  53. package/server/lib/code-generator.test.js +1 -1
  54. package/server/lib/cost-command.test.js +1 -1
  55. package/server/lib/cost-optimizer.test.js +1 -1
  56. package/server/lib/cost-projections.test.js +1 -1
  57. package/server/lib/cost-reports.test.js +1 -1
  58. package/server/lib/cost-tracker.test.js +1 -1
  59. package/server/lib/crypto-patterns.test.js +1 -1
  60. package/server/lib/design-command.test.js +1 -1
  61. package/server/lib/design-parser.test.js +1 -1
  62. package/server/lib/gemini-vision.test.js +1 -1
  63. package/server/lib/infra/infra-generator.js +331 -0
  64. package/server/lib/infra/infra-generator.test.js +146 -0
  65. package/server/lib/input-validator.test.js +1 -1
  66. package/server/lib/litellm-client.test.js +1 -1
  67. package/server/lib/litellm-command.test.js +1 -1
  68. package/server/lib/litellm-config.test.js +1 -1
  69. package/server/lib/llm/adapters/api-adapter.js +95 -0
  70. package/server/lib/llm/adapters/api-adapter.test.js +81 -0
  71. package/server/lib/llm/adapters/codex-adapter.js +85 -0
  72. package/server/lib/llm/adapters/codex-adapter.test.js +54 -0
  73. package/server/lib/llm/adapters/gemini-adapter.js +100 -0
  74. package/server/lib/llm/adapters/gemini-adapter.test.js +54 -0
  75. package/server/lib/llm/index.js +109 -0
  76. package/server/lib/llm/index.test.js +147 -0
  77. package/server/lib/llm/provider-executor.js +168 -0
  78. package/server/lib/llm/provider-executor.test.js +244 -0
  79. package/server/lib/llm/provider-registry.js +104 -0
  80. package/server/lib/llm/provider-registry.test.js +157 -0
  81. package/server/lib/llm/review-service.js +222 -0
  82. package/server/lib/llm/review-service.test.js +220 -0
  83. package/server/lib/model-pricing.test.js +1 -1
  84. package/server/lib/models-command.test.js +1 -1
  85. package/server/lib/optimize-command.test.js +1 -1
  86. package/server/lib/orchestration-integration.test.js +1 -1
  87. package/server/lib/output-encoder.test.js +1 -1
  88. package/server/lib/quality-evaluator.test.js +1 -1
  89. package/server/lib/quality-gate-command.test.js +1 -1
  90. package/server/lib/quality-gate-scorer.test.js +1 -1
  91. package/server/lib/quality-history.test.js +1 -1
  92. package/server/lib/quality-presets.test.js +1 -1
  93. package/server/lib/quality-retry.test.js +1 -1
  94. package/server/lib/quality-thresholds.test.js +1 -1
  95. package/server/lib/secure-auth.test.js +1 -1
  96. package/server/lib/secure-code-command.test.js +1 -1
  97. package/server/lib/secure-errors.test.js +1 -1
  98. package/server/lib/security/auth-security.test.js +4 -3
  99. package/server/lib/shame/shame-registry.js +224 -0
  100. package/server/lib/shame/shame-registry.test.js +202 -0
  101. package/server/lib/standards/cleanup-dry-run.js +254 -0
  102. package/server/lib/standards/cleanup-dry-run.test.js +220 -0
  103. package/server/lib/vision-command.test.js +1 -1
  104. package/server/lib/visual-command.test.js +1 -1
  105. package/server/lib/visual-testing.test.js +1 -1
@@ -0,0 +1,109 @@
1
+ /**
2
+ * LLM Service - Unified API
3
+ *
4
+ * createLLMService(config) → { review(diff), execute(prompt), health() }
5
+ *
6
+ * @module llm
7
+ */
8
+
9
+ const { createExecutor } = require('./provider-executor.js');
10
+ const { createRegistry } = require('./provider-registry.js');
11
+ const { createReviewService } = require('./review-service.js');
12
+
13
+ /**
14
+ * Create the unified LLM service
15
+ * @param {Object} config - Service configuration
16
+ * @param {Object} config.providers - Provider configs { name: { type, command, capabilities } }
17
+ * @param {boolean} config.multiModel - Enable multi-model review
18
+ * @param {number} config.timeout - Default timeout
19
+ * @param {Object} deps - Injectable dependencies
20
+ * @param {Function} deps.healthCheck - Health check function
21
+ * @param {Function} deps.spawn - Process spawn function
22
+ * @param {Function} deps.fetch - HTTP fetch function
23
+ * @returns {Object} Service with review, execute, health methods
24
+ */
25
+ function createLLMService(config = {}, deps = {}) {
26
+ const { providers = {}, multiModel = false, timeout } = config;
27
+
28
+ // Build health check from deps
29
+ const healthCheck = deps.healthCheck || (() => Promise.resolve({ available: false }));
30
+
31
+ // Create registry and load providers
32
+ const registry = createRegistry({ healthCheck, cacheTTL: config.cacheTTL });
33
+ registry.loadFromConfig({ providers });
34
+
35
+ // Create executor with real spawn/fetch
36
+ const executor = createExecutor({
37
+ spawn: deps.spawn,
38
+ fetch: deps.fetch,
39
+ });
40
+
41
+ // Create review service
42
+ const reviewService = createReviewService({
43
+ registry,
44
+ executor,
45
+ multiModel,
46
+ timeout,
47
+ standards: config.standards || '',
48
+ });
49
+
50
+ return {
51
+ /**
52
+ * Review a diff using configured providers
53
+ * @param {string} diff - Git diff content
54
+ * @param {Object} options - Review options
55
+ * @returns {Promise<Object>} { findings, summary, provider, latency }
56
+ */
57
+ review: (diff, options) => reviewService.review(diff, options),
58
+
59
+ /**
60
+ * Execute a generic prompt against best available provider
61
+ * @param {string} prompt - Prompt text
62
+ * @param {Object} options - Execution options
63
+ * @returns {Promise<Object>} { response, model, latency }
64
+ */
65
+ execute: async (prompt, options = {}) => {
66
+ const capability = options.capability || 'code-gen';
67
+ const provider = await registry.getBestProvider(capability);
68
+
69
+ if (!provider) {
70
+ // Fall back to first available provider
71
+ const allProviders = registry.list();
72
+ if (allProviders.length === 0) {
73
+ return { response: '', error: 'No providers configured' };
74
+ }
75
+
76
+ try {
77
+ return await executor.execute(prompt, { ...allProviders[0], timeout });
78
+ } catch (err) {
79
+ return { response: '', error: err.message };
80
+ }
81
+ }
82
+
83
+ try {
84
+ return await executor.execute(prompt, { ...provider, timeout });
85
+ } catch (err) {
86
+ return { response: '', error: err.message };
87
+ }
88
+ },
89
+
90
+ /**
91
+ * Get health status of all providers
92
+ * @returns {Promise<Object>} { providers: { name: status } }
93
+ */
94
+ health: async () => {
95
+ const allProviders = registry.list();
96
+ const statuses = {};
97
+
98
+ for (const provider of allProviders) {
99
+ statuses[provider.name] = await registry.checkHealth(provider.name);
100
+ }
101
+
102
+ return { providers: statuses };
103
+ },
104
+ };
105
+ }
106
+
107
+ module.exports = {
108
+ createLLMService,
109
+ };
@@ -0,0 +1,147 @@
1
+ /**
2
+ * LLM Service Integration Tests
3
+ *
4
+ * Unified API: createLLMService(config) → { review, execute, health }
5
+ */
6
+ import { describe, it, expect, vi } from 'vitest';
7
+
8
+ const {
9
+ createLLMService,
10
+ } = require('./index.js');
11
+
12
+ describe('LLM Service', () => {
13
+ const mockDeps = {
14
+ healthCheck: vi.fn().mockResolvedValue({ available: true }),
15
+ spawn: vi.fn().mockReturnValue({
16
+ stdout: { on: vi.fn((ev, cb) => { if (ev === 'data') cb(Buffer.from('{"findings": [], "summary": "Clean"}')); }) },
17
+ stderr: { on: vi.fn() },
18
+ stdin: { write: vi.fn(), end: vi.fn() },
19
+ on: vi.fn((ev, cb) => { if (ev === 'close') cb(0); }),
20
+ }),
21
+ };
22
+
23
+ describe('createLLMService', () => {
24
+ it('creates service with config', () => {
25
+ const service = createLLMService({
26
+ providers: {
27
+ codex: { type: 'cli', command: 'codex', capabilities: ['review'] },
28
+ },
29
+ }, mockDeps);
30
+
31
+ expect(service).toBeDefined();
32
+ expect(service.review).toBeDefined();
33
+ expect(service.execute).toBeDefined();
34
+ expect(service.health).toBeDefined();
35
+ });
36
+
37
+ it('creates service with zero config (auto-detect)', () => {
38
+ const service = createLLMService({}, mockDeps);
39
+ expect(service).toBeDefined();
40
+ });
41
+
42
+ it('review() returns structured findings', async () => {
43
+ const service = createLLMService({
44
+ providers: {
45
+ codex: { type: 'cli', command: 'codex', capabilities: ['review'] },
46
+ },
47
+ }, mockDeps);
48
+
49
+ const result = await service.review('diff content');
50
+ expect(result).toMatchObject({
51
+ findings: expect.any(Array),
52
+ summary: expect.any(String),
53
+ });
54
+ });
55
+
56
+ it('execute() returns raw response', async () => {
57
+ const service = createLLMService({
58
+ providers: {
59
+ codex: { type: 'cli', command: 'codex', capabilities: ['code-gen'] },
60
+ },
61
+ }, mockDeps);
62
+
63
+ const result = await service.execute('Write a function');
64
+ expect(result).toMatchObject({
65
+ response: expect.any(String),
66
+ });
67
+ });
68
+
69
+ it('health() returns provider statuses', async () => {
70
+ const service = createLLMService({
71
+ providers: {
72
+ codex: { type: 'cli', command: 'codex', capabilities: ['review'] },
73
+ },
74
+ }, mockDeps);
75
+
76
+ const status = await service.health();
77
+ expect(status.providers).toBeDefined();
78
+ });
79
+
80
+ it('falls back through providers on failure', async () => {
81
+ const failThenSucceed = vi.fn()
82
+ .mockReturnValueOnce({
83
+ stdout: { on: vi.fn() },
84
+ stderr: { on: vi.fn((ev, cb) => { if (ev === 'data') cb(Buffer.from('err')); }) },
85
+ stdin: { write: vi.fn(), end: vi.fn() },
86
+ on: vi.fn((ev, cb) => { if (ev === 'close') cb(1); }),
87
+ })
88
+ .mockReturnValueOnce({
89
+ stdout: { on: vi.fn((ev, cb) => { if (ev === 'data') cb(Buffer.from('{"findings": [], "summary": "OK"}')); }) },
90
+ stderr: { on: vi.fn() },
91
+ stdin: { write: vi.fn(), end: vi.fn() },
92
+ on: vi.fn((ev, cb) => { if (ev === 'close') cb(0); }),
93
+ });
94
+
95
+ const service = createLLMService({
96
+ providers: {
97
+ codex: { type: 'cli', command: 'codex', capabilities: ['review'] },
98
+ gemini: { type: 'cli', command: 'gemini', capabilities: ['review'] },
99
+ },
100
+ }, { ...mockDeps, spawn: failThenSucceed });
101
+
102
+ const result = await service.review('diff');
103
+ expect(result.findings).toBeDefined();
104
+ });
105
+
106
+ it('respects multi-model config', () => {
107
+ const service = createLLMService({
108
+ providers: {
109
+ codex: { type: 'cli', command: 'codex', capabilities: ['review'] },
110
+ gemini: { type: 'cli', command: 'gemini', capabilities: ['review'] },
111
+ },
112
+ multiModel: true,
113
+ }, mockDeps);
114
+
115
+ expect(service).toBeDefined();
116
+ });
117
+
118
+ it('works with single provider', () => {
119
+ const service = createLLMService({
120
+ providers: {
121
+ codex: { type: 'cli', command: 'codex', capabilities: ['review'] },
122
+ },
123
+ }, mockDeps);
124
+
125
+ expect(service).toBeDefined();
126
+ });
127
+
128
+ it('exports clean public API', () => {
129
+ const service = createLLMService({}, mockDeps);
130
+ const keys = Object.keys(service);
131
+ expect(keys).toContain('review');
132
+ expect(keys).toContain('execute');
133
+ expect(keys).toContain('health');
134
+ });
135
+
136
+ it('config validation catches bad provider references', () => {
137
+ // Should not throw, just log warning or skip bad providers
138
+ const service = createLLMService({
139
+ providers: {
140
+ invalid: { type: 'unknown', capabilities: ['review'] },
141
+ },
142
+ }, mockDeps);
143
+
144
+ expect(service).toBeDefined();
145
+ });
146
+ });
147
+ });
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Provider Executor
3
+ *
4
+ * Actually execute LLM requests through any provider.
5
+ * The bridge between "provider detected" and "review completed."
6
+ * Supports CLI (spawn) and API (HTTP) providers.
7
+ *
8
+ * @module llm/provider-executor
9
+ */
10
+
11
+ /** Strip ANSI escape codes from output */
12
+ function stripAnsi(str) {
13
+ return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
14
+ }
15
+
16
+ /**
17
+ * Execute a CLI provider by spawning the process
18
+ * @param {string} prompt - The prompt to send
19
+ * @param {Object} options - Execution options
20
+ * @param {string} options.command - CLI command to run
21
+ * @param {string[]} options.args - CLI arguments
22
+ * @param {Function} options.spawn - Spawn function (injectable)
23
+ * @param {number} options.timeout - Timeout in ms
24
+ * @returns {Promise<{response: string}>}
25
+ */
26
+ function executeCliProvider(prompt, options = {}) {
27
+ const { command, args = [], spawn, timeout } = options;
28
+
29
+ return new Promise((resolve, reject) => {
30
+ const proc = spawn(command, args);
31
+ let stdout = '';
32
+ let stderr = '';
33
+ let settled = false;
34
+ let timer;
35
+
36
+ proc.stdout.on('data', (data) => {
37
+ stdout += data.toString();
38
+ });
39
+
40
+ proc.stderr.on('data', (data) => {
41
+ stderr += data.toString();
42
+ });
43
+
44
+ proc.on('close', (code) => {
45
+ if (settled) return;
46
+ settled = true;
47
+ if (timer) clearTimeout(timer);
48
+
49
+ if (code !== 0) {
50
+ reject(new Error('Provider exited with exit code ' + code + ': ' + stderr));
51
+ return;
52
+ }
53
+
54
+ resolve({ response: stripAnsi(stdout) });
55
+ });
56
+
57
+ // Write prompt to stdin
58
+ proc.stdin.write(prompt);
59
+ proc.stdin.end();
60
+
61
+ // Timeout handling
62
+ if (timeout) {
63
+ timer = setTimeout(() => {
64
+ if (settled) return;
65
+ settled = true;
66
+ if (proc.kill) proc.kill();
67
+ reject(new Error('CLI provider timeout after ' + timeout + 'ms'));
68
+ }, timeout);
69
+ }
70
+ });
71
+ }
72
+
73
+ /**
74
+ * Execute an API provider via HTTP POST
75
+ * @param {string} prompt - The prompt to send
76
+ * @param {Object} options - Execution options
77
+ * @param {string} options.url - API endpoint URL
78
+ * @param {string} options.model - Model name
79
+ * @param {string} options.apiKey - API key
80
+ * @param {Function} options.fetch - Fetch function (injectable)
81
+ * @param {number} options.timeout - Timeout in ms
82
+ * @returns {Promise<{response: string, model: string, tokens: number}>}
83
+ */
84
+ async function executeApiProvider(prompt, options = {}) {
85
+ const { url, model, apiKey, timeout } = options;
86
+ const fetchFn = options.fetch || globalThis.fetch;
87
+
88
+ const body = {
89
+ model,
90
+ messages: [{ role: 'user', content: prompt }],
91
+ };
92
+
93
+ const headers = { 'Content-Type': 'application/json' };
94
+ if (apiKey) {
95
+ headers['Authorization'] = 'Bearer ' + apiKey;
96
+ }
97
+
98
+ let fetchPromise = fetchFn(url, {
99
+ method: 'POST',
100
+ headers,
101
+ body: JSON.stringify(body),
102
+ });
103
+
104
+ if (timeout) {
105
+ fetchPromise = Promise.race([
106
+ fetchPromise,
107
+ new Promise((_, reject) =>
108
+ setTimeout(() => reject(new Error('API provider timeout after ' + timeout + 'ms')), timeout)
109
+ ),
110
+ ]);
111
+ }
112
+
113
+ const resp = await fetchPromise;
114
+
115
+ if (!resp.ok) {
116
+ throw new Error('API returned status ' + resp.status);
117
+ }
118
+
119
+ const data = await resp.json();
120
+ const content = data.choices?.[0]?.message?.content || '';
121
+
122
+ return {
123
+ response: content,
124
+ model: data.model || model,
125
+ tokens: data.usage?.total_tokens || 0,
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Create a unified executor with injectable dependencies
131
+ * @param {Object} deps - Dependencies
132
+ * @param {Function} deps.spawn - Spawn function
133
+ * @param {Function} deps.fetch - Fetch function
134
+ * @returns {Object} Executor with execute method
135
+ */
136
+ function createExecutor(deps = {}) {
137
+ return {
138
+ execute: async (prompt, provider) => {
139
+ const start = Date.now();
140
+
141
+ let result;
142
+ if (provider.type === 'api') {
143
+ result = await executeApiProvider(prompt, {
144
+ ...provider,
145
+ fetch: deps.fetch,
146
+ });
147
+ } else {
148
+ result = await executeCliProvider(prompt, {
149
+ ...provider,
150
+ spawn: deps.spawn,
151
+ });
152
+ }
153
+
154
+ return {
155
+ response: result.response,
156
+ model: provider.model || provider.command || 'unknown',
157
+ latency: Date.now() - start,
158
+ tokens: result.tokens || 0,
159
+ };
160
+ },
161
+ };
162
+ }
163
+
164
+ module.exports = {
165
+ executeCliProvider,
166
+ executeApiProvider,
167
+ createExecutor,
168
+ };
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Provider Executor Tests
3
+ *
4
+ * Actually execute LLM requests through any provider.
5
+ * The bridge between "provider detected" and "review completed."
6
+ */
7
+ import { describe, it, expect, vi } from 'vitest';
8
+
9
+ const {
10
+ executeCliProvider,
11
+ executeApiProvider,
12
+ createExecutor,
13
+ } = require('./provider-executor.js');
14
+
15
+ describe('Provider Executor', () => {
16
+ describe('executeCliProvider', () => {
17
+ it('executes CLI provider via spawn', async () => {
18
+ const mockSpawn = vi.fn().mockReturnValue({
19
+ stdout: { on: vi.fn((ev, cb) => { if (ev === 'data') cb(Buffer.from('review result')); }) },
20
+ stderr: { on: vi.fn() },
21
+ stdin: { write: vi.fn(), end: vi.fn() },
22
+ on: vi.fn((ev, cb) => { if (ev === 'close') cb(0); }),
23
+ });
24
+
25
+ const result = await executeCliProvider('Review this code', {
26
+ command: 'codex',
27
+ args: [],
28
+ spawn: mockSpawn,
29
+ });
30
+
31
+ expect(mockSpawn).toHaveBeenCalled();
32
+ expect(result.response).toContain('review result');
33
+ });
34
+
35
+ it('passes prompt as stdin to CLI', async () => {
36
+ let writtenData = '';
37
+ const mockSpawn = vi.fn().mockReturnValue({
38
+ stdout: { on: vi.fn((ev, cb) => { if (ev === 'data') cb(Buffer.from('ok')); }) },
39
+ stderr: { on: vi.fn() },
40
+ stdin: { write: vi.fn((d) => { writtenData = d; }), end: vi.fn() },
41
+ on: vi.fn((ev, cb) => { if (ev === 'close') cb(0); }),
42
+ });
43
+
44
+ await executeCliProvider('my prompt text', {
45
+ command: 'codex',
46
+ args: [],
47
+ spawn: mockSpawn,
48
+ });
49
+
50
+ expect(writtenData).toContain('my prompt text');
51
+ });
52
+
53
+ it('captures stdout as response', async () => {
54
+ const mockSpawn = vi.fn().mockReturnValue({
55
+ stdout: { on: vi.fn((ev, cb) => {
56
+ if (ev === 'data') {
57
+ cb(Buffer.from('chunk1'));
58
+ cb(Buffer.from('chunk2'));
59
+ }
60
+ }) },
61
+ stderr: { on: vi.fn() },
62
+ stdin: { write: vi.fn(), end: vi.fn() },
63
+ on: vi.fn((ev, cb) => { if (ev === 'close') cb(0); }),
64
+ });
65
+
66
+ const result = await executeCliProvider('prompt', {
67
+ command: 'codex',
68
+ args: [],
69
+ spawn: mockSpawn,
70
+ });
71
+
72
+ expect(result.response).toBe('chunk1chunk2');
73
+ });
74
+
75
+ it('handles CLI timeout (kills process)', async () => {
76
+ const killFn = vi.fn();
77
+ const mockSpawn = vi.fn().mockReturnValue({
78
+ stdout: { on: vi.fn() },
79
+ stderr: { on: vi.fn() },
80
+ stdin: { write: vi.fn(), end: vi.fn() },
81
+ on: vi.fn(), // never calls close
82
+ kill: killFn,
83
+ });
84
+
85
+ await expect(
86
+ executeCliProvider('prompt', {
87
+ command: 'codex',
88
+ args: [],
89
+ spawn: mockSpawn,
90
+ timeout: 50,
91
+ })
92
+ ).rejects.toThrow(/timeout/i);
93
+ });
94
+
95
+ it('handles provider exit code != 0', async () => {
96
+ const mockSpawn = vi.fn().mockReturnValue({
97
+ stdout: { on: vi.fn() },
98
+ stderr: { on: vi.fn((ev, cb) => { if (ev === 'data') cb(Buffer.from('error occurred')); }) },
99
+ stdin: { write: vi.fn(), end: vi.fn() },
100
+ on: vi.fn((ev, cb) => { if (ev === 'close') cb(1); }),
101
+ });
102
+
103
+ await expect(
104
+ executeCliProvider('prompt', {
105
+ command: 'codex',
106
+ args: [],
107
+ spawn: mockSpawn,
108
+ })
109
+ ).rejects.toThrow(/exit code 1/i);
110
+ });
111
+
112
+ it('handles empty response', async () => {
113
+ const mockSpawn = vi.fn().mockReturnValue({
114
+ stdout: { on: vi.fn() },
115
+ stderr: { on: vi.fn() },
116
+ stdin: { write: vi.fn(), end: vi.fn() },
117
+ on: vi.fn((ev, cb) => { if (ev === 'close') cb(0); }),
118
+ });
119
+
120
+ const result = await executeCliProvider('prompt', {
121
+ command: 'codex',
122
+ args: [],
123
+ spawn: mockSpawn,
124
+ });
125
+
126
+ expect(result.response).toBe('');
127
+ });
128
+
129
+ it('respects provider-specific args from config', async () => {
130
+ let spawnedArgs = [];
131
+ const mockSpawn = vi.fn().mockImplementation((cmd, args) => {
132
+ spawnedArgs = args;
133
+ return {
134
+ stdout: { on: vi.fn((ev, cb) => { if (ev === 'data') cb(Buffer.from('ok')); }) },
135
+ stderr: { on: vi.fn() },
136
+ stdin: { write: vi.fn(), end: vi.fn() },
137
+ on: vi.fn((ev, cb) => { if (ev === 'close') cb(0); }),
138
+ };
139
+ });
140
+
141
+ await executeCliProvider('prompt', {
142
+ command: 'codex',
143
+ args: ['--model', 'gpt-4o', '--quiet'],
144
+ spawn: mockSpawn,
145
+ });
146
+
147
+ expect(spawnedArgs).toContain('--model');
148
+ expect(spawnedArgs).toContain('gpt-4o');
149
+ });
150
+
151
+ it('strips ANSI codes from CLI output', async () => {
152
+ const ansiOutput = '\x1b[31mred text\x1b[0m normal';
153
+ const mockSpawn = vi.fn().mockReturnValue({
154
+ stdout: { on: vi.fn((ev, cb) => { if (ev === 'data') cb(Buffer.from(ansiOutput)); }) },
155
+ stderr: { on: vi.fn() },
156
+ stdin: { write: vi.fn(), end: vi.fn() },
157
+ on: vi.fn((ev, cb) => { if (ev === 'close') cb(0); }),
158
+ });
159
+
160
+ const result = await executeCliProvider('prompt', {
161
+ command: 'codex',
162
+ args: [],
163
+ spawn: mockSpawn,
164
+ });
165
+
166
+ expect(result.response).not.toContain('\x1b[');
167
+ expect(result.response).toContain('red text');
168
+ });
169
+ });
170
+
171
+ describe('executeApiProvider', () => {
172
+ it('executes API provider via HTTP POST', async () => {
173
+ const mockFetch = vi.fn().mockResolvedValue({
174
+ ok: true,
175
+ json: () => Promise.resolve({
176
+ choices: [{ message: { content: 'review response' } }],
177
+ }),
178
+ });
179
+
180
+ const result = await executeApiProvider('prompt', {
181
+ url: 'http://localhost:4000/v1/chat/completions',
182
+ model: 'gpt-4o',
183
+ apiKey: 'test-key',
184
+ fetch: mockFetch,
185
+ });
186
+
187
+ expect(result.response).toBe('review response');
188
+ expect(mockFetch).toHaveBeenCalledWith(
189
+ expect.any(String),
190
+ expect.objectContaining({ method: 'POST' })
191
+ );
192
+ });
193
+
194
+ it('handles API timeout', async () => {
195
+ const mockFetch = vi.fn().mockImplementation(() =>
196
+ new Promise((resolve) => setTimeout(resolve, 10000))
197
+ );
198
+
199
+ await expect(
200
+ executeApiProvider('prompt', {
201
+ url: 'http://localhost:4000/v1/chat/completions',
202
+ model: 'gpt-4o',
203
+ fetch: mockFetch,
204
+ timeout: 50,
205
+ })
206
+ ).rejects.toThrow(/timeout/i);
207
+ });
208
+ });
209
+
210
+ describe('createExecutor', () => {
211
+ it('returns standardized { response, model, latency } format', async () => {
212
+ const mockSpawn = vi.fn().mockReturnValue({
213
+ stdout: { on: vi.fn((ev, cb) => { if (ev === 'data') cb(Buffer.from('result')); }) },
214
+ stderr: { on: vi.fn() },
215
+ stdin: { write: vi.fn(), end: vi.fn() },
216
+ on: vi.fn((ev, cb) => { if (ev === 'close') cb(0); }),
217
+ });
218
+
219
+ const executor = createExecutor({ spawn: mockSpawn });
220
+ const result = await executor.execute('prompt', {
221
+ type: 'cli',
222
+ command: 'codex',
223
+ args: [],
224
+ model: 'gpt-4o',
225
+ });
226
+
227
+ expect(result).toMatchObject({
228
+ response: expect.any(String),
229
+ model: 'gpt-4o',
230
+ latency: expect.any(Number),
231
+ });
232
+ });
233
+
234
+ it('works with injectable spawn/fetch for testing', () => {
235
+ const executor = createExecutor({
236
+ spawn: vi.fn(),
237
+ fetch: vi.fn(),
238
+ });
239
+
240
+ expect(executor).toBeDefined();
241
+ expect(executor.execute).toBeDefined();
242
+ });
243
+ });
244
+ });