@studio-foundation/runner 0.3.0-beta.1
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/ARCHITECTURE.md +53 -0
- package/LICENSE +663 -0
- package/README.md +125 -0
- package/configs/agents/analyst.agent.yaml +31 -0
- package/configs/agents/code-generator.agent.yaml +31 -0
- package/configs/agents/generic.agent.yaml +23 -0
- package/dist/__tests__/script-executor.test.d.ts +2 -0
- package/dist/__tests__/script-executor.test.d.ts.map +1 -0
- package/dist/__tests__/script-executor.test.js +173 -0
- package/dist/__tests__/script-executor.test.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/integration-loader.d.ts +20 -0
- package/dist/integrations/integration-loader.d.ts.map +1 -0
- package/dist/integrations/integration-loader.js +71 -0
- package/dist/integrations/integration-loader.js.map +1 -0
- package/dist/integrations/integration-loader.test.d.ts +2 -0
- package/dist/integrations/integration-loader.test.d.ts.map +1 -0
- package/dist/integrations/integration-loader.test.js +77 -0
- package/dist/integrations/integration-loader.test.js.map +1 -0
- package/dist/middleware/anonymization.d.ts +22 -0
- package/dist/middleware/anonymization.d.ts.map +1 -0
- package/dist/middleware/anonymization.js +37 -0
- package/dist/middleware/anonymization.js.map +1 -0
- package/dist/plugins/index.d.ts +5 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +10 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/plugins/mcp-client.d.ts +19 -0
- package/dist/plugins/mcp-client.d.ts.map +1 -0
- package/dist/plugins/mcp-client.js +126 -0
- package/dist/plugins/mcp-client.js.map +1 -0
- package/dist/plugins/mcp-client.test.d.ts +2 -0
- package/dist/plugins/mcp-client.test.d.ts.map +1 -0
- package/dist/plugins/mcp-client.test.js +160 -0
- package/dist/plugins/mcp-client.test.js.map +1 -0
- package/dist/plugins/oauth-provider.d.ts +27 -0
- package/dist/plugins/oauth-provider.d.ts.map +1 -0
- package/dist/plugins/oauth-provider.js +140 -0
- package/dist/plugins/oauth-provider.js.map +1 -0
- package/dist/plugins/oauth-provider.test.d.ts +2 -0
- package/dist/plugins/oauth-provider.test.d.ts.map +1 -0
- package/dist/plugins/oauth-provider.test.js +144 -0
- package/dist/plugins/oauth-provider.test.js.map +1 -0
- package/dist/plugins/plugin-loader.d.ts +29 -0
- package/dist/plugins/plugin-loader.d.ts.map +1 -0
- package/dist/plugins/plugin-loader.js +71 -0
- package/dist/plugins/plugin-loader.js.map +1 -0
- package/dist/plugins/plugin-loader.test.d.ts +2 -0
- package/dist/plugins/plugin-loader.test.d.ts.map +1 -0
- package/dist/plugins/plugin-loader.test.js +87 -0
- package/dist/plugins/plugin-loader.test.js.map +1 -0
- package/dist/prompt-builder.d.ts +47 -0
- package/dist/prompt-builder.d.ts.map +1 -0
- package/dist/prompt-builder.js +263 -0
- package/dist/prompt-builder.js.map +1 -0
- package/dist/prompt-builder.test.d.ts +2 -0
- package/dist/prompt-builder.test.d.ts.map +1 -0
- package/dist/prompt-builder.test.js +154 -0
- package/dist/prompt-builder.test.js.map +1 -0
- package/dist/providers/anthropic.d.ts +14 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +118 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/anthropic.test.d.ts +2 -0
- package/dist/providers/anthropic.test.d.ts.map +1 -0
- package/dist/providers/anthropic.test.js +76 -0
- package/dist/providers/anthropic.test.js.map +1 -0
- package/dist/providers/mock.d.ts +17 -0
- package/dist/providers/mock.d.ts.map +1 -0
- package/dist/providers/mock.js +39 -0
- package/dist/providers/mock.js.map +1 -0
- package/dist/providers/ollama.d.ts +18 -0
- package/dist/providers/ollama.d.ts.map +1 -0
- package/dist/providers/ollama.js +155 -0
- package/dist/providers/ollama.js.map +1 -0
- package/dist/providers/ollama.test.d.ts +2 -0
- package/dist/providers/ollama.test.d.ts.map +1 -0
- package/dist/providers/ollama.test.js +123 -0
- package/dist/providers/ollama.test.js.map +1 -0
- package/dist/providers/openai-responses.d.ts +18 -0
- package/dist/providers/openai-responses.d.ts.map +1 -0
- package/dist/providers/openai-responses.js +175 -0
- package/dist/providers/openai-responses.js.map +1 -0
- package/dist/providers/openai.d.ts +16 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +132 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/openai.test.d.ts +2 -0
- package/dist/providers/openai.test.d.ts.map +1 -0
- package/dist/providers/openai.test.js +51 -0
- package/dist/providers/openai.test.js.map +1 -0
- package/dist/providers/provider.d.ts +42 -0
- package/dist/providers/provider.d.ts.map +1 -0
- package/dist/providers/provider.js +10 -0
- package/dist/providers/provider.js.map +1 -0
- package/dist/providers/registry.d.ts +42 -0
- package/dist/providers/registry.d.ts.map +1 -0
- package/dist/providers/registry.js +65 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/runner.d.ts +46 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +324 -0
- package/dist/runner.js.map +1 -0
- package/dist/runner.test.d.ts +2 -0
- package/dist/runner.test.d.ts.map +1 -0
- package/dist/runner.test.js +280 -0
- package/dist/runner.test.js.map +1 -0
- package/dist/script-executor.d.ts +11 -0
- package/dist/script-executor.d.ts.map +1 -0
- package/dist/script-executor.js +85 -0
- package/dist/script-executor.js.map +1 -0
- package/dist/tools/builtin/git.d.ts +6 -0
- package/dist/tools/builtin/git.d.ts.map +1 -0
- package/dist/tools/builtin/git.js +294 -0
- package/dist/tools/builtin/git.js.map +1 -0
- package/dist/tools/builtin/patch.d.ts +6 -0
- package/dist/tools/builtin/patch.d.ts.map +1 -0
- package/dist/tools/builtin/patch.js +238 -0
- package/dist/tools/builtin/patch.js.map +1 -0
- package/dist/tools/builtin/repo-manager.d.ts +6 -0
- package/dist/tools/builtin/repo-manager.d.ts.map +1 -0
- package/dist/tools/builtin/repo-manager.js +171 -0
- package/dist/tools/builtin/repo-manager.js.map +1 -0
- package/dist/tools/builtin/search.d.ts +6 -0
- package/dist/tools/builtin/search.d.ts.map +1 -0
- package/dist/tools/builtin/search.js +104 -0
- package/dist/tools/builtin/search.js.map +1 -0
- package/dist/tools/builtin/shell.d.ts +6 -0
- package/dist/tools/builtin/shell.d.ts.map +1 -0
- package/dist/tools/builtin/shell.js +78 -0
- package/dist/tools/builtin/shell.js.map +1 -0
- package/dist/tools/builtin/studio-run.d.ts +12 -0
- package/dist/tools/builtin/studio-run.d.ts.map +1 -0
- package/dist/tools/builtin/studio-run.js +61 -0
- package/dist/tools/builtin/studio-run.js.map +1 -0
- package/dist/tools/builtin/web-search.d.ts +7 -0
- package/dist/tools/builtin/web-search.d.ts.map +1 -0
- package/dist/tools/builtin/web-search.js +94 -0
- package/dist/tools/builtin/web-search.js.map +1 -0
- package/dist/tools/builtin/web-search.test.d.ts +5 -0
- package/dist/tools/builtin/web-search.test.d.ts.map +1 -0
- package/dist/tools/builtin/web-search.test.js +90 -0
- package/dist/tools/builtin/web-search.test.js.map +1 -0
- package/dist/tools/errors.d.ts +4 -0
- package/dist/tools/errors.d.ts.map +1 -0
- package/dist/tools/errors.js +11 -0
- package/dist/tools/errors.js.map +1 -0
- package/dist/tools/errors.test.d.ts +2 -0
- package/dist/tools/errors.test.d.ts.map +1 -0
- package/dist/tools/errors.test.js +14 -0
- package/dist/tools/errors.test.js.map +1 -0
- package/dist/tools/plugin-loader.d.ts +31 -0
- package/dist/tools/plugin-loader.d.ts.map +1 -0
- package/dist/tools/plugin-loader.js +183 -0
- package/dist/tools/plugin-loader.js.map +1 -0
- package/dist/tools/plugin-loader.test.d.ts +2 -0
- package/dist/tools/plugin-loader.test.d.ts.map +1 -0
- package/dist/tools/plugin-loader.test.js +116 -0
- package/dist/tools/plugin-loader.test.js.map +1 -0
- package/dist/tools/skills/skill-loader.d.ts +23 -0
- package/dist/tools/skills/skill-loader.d.ts.map +1 -0
- package/dist/tools/skills/skill-loader.js +58 -0
- package/dist/tools/skills/skill-loader.js.map +1 -0
- package/dist/tools/skills/skill-loader.test.d.ts +2 -0
- package/dist/tools/skills/skill-loader.test.d.ts.map +1 -0
- package/dist/tools/skills/skill-loader.test.js +92 -0
- package/dist/tools/skills/skill-loader.test.js.map +1 -0
- package/dist/tools/tool-executor.d.ts +12 -0
- package/dist/tools/tool-executor.d.ts.map +1 -0
- package/dist/tools/tool-executor.js +76 -0
- package/dist/tools/tool-executor.js.map +1 -0
- package/dist/tools/tool-executor.test.d.ts +2 -0
- package/dist/tools/tool-executor.test.d.ts.map +1 -0
- package/dist/tools/tool-executor.test.js +83 -0
- package/dist/tools/tool-executor.test.js.map +1 -0
- package/dist/tools/tool-registry.d.ts +65 -0
- package/dist/tools/tool-registry.d.ts.map +1 -0
- package/dist/tools/tool-registry.js +112 -0
- package/dist/tools/tool-registry.js.map +1 -0
- package/dist/tools/yaml-executor.d.ts +22 -0
- package/dist/tools/yaml-executor.d.ts.map +1 -0
- package/dist/tools/yaml-executor.js +80 -0
- package/dist/tools/yaml-executor.js.map +1 -0
- package/dist/utils/race-signal.d.ts +6 -0
- package/dist/utils/race-signal.d.ts.map +1 -0
- package/dist/utils/race-signal.js +21 -0
- package/dist/utils/race-signal.js.map +1 -0
- package/dist/utils/race-signal.test.d.ts +2 -0
- package/dist/utils/race-signal.test.d.ts.map +1 -0
- package/dist/utils/race-signal.test.js +48 -0
- package/dist/utils/race-signal.test.js.map +1 -0
- package/package.json +36 -0
- package/src/__tests__/script-executor.test.ts +180 -0
- package/src/index.ts +68 -0
- package/src/integrations/integration-loader.test.ts +88 -0
- package/src/integrations/integration-loader.ts +68 -0
- package/src/middleware/anonymization.ts +38 -0
- package/src/plugins/index.ts +4 -0
- package/src/plugins/mcp-client.test.ts +148 -0
- package/src/plugins/mcp-client.ts +128 -0
- package/src/plugins/oauth-provider.test.ts +167 -0
- package/src/plugins/oauth-provider.ts +175 -0
- package/src/plugins/plugin-loader.test.ts +114 -0
- package/src/plugins/plugin-loader.ts +90 -0
- package/src/prompt-builder.test.ts +167 -0
- package/src/prompt-builder.ts +332 -0
- package/src/providers/anthropic.test.ts +101 -0
- package/src/providers/anthropic.ts +135 -0
- package/src/providers/mock.ts +57 -0
- package/src/providers/ollama.test.ts +166 -0
- package/src/providers/ollama.ts +152 -0
- package/src/providers/openai-responses.ts +212 -0
- package/src/providers/openai.test.ts +67 -0
- package/src/providers/openai.ts +139 -0
- package/src/providers/provider.ts +54 -0
- package/src/providers/registry.ts +77 -0
- package/src/runner.test.ts +343 -0
- package/src/runner.ts +396 -0
- package/src/script-executor.ts +107 -0
- package/src/tools/builtin/git.ts +311 -0
- package/src/tools/builtin/patch.ts +257 -0
- package/src/tools/builtin/repo-manager.ts +142 -0
- package/src/tools/builtin/search.ts +108 -0
- package/src/tools/builtin/shell.ts +82 -0
- package/src/tools/builtin/studio-run.ts +73 -0
- package/src/tools/builtin/web-search.test.ts +122 -0
- package/src/tools/builtin/web-search.ts +101 -0
- package/src/tools/errors.test.ts +12 -0
- package/src/tools/errors.ts +6 -0
- package/src/tools/plugin-loader.test.ts +130 -0
- package/src/tools/plugin-loader.ts +203 -0
- package/src/tools/skills/README.md +49 -0
- package/src/tools/skills/skill-loader.test.ts +106 -0
- package/src/tools/skills/skill-loader.ts +62 -0
- package/src/tools/tool-executor.test.ts +88 -0
- package/src/tools/tool-executor.ts +84 -0
- package/src/tools/tool-registry.ts +130 -0
- package/src/tools/yaml-executor.ts +120 -0
- package/src/utils/race-signal.test.ts +50 -0
- package/src/utils/race-signal.ts +17 -0
- package/templates/integrations/linear.integration.yaml +35 -0
- package/templates/integrations/slack.integration.yaml +22 -0
- package/templates/integrations/webhook.integration.yaml +17 -0
- package/templates/tools/git.tool.yaml +80 -0
- package/templates/tools/repo-manager.tool.yaml +64 -0
- package/templates/tools/search.tool.yaml +22 -0
- package/templates/tools/shell.tool.yaml +19 -0
- package/templates/tools/web-search.tool.yaml +24 -0
- package/tests/anonymization-middleware.test.ts +61 -0
- package/tests/anthropic.test.ts +87 -0
- package/tests/apply-patch.test.ts +355 -0
- package/tests/fixtures/tools/test-builtin.tool.yaml +14 -0
- package/tests/fixtures/tools/test-shell.tool.yaml +19 -0
- package/tests/mock-provider.test.ts +104 -0
- package/tests/openai.test.ts +72 -0
- package/tests/plugin-loader.test.ts +54 -0
- package/tests/prompt-builder.test.ts +468 -0
- package/tests/runner-anonymization.test.ts +89 -0
- package/tests/runner.test.ts +885 -0
- package/tests/studio-run.test.ts +94 -0
- package/tests/tool-executor.test.ts +115 -0
- package/tests/tool-registry.test.ts +84 -0
- package/tests/yaml-executor.test.ts +76 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic provider tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
6
|
+
|
|
7
|
+
// Mock the Anthropic SDK BEFORE importing the provider
|
|
8
|
+
const mockFinalMessage = {
|
|
9
|
+
content: [{ type: 'text', text: 'Hello world' }],
|
|
10
|
+
stop_reason: 'end_turn',
|
|
11
|
+
usage: { input_tokens: 10, output_tokens: 5, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const mockCreate = vi.fn().mockResolvedValue({
|
|
15
|
+
content: [{ type: 'text', text: 'Hello world' }],
|
|
16
|
+
stop_reason: 'end_turn',
|
|
17
|
+
usage: { input_tokens: 10, output_tokens: 5, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const mockStreamFn = vi.fn();
|
|
21
|
+
|
|
22
|
+
vi.mock('@anthropic-ai/sdk', () => ({
|
|
23
|
+
default: vi.fn(function () {
|
|
24
|
+
return { messages: { create: mockCreate, stream: mockStreamFn } };
|
|
25
|
+
}),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
import { AnthropicProvider } from '../src/providers/anthropic.js';
|
|
29
|
+
import type { LLMRequest } from '@studio-foundation/contracts';
|
|
30
|
+
|
|
31
|
+
const baseRequest: LLMRequest = {
|
|
32
|
+
model: 'claude-haiku-4-5',
|
|
33
|
+
messages: [{ role: 'user', content: 'Say hello' }],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
describe('AnthropicProvider', () => {
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
vi.clearAllMocks();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('calls messages.create when no onToken provided', async () => {
|
|
42
|
+
mockCreate.mockResolvedValueOnce({
|
|
43
|
+
content: [{ type: 'text', text: 'Hello world' }],
|
|
44
|
+
stop_reason: 'end_turn',
|
|
45
|
+
usage: { input_tokens: 10, output_tokens: 5, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 },
|
|
46
|
+
});
|
|
47
|
+
const provider = new AnthropicProvider('test-key');
|
|
48
|
+
await provider.call(baseRequest);
|
|
49
|
+
expect(mockCreate).toHaveBeenCalledTimes(1);
|
|
50
|
+
expect(mockStreamFn).not.toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('calls messages.stream when onToken is provided', async () => {
|
|
54
|
+
const tokens: string[] = [];
|
|
55
|
+
mockStreamFn.mockReturnValueOnce({
|
|
56
|
+
on: vi.fn((event: string, handler: (text: string) => void) => {
|
|
57
|
+
if (event === 'text') {
|
|
58
|
+
handler('Hello');
|
|
59
|
+
handler(' world');
|
|
60
|
+
}
|
|
61
|
+
}),
|
|
62
|
+
finalMessage: vi.fn().mockResolvedValue(mockFinalMessage),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const provider = new AnthropicProvider('test-key');
|
|
66
|
+
await provider.call(baseRequest, (t) => tokens.push(t));
|
|
67
|
+
|
|
68
|
+
expect(mockStreamFn).toHaveBeenCalledTimes(1);
|
|
69
|
+
expect(mockCreate).not.toHaveBeenCalled();
|
|
70
|
+
expect(tokens).toEqual(['Hello', ' world']);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('returns full LLMResponse with usage when streaming', async () => {
|
|
74
|
+
mockStreamFn.mockReturnValueOnce({
|
|
75
|
+
on: vi.fn((event: string, handler: (text: string) => void) => {
|
|
76
|
+
if (event === 'text') handler('Hello world');
|
|
77
|
+
}),
|
|
78
|
+
finalMessage: vi.fn().mockResolvedValue(mockFinalMessage),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const provider = new AnthropicProvider('test-key');
|
|
82
|
+
const result = await provider.call(baseRequest, () => {});
|
|
83
|
+
expect(result.content).toBe('Hello world');
|
|
84
|
+
expect(result.usage?.prompt_tokens).toBe(10);
|
|
85
|
+
expect(result.usage?.completion_tokens).toBe(5);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import { createPatchTools } from '../src/tools/builtin/patch.js';
|
|
6
|
+
import type { Tool } from '../src/tools/tool-registry.js';
|
|
7
|
+
|
|
8
|
+
describe('apply_patch tool', () => {
|
|
9
|
+
let tmpDir: string;
|
|
10
|
+
let tool: Tool;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'patch-test-'));
|
|
14
|
+
const tools = createPatchTools(tmpDir);
|
|
15
|
+
tool = tools[0];
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('applies a simple single-hunk patch', async () => {
|
|
23
|
+
await fs.writeFile(path.join(tmpDir, 'test.txt'), [
|
|
24
|
+
'line 1',
|
|
25
|
+
'line 2',
|
|
26
|
+
'line 3',
|
|
27
|
+
'line 4',
|
|
28
|
+
'line 5',
|
|
29
|
+
].join('\n'));
|
|
30
|
+
|
|
31
|
+
const patch = [
|
|
32
|
+
'@@ -1,5 +1,5 @@',
|
|
33
|
+
' line 1',
|
|
34
|
+
' line 2',
|
|
35
|
+
'-line 3',
|
|
36
|
+
'+line 3 modified',
|
|
37
|
+
' line 4',
|
|
38
|
+
' line 5',
|
|
39
|
+
].join('\n');
|
|
40
|
+
|
|
41
|
+
const result = await tool.execute({ path: 'test.txt', patch });
|
|
42
|
+
|
|
43
|
+
expect(result.success).toBe(true);
|
|
44
|
+
expect(result.output).toMatchObject({
|
|
45
|
+
hunks_applied: 1,
|
|
46
|
+
hunks_total: 1,
|
|
47
|
+
lines_added: 1,
|
|
48
|
+
lines_removed: 1,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const content = await fs.readFile(path.join(tmpDir, 'test.txt'), 'utf-8');
|
|
52
|
+
expect(content).toBe([
|
|
53
|
+
'line 1',
|
|
54
|
+
'line 2',
|
|
55
|
+
'line 3 modified',
|
|
56
|
+
'line 4',
|
|
57
|
+
'line 5',
|
|
58
|
+
].join('\n'));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('applies a multi-hunk patch', async () => {
|
|
62
|
+
await fs.writeFile(path.join(tmpDir, 'multi.txt'), [
|
|
63
|
+
'aaa', 'bbb', 'ccc', 'ddd', 'eee',
|
|
64
|
+
'fff', 'ggg', 'hhh', 'iii', 'jjj',
|
|
65
|
+
].join('\n'));
|
|
66
|
+
|
|
67
|
+
const patch = [
|
|
68
|
+
'@@ -1,5 +1,5 @@',
|
|
69
|
+
' aaa',
|
|
70
|
+
'-bbb',
|
|
71
|
+
'+bbb modified',
|
|
72
|
+
' ccc',
|
|
73
|
+
' ddd',
|
|
74
|
+
' eee',
|
|
75
|
+
'@@ -6,5 +6,5 @@',
|
|
76
|
+
' fff',
|
|
77
|
+
'-ggg',
|
|
78
|
+
'+ggg modified',
|
|
79
|
+
' hhh',
|
|
80
|
+
' iii',
|
|
81
|
+
' jjj',
|
|
82
|
+
].join('\n');
|
|
83
|
+
|
|
84
|
+
const result = await tool.execute({ path: 'multi.txt', patch });
|
|
85
|
+
|
|
86
|
+
expect(result.success).toBe(true);
|
|
87
|
+
expect(result.output).toMatchObject({
|
|
88
|
+
hunks_applied: 2,
|
|
89
|
+
hunks_total: 2,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const content = await fs.readFile(path.join(tmpDir, 'multi.txt'), 'utf-8');
|
|
93
|
+
expect(content).toContain('bbb modified');
|
|
94
|
+
expect(content).toContain('ggg modified');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('applies an addition-only hunk', async () => {
|
|
98
|
+
await fs.writeFile(path.join(tmpDir, 'add.txt'), [
|
|
99
|
+
'line 1',
|
|
100
|
+
'line 2',
|
|
101
|
+
'line 3',
|
|
102
|
+
].join('\n'));
|
|
103
|
+
|
|
104
|
+
const patch = [
|
|
105
|
+
'@@ -1,3 +1,5 @@',
|
|
106
|
+
' line 1',
|
|
107
|
+
'+new line A',
|
|
108
|
+
'+new line B',
|
|
109
|
+
' line 2',
|
|
110
|
+
' line 3',
|
|
111
|
+
].join('\n');
|
|
112
|
+
|
|
113
|
+
const result = await tool.execute({ path: 'add.txt', patch });
|
|
114
|
+
|
|
115
|
+
expect(result.success).toBe(true);
|
|
116
|
+
expect(result.output).toMatchObject({
|
|
117
|
+
lines_added: 2,
|
|
118
|
+
lines_removed: 0,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const content = await fs.readFile(path.join(tmpDir, 'add.txt'), 'utf-8');
|
|
122
|
+
expect(content).toBe([
|
|
123
|
+
'line 1',
|
|
124
|
+
'new line A',
|
|
125
|
+
'new line B',
|
|
126
|
+
'line 2',
|
|
127
|
+
'line 3',
|
|
128
|
+
].join('\n'));
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('applies a deletion-only hunk', async () => {
|
|
132
|
+
await fs.writeFile(path.join(tmpDir, 'del.txt'), [
|
|
133
|
+
'line 1',
|
|
134
|
+
'line 2',
|
|
135
|
+
'line 3',
|
|
136
|
+
'line 4',
|
|
137
|
+
].join('\n'));
|
|
138
|
+
|
|
139
|
+
const patch = [
|
|
140
|
+
'@@ -1,4 +1,2 @@',
|
|
141
|
+
' line 1',
|
|
142
|
+
'-line 2',
|
|
143
|
+
'-line 3',
|
|
144
|
+
' line 4',
|
|
145
|
+
].join('\n');
|
|
146
|
+
|
|
147
|
+
const result = await tool.execute({ path: 'del.txt', patch });
|
|
148
|
+
|
|
149
|
+
expect(result.success).toBe(true);
|
|
150
|
+
expect(result.output).toMatchObject({
|
|
151
|
+
lines_added: 0,
|
|
152
|
+
lines_removed: 2,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const content = await fs.readFile(path.join(tmpDir, 'del.txt'), 'utf-8');
|
|
156
|
+
expect(content).toBe(['line 1', 'line 4'].join('\n'));
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('returns error on context mismatch', async () => {
|
|
160
|
+
await fs.writeFile(path.join(tmpDir, 'mismatch.txt'), [
|
|
161
|
+
'actual line 1',
|
|
162
|
+
'actual line 2',
|
|
163
|
+
'actual line 3',
|
|
164
|
+
].join('\n'));
|
|
165
|
+
|
|
166
|
+
const patch = [
|
|
167
|
+
'@@ -1,3 +1,3 @@',
|
|
168
|
+
' wrong context',
|
|
169
|
+
'-actual line 2',
|
|
170
|
+
'+modified line 2',
|
|
171
|
+
' actual line 3',
|
|
172
|
+
].join('\n');
|
|
173
|
+
|
|
174
|
+
const result = await tool.execute({ path: 'mismatch.txt', patch });
|
|
175
|
+
|
|
176
|
+
expect(result.success).toBe(false);
|
|
177
|
+
expect(result.error).toContain('Context mismatch');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('returns error on file not found', async () => {
|
|
181
|
+
const patch = [
|
|
182
|
+
'@@ -1,1 +1,1 @@',
|
|
183
|
+
'-old',
|
|
184
|
+
'+new',
|
|
185
|
+
].join('\n');
|
|
186
|
+
|
|
187
|
+
const result = await tool.execute({ path: 'nonexistent.txt', patch });
|
|
188
|
+
|
|
189
|
+
expect(result.success).toBe(false);
|
|
190
|
+
expect(result.error).toContain('File not found');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('tolerates trailing whitespace differences in context', async () => {
|
|
194
|
+
await fs.writeFile(path.join(tmpDir, 'ws.txt'), [
|
|
195
|
+
'line 1 ',
|
|
196
|
+
'line 2',
|
|
197
|
+
'line 3',
|
|
198
|
+
].join('\n'));
|
|
199
|
+
|
|
200
|
+
const patch = [
|
|
201
|
+
'@@ -1,3 +1,3 @@',
|
|
202
|
+
' line 1',
|
|
203
|
+
'-line 2',
|
|
204
|
+
'+line 2 modified',
|
|
205
|
+
' line 3',
|
|
206
|
+
].join('\n');
|
|
207
|
+
|
|
208
|
+
const result = await tool.execute({ path: 'ws.txt', patch });
|
|
209
|
+
|
|
210
|
+
expect(result.success).toBe(true);
|
|
211
|
+
expect(result.output).toMatchObject({ hunks_applied: 1 });
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('matches by content when line number is wrong (offset matching)', async () => {
|
|
215
|
+
await fs.writeFile(path.join(tmpDir, 'offset.txt'), [
|
|
216
|
+
'header 1',
|
|
217
|
+
'header 2',
|
|
218
|
+
'header 3',
|
|
219
|
+
'header 4',
|
|
220
|
+
'header 5',
|
|
221
|
+
'target line A',
|
|
222
|
+
'target line B',
|
|
223
|
+
'target line C',
|
|
224
|
+
].join('\n'));
|
|
225
|
+
|
|
226
|
+
// Hunk says line 1 but content is at line 6
|
|
227
|
+
const patch = [
|
|
228
|
+
'@@ -1,3 +1,3 @@',
|
|
229
|
+
' target line A',
|
|
230
|
+
'-target line B',
|
|
231
|
+
'+target line B modified',
|
|
232
|
+
' target line C',
|
|
233
|
+
].join('\n');
|
|
234
|
+
|
|
235
|
+
const result = await tool.execute({ path: 'offset.txt', patch });
|
|
236
|
+
|
|
237
|
+
expect(result.success).toBe(true);
|
|
238
|
+
const content = await fs.readFile(path.join(tmpDir, 'offset.txt'), 'utf-8');
|
|
239
|
+
expect(content).toContain('target line B modified');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('returns error on ambiguous match', async () => {
|
|
243
|
+
await fs.writeFile(path.join(tmpDir, 'ambig.txt'), [
|
|
244
|
+
'unique header',
|
|
245
|
+
'repeat',
|
|
246
|
+
'target',
|
|
247
|
+
'repeat',
|
|
248
|
+
'repeat',
|
|
249
|
+
'target',
|
|
250
|
+
'repeat',
|
|
251
|
+
].join('\n'));
|
|
252
|
+
|
|
253
|
+
// Hint at line 50 (wrong) — forces slow path scan, finds 2 matches
|
|
254
|
+
const patch = [
|
|
255
|
+
'@@ -50,3 +50,3 @@',
|
|
256
|
+
' repeat',
|
|
257
|
+
'-target',
|
|
258
|
+
'+target modified',
|
|
259
|
+
' repeat',
|
|
260
|
+
].join('\n');
|
|
261
|
+
|
|
262
|
+
const result = await tool.execute({ path: 'ambig.txt', patch });
|
|
263
|
+
|
|
264
|
+
expect(result.success).toBe(false);
|
|
265
|
+
expect(result.error).toContain('Ambiguous');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('ignores --- and +++ headers from LLM output', async () => {
|
|
269
|
+
await fs.writeFile(path.join(tmpDir, 'headers.txt'), [
|
|
270
|
+
'line 1',
|
|
271
|
+
'line 2',
|
|
272
|
+
'line 3',
|
|
273
|
+
].join('\n'));
|
|
274
|
+
|
|
275
|
+
const patch = [
|
|
276
|
+
'--- a/headers.txt',
|
|
277
|
+
'+++ b/headers.txt',
|
|
278
|
+
'@@ -1,3 +1,3 @@',
|
|
279
|
+
' line 1',
|
|
280
|
+
'-line 2',
|
|
281
|
+
'+line 2 changed',
|
|
282
|
+
' line 3',
|
|
283
|
+
].join('\n');
|
|
284
|
+
|
|
285
|
+
const result = await tool.execute({ path: 'headers.txt', patch });
|
|
286
|
+
|
|
287
|
+
expect(result.success).toBe(true);
|
|
288
|
+
const content = await fs.readFile(path.join(tmpDir, 'headers.txt'), 'utf-8');
|
|
289
|
+
expect(content).toContain('line 2 changed');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('returns error on invalid patch format (no hunks)', async () => {
|
|
293
|
+
await fs.writeFile(path.join(tmpDir, 'file.txt'), 'content');
|
|
294
|
+
|
|
295
|
+
const result = await tool.execute({ path: 'file.txt', patch: 'not a patch' });
|
|
296
|
+
|
|
297
|
+
expect(result.success).toBe(false);
|
|
298
|
+
expect(result.error).toContain('Invalid patch format');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Bug: trailing '\n' in patch string → split('\n') produces a trailing ''
|
|
302
|
+
// which was treated as a blank context line, causing "Ambiguous match" when
|
|
303
|
+
// the file has multiple blank lines (each blank line matched the '' context).
|
|
304
|
+
it('does not fail with ambiguous match when all-addition patch ends with newline and file has blank lines', async () => {
|
|
305
|
+
await fs.writeFile(path.join(tmpDir, 'bug-report.txt'), [
|
|
306
|
+
'import A',
|
|
307
|
+
'',
|
|
308
|
+
'import B',
|
|
309
|
+
'',
|
|
310
|
+
'const x = 1',
|
|
311
|
+
'',
|
|
312
|
+
'export default x',
|
|
313
|
+
].join('\n'));
|
|
314
|
+
|
|
315
|
+
// All-addition patch ending with \n — the real-world failure pattern
|
|
316
|
+
const patch = '@@ -0,0 +1,3 @@\n+new line 1\n+new line 2\n+new line 3\n';
|
|
317
|
+
|
|
318
|
+
const result = await tool.execute({ path: 'bug-report.txt', patch });
|
|
319
|
+
|
|
320
|
+
expect(result.success).toBe(true);
|
|
321
|
+
const content = await fs.readFile(path.join(tmpDir, 'bug-report.txt'), 'utf-8');
|
|
322
|
+
expect(content).toContain('new line 1');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('applies patch with context lines correctly when patch string has trailing newline', async () => {
|
|
326
|
+
await fs.writeFile(path.join(tmpDir, 'tn.txt'), ['line 1', 'line 2', 'line 3'].join('\n'));
|
|
327
|
+
|
|
328
|
+
// Trailing \n makes split produce a trailing '' that must NOT extend the oldBlock
|
|
329
|
+
const patch = '@@ -1,3 +1,3 @@\n line 1\n-line 2\n+line 2 modified\n line 3\n';
|
|
330
|
+
|
|
331
|
+
const result = await tool.execute({ path: 'tn.txt', patch });
|
|
332
|
+
|
|
333
|
+
expect(result.success).toBe(true);
|
|
334
|
+
const content = await fs.readFile(path.join(tmpDir, 'tn.txt'), 'utf-8');
|
|
335
|
+
expect(content).toContain('line 2 modified');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Bug: when oldBlock is empty (pure insertion, @@ -0,0 +1,N @@), the slow-path
|
|
339
|
+
// scan vacuously matched every file position → "Ambiguous match".
|
|
340
|
+
it('applies pure-insertion hunk (@@ -0,0 +1,N @@) by prepending to an existing file', async () => {
|
|
341
|
+
await fs.writeFile(path.join(tmpDir, 'prepend.txt'), [
|
|
342
|
+
'existing line 1',
|
|
343
|
+
'existing line 2',
|
|
344
|
+
].join('\n'));
|
|
345
|
+
|
|
346
|
+
// Pure-insertion: old side has 0 lines, new side adds 2 lines
|
|
347
|
+
const patch = '@@ -0,0 +1,2 @@\n+prepended A\n+prepended B';
|
|
348
|
+
|
|
349
|
+
const result = await tool.execute({ path: 'prepend.txt', patch });
|
|
350
|
+
|
|
351
|
+
expect(result.success).toBe(true);
|
|
352
|
+
const content = await fs.readFile(path.join(tmpDir, 'prepend.txt'), 'utf-8');
|
|
353
|
+
expect(content).toBe('prepended A\nprepended B\nexisting line 1\nexisting line 2');
|
|
354
|
+
});
|
|
355
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
name: test_builtin
|
|
2
|
+
description: Builtin-backed test tool
|
|
3
|
+
version: 1
|
|
4
|
+
|
|
5
|
+
commands:
|
|
6
|
+
- name: repo_manager-list_files
|
|
7
|
+
description: List files in the workspace
|
|
8
|
+
parameters:
|
|
9
|
+
path:
|
|
10
|
+
type: string
|
|
11
|
+
required: false
|
|
12
|
+
execute:
|
|
13
|
+
type: builtin
|
|
14
|
+
parse_output: json
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: test_shell
|
|
2
|
+
description: Shell-based test tool
|
|
3
|
+
version: 1
|
|
4
|
+
|
|
5
|
+
commands:
|
|
6
|
+
- name: test_shell-echo
|
|
7
|
+
description: Echo a message
|
|
8
|
+
parameters:
|
|
9
|
+
message:
|
|
10
|
+
type: string
|
|
11
|
+
required: true
|
|
12
|
+
description: Message to echo
|
|
13
|
+
execute:
|
|
14
|
+
type: shell
|
|
15
|
+
command: echo {{message}}
|
|
16
|
+
parse_output: text
|
|
17
|
+
|
|
18
|
+
prompt_snippet: |
|
|
19
|
+
You have access to a test shell tool.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { MockProvider } from '../src/providers/mock.js';
|
|
3
|
+
|
|
4
|
+
const stagesMap = new Map([
|
|
5
|
+
['brief-analysis', {
|
|
6
|
+
output: { summary: 'mock summary', requirements: ['req1'] },
|
|
7
|
+
tool_calls: [],
|
|
8
|
+
}],
|
|
9
|
+
['code-generation', {
|
|
10
|
+
output: { summary: 'mock code', files_changed: ['foo.ts'] },
|
|
11
|
+
tool_calls: [
|
|
12
|
+
{ name: 'repo_manager-write_file', arguments: { path: 'foo.ts', content: '// mock' } },
|
|
13
|
+
],
|
|
14
|
+
}],
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
describe('MockProvider', () => {
|
|
18
|
+
it('returns predefined output for a known stage', async () => {
|
|
19
|
+
const provider = new MockProvider(stagesMap);
|
|
20
|
+
const executeTool = vi.fn().mockResolvedValue({ result: 'ok' });
|
|
21
|
+
|
|
22
|
+
const result = await provider.runAgentLoop(
|
|
23
|
+
{
|
|
24
|
+
model: 'mock',
|
|
25
|
+
messages: [],
|
|
26
|
+
stage_name: 'brief-analysis',
|
|
27
|
+
},
|
|
28
|
+
executeTool
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
expect(result.content).toBe(JSON.stringify({ summary: 'mock summary', requirements: ['req1'] }));
|
|
32
|
+
expect(result.tool_calls).toHaveLength(0);
|
|
33
|
+
expect(result.finish_reason).toBe('stop');
|
|
34
|
+
expect(result.usage?.total_tokens).toBe(0);
|
|
35
|
+
expect(executeTool).not.toHaveBeenCalled();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('calls executeTool for each tool call in config', async () => {
|
|
39
|
+
const provider = new MockProvider(stagesMap);
|
|
40
|
+
const executeTool = vi.fn().mockResolvedValue({ result: 'written' });
|
|
41
|
+
|
|
42
|
+
const result = await provider.runAgentLoop(
|
|
43
|
+
{
|
|
44
|
+
model: 'mock',
|
|
45
|
+
messages: [],
|
|
46
|
+
stage_name: 'code-generation',
|
|
47
|
+
},
|
|
48
|
+
executeTool
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
expect(executeTool).toHaveBeenCalledOnce();
|
|
52
|
+
expect(executeTool).toHaveBeenCalledWith(
|
|
53
|
+
'repo_manager-write_file',
|
|
54
|
+
{ path: 'foo.ts', content: '// mock' },
|
|
55
|
+
expect.any(String)
|
|
56
|
+
);
|
|
57
|
+
expect(result.tool_calls).toHaveLength(1);
|
|
58
|
+
expect(result.tool_calls[0].name).toBe('repo_manager-write_file');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('throws a clear error for unknown stage', async () => {
|
|
62
|
+
const provider = new MockProvider(stagesMap);
|
|
63
|
+
|
|
64
|
+
await expect(
|
|
65
|
+
provider.runAgentLoop({ model: 'mock', messages: [], stage_name: 'unknown-stage' }, vi.fn())
|
|
66
|
+
).rejects.toThrow('Unknown mock stage: "unknown-stage"');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('throws when stage_name is missing', async () => {
|
|
70
|
+
const provider = new MockProvider(stagesMap);
|
|
71
|
+
|
|
72
|
+
await expect(
|
|
73
|
+
provider.runAgentLoop({ model: 'mock', messages: [] }, vi.fn())
|
|
74
|
+
).rejects.toThrow('MockProvider requires stage_name');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('emits a fake token when onToken is provided', async () => {
|
|
78
|
+
const stages = new Map([
|
|
79
|
+
['test-stage', { output: { result: 'ok' }, tool_calls: [] }],
|
|
80
|
+
]);
|
|
81
|
+
const provider = new MockProvider(stages);
|
|
82
|
+
|
|
83
|
+
const tokens: string[] = [];
|
|
84
|
+
await provider.runAgentLoop(
|
|
85
|
+
{ model: 'mock', messages: [{ role: 'user', content: 'go' }], stage_name: 'test-stage' },
|
|
86
|
+
async () => ({ result: 'ok' }),
|
|
87
|
+
(token) => tokens.push(token)
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
expect(tokens.length).toBeGreaterThan(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('does not error when onToken is not provided', async () => {
|
|
94
|
+
const stages = new Map([
|
|
95
|
+
['test-stage', { output: { result: 'ok' }, tool_calls: [] }],
|
|
96
|
+
]);
|
|
97
|
+
const provider = new MockProvider(stages);
|
|
98
|
+
|
|
99
|
+
await expect(provider.runAgentLoop(
|
|
100
|
+
{ model: 'mock', messages: [{ role: 'user', content: 'go' }], stage_name: 'test-stage' },
|
|
101
|
+
async () => ({ result: 'ok' }),
|
|
102
|
+
)).resolves.toBeDefined();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Helper: build a text chunk
|
|
4
|
+
function textChunk(content: string) {
|
|
5
|
+
return { choices: [{ delta: { content, tool_calls: undefined }, finish_reason: null }], usage: null };
|
|
6
|
+
}
|
|
7
|
+
// Helper: build a stop chunk with usage
|
|
8
|
+
function stopChunk(usage = { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }) {
|
|
9
|
+
return { choices: [{ delta: {}, finish_reason: 'stop' }], usage };
|
|
10
|
+
}
|
|
11
|
+
// Helper: async generator
|
|
12
|
+
async function* makeStream(chunks: object[]) {
|
|
13
|
+
for (const c of chunks) yield c;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const mockCreate = vi.fn();
|
|
17
|
+
|
|
18
|
+
vi.mock('openai', () => ({
|
|
19
|
+
default: vi.fn(function () {
|
|
20
|
+
return { chat: { completions: { create: mockCreate } } };
|
|
21
|
+
}),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
import { OpenAIProvider } from '../src/providers/openai.js';
|
|
25
|
+
import type { LLMRequest } from '@studio-foundation/contracts';
|
|
26
|
+
|
|
27
|
+
const baseRequest: LLMRequest = {
|
|
28
|
+
model: 'gpt-4o-mini',
|
|
29
|
+
messages: [{ role: 'user', content: 'Say hello' }],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
describe('OpenAIProvider', () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('uses non-streaming create when no onToken provided', async () => {
|
|
38
|
+
mockCreate.mockResolvedValueOnce({
|
|
39
|
+
choices: [{ message: { content: 'Hello', tool_calls: null }, finish_reason: 'stop' }],
|
|
40
|
+
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
|
41
|
+
});
|
|
42
|
+
const provider = new OpenAIProvider('test-key');
|
|
43
|
+
await provider.call(baseRequest);
|
|
44
|
+
expect(mockCreate).toHaveBeenCalledWith(expect.not.objectContaining({ stream: true }), expect.anything());
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('uses streaming create when onToken is provided', async () => {
|
|
48
|
+
mockCreate.mockReturnValueOnce(makeStream([
|
|
49
|
+
textChunk('Hello'),
|
|
50
|
+
textChunk(' world'),
|
|
51
|
+
stopChunk(),
|
|
52
|
+
]));
|
|
53
|
+
const provider = new OpenAIProvider('test-key');
|
|
54
|
+
const tokens: string[] = [];
|
|
55
|
+
await provider.call(baseRequest, (t) => tokens.push(t));
|
|
56
|
+
expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({ stream: true }), expect.anything());
|
|
57
|
+
expect(tokens).toEqual(['Hello', ' world']);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('accumulates content and returns correct LLMResponse when streaming', async () => {
|
|
61
|
+
mockCreate.mockReturnValueOnce(makeStream([
|
|
62
|
+
textChunk('Hello'),
|
|
63
|
+
textChunk(' world'),
|
|
64
|
+
stopChunk({ prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }),
|
|
65
|
+
]));
|
|
66
|
+
const provider = new OpenAIProvider('test-key');
|
|
67
|
+
const result = await provider.call(baseRequest, () => {});
|
|
68
|
+
expect(result.content).toBe('Hello world');
|
|
69
|
+
expect(result.finish_reason).toBe('stop');
|
|
70
|
+
expect(result.usage?.prompt_tokens).toBe(10);
|
|
71
|
+
});
|
|
72
|
+
});
|