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,331 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterAll, mock } from 'bun:test';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const testDir = mkdtempSync(join(tmpdir(), 'contacts-tools-test-'));
|
|
7
|
+
|
|
8
|
+
mock.module('../util/platform.js', () => ({
|
|
9
|
+
getDataDir: () => testDir,
|
|
10
|
+
isMacOS: () => process.platform === 'darwin',
|
|
11
|
+
isLinux: () => process.platform === 'linux',
|
|
12
|
+
isWindows: () => process.platform === 'win32',
|
|
13
|
+
getSocketPath: () => join(testDir, 'test.sock'),
|
|
14
|
+
getPidPath: () => join(testDir, 'test.pid'),
|
|
15
|
+
getDbPath: () => join(testDir, 'test.db'),
|
|
16
|
+
getLogPath: () => join(testDir, 'test.log'),
|
|
17
|
+
ensureDataDir: () => {},
|
|
18
|
+
migrateToDataLayout: () => {},
|
|
19
|
+
migrateToWorkspaceLayout: () => {},
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
mock.module('../util/logger.js', () => ({
|
|
23
|
+
getLogger: () => new Proxy({} as Record<string, unknown>, {
|
|
24
|
+
get: () => () => {},
|
|
25
|
+
}),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
mock.module('../config/loader.js', () => ({
|
|
29
|
+
getConfig: () => ({ memory: {} }),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
import type { Database } from 'bun:sqlite';
|
|
33
|
+
import { initializeDb, getDb, resetDb } from '../memory/db.js';
|
|
34
|
+
import type { ToolContext } from '../tools/types.js';
|
|
35
|
+
import { executeContactUpsert } from '../tools/contacts/contact-upsert.js';
|
|
36
|
+
import { executeContactSearch } from '../tools/contacts/contact-search.js';
|
|
37
|
+
import { executeContactMerge } from '../tools/contacts/contact-merge.js';
|
|
38
|
+
|
|
39
|
+
initializeDb();
|
|
40
|
+
|
|
41
|
+
afterAll(() => {
|
|
42
|
+
resetDb();
|
|
43
|
+
try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function getRawDb(): Database {
|
|
47
|
+
return (getDb() as unknown as { $client: Database }).$client;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const ctx: ToolContext = {
|
|
51
|
+
workingDir: '/tmp',
|
|
52
|
+
sessionId: 'test-session',
|
|
53
|
+
conversationId: 'test-conversation',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function clearContacts(): void {
|
|
57
|
+
getRawDb().run('DELETE FROM contact_channels');
|
|
58
|
+
getRawDb().run('DELETE FROM contacts');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── contact_upsert ──────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
describe('contact_upsert tool', () => {
|
|
64
|
+
beforeEach(clearContacts);
|
|
65
|
+
|
|
66
|
+
test('creates a new contact with display name only', async () => {
|
|
67
|
+
const result = await executeContactUpsert({ display_name: 'Alice' }, ctx);
|
|
68
|
+
|
|
69
|
+
expect(result.isError).toBe(false);
|
|
70
|
+
expect(result.content).toContain('Created contact');
|
|
71
|
+
expect(result.content).toContain('Alice');
|
|
72
|
+
expect(result.content).toContain('Importance: 0.50');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('creates a contact with all fields', async () => {
|
|
76
|
+
const result = await executeContactUpsert({
|
|
77
|
+
display_name: 'Bob',
|
|
78
|
+
relationship: 'colleague',
|
|
79
|
+
importance: 0.8,
|
|
80
|
+
response_expectation: 'within_hours',
|
|
81
|
+
preferred_tone: 'professional',
|
|
82
|
+
channels: [
|
|
83
|
+
{ type: 'email', address: 'bob@example.com', is_primary: true },
|
|
84
|
+
{ type: 'slack', address: '@bob' },
|
|
85
|
+
],
|
|
86
|
+
}, ctx);
|
|
87
|
+
|
|
88
|
+
expect(result.isError).toBe(false);
|
|
89
|
+
expect(result.content).toContain('Bob');
|
|
90
|
+
expect(result.content).toContain('colleague');
|
|
91
|
+
expect(result.content).toContain('0.80');
|
|
92
|
+
expect(result.content).toContain('within_hours');
|
|
93
|
+
expect(result.content).toContain('professional');
|
|
94
|
+
expect(result.content).toContain('email: bob@example.com');
|
|
95
|
+
expect(result.content).toContain('slack: @bob');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('updates an existing contact by ID', async () => {
|
|
99
|
+
const createResult = await executeContactUpsert({ display_name: 'Charlie' }, ctx);
|
|
100
|
+
expect(createResult.isError).toBe(false);
|
|
101
|
+
|
|
102
|
+
// Extract ID from output
|
|
103
|
+
const idMatch = createResult.content.match(/Contact (\S+)/);
|
|
104
|
+
expect(idMatch).not.toBeNull();
|
|
105
|
+
const contactId = idMatch![1];
|
|
106
|
+
|
|
107
|
+
const updateResult = await executeContactUpsert({
|
|
108
|
+
id: contactId,
|
|
109
|
+
display_name: 'Charlie Updated',
|
|
110
|
+
importance: 0.9,
|
|
111
|
+
}, ctx);
|
|
112
|
+
|
|
113
|
+
expect(updateResult.isError).toBe(false);
|
|
114
|
+
expect(updateResult.content).toContain('Updated contact');
|
|
115
|
+
expect(updateResult.content).toContain('Charlie Updated');
|
|
116
|
+
expect(updateResult.content).toContain('0.90');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('auto-matches by channel address on create', async () => {
|
|
120
|
+
// Create a contact with an email
|
|
121
|
+
await executeContactUpsert({
|
|
122
|
+
display_name: 'Diana',
|
|
123
|
+
channels: [{ type: 'email', address: 'diana@example.com' }],
|
|
124
|
+
}, ctx);
|
|
125
|
+
|
|
126
|
+
// Upsert with same email but different display name
|
|
127
|
+
const result = await executeContactUpsert({
|
|
128
|
+
display_name: 'Diana Updated',
|
|
129
|
+
channels: [{ type: 'email', address: 'diana@example.com' }],
|
|
130
|
+
}, ctx);
|
|
131
|
+
|
|
132
|
+
expect(result.isError).toBe(false);
|
|
133
|
+
expect(result.content).toContain('Updated contact');
|
|
134
|
+
expect(result.content).toContain('Diana Updated');
|
|
135
|
+
|
|
136
|
+
// Should still be just 1 contact
|
|
137
|
+
const count = getRawDb().query('SELECT COUNT(*) as c FROM contacts').get() as { c: number };
|
|
138
|
+
expect(count.c).toBe(1);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('rejects missing display_name', async () => {
|
|
142
|
+
const result = await executeContactUpsert({}, ctx);
|
|
143
|
+
|
|
144
|
+
expect(result.isError).toBe(true);
|
|
145
|
+
expect(result.content).toContain('display_name is required');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('rejects empty display_name', async () => {
|
|
149
|
+
const result = await executeContactUpsert({ display_name: ' ' }, ctx);
|
|
150
|
+
|
|
151
|
+
expect(result.isError).toBe(true);
|
|
152
|
+
expect(result.content).toContain('display_name is required');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('rejects importance out of range', async () => {
|
|
156
|
+
const result = await executeContactUpsert({
|
|
157
|
+
display_name: 'Test',
|
|
158
|
+
importance: 1.5,
|
|
159
|
+
}, ctx);
|
|
160
|
+
|
|
161
|
+
expect(result.isError).toBe(true);
|
|
162
|
+
expect(result.content).toContain('importance must be a number between 0 and 1');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('rejects negative importance', async () => {
|
|
166
|
+
const result = await executeContactUpsert({
|
|
167
|
+
display_name: 'Test',
|
|
168
|
+
importance: -0.1,
|
|
169
|
+
}, ctx);
|
|
170
|
+
|
|
171
|
+
expect(result.isError).toBe(true);
|
|
172
|
+
expect(result.content).toContain('importance must be a number between 0 and 1');
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ── contact_search ──────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
describe('contact_search tool', () => {
|
|
179
|
+
beforeEach(clearContacts);
|
|
180
|
+
|
|
181
|
+
test('searches by display name', async () => {
|
|
182
|
+
await executeContactUpsert({ display_name: 'Alice Smith' }, ctx);
|
|
183
|
+
await executeContactUpsert({ display_name: 'Bob Jones' }, ctx);
|
|
184
|
+
|
|
185
|
+
const result = await executeContactSearch({ query: 'Alice' }, ctx);
|
|
186
|
+
|
|
187
|
+
expect(result.isError).toBe(false);
|
|
188
|
+
expect(result.content).toContain('Alice Smith');
|
|
189
|
+
expect(result.content).not.toContain('Bob Jones');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('searches by channel address', async () => {
|
|
193
|
+
await executeContactUpsert({
|
|
194
|
+
display_name: 'Charlie',
|
|
195
|
+
channels: [{ type: 'email', address: 'charlie@example.com' }],
|
|
196
|
+
}, ctx);
|
|
197
|
+
|
|
198
|
+
const result = await executeContactSearch({ channel_address: 'charlie@example' }, ctx);
|
|
199
|
+
|
|
200
|
+
expect(result.isError).toBe(false);
|
|
201
|
+
expect(result.content).toContain('Charlie');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('searches by relationship', async () => {
|
|
205
|
+
await executeContactUpsert({ display_name: 'Diana', relationship: 'friend' }, ctx);
|
|
206
|
+
await executeContactUpsert({ display_name: 'Eve', relationship: 'colleague' }, ctx);
|
|
207
|
+
|
|
208
|
+
const result = await executeContactSearch({ relationship: 'friend' }, ctx);
|
|
209
|
+
|
|
210
|
+
expect(result.isError).toBe(false);
|
|
211
|
+
expect(result.content).toContain('Diana');
|
|
212
|
+
expect(result.content).not.toContain('Eve');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('returns no results message when nothing matches', async () => {
|
|
216
|
+
await executeContactUpsert({ display_name: 'Existing' }, ctx);
|
|
217
|
+
|
|
218
|
+
const result = await executeContactSearch({ query: 'Nonexistent' }, ctx);
|
|
219
|
+
|
|
220
|
+
expect(result.isError).toBe(false);
|
|
221
|
+
expect(result.content).toContain('No contacts found');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('rejects search with no criteria', async () => {
|
|
225
|
+
const result = await executeContactSearch({}, ctx);
|
|
226
|
+
|
|
227
|
+
expect(result.isError).toBe(true);
|
|
228
|
+
expect(result.content).toContain('At least one search criterion is required');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('searches by channel address with type filter', async () => {
|
|
232
|
+
await executeContactUpsert({
|
|
233
|
+
display_name: 'Frank',
|
|
234
|
+
channels: [
|
|
235
|
+
{ type: 'email', address: 'frank@example.com' },
|
|
236
|
+
{ type: 'slack', address: 'frank@example.com' },
|
|
237
|
+
],
|
|
238
|
+
}, ctx);
|
|
239
|
+
|
|
240
|
+
const result = await executeContactSearch({
|
|
241
|
+
channel_address: 'frank@example',
|
|
242
|
+
channel_type: 'slack',
|
|
243
|
+
}, ctx);
|
|
244
|
+
|
|
245
|
+
expect(result.isError).toBe(false);
|
|
246
|
+
expect(result.content).toContain('Frank');
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// ── contact_merge ───────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
describe('contact_merge tool', () => {
|
|
253
|
+
beforeEach(clearContacts);
|
|
254
|
+
|
|
255
|
+
function extractContactId(result: { content: string }): string {
|
|
256
|
+
const match = result.content.match(/Contact (\S+)/);
|
|
257
|
+
expect(match).not.toBeNull();
|
|
258
|
+
return match![1];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
test('merges two contacts', async () => {
|
|
262
|
+
const r1 = await executeContactUpsert({
|
|
263
|
+
display_name: 'Alice (Email)',
|
|
264
|
+
importance: 0.7,
|
|
265
|
+
channels: [{ type: 'email', address: 'alice@example.com' }],
|
|
266
|
+
}, ctx);
|
|
267
|
+
const r2 = await executeContactUpsert({
|
|
268
|
+
display_name: 'Alice (Slack)',
|
|
269
|
+
importance: 0.9,
|
|
270
|
+
channels: [{ type: 'slack', address: '@alice' }],
|
|
271
|
+
}, ctx);
|
|
272
|
+
|
|
273
|
+
const keepId = extractContactId(r1);
|
|
274
|
+
const mergeId = extractContactId(r2);
|
|
275
|
+
|
|
276
|
+
const result = await executeContactMerge({
|
|
277
|
+
keep_id: keepId,
|
|
278
|
+
merge_id: mergeId,
|
|
279
|
+
}, ctx);
|
|
280
|
+
|
|
281
|
+
expect(result.isError).toBe(false);
|
|
282
|
+
expect(result.content).toContain('Merged');
|
|
283
|
+
expect(result.content).toContain('Importance: 0.90'); // takes higher importance
|
|
284
|
+
expect(result.content).toContain('email: alice@example.com');
|
|
285
|
+
expect(result.content).toContain('slack: @alice');
|
|
286
|
+
|
|
287
|
+
// Verify donor is deleted
|
|
288
|
+
const count = getRawDb().query('SELECT COUNT(*) as c FROM contacts').get() as { c: number };
|
|
289
|
+
expect(count.c).toBe(1);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('rejects missing keep_id', async () => {
|
|
293
|
+
const result = await executeContactMerge({ merge_id: 'some-id' }, ctx);
|
|
294
|
+
|
|
295
|
+
expect(result.isError).toBe(true);
|
|
296
|
+
expect(result.content).toContain('keep_id is required');
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('rejects missing merge_id', async () => {
|
|
300
|
+
const result = await executeContactMerge({ keep_id: 'some-id' }, ctx);
|
|
301
|
+
|
|
302
|
+
expect(result.isError).toBe(true);
|
|
303
|
+
expect(result.content).toContain('merge_id is required');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test('returns error for nonexistent keep_id', async () => {
|
|
307
|
+
const r = await executeContactUpsert({ display_name: 'Exists' }, ctx);
|
|
308
|
+
const existingId = extractContactId(r);
|
|
309
|
+
|
|
310
|
+
const result = await executeContactMerge({
|
|
311
|
+
keep_id: 'nonexistent',
|
|
312
|
+
merge_id: existingId,
|
|
313
|
+
}, ctx);
|
|
314
|
+
|
|
315
|
+
expect(result.isError).toBe(true);
|
|
316
|
+
expect(result.content).toContain('not found');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('returns error for nonexistent merge_id', async () => {
|
|
320
|
+
const r = await executeContactUpsert({ display_name: 'Exists' }, ctx);
|
|
321
|
+
const existingId = extractContactId(r);
|
|
322
|
+
|
|
323
|
+
const result = await executeContactMerge({
|
|
324
|
+
keep_id: existingId,
|
|
325
|
+
merge_id: 'nonexistent',
|
|
326
|
+
}, ctx);
|
|
327
|
+
|
|
328
|
+
expect(result.isError).toBe(true);
|
|
329
|
+
expect(result.content).toContain('not found');
|
|
330
|
+
});
|
|
331
|
+
});
|
|
@@ -23,7 +23,7 @@ mock.module('../util/logger.js', () => ({
|
|
|
23
23
|
}),
|
|
24
24
|
}));
|
|
25
25
|
|
|
26
|
-
import { initializeDb, getDb } from '../memory/db.js';
|
|
26
|
+
import { initializeDb, getDb, resetDb } from '../memory/db.js';
|
|
27
27
|
import {
|
|
28
28
|
createConversation,
|
|
29
29
|
getConversation,
|
|
@@ -40,13 +40,13 @@ import {
|
|
|
40
40
|
linkAttachmentToMessage,
|
|
41
41
|
getAttachmentsForMessage,
|
|
42
42
|
getAttachmentById,
|
|
43
|
-
getAttachmentsForMessageUnscoped,
|
|
44
43
|
} from '../memory/attachments-store.js';
|
|
45
44
|
|
|
46
45
|
// Initialize db once before all tests
|
|
47
46
|
initializeDb();
|
|
48
47
|
|
|
49
48
|
afterAll(() => {
|
|
49
|
+
resetDb();
|
|
50
50
|
try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
|
|
51
51
|
});
|
|
52
52
|
|
|
@@ -236,11 +236,11 @@ describe('attachment orphan cleanup', () => {
|
|
|
236
236
|
addMessage(conv.id, 'user', 'hello');
|
|
237
237
|
const assistantMsg = addMessage(conv.id, 'assistant', 'Here is a file');
|
|
238
238
|
|
|
239
|
-
const stored = uploadAttachment('
|
|
239
|
+
const stored = uploadAttachment('chart.png', 'image/png', 'iVBOR');
|
|
240
240
|
linkAttachmentToMessage(assistantMsg.id, stored.id, 0);
|
|
241
241
|
|
|
242
242
|
// Verify attachment is linked
|
|
243
|
-
expect(
|
|
243
|
+
expect(getAttachmentsForMessage(assistantMsg.id)).toHaveLength(1);
|
|
244
244
|
|
|
245
245
|
// Delete the exchange — should also clean up orphaned attachments
|
|
246
246
|
deleteLastExchange(conv.id);
|
|
@@ -257,7 +257,7 @@ describe('attachment orphan cleanup', () => {
|
|
|
257
257
|
addMessage(conv.id, 'user', 'question');
|
|
258
258
|
const msg2 = addMessage(conv.id, 'assistant', 'second');
|
|
259
259
|
|
|
260
|
-
const shared = uploadAttachment('
|
|
260
|
+
const shared = uploadAttachment('shared.png', 'image/png', 'AAAA');
|
|
261
261
|
linkAttachmentToMessage(msg1.id, shared.id, 0);
|
|
262
262
|
linkAttachmentToMessage(msg2.id, shared.id, 0);
|
|
263
263
|
|
|
@@ -273,7 +273,7 @@ describe('attachment orphan cleanup', () => {
|
|
|
273
273
|
test('clearAll removes all attachments', () => {
|
|
274
274
|
const conv = createConversation('test');
|
|
275
275
|
const msg = addMessage(conv.id, 'assistant', 'file');
|
|
276
|
-
const stored = uploadAttachment('
|
|
276
|
+
const stored = uploadAttachment('doc.pdf', 'application/pdf', 'JVBER');
|
|
277
277
|
linkAttachmentToMessage(msg.id, stored.id, 0);
|
|
278
278
|
|
|
279
279
|
clearAll();
|
|
@@ -291,11 +291,11 @@ describe('attachment orphan cleanup', () => {
|
|
|
291
291
|
const assistantMsg = addMessage(conv.id, 'assistant', 'Here is a file');
|
|
292
292
|
|
|
293
293
|
// An attachment linked to the assistant message (should be cleaned up)
|
|
294
|
-
const linked = uploadAttachment('
|
|
294
|
+
const linked = uploadAttachment('chart.png', 'image/png', 'iVBOR');
|
|
295
295
|
linkAttachmentToMessage(assistantMsg.id, linked.id, 0);
|
|
296
296
|
|
|
297
297
|
// A freshly uploaded attachment not linked to any message (should survive)
|
|
298
|
-
uploadAttachment('
|
|
298
|
+
uploadAttachment('pending.png', 'image/png', 'AAAA');
|
|
299
299
|
|
|
300
300
|
deleteLastExchange(conv.id);
|
|
301
301
|
|
|
@@ -440,7 +440,7 @@ describe('attachment reuse across thread lifecycles', () => {
|
|
|
440
440
|
test('attachment uploaded in conversation A is retrievable by ID without any conversation reference', () => {
|
|
441
441
|
const convA = createConversation('Thread A');
|
|
442
442
|
const msgA = addMessage(convA.id, 'assistant', 'Here is a file');
|
|
443
|
-
const stored = uploadAttachment('
|
|
443
|
+
const stored = uploadAttachment('report.pdf', 'application/pdf', 'JVBER');
|
|
444
444
|
linkAttachmentToMessage(msgA.id, stored.id, 0);
|
|
445
445
|
|
|
446
446
|
// Create a completely separate conversation
|
|
@@ -448,8 +448,7 @@ describe('attachment reuse across thread lifecycles', () => {
|
|
|
448
448
|
addMessage(convB.id, 'user', 'hello');
|
|
449
449
|
|
|
450
450
|
// The attachment is retrievable by ID regardless of which conversation is active.
|
|
451
|
-
|
|
452
|
-
const fetched = getAttachmentById('ast-1', stored.id);
|
|
451
|
+
const fetched = getAttachmentById(stored.id);
|
|
453
452
|
expect(fetched).not.toBeNull();
|
|
454
453
|
expect(fetched!.id).toBe(stored.id);
|
|
455
454
|
expect(fetched!.originalFilename).toBe('report.pdf');
|
|
@@ -464,16 +463,16 @@ describe('attachment reuse across thread lifecycles', () => {
|
|
|
464
463
|
const msgB = addMessage(convB.id, 'assistant', 'Reused file');
|
|
465
464
|
|
|
466
465
|
// Upload once, link to both conversations
|
|
467
|
-
const stored = uploadAttachment('
|
|
466
|
+
const stored = uploadAttachment('shared.png', 'image/png', 'iVBORw0K');
|
|
468
467
|
linkAttachmentToMessage(msgA.id, stored.id, 0);
|
|
469
468
|
linkAttachmentToMessage(msgB.id, stored.id, 0);
|
|
470
469
|
|
|
471
470
|
// Both messages see the attachment
|
|
472
|
-
const linkedA = getAttachmentsForMessage(msgA.id
|
|
471
|
+
const linkedA = getAttachmentsForMessage(msgA.id);
|
|
473
472
|
expect(linkedA).toHaveLength(1);
|
|
474
473
|
expect(linkedA[0].id).toBe(stored.id);
|
|
475
474
|
|
|
476
|
-
const linkedB = getAttachmentsForMessage(msgB.id
|
|
475
|
+
const linkedB = getAttachmentsForMessage(msgB.id);
|
|
477
476
|
expect(linkedB).toHaveLength(1);
|
|
478
477
|
expect(linkedB[0].id).toBe(stored.id);
|
|
479
478
|
});
|
|
@@ -489,7 +488,7 @@ describe('attachment reuse across thread lifecycles', () => {
|
|
|
489
488
|
addMessage(convB.id, 'user', 'Show me the chart');
|
|
490
489
|
const msgB = addMessage(convB.id, 'assistant', 'Reused');
|
|
491
490
|
|
|
492
|
-
const stored = uploadAttachment('
|
|
491
|
+
const stored = uploadAttachment('chart.png', 'image/png', 'AAAA');
|
|
493
492
|
linkAttachmentToMessage(msgA.id, stored.id, 0);
|
|
494
493
|
linkAttachmentToMessage(msgB.id, stored.id, 0);
|
|
495
494
|
|
|
@@ -497,11 +496,11 @@ describe('attachment reuse across thread lifecycles', () => {
|
|
|
497
496
|
deleteLastExchange(convA.id);
|
|
498
497
|
|
|
499
498
|
// Attachment survives because convB still references it
|
|
500
|
-
const fetched = getAttachmentById(
|
|
499
|
+
const fetched = getAttachmentById(stored.id);
|
|
501
500
|
expect(fetched).not.toBeNull();
|
|
502
501
|
|
|
503
502
|
// convB's message still has the attachment linked
|
|
504
|
-
const linkedB = getAttachmentsForMessage(msgB.id
|
|
503
|
+
const linkedB = getAttachmentsForMessage(msgB.id);
|
|
505
504
|
expect(linkedB).toHaveLength(1);
|
|
506
505
|
expect(linkedB[0].id).toBe(stored.id);
|
|
507
506
|
});
|
|
@@ -514,8 +513,8 @@ describe('attachment reuse across thread lifecycles', () => {
|
|
|
514
513
|
addMessage(convB.id, 'user', 'upload in B');
|
|
515
514
|
|
|
516
515
|
// Same content uploaded in two different conversation contexts
|
|
517
|
-
const first = uploadAttachment('
|
|
518
|
-
const second = uploadAttachment('
|
|
516
|
+
const first = uploadAttachment('photo.png', 'image/png', 'DEDUPCROSS');
|
|
517
|
+
const second = uploadAttachment('photo.png', 'image/png', 'DEDUPCROSS');
|
|
519
518
|
|
|
520
519
|
// Dedup returns the same attachment row
|
|
521
520
|
expect(second.id).toBe(first.id);
|
|
@@ -540,11 +539,11 @@ describe('no private-thread attachment visibility boundary', () => {
|
|
|
540
539
|
expect(privateConv.threadType).toBe('private');
|
|
541
540
|
|
|
542
541
|
const msg = addMessage(privateConv.id, 'assistant', 'Private content');
|
|
543
|
-
const stored = uploadAttachment('
|
|
542
|
+
const stored = uploadAttachment('secret.pdf', 'application/pdf', 'JVBER');
|
|
544
543
|
linkAttachmentToMessage(msg.id, stored.id, 0);
|
|
545
544
|
|
|
546
545
|
// Attachment is globally visible by ID — no thread-type filter exists
|
|
547
|
-
const fetched = getAttachmentById(
|
|
546
|
+
const fetched = getAttachmentById(stored.id);
|
|
548
547
|
expect(fetched).not.toBeNull();
|
|
549
548
|
expect(fetched!.originalFilename).toBe('secret.pdf');
|
|
550
549
|
});
|
|
@@ -556,27 +555,26 @@ describe('no private-thread attachment visibility boundary', () => {
|
|
|
556
555
|
const privateMsg = addMessage(privateConv.id, 'assistant', 'Private file');
|
|
557
556
|
const standardMsg = addMessage(standardConv.id, 'assistant', 'Reusing private file');
|
|
558
557
|
|
|
559
|
-
const stored = uploadAttachment('
|
|
558
|
+
const stored = uploadAttachment('private-doc.png', 'image/png', 'PRIVDATA');
|
|
560
559
|
linkAttachmentToMessage(privateMsg.id, stored.id, 0);
|
|
561
560
|
linkAttachmentToMessage(standardMsg.id, stored.id, 0);
|
|
562
561
|
|
|
563
562
|
// Both threads can see the attachment
|
|
564
|
-
const linkedPrivate = getAttachmentsForMessage(privateMsg.id
|
|
563
|
+
const linkedPrivate = getAttachmentsForMessage(privateMsg.id);
|
|
565
564
|
expect(linkedPrivate).toHaveLength(1);
|
|
566
565
|
|
|
567
|
-
const linkedStandard = getAttachmentsForMessage(standardMsg.id
|
|
566
|
+
const linkedStandard = getAttachmentsForMessage(standardMsg.id);
|
|
568
567
|
expect(linkedStandard).toHaveLength(1);
|
|
569
568
|
expect(linkedStandard[0].id).toBe(stored.id);
|
|
570
569
|
});
|
|
571
570
|
|
|
572
|
-
test('
|
|
571
|
+
test('getAttachmentsForMessage returns private thread attachments', () => {
|
|
573
572
|
const privateConv = createConversation({ title: 'Private', threadType: 'private' });
|
|
574
573
|
const msg = addMessage(privateConv.id, 'assistant', 'Private media');
|
|
575
|
-
const stored = uploadAttachment('
|
|
574
|
+
const stored = uploadAttachment('photo.jpg', 'image/jpeg', 'AAAA');
|
|
576
575
|
linkAttachmentToMessage(msg.id, stored.id, 0);
|
|
577
576
|
|
|
578
|
-
|
|
579
|
-
const linked = getAttachmentsForMessageUnscoped(msg.id);
|
|
577
|
+
const linked = getAttachmentsForMessage(msg.id);
|
|
580
578
|
expect(linked).toHaveLength(1);
|
|
581
579
|
expect(linked[0].id).toBe(stored.id);
|
|
582
580
|
});
|
|
@@ -586,8 +584,8 @@ describe('no private-thread attachment visibility boundary', () => {
|
|
|
586
584
|
createConversation({ title: 'Standard', threadType: 'standard' });
|
|
587
585
|
|
|
588
586
|
// Same content uploaded in private and standard contexts
|
|
589
|
-
const fromPrivate = uploadAttachment('
|
|
590
|
-
const fromStandard = uploadAttachment('
|
|
587
|
+
const fromPrivate = uploadAttachment('file.png', 'image/png', 'CROSSTHREAD');
|
|
588
|
+
const fromStandard = uploadAttachment('file.png', 'image/png', 'CROSSTHREAD');
|
|
591
589
|
|
|
592
590
|
// Dedup returns the same row — no thread-type isolation
|
|
593
591
|
expect(fromStandard.id).toBe(fromPrivate.id);
|
|
@@ -600,8 +598,8 @@ describe('no private-thread attachment visibility boundary', () => {
|
|
|
600
598
|
const privateMsg = addMessage(privateConv.id, 'assistant', 'Private file');
|
|
601
599
|
const standardMsg = addMessage(standardConv.id, 'assistant', 'Standard file');
|
|
602
600
|
|
|
603
|
-
const att1 = uploadAttachment('
|
|
604
|
-
const att2 = uploadAttachment('
|
|
601
|
+
const att1 = uploadAttachment('private.png', 'image/png', 'PRIV');
|
|
602
|
+
const att2 = uploadAttachment('standard.png', 'image/png', 'STD');
|
|
605
603
|
linkAttachmentToMessage(privateMsg.id, att1.id, 0);
|
|
606
604
|
linkAttachmentToMessage(standardMsg.id, att2.id, 0);
|
|
607
605
|
|
|
@@ -185,6 +185,10 @@ describe('Invariant 2: no generic plaintext secret read API', () => {
|
|
|
185
185
|
'email/providers/index.ts', // email provider API key lookup
|
|
186
186
|
'tools/network/script-proxy/session-manager.ts', // proxy credential injection at runtime
|
|
187
187
|
'messaging/registry.ts', // checks stored credentials for connected providers
|
|
188
|
+
'calls/twilio-config.ts', // call infrastructure credential lookup
|
|
189
|
+
'calls/twilio-provider.ts', // call infrastructure credential lookup
|
|
190
|
+
'runtime/http-server.ts', // HTTP server credential lookup
|
|
191
|
+
'daemon/handlers/twitter-auth.ts', // Twitter OAuth token storage
|
|
188
192
|
]);
|
|
189
193
|
|
|
190
194
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|