@meetquinn/animactl 0.1.2 → 0.1.3
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 +11 -6
- package/dist/server/{runtime/activity-payload.d.ts → activities/format.d.ts} +4 -1
- package/dist/server/activities/format.d.ts.map +1 -0
- package/dist/server/{runtime/activity-payload.js → activities/format.js} +16 -1
- package/dist/server/agents/agent-skills.d.ts +7 -3
- package/dist/server/agents/agent-skills.d.ts.map +1 -1
- package/dist/server/agents/agent-skills.js +243 -38
- package/dist/server/agents/agent-slack.service.d.ts +15 -0
- package/dist/server/agents/agent-slack.service.d.ts.map +1 -1
- package/dist/server/agents/agent-slack.service.js +43 -6
- package/dist/server/agents/default-skills.d.ts +9 -0
- package/dist/server/agents/default-skills.d.ts.map +1 -0
- package/dist/server/agents/default-skills.js +52 -0
- package/dist/server/cli/runtime-cli.d.ts +7 -1
- package/dist/server/cli/runtime-cli.d.ts.map +1 -1
- package/dist/server/cli/runtime-cli.js +107 -3
- package/dist/server/cli/services-cli.d.ts.map +1 -1
- package/dist/server/cli/services-cli.js +39 -0
- package/dist/server/inbox/slack-subscriber.d.ts +2 -0
- package/dist/server/inbox/slack-subscriber.d.ts.map +1 -1
- package/dist/server/inbox/slack-subscriber.js +28 -2
- package/dist/server/inbox/slack-subscription.service.d.ts.map +1 -1
- package/dist/server/inbox/slack-subscription.service.js +13 -1
- package/dist/server/inbox/wake-queue.service.d.ts +3 -1
- package/dist/server/inbox/wake-queue.service.d.ts.map +1 -1
- package/dist/server/inbox/wake-queue.service.js +5 -3
- package/dist/server/provider-usage/http.d.ts +1 -0
- package/dist/server/provider-usage/http.d.ts.map +1 -1
- package/dist/server/provider-usage/http.js +23 -1
- package/dist/server/provider-usage/provider-usage.service.d.ts +1 -4
- package/dist/server/provider-usage/provider-usage.service.d.ts.map +1 -1
- package/dist/server/provider-usage/provider-usage.service.js +1 -18
- package/dist/server/provider-usage/providers/claude.d.ts +1 -1
- package/dist/server/provider-usage/providers/claude.d.ts.map +1 -1
- package/dist/server/provider-usage/providers/claude.js +1 -1
- package/dist/server/provider-usage/providers/codex.d.ts +1 -1
- package/dist/server/provider-usage/providers/codex.d.ts.map +1 -1
- package/dist/server/provider-usage/providers/codex.js +1 -1
- package/dist/server/provider-usage/providers/kimi.d.ts +1 -1
- package/dist/server/provider-usage/providers/kimi.d.ts.map +1 -1
- package/dist/server/provider-usage/providers/kimi.js +4 -4
- package/dist/server/provider-usage/result.d.ts +5 -0
- package/dist/server/provider-usage/result.d.ts.map +1 -0
- package/dist/server/provider-usage/result.js +18 -0
- package/dist/server/providers/active-runtime.d.ts +1 -1
- package/dist/server/providers/active-runtime.d.ts.map +1 -1
- package/dist/server/providers/claude-events.d.ts +1 -1
- package/dist/server/providers/claude-events.d.ts.map +1 -1
- package/dist/server/providers/claude-events.js +2 -2
- package/dist/server/providers/claude.d.ts +1 -1
- package/dist/server/providers/claude.d.ts.map +1 -1
- package/dist/server/providers/claude.js +2 -2
- package/dist/server/providers/codex-app-server.d.ts +1 -1
- package/dist/server/providers/codex-app-server.d.ts.map +1 -1
- package/dist/server/providers/codex-app-server.js +1 -1
- package/dist/server/providers/codex-events.js +1 -1
- package/dist/server/providers/codex.d.ts +1 -1
- package/dist/server/providers/codex.d.ts.map +1 -1
- package/dist/server/providers/codex.js +2 -2
- package/dist/server/{runtime/provider-contract.d.ts → providers/contract.d.ts} +1 -1
- package/dist/server/providers/contract.d.ts.map +1 -0
- package/dist/server/providers/factory.d.ts +1 -1
- package/dist/server/providers/factory.d.ts.map +1 -1
- package/dist/server/providers/kimi-events.d.ts +1 -1
- package/dist/server/providers/kimi-events.d.ts.map +1 -1
- package/dist/server/providers/kimi-events.js +1 -1
- package/dist/server/providers/kimi.d.ts +1 -1
- package/dist/server/providers/kimi.d.ts.map +1 -1
- package/dist/server/providers/kimi.js +3 -2
- package/dist/server/providers/reasoning-events.js +1 -1
- package/dist/server/runtime/active-run-control.d.ts +19 -0
- package/dist/server/runtime/active-run-control.d.ts.map +1 -0
- package/dist/server/runtime/active-run-control.js +89 -0
- package/dist/server/runtime/activity.js +1 -1
- package/dist/server/runtime/agent-runner.d.ts +1 -1
- package/dist/server/runtime/agent-runner.d.ts.map +1 -1
- package/dist/server/runtime/delivery-prompt.d.ts +6 -0
- package/dist/server/runtime/delivery-prompt.d.ts.map +1 -1
- package/dist/server/runtime/delivery-prompt.js +53 -34
- package/dist/server/runtime/followup-appender.d.ts +20 -0
- package/dist/server/runtime/followup-appender.d.ts.map +1 -0
- package/dist/server/runtime/followup-appender.js +128 -0
- package/dist/server/runtime/host.d.ts +2 -0
- package/dist/server/runtime/host.d.ts.map +1 -1
- package/dist/server/runtime/host.js +8 -0
- package/dist/server/runtime/provider-runner.d.ts +16 -0
- package/dist/server/runtime/provider-runner.d.ts.map +1 -0
- package/dist/server/runtime/provider-runner.js +86 -0
- package/dist/server/runtime/runtime-bridge.d.ts +1 -1
- package/dist/server/runtime/runtime-bridge.d.ts.map +1 -1
- package/dist/server/runtime/runtime-session.service.d.ts +1 -1
- package/dist/server/runtime/runtime-session.service.d.ts.map +1 -1
- package/dist/server/runtime/runtime-session.service.js +1 -1
- package/dist/server/runtime/runtime-worker.d.ts +2 -10
- package/dist/server/runtime/runtime-worker.d.ts.map +1 -1
- package/dist/server/runtime/runtime-worker.js +60 -286
- package/dist/server/runtime-management/managed-runtime.d.ts.map +1 -0
- package/dist/server/runtime-management/runtime-release.d.ts.map +1 -0
- package/dist/server/{runtime → runtime-management}/runtime-upgrade.d.ts +1 -0
- package/dist/server/runtime-management/runtime-upgrade.d.ts.map +1 -0
- package/dist/server/{runtime → runtime-management}/runtime-upgrade.js +14 -0
- package/dist/server/services/system.service.d.ts +41 -0
- package/dist/server/services/system.service.d.ts.map +1 -0
- package/dist/server/services/system.service.js +154 -0
- package/dist/server/settings/settings.service.d.ts +2 -1
- package/dist/server/settings/settings.service.d.ts.map +1 -1
- package/dist/server/settings/settings.service.js +9 -1
- package/dist/server/slack/shortcuts.d.ts +0 -3
- package/dist/server/slack/shortcuts.d.ts.map +1 -1
- package/dist/server/slack/shortcuts.js +0 -3
- package/dist/server/slack-interactions/shortcut-ids.d.ts +4 -0
- package/dist/server/slack-interactions/shortcut-ids.d.ts.map +1 -0
- package/dist/server/slack-interactions/shortcut-ids.js +3 -0
- package/dist/server/slack-interactions/shortcut-views.d.ts +65 -0
- package/dist/server/slack-interactions/shortcut-views.d.ts.map +1 -0
- package/dist/server/slack-interactions/shortcut-views.js +211 -0
- package/dist/server/{slack → slack-interactions}/shortcut.service.d.ts +2 -47
- package/dist/server/slack-interactions/shortcut.service.d.ts.map +1 -0
- package/dist/server/slack-interactions/shortcut.service.js +227 -0
- package/dist/server/storage/jsonl-log.d.ts +9 -0
- package/dist/server/storage/jsonl-log.d.ts.map +1 -1
- package/dist/server/storage/jsonl-log.js +36 -1
- package/dist/server/storage/schema/server.store.d.ts +5 -0
- package/dist/server/storage/schema/server.store.d.ts.map +1 -1
- package/dist/server/storage/schema/server.store.js +2 -1
- package/dist/server/storage/schema/wake-queue.store.d.ts +15 -1
- package/dist/server/storage/schema/wake-queue.store.d.ts.map +1 -1
- package/dist/server/storage/schema/wake-queue.store.js +5 -3
- package/dist/server/tests/agent-config.test.d.ts +2 -0
- package/dist/server/tests/agent-config.test.d.ts.map +1 -0
- package/dist/server/tests/agent-config.test.js +229 -0
- package/dist/server/tests/agent-runtime.test.d.ts +2 -0
- package/dist/server/tests/agent-runtime.test.d.ts.map +1 -0
- package/dist/server/tests/agent-runtime.test.js +1110 -0
- package/dist/server/tests/agent-seed-memory.test.d.ts +2 -0
- package/dist/server/tests/agent-seed-memory.test.d.ts.map +1 -0
- package/dist/server/tests/agent-seed-memory.test.js +16 -0
- package/dist/server/tests/agent-skills.test.d.ts +2 -0
- package/dist/server/tests/agent-skills.test.d.ts.map +1 -0
- package/dist/server/tests/agent-skills.test.js +140 -0
- package/dist/server/tests/anima-home.d.ts +2 -0
- package/dist/server/tests/anima-home.d.ts.map +1 -0
- package/dist/server/tests/anima-home.js +4 -0
- package/dist/server/tests/cli-file.test.d.ts +2 -0
- package/dist/server/tests/cli-file.test.d.ts.map +1 -0
- package/dist/server/tests/cli-file.test.js +596 -0
- package/dist/server/tests/cli-message.test.d.ts +2 -0
- package/dist/server/tests/cli-message.test.d.ts.map +1 -0
- package/dist/server/tests/cli-message.test.js +1196 -0
- package/dist/server/tests/client-error-routes.test.d.ts +2 -0
- package/dist/server/tests/client-error-routes.test.d.ts.map +1 -0
- package/dist/server/tests/client-error-routes.test.js +163 -0
- package/dist/server/tests/config.test.d.ts +2 -0
- package/dist/server/tests/config.test.d.ts.map +1 -0
- package/dist/server/tests/config.test.js +497 -0
- package/dist/server/tests/default-skills.test.d.ts +2 -0
- package/dist/server/tests/default-skills.test.d.ts.map +1 -0
- package/dist/server/tests/default-skills.test.js +57 -0
- package/dist/server/tests/helpers/inbox.d.ts +10 -0
- package/dist/server/tests/helpers/inbox.d.ts.map +1 -0
- package/dist/server/tests/helpers/inbox.js +21 -0
- package/dist/server/tests/helpers/slack.d.ts +11 -0
- package/dist/server/tests/helpers/slack.d.ts.map +1 -0
- package/dist/server/tests/helpers/slack.js +17 -0
- package/dist/server/tests/helpers/state.d.ts +19 -0
- package/dist/server/tests/helpers/state.d.ts.map +1 -0
- package/dist/server/tests/helpers/state.js +69 -0
- package/dist/server/tests/inbox-slack-events.test.d.ts +2 -0
- package/dist/server/tests/inbox-slack-events.test.d.ts.map +1 -0
- package/dist/server/tests/inbox-slack-events.test.js +271 -0
- package/dist/server/tests/inbox.test.d.ts +2 -0
- package/dist/server/tests/inbox.test.d.ts.map +1 -0
- package/dist/server/tests/inbox.test.js +144 -0
- package/dist/server/tests/interactive-ask.test.d.ts +2 -0
- package/dist/server/tests/interactive-ask.test.d.ts.map +1 -0
- package/dist/server/tests/interactive-ask.test.js +387 -0
- package/dist/server/tests/kb.test.d.ts +2 -0
- package/dist/server/tests/kb.test.d.ts.map +1 -0
- package/dist/server/tests/kb.test.js +371 -0
- package/dist/server/tests/message.service.test.d.ts +2 -0
- package/dist/server/tests/message.service.test.d.ts.map +1 -0
- package/dist/server/tests/message.service.test.js +151 -0
- package/dist/server/tests/messages.test.d.ts +2 -0
- package/dist/server/tests/messages.test.d.ts.map +1 -0
- package/dist/server/tests/messages.test.js +12 -0
- package/dist/server/tests/prompt-attachments.test.d.ts +2 -0
- package/dist/server/tests/prompt-attachments.test.d.ts.map +1 -0
- package/dist/server/tests/prompt-attachments.test.js +274 -0
- package/dist/server/tests/provider-usage.test.d.ts +2 -0
- package/dist/server/tests/provider-usage.test.d.ts.map +1 -0
- package/dist/server/tests/provider-usage.test.js +97 -0
- package/dist/server/tests/reminders.test.d.ts +2 -0
- package/dist/server/tests/reminders.test.d.ts.map +1 -0
- package/dist/server/tests/reminders.test.js +383 -0
- package/dist/server/tests/runtime-cli.test.d.ts +2 -0
- package/dist/server/tests/runtime-cli.test.d.ts.map +1 -0
- package/dist/server/tests/runtime-cli.test.js +21 -0
- package/dist/server/tests/runtime-upgrade.test.d.ts +2 -0
- package/dist/server/tests/runtime-upgrade.test.d.ts.map +1 -0
- package/dist/server/tests/runtime-upgrade.test.js +307 -0
- package/dist/server/tests/runtime-worker.test.d.ts +2 -0
- package/dist/server/tests/runtime-worker.test.d.ts.map +1 -0
- package/dist/server/tests/runtime-worker.test.js +1021 -0
- package/dist/server/tests/runtime.test.d.ts +2 -0
- package/dist/server/tests/runtime.test.d.ts.map +1 -0
- package/dist/server/tests/runtime.test.js +354 -0
- package/dist/server/tests/services.test.d.ts +2 -0
- package/dist/server/tests/services.test.d.ts.map +1 -0
- package/dist/server/tests/services.test.js +636 -0
- package/dist/server/tests/slack-files.test.d.ts +2 -0
- package/dist/server/tests/slack-files.test.d.ts.map +1 -0
- package/dist/server/tests/slack-files.test.js +118 -0
- package/dist/server/tests/slack-shortcuts.test.d.ts +2 -0
- package/dist/server/tests/slack-shortcuts.test.d.ts.map +1 -0
- package/dist/server/tests/slack-shortcuts.test.js +310 -0
- package/dist/server/tests/slack.test.d.ts +2 -0
- package/dist/server/tests/slack.test.d.ts.map +1 -0
- package/dist/server/tests/slack.test.js +213 -0
- package/dist/server/tests/state-cache.test.d.ts +2 -0
- package/dist/server/tests/state-cache.test.d.ts.map +1 -0
- package/dist/server/tests/state-cache.test.js +184 -0
- package/dist/server/tests/subscriptions.test.d.ts +2 -0
- package/dist/server/tests/subscriptions.test.d.ts.map +1 -0
- package/dist/server/tests/subscriptions.test.js +292 -0
- package/dist/server/tests/system-service.test.d.ts +2 -0
- package/dist/server/tests/system-service.test.d.ts.map +1 -0
- package/dist/server/tests/system-service.test.js +20 -0
- package/dist/server/tests/url-routes.test.d.ts +2 -0
- package/dist/server/tests/url-routes.test.d.ts.map +1 -0
- package/dist/server/tests/url-routes.test.js +217 -0
- package/dist/server/tests/web-api.test.d.ts +2 -0
- package/dist/server/tests/web-api.test.d.ts.map +1 -0
- package/dist/server/tests/web-api.test.js +1759 -0
- package/dist/server/web/app.d.ts.map +1 -1
- package/dist/server/web/app.js +2 -0
- package/dist/server/web/client-error-routes.d.ts +3 -0
- package/dist/server/web/client-error-routes.d.ts.map +1 -0
- package/dist/server/web/client-error-routes.js +101 -0
- package/dist/server/web/system-routes.d.ts.map +1 -1
- package/dist/server/web/system-routes.js +21 -131
- package/dist/shared/agent-config.d.ts +8 -18
- package/dist/shared/agent-config.d.ts.map +1 -1
- package/dist/shared/agent-config.js +3 -4
- package/dist/shared/inbox.d.ts +27 -0
- package/dist/shared/inbox.d.ts.map +1 -1
- package/dist/shared/inbox.js +1 -0
- package/dist/shared/server-info.d.ts +6 -5
- package/dist/shared/server-info.d.ts.map +1 -1
- package/dist/shared/server-info.js +3 -1
- package/dist/shared/server-settings.d.ts +6 -0
- package/dist/shared/server-settings.d.ts.map +1 -1
- package/dist/shared/server-settings.js +1 -0
- package/dist/shared/skills.d.ts +24 -3
- package/dist/shared/skills.d.ts.map +1 -1
- package/dist/web/assets/activity-DHjip8tY.js +7 -0
- package/dist/web/assets/clipboard-D2NSC52v.js +1 -0
- package/dist/web/assets/{external-link-Dt0qTpmV.js → external-link-Bhr1WdRS.js} +1 -1
- package/dist/web/assets/{file-text-Dd9ys6py.js → file-text-CYMA_SMA.js} +1 -1
- package/dist/web/assets/{index-CzAQnbuO.js → index-CVpew409.js} +5 -5
- package/dist/web/assets/index-mh0uJTkY.css +2 -0
- package/dist/web/assets/kb-DDVpYkT9.js +35 -0
- package/dist/web/assets/profile-CBy6GNzp.js +1 -0
- package/dist/web/assets/reminders-Bbwb9QNT.js +1 -0
- package/dist/web/favicon.svg +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/templates/runtime-standing-prompt.md +16 -2
- package/templates/skills/find-skills/SKILL.md +133 -0
- package/dist/server/runtime/activity-payload.d.ts.map +0 -1
- package/dist/server/runtime/activity-text.d.ts +0 -4
- package/dist/server/runtime/activity-text.d.ts.map +0 -1
- package/dist/server/runtime/activity-text.js +0 -16
- package/dist/server/runtime/managed-runtime.d.ts.map +0 -1
- package/dist/server/runtime/provider-contract.d.ts.map +0 -1
- package/dist/server/runtime/runtime-release.d.ts.map +0 -1
- package/dist/server/runtime/runtime-upgrade.d.ts.map +0 -1
- package/dist/server/slack/shortcut.service.d.ts.map +0 -1
- package/dist/server/slack/shortcut.service.js +0 -493
- package/dist/web/assets/activity-BP0C81IV.js +0 -7
- package/dist/web/assets/globe-DmDZSgDP.js +0 -1
- package/dist/web/assets/index-DKecrvN0.css +0 -2
- package/dist/web/assets/kb-3wFeP2Jt.js +0 -32
- package/dist/web/assets/profile-D7wPZXW_.js +0 -1
- package/dist/web/assets/reminders-DOhroVwo.js +0 -1
- /package/dist/server/{runtime/provider-contract.js → providers/contract.js} +0 -0
- /package/dist/server/{runtime → runtime-management}/managed-runtime.d.ts +0 -0
- /package/dist/server/{runtime → runtime-management}/managed-runtime.js +0 -0
- /package/dist/server/{runtime → runtime-management}/runtime-release.d.ts +0 -0
- /package/dist/server/{runtime → runtime-management}/runtime-release.js +0 -0
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { once } from 'node:events';
|
|
3
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { createServer } from 'node:http';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
7
|
+
import test from 'node:test';
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
import { allActivities, loadState } from './helpers/state.js';
|
|
10
|
+
import { activitiesForInboxItemWindow } from '../runtime/item-activities.js';
|
|
11
|
+
import { makeSlackEvent } from './helpers/slack.js';
|
|
12
|
+
import { ingestEvent } from './helpers/inbox.js';
|
|
13
|
+
import { withAnimaHome } from './anima-home.js';
|
|
14
|
+
const cliPath = resolve('dist/server/cli/anima.js');
|
|
15
|
+
test('file send uploads a local file and records an audited Slack output', async () => {
|
|
16
|
+
const stateDir = await mkdtemp(join(tmpdir(), 'anima-cli-file-send-test-'));
|
|
17
|
+
const uploadUrls = [];
|
|
18
|
+
const completeCalls = [];
|
|
19
|
+
const uploadServerPosts = [];
|
|
20
|
+
const uploadServer = createServer((request, response) => {
|
|
21
|
+
void readBody(request).then((body) => {
|
|
22
|
+
uploadServerPosts.push(body);
|
|
23
|
+
response.writeHead(200, { 'content-type': 'application/json' });
|
|
24
|
+
response.end(JSON.stringify({ ok: true }));
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
uploadServer.listen(0, '127.0.0.1');
|
|
28
|
+
await once(uploadServer, 'listening');
|
|
29
|
+
const uploadAddr = uploadServer.address();
|
|
30
|
+
if (!uploadAddr || typeof uploadAddr === 'string')
|
|
31
|
+
throw new Error('upload server not bound');
|
|
32
|
+
let getUploadCount = 0;
|
|
33
|
+
const slackApi = await startSlackApiMock((method, body) => {
|
|
34
|
+
if (method === 'conversations.info') {
|
|
35
|
+
return {
|
|
36
|
+
channel: { id: 'C-product', is_channel: true, name: 'product', name_normalized: 'product' },
|
|
37
|
+
ok: true,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (method === 'files.getUploadURLExternal') {
|
|
41
|
+
getUploadCount += 1;
|
|
42
|
+
const uploadUrl = `http://127.0.0.1:${uploadAddr.port}/upload/${getUploadCount}`;
|
|
43
|
+
uploadUrls.push(uploadUrl);
|
|
44
|
+
return { ok: true, file_id: `F-test-${getUploadCount}`, upload_url: uploadUrl };
|
|
45
|
+
}
|
|
46
|
+
if (method === 'files.completeUploadExternal') {
|
|
47
|
+
completeCalls.push(slackRequestBody(body));
|
|
48
|
+
return {
|
|
49
|
+
ok: true,
|
|
50
|
+
files: [{ id: 'F-test-1', title: 'screenshot.png' }],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (method === 'files.info') {
|
|
54
|
+
return {
|
|
55
|
+
ok: true,
|
|
56
|
+
file: {
|
|
57
|
+
id: 'F-test-1',
|
|
58
|
+
mimetype: 'image/png',
|
|
59
|
+
name: 'screenshot.png',
|
|
60
|
+
permalink: 'https://anima.slack.com/files/U-scout/F-test-1/screenshot.png',
|
|
61
|
+
size: 12,
|
|
62
|
+
thumb_360: 'https://files.slack.com/secure/F-test-1/thumb_360.png',
|
|
63
|
+
thumb_720: 'https://files.slack.com/secure/F-test-1/thumb_720.png',
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
throw new Error(`unexpected method ${method}`);
|
|
68
|
+
});
|
|
69
|
+
try {
|
|
70
|
+
await withAnimaHome(stateDir, async () => {
|
|
71
|
+
await writeSlackConfig(stateDir);
|
|
72
|
+
const itemId = await ingestSlackThread(stateDir);
|
|
73
|
+
const localPath = join(stateDir, 'screenshot.png');
|
|
74
|
+
await writeFile(localPath, Buffer.from('fake-png-bytes'));
|
|
75
|
+
const send = await runNode([cliPath, 'file', 'send', '--channel', 'C-product', localPath], {
|
|
76
|
+
env: {
|
|
77
|
+
...process.env,
|
|
78
|
+
ANIMA_AGENT_ID: 'scout',
|
|
79
|
+
ANIMA_HOME: stateDir,
|
|
80
|
+
ANIMA_INBOX_ITEM_ID: itemId,
|
|
81
|
+
ANIMA_SLACK_API_URL: slackApi.url,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
assert.equal(send.status, 0, send.stderr || send.stdout);
|
|
85
|
+
assert.match(send.stdout, /uploaded successfully\. channel=#product, files=1\./);
|
|
86
|
+
// Slack upload POST happened
|
|
87
|
+
assert.equal(uploadUrls.length, 1);
|
|
88
|
+
assert.equal(uploadServerPosts.length, 1);
|
|
89
|
+
// completeUploadExternal got the single file id and channel
|
|
90
|
+
assert.equal(completeCalls.length, 1);
|
|
91
|
+
const complete = completeCalls[0];
|
|
92
|
+
assert.equal(complete['channel_id'], 'C-product');
|
|
93
|
+
const completeFiles = parseFilesField(complete['files']);
|
|
94
|
+
assert.equal(completeFiles.length, 1);
|
|
95
|
+
assert.equal(completeFiles[0]?.id, 'F-test-1');
|
|
96
|
+
// Audit completed activity
|
|
97
|
+
const activities = await activitiesForInboxItemWindow('scout', itemId);
|
|
98
|
+
const completed = activities.at(-1);
|
|
99
|
+
assert.equal(completed?.type, 'external.effect.completed');
|
|
100
|
+
assert.equal(completed?.payload?.['effect'], 'slack.file.send');
|
|
101
|
+
assert.equal(completed?.payload?.['tool'], 'anima.file.send');
|
|
102
|
+
assert.equal(completed?.payload?.['status'], 'sent');
|
|
103
|
+
assert.equal(completed?.payload?.['fileCount'], 1);
|
|
104
|
+
assert.equal(completed?.payload?.['channelDisplayName'], '#product');
|
|
105
|
+
const uploads = completed?.payload?.['uploads'];
|
|
106
|
+
assert.equal(uploads.length, 1);
|
|
107
|
+
assert.equal(uploads[0]?.['fileId'], 'F-test-1');
|
|
108
|
+
assert.equal(uploads[0]?.['filename'], 'screenshot.png');
|
|
109
|
+
assert.equal(uploads[0]?.['mimetype'], 'image/png');
|
|
110
|
+
assert.equal(uploads[0]?.['permalink'], 'https://anima.slack.com/files/U-scout/F-test-1/screenshot.png');
|
|
111
|
+
assert.equal(uploads[0]?.['thumb360'], 'https://files.slack.com/secure/F-test-1/thumb_360.png');
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
await slackApi.close();
|
|
116
|
+
uploadServer.close();
|
|
117
|
+
await once(uploadServer, 'close');
|
|
118
|
+
await rm(stateDir, { force: true, recursive: true });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
test('file send supports multi-file batch with caption and thread', async () => {
|
|
122
|
+
const stateDir = await mkdtemp(join(tmpdir(), 'anima-cli-file-batch-test-'));
|
|
123
|
+
const completeCalls = [];
|
|
124
|
+
const uploadServer = createServer((_request, response) => {
|
|
125
|
+
response.writeHead(200);
|
|
126
|
+
response.end();
|
|
127
|
+
});
|
|
128
|
+
uploadServer.listen(0, '127.0.0.1');
|
|
129
|
+
await once(uploadServer, 'listening');
|
|
130
|
+
const uploadAddr = uploadServer.address();
|
|
131
|
+
if (!uploadAddr || typeof uploadAddr === 'string')
|
|
132
|
+
throw new Error('upload server not bound');
|
|
133
|
+
let getUploadCount = 0;
|
|
134
|
+
const slackApi = await startSlackApiMock((method, body) => {
|
|
135
|
+
if (method === 'conversations.info') {
|
|
136
|
+
return { channel: { id: 'C-product', is_channel: true, name: 'product', name_normalized: 'product' }, ok: true };
|
|
137
|
+
}
|
|
138
|
+
if (method === 'files.getUploadURLExternal') {
|
|
139
|
+
getUploadCount += 1;
|
|
140
|
+
return {
|
|
141
|
+
ok: true,
|
|
142
|
+
file_id: `F-batch-${getUploadCount}`,
|
|
143
|
+
upload_url: `http://127.0.0.1:${uploadAddr.port}/upload/${getUploadCount}`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (method === 'files.completeUploadExternal') {
|
|
147
|
+
completeCalls.push(slackRequestBody(body));
|
|
148
|
+
return { ok: true, files: [{ id: 'F-batch-1' }, { id: 'F-batch-2' }] };
|
|
149
|
+
}
|
|
150
|
+
if (method === 'files.info') {
|
|
151
|
+
return { ok: true, file: { id: 'F-batch-1', mimetype: 'image/png', size: 8, permalink: 'https://anima.slack.com/files/F-batch-1' } };
|
|
152
|
+
}
|
|
153
|
+
throw new Error(`unexpected method ${method}`);
|
|
154
|
+
});
|
|
155
|
+
try {
|
|
156
|
+
await withAnimaHome(stateDir, async () => {
|
|
157
|
+
await writeSlackConfig(stateDir);
|
|
158
|
+
const itemId = await ingestSlackThread(stateDir);
|
|
159
|
+
const a = join(stateDir, 'a.png');
|
|
160
|
+
const b = join(stateDir, 'b.txt');
|
|
161
|
+
await writeFile(a, Buffer.from('aaaa'));
|
|
162
|
+
await writeFile(b, Buffer.from('bbbb'));
|
|
163
|
+
const send = await runNode([cliPath, 'file', 'send', '--channel', 'C-product', '--thread-ts', '1770000200.000001', '--caption', 'see attached', a, b], {
|
|
164
|
+
env: {
|
|
165
|
+
...process.env,
|
|
166
|
+
ANIMA_AGENT_ID: 'scout',
|
|
167
|
+
ANIMA_HOME: stateDir,
|
|
168
|
+
ANIMA_INBOX_ITEM_ID: itemId,
|
|
169
|
+
ANIMA_SLACK_API_URL: slackApi.url,
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
assert.equal(send.status, 0, send.stderr || send.stdout);
|
|
173
|
+
assert.match(send.stdout, /uploaded successfully\. channel=#product, thread_ts=1770000200\.000001, files=2\./);
|
|
174
|
+
assert.equal(completeCalls.length, 1);
|
|
175
|
+
const complete = completeCalls[0];
|
|
176
|
+
assert.equal(complete['initial_comment'], 'see attached');
|
|
177
|
+
assert.equal(complete['thread_ts'], '1770000200.000001');
|
|
178
|
+
const files = parseFilesField(complete['files']);
|
|
179
|
+
assert.equal(files.length, 2);
|
|
180
|
+
const completed = (await activitiesForInboxItemWindow('scout', itemId)).at(-1);
|
|
181
|
+
assert.equal(completed?.payload?.['fileCount'], 2);
|
|
182
|
+
assert.equal(completed?.payload?.['caption'], 'see attached');
|
|
183
|
+
assert.equal(completed?.payload?.['threadTs'], '1770000200.000001');
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
await slackApi.close();
|
|
188
|
+
uploadServer.close();
|
|
189
|
+
await once(uploadServer, 'close');
|
|
190
|
+
await rm(stateDir, { force: true, recursive: true });
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
test('file send accepts caption from stdin when --caption is not passed', async () => {
|
|
194
|
+
const stateDir = await mkdtemp(join(tmpdir(), 'anima-cli-file-stdin-caption-test-'));
|
|
195
|
+
const completeCalls = [];
|
|
196
|
+
const uploadServer = createServer((_request, response) => {
|
|
197
|
+
response.writeHead(200);
|
|
198
|
+
response.end();
|
|
199
|
+
});
|
|
200
|
+
uploadServer.listen(0, '127.0.0.1');
|
|
201
|
+
await once(uploadServer, 'listening');
|
|
202
|
+
const uploadAddr = uploadServer.address();
|
|
203
|
+
if (!uploadAddr || typeof uploadAddr === 'string')
|
|
204
|
+
throw new Error('upload server not bound');
|
|
205
|
+
let getUploadCount = 0;
|
|
206
|
+
const slackApi = await startSlackApiMock((method, body) => {
|
|
207
|
+
if (method === 'conversations.info') {
|
|
208
|
+
return { channel: { id: 'C-product', is_channel: true, name: 'product', name_normalized: 'product' }, ok: true };
|
|
209
|
+
}
|
|
210
|
+
if (method === 'files.getUploadURLExternal') {
|
|
211
|
+
getUploadCount += 1;
|
|
212
|
+
return {
|
|
213
|
+
ok: true,
|
|
214
|
+
file_id: `F-stdin-${getUploadCount}`,
|
|
215
|
+
upload_url: `http://127.0.0.1:${uploadAddr.port}/upload/${getUploadCount}`,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (method === 'files.completeUploadExternal') {
|
|
219
|
+
completeCalls.push(slackRequestBody(body));
|
|
220
|
+
return { ok: true, files: [{ id: 'F-stdin-1' }] };
|
|
221
|
+
}
|
|
222
|
+
if (method === 'files.info') {
|
|
223
|
+
return { ok: true, file: { id: 'F-stdin-1', mimetype: 'image/png', size: 4, permalink: 'https://anima.slack.com/files/F-stdin-1' } };
|
|
224
|
+
}
|
|
225
|
+
throw new Error(`unexpected method ${method}`);
|
|
226
|
+
});
|
|
227
|
+
// A caption that would be ruined by `bash -c "--caption \`anima file send\` ..."`:
|
|
228
|
+
// backticks, $(), single + double quotes all live happily inside a heredoc.
|
|
229
|
+
const captionWithShellMetachars = "uploaded via `anima file send` (cost: $(price) USD) — \"shipped\"";
|
|
230
|
+
try {
|
|
231
|
+
await withAnimaHome(stateDir, async () => {
|
|
232
|
+
await writeSlackConfig(stateDir);
|
|
233
|
+
const itemId = await ingestSlackThread(stateDir);
|
|
234
|
+
const localPath = join(stateDir, 'evidence.png');
|
|
235
|
+
await writeFile(localPath, Buffer.from('img!'));
|
|
236
|
+
const send = await runNode([cliPath, 'file', 'send', '--channel', 'C-product', localPath], {
|
|
237
|
+
env: {
|
|
238
|
+
...process.env,
|
|
239
|
+
ANIMA_AGENT_ID: 'scout',
|
|
240
|
+
ANIMA_HOME: stateDir,
|
|
241
|
+
ANIMA_INBOX_ITEM_ID: itemId,
|
|
242
|
+
ANIMA_SLACK_API_URL: slackApi.url,
|
|
243
|
+
},
|
|
244
|
+
input: captionWithShellMetachars,
|
|
245
|
+
});
|
|
246
|
+
assert.equal(send.status, 0, send.stderr || send.stdout);
|
|
247
|
+
// Caption survived shell-quoting hell because it never hit the shell.
|
|
248
|
+
assert.equal(completeCalls.length, 1);
|
|
249
|
+
const complete = completeCalls[0];
|
|
250
|
+
assert.equal(complete['initial_comment'], captionWithShellMetachars);
|
|
251
|
+
// Audit payload preserves the same caption string.
|
|
252
|
+
const completed = (await activitiesForInboxItemWindow('scout', itemId)).at(-1);
|
|
253
|
+
assert.equal(completed?.payload?.['caption'], captionWithShellMetachars);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
await slackApi.close();
|
|
258
|
+
uploadServer.close();
|
|
259
|
+
await once(uploadServer, 'close');
|
|
260
|
+
await rm(stateDir, { force: true, recursive: true });
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
test('file send records external.effect.failed when Slack rejects completeUploadExternal', async () => {
|
|
264
|
+
const stateDir = await mkdtemp(join(tmpdir(), 'anima-cli-file-complete-failure-test-'));
|
|
265
|
+
const uploadServer = createServer((_request, response) => {
|
|
266
|
+
response.writeHead(200);
|
|
267
|
+
response.end();
|
|
268
|
+
});
|
|
269
|
+
uploadServer.listen(0, '127.0.0.1');
|
|
270
|
+
await once(uploadServer, 'listening');
|
|
271
|
+
const uploadAddr = uploadServer.address();
|
|
272
|
+
if (!uploadAddr || typeof uploadAddr === 'string')
|
|
273
|
+
throw new Error('upload server not bound');
|
|
274
|
+
let completeCalls = 0;
|
|
275
|
+
const slackApi = await startSlackApiMock((method) => {
|
|
276
|
+
if (method === 'conversations.info') {
|
|
277
|
+
return { channel: { id: 'C-product', is_channel: true, name: 'product', name_normalized: 'product' }, ok: true };
|
|
278
|
+
}
|
|
279
|
+
if (method === 'files.getUploadURLExternal') {
|
|
280
|
+
return {
|
|
281
|
+
ok: true,
|
|
282
|
+
file_id: 'F-fail-1',
|
|
283
|
+
upload_url: `http://127.0.0.1:${uploadAddr.port}/upload/1`,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
if (method === 'files.completeUploadExternal') {
|
|
287
|
+
completeCalls += 1;
|
|
288
|
+
// Slack rejecting completion is the most user-visible failure mode:
|
|
289
|
+
// bytes already POSTed, but the message never posts. The audit must
|
|
290
|
+
// record `external.effect.failed` so users see why the file never
|
|
291
|
+
// appeared in the channel.
|
|
292
|
+
return { error: 'channel_not_found', ok: false };
|
|
293
|
+
}
|
|
294
|
+
throw new Error(`unexpected method ${method}`);
|
|
295
|
+
});
|
|
296
|
+
try {
|
|
297
|
+
await withAnimaHome(stateDir, async () => {
|
|
298
|
+
await writeSlackConfig(stateDir);
|
|
299
|
+
const itemId = await ingestSlackThread(stateDir);
|
|
300
|
+
const localPath = join(stateDir, 'doomed.png');
|
|
301
|
+
await writeFile(localPath, Buffer.from('img!'));
|
|
302
|
+
const send = await runNode([cliPath, 'file', 'send', '--channel', 'C-product', localPath], {
|
|
303
|
+
env: {
|
|
304
|
+
...process.env,
|
|
305
|
+
ANIMA_AGENT_ID: 'scout',
|
|
306
|
+
ANIMA_HOME: stateDir,
|
|
307
|
+
ANIMA_INBOX_ITEM_ID: itemId,
|
|
308
|
+
ANIMA_SLACK_API_URL: slackApi.url,
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
assert.notEqual(send.status, 0);
|
|
312
|
+
assert.match(send.stderr, /channel_not_found/);
|
|
313
|
+
assert.equal(completeCalls, 1);
|
|
314
|
+
const activities = await activitiesForInboxItemWindow('scout', itemId);
|
|
315
|
+
const failed = activities.at(-1);
|
|
316
|
+
assert.equal(failed?.type, 'external.effect.failed');
|
|
317
|
+
assert.equal(failed?.payload?.['effect'], 'slack.file.send');
|
|
318
|
+
assert.equal(failed?.payload?.['tool'], 'anima.file.send');
|
|
319
|
+
assert.match(String(failed?.payload?.['error'] ?? ''), /channel_not_found/);
|
|
320
|
+
// basePayload fields (target, file list, caption-absent) still made it
|
|
321
|
+
// into the failure activity so users can see what was attempted.
|
|
322
|
+
assert.equal(failed?.payload?.['fileCount'], 1);
|
|
323
|
+
assert.equal(failed?.payload?.['channelDisplayName'], '#product');
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
finally {
|
|
327
|
+
await slackApi.close();
|
|
328
|
+
uploadServer.close();
|
|
329
|
+
await once(uploadServer, 'close');
|
|
330
|
+
await rm(stateDir, { force: true, recursive: true });
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
test('file send records audit without an active item', async () => {
|
|
334
|
+
const stateDir = await mkdtemp(join(tmpdir(), 'anima-cli-file-no-item-test-'));
|
|
335
|
+
const uploadServer = createServer((request, response) => {
|
|
336
|
+
void readBody(request).then(() => {
|
|
337
|
+
response.writeHead(200, { 'content-type': 'application/json' });
|
|
338
|
+
response.end(JSON.stringify({ ok: true }));
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
uploadServer.listen(0, '127.0.0.1');
|
|
342
|
+
await once(uploadServer, 'listening');
|
|
343
|
+
const uploadAddr = uploadServer.address();
|
|
344
|
+
if (!uploadAddr || typeof uploadAddr === 'string')
|
|
345
|
+
throw new Error('upload server not bound');
|
|
346
|
+
let uploadCount = 0;
|
|
347
|
+
const slackApi = await startSlackApiMock((method) => {
|
|
348
|
+
if (method === 'conversations.info') {
|
|
349
|
+
return {
|
|
350
|
+
channel: { id: 'C-product', is_channel: true, name: 'product', name_normalized: 'product' },
|
|
351
|
+
ok: true,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
if (method === 'files.getUploadURLExternal') {
|
|
355
|
+
uploadCount += 1;
|
|
356
|
+
return { ok: true, file_id: `F-no-item-${uploadCount}`, upload_url: `http://127.0.0.1:${uploadAddr.port}/upload/${uploadCount}` };
|
|
357
|
+
}
|
|
358
|
+
if (method === 'files.completeUploadExternal') {
|
|
359
|
+
return { ok: true, files: [{ id: 'F-no-item-1', title: 'sent.png' }] };
|
|
360
|
+
}
|
|
361
|
+
if (method === 'files.info') {
|
|
362
|
+
return { ok: true, file: { id: 'F-no-item-1', mimetype: 'image/png', name: 'sent.png', size: 1 } };
|
|
363
|
+
}
|
|
364
|
+
throw new Error(`unexpected method ${method}`);
|
|
365
|
+
});
|
|
366
|
+
try {
|
|
367
|
+
await withAnimaHome(stateDir, async () => {
|
|
368
|
+
await writeSlackConfig(stateDir);
|
|
369
|
+
const localPath = join(stateDir, 'never.png');
|
|
370
|
+
await writeFile(localPath, Buffer.from('x'));
|
|
371
|
+
const send = await runNode([cliPath, 'file', 'send', '--channel', 'C-product', localPath], {
|
|
372
|
+
env: {
|
|
373
|
+
...process.env,
|
|
374
|
+
ANIMA_AGENT_ID: 'scout',
|
|
375
|
+
ANIMA_HOME: stateDir,
|
|
376
|
+
ANIMA_INBOX_ITEM_ID: '',
|
|
377
|
+
ANIMA_SLACK_API_URL: slackApi.url,
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
assert.equal(send.status, 0, send.stderr || send.stdout);
|
|
381
|
+
assert.equal(uploadCount, 1);
|
|
382
|
+
const completed = allActivities(await loadState()).at(-1);
|
|
383
|
+
assert.equal(completed?.type, 'external.effect.completed');
|
|
384
|
+
assert.equal(completed?.payload?.['effect'], 'slack.file.send');
|
|
385
|
+
assert.equal(Object.hasOwn(completed ?? {}, 'itemId'), false);
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
finally {
|
|
389
|
+
await slackApi.close();
|
|
390
|
+
uploadServer.close();
|
|
391
|
+
await once(uploadServer, 'close');
|
|
392
|
+
await rm(stateDir, { force: true, recursive: true });
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
test('file fetch supports --file-id/--output and positional output aliases', async () => {
|
|
396
|
+
const stateDir = await mkdtemp(join(tmpdir(), 'anima-cli-file-fetch-alias-test-'));
|
|
397
|
+
const downloadServer = createServer((_request, response) => {
|
|
398
|
+
response.writeHead(200, { 'content-type': 'text/plain' });
|
|
399
|
+
response.end('downloaded file bytes');
|
|
400
|
+
});
|
|
401
|
+
downloadServer.listen(0, '127.0.0.1');
|
|
402
|
+
await once(downloadServer, 'listening');
|
|
403
|
+
const downloadAddr = downloadServer.address();
|
|
404
|
+
if (!downloadAddr || typeof downloadAddr === 'string')
|
|
405
|
+
throw new Error('download server not bound');
|
|
406
|
+
let infoCalls = 0;
|
|
407
|
+
const slackApi = await startSlackApiMock((method) => {
|
|
408
|
+
if (method === 'auth.test')
|
|
409
|
+
return { ok: true, team_id: 'T-demo' };
|
|
410
|
+
if (method === 'files.info') {
|
|
411
|
+
infoCalls += 1;
|
|
412
|
+
return {
|
|
413
|
+
ok: true,
|
|
414
|
+
file: {
|
|
415
|
+
id: 'F-fetch',
|
|
416
|
+
mimetype: 'text/plain',
|
|
417
|
+
name: 'notes.txt',
|
|
418
|
+
size: 21,
|
|
419
|
+
url_private: `http://127.0.0.1:${downloadAddr.port}/notes.txt`,
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
throw new Error(`unexpected method ${method}`);
|
|
424
|
+
});
|
|
425
|
+
try {
|
|
426
|
+
await withAnimaHome(stateDir, async () => {
|
|
427
|
+
await writeSlackConfig(stateDir);
|
|
428
|
+
const env = {
|
|
429
|
+
...process.env,
|
|
430
|
+
ANIMA_AGENT_ID: 'scout',
|
|
431
|
+
ANIMA_HOME: stateDir,
|
|
432
|
+
ANIMA_SLACK_API_URL: slackApi.url,
|
|
433
|
+
};
|
|
434
|
+
const output = join(stateDir, 'exports', 'notes.txt');
|
|
435
|
+
const fetchByOption = await runNode([cliPath, 'file', 'fetch', '--file-id', 'F-fetch', '--output', output], { env });
|
|
436
|
+
assert.equal(fetchByOption.status, 0, fetchByOption.stderr || fetchByOption.stdout);
|
|
437
|
+
assert.equal(fetchByOption.stdout.trim(), output);
|
|
438
|
+
assert.equal(await readFile(output, 'utf8'), 'downloaded file bytes');
|
|
439
|
+
const positionalOutput = join(stateDir, 'exports', 'copy.txt');
|
|
440
|
+
const fetchByPosition = await runNode([cliPath, 'file', 'fetch', 'F-fetch', positionalOutput], { env });
|
|
441
|
+
assert.equal(fetchByPosition.status, 0, fetchByPosition.stderr || fetchByPosition.stdout);
|
|
442
|
+
assert.equal(fetchByPosition.stdout.trim(), positionalOutput);
|
|
443
|
+
assert.equal(await readFile(positionalOutput, 'utf8'), 'downloaded file bytes');
|
|
444
|
+
assert.equal(infoCalls, 1);
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
finally {
|
|
448
|
+
await slackApi.close();
|
|
449
|
+
downloadServer.close();
|
|
450
|
+
await once(downloadServer, 'close');
|
|
451
|
+
await rm(stateDir, { force: true, recursive: true });
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
test('file commands reject agent and item flags', async () => {
|
|
455
|
+
const fetch = await runNode([cliPath, 'file', '--agent', 'scout', 'fetch', 'F-test']);
|
|
456
|
+
assert.notEqual(fetch.status, 0);
|
|
457
|
+
assert.match(fetch.stderr, /unknown option '--agent'/);
|
|
458
|
+
const send = await runNode([cliPath, 'file', '--item', 'turn_123', 'send', '--channel', 'C-product', 'missing.png']);
|
|
459
|
+
assert.notEqual(send.status, 0);
|
|
460
|
+
assert.match(send.stderr, /unknown option '--item'/);
|
|
461
|
+
});
|
|
462
|
+
test('file send rejects missing local path before any Slack call', async () => {
|
|
463
|
+
const stateDir = await mkdtemp(join(tmpdir(), 'anima-cli-file-missing-path-test-'));
|
|
464
|
+
let upstreamCalls = 0;
|
|
465
|
+
const slackApi = await startSlackApiMock(() => {
|
|
466
|
+
upstreamCalls += 1;
|
|
467
|
+
return { ok: true };
|
|
468
|
+
});
|
|
469
|
+
try {
|
|
470
|
+
await withAnimaHome(stateDir, async () => {
|
|
471
|
+
await writeSlackConfig(stateDir);
|
|
472
|
+
const itemId = await ingestSlackThread(stateDir);
|
|
473
|
+
const send = await runNode([cliPath, 'file', 'send', '--channel', 'C-product', join(stateDir, 'does-not-exist.png')], {
|
|
474
|
+
env: {
|
|
475
|
+
...process.env,
|
|
476
|
+
ANIMA_AGENT_ID: 'scout',
|
|
477
|
+
ANIMA_HOME: stateDir,
|
|
478
|
+
ANIMA_INBOX_ITEM_ID: itemId,
|
|
479
|
+
ANIMA_SLACK_API_URL: slackApi.url,
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
assert.notEqual(send.status, 0);
|
|
483
|
+
assert.match(send.stderr, /file not found/);
|
|
484
|
+
assert.equal(upstreamCalls, 0);
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
finally {
|
|
488
|
+
await slackApi.close();
|
|
489
|
+
await rm(stateDir, { force: true, recursive: true });
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
async function writeSlackConfig(configDir, slack = {}) {
|
|
493
|
+
await mkdir(configDir, { recursive: true });
|
|
494
|
+
const agent = {
|
|
495
|
+
id: 'scout',
|
|
496
|
+
slack: {
|
|
497
|
+
appToken: slack.appToken ?? 'xapp-test',
|
|
498
|
+
botToken: slack.botToken ?? 'xoxb-test',
|
|
499
|
+
teamId: slack.teamId ?? 'T-demo',
|
|
500
|
+
},
|
|
501
|
+
};
|
|
502
|
+
const agentDir = join(configDir, 'agents', agent.id);
|
|
503
|
+
await mkdir(agentDir, { recursive: true });
|
|
504
|
+
await writeFile(join(configDir, 'config.json'), `${JSON.stringify({}, null, 2)}\n`, 'utf8');
|
|
505
|
+
await writeFile(join(agentDir, 'config.json'), `${JSON.stringify(agent, null, 2)}\n`, 'utf8');
|
|
506
|
+
}
|
|
507
|
+
async function ingestSlackThread(stateDir) {
|
|
508
|
+
return withAnimaHome(stateDir, async () => {
|
|
509
|
+
const ctx = await ingestEvent(exampleSlackThread(), { agentId: 'scout', stateDir });
|
|
510
|
+
return ctx.item.id;
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
function exampleSlackThread() {
|
|
514
|
+
return makeSlackEvent({
|
|
515
|
+
channelId: 'C-product',
|
|
516
|
+
channelName: 'product',
|
|
517
|
+
eventId: 'evt_example_slack_thread',
|
|
518
|
+
teamId: 'T-demo',
|
|
519
|
+
text: 'Can you turn this discussion into a tracked product spike?',
|
|
520
|
+
timestamp: '2026-05-11T00:00:00.000Z',
|
|
521
|
+
threadTs: '1770000200.000001',
|
|
522
|
+
ts: '1770000200.000002',
|
|
523
|
+
userId: 'U-alice',
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
async function runNode(args, options = {}) {
|
|
527
|
+
const child = spawn(process.execPath, args, {
|
|
528
|
+
cwd: options.cwd,
|
|
529
|
+
env: options.env,
|
|
530
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
531
|
+
});
|
|
532
|
+
let stdout = '';
|
|
533
|
+
let stderr = '';
|
|
534
|
+
child.stdout.setEncoding('utf8');
|
|
535
|
+
child.stderr.setEncoding('utf8');
|
|
536
|
+
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
537
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
538
|
+
child.stdin.end(options.input);
|
|
539
|
+
const [status] = (await once(child, 'exit'));
|
|
540
|
+
return { status, stderr, stdout };
|
|
541
|
+
}
|
|
542
|
+
async function startSlackApiMock(handler) {
|
|
543
|
+
const server = createServer(async (request, response) => {
|
|
544
|
+
const body = await readBody(request);
|
|
545
|
+
try {
|
|
546
|
+
const pathname = new URL(request.url ?? '/', 'http://127.0.0.1').pathname;
|
|
547
|
+
const normalizedMethod = pathname.replace(/^\/api\//, '');
|
|
548
|
+
const payload = handler(normalizedMethod, body);
|
|
549
|
+
response.writeHead(200, { 'content-type': 'application/json' });
|
|
550
|
+
response.end(JSON.stringify(payload));
|
|
551
|
+
}
|
|
552
|
+
catch (error) {
|
|
553
|
+
response.writeHead(500, { 'content-type': 'application/json' });
|
|
554
|
+
response.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error), ok: false }));
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
server.listen(0, '127.0.0.1');
|
|
558
|
+
await once(server, 'listening');
|
|
559
|
+
const address = server.address();
|
|
560
|
+
if (!address || typeof address === 'string') {
|
|
561
|
+
throw new Error('Expected Slack API mock to listen on a TCP address.');
|
|
562
|
+
}
|
|
563
|
+
return {
|
|
564
|
+
close: async () => {
|
|
565
|
+
server.close();
|
|
566
|
+
await once(server, 'close');
|
|
567
|
+
},
|
|
568
|
+
url: `http://127.0.0.1:${address.port}/api`,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
function parseFilesField(value) {
|
|
572
|
+
if (typeof value !== 'string')
|
|
573
|
+
return Array.isArray(value) ? value : [];
|
|
574
|
+
try {
|
|
575
|
+
const parsed = JSON.parse(value);
|
|
576
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
return [];
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
function slackRequestBody(body) {
|
|
583
|
+
try {
|
|
584
|
+
return JSON.parse(body);
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
return Object.fromEntries(new URLSearchParams(body));
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
async function readBody(request) {
|
|
591
|
+
let body = '';
|
|
592
|
+
request.setEncoding('utf8');
|
|
593
|
+
for await (const chunk of request)
|
|
594
|
+
body += chunk;
|
|
595
|
+
return body;
|
|
596
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-message.test.d.ts","sourceRoot":"","sources":["../../../server/tests/cli-message.test.ts"],"names":[],"mappings":""}
|