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.
- package/dashboard/dist/components/ContainerSecurityPane.d.ts +45 -0
- package/dashboard/dist/components/ContainerSecurityPane.js +44 -0
- package/dashboard/dist/components/ContainerSecurityPane.test.d.ts +1 -0
- package/dashboard/dist/components/ContainerSecurityPane.test.js +153 -0
- package/package.json +1 -1
- package/server/lib/access-control.test.js +1 -1
- package/server/lib/agents-cancel-command.test.js +1 -1
- package/server/lib/agents-get-command.test.js +1 -1
- package/server/lib/agents-list-command.test.js +1 -1
- package/server/lib/agents-logs-command.test.js +1 -1
- package/server/lib/agents-retry-command.test.js +1 -1
- package/server/lib/budget-limits.test.js +2 -2
- package/server/lib/code-gate/bypass-logger.js +129 -0
- package/server/lib/code-gate/bypass-logger.test.js +142 -0
- package/server/lib/code-gate/first-commit-audit.js +138 -0
- package/server/lib/code-gate/first-commit-audit.test.js +203 -0
- package/server/lib/code-gate/gate-command.js +114 -0
- package/server/lib/code-gate/gate-command.test.js +111 -0
- package/server/lib/code-gate/gate-config.js +163 -0
- package/server/lib/code-gate/gate-config.test.js +181 -0
- package/server/lib/code-gate/gate-engine.js +193 -0
- package/server/lib/code-gate/gate-engine.test.js +258 -0
- package/server/lib/code-gate/gate-reporter.js +123 -0
- package/server/lib/code-gate/gate-reporter.test.js +159 -0
- package/server/lib/code-gate/hooks-generator.js +149 -0
- package/server/lib/code-gate/hooks-generator.test.js +142 -0
- package/server/lib/code-gate/llm-reviewer.js +176 -0
- package/server/lib/code-gate/llm-reviewer.test.js +161 -0
- package/server/lib/code-gate/multi-model-reviewer.js +172 -0
- package/server/lib/code-gate/multi-model-reviewer.test.js +217 -0
- package/server/lib/code-gate/push-gate.js +133 -0
- package/server/lib/code-gate/push-gate.test.js +190 -0
- package/server/lib/code-gate/rules/architecture-rules.js +228 -0
- package/server/lib/code-gate/rules/architecture-rules.test.js +155 -0
- package/server/lib/code-gate/rules/client-rules.js +120 -0
- package/server/lib/code-gate/rules/client-rules.test.js +121 -0
- package/server/lib/code-gate/rules/config-rules.js +140 -0
- package/server/lib/code-gate/rules/config-rules.test.js +103 -0
- package/server/lib/code-gate/rules/database-rules.js +158 -0
- package/server/lib/code-gate/rules/database-rules.test.js +119 -0
- package/server/lib/code-gate/rules/docker-rules.js +201 -0
- package/server/lib/code-gate/rules/docker-rules.test.js +104 -0
- package/server/lib/code-gate/rules/quality-rules.js +304 -0
- package/server/lib/code-gate/rules/quality-rules.test.js +199 -0
- package/server/lib/code-gate/rules/security-rules.js +228 -0
- package/server/lib/code-gate/rules/security-rules.test.js +131 -0
- package/server/lib/code-gate/rules/structure-rules.js +155 -0
- package/server/lib/code-gate/rules/structure-rules.test.js +107 -0
- package/server/lib/code-gate/rules/test-rules.js +93 -0
- package/server/lib/code-gate/rules/test-rules.test.js +97 -0
- package/server/lib/code-gate/typescript-gate.js +128 -0
- package/server/lib/code-gate/typescript-gate.test.js +131 -0
- package/server/lib/code-generator.test.js +1 -1
- package/server/lib/cost-command.test.js +1 -1
- package/server/lib/cost-optimizer.test.js +1 -1
- package/server/lib/cost-projections.test.js +1 -1
- package/server/lib/cost-reports.test.js +1 -1
- package/server/lib/cost-tracker.test.js +1 -1
- package/server/lib/crypto-patterns.test.js +1 -1
- package/server/lib/design-command.test.js +1 -1
- package/server/lib/design-parser.test.js +1 -1
- package/server/lib/gemini-vision.test.js +1 -1
- package/server/lib/infra/infra-generator.js +331 -0
- package/server/lib/infra/infra-generator.test.js +146 -0
- package/server/lib/input-validator.test.js +1 -1
- package/server/lib/litellm-client.test.js +1 -1
- package/server/lib/litellm-command.test.js +1 -1
- package/server/lib/litellm-config.test.js +1 -1
- package/server/lib/llm/adapters/api-adapter.js +95 -0
- package/server/lib/llm/adapters/api-adapter.test.js +81 -0
- package/server/lib/llm/adapters/codex-adapter.js +85 -0
- package/server/lib/llm/adapters/codex-adapter.test.js +54 -0
- package/server/lib/llm/adapters/gemini-adapter.js +100 -0
- package/server/lib/llm/adapters/gemini-adapter.test.js +54 -0
- package/server/lib/llm/index.js +109 -0
- package/server/lib/llm/index.test.js +147 -0
- package/server/lib/llm/provider-executor.js +168 -0
- package/server/lib/llm/provider-executor.test.js +244 -0
- package/server/lib/llm/provider-registry.js +104 -0
- package/server/lib/llm/provider-registry.test.js +157 -0
- package/server/lib/llm/review-service.js +222 -0
- package/server/lib/llm/review-service.test.js +220 -0
- package/server/lib/model-pricing.test.js +1 -1
- package/server/lib/models-command.test.js +1 -1
- package/server/lib/optimize-command.test.js +1 -1
- package/server/lib/orchestration-integration.test.js +1 -1
- package/server/lib/output-encoder.test.js +1 -1
- package/server/lib/quality-evaluator.test.js +1 -1
- package/server/lib/quality-gate-command.test.js +1 -1
- package/server/lib/quality-gate-scorer.test.js +1 -1
- package/server/lib/quality-history.test.js +1 -1
- package/server/lib/quality-presets.test.js +1 -1
- package/server/lib/quality-retry.test.js +1 -1
- package/server/lib/quality-thresholds.test.js +1 -1
- package/server/lib/secure-auth.test.js +1 -1
- package/server/lib/secure-code-command.test.js +1 -1
- package/server/lib/secure-errors.test.js +1 -1
- package/server/lib/security/auth-security.test.js +4 -3
- package/server/lib/shame/shame-registry.js +224 -0
- package/server/lib/shame/shame-registry.test.js +202 -0
- package/server/lib/standards/cleanup-dry-run.js +254 -0
- package/server/lib/standards/cleanup-dry-run.test.js +220 -0
- package/server/lib/vision-command.test.js +1 -1
- package/server/lib/visual-command.test.js +1 -1
- 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
|
+
});
|