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,115 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { _setThreadAgentStorePathForTesting, bindThreadAgent, getAgentProfileByAlias, getThreadAgentByThreadId, listAgentProfiles, listThreadAgentBindings, parseDiscordAgentMention, removeAgentProfile, setAgentProfileModel, setAgentProfilePrompt, setAgentProfileThinking, upsertAgentProfile, } from '../channels/discord/thread-agents.js';
|
|
6
|
+
let tempDir;
|
|
7
|
+
let storePath;
|
|
8
|
+
describe('Discord thread agents registry', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
tempDir = mkdtempSync(join(tmpdir(), 'skimpyclaw-thread-agents-'));
|
|
11
|
+
storePath = join(tempDir, 'thread-agents.json');
|
|
12
|
+
_setThreadAgentStorePathForTesting(storePath);
|
|
13
|
+
});
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
_setThreadAgentStorePathForTesting(null);
|
|
16
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
it('stores reusable profiles and binds them to threads', () => {
|
|
19
|
+
const profile = upsertAgentProfile({
|
|
20
|
+
alias: 'Reviewer',
|
|
21
|
+
agentId: 'main',
|
|
22
|
+
createdBy: 'user-1',
|
|
23
|
+
});
|
|
24
|
+
const binding = bindThreadAgent({
|
|
25
|
+
threadId: 'thread-1',
|
|
26
|
+
alias: 'reviewer',
|
|
27
|
+
createdBy: 'user-1',
|
|
28
|
+
guildId: 'guild-1',
|
|
29
|
+
channelId: 'channel-1',
|
|
30
|
+
});
|
|
31
|
+
expect(profile.alias).toBe('reviewer');
|
|
32
|
+
expect(binding.alias).toBe('reviewer');
|
|
33
|
+
expect(getThreadAgentByThreadId('thread-1')?.agentId).toBe('main');
|
|
34
|
+
expect(listAgentProfiles()).toHaveLength(1);
|
|
35
|
+
expect(listThreadAgentBindings()).toHaveLength(1);
|
|
36
|
+
});
|
|
37
|
+
it('allows one profile to be reused across multiple threads', () => {
|
|
38
|
+
upsertAgentProfile({
|
|
39
|
+
alias: 'claude-coder',
|
|
40
|
+
agentId: 'main',
|
|
41
|
+
createdBy: 'user-1',
|
|
42
|
+
});
|
|
43
|
+
bindThreadAgent({ threadId: 'thread-1', alias: 'claude-coder', createdBy: 'user-1' });
|
|
44
|
+
bindThreadAgent({ threadId: 'thread-2', alias: 'claude-coder', createdBy: 'user-1' });
|
|
45
|
+
expect(listAgentProfiles()).toHaveLength(1);
|
|
46
|
+
expect(listThreadAgentBindings().map(record => record.threadId)).toEqual(['thread-1', 'thread-2']);
|
|
47
|
+
});
|
|
48
|
+
it('updates profile prompts, models, and thinking for all bound threads', () => {
|
|
49
|
+
upsertAgentProfile({
|
|
50
|
+
alias: 'claude-coder',
|
|
51
|
+
agentId: 'main',
|
|
52
|
+
createdBy: 'user-1',
|
|
53
|
+
});
|
|
54
|
+
bindThreadAgent({ threadId: 'thread-1', alias: 'claude-coder', createdBy: 'user-1' });
|
|
55
|
+
bindThreadAgent({ threadId: 'thread-2', alias: 'claude-coder', createdBy: 'user-1' });
|
|
56
|
+
expect(setAgentProfilePrompt('claude-coder', 'Review code like a senior engineer.')?.promptOverlay)
|
|
57
|
+
.toBe('Review code like a senior engineer.');
|
|
58
|
+
expect(setAgentProfileModel('claude-coder', 'anthropic/claude-sonnet-4-5')?.model)
|
|
59
|
+
.toBe('anthropic/claude-sonnet-4-5');
|
|
60
|
+
expect(setAgentProfileThinking('claude-coder', 'xhigh')?.thinking).toBe('xhigh');
|
|
61
|
+
expect(getThreadAgentByThreadId('thread-2')?.model).toBe('anthropic/claude-sonnet-4-5');
|
|
62
|
+
expect(getThreadAgentByThreadId('thread-2')?.thinking).toBe('xhigh');
|
|
63
|
+
});
|
|
64
|
+
it('deletes profiles with their bindings', () => {
|
|
65
|
+
upsertAgentProfile({
|
|
66
|
+
alias: 'reviewer',
|
|
67
|
+
agentId: 'main',
|
|
68
|
+
createdBy: 'user-1',
|
|
69
|
+
});
|
|
70
|
+
bindThreadAgent({ threadId: 'thread-1', alias: 'reviewer', createdBy: 'user-1' });
|
|
71
|
+
expect(removeAgentProfile('reviewer')).toBe(true);
|
|
72
|
+
expect(getAgentProfileByAlias('reviewer')).toBeNull();
|
|
73
|
+
expect(getThreadAgentByThreadId('thread-1')).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
it('migrates legacy thread-agent array stores', () => {
|
|
76
|
+
_setThreadAgentStorePathForTesting(null);
|
|
77
|
+
writeFileSync(storePath, JSON.stringify([
|
|
78
|
+
{
|
|
79
|
+
threadId: 'thread-1',
|
|
80
|
+
alias: 'Reviewer',
|
|
81
|
+
agentId: 'main',
|
|
82
|
+
model: 'anthropic/claude-sonnet-4-5',
|
|
83
|
+
thinking: 'high',
|
|
84
|
+
promptOverlay: 'Review carefully.',
|
|
85
|
+
createdBy: 'user-1',
|
|
86
|
+
createdAt: '2026-04-27T00:00:00.000Z',
|
|
87
|
+
updatedAt: '2026-04-27T00:00:00.000Z',
|
|
88
|
+
guildId: 'guild-1',
|
|
89
|
+
channelId: 'channel-1',
|
|
90
|
+
},
|
|
91
|
+
]), 'utf-8');
|
|
92
|
+
_setThreadAgentStorePathForTesting(storePath);
|
|
93
|
+
const migrated = getThreadAgentByThreadId('thread-1');
|
|
94
|
+
expect(migrated?.alias).toBe('reviewer');
|
|
95
|
+
expect(migrated?.model).toBe('anthropic/claude-sonnet-4-5');
|
|
96
|
+
expect(migrated?.thinking).toBe('high');
|
|
97
|
+
setAgentProfileModel('reviewer', 'anthropic/claude-opus-4-6');
|
|
98
|
+
const persisted = JSON.parse(readFileSync(storePath, 'utf-8'));
|
|
99
|
+
expect(persisted.version).toBe(2);
|
|
100
|
+
expect(persisted.profiles[0].alias).toBe('reviewer');
|
|
101
|
+
expect(persisted.bindings[0].profileAlias).toBe('reviewer');
|
|
102
|
+
});
|
|
103
|
+
it('parses leading agent mentions', () => {
|
|
104
|
+
expect(parseDiscordAgentMention('@Claude-Coder review this PR')).toEqual({
|
|
105
|
+
alias: 'claude-coder',
|
|
106
|
+
prompt: 'review this PR',
|
|
107
|
+
});
|
|
108
|
+
expect(parseDiscordAgentMention('@codex-reviewer')).toEqual({
|
|
109
|
+
alias: 'codex-reviewer',
|
|
110
|
+
prompt: '',
|
|
111
|
+
});
|
|
112
|
+
expect(parseDiscordAgentMention('please ask @codex-reviewer')).toBeNull();
|
|
113
|
+
expect(parseDiscordAgentMention('<@1234567890> review this')).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { buildCodeAgentThreadContext } from '../channels/discord/utils.js';
|
|
3
|
+
function makeMessage(channel) {
|
|
4
|
+
return { channel };
|
|
5
|
+
}
|
|
6
|
+
function makeTask(overrides = {}) {
|
|
7
|
+
return {
|
|
8
|
+
id: 'ca-12',
|
|
9
|
+
agent: 'codex',
|
|
10
|
+
task: 'Fix the Discord thread context bug',
|
|
11
|
+
status: 'completed',
|
|
12
|
+
discordThreadId: 'thread-1',
|
|
13
|
+
startedAt: '2026-04-27T14:00:00.000Z',
|
|
14
|
+
endedAt: '2026-04-27T14:05:00.000Z',
|
|
15
|
+
workdir: '/Users/katre/Sites/skimpyclaw',
|
|
16
|
+
outputPreview: 'Implemented a context bridge.',
|
|
17
|
+
validationPassed: true,
|
|
18
|
+
...overrides,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
describe('Discord coding-agent thread context', () => {
|
|
22
|
+
it('injects task metadata for messages inside a coding-agent thread', () => {
|
|
23
|
+
const message = makeMessage({
|
|
24
|
+
id: 'thread-1',
|
|
25
|
+
isDMBased: () => false,
|
|
26
|
+
isThread: () => true,
|
|
27
|
+
});
|
|
28
|
+
const context = buildCodeAgentThreadContext(message, [makeTask()]);
|
|
29
|
+
expect(context).toContain('Task ID: ca-12');
|
|
30
|
+
expect(context).toContain('Task status: completed');
|
|
31
|
+
expect(context).toContain('Fix the Discord thread context bug');
|
|
32
|
+
expect(context).toContain('check_code_agent with id "ca-12"');
|
|
33
|
+
});
|
|
34
|
+
it('returns null outside task threads', () => {
|
|
35
|
+
const message = makeMessage({
|
|
36
|
+
id: 'channel-1',
|
|
37
|
+
isDMBased: () => false,
|
|
38
|
+
isThread: () => false,
|
|
39
|
+
});
|
|
40
|
+
expect(buildCodeAgentThreadContext(message, [makeTask()])).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -21,11 +21,11 @@ describe('doctor formatters', () => {
|
|
|
21
21
|
remedy: 'Fix ~/.skimpyclaw/config.json to valid JSON and rerun doctor.',
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
|
-
name: '
|
|
24
|
+
name: 'provider_anthropic_auth',
|
|
25
25
|
category: 'provider_auth',
|
|
26
26
|
ok: false,
|
|
27
27
|
detail: '401 Unauthorized',
|
|
28
|
-
remedy: 'Check
|
|
28
|
+
remedy: 'Check ANTHROPIC_API_KEY and provider base URL.',
|
|
29
29
|
},
|
|
30
30
|
],
|
|
31
31
|
};
|
|
@@ -36,11 +36,11 @@ describe('doctor formatters', () => {
|
|
|
36
36
|
expect(output).toContain('provider_auth');
|
|
37
37
|
expect(output).toContain('✓ node_version');
|
|
38
38
|
expect(output).toContain('✗ config_json_valid');
|
|
39
|
-
expect(output).toContain('✗
|
|
39
|
+
expect(output).toContain('✗ provider_anthropic_auth');
|
|
40
40
|
expect(output).toContain('Unexpected token } in JSON at position 10');
|
|
41
41
|
expect(output).toContain('401 Unauthorized');
|
|
42
42
|
expect(output).toContain('Fix ~/.skimpyclaw/config.json to valid JSON and rerun doctor.');
|
|
43
|
-
expect(output).toContain('Check
|
|
43
|
+
expect(output).toContain('Check ANTHROPIC_API_KEY and provider base URL.');
|
|
44
44
|
});
|
|
45
45
|
it('does not print undefined when a failed check has no remedy', () => {
|
|
46
46
|
const output = formatDoctorHuman({
|
|
@@ -20,7 +20,7 @@ describe('doctor index integration', () => {
|
|
|
20
20
|
finishedAt: '2026-02-12T10:00:02.000Z',
|
|
21
21
|
checks: [
|
|
22
22
|
{ name: 'node_version', category: 'environment', ok: true, detail: 'v20.11.0' },
|
|
23
|
-
{ name: '
|
|
23
|
+
{ name: 'provider_anthropic_auth', category: 'provider_auth', ok: false, detail: '401 Unauthorized' },
|
|
24
24
|
],
|
|
25
25
|
};
|
|
26
26
|
beforeEach(() => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
const { mockLoadConfig, mockCheckNodeVersion, mockCheckPackageManagerAvailable, mockCheckTypeScriptCompile, mockCheckConfigExistsAndValidJson, mockCheckRequiredEnvVars, mockCheckEnvVarPatterns, mockCheckAllowedPathsWritable, mockCheckProviderAuth, mockCheckTelegramToken, mockCheckDiscordToken,
|
|
2
|
+
const { mockLoadConfig, mockCheckNodeVersion, mockCheckPackageManagerAvailable, mockCheckTypeScriptCompile, mockCheckConfigExistsAndValidJson, mockCheckRequiredEnvVars, mockCheckEnvVarPatterns, mockCheckAllowedPathsWritable, mockCheckProviderAuth, mockCheckTelegramToken, mockCheckDiscordToken, mockCheckVoiceDependencies, mockCheckMcpConfig, mockCheckGatewayHostBindable, mockCheckSkimpyclawDirWritable, mockCheckPortAvailability, } = vi.hoisted(() => ({
|
|
3
3
|
mockLoadConfig: vi.fn(),
|
|
4
4
|
mockCheckNodeVersion: vi.fn(),
|
|
5
5
|
mockCheckPackageManagerAvailable: vi.fn(),
|
|
@@ -11,14 +11,11 @@ const { mockLoadConfig, mockCheckNodeVersion, mockCheckPackageManagerAvailable,
|
|
|
11
11
|
mockCheckProviderAuth: vi.fn(),
|
|
12
12
|
mockCheckTelegramToken: vi.fn(),
|
|
13
13
|
mockCheckDiscordToken: vi.fn(),
|
|
14
|
-
mockCheckBrowserBinaryIfEnabled: vi.fn(),
|
|
15
|
-
mockCheckPlaywrightIfBrowserEnabled: vi.fn(),
|
|
16
14
|
mockCheckVoiceDependencies: vi.fn(),
|
|
17
15
|
mockCheckMcpConfig: vi.fn(),
|
|
18
16
|
mockCheckGatewayHostBindable: vi.fn(),
|
|
19
17
|
mockCheckSkimpyclawDirWritable: vi.fn(),
|
|
20
18
|
mockCheckPortAvailability: vi.fn(),
|
|
21
|
-
mockCheckSandboxAvailable: vi.fn(),
|
|
22
19
|
}));
|
|
23
20
|
vi.mock('../config.js', () => ({
|
|
24
21
|
loadConfig: mockLoadConfig,
|
|
@@ -34,14 +31,11 @@ vi.mock('../doctor/checks.js', () => ({
|
|
|
34
31
|
checkProviderAuth: mockCheckProviderAuth,
|
|
35
32
|
checkTelegramToken: mockCheckTelegramToken,
|
|
36
33
|
checkDiscordToken: mockCheckDiscordToken,
|
|
37
|
-
checkBrowserBinaryIfEnabled: mockCheckBrowserBinaryIfEnabled,
|
|
38
|
-
checkPlaywrightIfBrowserEnabled: mockCheckPlaywrightIfBrowserEnabled,
|
|
39
34
|
checkVoiceDependencies: mockCheckVoiceDependencies,
|
|
40
35
|
checkMcpConfig: mockCheckMcpConfig,
|
|
41
36
|
checkGatewayHostBindable: mockCheckGatewayHostBindable,
|
|
42
37
|
checkSkimpyclawDirWritable: mockCheckSkimpyclawDirWritable,
|
|
43
38
|
checkPortAvailability: mockCheckPortAvailability,
|
|
44
|
-
checkSandboxAvailable: mockCheckSandboxAvailable,
|
|
45
39
|
}));
|
|
46
40
|
import { computeExitCode, runDoctor } from '../doctor/runner.js';
|
|
47
41
|
function okCheck(name, category, detail = 'ok') {
|
|
@@ -53,7 +47,7 @@ describe('doctor runner', () => {
|
|
|
53
47
|
gateway: { port: 18790 },
|
|
54
48
|
models: {
|
|
55
49
|
providers: {
|
|
56
|
-
|
|
50
|
+
anthropic: { apiKey: '${ANTHROPIC_API_KEY}' },
|
|
57
51
|
},
|
|
58
52
|
},
|
|
59
53
|
channels: {
|
|
@@ -76,14 +70,11 @@ describe('doctor runner', () => {
|
|
|
76
70
|
mockCheckProviderAuth.mockReset();
|
|
77
71
|
mockCheckTelegramToken.mockReset();
|
|
78
72
|
mockCheckDiscordToken.mockReset();
|
|
79
|
-
mockCheckBrowserBinaryIfEnabled.mockReset();
|
|
80
|
-
mockCheckPlaywrightIfBrowserEnabled.mockReset();
|
|
81
73
|
mockCheckVoiceDependencies.mockReset();
|
|
82
74
|
mockCheckMcpConfig.mockReset();
|
|
83
75
|
mockCheckGatewayHostBindable.mockReset();
|
|
84
76
|
mockCheckSkimpyclawDirWritable.mockReset();
|
|
85
77
|
mockCheckPortAvailability.mockReset();
|
|
86
|
-
mockCheckSandboxAvailable.mockReset();
|
|
87
78
|
mockCheckNodeVersion.mockResolvedValue(okCheck('node_version', 'environment', 'v20.11.0'));
|
|
88
79
|
mockCheckPackageManagerAvailable.mockResolvedValue(okCheck('package_manager_available', 'environment', 'pnpm'));
|
|
89
80
|
mockCheckTypeScriptCompile.mockResolvedValue(okCheck('typescript_compile', 'environment'));
|
|
@@ -91,17 +82,14 @@ describe('doctor runner', () => {
|
|
|
91
82
|
mockCheckRequiredEnvVars.mockResolvedValue(okCheck('required_env_vars', 'configuration'));
|
|
92
83
|
mockCheckEnvVarPatterns.mockResolvedValue(okCheck('env_var_patterns', 'configuration'));
|
|
93
84
|
mockCheckAllowedPathsWritable.mockResolvedValue(okCheck('allowed_paths_writable', 'configuration'));
|
|
94
|
-
mockCheckProviderAuth.mockResolvedValue(okCheck('
|
|
85
|
+
mockCheckProviderAuth.mockResolvedValue(okCheck('provider_anthropic_auth', 'provider_auth'));
|
|
95
86
|
mockCheckTelegramToken.mockResolvedValue(okCheck('telegram_token_valid', 'channels'));
|
|
96
87
|
mockCheckDiscordToken.mockResolvedValue(okCheck('discord_token_valid', 'channels'));
|
|
97
|
-
mockCheckBrowserBinaryIfEnabled.mockResolvedValue(okCheck('browser_binary_available', 'runtime'));
|
|
98
|
-
mockCheckPlaywrightIfBrowserEnabled.mockResolvedValue(okCheck('playwright_installed', 'runtime', 'Browser tools disabled'));
|
|
99
88
|
mockCheckVoiceDependencies.mockResolvedValue(okCheck('voice_dependencies', 'runtime', 'Voice disabled'));
|
|
100
89
|
mockCheckMcpConfig.mockResolvedValue(okCheck('mcp_config', 'runtime', 'MCP tools not configured'));
|
|
101
90
|
mockCheckGatewayHostBindable.mockResolvedValue(okCheck('gateway_host_bindable', 'runtime', '127.0.0.1 (always available)'));
|
|
102
91
|
mockCheckSkimpyclawDirWritable.mockResolvedValue(okCheck('skimpyclaw_dirs_writable', 'runtime'));
|
|
103
92
|
mockCheckPortAvailability.mockResolvedValue(okCheck('gateway_port_available', 'runtime'));
|
|
104
|
-
mockCheckSandboxAvailable.mockResolvedValue(okCheck('sandbox_available', 'runtime', 'Sandbox disabled'));
|
|
105
93
|
});
|
|
106
94
|
it('computes exit code 0 when all checks pass', () => {
|
|
107
95
|
const code = computeExitCode({ checks: [okCheck('node_version', 'environment')] });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { sanitizeExecEnv, sanitizeCronEnv } from '../env-sanitizer.js';
|
|
3
|
+
describe('sanitizeExecEnv', () => {
|
|
4
|
+
it('strips sensitive env vars while preserving allowlisted values', () => {
|
|
5
|
+
process.env.OPENAI_API_KEY = 'secret-openai';
|
|
6
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = 'secret-claude';
|
|
7
|
+
process.env.GH_TOKEN = 'allowed-gh';
|
|
8
|
+
process.env.NORMAL_VAR = 'ok';
|
|
9
|
+
const env = sanitizeExecEnv();
|
|
10
|
+
expect(env.OPENAI_API_KEY).toBeUndefined();
|
|
11
|
+
expect(env.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined();
|
|
12
|
+
expect(env.GH_TOKEN).toBe('allowed-gh');
|
|
13
|
+
expect(env.NORMAL_VAR).toBe('ok');
|
|
14
|
+
delete process.env.OPENAI_API_KEY;
|
|
15
|
+
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
16
|
+
delete process.env.GH_TOKEN;
|
|
17
|
+
delete process.env.NORMAL_VAR;
|
|
18
|
+
});
|
|
19
|
+
it('ensures common tool paths are present in PATH', () => {
|
|
20
|
+
process.env.PATH = '/usr/bin:/bin';
|
|
21
|
+
const env = sanitizeExecEnv();
|
|
22
|
+
expect(env.PATH).toContain('/opt/homebrew/bin');
|
|
23
|
+
expect(env.PATH).toContain('/usr/local/bin');
|
|
24
|
+
});
|
|
25
|
+
it('applies strict allowlist for cron environments', () => {
|
|
26
|
+
process.env.OPENAI_API_KEY = 'secret';
|
|
27
|
+
process.env.GH_TOKEN = 'allowed-gh';
|
|
28
|
+
process.env.SKIMPYCLAW_MODE = 'prod';
|
|
29
|
+
process.env.CUSTOM_RANDOM_VAR = 'do-not-include';
|
|
30
|
+
process.env.PATH = '/usr/bin:/bin';
|
|
31
|
+
process.env.HOME = '/tmp/home';
|
|
32
|
+
const env = sanitizeCronEnv();
|
|
33
|
+
expect(env.OPENAI_API_KEY).toBeUndefined();
|
|
34
|
+
expect(env.CUSTOM_RANDOM_VAR).toBeUndefined();
|
|
35
|
+
expect(env.GH_TOKEN).toBe('allowed-gh');
|
|
36
|
+
expect(env.SKIMPYCLAW_MODE).toBe('prod');
|
|
37
|
+
expect(env.HOME).toBe('/tmp/home');
|
|
38
|
+
expect(env.PATH).toContain('/opt/homebrew/bin');
|
|
39
|
+
delete process.env.OPENAI_API_KEY;
|
|
40
|
+
delete process.env.GH_TOKEN;
|
|
41
|
+
delete process.env.SKIMPYCLAW_MODE;
|
|
42
|
+
delete process.env.CUSTOM_RANDOM_VAR;
|
|
43
|
+
delete process.env.HOME;
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -205,6 +205,14 @@ describe('approval registry', () => {
|
|
|
205
205
|
const found = findApprovedRequest('sudo apt update');
|
|
206
206
|
expect(found).toBeUndefined();
|
|
207
207
|
});
|
|
208
|
+
it('consumed approval is still retrievable by ID', () => {
|
|
209
|
+
const approval = createApprovalRequest('sudo apt update', undefined, { tier: 2, reason: 'test' });
|
|
210
|
+
approveRequest(approval.id);
|
|
211
|
+
consumeApproval(approval.id);
|
|
212
|
+
const fetched = getApproval(approval.id);
|
|
213
|
+
expect(fetched).toBeDefined();
|
|
214
|
+
expect(fetched?.status).toBe('consumed');
|
|
215
|
+
});
|
|
208
216
|
it('expires pending approvals past TTL', () => {
|
|
209
217
|
const approval = createApprovalRequest('sudo cmd', undefined, { tier: 2, reason: 'test' }, { ttlMs: 1 });
|
|
210
218
|
// Wait briefly for TTL to expire
|
|
@@ -321,3 +329,56 @@ describe('approval events', () => {
|
|
|
321
329
|
expect(events).toHaveLength(1); // No new event
|
|
322
330
|
});
|
|
323
331
|
});
|
|
332
|
+
describe('approval history', () => {
|
|
333
|
+
it('new approval starts with empty history', () => {
|
|
334
|
+
const approval = createApprovalRequest('sudo cmd', undefined, { tier: 2, reason: 'test' });
|
|
335
|
+
expect(approval.history).toEqual([]);
|
|
336
|
+
});
|
|
337
|
+
it('records a single transition on approve', () => {
|
|
338
|
+
const approval = createApprovalRequest('sudo cmd', undefined, { tier: 2, reason: 'test' });
|
|
339
|
+
approveRequest(approval.id, 'admin');
|
|
340
|
+
const fetched = getApproval(approval.id);
|
|
341
|
+
expect(fetched.history).toHaveLength(1);
|
|
342
|
+
expect(fetched.history[0].from).toBe('pending');
|
|
343
|
+
expect(fetched.history[0].to).toBe('approved');
|
|
344
|
+
expect(fetched.history[0].by).toBe('admin');
|
|
345
|
+
expect(fetched.history[0].at).toBeInstanceOf(Date);
|
|
346
|
+
});
|
|
347
|
+
it('records a single transition on deny', () => {
|
|
348
|
+
const approval = createApprovalRequest('sudo cmd', undefined, { tier: 2, reason: 'test' });
|
|
349
|
+
denyRequest(approval.id, 'security');
|
|
350
|
+
const fetched = getApproval(approval.id);
|
|
351
|
+
expect(fetched.history).toHaveLength(1);
|
|
352
|
+
expect(fetched.history[0].from).toBe('pending');
|
|
353
|
+
expect(fetched.history[0].to).toBe('denied');
|
|
354
|
+
expect(fetched.history[0].by).toBe('security');
|
|
355
|
+
});
|
|
356
|
+
it('records multiple transitions through full lifecycle', () => {
|
|
357
|
+
const approval = createApprovalRequest('sudo cmd', undefined, { tier: 2, reason: 'test' });
|
|
358
|
+
approveRequest(approval.id, 'admin');
|
|
359
|
+
consumeApproval(approval.id);
|
|
360
|
+
const fetched = getApproval(approval.id);
|
|
361
|
+
expect(fetched.history).toHaveLength(2);
|
|
362
|
+
expect(fetched.history[0]).toMatchObject({ from: 'pending', to: 'approved', by: 'admin' });
|
|
363
|
+
expect(fetched.history[1]).toMatchObject({ from: 'approved', to: 'consumed' });
|
|
364
|
+
expect(fetched.status).toBe('consumed');
|
|
365
|
+
});
|
|
366
|
+
it('no-op status update does not add history entries', () => {
|
|
367
|
+
const approval = createApprovalRequest('sudo cmd', undefined, { tier: 2, reason: 'test' });
|
|
368
|
+
approveRequest(approval.id, 'admin');
|
|
369
|
+
// Try to approve again — should fail, no history added
|
|
370
|
+
const result = approveRequest(approval.id, 'other');
|
|
371
|
+
expect(result).toBe(false);
|
|
372
|
+
const fetched = getApproval(approval.id);
|
|
373
|
+
expect(fetched.history).toHaveLength(1); // Only the first approve
|
|
374
|
+
});
|
|
375
|
+
it('records expiration transition in history', () => {
|
|
376
|
+
const approval = createApprovalRequest('sudo cmd', undefined, { tier: 2, reason: 'test' }, { ttlMs: 1 });
|
|
377
|
+
const start = Date.now();
|
|
378
|
+
while (Date.now() - start < 5) { /* spin for TTL */ }
|
|
379
|
+
cleanupExpired();
|
|
380
|
+
const fetched = getApproval(approval.id);
|
|
381
|
+
expect(fetched.history).toHaveLength(1);
|
|
382
|
+
expect(fetched.history[0]).toMatchObject({ from: 'pending', to: 'expired' });
|
|
383
|
+
});
|
|
384
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
const mockLookup = vi.hoisted(() => vi.fn());
|
|
3
|
+
vi.mock('dns/promises', () => ({
|
|
4
|
+
lookup: (...args) => mockLookup(...args),
|
|
5
|
+
}));
|
|
6
|
+
const mockFetch = vi.fn();
|
|
7
|
+
global.fetch = mockFetch;
|
|
8
|
+
const { executeFetch } = await import('../tools/fetch-tool.js');
|
|
9
|
+
describe('fetch-tool SSRF protections', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.clearAllMocks();
|
|
12
|
+
});
|
|
13
|
+
it('blocks localhost targets', async () => {
|
|
14
|
+
const out = await executeFetch({ url: 'http://localhost:8080/secret' }, {});
|
|
15
|
+
expect(out).toContain('Error:');
|
|
16
|
+
expect(out).toContain('Blocked host');
|
|
17
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
18
|
+
});
|
|
19
|
+
it('blocks non-http protocols', async () => {
|
|
20
|
+
const out = await executeFetch({ url: 'file:///etc/passwd' }, {});
|
|
21
|
+
expect(out).toContain('Error:');
|
|
22
|
+
expect(out).toContain('Unsupported protocol');
|
|
23
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
24
|
+
});
|
|
25
|
+
it('blocks hostnames resolving to private IPs', async () => {
|
|
26
|
+
mockLookup.mockResolvedValueOnce([{ address: '10.0.0.5', family: 4 }]);
|
|
27
|
+
const out = await executeFetch({ url: 'https://example.test/data' }, {});
|
|
28
|
+
expect(out).toContain('Error:');
|
|
29
|
+
expect(out).toContain('Blocked resolved IP');
|
|
30
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
31
|
+
});
|
|
32
|
+
it('blocks redirects to internal targets', async () => {
|
|
33
|
+
mockLookup.mockResolvedValueOnce([{ address: '93.184.216.34', family: 4 }]);
|
|
34
|
+
mockFetch.mockResolvedValueOnce({
|
|
35
|
+
status: 302,
|
|
36
|
+
statusText: 'Found',
|
|
37
|
+
headers: new Headers({ location: 'http://127.0.0.1/internal' }),
|
|
38
|
+
text: async () => '',
|
|
39
|
+
});
|
|
40
|
+
const out = await executeFetch({ url: 'https://public.example/path' }, {});
|
|
41
|
+
expect(out).toContain('Error:');
|
|
42
|
+
expect(out).toContain('Blocked target IP');
|
|
43
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
44
|
+
});
|
|
45
|
+
it('re-validates DNS on each redirect hop', async () => {
|
|
46
|
+
mockLookup
|
|
47
|
+
.mockResolvedValueOnce([{ address: '93.184.216.34', family: 4 }])
|
|
48
|
+
.mockResolvedValueOnce([{ address: '93.184.216.34', family: 4 }]);
|
|
49
|
+
mockFetch
|
|
50
|
+
.mockResolvedValueOnce({
|
|
51
|
+
status: 302,
|
|
52
|
+
statusText: 'Found',
|
|
53
|
+
headers: new Headers({ location: 'https://example.com/next' }),
|
|
54
|
+
text: async () => '',
|
|
55
|
+
})
|
|
56
|
+
.mockResolvedValueOnce({
|
|
57
|
+
status: 200,
|
|
58
|
+
statusText: 'OK',
|
|
59
|
+
headers: new Headers({ 'content-type': 'text/plain' }),
|
|
60
|
+
text: async () => 'ok',
|
|
61
|
+
});
|
|
62
|
+
const out = await executeFetch({ url: 'https://example.com/start' }, {});
|
|
63
|
+
expect(out).toContain('HTTP 200 OK');
|
|
64
|
+
expect(mockLookup).toHaveBeenCalledTimes(2);
|
|
65
|
+
});
|
|
66
|
+
it('returns response body for valid public targets', async () => {
|
|
67
|
+
mockLookup.mockResolvedValueOnce([{ address: '93.184.216.34', family: 4 }]);
|
|
68
|
+
mockFetch.mockResolvedValueOnce({
|
|
69
|
+
status: 200,
|
|
70
|
+
statusText: 'OK',
|
|
71
|
+
headers: new Headers({ 'content-type': 'text/plain' }),
|
|
72
|
+
text: async () => 'ok',
|
|
73
|
+
});
|
|
74
|
+
const out = await executeFetch({ url: 'https://example.com' }, {});
|
|
75
|
+
expect(out).toContain('HTTP 200 OK');
|
|
76
|
+
expect(out).toContain('ok');
|
|
77
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.any(URL), expect.objectContaining({ redirect: 'manual' }));
|
|
78
|
+
});
|
|
79
|
+
it('blocks internal-style host suffixes', async () => {
|
|
80
|
+
const out = await executeFetch({ url: 'https://service.internal/api' }, {});
|
|
81
|
+
expect(out).toContain('Error:');
|
|
82
|
+
expect(out).toContain('Blocked internal host');
|
|
83
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
vi.mock('../api.js', () => ({
|
|
3
|
+
registerDashboardAPI: vi.fn(),
|
|
4
|
+
}));
|
|
5
|
+
vi.mock('../dashboard-frontend.js', () => ({
|
|
6
|
+
registerDashboard: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
vi.mock('../cron.js', () => ({
|
|
9
|
+
getCronJobs: () => [{ id: 'job-1', name: 'Job 1', nextRun: undefined }],
|
|
10
|
+
runCronJob: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
vi.mock('../agent.js', () => ({
|
|
13
|
+
runAgentTurn: vi.fn().mockResolvedValue('ok'),
|
|
14
|
+
}));
|
|
15
|
+
vi.mock('../config.js', () => ({
|
|
16
|
+
ensureDashboardToken: () => 'test-token',
|
|
17
|
+
getLogsDir: () => '/tmp/test-skimpyclaw/logs',
|
|
18
|
+
}));
|
|
19
|
+
const { createGateway } = await import('../gateway.js');
|
|
20
|
+
const cfg = {
|
|
21
|
+
gateway: { port: 18790 },
|
|
22
|
+
agents: {
|
|
23
|
+
default: 'main',
|
|
24
|
+
list: {
|
|
25
|
+
main: {
|
|
26
|
+
model: 'anthropic/claude-haiku-4-5',
|
|
27
|
+
identity: { name: 'Bot', emoji: 'x' },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
cron: { jobs: [] },
|
|
32
|
+
};
|
|
33
|
+
describe('gateway /status auth', () => {
|
|
34
|
+
afterEach(async () => {
|
|
35
|
+
vi.clearAllMocks();
|
|
36
|
+
});
|
|
37
|
+
it('keeps /health unauthenticated', async () => {
|
|
38
|
+
const app = await createGateway(cfg);
|
|
39
|
+
try {
|
|
40
|
+
const res = await app.inject({ method: 'GET', url: '/health' });
|
|
41
|
+
expect(res.statusCode).toBe(200);
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await app.close();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
it('requires auth for /status', async () => {
|
|
48
|
+
const app = await createGateway(cfg);
|
|
49
|
+
try {
|
|
50
|
+
const res = await app.inject({ method: 'GET', url: '/status' });
|
|
51
|
+
expect(res.statusCode).toBe(401);
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
await app.close();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
it('allows /status with valid bearer token', async () => {
|
|
58
|
+
const app = await createGateway(cfg);
|
|
59
|
+
try {
|
|
60
|
+
const res = await app.inject({
|
|
61
|
+
method: 'GET',
|
|
62
|
+
url: '/status',
|
|
63
|
+
headers: { authorization: 'Bearer test-token' },
|
|
64
|
+
});
|
|
65
|
+
expect(res.statusCode).toBe(200);
|
|
66
|
+
expect(res.json().status).toBe('ok');
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await app.close();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -22,7 +22,7 @@ describe('heartbeat prompt path normalization', () => {
|
|
|
22
22
|
heartbeat: {
|
|
23
23
|
intervalMs: 60000,
|
|
24
24
|
prompt: 'Read /Users/example/HEARTBEAT.md only. Reply HEARTBEAT_OK.',
|
|
25
|
-
model: 'claude-
|
|
25
|
+
model: 'anthropic/claude-haiku-4-5',
|
|
26
26
|
tools: {
|
|
27
27
|
enabled: true,
|
|
28
28
|
allowedPaths: ['/Users/example/.skimpyclaw'],
|
|
@@ -39,7 +39,7 @@ describe('heartbeat prompt path normalization', () => {
|
|
|
39
39
|
};
|
|
40
40
|
await runHeartbeatCheck(config);
|
|
41
41
|
expect(mockRunAgentTurn).toHaveBeenCalledTimes(1);
|
|
42
|
-
expect(mockRunAgentTurn).toHaveBeenCalledWith('main', expect.stringContaining('/.skimpyclaw/agents/main/HEARTBEAT.md'), config, 'claude-
|
|
42
|
+
expect(mockRunAgentTurn).toHaveBeenCalledWith('main', expect.stringContaining('/.skimpyclaw/agents/main/HEARTBEAT.md'), config, 'anthropic/claude-haiku-4-5', expect.any(Object), undefined, expect.any(Object));
|
|
43
43
|
});
|
|
44
44
|
it('normalizes /workspace heartbeat path to agents/main/HEARTBEAT.md', async () => {
|
|
45
45
|
const config = {
|
|
@@ -47,7 +47,7 @@ describe('heartbeat prompt path normalization', () => {
|
|
|
47
47
|
heartbeat: {
|
|
48
48
|
intervalMs: 60000,
|
|
49
49
|
prompt: 'Read /workspace/HEARTBEAT.md only. Reply HEARTBEAT_OK.',
|
|
50
|
-
model: 'claude-
|
|
50
|
+
model: 'anthropic/claude-haiku-4-5',
|
|
51
51
|
tools: {
|
|
52
52
|
enabled: true,
|
|
53
53
|
allowedPaths: ['/Users/example/.skimpyclaw'],
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|