@rozek/nanoclaw 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.json +1 -0
- package/.claude/skills/add-compact/SKILL.md +135 -0
- package/.claude/skills/add-discord/SKILL.md +203 -0
- package/.claude/skills/add-gmail/SKILL.md +220 -0
- package/.claude/skills/add-image-vision/SKILL.md +94 -0
- package/.claude/skills/add-ollama-tool/SKILL.md +153 -0
- package/.claude/skills/add-parallel/SKILL.md +290 -0
- package/.claude/skills/add-pdf-reader/SKILL.md +104 -0
- package/.claude/skills/add-reactions/SKILL.md +117 -0
- package/.claude/skills/add-slack/SKILL.md +207 -0
- package/.claude/skills/add-telegram/SKILL.md +222 -0
- package/.claude/skills/add-telegram-swarm/SKILL.md +384 -0
- package/.claude/skills/add-voice-transcription/SKILL.md +148 -0
- package/.claude/skills/add-whatsapp/SKILL.md +372 -0
- package/.claude/skills/convert-to-apple-container/SKILL.md +175 -0
- package/.claude/skills/customize/SKILL.md +110 -0
- package/.claude/skills/debug/SKILL.md +349 -0
- package/.claude/skills/get-qodo-rules/SKILL.md +122 -0
- package/.claude/skills/get-qodo-rules/references/output-format.md +41 -0
- package/.claude/skills/get-qodo-rules/references/pagination.md +33 -0
- package/.claude/skills/get-qodo-rules/references/repository-scope.md +26 -0
- package/.claude/skills/qodo-pr-resolver/SKILL.md +326 -0
- package/.claude/skills/qodo-pr-resolver/resources/providers.md +329 -0
- package/.claude/skills/setup/SKILL.md +218 -0
- package/.claude/skills/update-nanoclaw/SKILL.md +235 -0
- package/.claude/skills/update-skills/SKILL.md +130 -0
- package/.claude/skills/use-local-whisper/SKILL.md +152 -0
- package/.claude/skills/x-integration/SKILL.md +417 -0
- package/.claude/skills/x-integration/agent.ts +243 -0
- package/.claude/skills/x-integration/host.ts +159 -0
- package/.claude/skills/x-integration/lib/browser.ts +148 -0
- package/.claude/skills/x-integration/lib/config.ts +62 -0
- package/.claude/skills/x-integration/scripts/like.ts +56 -0
- package/.claude/skills/x-integration/scripts/post.ts +66 -0
- package/.claude/skills/x-integration/scripts/quote.ts +80 -0
- package/.claude/skills/x-integration/scripts/reply.ts +74 -0
- package/.claude/skills/x-integration/scripts/retweet.ts +62 -0
- package/.claude/skills/x-integration/scripts/setup.ts +87 -0
- package/.env.example +1 -0
- package/.github/CODEOWNERS +10 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +14 -0
- package/.github/workflows/bump-version.yml +32 -0
- package/.github/workflows/ci.yml +25 -0
- package/.github/workflows/merge-forward-skills.yml +160 -0
- package/.github/workflows/update-tokens.yml +42 -0
- package/.husky/pre-commit +1 -0
- package/.mcp.json +3 -0
- package/.nvmrc +1 -0
- package/.prettierrc +3 -0
- package/CHANGELOG.md +8 -0
- package/CLAUDE.md +64 -0
- package/CONTRIBUTING.md +23 -0
- package/CONTRIBUTORS.md +15 -0
- package/LICENSE +21 -0
- package/NanoClaw_with_Web-Support.md +325 -0
- package/README.md +261 -0
- package/README_zh.md +200 -0
- package/assets/nanoclaw-favicon.png +0 -0
- package/assets/nanoclaw-icon.png +0 -0
- package/assets/nanoclaw-logo-dark.png +0 -0
- package/assets/nanoclaw-logo.png +0 -0
- package/assets/nanoclaw-profile.jpeg +0 -0
- package/assets/nanoclaw-sales.png +0 -0
- package/assets/social-preview.jpg +0 -0
- package/config-examples/mount-allowlist.json +25 -0
- package/container/Dockerfile +70 -0
- package/container/agent-runner/package.json +21 -0
- package/container/agent-runner/src/index.ts +774 -0
- package/container/agent-runner/src/ipc-mcp-stdio.ts +338 -0
- package/container/agent-runner/tsconfig.json +15 -0
- package/container/build.sh +23 -0
- package/container/skills/agent-browser/SKILL.md +159 -0
- package/container/skills/capabilities/SKILL.md +100 -0
- package/container/skills/cwd/SKILL.md +32 -0
- package/container/skills/pwd/SKILL.md +19 -0
- package/container/skills/status/SKILL.md +104 -0
- package/dist/channels/index.d.ts +2 -0
- package/dist/channels/index.d.ts.map +1 -0
- package/dist/channels/index.js +10 -0
- package/dist/channels/index.js.map +1 -0
- package/dist/channels/registry.d.ts +13 -0
- package/dist/channels/registry.d.ts.map +1 -0
- package/dist/channels/registry.js +11 -0
- package/dist/channels/registry.js.map +1 -0
- package/dist/channels/registry.test.d.ts +2 -0
- package/dist/channels/registry.test.d.ts.map +1 -0
- package/dist/channels/registry.test.js +32 -0
- package/dist/channels/registry.test.js.map +1 -0
- package/dist/channels/web.d.ts +2 -0
- package/dist/channels/web.d.ts.map +1 -0
- package/dist/channels/web.js +1843 -0
- package/dist/channels/web.js.map +1 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +182 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +36 -0
- package/dist/config.js.map +1 -0
- package/dist/container-runner.d.ts +44 -0
- package/dist/container-runner.d.ts.map +1 -0
- package/dist/container-runner.js +511 -0
- package/dist/container-runner.js.map +1 -0
- package/dist/container-runner.test.d.ts +2 -0
- package/dist/container-runner.test.d.ts.map +1 -0
- package/dist/container-runner.test.js +150 -0
- package/dist/container-runner.test.js.map +1 -0
- package/dist/container-runtime.d.ts +22 -0
- package/dist/container-runtime.d.ts.map +1 -0
- package/dist/container-runtime.js +96 -0
- package/dist/container-runtime.js.map +1 -0
- package/dist/container-runtime.test.d.ts +2 -0
- package/dist/container-runtime.test.d.ts.map +1 -0
- package/dist/container-runtime.test.js +93 -0
- package/dist/container-runtime.test.js.map +1 -0
- package/dist/credential-proxy.d.ts +21 -0
- package/dist/credential-proxy.d.ts.map +1 -0
- package/dist/credential-proxy.js +95 -0
- package/dist/credential-proxy.js.map +1 -0
- package/dist/credential-proxy.test.d.ts +2 -0
- package/dist/credential-proxy.test.d.ts.map +1 -0
- package/dist/credential-proxy.test.js +134 -0
- package/dist/credential-proxy.test.js.map +1 -0
- package/dist/db.d.ts +115 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +549 -0
- package/dist/db.js.map +1 -0
- package/dist/db.test.d.ts +2 -0
- package/dist/db.test.d.ts.map +1 -0
- package/dist/db.test.js +360 -0
- package/dist/db.test.js.map +1 -0
- package/dist/env.d.ts +8 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +42 -0
- package/dist/env.js.map +1 -0
- package/dist/formatting.test.d.ts +2 -0
- package/dist/formatting.test.d.ts.map +1 -0
- package/dist/formatting.test.js +183 -0
- package/dist/formatting.test.js.map +1 -0
- package/dist/group-folder.d.ts +5 -0
- package/dist/group-folder.d.ts.map +1 -0
- package/dist/group-folder.js +44 -0
- package/dist/group-folder.js.map +1 -0
- package/dist/group-folder.test.d.ts +2 -0
- package/dist/group-folder.test.d.ts.map +1 -0
- package/dist/group-folder.test.js +29 -0
- package/dist/group-folder.test.js.map +1 -0
- package/dist/group-queue.d.ts +40 -0
- package/dist/group-queue.d.ts.map +1 -0
- package/dist/group-queue.js +276 -0
- package/dist/group-queue.js.map +1 -0
- package/dist/group-queue.test.d.ts +2 -0
- package/dist/group-queue.test.d.ts.map +1 -0
- package/dist/group-queue.test.js +341 -0
- package/dist/group-queue.test.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +592 -0
- package/dist/index.js.map +1 -0
- package/dist/ipc-auth.test.d.ts +2 -0
- package/dist/ipc-auth.test.d.ts.map +1 -0
- package/dist/ipc-auth.test.js +434 -0
- package/dist/ipc-auth.test.js.map +1 -0
- package/dist/ipc.d.ts +32 -0
- package/dist/ipc.d.ts.map +1 -0
- package/dist/ipc.js +311 -0
- package/dist/ipc.js.map +1 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +14 -0
- package/dist/logger.js.map +1 -0
- package/dist/mount-security.d.ts +34 -0
- package/dist/mount-security.d.ts.map +1 -0
- package/dist/mount-security.js +325 -0
- package/dist/mount-security.js.map +1 -0
- package/dist/remote-control.d.ts +32 -0
- package/dist/remote-control.d.ts.map +1 -0
- package/dist/remote-control.js +185 -0
- package/dist/remote-control.js.map +1 -0
- package/dist/remote-control.test.d.ts +2 -0
- package/dist/remote-control.test.d.ts.map +1 -0
- package/dist/remote-control.test.js +321 -0
- package/dist/remote-control.test.js.map +1 -0
- package/dist/router.d.ts +8 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +37 -0
- package/dist/router.js.map +1 -0
- package/dist/routing.test.d.ts +2 -0
- package/dist/routing.test.d.ts.map +1 -0
- package/dist/routing.test.js +81 -0
- package/dist/routing.test.js.map +1 -0
- package/dist/sender-allowlist.d.ts +14 -0
- package/dist/sender-allowlist.d.ts.map +1 -0
- package/dist/sender-allowlist.js +79 -0
- package/dist/sender-allowlist.js.map +1 -0
- package/dist/sender-allowlist.test.d.ts +2 -0
- package/dist/sender-allowlist.test.d.ts.map +1 -0
- package/dist/sender-allowlist.test.js +186 -0
- package/dist/sender-allowlist.test.js.map +1 -0
- package/dist/session-commands.d.ts +47 -0
- package/dist/session-commands.d.ts.map +1 -0
- package/dist/session-commands.js +104 -0
- package/dist/session-commands.js.map +1 -0
- package/dist/session-commands.test.d.ts +2 -0
- package/dist/session-commands.test.d.ts.map +1 -0
- package/dist/session-commands.test.js +194 -0
- package/dist/session-commands.test.js.map +1 -0
- package/dist/task-scheduler.d.ts +22 -0
- package/dist/task-scheduler.d.ts.map +1 -0
- package/dist/task-scheduler.js +241 -0
- package/dist/task-scheduler.js.map +1 -0
- package/dist/task-scheduler.test.d.ts +2 -0
- package/dist/task-scheduler.test.d.ts.map +1 -0
- package/dist/task-scheduler.test.js +107 -0
- package/dist/task-scheduler.test.js.map +1 -0
- package/dist/timezone.d.ts +6 -0
- package/dist/timezone.d.ts.map +1 -0
- package/dist/timezone.js +17 -0
- package/dist/timezone.js.map +1 -0
- package/dist/timezone.test.d.ts +2 -0
- package/dist/timezone.test.d.ts.map +1 -0
- package/dist/timezone.test.js +23 -0
- package/dist/timezone.test.js.map +1 -0
- package/dist/types.d.ts +79 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/docs/APPLE-CONTAINER-NETWORKING.md +90 -0
- package/docs/DEBUG_CHECKLIST.md +143 -0
- package/docs/REQUIREMENTS.md +196 -0
- package/docs/SDK_DEEP_DIVE.md +643 -0
- package/docs/SECURITY.md +122 -0
- package/docs/SPEC.md +785 -0
- package/docs/docker-sandboxes.md +359 -0
- package/docs/nanoclaw-architecture-final.md +1063 -0
- package/docs/nanorepo-architecture.md +168 -0
- package/docs/skills-as-branches.md +662 -0
- package/groups/global/CLAUDE.md +58 -0
- package/groups/main/CLAUDE.md +246 -0
- package/launchd/com.nanoclaw.plist +32 -0
- package/package.json +45 -0
- package/repo-tokens/README.md +113 -0
- package/repo-tokens/action.yml +186 -0
- package/repo-tokens/badge.svg +23 -0
- package/repo-tokens/examples/green.svg +14 -0
- package/repo-tokens/examples/red.svg +14 -0
- package/repo-tokens/examples/yellow-green.svg +14 -0
- package/repo-tokens/examples/yellow.svg +14 -0
- package/scripts/run-migrations.ts +105 -0
- package/setup/container.ts +144 -0
- package/setup/environment.test.ts +121 -0
- package/setup/environment.ts +94 -0
- package/setup/groups.ts +229 -0
- package/setup/index.ts +58 -0
- package/setup/mounts.ts +115 -0
- package/setup/platform.test.ts +120 -0
- package/setup/platform.ts +132 -0
- package/setup/register.test.ts +257 -0
- package/setup/register.ts +177 -0
- package/setup/service.test.ts +187 -0
- package/setup/service.ts +362 -0
- package/setup/status.ts +16 -0
- package/setup/verify.ts +192 -0
- package/setup.sh +161 -0
- package/src/channels/index.ts +15 -0
- package/src/channels/registry.test.ts +42 -0
- package/src/channels/registry.ts +32 -0
- package/src/channels/web.ts +1931 -0
- package/src/cli.ts +209 -0
- package/src/config.ts +73 -0
- package/src/container-runner.test.ts +210 -0
- package/src/container-runner.ts +768 -0
- package/src/container-runtime.test.ts +149 -0
- package/src/container-runtime.ts +127 -0
- package/src/credential-proxy.test.ts +192 -0
- package/src/credential-proxy.ts +125 -0
- package/src/db.test.ts +484 -0
- package/src/db.ts +803 -0
- package/src/env.ts +42 -0
- package/src/formatting.test.ts +256 -0
- package/src/group-folder.test.ts +43 -0
- package/src/group-folder.ts +44 -0
- package/src/group-queue.test.ts +484 -0
- package/src/group-queue.ts +379 -0
- package/src/index.ts +832 -0
- package/src/ipc-auth.test.ts +679 -0
- package/src/ipc.ts +461 -0
- package/src/logger.ts +16 -0
- package/src/mount-security.ts +419 -0
- package/src/remote-control.test.ts +397 -0
- package/src/remote-control.ts +224 -0
- package/src/router.ts +52 -0
- package/src/routing.test.ts +170 -0
- package/src/sender-allowlist.test.ts +216 -0
- package/src/sender-allowlist.ts +128 -0
- package/src/session-commands.test.ts +247 -0
- package/src/session-commands.ts +163 -0
- package/src/task-scheduler.test.ts +129 -0
- package/src/task-scheduler.ts +328 -0
- package/src/timezone.test.ts +29 -0
- package/src/timezone.ts +16 -0
- package/src/types.ts +109 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +7 -0
- package/vitest.skills.config.ts +7 -0
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
_initTestDatabase,
|
|
5
|
+
createTask,
|
|
6
|
+
getAllTasks,
|
|
7
|
+
getRegisteredGroup,
|
|
8
|
+
getTaskById,
|
|
9
|
+
setRegisteredGroup,
|
|
10
|
+
} from './db.js';
|
|
11
|
+
import { processTaskIpc, IpcDeps } from './ipc.js';
|
|
12
|
+
import { RegisteredGroup } from './types.js';
|
|
13
|
+
|
|
14
|
+
// Set up registered groups used across tests
|
|
15
|
+
const MAIN_GROUP: RegisteredGroup = {
|
|
16
|
+
name: 'Main',
|
|
17
|
+
folder: 'whatsapp_main',
|
|
18
|
+
trigger: 'always',
|
|
19
|
+
added_at: '2024-01-01T00:00:00.000Z',
|
|
20
|
+
isMain: true,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const OTHER_GROUP: RegisteredGroup = {
|
|
24
|
+
name: 'Other',
|
|
25
|
+
folder: 'other-group',
|
|
26
|
+
trigger: '@Andy',
|
|
27
|
+
added_at: '2024-01-01T00:00:00.000Z',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const THIRD_GROUP: RegisteredGroup = {
|
|
31
|
+
name: 'Third',
|
|
32
|
+
folder: 'third-group',
|
|
33
|
+
trigger: '@Andy',
|
|
34
|
+
added_at: '2024-01-01T00:00:00.000Z',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
let groups: Record<string, RegisteredGroup>;
|
|
38
|
+
let deps: IpcDeps;
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
_initTestDatabase();
|
|
42
|
+
|
|
43
|
+
groups = {
|
|
44
|
+
'main@g.us': MAIN_GROUP,
|
|
45
|
+
'other@g.us': OTHER_GROUP,
|
|
46
|
+
'third@g.us': THIRD_GROUP,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Populate DB as well
|
|
50
|
+
setRegisteredGroup('main@g.us', MAIN_GROUP);
|
|
51
|
+
setRegisteredGroup('other@g.us', OTHER_GROUP);
|
|
52
|
+
setRegisteredGroup('third@g.us', THIRD_GROUP);
|
|
53
|
+
|
|
54
|
+
deps = {
|
|
55
|
+
sendMessage: async () => {},
|
|
56
|
+
registeredGroups: () => groups,
|
|
57
|
+
registerGroup: (jid, group) => {
|
|
58
|
+
groups[jid] = group;
|
|
59
|
+
setRegisteredGroup(jid, group);
|
|
60
|
+
// Mock the fs.mkdirSync that registerGroup does
|
|
61
|
+
},
|
|
62
|
+
syncGroups: async () => {},
|
|
63
|
+
getAvailableGroups: () => [],
|
|
64
|
+
writeGroupsSnapshot: () => {},
|
|
65
|
+
onTasksChanged: () => {},
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// --- schedule_task authorization ---
|
|
70
|
+
|
|
71
|
+
describe('schedule_task authorization', () => {
|
|
72
|
+
it('main group can schedule for another group', async () => {
|
|
73
|
+
await processTaskIpc(
|
|
74
|
+
{
|
|
75
|
+
type: 'schedule_task',
|
|
76
|
+
prompt: 'do something',
|
|
77
|
+
schedule_type: 'once',
|
|
78
|
+
schedule_value: '2025-06-01T00:00:00',
|
|
79
|
+
targetJid: 'other@g.us',
|
|
80
|
+
},
|
|
81
|
+
'whatsapp_main',
|
|
82
|
+
true,
|
|
83
|
+
deps,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Verify task was created in DB for the other group
|
|
87
|
+
const allTasks = getAllTasks();
|
|
88
|
+
expect(allTasks.length).toBe(1);
|
|
89
|
+
expect(allTasks[0].group_folder).toBe('other-group');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('non-main group can schedule for itself', async () => {
|
|
93
|
+
await processTaskIpc(
|
|
94
|
+
{
|
|
95
|
+
type: 'schedule_task',
|
|
96
|
+
prompt: 'self task',
|
|
97
|
+
schedule_type: 'once',
|
|
98
|
+
schedule_value: '2025-06-01T00:00:00',
|
|
99
|
+
targetJid: 'other@g.us',
|
|
100
|
+
},
|
|
101
|
+
'other-group',
|
|
102
|
+
false,
|
|
103
|
+
deps,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const allTasks = getAllTasks();
|
|
107
|
+
expect(allTasks.length).toBe(1);
|
|
108
|
+
expect(allTasks[0].group_folder).toBe('other-group');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('non-main group cannot schedule for another group', async () => {
|
|
112
|
+
await processTaskIpc(
|
|
113
|
+
{
|
|
114
|
+
type: 'schedule_task',
|
|
115
|
+
prompt: 'unauthorized',
|
|
116
|
+
schedule_type: 'once',
|
|
117
|
+
schedule_value: '2025-06-01T00:00:00',
|
|
118
|
+
targetJid: 'main@g.us',
|
|
119
|
+
},
|
|
120
|
+
'other-group',
|
|
121
|
+
false,
|
|
122
|
+
deps,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const allTasks = getAllTasks();
|
|
126
|
+
expect(allTasks.length).toBe(0);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('rejects schedule_task for unregistered target JID', async () => {
|
|
130
|
+
await processTaskIpc(
|
|
131
|
+
{
|
|
132
|
+
type: 'schedule_task',
|
|
133
|
+
prompt: 'no target',
|
|
134
|
+
schedule_type: 'once',
|
|
135
|
+
schedule_value: '2025-06-01T00:00:00',
|
|
136
|
+
targetJid: 'unknown@g.us',
|
|
137
|
+
},
|
|
138
|
+
'whatsapp_main',
|
|
139
|
+
true,
|
|
140
|
+
deps,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const allTasks = getAllTasks();
|
|
144
|
+
expect(allTasks.length).toBe(0);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// --- pause_task authorization ---
|
|
149
|
+
|
|
150
|
+
describe('pause_task authorization', () => {
|
|
151
|
+
beforeEach(() => {
|
|
152
|
+
createTask({
|
|
153
|
+
id: 'task-main',
|
|
154
|
+
group_folder: 'whatsapp_main',
|
|
155
|
+
chat_jid: 'main@g.us',
|
|
156
|
+
prompt: 'main task',
|
|
157
|
+
schedule_type: 'once',
|
|
158
|
+
schedule_value: '2025-06-01T00:00:00',
|
|
159
|
+
context_mode: 'isolated',
|
|
160
|
+
next_run: '2025-06-01T00:00:00.000Z',
|
|
161
|
+
status: 'active',
|
|
162
|
+
created_at: '2024-01-01T00:00:00.000Z',
|
|
163
|
+
});
|
|
164
|
+
createTask({
|
|
165
|
+
id: 'task-other',
|
|
166
|
+
group_folder: 'other-group',
|
|
167
|
+
chat_jid: 'other@g.us',
|
|
168
|
+
prompt: 'other task',
|
|
169
|
+
schedule_type: 'once',
|
|
170
|
+
schedule_value: '2025-06-01T00:00:00',
|
|
171
|
+
context_mode: 'isolated',
|
|
172
|
+
next_run: '2025-06-01T00:00:00.000Z',
|
|
173
|
+
status: 'active',
|
|
174
|
+
created_at: '2024-01-01T00:00:00.000Z',
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('main group can pause any task', async () => {
|
|
179
|
+
await processTaskIpc(
|
|
180
|
+
{ type: 'pause_task', taskId: 'task-other' },
|
|
181
|
+
'whatsapp_main',
|
|
182
|
+
true,
|
|
183
|
+
deps,
|
|
184
|
+
);
|
|
185
|
+
expect(getTaskById('task-other')!.status).toBe('paused');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('non-main group can pause its own task', async () => {
|
|
189
|
+
await processTaskIpc(
|
|
190
|
+
{ type: 'pause_task', taskId: 'task-other' },
|
|
191
|
+
'other-group',
|
|
192
|
+
false,
|
|
193
|
+
deps,
|
|
194
|
+
);
|
|
195
|
+
expect(getTaskById('task-other')!.status).toBe('paused');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('non-main group cannot pause another groups task', async () => {
|
|
199
|
+
await processTaskIpc(
|
|
200
|
+
{ type: 'pause_task', taskId: 'task-main' },
|
|
201
|
+
'other-group',
|
|
202
|
+
false,
|
|
203
|
+
deps,
|
|
204
|
+
);
|
|
205
|
+
expect(getTaskById('task-main')!.status).toBe('active');
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// --- resume_task authorization ---
|
|
210
|
+
|
|
211
|
+
describe('resume_task authorization', () => {
|
|
212
|
+
beforeEach(() => {
|
|
213
|
+
createTask({
|
|
214
|
+
id: 'task-paused',
|
|
215
|
+
group_folder: 'other-group',
|
|
216
|
+
chat_jid: 'other@g.us',
|
|
217
|
+
prompt: 'paused task',
|
|
218
|
+
schedule_type: 'once',
|
|
219
|
+
schedule_value: '2025-06-01T00:00:00',
|
|
220
|
+
context_mode: 'isolated',
|
|
221
|
+
next_run: '2025-06-01T00:00:00.000Z',
|
|
222
|
+
status: 'paused',
|
|
223
|
+
created_at: '2024-01-01T00:00:00.000Z',
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('main group can resume any task', async () => {
|
|
228
|
+
await processTaskIpc(
|
|
229
|
+
{ type: 'resume_task', taskId: 'task-paused' },
|
|
230
|
+
'whatsapp_main',
|
|
231
|
+
true,
|
|
232
|
+
deps,
|
|
233
|
+
);
|
|
234
|
+
expect(getTaskById('task-paused')!.status).toBe('active');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('non-main group can resume its own task', async () => {
|
|
238
|
+
await processTaskIpc(
|
|
239
|
+
{ type: 'resume_task', taskId: 'task-paused' },
|
|
240
|
+
'other-group',
|
|
241
|
+
false,
|
|
242
|
+
deps,
|
|
243
|
+
);
|
|
244
|
+
expect(getTaskById('task-paused')!.status).toBe('active');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('non-main group cannot resume another groups task', async () => {
|
|
248
|
+
await processTaskIpc(
|
|
249
|
+
{ type: 'resume_task', taskId: 'task-paused' },
|
|
250
|
+
'third-group',
|
|
251
|
+
false,
|
|
252
|
+
deps,
|
|
253
|
+
);
|
|
254
|
+
expect(getTaskById('task-paused')!.status).toBe('paused');
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// --- cancel_task authorization ---
|
|
259
|
+
|
|
260
|
+
describe('cancel_task authorization', () => {
|
|
261
|
+
it('main group can cancel any task', async () => {
|
|
262
|
+
createTask({
|
|
263
|
+
id: 'task-to-cancel',
|
|
264
|
+
group_folder: 'other-group',
|
|
265
|
+
chat_jid: 'other@g.us',
|
|
266
|
+
prompt: 'cancel me',
|
|
267
|
+
schedule_type: 'once',
|
|
268
|
+
schedule_value: '2025-06-01T00:00:00',
|
|
269
|
+
context_mode: 'isolated',
|
|
270
|
+
next_run: null,
|
|
271
|
+
status: 'active',
|
|
272
|
+
created_at: '2024-01-01T00:00:00.000Z',
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
await processTaskIpc(
|
|
276
|
+
{ type: 'cancel_task', taskId: 'task-to-cancel' },
|
|
277
|
+
'whatsapp_main',
|
|
278
|
+
true,
|
|
279
|
+
deps,
|
|
280
|
+
);
|
|
281
|
+
expect(getTaskById('task-to-cancel')).toBeUndefined();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('non-main group can cancel its own task', async () => {
|
|
285
|
+
createTask({
|
|
286
|
+
id: 'task-own',
|
|
287
|
+
group_folder: 'other-group',
|
|
288
|
+
chat_jid: 'other@g.us',
|
|
289
|
+
prompt: 'my task',
|
|
290
|
+
schedule_type: 'once',
|
|
291
|
+
schedule_value: '2025-06-01T00:00:00',
|
|
292
|
+
context_mode: 'isolated',
|
|
293
|
+
next_run: null,
|
|
294
|
+
status: 'active',
|
|
295
|
+
created_at: '2024-01-01T00:00:00.000Z',
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
await processTaskIpc(
|
|
299
|
+
{ type: 'cancel_task', taskId: 'task-own' },
|
|
300
|
+
'other-group',
|
|
301
|
+
false,
|
|
302
|
+
deps,
|
|
303
|
+
);
|
|
304
|
+
expect(getTaskById('task-own')).toBeUndefined();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('non-main group cannot cancel another groups task', async () => {
|
|
308
|
+
createTask({
|
|
309
|
+
id: 'task-foreign',
|
|
310
|
+
group_folder: 'whatsapp_main',
|
|
311
|
+
chat_jid: 'main@g.us',
|
|
312
|
+
prompt: 'not yours',
|
|
313
|
+
schedule_type: 'once',
|
|
314
|
+
schedule_value: '2025-06-01T00:00:00',
|
|
315
|
+
context_mode: 'isolated',
|
|
316
|
+
next_run: null,
|
|
317
|
+
status: 'active',
|
|
318
|
+
created_at: '2024-01-01T00:00:00.000Z',
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
await processTaskIpc(
|
|
322
|
+
{ type: 'cancel_task', taskId: 'task-foreign' },
|
|
323
|
+
'other-group',
|
|
324
|
+
false,
|
|
325
|
+
deps,
|
|
326
|
+
);
|
|
327
|
+
expect(getTaskById('task-foreign')).toBeDefined();
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// --- register_group authorization ---
|
|
332
|
+
|
|
333
|
+
describe('register_group authorization', () => {
|
|
334
|
+
it('non-main group cannot register a group', async () => {
|
|
335
|
+
await processTaskIpc(
|
|
336
|
+
{
|
|
337
|
+
type: 'register_group',
|
|
338
|
+
jid: 'new@g.us',
|
|
339
|
+
name: 'New Group',
|
|
340
|
+
folder: 'new-group',
|
|
341
|
+
trigger: '@Andy',
|
|
342
|
+
},
|
|
343
|
+
'other-group',
|
|
344
|
+
false,
|
|
345
|
+
deps,
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
// registeredGroups should not have changed
|
|
349
|
+
expect(groups['new@g.us']).toBeUndefined();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('main group cannot register with unsafe folder path', async () => {
|
|
353
|
+
await processTaskIpc(
|
|
354
|
+
{
|
|
355
|
+
type: 'register_group',
|
|
356
|
+
jid: 'new@g.us',
|
|
357
|
+
name: 'New Group',
|
|
358
|
+
folder: '../../outside',
|
|
359
|
+
trigger: '@Andy',
|
|
360
|
+
},
|
|
361
|
+
'whatsapp_main',
|
|
362
|
+
true,
|
|
363
|
+
deps,
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
expect(groups['new@g.us']).toBeUndefined();
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// --- refresh_groups authorization ---
|
|
371
|
+
|
|
372
|
+
describe('refresh_groups authorization', () => {
|
|
373
|
+
it('non-main group cannot trigger refresh', async () => {
|
|
374
|
+
// This should be silently blocked (no crash, no effect)
|
|
375
|
+
await processTaskIpc(
|
|
376
|
+
{ type: 'refresh_groups' },
|
|
377
|
+
'other-group',
|
|
378
|
+
false,
|
|
379
|
+
deps,
|
|
380
|
+
);
|
|
381
|
+
// If we got here without error, the auth gate worked
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// --- IPC message authorization ---
|
|
386
|
+
// Tests the authorization pattern from startIpcWatcher (ipc.ts).
|
|
387
|
+
// The logic: isMain || (targetGroup && targetGroup.folder === sourceGroup)
|
|
388
|
+
|
|
389
|
+
describe('IPC message authorization', () => {
|
|
390
|
+
// Replicate the exact check from the IPC watcher
|
|
391
|
+
function isMessageAuthorized(
|
|
392
|
+
sourceGroup: string,
|
|
393
|
+
isMain: boolean,
|
|
394
|
+
targetChatJid: string,
|
|
395
|
+
registeredGroups: Record<string, RegisteredGroup>,
|
|
396
|
+
): boolean {
|
|
397
|
+
const targetGroup = registeredGroups[targetChatJid];
|
|
398
|
+
return isMain || (!!targetGroup && targetGroup.folder === sourceGroup);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
it('main group can send to any group', () => {
|
|
402
|
+
expect(
|
|
403
|
+
isMessageAuthorized('whatsapp_main', true, 'other@g.us', groups),
|
|
404
|
+
).toBe(true);
|
|
405
|
+
expect(
|
|
406
|
+
isMessageAuthorized('whatsapp_main', true, 'third@g.us', groups),
|
|
407
|
+
).toBe(true);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('non-main group can send to its own chat', () => {
|
|
411
|
+
expect(
|
|
412
|
+
isMessageAuthorized('other-group', false, 'other@g.us', groups),
|
|
413
|
+
).toBe(true);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('non-main group cannot send to another groups chat', () => {
|
|
417
|
+
expect(isMessageAuthorized('other-group', false, 'main@g.us', groups)).toBe(
|
|
418
|
+
false,
|
|
419
|
+
);
|
|
420
|
+
expect(
|
|
421
|
+
isMessageAuthorized('other-group', false, 'third@g.us', groups),
|
|
422
|
+
).toBe(false);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('non-main group cannot send to unregistered JID', () => {
|
|
426
|
+
expect(
|
|
427
|
+
isMessageAuthorized('other-group', false, 'unknown@g.us', groups),
|
|
428
|
+
).toBe(false);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('main group can send to unregistered JID', () => {
|
|
432
|
+
// Main is always authorized regardless of target
|
|
433
|
+
expect(
|
|
434
|
+
isMessageAuthorized('whatsapp_main', true, 'unknown@g.us', groups),
|
|
435
|
+
).toBe(true);
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// --- schedule_task with cron and interval types ---
|
|
440
|
+
|
|
441
|
+
describe('schedule_task schedule types', () => {
|
|
442
|
+
it('creates task with cron schedule and computes next_run', async () => {
|
|
443
|
+
await processTaskIpc(
|
|
444
|
+
{
|
|
445
|
+
type: 'schedule_task',
|
|
446
|
+
prompt: 'cron task',
|
|
447
|
+
schedule_type: 'cron',
|
|
448
|
+
schedule_value: '0 9 * * *', // every day at 9am
|
|
449
|
+
targetJid: 'other@g.us',
|
|
450
|
+
},
|
|
451
|
+
'whatsapp_main',
|
|
452
|
+
true,
|
|
453
|
+
deps,
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
const tasks = getAllTasks();
|
|
457
|
+
expect(tasks).toHaveLength(1);
|
|
458
|
+
expect(tasks[0].schedule_type).toBe('cron');
|
|
459
|
+
expect(tasks[0].next_run).toBeTruthy();
|
|
460
|
+
// next_run should be a valid ISO date in the future
|
|
461
|
+
expect(new Date(tasks[0].next_run!).getTime()).toBeGreaterThan(
|
|
462
|
+
Date.now() - 60000,
|
|
463
|
+
);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('rejects invalid cron expression', async () => {
|
|
467
|
+
await processTaskIpc(
|
|
468
|
+
{
|
|
469
|
+
type: 'schedule_task',
|
|
470
|
+
prompt: 'bad cron',
|
|
471
|
+
schedule_type: 'cron',
|
|
472
|
+
schedule_value: 'not a cron',
|
|
473
|
+
targetJid: 'other@g.us',
|
|
474
|
+
},
|
|
475
|
+
'whatsapp_main',
|
|
476
|
+
true,
|
|
477
|
+
deps,
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
expect(getAllTasks()).toHaveLength(0);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('creates task with interval schedule', async () => {
|
|
484
|
+
const before = Date.now();
|
|
485
|
+
|
|
486
|
+
await processTaskIpc(
|
|
487
|
+
{
|
|
488
|
+
type: 'schedule_task',
|
|
489
|
+
prompt: 'interval task',
|
|
490
|
+
schedule_type: 'interval',
|
|
491
|
+
schedule_value: '3600000', // 1 hour
|
|
492
|
+
targetJid: 'other@g.us',
|
|
493
|
+
},
|
|
494
|
+
'whatsapp_main',
|
|
495
|
+
true,
|
|
496
|
+
deps,
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
const tasks = getAllTasks();
|
|
500
|
+
expect(tasks).toHaveLength(1);
|
|
501
|
+
expect(tasks[0].schedule_type).toBe('interval');
|
|
502
|
+
// next_run should be ~1 hour from now
|
|
503
|
+
const nextRun = new Date(tasks[0].next_run!).getTime();
|
|
504
|
+
expect(nextRun).toBeGreaterThanOrEqual(before + 3600000 - 1000);
|
|
505
|
+
expect(nextRun).toBeLessThanOrEqual(Date.now() + 3600000 + 1000);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('rejects invalid interval (non-numeric)', async () => {
|
|
509
|
+
await processTaskIpc(
|
|
510
|
+
{
|
|
511
|
+
type: 'schedule_task',
|
|
512
|
+
prompt: 'bad interval',
|
|
513
|
+
schedule_type: 'interval',
|
|
514
|
+
schedule_value: 'abc',
|
|
515
|
+
targetJid: 'other@g.us',
|
|
516
|
+
},
|
|
517
|
+
'whatsapp_main',
|
|
518
|
+
true,
|
|
519
|
+
deps,
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
expect(getAllTasks()).toHaveLength(0);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('rejects invalid interval (zero)', async () => {
|
|
526
|
+
await processTaskIpc(
|
|
527
|
+
{
|
|
528
|
+
type: 'schedule_task',
|
|
529
|
+
prompt: 'zero interval',
|
|
530
|
+
schedule_type: 'interval',
|
|
531
|
+
schedule_value: '0',
|
|
532
|
+
targetJid: 'other@g.us',
|
|
533
|
+
},
|
|
534
|
+
'whatsapp_main',
|
|
535
|
+
true,
|
|
536
|
+
deps,
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
expect(getAllTasks()).toHaveLength(0);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('rejects invalid once timestamp', async () => {
|
|
543
|
+
await processTaskIpc(
|
|
544
|
+
{
|
|
545
|
+
type: 'schedule_task',
|
|
546
|
+
prompt: 'bad once',
|
|
547
|
+
schedule_type: 'once',
|
|
548
|
+
schedule_value: 'not-a-date',
|
|
549
|
+
targetJid: 'other@g.us',
|
|
550
|
+
},
|
|
551
|
+
'whatsapp_main',
|
|
552
|
+
true,
|
|
553
|
+
deps,
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
expect(getAllTasks()).toHaveLength(0);
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// --- context_mode defaulting ---
|
|
561
|
+
|
|
562
|
+
describe('schedule_task context_mode', () => {
|
|
563
|
+
it('accepts context_mode=group', async () => {
|
|
564
|
+
await processTaskIpc(
|
|
565
|
+
{
|
|
566
|
+
type: 'schedule_task',
|
|
567
|
+
prompt: 'group context',
|
|
568
|
+
schedule_type: 'once',
|
|
569
|
+
schedule_value: '2025-06-01T00:00:00',
|
|
570
|
+
context_mode: 'group',
|
|
571
|
+
targetJid: 'other@g.us',
|
|
572
|
+
},
|
|
573
|
+
'whatsapp_main',
|
|
574
|
+
true,
|
|
575
|
+
deps,
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
const tasks = getAllTasks();
|
|
579
|
+
expect(tasks[0].context_mode).toBe('group');
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
it('accepts context_mode=isolated', async () => {
|
|
583
|
+
await processTaskIpc(
|
|
584
|
+
{
|
|
585
|
+
type: 'schedule_task',
|
|
586
|
+
prompt: 'isolated context',
|
|
587
|
+
schedule_type: 'once',
|
|
588
|
+
schedule_value: '2025-06-01T00:00:00',
|
|
589
|
+
context_mode: 'isolated',
|
|
590
|
+
targetJid: 'other@g.us',
|
|
591
|
+
},
|
|
592
|
+
'whatsapp_main',
|
|
593
|
+
true,
|
|
594
|
+
deps,
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
const tasks = getAllTasks();
|
|
598
|
+
expect(tasks[0].context_mode).toBe('isolated');
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it('defaults invalid context_mode to isolated', async () => {
|
|
602
|
+
await processTaskIpc(
|
|
603
|
+
{
|
|
604
|
+
type: 'schedule_task',
|
|
605
|
+
prompt: 'bad context',
|
|
606
|
+
schedule_type: 'once',
|
|
607
|
+
schedule_value: '2025-06-01T00:00:00',
|
|
608
|
+
context_mode: 'bogus' as any,
|
|
609
|
+
targetJid: 'other@g.us',
|
|
610
|
+
},
|
|
611
|
+
'whatsapp_main',
|
|
612
|
+
true,
|
|
613
|
+
deps,
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
const tasks = getAllTasks();
|
|
617
|
+
expect(tasks[0].context_mode).toBe('isolated');
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it('defaults missing context_mode to isolated', async () => {
|
|
621
|
+
await processTaskIpc(
|
|
622
|
+
{
|
|
623
|
+
type: 'schedule_task',
|
|
624
|
+
prompt: 'no context mode',
|
|
625
|
+
schedule_type: 'once',
|
|
626
|
+
schedule_value: '2025-06-01T00:00:00',
|
|
627
|
+
targetJid: 'other@g.us',
|
|
628
|
+
},
|
|
629
|
+
'whatsapp_main',
|
|
630
|
+
true,
|
|
631
|
+
deps,
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
const tasks = getAllTasks();
|
|
635
|
+
expect(tasks[0].context_mode).toBe('isolated');
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
// --- register_group success path ---
|
|
640
|
+
|
|
641
|
+
describe('register_group success', () => {
|
|
642
|
+
it('main group can register a new group', async () => {
|
|
643
|
+
await processTaskIpc(
|
|
644
|
+
{
|
|
645
|
+
type: 'register_group',
|
|
646
|
+
jid: 'new@g.us',
|
|
647
|
+
name: 'New Group',
|
|
648
|
+
folder: 'new-group',
|
|
649
|
+
trigger: '@Andy',
|
|
650
|
+
},
|
|
651
|
+
'whatsapp_main',
|
|
652
|
+
true,
|
|
653
|
+
deps,
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
// Verify group was registered in DB
|
|
657
|
+
const group = getRegisteredGroup('new@g.us');
|
|
658
|
+
expect(group).toBeDefined();
|
|
659
|
+
expect(group!.name).toBe('New Group');
|
|
660
|
+
expect(group!.folder).toBe('new-group');
|
|
661
|
+
expect(group!.trigger).toBe('@Andy');
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it('register_group rejects request with missing fields', async () => {
|
|
665
|
+
await processTaskIpc(
|
|
666
|
+
{
|
|
667
|
+
type: 'register_group',
|
|
668
|
+
jid: 'partial@g.us',
|
|
669
|
+
name: 'Partial',
|
|
670
|
+
// missing folder and trigger
|
|
671
|
+
},
|
|
672
|
+
'whatsapp_main',
|
|
673
|
+
true,
|
|
674
|
+
deps,
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
expect(getRegisteredGroup('partial@g.us')).toBeUndefined();
|
|
678
|
+
});
|
|
679
|
+
});
|