vellum 0.2.1 → 0.2.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.
- package/README.md +15 -2
- package/bun.lock +5 -2
- package/package.json +4 -2
- package/scripts/capture-x-graphql.ts +562 -0
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -1
- package/scripts/test.sh +5 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +133 -34
- package/src/__tests__/account-registry.test.ts +2 -1
- package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
- package/src/__tests__/asset-materialize-tool.test.ts +16 -15
- package/src/__tests__/asset-search-tool.test.ts +23 -22
- package/src/__tests__/attachments-store.test.ts +56 -127
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
- package/src/__tests__/browser-skill-endstate.test.ts +4 -3
- package/src/__tests__/call-bridge.test.ts +385 -0
- package/src/__tests__/call-constants.test.ts +40 -0
- package/src/__tests__/call-orchestrator.test.ts +130 -4
- package/src/__tests__/call-recovery.test.ts +518 -0
- package/src/__tests__/call-routes-http.test.ts +459 -0
- package/src/__tests__/call-state-machine.test.ts +143 -0
- package/src/__tests__/call-store.test.ts +216 -1
- package/src/__tests__/cli-discover.test.ts +1 -1
- package/src/__tests__/commit-message-enrichment-service.test.ts +148 -7
- package/src/__tests__/compaction.benchmark.test.ts +176 -0
- package/src/__tests__/computer-use-tools.test.ts +250 -0
- package/src/__tests__/config-schema.test.ts +299 -3
- package/src/__tests__/conflict-store.test.ts +2 -1
- package/src/__tests__/contacts-tools.test.ts +331 -0
- package/src/__tests__/conversation-store.test.ts +30 -32
- package/src/__tests__/credential-security-invariants.test.ts +4 -0
- package/src/__tests__/date-context.test.ts +373 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
- package/src/__tests__/followup-tools.test.ts +303 -0
- package/src/__tests__/handlers-twitter-config.test.ts +718 -0
- package/src/__tests__/intent-routing.test.ts +64 -57
- package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
- package/src/__tests__/ipc-snapshot.test.ts +62 -28
- package/src/__tests__/llm-usage-store.test.ts +3 -8
- package/src/__tests__/media-generate-image.test.ts +1 -1
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
- package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
- package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
- package/src/__tests__/playbook-tools.test.ts +342 -0
- package/src/__tests__/profile-compiler.test.ts +2 -1
- package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
- package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
- package/src/__tests__/recurrence-engine.test.ts +69 -0
- package/src/__tests__/recurrence-types.test.ts +71 -0
- package/src/__tests__/registry.test.ts +5 -3
- package/src/__tests__/relay-server.test.ts +633 -0
- package/src/__tests__/reminder-store.test.ts +6 -3
- package/src/__tests__/reminder.test.ts +43 -77
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +8 -4
- package/src/__tests__/run-orchestrator.test.ts +4 -4
- package/src/__tests__/runtime-attachment-metadata.test.ts +7 -6
- package/src/__tests__/runtime-runs-http.test.ts +4 -4
- package/src/__tests__/runtime-runs.test.ts +4 -4
- package/src/__tests__/schedule-store.test.ts +482 -0
- package/src/__tests__/schedule-tools.test.ts +700 -0
- package/src/__tests__/scheduler-recurrence.test.ts +329 -0
- package/src/__tests__/server-history-render.test.ts +14 -13
- package/src/__tests__/session-error.test.ts +28 -0
- package/src/__tests__/session-init.benchmark.test.ts +462 -0
- package/src/__tests__/session-queue.test.ts +71 -48
- package/src/__tests__/session-runtime-assembly.test.ts +161 -0
- package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
- package/src/__tests__/signup-e2e.test.ts +2 -1
- package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
- package/src/__tests__/skill-script-runner.test.ts +159 -0
- package/src/__tests__/speaker-identification.test.ts +52 -0
- package/src/__tests__/subagent-manager-notify.test.ts +42 -10
- package/src/__tests__/subagent-tools.test.ts +141 -41
- package/src/__tests__/task-compiler.test.ts +2 -1
- package/src/__tests__/task-runner.test.ts +2 -1
- package/src/__tests__/task-scheduler.test.ts +2 -1
- package/src/__tests__/task-tools.test.ts +49 -56
- package/src/__tests__/tool-audit-listener.test.ts +1 -0
- package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
- package/src/__tests__/tool-executor.test.ts +13 -17
- package/src/__tests__/turn-commit.test.ts +218 -3
- package/src/__tests__/twilio-provider.test.ts +143 -0
- package/src/__tests__/twilio-routes.test.ts +789 -0
- package/src/__tests__/twitter-auth-handler.test.ts +581 -0
- package/src/__tests__/view-image-tool.test.ts +217 -0
- package/src/__tests__/workspace-git-service.test.ts +186 -0
- package/src/__tests__/workspace-heartbeat-service.test.ts +13 -3
- package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
- package/src/bundler/app-bundler.ts +12 -8
- package/src/calls/call-bridge.ts +95 -0
- package/src/calls/call-constants.ts +43 -5
- package/src/calls/call-domain.ts +276 -0
- package/src/calls/call-orchestrator.ts +43 -17
- package/src/calls/call-recovery.ts +207 -0
- package/src/calls/call-state-machine.ts +68 -0
- package/src/calls/call-store.ts +192 -5
- package/src/calls/relay-server.ts +41 -4
- package/src/calls/speaker-identification.ts +213 -0
- package/src/calls/twilio-provider.ts +10 -6
- package/src/calls/twilio-routes.ts +90 -76
- package/src/calls/types.ts +1 -1
- package/src/cli/config-commands.ts +334 -0
- package/src/cli/core-commands.ts +776 -0
- package/src/cli/doordash.ts +251 -1
- package/src/cli/ipc-client.ts +82 -0
- package/src/cli/map.ts +246 -0
- package/src/cli/twitter.ts +575 -0
- package/src/cli.ts +7 -5
- package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
- package/src/commands/cc-command-registry.ts +209 -0
- package/src/config/bundled-skills/contacts/SKILL.md +39 -0
- package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
- package/src/config/bundled-skills/document/SKILL.md +18 -0
- package/src/config/bundled-skills/document/TOOLS.json +53 -0
- package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
- package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
- package/src/config/bundled-skills/doordash/SKILL.md +82 -23
- package/src/config/bundled-skills/followups/SKILL.md +32 -0
- package/src/config/bundled-skills/followups/TOOLS.json +100 -0
- package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +1 -23
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
- package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
- package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
- package/src/config/bundled-skills/reminder/SKILL.md +20 -0
- package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
- package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
- package/src/config/bundled-skills/schedule/SKILL.md +74 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
- package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
- package/src/config/bundled-skills/subagent/SKILL.md +25 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
- package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
- package/src/config/bundled-skills/tasks/SKILL.md +28 -0
- package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
- package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
- package/src/config/bundled-skills/twitter/SKILL.md +134 -0
- package/src/config/bundled-skills/watcher/SKILL.md +27 -0
- package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
- package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
- package/src/config/defaults.ts +33 -0
- package/src/config/loader.ts +4 -1
- package/src/config/schema.ts +161 -1
- package/src/config/system-prompt.ts +61 -16
- package/src/config/templates/IDENTITY.md +7 -0
- package/src/config/types.ts +4 -0
- package/src/contacts/contact-store.ts +4 -4
- package/src/daemon/assistant-attachments.ts +10 -0
- package/src/daemon/classifier.ts +3 -1
- package/src/daemon/computer-use-session.ts +3 -1
- package/src/daemon/date-context.ts +136 -0
- package/src/daemon/handlers/apps.ts +16 -1
- package/src/daemon/handlers/browser.ts +54 -0
- package/src/daemon/handlers/computer-use.ts +7 -1
- package/src/daemon/handlers/config.ts +163 -5
- package/src/daemon/handlers/diagnostics.ts +5 -1
- package/src/daemon/handlers/documents.ts +18 -29
- package/src/daemon/handlers/home-base.ts +5 -1
- package/src/daemon/handlers/index.ts +40 -277
- package/src/daemon/handlers/misc.ts +9 -1
- package/src/daemon/handlers/publish.ts +6 -1
- package/src/daemon/handlers/sessions.ts +65 -12
- package/src/daemon/handlers/shared.ts +36 -1
- package/src/daemon/handlers/signing.ts +37 -0
- package/src/daemon/handlers/skills.ts +20 -6
- package/src/daemon/handlers/subagents.ts +8 -3
- package/src/daemon/handlers/twitter-auth.ts +169 -0
- package/src/daemon/handlers/work-items.ts +384 -68
- package/src/daemon/ipc-contract-inventory.json +28 -4
- package/src/daemon/ipc-contract.ts +133 -37
- package/src/daemon/ipc-protocol.ts +7 -2
- package/src/daemon/lifecycle.ts +21 -0
- package/src/daemon/main.ts +10 -4
- package/src/daemon/ride-shotgun-handler.ts +74 -10
- package/src/daemon/server.ts +143 -26
- package/src/daemon/session-agent-loop.ts +887 -0
- package/src/daemon/session-attachments.ts +28 -5
- package/src/daemon/session-error.ts +24 -3
- package/src/daemon/session-lifecycle.ts +147 -0
- package/src/daemon/session-media-retry.ts +147 -0
- package/src/daemon/session-messaging.ts +145 -0
- package/src/daemon/session-notifiers.ts +164 -0
- package/src/daemon/session-process.ts +2 -2
- package/src/daemon/session-queue-manager.ts +1 -0
- package/src/daemon/session-runtime-assembly.ts +52 -0
- package/src/daemon/session-skill-tools.ts +124 -5
- package/src/daemon/session-slash.ts +3 -0
- package/src/daemon/session-surfaces.ts +77 -2
- package/src/daemon/session-tool-setup.ts +216 -2
- package/src/daemon/session-usage.ts +0 -2
- package/src/daemon/session.ts +114 -1404
- package/src/daemon/video-thumbnail.ts +60 -0
- package/src/doordash/client.ts +121 -27
- package/src/doordash/queries.ts +1 -2
- package/src/export/formatter.ts +3 -1
- package/src/followups/followup-store.ts +4 -2
- package/src/followups/types.ts +6 -0
- package/src/hooks/templates.ts +1 -1
- package/src/index.ts +32 -1153
- package/src/memory/attachments-store.ts +28 -83
- package/src/memory/channel-delivery-store.ts +7 -21
- package/src/memory/clarification-resolver.ts +6 -5
- package/src/memory/contradiction-checker.ts +3 -2
- package/src/memory/conversation-key-store.ts +10 -29
- package/src/memory/conversation-store.ts +2 -1
- package/src/memory/db.ts +96 -2
- package/src/memory/entity-extractor.ts +6 -3
- package/src/memory/items-extractor.ts +5 -4
- package/src/memory/jobs-store.ts +3 -2
- package/src/memory/llm-usage-store.ts +1 -2
- package/src/memory/runs-store.ts +1 -2
- package/src/memory/schema.ts +23 -2
- package/src/messaging/style-analyzer.ts +3 -2
- package/src/messaging/thread-summarizer.ts +8 -12
- package/src/messaging/triage-engine.ts +4 -2
- package/src/providers/openrouter/client.ts +20 -0
- package/src/providers/registry.ts +8 -0
- package/src/runtime/http-server.ts +108 -20
- package/src/runtime/routes/attachment-routes.ts +2 -3
- package/src/runtime/routes/call-routes.ts +140 -0
- package/src/runtime/routes/channel-routes.ts +5 -10
- package/src/runtime/routes/conversation-routes.ts +5 -5
- package/src/runtime/routes/run-routes.ts +2 -2
- package/src/runtime/run-orchestrator.ts +9 -3
- package/src/schedule/recurrence-engine.ts +138 -0
- package/src/schedule/recurrence-types.ts +67 -0
- package/src/schedule/schedule-store.ts +102 -57
- package/src/schedule/scheduler.ts +9 -6
- package/src/security/oauth2.ts +29 -4
- package/src/security/secret-allowlist.ts +46 -0
- package/src/skills/clawhub.ts +1 -1
- package/src/subagent/manager.ts +40 -8
- package/src/swarm/backend-claude-code.ts +64 -9
- package/src/swarm/worker-prompts.ts +2 -1
- package/src/tasks/SPEC.md +34 -28
- package/src/tasks/ephemeral-permissions.ts +16 -7
- package/src/tasks/task-compiler.ts +5 -4
- package/src/tasks/task-runner.ts +10 -5
- package/src/tasks/task-scheduler.ts +1 -1
- package/src/tasks/tool-sanitizer.ts +36 -0
- package/src/tools/assets/search.ts +4 -4
- package/src/tools/browser/api-map.ts +220 -0
- package/src/tools/browser/auto-navigate.ts +270 -0
- package/src/tools/browser/browser-execution.ts +2 -1
- package/src/tools/browser/browser-manager.ts +2 -2
- package/src/tools/browser/network-recorder.ts +5 -4
- package/src/tools/browser/x-auto-navigate.ts +207 -0
- package/src/tools/calls/call-end.ts +17 -67
- package/src/tools/calls/call-start.ts +24 -85
- package/src/tools/calls/call-status.ts +35 -51
- package/src/tools/claude-code/claude-code.ts +77 -11
- package/src/tools/contacts/contact-merge.ts +46 -78
- package/src/tools/contacts/contact-search.ts +35 -79
- package/src/tools/contacts/contact-upsert.ts +35 -108
- package/src/tools/credentials/vault.ts +20 -4
- package/src/tools/document/document-tool.ts +71 -144
- package/src/tools/executor.ts +129 -10
- package/src/tools/followups/followup_create.ts +46 -88
- package/src/tools/followups/followup_list.ts +34 -74
- package/src/tools/followups/followup_resolve.ts +31 -66
- package/src/tools/host-terminal/cli-discover.ts +2 -1
- package/src/tools/host-terminal/host-shell.ts +10 -0
- package/src/tools/memory/handlers.ts +5 -4
- package/src/tools/network/__tests__/web-search.test.ts +427 -0
- package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
- package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
- package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
- package/src/tools/network/web-fetch.ts +18 -6
- package/src/tools/playbooks/index.ts +4 -5
- package/src/tools/playbooks/playbook-create.ts +3 -47
- package/src/tools/playbooks/playbook-delete.ts +1 -25
- package/src/tools/playbooks/playbook-list.ts +1 -28
- package/src/tools/playbooks/playbook-update.ts +3 -51
- package/src/tools/reminder/reminder.ts +5 -78
- package/src/tools/schedule/create.ts +69 -74
- package/src/tools/schedule/delete.ts +21 -47
- package/src/tools/schedule/list.ts +55 -74
- package/src/tools/schedule/update.ts +77 -84
- package/src/tools/subagent/abort.ts +29 -58
- package/src/tools/subagent/message.ts +30 -63
- package/src/tools/subagent/read.ts +53 -84
- package/src/tools/subagent/spawn.ts +43 -82
- package/src/tools/subagent/status.ts +42 -71
- package/src/tools/swarm/delegate.ts +2 -1
- package/src/tools/tasks/index.ts +8 -8
- package/src/tools/tasks/task-delete.ts +60 -88
- package/src/tools/tasks/task-list.ts +31 -52
- package/src/tools/tasks/task-run.ts +72 -108
- package/src/tools/tasks/task-save.ts +33 -65
- package/src/tools/tasks/work-item-enqueue.ts +183 -215
- package/src/tools/tasks/work-item-list.ts +33 -63
- package/src/tools/tasks/work-item-remove.ts +45 -97
- package/src/tools/tasks/work-item-update.ts +91 -163
- package/src/tools/terminal/backends/native.ts +3 -1
- package/src/tools/tool-manifest.ts +0 -62
- package/src/tools/types.ts +6 -0
- package/src/tools/ui-surface/definitions.ts +3 -1
- package/src/tools/watch/screen-watch.ts +3 -1
- package/src/tools/watcher/create.ts +52 -98
- package/src/tools/watcher/delete.ts +20 -46
- package/src/tools/watcher/digest.ts +36 -70
- package/src/tools/watcher/list.ts +49 -79
- package/src/tools/watcher/update.ts +45 -91
- package/src/twitter/client.ts +690 -0
- package/src/twitter/session.ts +91 -0
- package/src/usage/types.ts +0 -1
- package/src/util/truncate.ts +6 -0
- package/src/watcher/providers/slack.ts +2 -1
- package/src/watcher/watcher-store.ts +3 -2
- package/src/work-items/work-item-store.ts +27 -2
- package/src/workspace/commit-message-enrichment-service.ts +31 -7
- package/src/workspace/git-service.ts +87 -22
- package/src/workspace/provider-commit-message-generator.ts +242 -0
- package/src/workspace/turn-commit.ts +62 -3
- package/src/tools/contacts/index.ts +0 -4
- package/src/tools/document/index.ts +0 -5
- package/src/tools/followups/index.ts +0 -3
- package/src/tools/subagent/index.ts +0 -5
- /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, test, expect, afterAll } from 'bun:test';
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { runSkillToolScript } from '../tools/skills/skill-script-runner.js';
|
|
6
|
+
import type { ToolContext } from '../tools/types.js';
|
|
7
|
+
|
|
8
|
+
const testDir = mkdtempSync(join(tmpdir(), 'skill-runner-test-'));
|
|
9
|
+
|
|
10
|
+
afterAll(() => {
|
|
11
|
+
try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const ctx: ToolContext = {
|
|
15
|
+
workingDir: '/tmp',
|
|
16
|
+
sessionId: 'test-session',
|
|
17
|
+
conversationId: 'test-conversation',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function makeSkillDir(name: string): string {
|
|
21
|
+
const dir = join(testDir, name);
|
|
22
|
+
mkdirSync(dir, { recursive: true });
|
|
23
|
+
return dir;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ── Host execution ──────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
describe('runSkillToolScript (host)', () => {
|
|
29
|
+
test('runs a valid skill script and returns result', async () => {
|
|
30
|
+
const skillDir = makeSkillDir('valid-skill');
|
|
31
|
+
writeFileSync(join(skillDir, 'tool.ts'), `
|
|
32
|
+
export async function run(input, context) {
|
|
33
|
+
return { content: 'Hello from skill! Input: ' + JSON.stringify(input), isError: false };
|
|
34
|
+
}
|
|
35
|
+
`);
|
|
36
|
+
|
|
37
|
+
const result = await runSkillToolScript(skillDir, 'tool.ts', { foo: 'bar' }, ctx);
|
|
38
|
+
|
|
39
|
+
expect(result.isError).toBe(false);
|
|
40
|
+
expect(result.content).toContain('Hello from skill!');
|
|
41
|
+
expect(result.content).toContain('"foo":"bar"');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('blocks path traversal escape', async () => {
|
|
45
|
+
const skillDir = makeSkillDir('escape-skill');
|
|
46
|
+
|
|
47
|
+
const result = await runSkillToolScript(skillDir, '../../../etc/passwd', {}, ctx);
|
|
48
|
+
|
|
49
|
+
expect(result.isError).toBe(true);
|
|
50
|
+
expect(result.content).toContain('escapes the skill directory');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('returns error when script does not export run()', async () => {
|
|
54
|
+
const skillDir = makeSkillDir('no-run-skill');
|
|
55
|
+
writeFileSync(join(skillDir, 'bad.ts'), `
|
|
56
|
+
export const name = 'not a runner';
|
|
57
|
+
`);
|
|
58
|
+
|
|
59
|
+
const result = await runSkillToolScript(skillDir, 'bad.ts', {}, ctx);
|
|
60
|
+
|
|
61
|
+
expect(result.isError).toBe(true);
|
|
62
|
+
expect(result.content).toContain('does not export a "run" function');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('returns error when script throws', async () => {
|
|
66
|
+
const skillDir = makeSkillDir('throw-skill');
|
|
67
|
+
writeFileSync(join(skillDir, 'throw.ts'), `
|
|
68
|
+
export async function run() {
|
|
69
|
+
throw new Error('Intentional test error');
|
|
70
|
+
}
|
|
71
|
+
`);
|
|
72
|
+
|
|
73
|
+
const result = await runSkillToolScript(skillDir, 'throw.ts', {}, ctx);
|
|
74
|
+
|
|
75
|
+
expect(result.isError).toBe(true);
|
|
76
|
+
expect(result.content).toContain('Intentional test error');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('returns error when script file does not exist', async () => {
|
|
80
|
+
const skillDir = makeSkillDir('missing-skill');
|
|
81
|
+
|
|
82
|
+
const result = await runSkillToolScript(skillDir, 'nonexistent.ts', {}, ctx);
|
|
83
|
+
|
|
84
|
+
expect(result.isError).toBe(true);
|
|
85
|
+
expect(result.content).toContain('Failed to load skill tool script');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ── Version hash checking ───────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
describe('runSkillToolScript version hash', () => {
|
|
92
|
+
test('blocks execution when hash mismatches', async () => {
|
|
93
|
+
const skillDir = makeSkillDir('hash-mismatch');
|
|
94
|
+
writeFileSync(join(skillDir, 'tool.ts'), `
|
|
95
|
+
export async function run() {
|
|
96
|
+
return { content: 'should not run', isError: false };
|
|
97
|
+
}
|
|
98
|
+
`);
|
|
99
|
+
|
|
100
|
+
const result = await runSkillToolScript(skillDir, 'tool.ts', {}, ctx, {
|
|
101
|
+
expectedSkillVersionHash: 'expected-hash-123',
|
|
102
|
+
skillDirHashResolver: () => 'different-hash-456',
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(result.isError).toBe(true);
|
|
106
|
+
expect(result.content).toContain('Skill version mismatch');
|
|
107
|
+
expect(result.content).toContain('expected-hash-123');
|
|
108
|
+
expect(result.content).toContain('different-hash-456');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('allows execution when hash matches', async () => {
|
|
112
|
+
const skillDir = makeSkillDir('hash-match');
|
|
113
|
+
writeFileSync(join(skillDir, 'tool.ts'), `
|
|
114
|
+
export async function run() {
|
|
115
|
+
return { content: 'hash matched', isError: false };
|
|
116
|
+
}
|
|
117
|
+
`);
|
|
118
|
+
|
|
119
|
+
const result = await runSkillToolScript(skillDir, 'tool.ts', {}, ctx, {
|
|
120
|
+
expectedSkillVersionHash: 'matching-hash',
|
|
121
|
+
skillDirHashResolver: () => 'matching-hash',
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(result.isError).toBe(false);
|
|
125
|
+
expect(result.content).toBe('hash matched');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('returns error when hash resolver throws', async () => {
|
|
129
|
+
const skillDir = makeSkillDir('hash-error');
|
|
130
|
+
writeFileSync(join(skillDir, 'tool.ts'), `
|
|
131
|
+
export async function run() {
|
|
132
|
+
return { content: 'should not run', isError: false };
|
|
133
|
+
}
|
|
134
|
+
`);
|
|
135
|
+
|
|
136
|
+
const result = await runSkillToolScript(skillDir, 'tool.ts', {}, ctx, {
|
|
137
|
+
expectedSkillVersionHash: 'some-hash',
|
|
138
|
+
skillDirHashResolver: () => { throw new Error('hash computation failed'); },
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(result.isError).toBe(true);
|
|
142
|
+
expect(result.content).toContain('Failed to compute skill version hash');
|
|
143
|
+
expect(result.content).toContain('hash computation failed');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('skips hash check when no expected hash provided', async () => {
|
|
147
|
+
const skillDir = makeSkillDir('no-hash');
|
|
148
|
+
writeFileSync(join(skillDir, 'tool.ts'), `
|
|
149
|
+
export async function run() {
|
|
150
|
+
return { content: 'no hash check', isError: false };
|
|
151
|
+
}
|
|
152
|
+
`);
|
|
153
|
+
|
|
154
|
+
const result = await runSkillToolScript(skillDir, 'tool.ts', {}, ctx);
|
|
155
|
+
|
|
156
|
+
expect(result.isError).toBe(false);
|
|
157
|
+
expect(result.content).toBe('no hash check');
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { extractPromptSpeakerMetadata, SpeakerIdentityTracker } from '../calls/speaker-identification.js';
|
|
3
|
+
|
|
4
|
+
describe('speaker-identification', () => {
|
|
5
|
+
test('extractPromptSpeakerMetadata: reads top-level and nested fields', () => {
|
|
6
|
+
const metadata = extractPromptSpeakerMetadata({
|
|
7
|
+
speaker_id: 'spk-7',
|
|
8
|
+
speaker_label: 'Conference room mic',
|
|
9
|
+
speaker_confidence: '0.88',
|
|
10
|
+
metadata: {
|
|
11
|
+
participantId: 'participant-22',
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
expect(metadata.speakerId).toBe('spk-7');
|
|
16
|
+
expect(metadata.speakerLabel).toBe('Conference room mic');
|
|
17
|
+
expect(metadata.speakerConfidence).toBe(0.88);
|
|
18
|
+
expect(metadata.participantId).toBe('participant-22');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('SpeakerIdentityTracker: keeps stable identity for provider speaker id', () => {
|
|
22
|
+
const tracker = new SpeakerIdentityTracker();
|
|
23
|
+
|
|
24
|
+
const first = tracker.identifySpeaker({
|
|
25
|
+
speakerId: 'speaker-a',
|
|
26
|
+
speakerName: 'Aaron',
|
|
27
|
+
speakerConfidence: 0.93,
|
|
28
|
+
});
|
|
29
|
+
const second = tracker.identifySpeaker({
|
|
30
|
+
speakerId: 'speaker-a',
|
|
31
|
+
speakerName: 'Aaron',
|
|
32
|
+
speakerConfidence: 0.81,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(first.speakerId).toBe('speaker-a');
|
|
36
|
+
expect(first.speakerLabel).toBe('Aaron');
|
|
37
|
+
expect(first.source).toBe('provider');
|
|
38
|
+
expect(second.speakerId).toBe('speaker-a');
|
|
39
|
+
expect(second.speakerLabel).toBe('Aaron');
|
|
40
|
+
expect(second.speakerConfidence).toBe(0.81);
|
|
41
|
+
expect(tracker.listProfiles()).toHaveLength(1);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('SpeakerIdentityTracker: falls back to inferred primary speaker without provider ids', () => {
|
|
45
|
+
const tracker = new SpeakerIdentityTracker();
|
|
46
|
+
const speaker = tracker.identifySpeaker({});
|
|
47
|
+
|
|
48
|
+
expect(speaker.speakerId).toBe('primary-speaker');
|
|
49
|
+
expect(speaker.speakerLabel).toBe('Speaker 1');
|
|
50
|
+
expect(speaker.source).toBe('inferred');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -9,13 +9,14 @@ interface FakeManagedSubagent {
|
|
|
9
9
|
abort: () => void;
|
|
10
10
|
dispose: () => void;
|
|
11
11
|
messages: Array<{ role: string; content: Array<{ type: string; text: string }> }>;
|
|
12
|
-
sendToClient: () => void;
|
|
12
|
+
sendToClient: (msg: ServerMessage) => void;
|
|
13
13
|
loadFromDb?: () => Promise<void>;
|
|
14
14
|
persistUserMessage?: (msg: string) => string;
|
|
15
15
|
runAgentLoop?: () => Promise<void>;
|
|
16
|
+
usageStats: { inputTokens: number; outputTokens: number; estimatedCost: number };
|
|
16
17
|
};
|
|
17
18
|
state: SubagentState;
|
|
18
|
-
parentSendToClient: () => void;
|
|
19
|
+
parentSendToClient: (msg: ServerMessage) => void;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
/** Type-safe accessor for SubagentManager's private internals via bracket notation. */
|
|
@@ -37,19 +38,21 @@ function injectFakeSubagent(
|
|
|
37
38
|
manager: SubagentManager,
|
|
38
39
|
subagentId: string,
|
|
39
40
|
state: SubagentState,
|
|
41
|
+
parentSendToClient?: (msg: ServerMessage) => void,
|
|
40
42
|
): void {
|
|
41
43
|
const fakeSession: FakeManagedSubagent['session'] = {
|
|
42
44
|
abort: () => {},
|
|
43
45
|
dispose: () => {},
|
|
44
46
|
messages: [],
|
|
45
47
|
sendToClient: () => {},
|
|
48
|
+
usageStats: { inputTokens: 100, outputTokens: 50, estimatedCost: 0.005 },
|
|
46
49
|
};
|
|
47
50
|
|
|
48
51
|
const internals = asInternals(manager);
|
|
49
52
|
const subagents = internals.subagents;
|
|
50
53
|
const parentToChildren = internals.parentToChildren;
|
|
51
54
|
|
|
52
|
-
subagents.set(subagentId, { session: fakeSession, state, parentSendToClient: () => {} });
|
|
55
|
+
subagents.set(subagentId, { session: fakeSession, state, parentSendToClient: parentSendToClient ?? (() => {}) });
|
|
53
56
|
|
|
54
57
|
const parentId = state.config.parentSessionId;
|
|
55
58
|
if (!parentToChildren.has(parentId)) {
|
|
@@ -78,7 +81,7 @@ function makeState(
|
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
describe('SubagentManager abort notification', () => {
|
|
81
|
-
test('abort notifies parent with
|
|
84
|
+
test('abort notifies parent with do-not-respawn message', () => {
|
|
82
85
|
const manager = new SubagentManager();
|
|
83
86
|
const subagentId = 'sub-1';
|
|
84
87
|
const state = makeState(subagentId);
|
|
@@ -97,18 +100,45 @@ describe('SubagentManager abort notification', () => {
|
|
|
97
100
|
expect(result).toBe(true);
|
|
98
101
|
expect(state.status).toBe('aborted');
|
|
99
102
|
expect(notifications).toHaveLength(1);
|
|
100
|
-
expect(notifications[0].
|
|
101
|
-
expect(notifications[0].message).toContain('
|
|
103
|
+
expect(notifications[0].message).toContain('explicitly aborted');
|
|
104
|
+
expect(notifications[0].message).toContain('Do NOT re-spawn');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('abort notification routes to parent sender, not aborting sender', () => {
|
|
108
|
+
const manager = new SubagentManager();
|
|
109
|
+
const subagentId = 'sub-1';
|
|
110
|
+
const state = makeState(subagentId);
|
|
111
|
+
|
|
112
|
+
// Track which sender onSubagentFinished receives.
|
|
113
|
+
let notificationSender: unknown = null;
|
|
114
|
+
manager.onSubagentFinished = (_pid, _message, sender) => {
|
|
115
|
+
notificationSender = sender;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// The parent's stored sender (set at spawn time).
|
|
119
|
+
const parentSender = () => {};
|
|
120
|
+
injectFakeSubagent(manager, subagentId, state, parentSender);
|
|
121
|
+
|
|
122
|
+
// A different sender (simulating abort from a different thread's socket).
|
|
123
|
+
const abortingSender = ((_msg: ServerMessage) => {}) as (msg: ServerMessage) => void;
|
|
124
|
+
|
|
125
|
+
manager.abort(subagentId, abortingSender);
|
|
126
|
+
|
|
127
|
+
// onSubagentFinished should receive the parent's sender, not the aborting one.
|
|
128
|
+
expect(notificationSender).toBe(parentSender);
|
|
129
|
+
expect(notificationSender).not.toBe(abortingSender);
|
|
102
130
|
});
|
|
103
131
|
|
|
104
132
|
test('abort sends subagent_status_changed to client', () => {
|
|
105
133
|
const manager = new SubagentManager();
|
|
106
134
|
const subagentId = 'sub-1';
|
|
107
|
-
injectFakeSubagent(manager, subagentId, makeState(subagentId));
|
|
108
135
|
|
|
109
136
|
const clientMessages: ServerMessage[] = [];
|
|
110
137
|
const sendToClient = (msg: ServerMessage) => clientMessages.push(msg);
|
|
111
138
|
|
|
139
|
+
// Pass the sender as parentSendToClient so the stored sender receives the status update.
|
|
140
|
+
injectFakeSubagent(manager, subagentId, makeState(subagentId), sendToClient);
|
|
141
|
+
|
|
112
142
|
manager.abort(subagentId, sendToClient);
|
|
113
143
|
|
|
114
144
|
const statusMsg = clientMessages.find((m) => m.type === 'subagent_status_changed');
|
|
@@ -222,13 +252,14 @@ describe('SubagentManager notifyParent (via runSubagent)', () => {
|
|
|
222
252
|
await asInternals(manager).runSubagent(subagentId, 'Do something');
|
|
223
253
|
|
|
224
254
|
expect(state.status).toBe('completed');
|
|
255
|
+
expect(state.usage).toEqual({ inputTokens: 100, outputTokens: 50, estimatedCost: 0.005 });
|
|
225
256
|
expect(notifications).toHaveLength(1);
|
|
226
257
|
expect(notifications[0].parentSessionId).toBe('parent-sess-1');
|
|
227
258
|
expect(notifications[0].message).toContain('[Subagent "Test subagent" completed]');
|
|
228
259
|
expect(notifications[0].message).toContain('subagent_read');
|
|
229
260
|
});
|
|
230
261
|
|
|
231
|
-
test('failed subagent notifies parent with error', async () => {
|
|
262
|
+
test('failed subagent notifies parent with error and asks user before retry', async () => {
|
|
232
263
|
const manager = new SubagentManager();
|
|
233
264
|
const subagentId = 'sub-1';
|
|
234
265
|
const state = makeState(subagentId);
|
|
@@ -251,10 +282,11 @@ describe('SubagentManager notifyParent (via runSubagent)', () => {
|
|
|
251
282
|
|
|
252
283
|
expect(state.status).toBe('failed');
|
|
253
284
|
expect(state.error).toBe('API rate limit exceeded');
|
|
285
|
+
expect(state.usage).toEqual({ inputTokens: 100, outputTokens: 50, estimatedCost: 0.005 });
|
|
254
286
|
expect(notifications).toHaveLength(1);
|
|
255
|
-
expect(notifications[0].
|
|
256
|
-
expect(notifications[0].message).toContain('[Subagent "Test subagent" failed]');
|
|
287
|
+
expect(notifications[0].message).toContain('failed');
|
|
257
288
|
expect(notifications[0].message).toContain('API rate limit exceeded');
|
|
289
|
+
expect(notifications[0].message).toContain('Do NOT re-spawn');
|
|
258
290
|
});
|
|
259
291
|
|
|
260
292
|
test('failed subagent does not notify if already aborted', async () => {
|
|
@@ -1,51 +1,49 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { executeSubagentSpawn } from '../tools/subagent/spawn.js';
|
|
5
|
+
import { executeSubagentStatus } from '../tools/subagent/status.js';
|
|
6
|
+
import { executeSubagentAbort } from '../tools/subagent/abort.js';
|
|
7
|
+
import { executeSubagentMessage } from '../tools/subagent/message.js';
|
|
8
|
+
import { executeSubagentRead } from '../tools/subagent/read.js';
|
|
9
|
+
import { SubagentManager } from '../subagent/manager.js';
|
|
10
|
+
import type { SubagentState } from '../subagent/types.js';
|
|
7
11
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
expect(typeof subagentSpawnTool.getDefinition).toBe('function');
|
|
14
|
-
const def = subagentSpawnTool.getDefinition!();
|
|
15
|
-
expect((def.input_schema as Record<string, unknown>).required).toEqual(['label', 'objective']);
|
|
16
|
-
});
|
|
12
|
+
// Load tool definitions from the bundled skill TOOLS.json
|
|
13
|
+
const toolsJson = JSON.parse(
|
|
14
|
+
readFileSync(join(import.meta.dirname, '../config/bundled-skills/subagent/TOOLS.json'), 'utf-8'),
|
|
15
|
+
);
|
|
16
|
+
const findTool = (name: string) => toolsJson.tools.find((t: { name: string }) => t.name === name);
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
expect(
|
|
18
|
+
describe('Subagent tool definitions', () => {
|
|
19
|
+
test('spawn tool has correct definition', () => {
|
|
20
|
+
const def = findTool('subagent_spawn');
|
|
21
|
+
expect(def).toBeDefined();
|
|
22
|
+
expect(def.input_schema.required).toEqual(['label', 'objective']);
|
|
22
23
|
});
|
|
23
24
|
|
|
24
|
-
test('abort tool has correct
|
|
25
|
-
|
|
26
|
-
expect(
|
|
27
|
-
|
|
28
|
-
expect((def.input_schema as Record<string, unknown>).required).toEqual(['subagent_id']);
|
|
25
|
+
test('abort tool has correct definition', () => {
|
|
26
|
+
const def = findTool('subagent_abort');
|
|
27
|
+
expect(def).toBeDefined();
|
|
28
|
+
expect(def.input_schema.required).toEqual(['subagent_id']);
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
test('message tool has correct
|
|
32
|
-
|
|
33
|
-
expect(
|
|
34
|
-
|
|
35
|
-
expect((def.input_schema as Record<string, unknown>).required).toEqual(['subagent_id', 'content']);
|
|
31
|
+
test('message tool has correct definition', () => {
|
|
32
|
+
const def = findTool('subagent_message');
|
|
33
|
+
expect(def).toBeDefined();
|
|
34
|
+
expect(def.input_schema.required).toEqual(['subagent_id', 'content']);
|
|
36
35
|
});
|
|
37
36
|
|
|
38
|
-
test('read tool has correct
|
|
39
|
-
|
|
40
|
-
expect(
|
|
41
|
-
|
|
42
|
-
expect((def.input_schema as Record<string, unknown>).required).toEqual(['subagent_id']);
|
|
37
|
+
test('read tool has correct definition', () => {
|
|
38
|
+
const def = findTool('subagent_read');
|
|
39
|
+
expect(def).toBeDefined();
|
|
40
|
+
expect(def.input_schema.required).toEqual(['subagent_id']);
|
|
43
41
|
});
|
|
44
42
|
});
|
|
45
43
|
|
|
46
44
|
describe('Subagent tool execute validation', () => {
|
|
47
45
|
test('spawn returns error when no sendToClient', async () => {
|
|
48
|
-
const result = await
|
|
46
|
+
const result = await executeSubagentSpawn(
|
|
49
47
|
{ label: 'test', objective: 'do something' },
|
|
50
48
|
{ workingDir: '/tmp', sessionId: 'sess-1', conversationId: 'conv-1' },
|
|
51
49
|
);
|
|
@@ -54,7 +52,7 @@ describe('Subagent tool execute validation', () => {
|
|
|
54
52
|
});
|
|
55
53
|
|
|
56
54
|
test('spawn returns error when missing label', async () => {
|
|
57
|
-
const result = await
|
|
55
|
+
const result = await executeSubagentSpawn(
|
|
58
56
|
{ objective: 'do something' },
|
|
59
57
|
{ workingDir: '/tmp', sessionId: 'sess-1', conversationId: 'conv-1', sendToClient: () => {} },
|
|
60
58
|
);
|
|
@@ -63,7 +61,7 @@ describe('Subagent tool execute validation', () => {
|
|
|
63
61
|
});
|
|
64
62
|
|
|
65
63
|
test('status returns empty when no subagents', async () => {
|
|
66
|
-
const result = await
|
|
64
|
+
const result = await executeSubagentStatus(
|
|
67
65
|
{},
|
|
68
66
|
{ workingDir: '/tmp', sessionId: 'nonexistent-session', conversationId: 'conv-1' },
|
|
69
67
|
);
|
|
@@ -72,7 +70,7 @@ describe('Subagent tool execute validation', () => {
|
|
|
72
70
|
});
|
|
73
71
|
|
|
74
72
|
test('status returns error for unknown subagent_id', async () => {
|
|
75
|
-
const result = await
|
|
73
|
+
const result = await executeSubagentStatus(
|
|
76
74
|
{ subagent_id: 'nonexistent-id' },
|
|
77
75
|
{ workingDir: '/tmp', sessionId: 'sess-1', conversationId: 'conv-1' },
|
|
78
76
|
);
|
|
@@ -81,7 +79,7 @@ describe('Subagent tool execute validation', () => {
|
|
|
81
79
|
});
|
|
82
80
|
|
|
83
81
|
test('abort returns error for unknown subagent_id', async () => {
|
|
84
|
-
const result = await
|
|
82
|
+
const result = await executeSubagentAbort(
|
|
85
83
|
{ subagent_id: 'nonexistent-id' },
|
|
86
84
|
{ workingDir: '/tmp', sessionId: 'sess-1', conversationId: 'conv-1' },
|
|
87
85
|
);
|
|
@@ -90,7 +88,7 @@ describe('Subagent tool execute validation', () => {
|
|
|
90
88
|
});
|
|
91
89
|
|
|
92
90
|
test('abort returns error when missing subagent_id', async () => {
|
|
93
|
-
const result = await
|
|
91
|
+
const result = await executeSubagentAbort(
|
|
94
92
|
{},
|
|
95
93
|
{ workingDir: '/tmp', sessionId: 'sess-1', conversationId: 'conv-1' },
|
|
96
94
|
);
|
|
@@ -99,7 +97,7 @@ describe('Subagent tool execute validation', () => {
|
|
|
99
97
|
});
|
|
100
98
|
|
|
101
99
|
test('message returns error for unknown subagent_id', async () => {
|
|
102
|
-
const result = await
|
|
100
|
+
const result = await executeSubagentMessage(
|
|
103
101
|
{ subagent_id: 'nonexistent-id', content: 'hello' },
|
|
104
102
|
{ workingDir: '/tmp', sessionId: 'sess-1', conversationId: 'conv-1' },
|
|
105
103
|
);
|
|
@@ -108,7 +106,7 @@ describe('Subagent tool execute validation', () => {
|
|
|
108
106
|
});
|
|
109
107
|
|
|
110
108
|
test('message returns error when missing required fields', async () => {
|
|
111
|
-
const result = await
|
|
109
|
+
const result = await executeSubagentMessage(
|
|
112
110
|
{ subagent_id: 'some-id' },
|
|
113
111
|
{ workingDir: '/tmp', sessionId: 'sess-1', conversationId: 'conv-1' },
|
|
114
112
|
);
|
|
@@ -116,3 +114,105 @@ describe('Subagent tool execute validation', () => {
|
|
|
116
114
|
expect(result.content).toContain('required');
|
|
117
115
|
});
|
|
118
116
|
});
|
|
117
|
+
|
|
118
|
+
// ── Ownership validation tests ──────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Inject a fake subagent into the singleton manager so tool executors
|
|
122
|
+
* can find it. Uses the same private-internals trick as the notify tests.
|
|
123
|
+
*/
|
|
124
|
+
function injectSubagent(
|
|
125
|
+
manager: SubagentManager,
|
|
126
|
+
subagentId: string,
|
|
127
|
+
parentSessionId: string,
|
|
128
|
+
status: SubagentState['status'] = 'running',
|
|
129
|
+
): void {
|
|
130
|
+
const internals = manager as unknown as {
|
|
131
|
+
subagents: Map<string, { session: unknown; state: SubagentState; parentSendToClient: () => void }>;
|
|
132
|
+
parentToChildren: Map<string, Set<string>>;
|
|
133
|
+
};
|
|
134
|
+
const state: SubagentState = {
|
|
135
|
+
config: { id: subagentId, parentSessionId, label: 'Test', objective: 'test' },
|
|
136
|
+
status,
|
|
137
|
+
conversationId: `conv-${subagentId}`,
|
|
138
|
+
createdAt: Date.now(),
|
|
139
|
+
usage: { inputTokens: 0, outputTokens: 0, estimatedCost: 0 },
|
|
140
|
+
};
|
|
141
|
+
const fakeSession = {
|
|
142
|
+
abort: () => {},
|
|
143
|
+
dispose: () => {},
|
|
144
|
+
messages: [],
|
|
145
|
+
sendToClient: () => {},
|
|
146
|
+
usageStats: { inputTokens: 0, outputTokens: 0, estimatedCost: 0 },
|
|
147
|
+
};
|
|
148
|
+
internals.subagents.set(subagentId, { session: fakeSession, state, parentSendToClient: () => {} });
|
|
149
|
+
if (!internals.parentToChildren.has(parentSessionId)) {
|
|
150
|
+
internals.parentToChildren.set(parentSessionId, new Set());
|
|
151
|
+
}
|
|
152
|
+
internals.parentToChildren.get(parentSessionId)!.add(subagentId);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
import { getSubagentManager } from '../subagent/index.js';
|
|
156
|
+
|
|
157
|
+
describe('Subagent tool ownership validation', () => {
|
|
158
|
+
const ownerSession = 'owner-sess';
|
|
159
|
+
const otherSession = 'other-sess';
|
|
160
|
+
const subagentId = 'owned-sub-1';
|
|
161
|
+
|
|
162
|
+
// Inject once — all tests share this subagent.
|
|
163
|
+
const manager = getSubagentManager();
|
|
164
|
+
injectSubagent(manager, subagentId, ownerSession);
|
|
165
|
+
|
|
166
|
+
test('status rejects non-owner session', async () => {
|
|
167
|
+
const result = await executeSubagentStatus(
|
|
168
|
+
{ subagent_id: subagentId },
|
|
169
|
+
{ workingDir: '/tmp', sessionId: otherSession, conversationId: 'conv-1' },
|
|
170
|
+
);
|
|
171
|
+
expect(result.isError).toBe(true);
|
|
172
|
+
expect(result.content).toContain('No subagent found');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('status succeeds for owner session', async () => {
|
|
176
|
+
const result = await executeSubagentStatus(
|
|
177
|
+
{ subagent_id: subagentId },
|
|
178
|
+
{ workingDir: '/tmp', sessionId: ownerSession, conversationId: 'conv-1' },
|
|
179
|
+
);
|
|
180
|
+
expect(result.isError).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('message rejects non-owner session', async () => {
|
|
184
|
+
const result = await executeSubagentMessage(
|
|
185
|
+
{ subagent_id: subagentId, content: 'hello' },
|
|
186
|
+
{ workingDir: '/tmp', sessionId: otherSession, conversationId: 'conv-1' },
|
|
187
|
+
);
|
|
188
|
+
expect(result.isError).toBe(true);
|
|
189
|
+
expect(result.content).toContain('Could not send');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('read rejects non-owner session', async () => {
|
|
193
|
+
const result = await executeSubagentRead(
|
|
194
|
+
{ subagent_id: subagentId },
|
|
195
|
+
{ workingDir: '/tmp', sessionId: otherSession, conversationId: 'conv-1' },
|
|
196
|
+
);
|
|
197
|
+
expect(result.isError).toBe(true);
|
|
198
|
+
expect(result.content).toContain('No subagent found');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('abort rejects non-owner session', async () => {
|
|
202
|
+
const result = await executeSubagentAbort(
|
|
203
|
+
{ subagent_id: subagentId },
|
|
204
|
+
{ workingDir: '/tmp', sessionId: otherSession, conversationId: 'conv-1' },
|
|
205
|
+
);
|
|
206
|
+
expect(result.isError).toBe(true);
|
|
207
|
+
expect(result.content).toContain('Could not abort');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('abort succeeds for owner session', async () => {
|
|
211
|
+
const result = await executeSubagentAbort(
|
|
212
|
+
{ subagent_id: subagentId },
|
|
213
|
+
{ workingDir: '/tmp', sessionId: ownerSession, conversationId: 'conv-1' },
|
|
214
|
+
);
|
|
215
|
+
// Abort succeeds (subagent was running)
|
|
216
|
+
expect(result.isError).toBe(false);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
@@ -35,7 +35,7 @@ mock.module('./indexer.js', () => ({
|
|
|
35
35
|
}));
|
|
36
36
|
|
|
37
37
|
import type { Database } from 'bun:sqlite';
|
|
38
|
-
import { initializeDb, getDb } from '../memory/db.js';
|
|
38
|
+
import { initializeDb, getDb, resetDb } from '../memory/db.js';
|
|
39
39
|
import { renderTemplate } from '../tasks/task-runner.js';
|
|
40
40
|
import { compileTaskFromConversation, saveCompiledTask } from '../tasks/task-compiler.js';
|
|
41
41
|
import { getTask } from '../tasks/task-store.js';
|
|
@@ -43,6 +43,7 @@ import { getTask } from '../tasks/task-store.js';
|
|
|
43
43
|
initializeDb();
|
|
44
44
|
|
|
45
45
|
afterAll(() => {
|
|
46
|
+
resetDb();
|
|
46
47
|
try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
|
|
47
48
|
});
|
|
48
49
|
|
|
@@ -25,7 +25,7 @@ mock.module('../util/logger.js', () => ({
|
|
|
25
25
|
}),
|
|
26
26
|
}));
|
|
27
27
|
|
|
28
|
-
import { initializeDb, getDb } from '../memory/db.js';
|
|
28
|
+
import { initializeDb, getDb, resetDb } from '../memory/db.js';
|
|
29
29
|
import { createTask } from '../tasks/task-store.js';
|
|
30
30
|
import { getTaskRunRules } from '../tasks/ephemeral-permissions.js';
|
|
31
31
|
import { renderTemplate, runTask } from '../tasks/task-runner.js';
|
|
@@ -33,6 +33,7 @@ import { renderTemplate, runTask } from '../tasks/task-runner.js';
|
|
|
33
33
|
initializeDb();
|
|
34
34
|
|
|
35
35
|
afterAll(() => {
|
|
36
|
+
resetDb();
|
|
36
37
|
try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
|
|
37
38
|
});
|
|
38
39
|
|
|
@@ -25,7 +25,7 @@ mock.module('../util/logger.js', () => ({
|
|
|
25
25
|
}),
|
|
26
26
|
}));
|
|
27
27
|
|
|
28
|
-
import { initializeDb, getDb } from '../memory/db.js';
|
|
28
|
+
import { initializeDb, getDb, resetDb } from '../memory/db.js';
|
|
29
29
|
import { createTask } from '../tasks/task-store.js';
|
|
30
30
|
import { scheduleTask } from '../tasks/task-scheduler.js';
|
|
31
31
|
import {
|
|
@@ -48,6 +48,7 @@ function forceScheduleDue(scheduleId: string): void {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
afterAll(() => {
|
|
51
|
+
resetDb();
|
|
51
52
|
try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
|
|
52
53
|
});
|
|
53
54
|
|