alvin-bot 4.18.0 → 4.18.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/CHANGELOG.md +22 -2
- package/bin/cli.js +1 -1
- package/package.json +3 -1
- package/test/allowed-users-gate.test.ts +0 -98
- package/test/alvin-dispatch.test.ts +0 -220
- package/test/async-agent-chunk-flow.test.ts +0 -244
- package/test/async-agent-parser-staleness.test.ts +0 -412
- package/test/async-agent-parser-streamjson.test.ts +0 -273
- package/test/async-agent-parser.test.ts +0 -322
- package/test/async-agent-watcher.test.ts +0 -229
- package/test/background-bypass-integration.test.ts +0 -443
- package/test/background-bypass-stress.test.ts +0 -417
- package/test/background-bypass.test.ts +0 -127
- package/test/browser-webfetch.test.ts +0 -121
- package/test/claude-sdk-provider.test.ts +0 -115
- package/test/claude-sdk-tool-use-id.test.ts +0 -180
- package/test/console-timestamps.test.ts +0 -98
- package/test/cron-progress-ticker.test.ts +0 -76
- package/test/cron-restart-resilience.test.ts +0 -191
- package/test/cron-run-resolver.test.ts +0 -133
- package/test/cron-runjobnow-throw.test.ts +0 -100
- package/test/debounce.test.ts +0 -60
- package/test/delivery-registry.test.ts +0 -71
- package/test/exec-guard-metachars.test.ts +0 -110
- package/test/file-permissions.test.ts +0 -130
- package/test/i18n.test.ts +0 -108
- package/test/list-subagents-merged.test.ts +0 -172
- package/test/memory-extractor.test.ts +0 -151
- package/test/memory-layers.test.ts +0 -169
- package/test/memory-sdk-injection.test.ts +0 -146
- package/test/memory-stress-restart.test.ts +0 -337
- package/test/multi-session-stress.test.ts +0 -255
- package/test/platform-session-key.test.ts +0 -69
- package/test/process-manager.test.ts +0 -186
- package/test/registry.test.ts +0 -201
- package/test/session-pending-background.test.ts +0 -59
- package/test/session-persistence.test.ts +0 -195
- package/test/slack-progress-ticker.test.ts +0 -123
- package/test/slack-slash-command.test.ts +0 -61
- package/test/slack-test-connection.test.ts +0 -176
- package/test/stress-scenarios.test.ts +0 -356
- package/test/stuck-timer.test.ts +0 -116
- package/test/subagent-delivery-markdown-fallback.test.ts +0 -147
- package/test/subagent-delivery-platform-routing.test.ts +0 -232
- package/test/subagent-delivery.test.ts +0 -273
- package/test/subagent-final-text.test.ts +0 -132
- package/test/subagent-stats.test.ts +0 -119
- package/test/subagent-toolset-allowlist.test.ts +0 -146
- package/test/subagents-commands.test.ts +0 -64
- package/test/subagents-config.test.ts +0 -114
- package/test/subagents-depth.test.ts +0 -58
- package/test/subagents-inheritance.test.ts +0 -67
- package/test/subagents-name-resolver.test.ts +0 -122
- package/test/subagents-priority-reject.test.ts +0 -88
- package/test/subagents-queue.test.ts +0 -127
- package/test/subagents-shutdown.test.ts +0 -126
- package/test/subagents-toolset.test.ts +0 -71
- package/test/sync-task-timeout.test.ts +0 -153
- package/test/system-prompt-background-hint.test.ts +0 -65
- package/test/telegram-error-filter.test.ts +0 -85
- package/test/telegram-workspace-command.test.ts +0 -78
- package/test/timing-safe-bearer.test.ts +0 -65
- package/test/watchdog-brake.test.ts +0 -157
- package/test/watcher-pending-count.test.ts +0 -228
- package/test/watcher-zombie-fix.test.ts +0 -252
- package/test/web-server-integration.test.ts +0 -189
- package/test/web-server-resilience.test.ts +0 -118
- package/test/web-server-shutdown.test.ts +0 -117
- package/test/whatsapp-auth-resilience.test.ts +0 -96
- package/test/workspaces.test.ts +0 -196
- package/vitest.config.ts +0 -17
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Alvin Bot are documented here.
|
|
4
4
|
|
|
5
|
+
## [4.18.1] — 2026-04-20
|
|
6
|
+
|
|
7
|
+
### 🔒 Privacy-Guard: pre-publish check blocks PII leaks in shipped files
|
|
8
|
+
|
|
9
|
+
Adds an automated gate that runs on every `npm publish` and prevents personal information from accidentally shipping. After the 4.18.0 privacy sanitization, this ensures it never happens again.
|
|
10
|
+
|
|
11
|
+
**New:**
|
|
12
|
+
- `scripts/privacy-check.sh` — scans the exact file list that `npm pack` would ship. Case-insensitive regex match against a patterns file. Any hit fails the publish.
|
|
13
|
+
- `scripts/privacy-patterns.default.txt` — bundled, contains only generic patterns (email shape, IP addresses, postal codes, personal task phrasings). No project or person names — so safe to ship.
|
|
14
|
+
- `package.json` `prepublishOnly` hook — runs the check automatically.
|
|
15
|
+
- `npm run privacy-check` — manual run anytime.
|
|
16
|
+
|
|
17
|
+
**Maintainer-local overrides:** Put `~/.alvin-bot/privacy-patterns.txt` with personal/project-specific patterns. That file is gitignored, never leaves your machine, and takes precedence over the bundled defaults.
|
|
18
|
+
|
|
19
|
+
**CI override:** Set `$ALVIN_PRIVACY_PATTERNS` to an absolute path; takes top precedence over both files above.
|
|
20
|
+
|
|
21
|
+
**Hardening: `.npmignore`** — added `test/` and `vitest.config.ts` to the ignore list. Previously the full test suite shipped with every npm tarball, adding ~2 MB and exposing test fixtures that sometimes referenced internal project names.
|
|
22
|
+
|
|
23
|
+
**CLAUDE.md** — documents the rule and the patterns-file lookup order so future maintenance sessions catch new cases proactively.
|
|
24
|
+
|
|
5
25
|
## [4.18.0] — 2026-04-20
|
|
6
26
|
|
|
7
27
|
### ⚡ Performance + Hardening: medium-priority cleanups from the stability audit
|
|
@@ -169,8 +189,8 @@ The three aliased entries all route through `ClaudeSDKProvider` with different `
|
|
|
169
189
|
|
|
170
190
|
```yaml
|
|
171
191
|
---
|
|
172
|
-
purpose:
|
|
173
|
-
cwd: ~/
|
|
192
|
+
purpose: my-project
|
|
193
|
+
cwd: ~/Projects/my-project
|
|
174
194
|
model: sonnet # opus | sonnet | haiku | claude-opus-4-7 | ...
|
|
175
195
|
---
|
|
176
196
|
```
|
package/bin/cli.js
CHANGED
|
@@ -1828,7 +1828,7 @@ switch (cmd) {
|
|
|
1828
1828
|
const searchQuery = process.argv.slice(3).join(" ");
|
|
1829
1829
|
if (!searchQuery) {
|
|
1830
1830
|
console.log("Usage: alvin-bot search <query>");
|
|
1831
|
-
console.log('Example: alvin-bot search "
|
|
1831
|
+
console.log('Example: alvin-bot search "tax document 2024"');
|
|
1832
1832
|
process.exit(1);
|
|
1833
1833
|
}
|
|
1834
1834
|
const { searchSelf, formatSearchResults } = await import("../dist/services/self-search.js");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alvin-bot",
|
|
3
|
-
"version": "4.18.
|
|
3
|
+
"version": "4.18.1",
|
|
4
4
|
"description": "Alvin Bot \u2014 Your personal AI agent on Telegram, WhatsApp, Discord, Signal, and Web.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
"test": "vitest run",
|
|
16
16
|
"test:watch": "vitest",
|
|
17
17
|
"test:ui": "vitest --ui",
|
|
18
|
+
"privacy-check": "bash scripts/privacy-check.sh",
|
|
19
|
+
"prepublishOnly": "bash scripts/privacy-check.sh",
|
|
18
20
|
"electron:compile": "tsc -p electron/tsconfig.json",
|
|
19
21
|
"electron:dev": "npm run electron:compile && electron .",
|
|
20
22
|
"electron:build": "npm run build && npm run electron:compile && electron-builder --publish never",
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* v4.12.2 — ALLOWED_USERS startup hard-fail gate.
|
|
3
|
-
*
|
|
4
|
-
* When the Telegram bot token is configured but ALLOWED_USERS is empty,
|
|
5
|
-
* starting the bot would leave it open to any Telegram user sending a DM.
|
|
6
|
-
* Previously this only emitted a console.warn and the bot started anyway.
|
|
7
|
-
*
|
|
8
|
-
* v4.12.2 introduces a pure gate function that decides whether to refuse
|
|
9
|
-
* startup, with two explicit escape hatches:
|
|
10
|
-
* 1. AUTH_MODE=open — user explicitly wants an open bot
|
|
11
|
-
* 2. ALVIN_INSECURE_ACKNOWLEDGED=1 — explicit opt-out for test/scripted envs
|
|
12
|
-
*
|
|
13
|
-
* This test file exercises the pure gate. The actual wiring in src/index.ts
|
|
14
|
-
* is a thin if-block that calls process.exit(1) on deny.
|
|
15
|
-
*/
|
|
16
|
-
import { describe, it, expect } from "vitest";
|
|
17
|
-
import { checkAllowedUsersGate } from "../src/services/allowed-users-gate.js";
|
|
18
|
-
|
|
19
|
-
describe("allowed-users-gate (v4.12.2)", () => {
|
|
20
|
-
it("allows startup when ALLOWED_USERS is populated", () => {
|
|
21
|
-
const result = checkAllowedUsersGate({
|
|
22
|
-
hasTelegram: true,
|
|
23
|
-
allowedUsersCount: 1,
|
|
24
|
-
authMode: "allowlist",
|
|
25
|
-
insecureAcknowledged: false,
|
|
26
|
-
});
|
|
27
|
-
expect(result.allowed).toBe(true);
|
|
28
|
-
expect(result.reason).toBeUndefined();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("BLOCKS startup when telegram enabled but allowedUsers empty (allowlist mode)", () => {
|
|
32
|
-
const result = checkAllowedUsersGate({
|
|
33
|
-
hasTelegram: true,
|
|
34
|
-
allowedUsersCount: 0,
|
|
35
|
-
authMode: "allowlist",
|
|
36
|
-
insecureAcknowledged: false,
|
|
37
|
-
});
|
|
38
|
-
expect(result.allowed).toBe(false);
|
|
39
|
-
expect(result.reason).toContain("ALLOWED_USERS");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("BLOCKS startup when telegram enabled but allowedUsers empty (pairing mode)", () => {
|
|
43
|
-
// Pairing mode needs allowedUsers[0] as the admin for approval routing.
|
|
44
|
-
// Empty array breaks the whole pairing flow.
|
|
45
|
-
const result = checkAllowedUsersGate({
|
|
46
|
-
hasTelegram: true,
|
|
47
|
-
allowedUsersCount: 0,
|
|
48
|
-
authMode: "pairing",
|
|
49
|
-
insecureAcknowledged: false,
|
|
50
|
-
});
|
|
51
|
-
expect(result.allowed).toBe(false);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("ALLOWS startup when AUTH_MODE=open explicitly", () => {
|
|
55
|
-
const result = checkAllowedUsersGate({
|
|
56
|
-
hasTelegram: true,
|
|
57
|
-
allowedUsersCount: 0,
|
|
58
|
-
authMode: "open",
|
|
59
|
-
insecureAcknowledged: false,
|
|
60
|
-
});
|
|
61
|
-
expect(result.allowed).toBe(true);
|
|
62
|
-
expect(result.warning).toContain("open");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("ALLOWS startup when ALVIN_INSECURE_ACKNOWLEDGED=1", () => {
|
|
66
|
-
const result = checkAllowedUsersGate({
|
|
67
|
-
hasTelegram: true,
|
|
68
|
-
allowedUsersCount: 0,
|
|
69
|
-
authMode: "allowlist",
|
|
70
|
-
insecureAcknowledged: true,
|
|
71
|
-
});
|
|
72
|
-
expect(result.allowed).toBe(true);
|
|
73
|
-
expect(result.warning).toContain("INSECURE");
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("ALLOWS startup when telegram is NOT enabled (bot is WebUI-only)", () => {
|
|
77
|
-
// WebUI-only deployments don't have a BOT_TOKEN and don't need
|
|
78
|
-
// ALLOWED_USERS — the gate only applies when hasTelegram === true.
|
|
79
|
-
const result = checkAllowedUsersGate({
|
|
80
|
-
hasTelegram: false,
|
|
81
|
-
allowedUsersCount: 0,
|
|
82
|
-
authMode: "allowlist",
|
|
83
|
-
insecureAcknowledged: false,
|
|
84
|
-
});
|
|
85
|
-
expect(result.allowed).toBe(true);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("reason message mentions ~/.alvin-bot/.env and @userinfobot for operator guidance", () => {
|
|
89
|
-
const result = checkAllowedUsersGate({
|
|
90
|
-
hasTelegram: true,
|
|
91
|
-
allowedUsersCount: 0,
|
|
92
|
-
authMode: "allowlist",
|
|
93
|
-
insecureAcknowledged: false,
|
|
94
|
-
});
|
|
95
|
-
expect(result.reason).toMatch(/\.env|alvin-bot/i);
|
|
96
|
-
expect(result.reason).toMatch(/userinfobot|telegram/i);
|
|
97
|
-
});
|
|
98
|
-
});
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* v4.13 — alvin_dispatch custom-tool service.
|
|
3
|
-
*
|
|
4
|
-
* `dispatchDetachedAgent(input)` spawns a truly independent `claude -p`
|
|
5
|
-
* subprocess that survives the parent handler's abort. This is the
|
|
6
|
-
* architectural replacement for SDK's built-in Task(run_in_background)
|
|
7
|
-
* tool, which was tied to the parent SDK subprocess lifecycle.
|
|
8
|
-
*
|
|
9
|
-
* Contract:
|
|
10
|
-
* - Input: { prompt, description, chatId, userId, sessionKey }
|
|
11
|
-
* - Output (synchronous): { agentId, outputFile, spawned: true }
|
|
12
|
-
* - Side effect: spawns detached subprocess writing stream-json
|
|
13
|
-
* output to outputFile, registers with async-agent-watcher.
|
|
14
|
-
*
|
|
15
|
-
* These tests stub child_process.spawn so they run fast and deterministic.
|
|
16
|
-
* The "real subprocess survives parent" property was verified empirically
|
|
17
|
-
* in Phase A (see plan doc).
|
|
18
|
-
*/
|
|
19
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
20
|
-
import os from "os";
|
|
21
|
-
import fs from "fs";
|
|
22
|
-
import { resolve } from "path";
|
|
23
|
-
|
|
24
|
-
const TEST_DATA_DIR = resolve(
|
|
25
|
-
os.tmpdir(),
|
|
26
|
-
`alvin-dispatch-${process.pid}-${Date.now()}`,
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
interface SpawnRecord {
|
|
30
|
-
cmd: string;
|
|
31
|
-
args: string[];
|
|
32
|
-
opts: {
|
|
33
|
-
detached?: boolean;
|
|
34
|
-
stdio?: unknown;
|
|
35
|
-
cwd?: string;
|
|
36
|
-
env?: Record<string, string | undefined>;
|
|
37
|
-
};
|
|
38
|
-
unreffed: boolean;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
let spawned: SpawnRecord[] = [];
|
|
42
|
-
|
|
43
|
-
beforeEach(async () => {
|
|
44
|
-
if (fs.existsSync(TEST_DATA_DIR))
|
|
45
|
-
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
|
46
|
-
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
|
47
|
-
process.env.ALVIN_DATA_DIR = TEST_DATA_DIR;
|
|
48
|
-
spawned = [];
|
|
49
|
-
vi.resetModules();
|
|
50
|
-
|
|
51
|
-
vi.doMock("node:child_process", async () => {
|
|
52
|
-
const actual = await vi.importActual<typeof import("node:child_process")>(
|
|
53
|
-
"node:child_process",
|
|
54
|
-
);
|
|
55
|
-
return {
|
|
56
|
-
...actual,
|
|
57
|
-
spawn: (cmd: string, args: string[], opts: SpawnRecord["opts"]) => {
|
|
58
|
-
const record: SpawnRecord = {
|
|
59
|
-
cmd,
|
|
60
|
-
args,
|
|
61
|
-
opts,
|
|
62
|
-
unreffed: false,
|
|
63
|
-
};
|
|
64
|
-
spawned.push(record);
|
|
65
|
-
return {
|
|
66
|
-
pid: 12345,
|
|
67
|
-
unref() {
|
|
68
|
-
record.unreffed = true;
|
|
69
|
-
},
|
|
70
|
-
on() {},
|
|
71
|
-
kill() {},
|
|
72
|
-
};
|
|
73
|
-
},
|
|
74
|
-
};
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
vi.doMock("../src/services/subagent-delivery.js", () => ({
|
|
78
|
-
deliverSubAgentResult: async () => {},
|
|
79
|
-
attachBotApi: () => {},
|
|
80
|
-
__setBotApiForTest: () => {},
|
|
81
|
-
}));
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
afterEach(async () => {
|
|
85
|
-
try {
|
|
86
|
-
const mod = await import("../src/services/async-agent-watcher.js");
|
|
87
|
-
mod.stopWatcher();
|
|
88
|
-
mod.__resetForTest();
|
|
89
|
-
} catch {
|
|
90
|
-
/* ignore */
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
describe("dispatchDetachedAgent (v4.13)", () => {
|
|
95
|
-
it("spawns claude -p with detached: true and unrefs", async () => {
|
|
96
|
-
const mod = await import("../src/services/alvin-dispatch.js");
|
|
97
|
-
const result = mod.dispatchDetachedAgent({
|
|
98
|
-
prompt: "research X",
|
|
99
|
-
description: "X research",
|
|
100
|
-
chatId: 42,
|
|
101
|
-
userId: 42,
|
|
102
|
-
sessionKey: "s1",
|
|
103
|
-
});
|
|
104
|
-
expect(result.agentId).toMatch(/^alvin-[a-f0-9]{16,}$/);
|
|
105
|
-
expect(result.outputFile).toContain(TEST_DATA_DIR);
|
|
106
|
-
expect(result.spawned).toBe(true);
|
|
107
|
-
|
|
108
|
-
expect(spawned).toHaveLength(1);
|
|
109
|
-
const [s] = spawned;
|
|
110
|
-
expect(s.cmd).toMatch(/claude/);
|
|
111
|
-
expect(s.args).toContain("-p");
|
|
112
|
-
expect(s.args).toContain("research X");
|
|
113
|
-
expect(s.args).toContain("--output-format");
|
|
114
|
-
expect(s.args).toContain("stream-json");
|
|
115
|
-
expect(s.opts.detached).toBe(true);
|
|
116
|
-
expect(s.unreffed).toBe(true);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("returns unique agentIds for concurrent dispatches", async () => {
|
|
120
|
-
const mod = await import("../src/services/alvin-dispatch.js");
|
|
121
|
-
const r1 = mod.dispatchDetachedAgent({
|
|
122
|
-
prompt: "a",
|
|
123
|
-
description: "a",
|
|
124
|
-
chatId: 1,
|
|
125
|
-
userId: 1,
|
|
126
|
-
sessionKey: "s1",
|
|
127
|
-
});
|
|
128
|
-
const r2 = mod.dispatchDetachedAgent({
|
|
129
|
-
prompt: "b",
|
|
130
|
-
description: "b",
|
|
131
|
-
chatId: 1,
|
|
132
|
-
userId: 1,
|
|
133
|
-
sessionKey: "s1",
|
|
134
|
-
});
|
|
135
|
-
expect(r1.agentId).not.toBe(r2.agentId);
|
|
136
|
-
expect(r1.outputFile).not.toBe(r2.outputFile);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("registers the pending agent with the watcher", async () => {
|
|
140
|
-
const mod = await import("../src/services/alvin-dispatch.js");
|
|
141
|
-
const watcher = await import("../src/services/async-agent-watcher.js");
|
|
142
|
-
|
|
143
|
-
mod.dispatchDetachedAgent({
|
|
144
|
-
prompt: "x",
|
|
145
|
-
description: "X audit",
|
|
146
|
-
chatId: 42,
|
|
147
|
-
userId: 42,
|
|
148
|
-
sessionKey: "s1",
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
const pending = watcher.listPendingAgents();
|
|
152
|
-
expect(pending).toHaveLength(1);
|
|
153
|
-
expect(pending[0].description).toBe("X audit");
|
|
154
|
-
expect(pending[0].sessionKey).toBe("s1");
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("increments session.pendingBackgroundCount on dispatch", async () => {
|
|
158
|
-
const mod = await import("../src/services/alvin-dispatch.js");
|
|
159
|
-
const { getSession } = await import("../src/services/session.js");
|
|
160
|
-
|
|
161
|
-
const session = getSession("s-count");
|
|
162
|
-
session.pendingBackgroundCount = 0;
|
|
163
|
-
|
|
164
|
-
mod.dispatchDetachedAgent({
|
|
165
|
-
prompt: "p",
|
|
166
|
-
description: "d",
|
|
167
|
-
chatId: 1,
|
|
168
|
-
userId: 1,
|
|
169
|
-
sessionKey: "s-count",
|
|
170
|
-
});
|
|
171
|
-
expect(session.pendingBackgroundCount).toBe(1);
|
|
172
|
-
|
|
173
|
-
mod.dispatchDetachedAgent({
|
|
174
|
-
prompt: "p2",
|
|
175
|
-
description: "d2",
|
|
176
|
-
chatId: 1,
|
|
177
|
-
userId: 1,
|
|
178
|
-
sessionKey: "s-count",
|
|
179
|
-
});
|
|
180
|
-
expect(session.pendingBackgroundCount).toBe(2);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it("uses stdio redirect so child's stdout goes to outputFile", async () => {
|
|
184
|
-
const mod = await import("../src/services/alvin-dispatch.js");
|
|
185
|
-
mod.dispatchDetachedAgent({
|
|
186
|
-
prompt: "p",
|
|
187
|
-
description: "d",
|
|
188
|
-
chatId: 1,
|
|
189
|
-
userId: 1,
|
|
190
|
-
sessionKey: "s1",
|
|
191
|
-
});
|
|
192
|
-
const [s] = spawned;
|
|
193
|
-
// stdio should be an array with FD redirects (ignore, pipe-to-file, ignore)
|
|
194
|
-
// or similar. We verify it's NOT "inherit" (which would attach to parent).
|
|
195
|
-
expect(s.opts.stdio).not.toBe("inherit");
|
|
196
|
-
expect(s.opts.stdio).not.toBe(undefined);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it("cleans env of CLAUDECODE/CLAUDE_CODE_ENTRYPOINT to prevent nested session errors", async () => {
|
|
200
|
-
const mod = await import("../src/services/alvin-dispatch.js");
|
|
201
|
-
process.env.CLAUDECODE = "1";
|
|
202
|
-
process.env.CLAUDE_CODE_ENTRYPOINT = "cli";
|
|
203
|
-
try {
|
|
204
|
-
mod.dispatchDetachedAgent({
|
|
205
|
-
prompt: "p",
|
|
206
|
-
description: "d",
|
|
207
|
-
chatId: 1,
|
|
208
|
-
userId: 1,
|
|
209
|
-
sessionKey: "s1",
|
|
210
|
-
});
|
|
211
|
-
const [s] = spawned;
|
|
212
|
-
expect(s.opts.env).toBeDefined();
|
|
213
|
-
expect(s.opts.env?.CLAUDECODE).toBeUndefined();
|
|
214
|
-
expect(s.opts.env?.CLAUDE_CODE_ENTRYPOINT).toBeUndefined();
|
|
215
|
-
} finally {
|
|
216
|
-
delete process.env.CLAUDECODE;
|
|
217
|
-
delete process.env.CLAUDE_CODE_ENTRYPOINT;
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
});
|
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fix #17 (Stage 2) — when the SDK yields a tool_result chunk with an
|
|
3
|
-
* "Async agent launched successfully" payload, the message handler
|
|
4
|
-
* must register the pending agent with the watcher.
|
|
5
|
-
*
|
|
6
|
-
* This tests the helper `handleToolResultChunk` in isolation —
|
|
7
|
-
* the integration with message.ts is covered by the live e2e test.
|
|
8
|
-
*/
|
|
9
|
-
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
10
|
-
|
|
11
|
-
describe("async agent chunk flow (Stage 2)", () => {
|
|
12
|
-
beforeEach(() => vi.resetModules());
|
|
13
|
-
|
|
14
|
-
it("tool_result with async_launched gets registered with the watcher", async () => {
|
|
15
|
-
const registered: unknown[] = [];
|
|
16
|
-
vi.doMock("../src/services/async-agent-watcher.js", () => ({
|
|
17
|
-
registerPendingAgent: (input: unknown) => registered.push(input),
|
|
18
|
-
startWatcher: () => {},
|
|
19
|
-
stopWatcher: () => {},
|
|
20
|
-
pollOnce: async () => {},
|
|
21
|
-
listPendingAgents: () => [],
|
|
22
|
-
}));
|
|
23
|
-
|
|
24
|
-
const { handleToolResultChunk } = await import(
|
|
25
|
-
"../src/handlers/async-agent-chunk-handler.js"
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
const chunk = {
|
|
29
|
-
type: "tool_result" as const,
|
|
30
|
-
toolUseId: "toolu_1",
|
|
31
|
-
toolResultContent:
|
|
32
|
-
"Async agent launched successfully.\n" +
|
|
33
|
-
"agentId: abc-1 (something)\n" +
|
|
34
|
-
"output_file: /tmp/out-abc-1.jsonl\n" +
|
|
35
|
-
"If asked, you can check progress.",
|
|
36
|
-
};
|
|
37
|
-
handleToolResultChunk(chunk, {
|
|
38
|
-
chatId: 42,
|
|
39
|
-
userId: 99,
|
|
40
|
-
lastToolUseInput: {
|
|
41
|
-
description: "SEO audit",
|
|
42
|
-
prompt: "audit example.com",
|
|
43
|
-
},
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
expect(registered).toHaveLength(1);
|
|
47
|
-
const r = registered[0] as { agentId: string; description: string; outputFile: string };
|
|
48
|
-
expect(r.agentId).toBe("abc-1");
|
|
49
|
-
expect(r.description).toBe("SEO audit");
|
|
50
|
-
expect(r.outputFile).toBe("/tmp/out-abc-1.jsonl");
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("v4.12.3 — passes sessionKey to registerPendingAgent and increments session counter", async () => {
|
|
54
|
-
const registered: Array<{ sessionKey?: string }> = [];
|
|
55
|
-
vi.doMock("../src/services/async-agent-watcher.js", () => ({
|
|
56
|
-
registerPendingAgent: (input: { sessionKey?: string }) =>
|
|
57
|
-
registered.push(input),
|
|
58
|
-
startWatcher: () => {},
|
|
59
|
-
stopWatcher: () => {},
|
|
60
|
-
pollOnce: async () => {},
|
|
61
|
-
listPendingAgents: () => [],
|
|
62
|
-
}));
|
|
63
|
-
|
|
64
|
-
const { getSession } = await import("../src/services/session.js");
|
|
65
|
-
const session = getSession("v412-chunk-test-session");
|
|
66
|
-
session.pendingBackgroundCount = 0;
|
|
67
|
-
|
|
68
|
-
const { handleToolResultChunk } = await import(
|
|
69
|
-
"../src/handlers/async-agent-chunk-handler.js"
|
|
70
|
-
);
|
|
71
|
-
handleToolResultChunk(
|
|
72
|
-
{
|
|
73
|
-
type: "tool_result",
|
|
74
|
-
toolUseId: "toolu_sess",
|
|
75
|
-
toolResultContent:
|
|
76
|
-
"Async agent launched successfully.\n" +
|
|
77
|
-
"agentId: ag-sess\n" +
|
|
78
|
-
"output_file: /tmp/ag-sess.jsonl\n",
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
chatId: 10,
|
|
82
|
-
userId: 20,
|
|
83
|
-
sessionKey: "v412-chunk-test-session",
|
|
84
|
-
lastToolUseInput: { description: "SEO", prompt: "do it" },
|
|
85
|
-
},
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
expect(registered).toHaveLength(1);
|
|
89
|
-
expect(registered[0].sessionKey).toBe("v412-chunk-test-session");
|
|
90
|
-
expect(session.pendingBackgroundCount).toBe(1);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("v4.12.3 — multiple async launches in same turn stack the counter", async () => {
|
|
94
|
-
vi.doMock("../src/services/async-agent-watcher.js", () => ({
|
|
95
|
-
registerPendingAgent: () => {},
|
|
96
|
-
startWatcher: () => {},
|
|
97
|
-
stopWatcher: () => {},
|
|
98
|
-
pollOnce: async () => {},
|
|
99
|
-
listPendingAgents: () => [],
|
|
100
|
-
}));
|
|
101
|
-
|
|
102
|
-
const { getSession } = await import("../src/services/session.js");
|
|
103
|
-
const session = getSession("v412-chunk-stack");
|
|
104
|
-
session.pendingBackgroundCount = 0;
|
|
105
|
-
|
|
106
|
-
const { handleToolResultChunk } = await import(
|
|
107
|
-
"../src/handlers/async-agent-chunk-handler.js"
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
for (let i = 0; i < 3; i++) {
|
|
111
|
-
handleToolResultChunk(
|
|
112
|
-
{
|
|
113
|
-
type: "tool_result",
|
|
114
|
-
toolUseId: `toolu_${i}`,
|
|
115
|
-
toolResultContent:
|
|
116
|
-
`Async agent launched successfully.\n` +
|
|
117
|
-
`agentId: ag-${i}\n` +
|
|
118
|
-
`output_file: /tmp/ag-${i}.jsonl\n`,
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
chatId: 10,
|
|
122
|
-
userId: 20,
|
|
123
|
-
sessionKey: "v412-chunk-stack",
|
|
124
|
-
lastToolUseInput: { description: `task ${i}`, prompt: "p" },
|
|
125
|
-
},
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
expect(session.pendingBackgroundCount).toBe(3);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("v4.12.3 — non-async tool_result does not increment the counter", async () => {
|
|
133
|
-
vi.doMock("../src/services/async-agent-watcher.js", () => ({
|
|
134
|
-
registerPendingAgent: () => {
|
|
135
|
-
throw new Error("should not be called");
|
|
136
|
-
},
|
|
137
|
-
startWatcher: () => {},
|
|
138
|
-
stopWatcher: () => {},
|
|
139
|
-
pollOnce: async () => {},
|
|
140
|
-
listPendingAgents: () => [],
|
|
141
|
-
}));
|
|
142
|
-
|
|
143
|
-
const { getSession } = await import("../src/services/session.js");
|
|
144
|
-
const session = getSession("v412-chunk-nonasync");
|
|
145
|
-
session.pendingBackgroundCount = 0;
|
|
146
|
-
|
|
147
|
-
const { handleToolResultChunk } = await import(
|
|
148
|
-
"../src/handlers/async-agent-chunk-handler.js"
|
|
149
|
-
);
|
|
150
|
-
handleToolResultChunk(
|
|
151
|
-
{
|
|
152
|
-
type: "tool_result",
|
|
153
|
-
toolUseId: "toolu_read",
|
|
154
|
-
toolResultContent: "plain read result — no async_launched marker",
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
chatId: 1,
|
|
158
|
-
userId: 1,
|
|
159
|
-
sessionKey: "v412-chunk-nonasync",
|
|
160
|
-
lastToolUseInput: { description: "read", prompt: "p" },
|
|
161
|
-
},
|
|
162
|
-
);
|
|
163
|
-
expect(session.pendingBackgroundCount).toBe(0);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("falls back to a generic description when no toolUseInput is provided", async () => {
|
|
167
|
-
const registered: unknown[] = [];
|
|
168
|
-
vi.doMock("../src/services/async-agent-watcher.js", () => ({
|
|
169
|
-
registerPendingAgent: (input: unknown) => registered.push(input),
|
|
170
|
-
startWatcher: () => {},
|
|
171
|
-
stopWatcher: () => {},
|
|
172
|
-
pollOnce: async () => {},
|
|
173
|
-
listPendingAgents: () => [],
|
|
174
|
-
}));
|
|
175
|
-
|
|
176
|
-
const { handleToolResultChunk } = await import(
|
|
177
|
-
"../src/handlers/async-agent-chunk-handler.js"
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
handleToolResultChunk(
|
|
181
|
-
{
|
|
182
|
-
type: "tool_result",
|
|
183
|
-
toolUseId: "toolu_2",
|
|
184
|
-
toolResultContent:
|
|
185
|
-
"Async agent launched successfully.\n" +
|
|
186
|
-
"agentId: x\n" +
|
|
187
|
-
"output_file: /tmp/o\n",
|
|
188
|
-
},
|
|
189
|
-
{ chatId: 42, userId: 99 },
|
|
190
|
-
);
|
|
191
|
-
|
|
192
|
-
expect(registered).toHaveLength(1);
|
|
193
|
-
const r = registered[0] as { description: string };
|
|
194
|
-
expect(r.description.length).toBeGreaterThan(0);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it("non-async tool_result (e.g. Read) is ignored", async () => {
|
|
198
|
-
const registered: unknown[] = [];
|
|
199
|
-
vi.doMock("../src/services/async-agent-watcher.js", () => ({
|
|
200
|
-
registerPendingAgent: (input: unknown) => registered.push(input),
|
|
201
|
-
startWatcher: () => {},
|
|
202
|
-
stopWatcher: () => {},
|
|
203
|
-
pollOnce: async () => {},
|
|
204
|
-
listPendingAgents: () => [],
|
|
205
|
-
}));
|
|
206
|
-
|
|
207
|
-
const { handleToolResultChunk } = await import(
|
|
208
|
-
"../src/handlers/async-agent-chunk-handler.js"
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
handleToolResultChunk(
|
|
212
|
-
{
|
|
213
|
-
type: "tool_result",
|
|
214
|
-
toolUseId: "toolu_3",
|
|
215
|
-
toolResultContent: "file contents here (plain Read result)",
|
|
216
|
-
},
|
|
217
|
-
{ chatId: 42, userId: 99 },
|
|
218
|
-
);
|
|
219
|
-
expect(registered).toHaveLength(0);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it("non-tool_result chunks are ignored without throwing", async () => {
|
|
223
|
-
vi.doMock("../src/services/async-agent-watcher.js", () => ({
|
|
224
|
-
registerPendingAgent: () => {
|
|
225
|
-
throw new Error("should not be called");
|
|
226
|
-
},
|
|
227
|
-
startWatcher: () => {},
|
|
228
|
-
stopWatcher: () => {},
|
|
229
|
-
pollOnce: async () => {},
|
|
230
|
-
listPendingAgents: () => [],
|
|
231
|
-
}));
|
|
232
|
-
|
|
233
|
-
const { handleToolResultChunk } = await import(
|
|
234
|
-
"../src/handlers/async-agent-chunk-handler.js"
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
expect(() =>
|
|
238
|
-
handleToolResultChunk(
|
|
239
|
-
{ type: "text", text: "hi" },
|
|
240
|
-
{ chatId: 42, userId: 99 },
|
|
241
|
-
),
|
|
242
|
-
).not.toThrow();
|
|
243
|
-
});
|
|
244
|
-
});
|