skimpyclaw 0.3.15 → 0.4.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/README.md +47 -37
- package/dist/__tests__/adapter-types.test.d.ts +4 -0
- package/dist/__tests__/adapter-types.test.js +63 -0
- package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
- package/dist/__tests__/anthropic-adapter.test.js +264 -0
- package/dist/__tests__/api.test.js +0 -1
- package/dist/__tests__/cli.integration.test.js +2 -4
- package/dist/__tests__/cli.test.js +0 -1
- package/dist/__tests__/code-agents-notifications.test.js +137 -0
- package/dist/__tests__/code-agents-parser.test.js +19 -1
- package/dist/__tests__/code-agents-preflight.test.js +3 -28
- package/dist/__tests__/code-agents-utils.test.js +34 -9
- package/dist/__tests__/code-agents-worktrees.test.js +116 -0
- package/dist/__tests__/codex-adapter.test.js +184 -0
- package/dist/__tests__/codex-auth.test.js +66 -0
- package/dist/__tests__/codex-provider-gating.test.js +35 -0
- package/dist/__tests__/codex-unified-loop.test.js +111 -0
- package/dist/__tests__/config-security.test.js +127 -0
- package/dist/__tests__/config.test.js +23 -0
- package/dist/__tests__/context-manager.test.js +243 -164
- package/dist/__tests__/cron-run.test.js +250 -0
- package/dist/__tests__/cron.test.js +12 -38
- package/dist/__tests__/digests.test.js +67 -0
- package/dist/__tests__/discord-attachments.test.js +211 -0
- package/dist/__tests__/discord-docs.test.d.ts +1 -0
- package/dist/__tests__/discord-docs.test.js +27 -0
- package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
- package/dist/__tests__/discord-thread-agents.test.js +115 -0
- package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
- package/dist/__tests__/discord-thread-context.test.js +42 -0
- package/dist/__tests__/doctor.formatters.test.js +4 -4
- package/dist/__tests__/doctor.index.test.js +1 -1
- package/dist/__tests__/doctor.runner.test.js +3 -15
- package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
- package/dist/__tests__/env-sanitizer.test.js +45 -0
- package/dist/__tests__/exec-approval.test.js +61 -0
- package/dist/__tests__/fetch-tool.test.d.ts +1 -0
- package/dist/__tests__/fetch-tool.test.js +85 -0
- package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
- package/dist/__tests__/gateway-status-auth.test.js +72 -0
- package/dist/__tests__/heartbeat.test.js +3 -3
- package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
- package/dist/__tests__/interactive-sessions.test.js +96 -0
- package/dist/__tests__/langfuse.test.js +6 -18
- package/dist/__tests__/model-selection.test.js +3 -4
- package/dist/__tests__/providers-init.test.js +2 -8
- package/dist/__tests__/providers-routing.test.js +1 -1
- package/dist/__tests__/providers-utils.test.js +13 -3
- package/dist/__tests__/sessions.test.js +14 -10
- package/dist/__tests__/setup.test.js +12 -29
- package/dist/__tests__/skills.test.js +10 -7
- package/dist/__tests__/stream-formatter.test.d.ts +1 -0
- package/dist/__tests__/stream-formatter.test.js +114 -0
- package/dist/__tests__/token-efficiency.test.js +131 -15
- package/dist/__tests__/tool-loop.test.d.ts +4 -0
- package/dist/__tests__/tool-loop.test.js +505 -0
- package/dist/__tests__/tools.test.js +100 -276
- package/dist/__tests__/utils.test.d.ts +1 -0
- package/dist/__tests__/utils.test.js +14 -0
- package/dist/__tests__/voice.test.js +21 -0
- package/dist/agent.js +35 -4
- package/dist/api.js +113 -37
- package/dist/channels/discord/attachments.d.ts +50 -0
- package/dist/channels/discord/attachments.js +137 -0
- package/dist/channels/discord/delegation.d.ts +5 -0
- package/dist/channels/discord/delegation.js +136 -0
- package/dist/channels/discord/handlers.js +694 -7
- package/dist/channels/discord/index.d.ts +16 -1
- package/dist/channels/discord/index.js +64 -1
- package/dist/channels/discord/thread-agents.d.ts +54 -0
- package/dist/channels/discord/thread-agents.js +323 -0
- package/dist/channels/discord/threads.d.ts +58 -0
- package/dist/channels/discord/threads.js +192 -0
- package/dist/channels/discord/types.js +4 -2
- package/dist/channels/discord/utils.d.ts +16 -0
- package/dist/channels/discord/utils.js +86 -6
- package/dist/channels/telegram/index.d.ts +1 -1
- package/dist/channels/telegram/types.js +1 -1
- package/dist/channels/telegram/utils.js +9 -3
- package/dist/channels.d.ts +1 -1
- package/dist/cli.js +20 -400
- package/dist/code-agents/executor.d.ts +1 -1
- package/dist/code-agents/executor.js +101 -45
- package/dist/code-agents/index.d.ts +2 -7
- package/dist/code-agents/index.js +111 -80
- package/dist/code-agents/interactive-resume.d.ts +6 -0
- package/dist/code-agents/interactive-resume.js +98 -0
- package/dist/code-agents/interactive-sessions.d.ts +20 -0
- package/dist/code-agents/interactive-sessions.js +132 -0
- package/dist/code-agents/parser.js +5 -1
- package/dist/code-agents/registry.d.ts +7 -1
- package/dist/code-agents/registry.js +11 -23
- package/dist/code-agents/stream-formatter.d.ts +8 -0
- package/dist/code-agents/stream-formatter.js +92 -0
- package/dist/code-agents/types.d.ts +16 -24
- package/dist/code-agents/utils.d.ts +35 -11
- package/dist/code-agents/utils.js +348 -95
- package/dist/code-agents/worktrees.d.ts +37 -0
- package/dist/code-agents/worktrees.js +116 -0
- package/dist/config.d.ts +2 -4
- package/dist/config.js +123 -23
- package/dist/cron.d.ts +1 -6
- package/dist/cron.js +175 -82
- package/dist/dashboard/assets/index-B345aOO-.js +65 -0
- package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/digests.d.ts +1 -0
- package/dist/digests.js +132 -42
- package/dist/doctor/checks.d.ts +0 -3
- package/dist/doctor/checks.js +1 -108
- package/dist/doctor/runner.js +1 -4
- package/dist/env-sanitizer.d.ts +2 -0
- package/dist/env-sanitizer.js +61 -0
- package/dist/exec-approval.d.ts +11 -1
- package/dist/exec-approval.js +17 -4
- package/dist/gateway.d.ts +3 -1
- package/dist/gateway.js +17 -7
- package/dist/heartbeat.js +1 -6
- package/dist/langfuse.js +3 -29
- package/dist/model-selection.js +3 -1
- package/dist/providers/adapter.d.ts +118 -0
- package/dist/providers/adapter.js +6 -0
- package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
- package/dist/providers/adapters/anthropic-adapter.js +204 -0
- package/dist/providers/adapters/codex-adapter.d.ts +26 -0
- package/dist/providers/adapters/codex-adapter.js +203 -0
- package/dist/providers/anthropic.d.ts +1 -0
- package/dist/providers/anthropic.js +10 -272
- package/dist/providers/codex.d.ts +21 -0
- package/dist/providers/codex.js +149 -330
- package/dist/providers/content.d.ts +1 -1
- package/dist/providers/content.js +2 -2
- package/dist/providers/context-manager.d.ts +18 -6
- package/dist/providers/context-manager.js +199 -223
- package/dist/providers/index.d.ts +9 -1
- package/dist/providers/index.js +73 -64
- package/dist/providers/loop-utils.d.ts +20 -0
- package/dist/providers/loop-utils.js +30 -0
- package/dist/providers/tool-loop.d.ts +12 -0
- package/dist/providers/tool-loop.js +251 -0
- package/dist/providers/utils.d.ts +19 -3
- package/dist/providers/utils.js +100 -29
- package/dist/secure-store.d.ts +8 -0
- package/dist/secure-store.js +80 -0
- package/dist/service.js +3 -28
- package/dist/sessions.d.ts +3 -0
- package/dist/sessions.js +147 -18
- package/dist/setup-templates.js +13 -25
- package/dist/setup.d.ts +10 -6
- package/dist/setup.js +84 -292
- package/dist/skills.js +3 -11
- package/dist/tools/agent-delegation.d.ts +19 -0
- package/dist/tools/agent-delegation.js +49 -0
- package/dist/tools/bash-tool.js +89 -34
- package/dist/tools/definitions.d.ts +199 -302
- package/dist/tools/definitions.js +70 -123
- package/dist/tools/execute-context.d.ts +13 -4
- package/dist/tools/fetch-tool.js +109 -13
- package/dist/tools/file-tools.js +7 -1
- package/dist/tools.d.ts +7 -7
- package/dist/tools.js +133 -151
- package/dist/types.d.ts +37 -30
- package/dist/utils.js +4 -6
- package/dist/voice.d.ts +1 -1
- package/dist/voice.js +17 -4
- package/package.json +33 -23
- package/templates/TOOLS.md +0 -27
- package/dist/__tests__/audit.test.js +0 -122
- package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
- package/dist/__tests__/code-agents-sandbox.test.js +0 -163
- package/dist/__tests__/orchestrator.test.js +0 -425
- package/dist/__tests__/sandbox-bridge.test.js +0 -116
- package/dist/__tests__/sandbox-manager.test.js +0 -144
- package/dist/__tests__/sandbox-mount-security.test.js +0 -139
- package/dist/__tests__/sandbox-runtime.test.js +0 -176
- package/dist/__tests__/subagent.test.js +0 -240
- package/dist/__tests__/telegram.test.js +0 -42
- package/dist/code-agents/orchestrator.d.ts +0 -29
- package/dist/code-agents/orchestrator.js +0 -694
- package/dist/code-agents/worktree.d.ts +0 -40
- package/dist/code-agents/worktree.js +0 -215
- package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
- package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
- package/dist/dashboard.d.ts +0 -8
- package/dist/dashboard.js +0 -4071
- package/dist/discord.d.ts +0 -8
- package/dist/discord.js +0 -792
- package/dist/mcp-context-a8c.d.ts +0 -13
- package/dist/mcp-context-a8c.js +0 -34
- package/dist/orchestrator.d.ts +0 -15
- package/dist/orchestrator.js +0 -676
- package/dist/providers/openai.d.ts +0 -10
- package/dist/providers/openai.js +0 -355
- package/dist/sandbox/bridge.d.ts +0 -5
- package/dist/sandbox/bridge.js +0 -63
- package/dist/sandbox/index.d.ts +0 -5
- package/dist/sandbox/index.js +0 -4
- package/dist/sandbox/manager.d.ts +0 -7
- package/dist/sandbox/manager.js +0 -100
- package/dist/sandbox/mount-security.d.ts +0 -12
- package/dist/sandbox/mount-security.js +0 -122
- package/dist/sandbox/runtime.d.ts +0 -39
- package/dist/sandbox/runtime.js +0 -192
- package/dist/sandbox-utils.d.ts +0 -6
- package/dist/sandbox-utils.js +0 -36
- package/dist/subagent.d.ts +0 -19
- package/dist/subagent.js +0 -407
- package/dist/telegram.d.ts +0 -2
- package/dist/telegram.js +0 -11
- package/dist/tools/browser-tool.d.ts +0 -3
- package/dist/tools/browser-tool.js +0 -266
- package/sandbox/Dockerfile +0 -40
- /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
- /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
- /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
- /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
- /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
- /package/dist/__tests__/{telegram.test.d.ts → discord-attachments.test.d.ts} +0 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
const { sendActiveChannelProactiveMessageMock, sendToDiscordThreadMock, sendToDiscordThreadWithAttachmentsMock, sendDiscordProactiveMessageMock, sendDiscordProactiveMessageWithAttachmentsMock, } = vi.hoisted(() => ({
|
|
3
|
+
sendActiveChannelProactiveMessageMock: vi.fn(async () => true),
|
|
4
|
+
sendToDiscordThreadMock: vi.fn(async () => false),
|
|
5
|
+
sendToDiscordThreadWithAttachmentsMock: vi.fn(async () => false),
|
|
6
|
+
sendDiscordProactiveMessageMock: vi.fn(async () => undefined),
|
|
7
|
+
sendDiscordProactiveMessageWithAttachmentsMock: vi.fn(async () => undefined),
|
|
8
|
+
}));
|
|
9
|
+
vi.mock('../channels.js', () => ({
|
|
10
|
+
sendActiveChannelProactiveMessage: sendActiveChannelProactiveMessageMock,
|
|
11
|
+
}));
|
|
12
|
+
vi.mock('../channels/discord/index.js', () => ({
|
|
13
|
+
sendToDiscordThread: sendToDiscordThreadMock,
|
|
14
|
+
sendToDiscordThreadWithAttachments: sendToDiscordThreadWithAttachmentsMock,
|
|
15
|
+
sendDiscordProactiveMessage: sendDiscordProactiveMessageMock,
|
|
16
|
+
sendDiscordProactiveMessageWithAttachments: sendDiscordProactiveMessageWithAttachmentsMock,
|
|
17
|
+
}));
|
|
18
|
+
import { buildCodeAgentDiscordNotification, notifyCodeAgentResult, setCodeAgentConfig } from '../code-agents/utils.js';
|
|
19
|
+
describe('notifyCodeAgentResult Discord routing', () => {
|
|
20
|
+
const config = { channels: { active: 'discord' } };
|
|
21
|
+
const completedTask = {
|
|
22
|
+
id: 'ca-1',
|
|
23
|
+
agent: 'codex',
|
|
24
|
+
task: 'Fix Discord routing',
|
|
25
|
+
status: 'completed',
|
|
26
|
+
startedAt: '2026-04-12T00:00:00.000Z',
|
|
27
|
+
durationSeconds: 12,
|
|
28
|
+
workdir: '/tmp',
|
|
29
|
+
outputPreview: 'Patched notifier routing.',
|
|
30
|
+
};
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
vi.clearAllMocks();
|
|
33
|
+
setCodeAgentConfig(config);
|
|
34
|
+
sendToDiscordThreadMock.mockResolvedValue(false);
|
|
35
|
+
sendToDiscordThreadWithAttachmentsMock.mockResolvedValue(false);
|
|
36
|
+
sendDiscordProactiveMessageMock.mockResolvedValue(undefined);
|
|
37
|
+
sendDiscordProactiveMessageWithAttachmentsMock.mockResolvedValue(undefined);
|
|
38
|
+
sendActiveChannelProactiveMessageMock.mockResolvedValue(true);
|
|
39
|
+
});
|
|
40
|
+
it('falls back from Discord thread to the originating Discord channel', async () => {
|
|
41
|
+
await notifyCodeAgentResult({
|
|
42
|
+
...completedTask,
|
|
43
|
+
discordThreadId: 'thread-1',
|
|
44
|
+
discordChannelId: 'channel-1',
|
|
45
|
+
});
|
|
46
|
+
expect(sendToDiscordThreadMock).toHaveBeenCalledWith('thread-1', expect.stringContaining('`ca-1` completed'));
|
|
47
|
+
expect(sendDiscordProactiveMessageMock).toHaveBeenCalledWith('channel-1', expect.stringContaining('`ca-1` completed'));
|
|
48
|
+
expect(sendActiveChannelProactiveMessageMock).not.toHaveBeenCalled();
|
|
49
|
+
});
|
|
50
|
+
it('does not fall back to the global active channel when Discord-scoped delivery fails', async () => {
|
|
51
|
+
sendDiscordProactiveMessageMock.mockRejectedValue(new Error('channel missing'));
|
|
52
|
+
await notifyCodeAgentResult({
|
|
53
|
+
...completedTask,
|
|
54
|
+
discordThreadId: 'thread-1',
|
|
55
|
+
discordChannelId: 'channel-1',
|
|
56
|
+
});
|
|
57
|
+
expect(sendToDiscordThreadMock).toHaveBeenCalled();
|
|
58
|
+
expect(sendDiscordProactiveMessageMock).toHaveBeenCalled();
|
|
59
|
+
expect(sendActiveChannelProactiveMessageMock).not.toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
it('uses the active channel only when the task has no Discord-specific route', async () => {
|
|
62
|
+
await notifyCodeAgentResult(completedTask);
|
|
63
|
+
expect(sendToDiscordThreadMock).not.toHaveBeenCalled();
|
|
64
|
+
expect(sendDiscordProactiveMessageMock).not.toHaveBeenCalled();
|
|
65
|
+
expect(sendActiveChannelProactiveMessageMock).toHaveBeenCalledWith(config, expect.stringContaining('Coding agent ca-1 completed'));
|
|
66
|
+
});
|
|
67
|
+
it('sends compact Discord summary with full report attachment for long output', async () => {
|
|
68
|
+
const longOutput = [
|
|
69
|
+
'Findings',
|
|
70
|
+
'',
|
|
71
|
+
'- High: important issue was resolved on current branch.',
|
|
72
|
+
'- Low: coverage gaps remain.',
|
|
73
|
+
'',
|
|
74
|
+
'Risk Checks',
|
|
75
|
+
'',
|
|
76
|
+
'No reproducible leak found in current code.',
|
|
77
|
+
'',
|
|
78
|
+
'Recommendation',
|
|
79
|
+
'',
|
|
80
|
+
'Merge only if the PR head is current.',
|
|
81
|
+
'',
|
|
82
|
+
'Details '.repeat(500),
|
|
83
|
+
].join('\n');
|
|
84
|
+
await notifyCodeAgentResult({
|
|
85
|
+
...completedTask,
|
|
86
|
+
agent: 'claude',
|
|
87
|
+
model: 'claude-opus-4-6',
|
|
88
|
+
effort: 'xhigh',
|
|
89
|
+
discordThreadId: 'thread-1',
|
|
90
|
+
outputPreview: longOutput,
|
|
91
|
+
});
|
|
92
|
+
expect(sendToDiscordThreadWithAttachmentsMock).toHaveBeenCalledWith('thread-1', expect.stringContaining('`ca-1` completed · CLAUDE · claude-opus-4-6 · effort xhigh'), [expect.objectContaining({
|
|
93
|
+
name: 'ca-1-report.md',
|
|
94
|
+
content: expect.stringContaining('## Result'),
|
|
95
|
+
})]);
|
|
96
|
+
const message = sendToDiscordThreadWithAttachmentsMock.mock.calls[0][1];
|
|
97
|
+
expect(message.length).toBeLessThan(1900);
|
|
98
|
+
expect(message).toContain('Full report attached.');
|
|
99
|
+
});
|
|
100
|
+
it('strips raw Codex stream-json from Discord summary', () => {
|
|
101
|
+
const rawCodexOutput = [
|
|
102
|
+
JSON.stringify({ type: 'thread.started', thread_id: 't1' }),
|
|
103
|
+
JSON.stringify({ type: 'turn.started' }),
|
|
104
|
+
JSON.stringify({
|
|
105
|
+
type: 'item.completed',
|
|
106
|
+
item: {
|
|
107
|
+
id: 'item_1',
|
|
108
|
+
type: 'agent_message',
|
|
109
|
+
text: 'Findings\n\n- High: state contract can regress.\n\nRecommendation\n\nDo not merge until fixed.',
|
|
110
|
+
},
|
|
111
|
+
}),
|
|
112
|
+
JSON.stringify({
|
|
113
|
+
type: 'item.started',
|
|
114
|
+
item: {
|
|
115
|
+
id: 'item_2',
|
|
116
|
+
type: 'command_execution',
|
|
117
|
+
command: 'git status --short',
|
|
118
|
+
status: 'in_progress',
|
|
119
|
+
},
|
|
120
|
+
}),
|
|
121
|
+
].join('\n');
|
|
122
|
+
const notification = buildCodeAgentDiscordNotification({
|
|
123
|
+
...completedTask,
|
|
124
|
+
agent: 'codex',
|
|
125
|
+
model: 'gpt-5.5',
|
|
126
|
+
effort: 'high',
|
|
127
|
+
outputPreview: rawCodexOutput,
|
|
128
|
+
task: 'Review PR https://github.com/Automattic/wp-calypso/pull/110225',
|
|
129
|
+
});
|
|
130
|
+
expect(notification.content).toContain('**Findings**');
|
|
131
|
+
expect(notification.content).toContain('state contract can regress');
|
|
132
|
+
expect(notification.content).toContain('**Decision**');
|
|
133
|
+
expect(notification.content).not.toContain('thread.started');
|
|
134
|
+
expect(notification.content).not.toContain('command_execution');
|
|
135
|
+
expect(notification.content).not.toContain('{"type"');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { parseClaudeOutput, parseStreamJsonForLive } from '../code-agents/parser.js';
|
|
2
|
+
import { parseClaudeOutput, parseCodexOutput, parseStreamJsonForLive } from '../code-agents/parser.js';
|
|
3
3
|
describe('code-agents parser', () => {
|
|
4
4
|
it('parses newer Claude stream item.completed agent_message events', () => {
|
|
5
5
|
const stdout = [
|
|
@@ -36,4 +36,22 @@ describe('code-agents parser', () => {
|
|
|
36
36
|
expect(live).toContain('New format message');
|
|
37
37
|
expect(live).toContain('[Read]');
|
|
38
38
|
});
|
|
39
|
+
it('parses Codex stream-json without leaking raw events', () => {
|
|
40
|
+
const stdout = [
|
|
41
|
+
JSON.stringify({ type: 'thread.started', thread_id: 't1' }),
|
|
42
|
+
JSON.stringify({ type: 'turn.started' }),
|
|
43
|
+
JSON.stringify({
|
|
44
|
+
type: 'item.completed',
|
|
45
|
+
item: { id: 'item_1', type: 'agent_message', text: 'Final review text.' },
|
|
46
|
+
}),
|
|
47
|
+
JSON.stringify({
|
|
48
|
+
type: 'item.started',
|
|
49
|
+
item: { id: 'item_2', type: 'command_execution', command: 'git status', status: 'in_progress' },
|
|
50
|
+
}),
|
|
51
|
+
].join('\n');
|
|
52
|
+
const parsed = parseCodexOutput(stdout);
|
|
53
|
+
expect(parsed).toBe('Final review text.');
|
|
54
|
+
expect(parsed).not.toContain('thread.started');
|
|
55
|
+
expect(parsed).not.toContain('command_execution');
|
|
56
|
+
});
|
|
39
57
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
const PRECHECK_ERROR = 'Error: No supported coding CLI found on PATH. Install Codex CLI (`codex`)
|
|
2
|
+
const PRECHECK_ERROR = 'Error: No supported coding CLI found on PATH. Install Codex CLI (`codex`) or Claude Code CLI (`claude` or `claude-code`).';
|
|
3
3
|
const toolConfig = {
|
|
4
4
|
enabled: true,
|
|
5
5
|
allowedPaths: [process.cwd()],
|
|
@@ -9,7 +9,6 @@ const toolConfig = {
|
|
|
9
9
|
async function loadSubject(preflightError) {
|
|
10
10
|
vi.resetModules();
|
|
11
11
|
const runCodeAgentBackground = vi.fn().mockResolvedValue(undefined);
|
|
12
|
-
const runTeamOrchestrator = vi.fn().mockResolvedValue(undefined);
|
|
13
12
|
vi.doMock('../code-agents/utils.js', async () => {
|
|
14
13
|
const actual = await vi.importActual('../code-agents/utils.js');
|
|
15
14
|
return {
|
|
@@ -22,13 +21,6 @@ async function loadSubject(preflightError) {
|
|
|
22
21
|
runValidation: vi.fn(),
|
|
23
22
|
buildValidationCommand: vi.fn(() => 'pnpm build && pnpm test'),
|
|
24
23
|
}));
|
|
25
|
-
vi.doMock('../code-agents/orchestrator.js', () => ({
|
|
26
|
-
runTeamOrchestrator,
|
|
27
|
-
computeWaves: vi.fn(),
|
|
28
|
-
decomposeTask: vi.fn(),
|
|
29
|
-
synthesizeResults: vi.fn(),
|
|
30
|
-
gatherCodebaseContext: vi.fn(),
|
|
31
|
-
}));
|
|
32
24
|
vi.doMock('../code-agents/registry.js', () => ({
|
|
33
25
|
getNextCodeAgentId: vi.fn(() => 'ca_test_1'),
|
|
34
26
|
storeCodeAgentTask: vi.fn(),
|
|
@@ -42,17 +34,16 @@ async function loadSubject(preflightError) {
|
|
|
42
34
|
getCodeAgentsDir: vi.fn(() => process.cwd()),
|
|
43
35
|
}));
|
|
44
36
|
const subject = await import('../code-agents/index.js');
|
|
45
|
-
return { ...subject, runCodeAgentBackground
|
|
37
|
+
return { ...subject, runCodeAgentBackground };
|
|
46
38
|
}
|
|
47
39
|
afterEach(() => {
|
|
48
40
|
vi.doUnmock('../code-agents/utils.js');
|
|
49
41
|
vi.doUnmock('../code-agents/executor.js');
|
|
50
|
-
vi.doUnmock('../code-agents/orchestrator.js');
|
|
51
42
|
vi.doUnmock('../code-agents/registry.js');
|
|
52
43
|
vi.clearAllMocks();
|
|
53
44
|
vi.resetModules();
|
|
54
45
|
});
|
|
55
|
-
describe('coding CLI preflight guard', () => {
|
|
46
|
+
describe('coding CLI preflight guard', { timeout: 15000 }, () => {
|
|
56
47
|
it('fails code_with_agent before spawning when no supported CLI is available', async () => {
|
|
57
48
|
const { executeCodeWithAgent, runCodeAgentBackground } = await loadSubject(PRECHECK_ERROR);
|
|
58
49
|
const result = await executeCodeWithAgent({ task: 'Fix bug', workdir: process.cwd() }, toolConfig, {
|
|
@@ -61,14 +52,6 @@ describe('coding CLI preflight guard', () => {
|
|
|
61
52
|
expect(result).toBe(PRECHECK_ERROR);
|
|
62
53
|
expect(runCodeAgentBackground).not.toHaveBeenCalled();
|
|
63
54
|
});
|
|
64
|
-
it('fails code_with_team before spawning when no supported CLI is available', async () => {
|
|
65
|
-
const { executeCodeWithTeam, runTeamOrchestrator } = await loadSubject(PRECHECK_ERROR);
|
|
66
|
-
const result = await executeCodeWithTeam({ task: 'Refactor auth', workdir: process.cwd() }, toolConfig, {
|
|
67
|
-
fullConfig: { codeAgents: { maxConcurrent: 99 } },
|
|
68
|
-
});
|
|
69
|
-
expect(result).toBe(PRECHECK_ERROR);
|
|
70
|
-
expect(runTeamOrchestrator).not.toHaveBeenCalled();
|
|
71
|
-
});
|
|
72
55
|
it('allows code_with_agent when at least one supported CLI exists', async () => {
|
|
73
56
|
const { executeCodeWithAgent, runCodeAgentBackground } = await loadSubject(null);
|
|
74
57
|
const result = await executeCodeWithAgent({ task: 'Fix bug', workdir: process.cwd() }, toolConfig, {
|
|
@@ -77,12 +60,4 @@ describe('coding CLI preflight guard', () => {
|
|
|
77
60
|
expect(result).toContain('Started coding agent');
|
|
78
61
|
expect(runCodeAgentBackground).toHaveBeenCalledTimes(1);
|
|
79
62
|
});
|
|
80
|
-
it('allows code_with_team when at least one supported CLI exists', async () => {
|
|
81
|
-
const { executeCodeWithTeam, runTeamOrchestrator } = await loadSubject(null);
|
|
82
|
-
const result = await executeCodeWithTeam({ task: 'Refactor auth', workdir: process.cwd() }, toolConfig, {
|
|
83
|
-
fullConfig: { codeAgents: { maxConcurrent: 99 } },
|
|
84
|
-
});
|
|
85
|
-
expect(result).toContain('Started coding team');
|
|
86
|
-
expect(runTeamOrchestrator).toHaveBeenCalledTimes(1);
|
|
87
|
-
});
|
|
88
63
|
});
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import { mkdtempSync, writeFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { homedir, tmpdir } from 'os';
|
|
5
|
+
import { buildCodeAgentSpawnEnv, normalizeCodeAgent, resolveSelectedCodeAgent, getAvailableCodingCliTools, getCodingCliPreflightError, readClaudeCodeDefaultModel, resolveCodeAgentModelLabel, } from '../code-agents/utils.js';
|
|
3
6
|
describe('normalizeCodeAgent', () => {
|
|
4
7
|
it('accepts strict ids', () => {
|
|
5
8
|
expect(normalizeCodeAgent('claude')).toBe('claude');
|
|
6
9
|
expect(normalizeCodeAgent('codex')).toBe('codex');
|
|
7
|
-
expect(normalizeCodeAgent('kimi')).toBe('kimi');
|
|
8
10
|
});
|
|
9
11
|
it('maps legacy alias-like values', () => {
|
|
10
|
-
expect(normalizeCodeAgent('claude-
|
|
12
|
+
expect(normalizeCodeAgent('claude-coder')).toBe('claude');
|
|
11
13
|
expect(normalizeCodeAgent('codex5.3')).toBe('codex');
|
|
12
|
-
expect(normalizeCodeAgent('kimi/coding')).toBe('kimi');
|
|
13
14
|
});
|
|
14
15
|
it('returns null for unknown values', () => {
|
|
15
16
|
expect(normalizeCodeAgent('gpt')).toBeNull();
|
|
@@ -21,7 +22,7 @@ describe('resolveSelectedCodeAgent', () => {
|
|
|
21
22
|
expect(resolveSelectedCodeAgent('codex5.3', 'claude')).toBe('codex');
|
|
22
23
|
});
|
|
23
24
|
it('falls back to configured default agent', () => {
|
|
24
|
-
expect(resolveSelectedCodeAgent(undefined, 'claude-
|
|
25
|
+
expect(resolveSelectedCodeAgent(undefined, 'claude-coder')).toBe('claude');
|
|
25
26
|
});
|
|
26
27
|
it('uses claude as hard default', () => {
|
|
27
28
|
expect(resolveSelectedCodeAgent(undefined, undefined)).toBe('claude');
|
|
@@ -32,9 +33,6 @@ describe('resolveSelectedCodeAgent', () => {
|
|
|
32
33
|
expect(resolveSelectedCodeAgent(undefined, 'claude', 'openai/gpt-4.1')).toBe('codex');
|
|
33
34
|
expect(resolveSelectedCodeAgent(undefined, 'claude', 'o3-pro')).toBe('codex');
|
|
34
35
|
});
|
|
35
|
-
it('auto-selects kimi when model is a kimi model and no agent specified', () => {
|
|
36
|
-
expect(resolveSelectedCodeAgent(undefined, 'claude', 'kimi-for-coding')).toBe('kimi');
|
|
37
|
-
});
|
|
38
36
|
it('respects explicit agent even when model suggests different agent', () => {
|
|
39
37
|
expect(resolveSelectedCodeAgent('claude', 'claude', 'gpt-4.1')).toBe('claude');
|
|
40
38
|
});
|
|
@@ -42,7 +40,7 @@ describe('resolveSelectedCodeAgent', () => {
|
|
|
42
40
|
describe('coding CLI preflight', () => {
|
|
43
41
|
it('returns a clear error when no supported CLI is found on PATH', () => {
|
|
44
42
|
expect(getAvailableCodingCliTools(() => false)).toEqual([]);
|
|
45
|
-
expect(getCodingCliPreflightError(() => false)).toBe('Error: No supported coding CLI found on PATH. Install Codex CLI (`codex`)
|
|
43
|
+
expect(getCodingCliPreflightError(() => false)).toBe('Error: No supported coding CLI found on PATH. Install Codex CLI (`codex`) or Claude Code CLI (`claude` or `claude-code`).');
|
|
46
44
|
});
|
|
47
45
|
it('accepts claude-code binary as claude support', () => {
|
|
48
46
|
const hasCommand = (name) => name === 'claude-code';
|
|
@@ -50,3 +48,30 @@ describe('coding CLI preflight', () => {
|
|
|
50
48
|
expect(getCodingCliPreflightError(hasCommand)).toBeNull();
|
|
51
49
|
});
|
|
52
50
|
});
|
|
51
|
+
describe('buildCodeAgentSpawnEnv', () => {
|
|
52
|
+
it('points Codex subprocesses at the standard ~/.codex state directory', () => {
|
|
53
|
+
const env = buildCodeAgentSpawnEnv({
|
|
54
|
+
HOME: '/tmp/ignored-home',
|
|
55
|
+
CODEX_HOME: '/tmp/old-codex-home',
|
|
56
|
+
GH_TOKEN: 'stale-gh',
|
|
57
|
+
GITHUB_TOKEN: 'stale-github',
|
|
58
|
+
CLAUDECODE: 'nested',
|
|
59
|
+
});
|
|
60
|
+
expect(env.CODEX_HOME).toBe(join(homedir(), '.codex'));
|
|
61
|
+
expect(env.CLAUDECODE).toBeUndefined();
|
|
62
|
+
expect(env.GH_TOKEN).toBeUndefined();
|
|
63
|
+
expect(env.GITHUB_TOKEN).toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe('code agent model labels', () => {
|
|
67
|
+
it('reads Claude Code default model from settings', () => {
|
|
68
|
+
const dir = mkdtempSync(join(tmpdir(), 'skimpyclaw-claude-settings-'));
|
|
69
|
+
const settingsPath = join(dir, 'settings.json');
|
|
70
|
+
writeFileSync(settingsPath, JSON.stringify({ model: 'opus[1m]' }));
|
|
71
|
+
expect(readClaudeCodeDefaultModel(settingsPath)).toBe('opus[1m]');
|
|
72
|
+
});
|
|
73
|
+
it('strips provider prefixes for explicit model labels', () => {
|
|
74
|
+
expect(resolveCodeAgentModelLabel('claude', 'anthropic/claude-opus-4-7')).toBe('claude-opus-4-7');
|
|
75
|
+
expect(resolveCodeAgentModelLabel('codex', 'codex/gpt-5.5')).toBe('gpt-5.5');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'fs';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
|
+
import { cleanupCodeAgentWorktree, normalizeWorktreeRequest, prepareCodeAgentWorktree, shouldAutoWorktreeTask, shouldUseCodeAgentWorktree, } from '../code-agents/worktrees.js';
|
|
7
|
+
const roots = [];
|
|
8
|
+
function git(cwd, args) {
|
|
9
|
+
execFileSync('git', args, { cwd, stdio: 'ignore' });
|
|
10
|
+
}
|
|
11
|
+
function makeRepo() {
|
|
12
|
+
const root = mkdtempSync(join(tmpdir(), 'skimpyclaw-worktree-test-'));
|
|
13
|
+
roots.push(root);
|
|
14
|
+
git(root, ['init']);
|
|
15
|
+
git(root, ['config', 'user.email', 'test@example.com']);
|
|
16
|
+
git(root, ['config', 'user.name', 'Test']);
|
|
17
|
+
mkdirSync(join(root, 'packages', 'demo'), { recursive: true });
|
|
18
|
+
writeFileSync(join(root, 'README.md'), '# test\n');
|
|
19
|
+
writeFileSync(join(root, 'packages', 'demo', 'package.json'), '{"name":"demo"}\n');
|
|
20
|
+
git(root, ['add', '.']);
|
|
21
|
+
git(root, ['commit', '-m', 'init']);
|
|
22
|
+
return root;
|
|
23
|
+
}
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
for (const root of roots.splice(0)) {
|
|
26
|
+
rmSync(root, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
describe('code agent worktrees', () => {
|
|
30
|
+
it('normalizes worktree requests', () => {
|
|
31
|
+
expect(normalizeWorktreeRequest(true)).toBe(true);
|
|
32
|
+
expect(normalizeWorktreeRequest('yes')).toBe(true);
|
|
33
|
+
expect(normalizeWorktreeRequest('off')).toBe(false);
|
|
34
|
+
expect(normalizeWorktreeRequest('auto')).toBe('auto');
|
|
35
|
+
expect(normalizeWorktreeRequest('wat')).toBeUndefined();
|
|
36
|
+
});
|
|
37
|
+
it('defaults auto worktrees to review and rebase tasks only', () => {
|
|
38
|
+
expect(shouldAutoWorktreeTask('Review PR https://github.com/org/repo/pull/123')).toBe(true);
|
|
39
|
+
expect(shouldAutoWorktreeTask('rebase this branch from trunk')).toBe(true);
|
|
40
|
+
expect(shouldAutoWorktreeTask('fix this css bug')).toBe(false);
|
|
41
|
+
expect(shouldUseCodeAgentWorktree('fix this css bug', undefined, { mode: 'always' })).toBe(true);
|
|
42
|
+
expect(shouldUseCodeAgentWorktree('Review PR', false, { mode: 'always' })).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
it('creates a detached worktree and preserves subdirectory workdir', () => {
|
|
45
|
+
const repo = makeRepo();
|
|
46
|
+
const root = mkdtempSync(join(tmpdir(), 'skimpyclaw-worktrees-'));
|
|
47
|
+
roots.push(root);
|
|
48
|
+
const result = prepareCodeAgentWorktree({
|
|
49
|
+
id: 'ca-123',
|
|
50
|
+
sourceWorkdir: join(repo, 'packages', 'demo'),
|
|
51
|
+
config: { root },
|
|
52
|
+
required: true,
|
|
53
|
+
});
|
|
54
|
+
expect(result).toBeDefined();
|
|
55
|
+
expect(result?.sourceWorkdir).toBe(realpathSync(join(repo, 'packages', 'demo')));
|
|
56
|
+
expect(result?.runWorkdir).toBe(join(result.worktreePath, 'packages', 'demo'));
|
|
57
|
+
expect(existsSync(result.worktreePath)).toBe(true);
|
|
58
|
+
expect(existsSync(result.runWorkdir)).toBe(true);
|
|
59
|
+
expect(result?.worktreeRef).toMatch(/^[0-9a-f]{40}$/);
|
|
60
|
+
});
|
|
61
|
+
it('removes clean unchanged worktrees', () => {
|
|
62
|
+
const repo = makeRepo();
|
|
63
|
+
const root = mkdtempSync(join(tmpdir(), 'skimpyclaw-worktrees-'));
|
|
64
|
+
roots.push(root);
|
|
65
|
+
const result = prepareCodeAgentWorktree({
|
|
66
|
+
id: 'ca-clean',
|
|
67
|
+
sourceWorkdir: repo,
|
|
68
|
+
config: { root },
|
|
69
|
+
required: true,
|
|
70
|
+
});
|
|
71
|
+
const cleanup = cleanupCodeAgentWorktree(result);
|
|
72
|
+
expect(cleanup.status).toBe('removed');
|
|
73
|
+
expect(existsSync(result.worktreePath)).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
it('preserves dirty worktrees', () => {
|
|
76
|
+
const repo = makeRepo();
|
|
77
|
+
const root = mkdtempSync(join(tmpdir(), 'skimpyclaw-worktrees-'));
|
|
78
|
+
roots.push(root);
|
|
79
|
+
const result = prepareCodeAgentWorktree({
|
|
80
|
+
id: 'ca-dirty',
|
|
81
|
+
sourceWorkdir: repo,
|
|
82
|
+
config: { root },
|
|
83
|
+
required: true,
|
|
84
|
+
});
|
|
85
|
+
writeFileSync(join(result.worktreePath, 'dirty.txt'), 'dirty\n');
|
|
86
|
+
const cleanup = cleanupCodeAgentWorktree(result);
|
|
87
|
+
expect(cleanup.status).toBe('preserved');
|
|
88
|
+
expect(cleanup.reason).toContain('uncommitted changes');
|
|
89
|
+
expect(existsSync(result.worktreePath)).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
it('preserves worktrees whose HEAD changed', () => {
|
|
92
|
+
const repo = makeRepo();
|
|
93
|
+
const root = mkdtempSync(join(tmpdir(), 'skimpyclaw-worktrees-'));
|
|
94
|
+
roots.push(root);
|
|
95
|
+
const result = prepareCodeAgentWorktree({
|
|
96
|
+
id: 'ca-head',
|
|
97
|
+
sourceWorkdir: repo,
|
|
98
|
+
config: { root },
|
|
99
|
+
required: true,
|
|
100
|
+
});
|
|
101
|
+
writeFileSync(join(result.worktreePath, 'README.md'), '# changed\n');
|
|
102
|
+
git(result.worktreePath, ['add', 'README.md']);
|
|
103
|
+
git(result.worktreePath, ['commit', '-m', 'change']);
|
|
104
|
+
const cleanup = cleanupCodeAgentWorktree(result);
|
|
105
|
+
expect(cleanup.status).toBe('preserved');
|
|
106
|
+
expect(cleanup.reason).toContain('HEAD changed');
|
|
107
|
+
expect(existsSync(result.worktreePath)).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
it('skips non-git directories in optional mode and errors in required mode', () => {
|
|
110
|
+
const dir = mkdtempSync(join(tmpdir(), 'skimpyclaw-not-git-'));
|
|
111
|
+
roots.push(dir);
|
|
112
|
+
expect(prepareCodeAgentWorktree({ id: 'ca-1', sourceWorkdir: dir })).toBeUndefined();
|
|
113
|
+
expect(() => prepareCodeAgentWorktree({ id: 'ca-1', sourceWorkdir: dir, required: true }))
|
|
114
|
+
.toThrow(/not inside a git repository/);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { CodexAdapter } from '../providers/adapters/codex-adapter.js';
|
|
3
|
+
const { mockCodexFetch, mockParseCodexSSE, mockRecordCodexUsage } = vi.hoisted(() => ({
|
|
4
|
+
mockCodexFetch: vi.fn(),
|
|
5
|
+
mockParseCodexSSE: vi.fn(),
|
|
6
|
+
mockRecordCodexUsage: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
vi.mock('../providers/codex.js', () => ({
|
|
9
|
+
codexFetch: mockCodexFetch,
|
|
10
|
+
parseCodexSSE: mockParseCodexSSE,
|
|
11
|
+
recordCodexUsage: mockRecordCodexUsage,
|
|
12
|
+
}));
|
|
13
|
+
describe('CodexAdapter', () => {
|
|
14
|
+
let adapter;
|
|
15
|
+
let config;
|
|
16
|
+
let options;
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
adapter = new CodexAdapter();
|
|
19
|
+
config = {
|
|
20
|
+
gateway: { port: 18790, mode: 'local' },
|
|
21
|
+
agents: { default: 'main', list: {} },
|
|
22
|
+
models: { providers: {}, aliases: {} },
|
|
23
|
+
channels: { telegram: { enabled: false, token: 't', allowFrom: [] } },
|
|
24
|
+
cron: { jobs: [] },
|
|
25
|
+
heartbeat: { intervalMs: 300000, prompt: 'HEARTBEAT' },
|
|
26
|
+
};
|
|
27
|
+
options = { model: 'codex/gpt-5.5' };
|
|
28
|
+
mockCodexFetch.mockReset();
|
|
29
|
+
mockParseCodexSSE.mockReset();
|
|
30
|
+
});
|
|
31
|
+
it('builds Codex messages and extracts system instructions', () => {
|
|
32
|
+
const messages = [
|
|
33
|
+
{ role: 'system', content: 'System prompt' },
|
|
34
|
+
{ role: 'user', content: 'Hello' },
|
|
35
|
+
{ role: 'assistant', content: 'Hi' },
|
|
36
|
+
];
|
|
37
|
+
const result = adapter.buildMessages(messages, options, config);
|
|
38
|
+
expect(result.systemParam).toBe('System prompt');
|
|
39
|
+
expect(result.messages).toHaveLength(2);
|
|
40
|
+
expect(result.messages[0].type).toBe('message');
|
|
41
|
+
expect(result.messages[0].role).toBe('user');
|
|
42
|
+
expect(result.messages[1].role).toBe('assistant');
|
|
43
|
+
});
|
|
44
|
+
it('normalizes function calls when arguments are missing', async () => {
|
|
45
|
+
mockCodexFetch.mockResolvedValue('sse');
|
|
46
|
+
mockParseCodexSSE.mockReturnValue({
|
|
47
|
+
outputText: '',
|
|
48
|
+
functionCalls: [{ callId: 'fc_1', name: 'Read', arguments: undefined }],
|
|
49
|
+
response: { usage: { input_tokens: 10, output_tokens: 5 }, output: [] },
|
|
50
|
+
});
|
|
51
|
+
const providerMessages = {
|
|
52
|
+
messages: [{ type: 'message', role: 'user', content: [{ type: 'input_text', text: 'x' }] }],
|
|
53
|
+
systemParam: 'sys',
|
|
54
|
+
};
|
|
55
|
+
const result = await adapter.call(providerMessages, [], options, config);
|
|
56
|
+
expect(result.hasToolCalls).toBe(true);
|
|
57
|
+
expect(result.toolCalls).toHaveLength(1);
|
|
58
|
+
expect(result.toolCalls[0].id).toBe('fc_1');
|
|
59
|
+
expect(result.toolCalls[0].rawArgs).toBe('{}');
|
|
60
|
+
expect(result.toolCalls[0].args).toEqual({});
|
|
61
|
+
});
|
|
62
|
+
it('appends assistant output items and function_call_output results', () => {
|
|
63
|
+
const providerMessages = { messages: [] };
|
|
64
|
+
adapter.appendAssistantResponse(providerMessages, {
|
|
65
|
+
output: [
|
|
66
|
+
{ type: 'function_call', call_id: 'fc_1', name: 'Read', arguments: '{}' },
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
adapter.appendToolResult(providerMessages, 'fc_1', 'ok');
|
|
70
|
+
expect(providerMessages.messages).toHaveLength(2);
|
|
71
|
+
expect(providerMessages.messages[0].type).toBe('function_call');
|
|
72
|
+
expect(providerMessages.messages[1]).toEqual({
|
|
73
|
+
type: 'function_call_output',
|
|
74
|
+
call_id: 'fc_1',
|
|
75
|
+
output: 'ok',
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
it('enables MCP tool discovery for Codex', () => {
|
|
79
|
+
expect(adapter.getToolDefinitionOptions()).toEqual({ includeMcp: true });
|
|
80
|
+
});
|
|
81
|
+
it('passes xhigh thinking through as Codex reasoning effort', async () => {
|
|
82
|
+
mockCodexFetch.mockResolvedValue('sse');
|
|
83
|
+
mockParseCodexSSE.mockReturnValue({
|
|
84
|
+
outputText: 'ok',
|
|
85
|
+
functionCalls: [],
|
|
86
|
+
response: { usage: { input_tokens: 10, output_tokens: 5 } },
|
|
87
|
+
});
|
|
88
|
+
await adapter.chat([{ role: 'user', content: 'Use deeper reasoning' }], { ...options, thinking: 'xhigh' }, config);
|
|
89
|
+
expect(mockCodexFetch).toHaveBeenCalledWith(expect.objectContaining({
|
|
90
|
+
reasoning: { effort: 'xhigh', summary: 'auto' },
|
|
91
|
+
}));
|
|
92
|
+
});
|
|
93
|
+
it('keeps Codex reasoning at medium by default', async () => {
|
|
94
|
+
mockCodexFetch.mockResolvedValue('sse');
|
|
95
|
+
mockParseCodexSSE.mockReturnValue({
|
|
96
|
+
outputText: 'ok',
|
|
97
|
+
functionCalls: [],
|
|
98
|
+
response: { usage: { input_tokens: 10, output_tokens: 5 } },
|
|
99
|
+
});
|
|
100
|
+
await adapter.call({ messages: [], systemParam: 'sys' }, [], options, config);
|
|
101
|
+
expect(mockCodexFetch).toHaveBeenCalledWith(expect.objectContaining({
|
|
102
|
+
reasoning: { effort: 'medium', summary: 'auto' },
|
|
103
|
+
}));
|
|
104
|
+
});
|
|
105
|
+
describe('onEmptyFinalResponse', () => {
|
|
106
|
+
it('makes a finalization API call and returns text', async () => {
|
|
107
|
+
mockCodexFetch.mockResolvedValue('sse-finalize');
|
|
108
|
+
mockParseCodexSSE.mockReturnValue({
|
|
109
|
+
outputText: 'Here is the final answer.',
|
|
110
|
+
functionCalls: [],
|
|
111
|
+
response: { usage: { input_tokens: 50, output_tokens: 20 } },
|
|
112
|
+
});
|
|
113
|
+
const providerMessages = {
|
|
114
|
+
messages: [
|
|
115
|
+
{ type: 'message', role: 'user', content: [{ type: 'input_text', text: 'test' }] },
|
|
116
|
+
{ type: 'function_call_output', call_id: 'fc_1', output: 'result data' },
|
|
117
|
+
],
|
|
118
|
+
systemParam: 'System prompt',
|
|
119
|
+
};
|
|
120
|
+
const result = await adapter.onEmptyFinalResponse(providerMessages, [], options, config);
|
|
121
|
+
expect(result).toBe('Here is the final answer.');
|
|
122
|
+
expect(mockCodexFetch).toHaveBeenCalledTimes(1);
|
|
123
|
+
// Should NOT include tools in the finalization body
|
|
124
|
+
const body = mockCodexFetch.mock.calls[0][0];
|
|
125
|
+
expect(body.tools).toBeUndefined();
|
|
126
|
+
// Should include the nudge message
|
|
127
|
+
const lastInput = body.input[body.input.length - 1];
|
|
128
|
+
expect(lastInput.role).toBe('user');
|
|
129
|
+
expect(lastInput.content[0].text).toContain('final answer');
|
|
130
|
+
});
|
|
131
|
+
it('returns undefined when finalization yields empty text', async () => {
|
|
132
|
+
mockCodexFetch.mockResolvedValue('sse-empty');
|
|
133
|
+
mockParseCodexSSE.mockReturnValue({
|
|
134
|
+
outputText: '',
|
|
135
|
+
functionCalls: [],
|
|
136
|
+
response: { usage: { input_tokens: 10, output_tokens: 0 } },
|
|
137
|
+
});
|
|
138
|
+
const providerMessages = {
|
|
139
|
+
messages: [],
|
|
140
|
+
systemParam: 'sys',
|
|
141
|
+
};
|
|
142
|
+
const result = await adapter.onEmptyFinalResponse(providerMessages, [], options, config);
|
|
143
|
+
expect(result).toBeUndefined();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
describe('call – response normalization', () => {
|
|
147
|
+
it('returns textContent from outputText when no tool calls', async () => {
|
|
148
|
+
mockCodexFetch.mockResolvedValue('sse');
|
|
149
|
+
mockParseCodexSSE.mockReturnValue({
|
|
150
|
+
outputText: 'Just text',
|
|
151
|
+
functionCalls: [],
|
|
152
|
+
response: { usage: { input_tokens: 100, output_tokens: 50 } },
|
|
153
|
+
});
|
|
154
|
+
const providerMessages = {
|
|
155
|
+
messages: [{ type: 'message', role: 'user', content: [{ type: 'input_text', text: 'hi' }] }],
|
|
156
|
+
systemParam: 'sys',
|
|
157
|
+
};
|
|
158
|
+
const result = await adapter.call(providerMessages, [], options, config);
|
|
159
|
+
expect(result.hasToolCalls).toBe(false);
|
|
160
|
+
expect(result.textContent).toBe('Just text');
|
|
161
|
+
expect(result.toolCalls).toHaveLength(0);
|
|
162
|
+
expect(result.usage?.inputTokens).toBe(100);
|
|
163
|
+
expect(result.usage?.outputTokens).toBe(50);
|
|
164
|
+
});
|
|
165
|
+
it('parses multiple function calls correctly', async () => {
|
|
166
|
+
mockCodexFetch.mockResolvedValue('sse');
|
|
167
|
+
mockParseCodexSSE.mockReturnValue({
|
|
168
|
+
outputText: '',
|
|
169
|
+
functionCalls: [
|
|
170
|
+
{ callId: 'fc_1', name: 'Read', arguments: '{"path":"/a.ts"}' },
|
|
171
|
+
{ callId: 'fc_2', name: 'Bash', arguments: '{"command":"ls"}' },
|
|
172
|
+
],
|
|
173
|
+
response: { usage: { input_tokens: 200, output_tokens: 100 } },
|
|
174
|
+
});
|
|
175
|
+
const providerMessages = { messages: [], systemParam: 'sys' };
|
|
176
|
+
const result = await adapter.call(providerMessages, [], options, config);
|
|
177
|
+
expect(result.hasToolCalls).toBe(true);
|
|
178
|
+
expect(result.toolCalls).toHaveLength(2);
|
|
179
|
+
expect(result.toolCalls[0].name).toBe('Read');
|
|
180
|
+
expect(result.toolCalls[0].args).toEqual({ path: '/a.ts' });
|
|
181
|
+
expect(result.toolCalls[1].name).toBe('Bash');
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
});
|