alvin-bot 4.18.0 → 4.18.2

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.
Files changed (74) hide show
  1. package/AEC-PLUGINS-SOURCES.md +53 -0
  2. package/CHANGELOG.md +37 -2
  3. package/DESIGN-SKILLS-SOURCES.md +81 -0
  4. package/bin/cli.js +1 -1
  5. package/dist/providers/claude-sdk-provider.js +24 -0
  6. package/package.json +3 -1
  7. package/test/allowed-users-gate.test.ts +0 -98
  8. package/test/alvin-dispatch.test.ts +0 -220
  9. package/test/async-agent-chunk-flow.test.ts +0 -244
  10. package/test/async-agent-parser-staleness.test.ts +0 -412
  11. package/test/async-agent-parser-streamjson.test.ts +0 -273
  12. package/test/async-agent-parser.test.ts +0 -322
  13. package/test/async-agent-watcher.test.ts +0 -229
  14. package/test/background-bypass-integration.test.ts +0 -443
  15. package/test/background-bypass-stress.test.ts +0 -417
  16. package/test/background-bypass.test.ts +0 -127
  17. package/test/browser-webfetch.test.ts +0 -121
  18. package/test/claude-sdk-provider.test.ts +0 -115
  19. package/test/claude-sdk-tool-use-id.test.ts +0 -180
  20. package/test/console-timestamps.test.ts +0 -98
  21. package/test/cron-progress-ticker.test.ts +0 -76
  22. package/test/cron-restart-resilience.test.ts +0 -191
  23. package/test/cron-run-resolver.test.ts +0 -133
  24. package/test/cron-runjobnow-throw.test.ts +0 -100
  25. package/test/debounce.test.ts +0 -60
  26. package/test/delivery-registry.test.ts +0 -71
  27. package/test/exec-guard-metachars.test.ts +0 -110
  28. package/test/file-permissions.test.ts +0 -130
  29. package/test/i18n.test.ts +0 -108
  30. package/test/list-subagents-merged.test.ts +0 -172
  31. package/test/memory-extractor.test.ts +0 -151
  32. package/test/memory-layers.test.ts +0 -169
  33. package/test/memory-sdk-injection.test.ts +0 -146
  34. package/test/memory-stress-restart.test.ts +0 -337
  35. package/test/multi-session-stress.test.ts +0 -255
  36. package/test/platform-session-key.test.ts +0 -69
  37. package/test/process-manager.test.ts +0 -186
  38. package/test/registry.test.ts +0 -201
  39. package/test/session-pending-background.test.ts +0 -59
  40. package/test/session-persistence.test.ts +0 -195
  41. package/test/slack-progress-ticker.test.ts +0 -123
  42. package/test/slack-slash-command.test.ts +0 -61
  43. package/test/slack-test-connection.test.ts +0 -176
  44. package/test/stress-scenarios.test.ts +0 -356
  45. package/test/stuck-timer.test.ts +0 -116
  46. package/test/subagent-delivery-markdown-fallback.test.ts +0 -147
  47. package/test/subagent-delivery-platform-routing.test.ts +0 -232
  48. package/test/subagent-delivery.test.ts +0 -273
  49. package/test/subagent-final-text.test.ts +0 -132
  50. package/test/subagent-stats.test.ts +0 -119
  51. package/test/subagent-toolset-allowlist.test.ts +0 -146
  52. package/test/subagents-commands.test.ts +0 -64
  53. package/test/subagents-config.test.ts +0 -114
  54. package/test/subagents-depth.test.ts +0 -58
  55. package/test/subagents-inheritance.test.ts +0 -67
  56. package/test/subagents-name-resolver.test.ts +0 -122
  57. package/test/subagents-priority-reject.test.ts +0 -88
  58. package/test/subagents-queue.test.ts +0 -127
  59. package/test/subagents-shutdown.test.ts +0 -126
  60. package/test/subagents-toolset.test.ts +0 -71
  61. package/test/sync-task-timeout.test.ts +0 -153
  62. package/test/system-prompt-background-hint.test.ts +0 -65
  63. package/test/telegram-error-filter.test.ts +0 -85
  64. package/test/telegram-workspace-command.test.ts +0 -78
  65. package/test/timing-safe-bearer.test.ts +0 -65
  66. package/test/watchdog-brake.test.ts +0 -157
  67. package/test/watcher-pending-count.test.ts +0 -228
  68. package/test/watcher-zombie-fix.test.ts +0 -252
  69. package/test/web-server-integration.test.ts +0 -189
  70. package/test/web-server-resilience.test.ts +0 -118
  71. package/test/web-server-shutdown.test.ts +0 -117
  72. package/test/whatsapp-auth-resilience.test.ts +0 -96
  73. package/test/workspaces.test.ts +0 -196
  74. package/vitest.config.ts +0 -17
@@ -0,0 +1,53 @@
1
+ # AEC Plugin Skills — Installed 22.04.2026
2
+
3
+ 54 Skills aus 3 Claude-Code-Plugins, erstellt von **Abhinav Bhardwaj** (GitHub: `Amanbh997`).
4
+ Installiert via direkte Kopie in `~/.claude/skills/` — Claude Code auto-discovered via SKILL.md frontmatter.
5
+
6
+ Vorher: 89 Skills. Nachher: **143 Skills** (+54).
7
+
8
+ ## Quellen
9
+
10
+ | Plugin | Repo | Skills | Stars |
11
+ |---|---|---|---|
12
+ | **Urban Design** | [Amanbh997/Urban-Design-Skills-Claude](https://github.com/Amanbh997/Urban-Design-Skills-Claude) | 18 | ⭐ 76 |
13
+ | **Architecture** | [Amanbh997/Skills-Architects](https://github.com/Amanbh997/Skills-Architects) | 18 | ⭐ 118 |
14
+ | **Computational Design** | [Amanbh997/Claude-skills-for-Computational-Designers](https://github.com/Amanbh997/Claude-skills-for-Computational-Designers) | 18 | ⭐ 127 |
15
+
16
+ Jedes Plugin: 35.000+ Zeilen, 7 Python-Calculators, 50+ Theorists, 30+ Tools, hunderte numerische Benchmarks.
17
+
18
+ ## Skill-Inventar
19
+
20
+ ### Urban Design (18)
21
+ block-and-density · climate-responsive-design · cost-estimation · design-brief · design-evaluation · masterplan-design · mixed-use-programming · mobility-and-transport · precedent-study · public-space-design · site-analysis · street-design · sustainability-scoring · tod-design · urban-calculator · urban-design-foundations · urban-regeneration · zoning-and-codes
22
+
23
+ ### Architecture (18)
24
+ accessibility-design · acoustic-design · architect-calculator · architect-foundations · building-codes · building-envelope · building-programming · building-services · building-sustainability · building-typology · concept-design · construction-documentation · daylighting-design · design-theory · fire-life-safety · material-selection · spatial-planning · structural-systems
25
+
26
+ ### Computational Design (18)
27
+ algorithmic-patterns · bim-scripting · cd-calculator · cd-foundations · computational-geometry · data-driven-design · design-automation · digital-fabrication · environmental-simulation · facade-computation · generative-design · interoperability · mesh-processing · ml-for-aec · optimization-methods · parametric-modeling · scripting-reference · structural-computation
28
+
29
+ ## Update ziehen
30
+
31
+ ```bash
32
+ cd /tmp && rm -rf aec-skills && mkdir aec-skills && cd aec-skills
33
+ git clone --depth 1 https://github.com/Amanbh997/Urban-Design-Skills-Claude.git
34
+ git clone --depth 1 https://github.com/Amanbh997/Skills-Architects.git
35
+ git clone --depth 1 https://github.com/Amanbh997/Claude-skills-for-Computational-Designers.git
36
+ cp -R Urban-Design-Skills-Claude/skills/* ~/.claude/skills/
37
+ cp -R Skills-Architects/skills/* ~/.claude/skills/
38
+ cp -R Claude-skills-for-Computational-Designers/skills/* ~/.claude/skills/
39
+ ```
40
+
41
+ ## Wie die Skills ausgelöst werden
42
+
43
+ Jeder Skill hat im SKILL.md frontmatter eine `description`-Zeile die spezifische User-Anfragen matcht. Beispiele:
44
+
45
+ - „Design a mixed-use quarter" → Urban Design Plugin (masterplan-design, tod-design, mixed-use-programming, zoning-and-codes, mobility-and-transport …)
46
+ - „Check if this floor plan meets fire code" → Architecture Plugin (fire-life-safety, building-codes, accessibility-design, construction-documentation …)
47
+ - „Generate a parametric facade pattern in Grasshopper" → Computational Design Plugin (parametric-modeling, facade-computation, algorithmic-patterns, digital-fabrication …)
48
+
49
+ Laut Creator: *"Ask Claude to design a mixed-use quarter and all three plugins kick in working together."*
50
+
51
+ ## Quelle
52
+ Reel vom Creator: https://www.instagram.com/reel/DXXhJS2kqAf/
53
+ Uploader: Abhinav Bhardwaj / "Claude for AEC!"
package/CHANGELOG.md CHANGED
@@ -2,6 +2,41 @@
2
2
 
3
3
  All notable changes to Alvin Bot are documented here.
4
4
 
5
+ ## [4.18.2] — 2026-04-23
6
+
7
+ ### 🐛 Fix: silent empty-stream after OAuth-token rotation
8
+
9
+ **Problem:** After running `/extra-usage`, `/login`, or any other flow that rotates the Claude OAuth token in the macOS Keychain, the Alvin-Bot silently broke for long-lived sessions. The in-memory Claude SDK client held the old token, the CLI subprocess emitted no text chunks (the 401 was swallowed upstream), the stream terminated normally with zero output tokens, and the user saw the fallback `"(Keine Antwort)"` — with no indication that a token refresh was needed.
10
+
11
+ **Fix** (`src/providers/claude-sdk-provider.ts`): In the `result` branch of the SDK stream loop, detect the empty-stream signature (`accumulatedText === ""` and `outputTokens === 0`). When that fires:
12
+
13
+ 1. Invalidate the `isAvailable()` cache so the next heartbeat probe spawns a fresh CLI subprocess that reads the current Keychain entry.
14
+ 2. Yield an explicit `error` chunk with actionable text so the user sees *"…token rotation — please resend your message"* instead of a silent `"(Keine Antwort)"`.
15
+
16
+ **Applies to:** every token-rotation flow — extra-usage activation, extra-usage expiry, weekly-reset (no rotation → unaffected), manual `claude login`.
17
+
18
+ **Net effect:** Bot self-heals after token changes. A single resend on the user side is enough; no manual restart required.
19
+
20
+ ## [4.18.1] — 2026-04-20
21
+
22
+ ### 🔒 Privacy-Guard: pre-publish check blocks PII leaks in shipped files
23
+
24
+ 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.
25
+
26
+ **New:**
27
+ - `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.
28
+ - `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.
29
+ - `package.json` `prepublishOnly` hook — runs the check automatically.
30
+ - `npm run privacy-check` — manual run anytime.
31
+
32
+ **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.
33
+
34
+ **CI override:** Set `$ALVIN_PRIVACY_PATTERNS` to an absolute path; takes top precedence over both files above.
35
+
36
+ **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.
37
+
38
+ **CLAUDE.md** — documents the rule and the patterns-file lookup order so future maintenance sessions catch new cases proactively.
39
+
5
40
  ## [4.18.0] — 2026-04-20
6
41
 
7
42
  ### ⚡ Performance + Hardening: medium-priority cleanups from the stability audit
@@ -169,8 +204,8 @@ The three aliased entries all route through `ClaudeSDKProvider` with different `
169
204
 
170
205
  ```yaml
171
206
  ---
172
- purpose: Interview prep
173
- cwd: ~/Documents/Interviews
207
+ purpose: my-project
208
+ cwd: ~/Projects/my-project
174
209
  model: sonnet # opus | sonnet | haiku | claude-opus-4-7 | ...
175
210
  ---
176
211
  ```
@@ -0,0 +1,81 @@
1
+ # Design Skills — Installed 22.04.2026
2
+
3
+ 19 neue Skills aus 2 Repos + Bestätigung dass Frontend Design bereits via Anthropic Plugin installiert ist.
4
+
5
+ ## Quellen
6
+
7
+ | Skill | Repo | Stars | Status |
8
+ |---|---|---|---|
9
+ | **Impeccable** (18 commands) | [pbakaus/impeccable](https://github.com/pbakaus/impeccable) | ⭐ 21.419 | ✅ Neu installiert |
10
+ | **Design Motion Principles** | [kylezantos/design-motion-principles](https://github.com/kylezantos/design-motion-principles) | ⭐ 293 | ✅ Neu installiert |
11
+ | **Frontend Design** | [anthropics/skills/frontend-design](https://github.com/anthropics/skills) | — | ℹ️ War bereits aktiv (Plugin `frontend-design:frontend-design`) |
12
+
13
+ ## Reel-Quelle
14
+ Marc Cleroux auf Instagram — https://www.instagram.com/reel/DXdVUiOjKUv/ (22.04.2026)
15
+
16
+ ## Impeccable-Commands (18)
17
+
18
+ Vokabular um Claude bei frontend-design zu steuern wenn dir die richtigen Worte fehlen.
19
+
20
+ | Command | Funktion |
21
+ |---|---|
22
+ | `/impeccable` | Core skill mit 18 commands + 7 Referenz-Domänen |
23
+ | `/adapt` | Responsive / Breakpoints / Touch-Targets |
24
+ | `/animate` | Purposeful motion + micro-interactions |
25
+ | `/audit` | Technische Quality Checks (a11y, perf, responsive) + scored report |
26
+ | `/bolder` | Bland → visuell interessanter |
27
+ | `/clarify` | Unklare UX-Copy verbessern |
28
+ | `/colorize` | Strategische Farbe einführen |
29
+ | `/critique` | UX Design Review (Hierarchie, Klarheit, Resonanz) |
30
+ | `/delight` | Joy-Momente, Personality, Memorable Touches |
31
+ | `/distill` | Auf Essenz reduzieren, Complexity weg |
32
+ | `/harden` | Error Handling, Empty States, Onboarding, i18n, Edge Cases |
33
+ | `/layout` | Layout / Spacing / Visual Rhythm fixen |
34
+ | `/optimize` | Performance (Bundle, Rendering, Images) |
35
+ | `/overdrive` | Technisch ambitioniert — Shaders, Spring Physics, 60fps |
36
+ | `/polish` | Final Quality Pass pre-shipping |
37
+ | `/quieter` | Overstimulating Design beruhigen |
38
+ | `/shape` | UX/UI-Plan vor Code |
39
+ | `/typeset` | Typografie fixen |
40
+
41
+ ### 7 Referenz-Domänen in `impeccable`
42
+ typography · color-and-contrast · spatial-design · motion-design · interaction-design · responsive-design · ux-writing
43
+
44
+ ## Design Motion Principles
45
+
46
+ Motion-Audit-Skill basierend auf den Philosophien von:
47
+ - **Emil Kowalski** — iOS-native UI polish
48
+ - **Jakub Krehel** — smooth interaction design
49
+ - **Jhey Tompkins** — creative web animation
50
+
51
+ Use case: *"Audit the hover states and transitions on my landing page"* → strukturierter Motion-Report.
52
+
53
+ ## Installation
54
+
55
+ Geklont + direkt in `~/.claude/skills/` einzeln kopiert (Impeccable folgt dem Pattern aus ihrer README: `cp -r dist/claude-code/.claude/* ~/.claude/`).
56
+
57
+ ## Update ziehen
58
+
59
+ ```bash
60
+ cd /tmp && rm -rf design-skills && mkdir design-skills && cd design-skills
61
+ git clone --depth 1 https://github.com/pbakaus/impeccable.git
62
+ git clone --depth 1 https://github.com/kylezantos/design-motion-principles.git
63
+ cp -R impeccable/.claude/skills/. ~/.claude/skills/
64
+ cp -R design-motion-principles/skills/design-motion-principles ~/.claude/skills/
65
+ ```
66
+
67
+ ## Stand Skills-Inventar (22.04.2026)
68
+ - Vorher: 143 (nach AEC-Installation)
69
+ - Nachher: **162** (+19)
70
+
71
+ ## Nebenbaustelle
72
+ Impeccable bringt zusätzlich eine `.claude/agents/anti-patterns.md` mit — nicht installiert (würde Permission für `~/.claude/agents/` brauchen). Das ist optional: Die 18 Skills funktionieren unabhängig vom Anti-Patterns-Agent. Falls später gewünscht: `cp -R /tmp/design-skills/impeccable/.claude/agents/. ~/.claude/agents/`
73
+
74
+ ## Empfohlene Workflow-Kombination
75
+
76
+ Laut Marc Cleroux im Reel ist das Power-Trio:
77
+ 1. **shape** (plan before code) — Teil von Impeccable
78
+ 2. **frontend-design** (implement mit Geschmack) — schon installiert
79
+ 3. **audit** + **critique** + **polish** (iterieren) — Impeccable
80
+
81
+ Bei jedem neuen Frontend-Build: `/shape` → `/impeccable craft` → `/audit` → `/critique` → `/polish` → shippen.
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 "cover letter"');
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");
@@ -309,6 +309,30 @@ export class ClaudeSDKProvider {
309
309
  ? (usage.input_tokens || 0) + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0)
310
310
  : 0;
311
311
  const outputTok = usage?.output_tokens || 0;
312
+ // v4.18.2 — Silent-empty-stream detection.
313
+ //
314
+ // If the stream terminated cleanly but produced ZERO text chunks,
315
+ // something went wrong that the SDK didn't surface as an error:
316
+ // most commonly a stale OAuth token after /extra-usage or /login
317
+ // rotated the Keychain entry while our in-memory SDK client was
318
+ // still holding the old one. The CLI subprocess silently gets a
319
+ // 401, emits no text, and we complete the stream with
320
+ // accumulatedText === "". The user sees "(Keine Antwort)".
321
+ //
322
+ // We flip this from silent failure to explicit error. Clearing
323
+ // the availability cache forces the next heartbeat probe to
324
+ // re-check `claude auth status` with a fresh subprocess (which
325
+ // reads the current Keychain entry).
326
+ if (accumulatedText === "" && outputTok === 0) {
327
+ this.invalidateAvailabilityCache();
328
+ yield {
329
+ type: "error",
330
+ error: "Claude returned an empty response. " +
331
+ "This can happen right after /extra-usage, /login, or a token refresh — " +
332
+ "the SDK held a stale auth token. I've invalidated the cache; please resend your message.",
333
+ };
334
+ return;
335
+ }
312
336
  yield {
313
337
  type: "done",
314
338
  text: accumulatedText,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alvin-bot",
3
- "version": "4.18.0",
3
+ "version": "4.18.2",
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
- });