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
|
@@ -35,7 +35,7 @@ mock.module('../config/loader.js', () => ({
|
|
|
35
35
|
}),
|
|
36
36
|
}));
|
|
37
37
|
|
|
38
|
-
import { initializeDb, getDb } from '../memory/db.js';
|
|
38
|
+
import { initializeDb, getDb, resetDb } from '../memory/db.js';
|
|
39
39
|
import { uploadAttachment, linkAttachmentToMessage } from '../memory/attachments-store.js';
|
|
40
40
|
import { createConversation, addMessage } from '../memory/conversation-store.js';
|
|
41
41
|
import { searchAttachments } from '../tools/assets/search.js';
|
|
@@ -45,6 +45,7 @@ import type { ToolContext } from '../tools/types.js';
|
|
|
45
45
|
initializeDb();
|
|
46
46
|
|
|
47
47
|
afterAll(() => {
|
|
48
|
+
resetDb();
|
|
48
49
|
try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
|
|
49
50
|
});
|
|
50
51
|
|
|
@@ -66,10 +67,10 @@ function seedAttachments() {
|
|
|
66
67
|
// Force createdAt by uploading then manipulating the DB
|
|
67
68
|
const db = getDb();
|
|
68
69
|
|
|
69
|
-
const png1 = uploadAttachment('
|
|
70
|
-
const jpg1 = uploadAttachment('
|
|
71
|
-
const pdf1 = uploadAttachment('
|
|
72
|
-
const png2 = uploadAttachment('
|
|
70
|
+
const png1 = uploadAttachment('selfie.png', 'image/png', 'AAAA');
|
|
71
|
+
const jpg1 = uploadAttachment('photo.jpg', 'image/jpeg', 'BBBB');
|
|
72
|
+
const pdf1 = uploadAttachment('report.pdf', 'application/pdf', 'CCCC');
|
|
73
|
+
const png2 = uploadAttachment('screenshot.png', 'image/png', 'DDDD');
|
|
73
74
|
|
|
74
75
|
// Backdate some attachments for recency testing
|
|
75
76
|
db.run(`UPDATE attachments SET created_at = ${oneDayAgo} WHERE id = '${jpg1.id}'`);
|
|
@@ -217,8 +218,8 @@ describe('searchAttachments with conversation_id', () => {
|
|
|
217
218
|
beforeEach(resetTables);
|
|
218
219
|
|
|
219
220
|
test('returns only attachments linked to the specified conversation', () => {
|
|
220
|
-
const png1 = uploadAttachment('
|
|
221
|
-
const png2 = uploadAttachment('
|
|
221
|
+
const png1 = uploadAttachment('in-conv.png', 'image/png', 'AAAA');
|
|
222
|
+
const png2 = uploadAttachment('other-conv.png', 'image/png', 'BBBB');
|
|
222
223
|
|
|
223
224
|
const conv1 = createConversation();
|
|
224
225
|
const conv2 = createConversation();
|
|
@@ -234,7 +235,7 @@ describe('searchAttachments with conversation_id', () => {
|
|
|
234
235
|
});
|
|
235
236
|
|
|
236
237
|
test('returns empty when conversation has no attachments', () => {
|
|
237
|
-
uploadAttachment('
|
|
238
|
+
uploadAttachment('orphan.png', 'image/png', 'AAAA');
|
|
238
239
|
const conv = createConversation();
|
|
239
240
|
addMessage(conv.id, 'user', 'No attachments here');
|
|
240
241
|
|
|
@@ -243,14 +244,14 @@ describe('searchAttachments with conversation_id', () => {
|
|
|
243
244
|
});
|
|
244
245
|
|
|
245
246
|
test('returns empty for nonexistent conversation_id', () => {
|
|
246
|
-
uploadAttachment('
|
|
247
|
+
uploadAttachment('file.png', 'image/png', 'AAAA');
|
|
247
248
|
const results = searchAttachments({ conversation_id: 'conv-nonexistent' });
|
|
248
249
|
expect(results.length).toBe(0);
|
|
249
250
|
});
|
|
250
251
|
|
|
251
252
|
test('combines conversation_id with mime_type filter', () => {
|
|
252
|
-
const png = uploadAttachment('
|
|
253
|
-
const pdf = uploadAttachment('
|
|
253
|
+
const png = uploadAttachment('image.png', 'image/png', 'AAAA');
|
|
254
|
+
const pdf = uploadAttachment('doc.pdf', 'application/pdf', 'BBBB');
|
|
254
255
|
|
|
255
256
|
const conv = createConversation();
|
|
256
257
|
const msg = addMessage(conv.id, 'user', 'Both types');
|
|
@@ -264,8 +265,8 @@ describe('searchAttachments with conversation_id', () => {
|
|
|
264
265
|
});
|
|
265
266
|
|
|
266
267
|
test('combines conversation_id with filename filter', () => {
|
|
267
|
-
const a = uploadAttachment('
|
|
268
|
-
const b = uploadAttachment('
|
|
268
|
+
const a = uploadAttachment('target.png', 'image/png', 'AAAA');
|
|
269
|
+
const b = uploadAttachment('other.png', 'image/png', 'BBBB');
|
|
269
270
|
|
|
270
271
|
const conv = createConversation();
|
|
271
272
|
const msg = addMessage(conv.id, 'user', 'Both');
|
|
@@ -294,7 +295,7 @@ describe('AssetSearchTool.execute', () => {
|
|
|
294
295
|
});
|
|
295
296
|
|
|
296
297
|
test('returns formatted results for matching assets', async () => {
|
|
297
|
-
uploadAttachment('
|
|
298
|
+
uploadAttachment('selfie.png', 'image/png', 'AAAA');
|
|
298
299
|
const result = await tool.execute({}, dummyContext);
|
|
299
300
|
expect(result.isError).toBe(false);
|
|
300
301
|
expect(result.content).toContain('selfie.png');
|
|
@@ -320,14 +321,14 @@ describe('AssetSearchTool.execute', () => {
|
|
|
320
321
|
});
|
|
321
322
|
|
|
322
323
|
test('includes attachment ID in output', async () => {
|
|
323
|
-
const stored = uploadAttachment('
|
|
324
|
+
const stored = uploadAttachment('chart.png', 'image/png', 'AAAA');
|
|
324
325
|
const result = await tool.execute({}, dummyContext);
|
|
325
326
|
expect(result.isError).toBe(false);
|
|
326
327
|
expect(result.content).toContain(stored.id);
|
|
327
328
|
});
|
|
328
329
|
|
|
329
330
|
test('includes MIME type and kind in output', async () => {
|
|
330
|
-
uploadAttachment('
|
|
331
|
+
uploadAttachment('chart.png', 'image/png', 'AAAA');
|
|
331
332
|
const result = await tool.execute({}, dummyContext);
|
|
332
333
|
expect(result.isError).toBe(false);
|
|
333
334
|
expect(result.content).toContain('image/png');
|
|
@@ -363,7 +364,7 @@ describe('AssetSearchTool visibility policy', () => {
|
|
|
363
364
|
|
|
364
365
|
test('attachments from standard threads are visible from any context', async () => {
|
|
365
366
|
const standardConv = createConversation({ title: 'standard-conv' });
|
|
366
|
-
const attachment = uploadAttachment('
|
|
367
|
+
const attachment = uploadAttachment('public.png', 'image/png', 'AAAA');
|
|
367
368
|
const msg = addMessage(standardConv.id, 'user', 'standard message');
|
|
368
369
|
linkAttachmentToMessage(msg.id, attachment.id, 0);
|
|
369
370
|
|
|
@@ -382,7 +383,7 @@ describe('AssetSearchTool visibility policy', () => {
|
|
|
382
383
|
|
|
383
384
|
test('attachments from private threads are visible within the same private thread', async () => {
|
|
384
385
|
const privateConv = createConversation({ title: 'private-conv', threadType: 'private' });
|
|
385
|
-
const attachment = uploadAttachment('
|
|
386
|
+
const attachment = uploadAttachment('secret.png', 'image/png', 'AAAA');
|
|
386
387
|
const msg = addMessage(privateConv.id, 'user', 'private message');
|
|
387
388
|
linkAttachmentToMessage(msg.id, attachment.id, 0);
|
|
388
389
|
|
|
@@ -400,7 +401,7 @@ describe('AssetSearchTool visibility policy', () => {
|
|
|
400
401
|
|
|
401
402
|
test('attachments from private threads are NOT visible from a different conversation', async () => {
|
|
402
403
|
const privateConv = createConversation({ title: 'private-conv', threadType: 'private' });
|
|
403
|
-
const attachment = uploadAttachment('
|
|
404
|
+
const attachment = uploadAttachment('secret.png', 'image/png', 'AAAA');
|
|
404
405
|
const msg = addMessage(privateConv.id, 'user', 'private message');
|
|
405
406
|
linkAttachmentToMessage(msg.id, attachment.id, 0);
|
|
406
407
|
|
|
@@ -419,7 +420,7 @@ describe('AssetSearchTool visibility policy', () => {
|
|
|
419
420
|
|
|
420
421
|
test('attachments from private threads are NOT visible from standard threads', async () => {
|
|
421
422
|
const privateConv = createConversation({ title: 'private-conv', threadType: 'private' });
|
|
422
|
-
const attachment = uploadAttachment('
|
|
423
|
+
const attachment = uploadAttachment('secret.png', 'image/png', 'AAAA');
|
|
423
424
|
const msg = addMessage(privateConv.id, 'user', 'private message');
|
|
424
425
|
linkAttachmentToMessage(msg.id, attachment.id, 0);
|
|
425
426
|
|
|
@@ -439,7 +440,7 @@ describe('AssetSearchTool visibility policy', () => {
|
|
|
439
440
|
test('attachment linked to both private and standard threads is visible everywhere', async () => {
|
|
440
441
|
const privateConv = createConversation({ title: 'private-conv', threadType: 'private' });
|
|
441
442
|
const standardConv = createConversation({ title: 'standard-conv' });
|
|
442
|
-
const attachment = uploadAttachment('
|
|
443
|
+
const attachment = uploadAttachment('shared.png', 'image/png', 'AAAA');
|
|
443
444
|
|
|
444
445
|
const msg1 = addMessage(privateConv.id, 'user', 'private message');
|
|
445
446
|
const msg2 = addMessage(standardConv.id, 'user', 'standard message');
|
|
@@ -460,7 +461,7 @@ describe('AssetSearchTool visibility policy', () => {
|
|
|
460
461
|
});
|
|
461
462
|
|
|
462
463
|
test('orphan attachments (no message linkage) remain visible', async () => {
|
|
463
|
-
uploadAttachment('
|
|
464
|
+
uploadAttachment('orphan.png', 'image/png', 'AAAA');
|
|
464
465
|
|
|
465
466
|
const conv = createConversation({ title: 'any-conv' });
|
|
466
467
|
const context: ToolContext = {
|
|
@@ -34,7 +34,7 @@ mock.module('../config/loader.js', () => ({
|
|
|
34
34
|
}),
|
|
35
35
|
}));
|
|
36
36
|
|
|
37
|
-
import { initializeDb, getDb } from '../memory/db.js';
|
|
37
|
+
import { initializeDb, getDb, resetDb } from '../memory/db.js';
|
|
38
38
|
import {
|
|
39
39
|
uploadAttachment,
|
|
40
40
|
deleteAttachment,
|
|
@@ -42,7 +42,6 @@ import {
|
|
|
42
42
|
getAttachmentById,
|
|
43
43
|
linkAttachmentToMessage,
|
|
44
44
|
getAttachmentsForMessage,
|
|
45
|
-
getAttachmentsForMessageUnscoped,
|
|
46
45
|
deleteOrphanAttachments,
|
|
47
46
|
validateAttachmentUpload,
|
|
48
47
|
isValidBase64,
|
|
@@ -54,6 +53,7 @@ import { createConversation, addMessage } from '../memory/conversation-store.js'
|
|
|
54
53
|
initializeDb();
|
|
55
54
|
|
|
56
55
|
afterAll(() => {
|
|
56
|
+
resetDb();
|
|
57
57
|
try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
|
|
58
58
|
});
|
|
59
59
|
|
|
@@ -73,10 +73,9 @@ describe('uploadAttachment', () => {
|
|
|
73
73
|
beforeEach(resetTables);
|
|
74
74
|
|
|
75
75
|
test('stores attachment and returns metadata', () => {
|
|
76
|
-
const stored = uploadAttachment('
|
|
76
|
+
const stored = uploadAttachment('chart.png', 'image/png', 'iVBORw0K');
|
|
77
77
|
|
|
78
78
|
expect(stored.id).toBeDefined();
|
|
79
|
-
expect(stored.assistantId).toBe('ast-1');
|
|
80
79
|
expect(stored.originalFilename).toBe('chart.png');
|
|
81
80
|
expect(stored.mimeType).toBe('image/png');
|
|
82
81
|
expect(stored.kind).toBe('image');
|
|
@@ -85,48 +84,42 @@ describe('uploadAttachment', () => {
|
|
|
85
84
|
});
|
|
86
85
|
|
|
87
86
|
test('classifies image MIME as image kind', () => {
|
|
88
|
-
const stored = uploadAttachment('
|
|
87
|
+
const stored = uploadAttachment('pic.jpg', 'image/jpeg', 'AAAA');
|
|
89
88
|
expect(stored.kind).toBe('image');
|
|
90
89
|
});
|
|
91
90
|
|
|
92
91
|
test('classifies non-image MIME as document kind', () => {
|
|
93
|
-
const stored = uploadAttachment('
|
|
92
|
+
const stored = uploadAttachment('doc.pdf', 'application/pdf', 'JVBER');
|
|
94
93
|
expect(stored.kind).toBe('document');
|
|
95
94
|
});
|
|
96
95
|
|
|
97
96
|
test('generates unique IDs for each upload', () => {
|
|
98
|
-
const a = uploadAttachment('
|
|
99
|
-
const b = uploadAttachment('
|
|
97
|
+
const a = uploadAttachment('a.txt', 'text/plain', 'AA==');
|
|
98
|
+
const b = uploadAttachment('b.txt', 'text/plain', 'QQ==');
|
|
100
99
|
expect(a.id).not.toBe(b.id);
|
|
101
100
|
});
|
|
102
101
|
|
|
103
102
|
test('computes sizeBytes from base64 correctly', () => {
|
|
104
103
|
// "hello" = "aGVsbG8=" (8 chars, 1 pad → 5 bytes)
|
|
105
|
-
const stored = uploadAttachment('
|
|
104
|
+
const stored = uploadAttachment('hello.txt', 'text/plain', 'aGVsbG8=');
|
|
106
105
|
expect(stored.sizeBytes).toBe(5);
|
|
107
106
|
});
|
|
108
107
|
|
|
109
|
-
test('deduplicates by content hash
|
|
110
|
-
const first = uploadAttachment('
|
|
111
|
-
const second = uploadAttachment('
|
|
108
|
+
test('deduplicates by content hash', () => {
|
|
109
|
+
const first = uploadAttachment('photo.png', 'image/png', 'iVBORw0KGgoAAAANSUh');
|
|
110
|
+
const second = uploadAttachment('photo.png', 'image/png', 'iVBORw0KGgoAAAANSUh');
|
|
112
111
|
expect(second.id).toBe(first.id);
|
|
113
112
|
});
|
|
114
113
|
|
|
115
114
|
test('deduplicates even when filenames differ', () => {
|
|
116
|
-
const first = uploadAttachment('
|
|
117
|
-
const second = uploadAttachment('
|
|
115
|
+
const first = uploadAttachment('original.png', 'image/png', 'DUPECONTENT123');
|
|
116
|
+
const second = uploadAttachment('renamed.png', 'image/png', 'DUPECONTENT123');
|
|
118
117
|
expect(second.id).toBe(first.id);
|
|
119
118
|
});
|
|
120
119
|
|
|
121
|
-
test('does not deduplicate across different assistants', () => {
|
|
122
|
-
const first = uploadAttachment('ast-1', 'file.txt', 'text/plain', 'CROSSASSISTANT');
|
|
123
|
-
const second = uploadAttachment('ast-2', 'file.txt', 'text/plain', 'CROSSASSISTANT');
|
|
124
|
-
expect(second.id).not.toBe(first.id);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
120
|
test('does not deduplicate different content', () => {
|
|
128
|
-
const first = uploadAttachment('
|
|
129
|
-
const second = uploadAttachment('
|
|
121
|
+
const first = uploadAttachment('a.txt', 'text/plain', 'CONTENTA');
|
|
122
|
+
const second = uploadAttachment('b.txt', 'text/plain', 'CONTENTB');
|
|
130
123
|
expect(second.id).not.toBe(first.id);
|
|
131
124
|
});
|
|
132
125
|
|
|
@@ -137,20 +130,20 @@ describe('uploadAttachment', () => {
|
|
|
137
130
|
const oversizedData = 'A'.repeat(oversizedLength);
|
|
138
131
|
|
|
139
132
|
expect(() =>
|
|
140
|
-
uploadAttachment('
|
|
133
|
+
uploadAttachment('huge.bin', 'application/octet-stream', oversizedData),
|
|
141
134
|
).toThrow(AttachmentUploadError);
|
|
142
135
|
});
|
|
143
136
|
|
|
144
137
|
test('rejects invalid base64 data', () => {
|
|
145
138
|
expect(() =>
|
|
146
|
-
uploadAttachment('
|
|
139
|
+
uploadAttachment('bad.txt', 'text/plain', '!!!not-base64!!!'),
|
|
147
140
|
).toThrow(AttachmentUploadError);
|
|
148
141
|
});
|
|
149
142
|
|
|
150
143
|
test('accepts base64 with non-standard padding/length', () => {
|
|
151
144
|
// Lenient on length — only character set is validated
|
|
152
145
|
expect(() =>
|
|
153
|
-
uploadAttachment('
|
|
146
|
+
uploadAttachment('ok.txt', 'text/plain', 'AAA'),
|
|
154
147
|
).not.toThrow();
|
|
155
148
|
});
|
|
156
149
|
|
|
@@ -161,7 +154,7 @@ describe('uploadAttachment', () => {
|
|
|
161
154
|
const exactData = 'A'.repeat(exactLength);
|
|
162
155
|
|
|
163
156
|
expect(() =>
|
|
164
|
-
uploadAttachment('
|
|
157
|
+
uploadAttachment('exact.bin', 'application/octet-stream', exactData),
|
|
165
158
|
).not.toThrow();
|
|
166
159
|
});
|
|
167
160
|
});
|
|
@@ -199,27 +192,17 @@ describe('deleteAttachment', () => {
|
|
|
199
192
|
beforeEach(resetTables);
|
|
200
193
|
|
|
201
194
|
test('deletes existing attachment and returns deleted', () => {
|
|
202
|
-
const stored = uploadAttachment('
|
|
203
|
-
const result = deleteAttachment(
|
|
195
|
+
const stored = uploadAttachment('file.txt', 'text/plain', 'dGVzdA==');
|
|
196
|
+
const result = deleteAttachment(stored.id);
|
|
204
197
|
expect(result).toBe('deleted');
|
|
205
198
|
|
|
206
|
-
const fetched = getAttachmentById(
|
|
199
|
+
const fetched = getAttachmentById(stored.id);
|
|
207
200
|
expect(fetched).toBeNull();
|
|
208
201
|
});
|
|
209
202
|
|
|
210
203
|
test('returns not_found for nonexistent attachment', () => {
|
|
211
|
-
const result = deleteAttachment('
|
|
212
|
-
expect(result).toBe('not_found');
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
test('returns not_found when assistantId does not match', () => {
|
|
216
|
-
const stored = uploadAttachment('ast-owner', 'file.txt', 'text/plain', 'dGVzdA==');
|
|
217
|
-
const result = deleteAttachment('ast-other', stored.id);
|
|
204
|
+
const result = deleteAttachment('nonexistent-id');
|
|
218
205
|
expect(result).toBe('not_found');
|
|
219
|
-
|
|
220
|
-
// Original still exists
|
|
221
|
-
const fetched = getAttachmentById('ast-owner', stored.id);
|
|
222
|
-
expect(fetched).not.toBeNull();
|
|
223
206
|
});
|
|
224
207
|
|
|
225
208
|
test('returns still_referenced when messages reference the attachment', () => {
|
|
@@ -228,35 +211,35 @@ describe('deleteAttachment', () => {
|
|
|
228
211
|
const msg2 = addMessage(conv.id, 'user', 'Duplicate upload');
|
|
229
212
|
|
|
230
213
|
// Dedup: both uploads return the same attachment row
|
|
231
|
-
const first = uploadAttachment('
|
|
232
|
-
const second = uploadAttachment('
|
|
214
|
+
const first = uploadAttachment('photo.png', 'image/png', 'SHAREDCONTENT1');
|
|
215
|
+
const second = uploadAttachment('photo.png', 'image/png', 'SHAREDCONTENT1');
|
|
233
216
|
expect(second.id).toBe(first.id);
|
|
234
217
|
|
|
235
218
|
linkAttachmentToMessage(msg1.id, first.id, 0);
|
|
236
219
|
linkAttachmentToMessage(msg2.id, second.id, 0);
|
|
237
220
|
|
|
238
221
|
// Delete should return still_referenced and NOT remove the attachment row
|
|
239
|
-
const result = deleteAttachment(
|
|
222
|
+
const result = deleteAttachment(first.id);
|
|
240
223
|
expect(result).toBe('still_referenced');
|
|
241
224
|
|
|
242
225
|
// Attachment row still exists because messages reference it
|
|
243
|
-
const fetched = getAttachmentById(
|
|
226
|
+
const fetched = getAttachmentById(first.id);
|
|
244
227
|
expect(fetched).not.toBeNull();
|
|
245
228
|
|
|
246
229
|
// Both messages still see the attachment
|
|
247
|
-
const linked1 = getAttachmentsForMessage(msg1.id
|
|
230
|
+
const linked1 = getAttachmentsForMessage(msg1.id);
|
|
248
231
|
expect(linked1).toHaveLength(1);
|
|
249
|
-
const linked2 = getAttachmentsForMessage(msg2.id
|
|
232
|
+
const linked2 = getAttachmentsForMessage(msg2.id);
|
|
250
233
|
expect(linked2).toHaveLength(1);
|
|
251
234
|
});
|
|
252
235
|
|
|
253
236
|
test('deletes attachment when no messages reference it', () => {
|
|
254
|
-
const stored = uploadAttachment('
|
|
237
|
+
const stored = uploadAttachment('lonely.txt', 'text/plain', 'UNREFERENCED');
|
|
255
238
|
// No linkAttachmentToMessage call — zero references
|
|
256
|
-
const result = deleteAttachment(
|
|
239
|
+
const result = deleteAttachment(stored.id);
|
|
257
240
|
expect(result).toBe('deleted');
|
|
258
241
|
|
|
259
|
-
const fetched = getAttachmentById(
|
|
242
|
+
const fetched = getAttachmentById(stored.id);
|
|
260
243
|
expect(fetched).toBeNull();
|
|
261
244
|
});
|
|
262
245
|
});
|
|
@@ -269,31 +252,25 @@ describe('getAttachmentsByIds', () => {
|
|
|
269
252
|
beforeEach(resetTables);
|
|
270
253
|
|
|
271
254
|
test('returns matching attachments with data', () => {
|
|
272
|
-
const a = uploadAttachment('
|
|
273
|
-
const b = uploadAttachment('
|
|
255
|
+
const a = uploadAttachment('a.txt', 'text/plain', 'AAAA');
|
|
256
|
+
const b = uploadAttachment('b.txt', 'text/plain', 'BBBB');
|
|
274
257
|
|
|
275
|
-
const results = getAttachmentsByIds(
|
|
258
|
+
const results = getAttachmentsByIds([a.id, b.id]);
|
|
276
259
|
expect(results).toHaveLength(2);
|
|
277
260
|
expect(results[0].dataBase64).toBe('AAAA');
|
|
278
261
|
expect(results[1].dataBase64).toBe('BBBB');
|
|
279
262
|
});
|
|
280
263
|
|
|
281
264
|
test('returns empty array for empty IDs list', () => {
|
|
282
|
-
const results = getAttachmentsByIds(
|
|
265
|
+
const results = getAttachmentsByIds([]);
|
|
283
266
|
expect(results).toHaveLength(0);
|
|
284
267
|
});
|
|
285
268
|
|
|
286
269
|
test('skips IDs that do not exist', () => {
|
|
287
|
-
const a = uploadAttachment('
|
|
288
|
-
const results = getAttachmentsByIds(
|
|
270
|
+
const a = uploadAttachment('a.txt', 'text/plain', 'AAAA');
|
|
271
|
+
const results = getAttachmentsByIds([a.id, 'nonexistent']);
|
|
289
272
|
expect(results).toHaveLength(1);
|
|
290
273
|
});
|
|
291
|
-
|
|
292
|
-
test('enforces assistantId scoping', () => {
|
|
293
|
-
const a = uploadAttachment('ast-owner', 'a.txt', 'text/plain', 'AAAA');
|
|
294
|
-
const results = getAttachmentsByIds('ast-other', [a.id]);
|
|
295
|
-
expect(results).toHaveLength(0);
|
|
296
|
-
});
|
|
297
274
|
});
|
|
298
275
|
|
|
299
276
|
// ---------------------------------------------------------------------------
|
|
@@ -304,8 +281,8 @@ describe('getAttachmentById', () => {
|
|
|
304
281
|
beforeEach(resetTables);
|
|
305
282
|
|
|
306
283
|
test('returns attachment with data when found', () => {
|
|
307
|
-
const stored = uploadAttachment('
|
|
308
|
-
const result = getAttachmentById(
|
|
284
|
+
const stored = uploadAttachment('report.pdf', 'application/pdf', 'JVBER');
|
|
285
|
+
const result = getAttachmentById(stored.id);
|
|
309
286
|
|
|
310
287
|
expect(result).not.toBeNull();
|
|
311
288
|
expect(result!.id).toBe(stored.id);
|
|
@@ -313,14 +290,8 @@ describe('getAttachmentById', () => {
|
|
|
313
290
|
expect(result!.dataBase64).toBe('JVBER');
|
|
314
291
|
});
|
|
315
292
|
|
|
316
|
-
test('returns null for wrong assistantId', () => {
|
|
317
|
-
const stored = uploadAttachment('ast-1', 'file.txt', 'text/plain', 'dGVzdA==');
|
|
318
|
-
const result = getAttachmentById('ast-other', stored.id);
|
|
319
|
-
expect(result).toBeNull();
|
|
320
|
-
});
|
|
321
|
-
|
|
322
293
|
test('returns null for nonexistent ID', () => {
|
|
323
|
-
const result = getAttachmentById('
|
|
294
|
+
const result = getAttachmentById('no-such-id');
|
|
324
295
|
expect(result).toBeNull();
|
|
325
296
|
});
|
|
326
297
|
});
|
|
@@ -335,11 +306,11 @@ describe('linkAttachmentToMessage + getAttachmentsForMessage', () => {
|
|
|
335
306
|
test('links attachment and retrieves it by message', () => {
|
|
336
307
|
const conv = createConversation();
|
|
337
308
|
const msg = addMessage(conv.id, 'assistant', 'Here is a chart');
|
|
338
|
-
const stored = uploadAttachment('
|
|
309
|
+
const stored = uploadAttachment('chart.png', 'image/png', 'iVBORw0K');
|
|
339
310
|
|
|
340
311
|
linkAttachmentToMessage(msg.id, stored.id, 0);
|
|
341
312
|
|
|
342
|
-
const linked = getAttachmentsForMessage(msg.id
|
|
313
|
+
const linked = getAttachmentsForMessage(msg.id);
|
|
343
314
|
expect(linked).toHaveLength(1);
|
|
344
315
|
expect(linked[0].id).toBe(stored.id);
|
|
345
316
|
expect(linked[0].originalFilename).toBe('chart.png');
|
|
@@ -349,14 +320,14 @@ describe('linkAttachmentToMessage + getAttachmentsForMessage', () => {
|
|
|
349
320
|
test('returns attachments in position order', () => {
|
|
350
321
|
const conv = createConversation();
|
|
351
322
|
const msg = addMessage(conv.id, 'assistant', 'Multiple files');
|
|
352
|
-
const a = uploadAttachment('
|
|
353
|
-
const b = uploadAttachment('
|
|
323
|
+
const a = uploadAttachment('first.txt', 'text/plain', 'AAAA');
|
|
324
|
+
const b = uploadAttachment('second.txt', 'text/plain', 'BBBB');
|
|
354
325
|
|
|
355
326
|
// Link in reverse order
|
|
356
327
|
linkAttachmentToMessage(msg.id, b.id, 1);
|
|
357
328
|
linkAttachmentToMessage(msg.id, a.id, 0);
|
|
358
329
|
|
|
359
|
-
const linked = getAttachmentsForMessage(msg.id
|
|
330
|
+
const linked = getAttachmentsForMessage(msg.id);
|
|
360
331
|
expect(linked).toHaveLength(2);
|
|
361
332
|
expect(linked[0].originalFilename).toBe('first.txt');
|
|
362
333
|
expect(linked[1].originalFilename).toBe('second.txt');
|
|
@@ -366,49 +337,7 @@ describe('linkAttachmentToMessage + getAttachmentsForMessage', () => {
|
|
|
366
337
|
const conv = createConversation();
|
|
367
338
|
const msg = addMessage(conv.id, 'assistant', 'No attachments');
|
|
368
339
|
|
|
369
|
-
const linked = getAttachmentsForMessage(msg.id
|
|
370
|
-
expect(linked).toHaveLength(0);
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
test('enforces assistantId scoping on retrieval', () => {
|
|
374
|
-
const conv = createConversation();
|
|
375
|
-
const msg = addMessage(conv.id, 'assistant', 'Scoped');
|
|
376
|
-
const stored = uploadAttachment('ast-owner', 'secret.txt', 'text/plain', 'c2VjcmV0');
|
|
377
|
-
|
|
378
|
-
linkAttachmentToMessage(msg.id, stored.id, 0);
|
|
379
|
-
|
|
380
|
-
const wrongScope = getAttachmentsForMessage(msg.id, 'ast-other');
|
|
381
|
-
expect(wrongScope).toHaveLength(0);
|
|
382
|
-
|
|
383
|
-
const rightScope = getAttachmentsForMessage(msg.id, 'ast-owner');
|
|
384
|
-
expect(rightScope).toHaveLength(1);
|
|
385
|
-
});
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
// ---------------------------------------------------------------------------
|
|
389
|
-
// getAttachmentsForMessageUnscoped
|
|
390
|
-
// ---------------------------------------------------------------------------
|
|
391
|
-
|
|
392
|
-
describe('getAttachmentsForMessageUnscoped', () => {
|
|
393
|
-
beforeEach(resetTables);
|
|
394
|
-
|
|
395
|
-
test('returns attachments without assistant scoping', () => {
|
|
396
|
-
const conv = createConversation();
|
|
397
|
-
const msg = addMessage(conv.id, 'assistant', 'Desktop history');
|
|
398
|
-
const stored = uploadAttachment('ast-1', 'result.png', 'image/png', 'iVBORw0K');
|
|
399
|
-
|
|
400
|
-
linkAttachmentToMessage(msg.id, stored.id, 0);
|
|
401
|
-
|
|
402
|
-
const linked = getAttachmentsForMessageUnscoped(msg.id);
|
|
403
|
-
expect(linked).toHaveLength(1);
|
|
404
|
-
expect(linked[0].id).toBe(stored.id);
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
test('returns empty for message with no links', () => {
|
|
408
|
-
const conv = createConversation();
|
|
409
|
-
const msg = addMessage(conv.id, 'assistant', 'Nothing here');
|
|
410
|
-
|
|
411
|
-
const linked = getAttachmentsForMessageUnscoped(msg.id);
|
|
340
|
+
const linked = getAttachmentsForMessage(msg.id);
|
|
412
341
|
expect(linked).toHaveLength(0);
|
|
413
342
|
});
|
|
414
343
|
});
|
|
@@ -421,7 +350,7 @@ describe('deleteOrphanAttachments', () => {
|
|
|
421
350
|
beforeEach(resetTables);
|
|
422
351
|
|
|
423
352
|
test('removes candidate attachments with no message links', () => {
|
|
424
|
-
const stored = uploadAttachment('
|
|
353
|
+
const stored = uploadAttachment('orphan.txt', 'text/plain', 'ZGF0YQ==');
|
|
425
354
|
|
|
426
355
|
const removed = deleteOrphanAttachments([stored.id]);
|
|
427
356
|
expect(removed).toBe(1);
|
|
@@ -430,27 +359,27 @@ describe('deleteOrphanAttachments', () => {
|
|
|
430
359
|
test('preserves attachments that are still linked', () => {
|
|
431
360
|
const conv = createConversation();
|
|
432
361
|
const msg = addMessage(conv.id, 'assistant', 'With attachment');
|
|
433
|
-
const stored = uploadAttachment('
|
|
362
|
+
const stored = uploadAttachment('linked.txt', 'text/plain', 'ZGF0YQ==');
|
|
434
363
|
linkAttachmentToMessage(msg.id, stored.id, 0);
|
|
435
364
|
|
|
436
365
|
const removed = deleteOrphanAttachments([stored.id]);
|
|
437
366
|
expect(removed).toBe(0);
|
|
438
367
|
|
|
439
|
-
const fetched = getAttachmentById(
|
|
368
|
+
const fetched = getAttachmentById(stored.id);
|
|
440
369
|
expect(fetched).not.toBeNull();
|
|
441
370
|
});
|
|
442
371
|
|
|
443
372
|
test('removes only orphans when mixed candidates provided', () => {
|
|
444
373
|
const conv = createConversation();
|
|
445
374
|
const msg = addMessage(conv.id, 'assistant', 'Mixed');
|
|
446
|
-
const linked = uploadAttachment('
|
|
447
|
-
const orphan = uploadAttachment('
|
|
375
|
+
const linked = uploadAttachment('linked.txt', 'text/plain', 'AAAA');
|
|
376
|
+
const orphan = uploadAttachment('orphan.txt', 'text/plain', 'BBBB');
|
|
448
377
|
linkAttachmentToMessage(msg.id, linked.id, 0);
|
|
449
378
|
|
|
450
379
|
const removed = deleteOrphanAttachments([linked.id, orphan.id]);
|
|
451
380
|
expect(removed).toBe(1);
|
|
452
381
|
|
|
453
|
-
const remaining = getAttachmentById(
|
|
382
|
+
const remaining = getAttachmentById(linked.id);
|
|
454
383
|
expect(remaining).not.toBeNull();
|
|
455
384
|
});
|
|
456
385
|
|
|
@@ -460,14 +389,14 @@ describe('deleteOrphanAttachments', () => {
|
|
|
460
389
|
});
|
|
461
390
|
|
|
462
391
|
test('does not delete attachments outside the candidate set', () => {
|
|
463
|
-
const unrelated = uploadAttachment('
|
|
464
|
-
const candidate = uploadAttachment('
|
|
392
|
+
const unrelated = uploadAttachment('unrelated.txt', 'text/plain', 'AAAA');
|
|
393
|
+
const candidate = uploadAttachment('candidate.txt', 'text/plain', 'BBBB');
|
|
465
394
|
|
|
466
395
|
const removed = deleteOrphanAttachments([candidate.id]);
|
|
467
396
|
expect(removed).toBe(1);
|
|
468
397
|
|
|
469
398
|
// The unrelated attachment should still exist
|
|
470
|
-
const fetched = getAttachmentById(
|
|
399
|
+
const fetched = getAttachmentById(unrelated.id);
|
|
471
400
|
expect(fetched).not.toBeNull();
|
|
472
401
|
});
|
|
473
402
|
});
|
|
@@ -50,11 +50,12 @@ describe('browser skill cutover — startup tool payload', () => {
|
|
|
50
50
|
test('serialized tool definitions payload still exceeds a reasonable floor', () => {
|
|
51
51
|
const definitions = getAllToolDefinitions();
|
|
52
52
|
const serialized = JSON.stringify(definitions);
|
|
53
|
-
// Startup payload is ~
|
|
54
|
-
// Floor at 30 000 catches accidental wholesale removal; ceiling
|
|
55
|
-
//
|
|
53
|
+
// Startup payload is ~45 034 chars without browser tools.
|
|
54
|
+
// Floor at 30 000 catches accidental wholesale removal; ceiling at 47 000
|
|
55
|
+
// gives ~2 000 char headroom while still catching browser tool leakage
|
|
56
|
+
// (~4 640 chars would push it past the ceiling).
|
|
56
57
|
expect(serialized.length).toBeGreaterThan(30_000);
|
|
57
|
-
expect(serialized.length).toBeLessThan(
|
|
58
|
+
expect(serialized.length).toBeLessThan(47_000);
|
|
58
59
|
});
|
|
59
60
|
|
|
60
61
|
test('no browser-categorised tools remain in startup registry', () => {
|
|
@@ -71,10 +71,11 @@ describe('browser skill migration end-state', () => {
|
|
|
71
71
|
expect(defNames).not.toContain(name);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
// Payload ceiling:
|
|
75
|
-
//
|
|
74
|
+
// Payload ceiling: startup payload is ~45 034 chars. Browser tools
|
|
75
|
+
// contribute ~4 640 chars — if they leak back in, the total would exceed
|
|
76
|
+
// 47 000. The 2 000-char margin absorbs minor tool additions.
|
|
76
77
|
const payloadSize = JSON.stringify(definitions).length;
|
|
77
|
-
expect(payloadSize).toBeLessThan(
|
|
78
|
+
expect(payloadSize).toBeLessThan(47_000);
|
|
78
79
|
});
|
|
79
80
|
|
|
80
81
|
// ── 2. Browser skill exists and is active ──────────────────────────
|