skimpyclaw 0.3.14 → 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 +101 -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 +349 -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,96 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach, vi } from 'vitest';
|
|
2
|
+
// Mock fs before importing the module so the module never touches disk during import.
|
|
3
|
+
vi.mock('fs', async () => {
|
|
4
|
+
const actual = await vi.importActual('fs');
|
|
5
|
+
return {
|
|
6
|
+
...actual,
|
|
7
|
+
existsSync: vi.fn(() => false),
|
|
8
|
+
readFileSync: vi.fn(() => '[]'),
|
|
9
|
+
writeFileSync: vi.fn(),
|
|
10
|
+
mkdirSync: vi.fn(),
|
|
11
|
+
};
|
|
12
|
+
});
|
|
13
|
+
import { addSession, getSession, updateStatus, enqueue, dequeue, markIdle, hasPending, listSessions, _resetForTesting, } from '../code-agents/interactive-sessions.js';
|
|
14
|
+
function makeSession(threadId = 't1', cliAgent = 'claude') {
|
|
15
|
+
return {
|
|
16
|
+
discordThreadId: threadId,
|
|
17
|
+
cliSessionId: 'uuid-' + threadId,
|
|
18
|
+
cliAgent,
|
|
19
|
+
status: 'active',
|
|
20
|
+
createdAt: new Date().toISOString(),
|
|
21
|
+
lastActivityAt: new Date().toISOString(),
|
|
22
|
+
initialTask: 'test task',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
describe('interactive-sessions state store', () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
_resetForTesting();
|
|
28
|
+
});
|
|
29
|
+
it('stores and retrieves a session', () => {
|
|
30
|
+
const s = makeSession();
|
|
31
|
+
addSession(s);
|
|
32
|
+
expect(getSession('t1')).toEqual(s);
|
|
33
|
+
expect(listSessions()).toHaveLength(1);
|
|
34
|
+
});
|
|
35
|
+
it('updates status and activity', () => {
|
|
36
|
+
addSession(makeSession());
|
|
37
|
+
updateStatus('t1', 'errored');
|
|
38
|
+
const s = getSession('t1');
|
|
39
|
+
expect(s?.status).toBe('errored');
|
|
40
|
+
});
|
|
41
|
+
it('returns undefined for unknown thread', () => {
|
|
42
|
+
expect(getSession('nope')).toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
it('enqueue returns shouldStart=true for first message', () => {
|
|
45
|
+
addSession(makeSession());
|
|
46
|
+
const r = enqueue('t1', 'hello');
|
|
47
|
+
expect(r.shouldStart).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
it('enqueue returns shouldStart=false while in-flight', () => {
|
|
50
|
+
addSession(makeSession());
|
|
51
|
+
enqueue('t1', 'first'); // marks inFlight
|
|
52
|
+
const r = enqueue('t1', 'second');
|
|
53
|
+
expect(r.shouldStart).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
it('dequeue returns messages FIFO', () => {
|
|
56
|
+
addSession(makeSession());
|
|
57
|
+
enqueue('t1', 'one');
|
|
58
|
+
enqueue('t1', 'two');
|
|
59
|
+
enqueue('t1', 'three');
|
|
60
|
+
// First was taken by the "in flight" starter; caller is responsible for
|
|
61
|
+
// processing it. The remaining two should dequeue in order.
|
|
62
|
+
// Our API returns the first via shouldStart=true — the caller then calls
|
|
63
|
+
// dequeue() to consume subsequent ones.
|
|
64
|
+
const first = dequeue('t1');
|
|
65
|
+
expect(first?.content).toBe('one');
|
|
66
|
+
const second = dequeue('t1');
|
|
67
|
+
expect(second?.content).toBe('two');
|
|
68
|
+
const third = dequeue('t1');
|
|
69
|
+
expect(third?.content).toBe('three');
|
|
70
|
+
expect(dequeue('t1')).toBeUndefined();
|
|
71
|
+
});
|
|
72
|
+
it('markIdle allows shouldStart=true on next enqueue', () => {
|
|
73
|
+
addSession(makeSession());
|
|
74
|
+
enqueue('t1', 'first');
|
|
75
|
+
markIdle('t1');
|
|
76
|
+
// Queue is empty AND not in flight → next enqueue starts
|
|
77
|
+
const r = enqueue('t1', 'next');
|
|
78
|
+
expect(r.shouldStart).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
it('hasPending reflects queue state', () => {
|
|
81
|
+
addSession(makeSession());
|
|
82
|
+
expect(hasPending('t1')).toBe(false);
|
|
83
|
+
enqueue('t1', 'one'); // inFlight=true, queue empty (first message is the "flight")
|
|
84
|
+
expect(hasPending('t1')).toBe(true); // we pushed to queue first
|
|
85
|
+
dequeue('t1');
|
|
86
|
+
expect(hasPending('t1')).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
it('per-thread queues are isolated', () => {
|
|
89
|
+
addSession(makeSession('t1'));
|
|
90
|
+
addSession(makeSession('t2'));
|
|
91
|
+
enqueue('t1', 'to t1');
|
|
92
|
+
enqueue('t2', 'to t2');
|
|
93
|
+
expect(dequeue('t1')?.content).toBe('to t1');
|
|
94
|
+
expect(dequeue('t2')?.content).toBe('to t2');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -1,18 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { calculateUsageCost } from '../langfuse.js';
|
|
3
3
|
describe('calculateUsageCost', () => {
|
|
4
|
-
it('uses updated OpenAI pricing for gpt-4o', () => {
|
|
5
|
-
const cost = calculateUsageCost('openai/gpt-4o', 1_000_000, 1_000_000);
|
|
6
|
-
expect(cost.inputCost).toBe(2.5);
|
|
7
|
-
expect(cost.outputCost).toBe(10);
|
|
8
|
-
expect(cost.totalCost).toBe(12.5);
|
|
9
|
-
});
|
|
10
|
-
it('uses updated OpenAI pricing for gpt-4o-mini', () => {
|
|
11
|
-
const cost = calculateUsageCost('gpt-4o-mini', 1_000_000, 1_000_000);
|
|
12
|
-
expect(cost.inputCost).toBe(0.15);
|
|
13
|
-
expect(cost.outputCost).toBe(0.6);
|
|
14
|
-
expect(cost.totalCost).toBe(0.75);
|
|
15
|
-
});
|
|
16
4
|
it('resolves codex provider model names', () => {
|
|
17
5
|
const cost = calculateUsageCost('codex/codex-5.3', 1_000_000, 1_000_000);
|
|
18
6
|
expect(cost.inputCost).toBe(1.75);
|
|
@@ -25,16 +13,16 @@ describe('calculateUsageCost', () => {
|
|
|
25
13
|
expect(cost.outputCost).toBe(14);
|
|
26
14
|
expect(cost.totalCost).toBe(15.75);
|
|
27
15
|
});
|
|
16
|
+
it('resolves codex5.5 alias pricing', () => {
|
|
17
|
+
const cost = calculateUsageCost('codex5.5', 1_000_000, 1_000_000);
|
|
18
|
+
expect(cost.inputCost).toBe(5);
|
|
19
|
+
expect(cost.outputCost).toBe(30);
|
|
20
|
+
expect(cost.totalCost).toBe(35);
|
|
21
|
+
});
|
|
28
22
|
it('resolves codex5.1 model pricing', () => {
|
|
29
23
|
const cost = calculateUsageCost('codex/gpt-5.1-codex', 1_000_000, 1_000_000);
|
|
30
24
|
expect(cost.inputCost).toBe(1.25);
|
|
31
25
|
expect(cost.outputCost).toBe(10);
|
|
32
26
|
expect(cost.totalCost).toBe(11.25);
|
|
33
27
|
});
|
|
34
|
-
it('resolves minimax alias case correctly', () => {
|
|
35
|
-
const cost = calculateUsageCost('minimax', 1_000_000, 1_000_000);
|
|
36
|
-
expect(cost.inputCost).toBe(0.3);
|
|
37
|
-
expect(cost.outputCost).toBe(1.2);
|
|
38
|
-
expect(cost.totalCost).toBe(1.5);
|
|
39
|
-
});
|
|
40
28
|
});
|
|
@@ -4,7 +4,6 @@ function mockConfig() {
|
|
|
4
4
|
return {
|
|
5
5
|
models: {
|
|
6
6
|
aliases: {
|
|
7
|
-
'claude-fast': 'anthropic/claude-haiku-4-5',
|
|
8
7
|
codex5: 'codex/gpt-5.3-codex',
|
|
9
8
|
},
|
|
10
9
|
},
|
|
@@ -12,10 +11,10 @@ function mockConfig() {
|
|
|
12
11
|
}
|
|
13
12
|
describe('model-selection', () => {
|
|
14
13
|
it('lists aliases sorted', () => {
|
|
15
|
-
expect(listModelAliases(mockConfig())).toEqual(['
|
|
14
|
+
expect(listModelAliases(mockConfig())).toEqual(['codex5']);
|
|
16
15
|
});
|
|
17
16
|
it('formats aliases', () => {
|
|
18
|
-
expect(formatAliases(mockConfig())).toBe('
|
|
17
|
+
expect(formatAliases(mockConfig())).toBe('codex5');
|
|
19
18
|
});
|
|
20
19
|
it('returns model selection usage string', () => {
|
|
21
20
|
expect(getModelSelectionUsage()).toBe('Use alias, provider/model, or model-id.');
|
|
@@ -56,7 +55,7 @@ describe('model-selection', () => {
|
|
|
56
55
|
it('formats model selection errors with aliases and usage', () => {
|
|
57
56
|
const text = formatModelSelectionError('Unknown model alias: "x"', mockConfig());
|
|
58
57
|
expect(text).toContain('Unknown model alias: "x"');
|
|
59
|
-
expect(text).toContain('Available aliases:
|
|
58
|
+
expect(text).toContain('Available aliases: codex5');
|
|
60
59
|
expect(text).toContain('Use alias, provider/model, or model-id.');
|
|
61
60
|
});
|
|
62
61
|
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { initProviders,
|
|
2
|
+
import { initProviders, addResponsesApiProvider, isResponsesApiProvider, setUsingOAuth, isUsingOAuth, } from '../providers/index.js';
|
|
3
3
|
function baseConfig(providers) {
|
|
4
4
|
return {
|
|
5
5
|
gateway: { port: 18790, mode: 'local' },
|
|
6
|
-
agents: { default: 'main', list: { main: { identity: { name: 'bot', emoji: 'x' }, model: '
|
|
6
|
+
agents: { default: 'main', list: { main: { identity: { name: 'bot', emoji: 'x' }, model: 'anthropic/claude-sonnet-4-6' } } },
|
|
7
7
|
models: { providers, aliases: {} },
|
|
8
8
|
channels: { telegram: { enabled: false, token: '', allowFrom: [] }, discord: { enabled: false, token: '', allowFrom: [] } },
|
|
9
9
|
cron: { jobs: [] },
|
|
@@ -11,12 +11,6 @@ function baseConfig(providers) {
|
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
13
|
describe('providers init reset behavior', () => {
|
|
14
|
-
it('clears stale OpenAI clients on re-init', async () => {
|
|
15
|
-
await initProviders(baseConfig({ openai: { apiKey: 'sk-test' } }));
|
|
16
|
-
expect(hasOpenAIClient('openai')).toBe(true);
|
|
17
|
-
await initProviders(baseConfig({}));
|
|
18
|
-
expect(hasOpenAIClient('openai')).toBe(false);
|
|
19
|
-
});
|
|
20
14
|
it('clears stale codex provider registrations on re-init', async () => {
|
|
21
15
|
addResponsesApiProvider('openai');
|
|
22
16
|
expect(isResponsesApiProvider('openai')).toBe(true);
|
|
@@ -3,7 +3,7 @@ import { addResponsesApiProvider, chat, initProviders } from '../providers/index
|
|
|
3
3
|
function baseConfig(providers) {
|
|
4
4
|
return {
|
|
5
5
|
gateway: { port: 18790, mode: 'local' },
|
|
6
|
-
agents: { default: 'main', list: { main: { identity: { name: 'bot', emoji: 'x' }, model: '
|
|
6
|
+
agents: { default: 'main', list: { main: { identity: { name: 'bot', emoji: 'x' }, model: 'anthropic/claude-sonnet-4-6' } } },
|
|
7
7
|
models: { providers, aliases: {} },
|
|
8
8
|
channels: { telegram: { enabled: false, token: '', allowFrom: [] }, discord: { enabled: false, token: '', allowFrom: [] } },
|
|
9
9
|
cron: { jobs: [] },
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { getProvider, stripProvider, resolveModel, resolveProviderRoute, shouldUseCodexAliasProvider, } from '../providers/utils.js';
|
|
2
|
+
import { getProvider, stripProvider, resolveModel, resolveProviderRoute, shouldUseCodexAliasProvider, buildThinkingConfig, } from '../providers/utils.js';
|
|
3
3
|
describe('provider utils', () => {
|
|
4
4
|
it('detects provider from explicit prefix', () => {
|
|
5
5
|
expect(getProvider('openai/gpt-5.3-codex')).toBe('openai');
|
|
6
6
|
expect(getProvider('codex/gpt-5.3-codex')).toBe('codex');
|
|
7
7
|
expect(getProvider('anthropic/claude-sonnet-4-6')).toBe('anthropic');
|
|
8
|
+
expect(getProvider('gpt-5.5')).toBe('codex');
|
|
8
9
|
});
|
|
9
10
|
it('strips provider prefix when registries are omitted', () => {
|
|
10
11
|
expect(stripProvider('openai/gpt-5.3-codex')).toBe('gpt-5.3-codex');
|
|
@@ -47,8 +48,14 @@ describe('provider utils', () => {
|
|
|
47
48
|
});
|
|
48
49
|
it('migrates deprecated claude opus 4 model ids', () => {
|
|
49
50
|
const cfg = { models: { aliases: {} } };
|
|
50
|
-
expect(resolveModel('claude-opus-4', cfg)).toBe('claude-opus-4-
|
|
51
|
-
expect(resolveModel('anthropic/claude-opus-4', cfg)).toBe('anthropic/claude-opus-4-
|
|
51
|
+
expect(resolveModel('claude-opus-4', cfg)).toBe('claude-opus-4-7');
|
|
52
|
+
expect(resolveModel('anthropic/claude-opus-4', cfg)).toBe('anthropic/claude-opus-4-7');
|
|
53
|
+
});
|
|
54
|
+
it('preserves explicit claude opus 4.6 model ids', () => {
|
|
55
|
+
const cfg = { models: { aliases: {} } };
|
|
56
|
+
expect(resolveModel('claude-opus-4.6', cfg)).toBe('claude-opus-4-6');
|
|
57
|
+
expect(resolveModel('anthropic/claude-opus-4.6', cfg)).toBe('anthropic/claude-opus-4-6');
|
|
58
|
+
expect(resolveModel('anthropic/claude-opus-4-6', cfg)).toBe('anthropic/claude-opus-4-6');
|
|
52
59
|
});
|
|
53
60
|
it('normalizes provider route fields after deprecated model migration', () => {
|
|
54
61
|
const cfg = { models: { aliases: {} } };
|
|
@@ -58,4 +65,7 @@ describe('provider utils', () => {
|
|
|
58
65
|
expect(route.modelId).toBe('claude-sonnet-4-6');
|
|
59
66
|
expect(route.isCodexModel).toBe(false);
|
|
60
67
|
});
|
|
68
|
+
it('supports xhigh thinking budgets', () => {
|
|
69
|
+
expect(buildThinkingConfig('xhigh')).toEqual({ budget: 32768, maxTokens: 36864 });
|
|
70
|
+
});
|
|
61
71
|
});
|
|
@@ -2,14 +2,17 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
|
2
2
|
import { mkdirSync, rmSync, existsSync, readFileSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { tmpdir } from 'os';
|
|
5
|
-
import { loadHistory, saveExchange, replaceWithSummary, clearHistory, setSessionsDir, MAX_HISTORY_PAIRS, } from '../sessions.js';
|
|
5
|
+
import { loadHistory, saveExchange, replaceWithSummary, clearHistory, setSessionsDir, clearSessionKeyCacheForTests, MAX_HISTORY_PAIRS, } from '../sessions.js';
|
|
6
6
|
let testSessionsDir;
|
|
7
7
|
beforeEach(() => {
|
|
8
|
+
clearSessionKeyCacheForTests();
|
|
9
|
+
process.env.SKIMPYCLAW_HISTORY_KEY = 'test-history-key';
|
|
8
10
|
testSessionsDir = join(tmpdir(), `sk-sessions-test-${Date.now()}`);
|
|
9
11
|
mkdirSync(testSessionsDir, { recursive: true });
|
|
10
12
|
setSessionsDir(testSessionsDir);
|
|
11
13
|
});
|
|
12
14
|
afterEach(() => {
|
|
15
|
+
delete process.env.SKIMPYCLAW_HISTORY_KEY;
|
|
13
16
|
if (existsSync(testSessionsDir)) {
|
|
14
17
|
rmSync(testSessionsDir, { recursive: true, force: true });
|
|
15
18
|
}
|
|
@@ -30,10 +33,10 @@ describe('saveExchange', () => {
|
|
|
30
33
|
const filePath = join(testSessionsDir, 'telegram-111.jsonl');
|
|
31
34
|
expect(existsSync(filePath)).toBe(true);
|
|
32
35
|
const content = readFileSync(filePath, 'utf-8');
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
expect(
|
|
36
|
-
expect(
|
|
36
|
+
expect(content.trim().startsWith('ENCv1:')).toBe(true);
|
|
37
|
+
const messages = await loadHistory('telegram', '111');
|
|
38
|
+
expect(messages[0].content).toBe('hello');
|
|
39
|
+
expect(messages[1].content).toBe('hi there');
|
|
37
40
|
});
|
|
38
41
|
it('appends multiple entries', async () => {
|
|
39
42
|
await saveExchange('telegram', '222', 'msg1', 'reply1');
|
|
@@ -42,8 +45,8 @@ describe('saveExchange', () => {
|
|
|
42
45
|
const content = readFileSync(filePath, 'utf-8');
|
|
43
46
|
const lines = content.trim().split('\n').filter(Boolean);
|
|
44
47
|
expect(lines).toHaveLength(2);
|
|
45
|
-
expect(
|
|
46
|
-
expect(
|
|
48
|
+
expect(lines[0].startsWith('ENCv1:')).toBe(true);
|
|
49
|
+
expect(lines[1].startsWith('ENCv1:')).toBe(true);
|
|
47
50
|
});
|
|
48
51
|
});
|
|
49
52
|
describe('loadHistory round-trip', () => {
|
|
@@ -96,9 +99,10 @@ describe('replaceWithSummary', () => {
|
|
|
96
99
|
const content = readFileSync(filePath, 'utf-8');
|
|
97
100
|
const lines = content.trim().split('\n').filter(Boolean);
|
|
98
101
|
expect(lines).toHaveLength(1);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
expect(
|
|
102
|
+
expect(lines[0].startsWith('ENCv1:')).toBe(true);
|
|
103
|
+
const messages = await loadHistory('telegram', '777');
|
|
104
|
+
expect(messages[0].content).toBe('Summary of our previous conversation:');
|
|
105
|
+
expect(messages[1].content).toBe('We talked about greetings.');
|
|
102
106
|
});
|
|
103
107
|
it('loadHistory after replaceWithSummary returns the summary pair', async () => {
|
|
104
108
|
await saveExchange('telegram', '888', 'hello', 'world');
|
|
@@ -11,7 +11,7 @@ describe('setup config generation', () => {
|
|
|
11
11
|
selectedProviders,
|
|
12
12
|
providerSecrets: { anthropicKey: 'sk-ant-test' },
|
|
13
13
|
});
|
|
14
|
-
expect(config.agents.list.main.model).toBe('claude-opus');
|
|
14
|
+
expect(config.agents.list.main.model).toBe('anthropic/claude-opus-4-7');
|
|
15
15
|
expect(config.models.providers.anthropic.apiKey).toBe('${ANTHROPIC_API_KEY}');
|
|
16
16
|
expect(config.models.providers.codex.authPath).toBe('${HOME}/.codex/auth.json');
|
|
17
17
|
expect(config.channels.telegram.allowFrom).toEqual([12345]);
|
|
@@ -19,30 +19,11 @@ describe('setup config generation', () => {
|
|
|
19
19
|
expect(config.channels.telegram.defaultAllowedPaths).toEqual(['${HOME}/.skimpyclaw']);
|
|
20
20
|
expect(config.channels.discord.defaultAllowedPaths).toEqual(['${HOME}/.skimpyclaw']);
|
|
21
21
|
expect(config.heartbeat.tools.allowedPaths).toEqual(['${HOME}/.skimpyclaw']);
|
|
22
|
-
expect(config.models.aliases
|
|
23
|
-
expect(config.models.aliases['claude-opus']).toBe('anthropic/claude-opus-4-6');
|
|
24
|
-
expect(config.models.aliases.codex).toBe('codex/gpt-5.3-codex');
|
|
22
|
+
expect(config.models.aliases.codex).toBe('codex/gpt-5.5');
|
|
25
23
|
expect(config.models.aliases['codex5.1']).toBe('codex/gpt-5.1-codex');
|
|
26
24
|
expect(config.models.aliases['codex5.2']).toBe('codex/gpt-5.2-codex');
|
|
27
25
|
expect(config.models.aliases['codex5.3']).toBe('codex/gpt-5.3-codex');
|
|
28
|
-
|
|
29
|
-
it('builds OpenAI-only config and env content', () => {
|
|
30
|
-
const selectedProviders = new Set(['openai-api']);
|
|
31
|
-
const { configJson, envContent } = buildSetupArtifacts({
|
|
32
|
-
workspaceDir: '/tmp/workspace',
|
|
33
|
-
telegramId: 'abc-user',
|
|
34
|
-
telegramToken: 'tg-token',
|
|
35
|
-
agentName: 'Claw',
|
|
36
|
-
selectedProviders,
|
|
37
|
-
providerSecrets: { openaiKey: 'sk-openai-test' },
|
|
38
|
-
});
|
|
39
|
-
const config = JSON.parse(configJson);
|
|
40
|
-
expect(config.agents.list.main.model).toBe('openai/gpt-4o');
|
|
41
|
-
expect(config.models.providers.openai.apiKey).toBe('${OPENAI_API_KEY}');
|
|
42
|
-
expect(config.channels.telegram.allowFrom).toEqual(['abc-user']);
|
|
43
|
-
expect(envContent).toContain('OPENAI_API_KEY=sk-openai-test');
|
|
44
|
-
expect(envContent).toContain('TELEGRAM_BOT_TOKEN=tg-token');
|
|
45
|
-
expect(envContent).not.toContain('ANTHROPIC_API_KEY=');
|
|
26
|
+
expect(config.models.aliases['codex5.5']).toBe('codex/gpt-5.5');
|
|
46
27
|
});
|
|
47
28
|
it('includes oauth placeholders when Anthropic OAuth is selected', () => {
|
|
48
29
|
const selectedProviders = new Set(['anthropic-oauth']);
|
|
@@ -71,7 +52,7 @@ describe('setup config generation', () => {
|
|
|
71
52
|
expect(config.gateway.host).toBe('127.0.0.1');
|
|
72
53
|
expect(config.gateway.port).toBe(18790);
|
|
73
54
|
});
|
|
74
|
-
it('
|
|
55
|
+
it('does not include browser tools in generated config', () => {
|
|
75
56
|
const config = buildSetupConfig({
|
|
76
57
|
workspaceDir: '/tmp/workspace',
|
|
77
58
|
telegramId: '12345',
|
|
@@ -79,12 +60,14 @@ describe('setup config generation', () => {
|
|
|
79
60
|
agentName: 'Claw',
|
|
80
61
|
selectedProviders: new Set(['anthropic-api']),
|
|
81
62
|
providerSecrets: { anthropicKey: 'sk-ant-test' },
|
|
82
|
-
features: {
|
|
63
|
+
features: { voice: false, mcp: false },
|
|
83
64
|
});
|
|
84
|
-
expect(config.heartbeat.tools.browser
|
|
65
|
+
expect(config.heartbeat.tools.browser).toBeUndefined();
|
|
66
|
+
expect(config.channels.telegram.tools.browser).toBeUndefined();
|
|
67
|
+
expect(config.channels.discord.tools.browser).toBeUndefined();
|
|
85
68
|
expect(config.voice).toBeUndefined();
|
|
86
69
|
});
|
|
87
|
-
it('enables
|
|
70
|
+
it('enables voice when requested', () => {
|
|
88
71
|
const config = buildSetupConfig({
|
|
89
72
|
workspaceDir: '/tmp/workspace',
|
|
90
73
|
telegramId: '12345',
|
|
@@ -92,9 +75,9 @@ describe('setup config generation', () => {
|
|
|
92
75
|
agentName: 'Claw',
|
|
93
76
|
selectedProviders: new Set(['anthropic-api']),
|
|
94
77
|
providerSecrets: { anthropicKey: 'sk-ant-test' },
|
|
95
|
-
features: {
|
|
78
|
+
features: { voice: true, mcp: false },
|
|
96
79
|
});
|
|
97
|
-
expect(config.heartbeat.tools.browser
|
|
80
|
+
expect(config.heartbeat.tools.browser).toBeUndefined();
|
|
98
81
|
expect(config.voice).toBeDefined();
|
|
99
82
|
expect(config.voice.enabled).toBe(true);
|
|
100
83
|
expect(config.voice.channels.telegram.sendVoice).toBe(true);
|
|
@@ -135,7 +118,7 @@ describe('setup config generation', () => {
|
|
|
135
118
|
});
|
|
136
119
|
expect(config.cron.jobs).toHaveLength(3);
|
|
137
120
|
expect(config.cron.jobs[0].id).toBe('memory-trim');
|
|
138
|
-
expect(config.cron.jobs[0].model).toBe('claude-
|
|
121
|
+
expect(config.cron.jobs[0].model).toBe('anthropic/claude-haiku-4-5');
|
|
139
122
|
expect(config.cron.jobs[1].id).toBe('tech-digest');
|
|
140
123
|
expect(config.cron.jobs[2].id).toBe('weather');
|
|
141
124
|
expect(config.cron.jobs[2].schedule.tz).toBe('America/New_York');
|
|
@@ -2,14 +2,17 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
|
2
2
|
import { mkdirSync, writeFileSync, rmSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { tmpdir } from 'os';
|
|
5
|
-
import {
|
|
5
|
+
import { randomUUID } from 'crypto';
|
|
6
|
+
import { loadSkills, getSkillsForContext, formatSkillsPrompt, checkEligibility, clearSkillsCache } from '../skills.js';
|
|
6
7
|
// Create a temp skills directory for each test
|
|
7
8
|
let skillsDir;
|
|
8
9
|
beforeEach(() => {
|
|
9
|
-
|
|
10
|
+
clearSkillsCache();
|
|
11
|
+
skillsDir = join(tmpdir(), `skimpyclaw-skills-test-${Date.now()}-${randomUUID()}`);
|
|
10
12
|
mkdirSync(skillsDir, { recursive: true });
|
|
11
13
|
});
|
|
12
14
|
afterEach(() => {
|
|
15
|
+
clearSkillsCache();
|
|
13
16
|
rmSync(skillsDir, { recursive: true, force: true });
|
|
14
17
|
});
|
|
15
18
|
function writeSkill(name, content) {
|
|
@@ -198,17 +201,17 @@ describe('checkEligibility', () => {
|
|
|
198
201
|
expect(result.reason).toContain('Missing path');
|
|
199
202
|
});
|
|
200
203
|
it('fails on tools requirement when tools not enabled', () => {
|
|
201
|
-
const result = checkEligibility({ name: 'test', description: 'test', requires: { tools: ['
|
|
204
|
+
const result = checkEligibility({ name: 'test', description: 'test', requires: { tools: ['Fetch'] } }, { enabled: false, allowedPaths: [] });
|
|
202
205
|
expect(result.eligible).toBe(false);
|
|
203
206
|
expect(result.reason).toContain('Tools not enabled');
|
|
204
207
|
});
|
|
205
|
-
it('fails on
|
|
208
|
+
it('fails on unsupported tool requirement', () => {
|
|
206
209
|
const result = checkEligibility({ name: 'test', description: 'test', requires: { tools: ['Browser'] } }, { enabled: true, allowedPaths: ['/tmp'] });
|
|
207
210
|
expect(result.eligible).toBe(false);
|
|
208
|
-
expect(result.reason).toContain('Browser');
|
|
211
|
+
expect(result.reason).toContain('Unsupported tools requested: Browser');
|
|
209
212
|
});
|
|
210
|
-
it('passes on
|
|
211
|
-
const result = checkEligibility({ name: 'test', description: 'test', requires: { tools: ['
|
|
213
|
+
it('passes on fetch tool requirement when tools are enabled', () => {
|
|
214
|
+
const result = checkEligibility({ name: 'test', description: 'test', requires: { tools: ['Fetch'] } }, { enabled: true, allowedPaths: ['/tmp'] });
|
|
212
215
|
expect(result.eligible).toBe(true);
|
|
213
216
|
});
|
|
214
217
|
it('passes on code_with_agent requirement when tools enabled', () => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { stripAnsi, chunkForDiscord, parseCodexJsonl, formatCodexOutput, } from '../code-agents/stream-formatter.js';
|
|
3
|
+
describe('stripAnsi', () => {
|
|
4
|
+
it('removes color codes', () => {
|
|
5
|
+
const input = '\x1B[31mred text\x1B[0m plain';
|
|
6
|
+
expect(stripAnsi(input)).toBe('red text plain');
|
|
7
|
+
});
|
|
8
|
+
it('removes cursor moves', () => {
|
|
9
|
+
const input = 'a\x1B[2Kb';
|
|
10
|
+
expect(stripAnsi(input)).toBe('ab');
|
|
11
|
+
});
|
|
12
|
+
it('passes through plain text unchanged', () => {
|
|
13
|
+
expect(stripAnsi('hello world')).toBe('hello world');
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
describe('chunkForDiscord', () => {
|
|
17
|
+
it('returns empty array for empty input', () => {
|
|
18
|
+
expect(chunkForDiscord('')).toEqual([]);
|
|
19
|
+
});
|
|
20
|
+
it('returns single chunk for short input', () => {
|
|
21
|
+
expect(chunkForDiscord('short text')).toEqual(['short text']);
|
|
22
|
+
});
|
|
23
|
+
it('splits on paragraph boundaries when possible', () => {
|
|
24
|
+
const p1 = 'a'.repeat(1000);
|
|
25
|
+
const p2 = 'b'.repeat(1000);
|
|
26
|
+
const input = p1 + '\n\n' + p2;
|
|
27
|
+
const chunks = chunkForDiscord(input, 1900);
|
|
28
|
+
// Both paragraphs fit in 1900 together (1000 + 2 + 1000 = 2002 > 1900)
|
|
29
|
+
expect(chunks).toHaveLength(2);
|
|
30
|
+
expect(chunks[0]).toBe(p1);
|
|
31
|
+
expect(chunks[1]).toBe(p2);
|
|
32
|
+
});
|
|
33
|
+
it('packs multiple small paragraphs together', () => {
|
|
34
|
+
const input = 'one\n\ntwo\n\nthree';
|
|
35
|
+
expect(chunkForDiscord(input, 1900)).toEqual(['one\n\ntwo\n\nthree']);
|
|
36
|
+
});
|
|
37
|
+
it('hard-splits a paragraph that is larger than max', () => {
|
|
38
|
+
const huge = 'x'.repeat(5000);
|
|
39
|
+
const chunks = chunkForDiscord(huge, 1900);
|
|
40
|
+
expect(chunks).toHaveLength(Math.ceil(5000 / 1900));
|
|
41
|
+
expect(chunks.every(c => c.length <= 1900)).toBe(true);
|
|
42
|
+
expect(chunks.join('')).toBe(huge);
|
|
43
|
+
});
|
|
44
|
+
it('strips ANSI before chunking', () => {
|
|
45
|
+
const input = '\x1B[31mred\x1B[0m and more';
|
|
46
|
+
expect(chunkForDiscord(input)).toEqual(['red and more']);
|
|
47
|
+
});
|
|
48
|
+
it('trims trailing whitespace but preserves internal', () => {
|
|
49
|
+
expect(chunkForDiscord('hello\n\n \n')).toEqual(['hello']);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe('parseCodexJsonl', () => {
|
|
53
|
+
it('captures thread_id from thread.started', () => {
|
|
54
|
+
const input = JSON.stringify({ type: 'thread.started', thread_id: 'abc-123' });
|
|
55
|
+
const r = parseCodexJsonl(input);
|
|
56
|
+
expect(r.threadId).toBe('abc-123');
|
|
57
|
+
expect(r.messages).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
it('extracts agent_message text', () => {
|
|
60
|
+
const input = [
|
|
61
|
+
JSON.stringify({ type: 'thread.started', thread_id: 't1' }),
|
|
62
|
+
JSON.stringify({ type: 'turn.started' }),
|
|
63
|
+
JSON.stringify({ type: 'item.completed', item: { type: 'agent_message', text: 'hi there' } }),
|
|
64
|
+
JSON.stringify({ type: 'turn.completed' }),
|
|
65
|
+
].join('\n');
|
|
66
|
+
const r = parseCodexJsonl(input);
|
|
67
|
+
expect(r.threadId).toBe('t1');
|
|
68
|
+
expect(r.messages).toEqual(['hi there']);
|
|
69
|
+
});
|
|
70
|
+
it('condenses command_execution as tool-call line', () => {
|
|
71
|
+
const input = JSON.stringify({
|
|
72
|
+
type: 'item.completed',
|
|
73
|
+
item: { type: 'command_execution', command: 'ls -la', status: 'completed' },
|
|
74
|
+
});
|
|
75
|
+
const r = parseCodexJsonl(input);
|
|
76
|
+
expect(r.messages[0]).toContain('✓');
|
|
77
|
+
expect(r.messages[0]).toContain('ls -la');
|
|
78
|
+
});
|
|
79
|
+
it('condenses file_change with paths', () => {
|
|
80
|
+
const input = JSON.stringify({
|
|
81
|
+
type: 'item.completed',
|
|
82
|
+
item: { type: 'file_change', changes: [{ path: 'src/foo.ts' }, { path: 'src/bar.ts' }] },
|
|
83
|
+
});
|
|
84
|
+
const r = parseCodexJsonl(input);
|
|
85
|
+
expect(r.messages[0]).toContain('src/foo.ts');
|
|
86
|
+
expect(r.messages[0]).toContain('src/bar.ts');
|
|
87
|
+
});
|
|
88
|
+
it('ignores turn.started/completed', () => {
|
|
89
|
+
const input = [
|
|
90
|
+
JSON.stringify({ type: 'turn.started' }),
|
|
91
|
+
JSON.stringify({ type: 'turn.completed' }),
|
|
92
|
+
].join('\n');
|
|
93
|
+
const r = parseCodexJsonl(input);
|
|
94
|
+
expect(r.messages).toEqual([]);
|
|
95
|
+
});
|
|
96
|
+
it('tolerates invalid JSON lines', () => {
|
|
97
|
+
const input = [
|
|
98
|
+
'not json',
|
|
99
|
+
JSON.stringify({ type: 'item.completed', item: { type: 'agent_message', text: 'hi' } }),
|
|
100
|
+
'{"broken":',
|
|
101
|
+
].join('\n');
|
|
102
|
+
const r = parseCodexJsonl(input);
|
|
103
|
+
expect(r.messages).toEqual(['hi']);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
describe('formatCodexOutput', () => {
|
|
107
|
+
it('chunks each message independently', () => {
|
|
108
|
+
const short = 'hi';
|
|
109
|
+
const long = 'y'.repeat(5000);
|
|
110
|
+
const chunks = formatCodexOutput([short, long]);
|
|
111
|
+
expect(chunks[0]).toBe('hi');
|
|
112
|
+
expect(chunks.slice(1).every(c => c.length <= 1900)).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
});
|