@vellumai/assistant 0.3.15 → 0.3.16
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/ARCHITECTURE.md +142 -0
- package/Dockerfile +1 -1
- package/README.md +5 -5
- package/docs/architecture/http-token-refresh.md +252 -0
- package/docs/architecture/memory.md +5 -4
- package/docs/architecture/scheduling.md +4 -88
- package/docs/runbook-trusted-contacts.md +283 -0
- package/docs/trusted-contact-access.md +247 -0
- package/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +2 -6
- package/src/__tests__/access-request-decision.test.ts +331 -0
- package/src/__tests__/asset-materialize-tool.test.ts +7 -7
- package/src/__tests__/asset-search-tool.test.ts +15 -15
- package/src/__tests__/attachments-store.test.ts +13 -13
- package/src/__tests__/call-controller.test.ts +150 -4
- package/src/__tests__/call-conversation-messages.test.ts +2 -2
- package/src/__tests__/call-pointer-messages.test.ts +28 -0
- package/src/__tests__/call-start-guardian-guard.test.ts +93 -0
- package/src/__tests__/channel-approval-routes.test.ts +108 -12
- package/src/__tests__/channel-guardian.test.ts +16 -14
- package/src/__tests__/checker.test.ts +24 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +2 -2
- package/src/__tests__/config-watcher.test.ts +358 -0
- package/src/__tests__/conversation-pairing.test.ts +24 -24
- package/src/__tests__/conversation-store.test.ts +36 -36
- package/src/__tests__/date-context.test.ts +179 -1
- package/src/__tests__/db-migration-rollback.test.ts +4 -7
- package/src/__tests__/deterministic-verification-control-plane.test.ts +5 -5
- package/src/__tests__/emit-signal-routing-intent.test.ts +179 -0
- package/src/__tests__/gateway-only-guard.test.ts +188 -0
- package/src/__tests__/guardian-action-conversation-turn.test.ts +451 -0
- package/src/__tests__/guardian-action-copy-generator.test.ts +197 -0
- package/src/__tests__/guardian-action-followup-executor.test.ts +379 -0
- package/src/__tests__/guardian-action-followup-store.test.ts +376 -0
- package/src/__tests__/guardian-action-late-reply.test.ts +294 -0
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +71 -0
- package/src/__tests__/guardian-action-sweep.test.ts +9 -9
- package/src/__tests__/guardian-outbound-http.test.ts +194 -2
- package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
- package/src/__tests__/handlers-telegram-config.test.ts +6 -6
- package/src/__tests__/hooks-runner.test.ts +13 -4
- package/src/__tests__/ingress-routes-http.test.ts +443 -0
- package/src/__tests__/intent-routing.test.ts +14 -0
- package/src/__tests__/ipc-snapshot.test.ts +2 -5
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
- package/src/__tests__/memory-regressions.test.ts +16 -12
- package/src/__tests__/non-member-access-request.test.ts +282 -0
- package/src/__tests__/notification-decision-strategy.test.ts +136 -0
- package/src/__tests__/notification-routing-intent.test.ts +11 -1
- package/src/__tests__/notification-thread-candidates.test.ts +166 -0
- package/src/__tests__/recording-intent.test.ts +1 -0
- package/src/__tests__/recording-state-machine.test.ts +328 -17
- package/src/__tests__/registry.test.ts +17 -8
- package/src/__tests__/relay-server.test.ts +105 -0
- package/src/__tests__/reminder.test.ts +13 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
- package/src/__tests__/scheduler-recurrence.test.ts +50 -0
- package/src/__tests__/server-history-render.test.ts +8 -8
- package/src/__tests__/session-agent-loop.test.ts +1 -0
- package/src/__tests__/session-runtime-assembly.test.ts +49 -0
- package/src/__tests__/session-skill-tools.test.ts +1 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
- package/src/__tests__/slack-channel-config.test.ts +230 -0
- package/src/__tests__/subagent-manager-notify.test.ts +4 -4
- package/src/__tests__/swarm-session-integration.test.ts +2 -2
- package/src/__tests__/system-prompt.test.ts +43 -0
- package/src/__tests__/task-management-tools.test.ts +3 -3
- package/src/__tests__/task-tools.test.ts +3 -3
- package/src/__tests__/trust-store.test.ts +17 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +491 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +409 -0
- package/src/__tests__/trusted-contact-verification.test.ts +360 -0
- package/src/__tests__/update-bulletin-format.test.ts +119 -0
- package/src/__tests__/update-bulletin-state.test.ts +129 -0
- package/src/__tests__/update-bulletin.test.ts +260 -0
- package/src/__tests__/update-template-contract.test.ts +29 -0
- package/src/agent/loop.ts +2 -2
- package/src/amazon/client.ts +2 -3
- package/src/calls/call-controller.ts +115 -34
- package/src/calls/call-conversation-messages.ts +2 -2
- package/src/calls/call-domain.ts +10 -3
- package/src/calls/call-pointer-messages.ts +17 -5
- package/src/calls/guardian-action-sweep.ts +77 -36
- package/src/calls/relay-server.ts +51 -12
- package/src/calls/twilio-routes.ts +3 -1
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +4 -4
- package/src/cli/core-commands.ts +3 -3
- package/src/cli/map.ts +8 -5
- package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
- package/src/config/bundled-skills/tasks/SKILL.md +1 -1
- package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
- package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
- package/src/config/computer-use-prompt.ts +1 -0
- package/src/config/core-schema.ts +16 -0
- package/src/config/env-registry.ts +1 -0
- package/src/config/env.ts +16 -1
- package/src/config/memory-schema.ts +5 -0
- package/src/config/schema.ts +4 -0
- package/src/config/system-prompt.ts +69 -2
- package/src/config/templates/BOOTSTRAP.md +1 -1
- package/src/config/templates/IDENTITY.md +8 -4
- package/src/config/templates/SOUL.md +14 -0
- package/src/config/templates/UPDATES.md +16 -0
- package/src/config/templates/USER.md +5 -1
- package/src/config/types.ts +1 -0
- package/src/config/update-bulletin-format.ts +52 -0
- package/src/config/update-bulletin-state.ts +49 -0
- package/src/config/update-bulletin.ts +82 -0
- package/src/config/vellum-skills/catalog.json +6 -0
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
- package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
- package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
- package/src/context/window-manager.ts +43 -3
- package/src/daemon/config-watcher.ts +1 -0
- package/src/daemon/connection-policy.ts +21 -1
- package/src/daemon/daemon-control.ts +164 -7
- package/src/daemon/date-context.ts +174 -1
- package/src/daemon/guardian-action-generators.ts +175 -0
- package/src/daemon/guardian-verification-intent.ts +120 -0
- package/src/daemon/handlers/apps.ts +1 -3
- package/src/daemon/handlers/config-channels.ts +2 -2
- package/src/daemon/handlers/config-heartbeat.ts +1 -1
- package/src/daemon/handlers/config-inbox.ts +55 -159
- package/src/daemon/handlers/config-ingress.ts +1 -1
- package/src/daemon/handlers/config-integrations.ts +1 -1
- package/src/daemon/handlers/config-platform.ts +1 -1
- package/src/daemon/handlers/config-scheduling.ts +2 -2
- package/src/daemon/handlers/config-slack-channel.ts +190 -0
- package/src/daemon/handlers/config-telegram.ts +1 -1
- package/src/daemon/handlers/config-twilio.ts +1 -1
- package/src/daemon/handlers/config-voice.ts +100 -0
- package/src/daemon/handlers/config.ts +3 -0
- package/src/daemon/handlers/misc.ts +83 -5
- package/src/daemon/handlers/navigate-settings.ts +27 -0
- package/src/daemon/handlers/recording.ts +270 -144
- package/src/daemon/handlers/sessions.ts +100 -17
- package/src/daemon/handlers/subagents.ts +3 -3
- package/src/daemon/handlers/work-items.ts +10 -7
- package/src/daemon/ipc-contract/integrations.ts +9 -1
- package/src/daemon/ipc-contract/messages.ts +4 -0
- package/src/daemon/ipc-contract/sessions.ts +1 -1
- package/src/daemon/ipc-contract/settings.ts +26 -0
- package/src/daemon/ipc-contract/shared.ts +2 -0
- package/src/daemon/ipc-contract/work-items.ts +1 -7
- package/src/daemon/ipc-contract-inventory.json +5 -1
- package/src/daemon/ipc-contract.ts +5 -1
- package/src/daemon/lifecycle.ts +306 -266
- package/src/daemon/recording-intent.ts +0 -41
- package/src/daemon/response-tier.ts +2 -2
- package/src/daemon/server.ts +6 -6
- package/src/daemon/session-agent-loop-handlers.ts +34 -9
- package/src/daemon/session-agent-loop.ts +15 -8
- package/src/daemon/session-history.ts +3 -2
- package/src/daemon/session-media-retry.ts +3 -0
- package/src/daemon/session-messaging.ts +38 -4
- package/src/daemon/session-notifiers.ts +2 -2
- package/src/daemon/session-process.ts +256 -23
- package/src/daemon/session-queue-manager.ts +2 -0
- package/src/daemon/session-runtime-assembly.ts +39 -0
- package/src/daemon/session-skill-tools.ts +13 -4
- package/src/daemon/session-tool-setup.ts +5 -6
- package/src/daemon/session.ts +19 -8
- package/src/daemon/tls-certs.ts +55 -13
- package/src/daemon/tool-side-effects.ts +13 -5
- package/src/gallery/default-gallery.ts +32 -9
- package/src/influencer/client.ts +2 -1
- package/src/memory/channel-delivery-store.ts +37 -567
- package/src/memory/channel-guardian-store.ts +66 -1317
- package/src/memory/conflict-store.ts +4 -4
- package/src/memory/conversation-attention-store.ts +0 -3
- package/src/memory/conversation-crud.ts +668 -0
- package/src/memory/conversation-queries.ts +361 -0
- package/src/memory/conversation-store.ts +45 -983
- package/src/memory/db-connection.ts +3 -0
- package/src/memory/db-init.ts +25 -0
- package/src/memory/delivery-channels.ts +175 -0
- package/src/memory/delivery-crud.ts +211 -0
- package/src/memory/delivery-status.ts +199 -0
- package/src/memory/embedding-backend.ts +70 -4
- package/src/memory/embedding-local.ts +12 -2
- package/src/memory/entity-extractor.ts +3 -8
- package/src/memory/fts-reconciler.ts +121 -0
- package/src/memory/guardian-action-store.ts +366 -3
- package/src/memory/guardian-approvals.ts +569 -0
- package/src/memory/guardian-bindings.ts +130 -0
- package/src/memory/guardian-rate-limits.ts +196 -0
- package/src/memory/guardian-verification.ts +520 -0
- package/src/memory/job-handlers/index-maintenance.ts +2 -1
- package/src/memory/job-utils.ts +8 -5
- package/src/memory/jobs-store.ts +66 -6
- package/src/memory/jobs-worker.ts +23 -1
- package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
- package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
- package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
- package/src/memory/migrations/100-core-tables.ts +1 -1
- package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
- package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
- package/src/memory/migrations/112-assistant-inbox.ts +1 -1
- package/src/memory/migrations/113-late-migrations.ts +1 -1
- package/src/memory/migrations/116-messages-fts.ts +13 -0
- package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
- package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
- package/src/memory/migrations/index.ts +8 -3
- package/src/memory/migrations/validate-migration-state.ts +114 -15
- package/src/memory/qdrant-circuit-breaker.ts +105 -0
- package/src/memory/retriever.ts +46 -13
- package/src/memory/schema-migration.ts +3 -0
- package/src/memory/schema.ts +25 -7
- package/src/memory/search/semantic.ts +8 -90
- package/src/notifications/README.md +1 -1
- package/src/notifications/broadcaster.ts +20 -2
- package/src/notifications/conversation-pairing.ts +3 -3
- package/src/notifications/decision-engine.ts +173 -8
- package/src/notifications/deliveries-store.ts +27 -8
- package/src/notifications/preferences-store.ts +7 -7
- package/src/notifications/thread-candidates.ts +234 -0
- package/src/notifications/types.ts +18 -0
- package/src/permissions/defaults.ts +11 -1
- package/src/permissions/prompter.ts +17 -0
- package/src/permissions/trust-store.ts +2 -0
- package/src/providers/failover.ts +19 -0
- package/src/providers/registry.ts +46 -1
- package/src/runtime/approval-message-composer.ts +1 -1
- package/src/runtime/channel-guardian-service.ts +15 -3
- package/src/runtime/channel-retry-sweep.ts +7 -2
- package/src/runtime/guardian-action-conversation-turn.ts +85 -0
- package/src/runtime/guardian-action-followup-executor.ts +301 -0
- package/src/runtime/guardian-action-message-composer.ts +245 -0
- package/src/runtime/guardian-outbound-actions.ts +26 -6
- package/src/runtime/guardian-verification-templates.ts +15 -9
- package/src/runtime/http-errors.ts +93 -0
- package/src/runtime/http-server.ts +133 -44
- package/src/runtime/http-types.ts +53 -0
- package/src/runtime/ingress-service.ts +237 -0
- package/src/runtime/middleware/error-handler.ts +4 -3
- package/src/runtime/middleware/rate-limiter.ts +160 -0
- package/src/runtime/middleware/request-logger.ts +71 -0
- package/src/runtime/middleware/twilio-validation.ts +7 -6
- package/src/runtime/pending-interactions.ts +12 -0
- package/src/runtime/routes/access-request-decision.ts +215 -0
- package/src/runtime/routes/app-routes.ts +25 -18
- package/src/runtime/routes/approval-routes.ts +18 -47
- package/src/runtime/routes/attachment-routes.ts +15 -41
- package/src/runtime/routes/call-routes.ts +20 -20
- package/src/runtime/routes/channel-delivery-routes.ts +6 -5
- package/src/runtime/routes/contact-routes.ts +4 -9
- package/src/runtime/routes/conversation-attention-routes.ts +2 -1
- package/src/runtime/routes/conversation-routes.ts +26 -57
- package/src/runtime/routes/debug-routes.ts +71 -0
- package/src/runtime/routes/events-routes.ts +3 -2
- package/src/runtime/routes/guardian-approval-interception.ts +221 -0
- package/src/runtime/routes/identity-routes.ts +14 -10
- package/src/runtime/routes/inbound-conversation.ts +3 -2
- package/src/runtime/routes/inbound-message-handler.ts +527 -62
- package/src/runtime/routes/ingress-routes.ts +174 -0
- package/src/runtime/routes/integration-routes.ts +78 -16
- package/src/runtime/routes/pairing-routes.ts +11 -10
- package/src/runtime/routes/secret-routes.ts +10 -18
- package/src/runtime/verification-rate-limiter.ts +83 -0
- package/src/schedule/schedule-store.ts +13 -1
- package/src/schedule/scheduler.ts +1 -1
- package/src/security/secret-ingress.ts +5 -2
- package/src/security/secret-scanner.ts +72 -6
- package/src/subagent/manager.ts +6 -4
- package/src/swarm/plan-validator.ts +4 -1
- package/src/tasks/task-runner.ts +3 -1
- package/src/tools/browser/api-map.ts +9 -6
- package/src/tools/calls/call-start.ts +20 -0
- package/src/tools/executor.ts +50 -568
- package/src/tools/permission-checker.ts +272 -0
- package/src/tools/registry.ts +14 -6
- package/src/tools/reminder/reminder-store.ts +7 -7
- package/src/tools/reminder/reminder.ts +6 -3
- package/src/tools/secret-detection-handler.ts +301 -0
- package/src/tools/subagent/message.ts +1 -1
- package/src/tools/system/voice-config.ts +62 -0
- package/src/tools/tasks/index.ts +3 -3
- package/src/tools/tasks/work-item-list.ts +3 -3
- package/src/tools/tasks/work-item-update.ts +4 -5
- package/src/tools/tool-approval-handler.ts +192 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/watcher/watcher-store.ts +9 -9
- package/src/work-items/work-item-runner.ts +9 -6
- /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts → 026a-embeddings-nullable-vector-json.ts} +0 -0
- /package/src/memory/migrations/{027-guardian-bootstrap-token.ts → 027a-guardian-bootstrap-token.ts} +0 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the access request decision flow.
|
|
3
|
+
*
|
|
4
|
+
* When a guardian approves or denies an `ingress_access_request`:
|
|
5
|
+
* - Approve: creates a verification session, delivers code to guardian,
|
|
6
|
+
* notifies requester to expect a code.
|
|
7
|
+
* - Deny: sends refusal reply to requester.
|
|
8
|
+
* - Stale: handles already-resolved requests gracefully.
|
|
9
|
+
* - Idempotent: approving same request twice does not create duplicate sessions.
|
|
10
|
+
*/
|
|
11
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
|
|
15
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Test isolation: in-memory SQLite via temp directory
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
const testDir = mkdtempSync(join(tmpdir(), 'access-request-decision-test-'));
|
|
22
|
+
|
|
23
|
+
mock.module('../util/platform.js', () => ({
|
|
24
|
+
getRootDir: () => testDir,
|
|
25
|
+
getDataDir: () => testDir,
|
|
26
|
+
isMacOS: () => process.platform === 'darwin',
|
|
27
|
+
isLinux: () => process.platform === 'linux',
|
|
28
|
+
isWindows: () => process.platform === 'win32',
|
|
29
|
+
getSocketPath: () => join(testDir, 'test.sock'),
|
|
30
|
+
getPidPath: () => join(testDir, 'test.pid'),
|
|
31
|
+
getDbPath: () => join(testDir, 'test.db'),
|
|
32
|
+
getLogPath: () => join(testDir, 'test.log'),
|
|
33
|
+
ensureDataDir: () => {},
|
|
34
|
+
normalizeAssistantId: (id: string) => id === 'self' ? 'self' : id,
|
|
35
|
+
readHttpToken: () => 'test-bearer-token',
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
mock.module('../util/logger.js', () => ({
|
|
39
|
+
getLogger: () => new Proxy({} as Record<string, unknown>, {
|
|
40
|
+
get: () => () => {},
|
|
41
|
+
}),
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
// Track deliverChannelReply calls and allow injecting failures
|
|
45
|
+
const deliverReplyCalls: Array<{ url: string; payload: Record<string, unknown> }> = [];
|
|
46
|
+
let deliverReplyError: Error | null = null;
|
|
47
|
+
mock.module('../runtime/gateway-client.js', () => ({
|
|
48
|
+
deliverChannelReply: async (url: string, payload: Record<string, unknown>) => {
|
|
49
|
+
if (deliverReplyError) {
|
|
50
|
+
throw deliverReplyError;
|
|
51
|
+
}
|
|
52
|
+
deliverReplyCalls.push({ url, payload });
|
|
53
|
+
},
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
import {
|
|
57
|
+
createApprovalRequest,
|
|
58
|
+
createBinding,
|
|
59
|
+
getApprovalRequestById,
|
|
60
|
+
findPendingAccessRequestForRequester,
|
|
61
|
+
} from '../memory/channel-guardian-store.js';
|
|
62
|
+
import {
|
|
63
|
+
findActiveSession,
|
|
64
|
+
} from '../runtime/channel-guardian-service.js';
|
|
65
|
+
import { initializeDb, resetDb } from '../memory/db.js';
|
|
66
|
+
import {
|
|
67
|
+
handleAccessRequestDecision,
|
|
68
|
+
deliverVerificationCodeToGuardian,
|
|
69
|
+
notifyRequesterOfApproval,
|
|
70
|
+
notifyRequesterOfDenial,
|
|
71
|
+
notifyRequesterOfDeliveryFailure,
|
|
72
|
+
} from '../runtime/routes/access-request-decision.js';
|
|
73
|
+
|
|
74
|
+
initializeDb();
|
|
75
|
+
|
|
76
|
+
afterAll(() => {
|
|
77
|
+
resetDb();
|
|
78
|
+
try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Helpers
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
const GUARDIAN_APPROVAL_TTL_MS = 5 * 60 * 1000;
|
|
86
|
+
|
|
87
|
+
function resetState(): void {
|
|
88
|
+
const { getDb } = require('../memory/db.js');
|
|
89
|
+
const db = getDb();
|
|
90
|
+
db.run('DELETE FROM channel_guardian_approval_requests');
|
|
91
|
+
db.run('DELETE FROM channel_guardian_bindings');
|
|
92
|
+
db.run('DELETE FROM channel_guardian_verification_challenges');
|
|
93
|
+
deliverReplyCalls.length = 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function createTestApproval(overrides: Record<string, unknown> = {}) {
|
|
97
|
+
return createApprovalRequest({
|
|
98
|
+
runId: `ingress-access-request-${Date.now()}`,
|
|
99
|
+
conversationId: `access-req-telegram-user-unknown-456`,
|
|
100
|
+
assistantId: 'self',
|
|
101
|
+
channel: 'telegram',
|
|
102
|
+
requesterExternalUserId: 'user-unknown-456',
|
|
103
|
+
requesterChatId: 'chat-123',
|
|
104
|
+
guardianExternalUserId: 'guardian-user-789',
|
|
105
|
+
guardianChatId: 'guardian-chat-789',
|
|
106
|
+
toolName: 'ingress_access_request',
|
|
107
|
+
riskLevel: 'access_request',
|
|
108
|
+
reason: 'Alice Unknown is requesting access to the assistant',
|
|
109
|
+
expiresAt: Date.now() + GUARDIAN_APPROVAL_TTL_MS,
|
|
110
|
+
...overrides,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Tests
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
describe('access request decision handler', () => {
|
|
119
|
+
beforeEach(() => {
|
|
120
|
+
resetState();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('guardian approve creates a verification session', () => {
|
|
124
|
+
const approval = createTestApproval();
|
|
125
|
+
|
|
126
|
+
const result = handleAccessRequestDecision(
|
|
127
|
+
approval,
|
|
128
|
+
'approve',
|
|
129
|
+
'guardian-user-789',
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
expect(result.handled).toBe(true);
|
|
133
|
+
expect(result.type).toBe('approved');
|
|
134
|
+
expect(result.verificationSessionId).toBeDefined();
|
|
135
|
+
expect(result.verificationCode).toBeDefined();
|
|
136
|
+
// Verification code should be a 6-digit numeric string
|
|
137
|
+
expect(result.verificationCode).toMatch(/^\d{6}$/);
|
|
138
|
+
|
|
139
|
+
// Approval record should be updated to 'approved'
|
|
140
|
+
const updated = getApprovalRequestById(approval.id);
|
|
141
|
+
expect(updated).not.toBeNull();
|
|
142
|
+
expect(updated!.status).toBe('approved');
|
|
143
|
+
expect(updated!.decidedByExternalUserId).toBe('guardian-user-789');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('verification session is identity-bound to the requester', () => {
|
|
147
|
+
const approval = createTestApproval();
|
|
148
|
+
|
|
149
|
+
const result = handleAccessRequestDecision(
|
|
150
|
+
approval,
|
|
151
|
+
'approve',
|
|
152
|
+
'guardian-user-789',
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
expect(result.type).toBe('approved');
|
|
156
|
+
|
|
157
|
+
// There should be an active session for this channel
|
|
158
|
+
const session = findActiveSession('self', 'telegram');
|
|
159
|
+
expect(session).not.toBeNull();
|
|
160
|
+
expect(session!.expectedExternalUserId).toBe('user-unknown-456');
|
|
161
|
+
expect(session!.expectedChatId).toBe('chat-123');
|
|
162
|
+
expect(session!.identityBindingStatus).toBe('bound');
|
|
163
|
+
expect(session!.status).toBe('awaiting_response');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('guardian deny marks approval as denied', () => {
|
|
167
|
+
const approval = createTestApproval();
|
|
168
|
+
|
|
169
|
+
const result = handleAccessRequestDecision(
|
|
170
|
+
approval,
|
|
171
|
+
'deny',
|
|
172
|
+
'guardian-user-789',
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
expect(result.handled).toBe(true);
|
|
176
|
+
expect(result.type).toBe('denied');
|
|
177
|
+
expect(result.verificationSessionId).toBeUndefined();
|
|
178
|
+
expect(result.verificationCode).toBeUndefined();
|
|
179
|
+
|
|
180
|
+
// Approval record should be updated to 'denied'
|
|
181
|
+
const updated = getApprovalRequestById(approval.id);
|
|
182
|
+
expect(updated).not.toBeNull();
|
|
183
|
+
expect(updated!.status).toBe('denied');
|
|
184
|
+
expect(updated!.decidedByExternalUserId).toBe('guardian-user-789');
|
|
185
|
+
|
|
186
|
+
// No verification session should be created
|
|
187
|
+
const session = findActiveSession('self', 'telegram');
|
|
188
|
+
expect(session).toBeNull();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('stale decision (already resolved) returns stale', () => {
|
|
192
|
+
const approval = createTestApproval();
|
|
193
|
+
|
|
194
|
+
// Approve first
|
|
195
|
+
handleAccessRequestDecision(approval, 'approve', 'guardian-user-789');
|
|
196
|
+
|
|
197
|
+
// Try to deny the same approval — should be stale
|
|
198
|
+
const result = handleAccessRequestDecision(
|
|
199
|
+
approval,
|
|
200
|
+
'deny',
|
|
201
|
+
'guardian-user-789',
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
expect(result.handled).toBe(true);
|
|
205
|
+
expect(result.type).toBe('stale');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('idempotent approval does not create duplicate verification sessions', () => {
|
|
209
|
+
const approval = createTestApproval();
|
|
210
|
+
|
|
211
|
+
// Approve first
|
|
212
|
+
const result1 = handleAccessRequestDecision(
|
|
213
|
+
approval,
|
|
214
|
+
'approve',
|
|
215
|
+
'guardian-user-789',
|
|
216
|
+
);
|
|
217
|
+
expect(result1.type).toBe('approved');
|
|
218
|
+
const sessionId1 = result1.verificationSessionId;
|
|
219
|
+
|
|
220
|
+
// Approve again — should be idempotent (already resolved with same decision)
|
|
221
|
+
const result2 = handleAccessRequestDecision(
|
|
222
|
+
approval,
|
|
223
|
+
'approve',
|
|
224
|
+
'guardian-user-789',
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// resolveApprovalRequest returns the existing record for same-decision idempotency,
|
|
228
|
+
// but since the approval is no longer 'pending', a second createOutboundSession
|
|
229
|
+
// will still be called. However, createOutboundSession auto-revokes prior sessions,
|
|
230
|
+
// so there will be exactly one active session at the end.
|
|
231
|
+
// The important thing is that the result indicates approval was handled.
|
|
232
|
+
expect(result2.handled).toBe(true);
|
|
233
|
+
// Either 'approved' (creates a new session) or something else is acceptable,
|
|
234
|
+
// but it should not crash.
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('access request notification delivery', () => {
|
|
239
|
+
beforeEach(() => {
|
|
240
|
+
deliverReplyCalls.length = 0;
|
|
241
|
+
deliverReplyError = null;
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('delivers verification code to guardian and returns ok', async () => {
|
|
245
|
+
const result = await deliverVerificationCodeToGuardian({
|
|
246
|
+
replyCallbackUrl: 'http://localhost:7830/deliver/telegram',
|
|
247
|
+
guardianChatId: 'guardian-chat-789',
|
|
248
|
+
requesterIdentifier: 'user-unknown-456',
|
|
249
|
+
verificationCode: '123456',
|
|
250
|
+
assistantId: 'self',
|
|
251
|
+
bearerToken: 'test-token',
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
expect(result.ok).toBe(true);
|
|
255
|
+
expect(deliverReplyCalls.length).toBe(1);
|
|
256
|
+
const call = deliverReplyCalls[0];
|
|
257
|
+
expect(call.payload.chatId).toBe('guardian-chat-789');
|
|
258
|
+
const text = call.payload.text as string;
|
|
259
|
+
expect(text).toContain('123456');
|
|
260
|
+
expect(text).toContain('user-unknown-456');
|
|
261
|
+
expect(text).toContain('10 minutes');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('returns failure result when guardian code delivery fails', async () => {
|
|
265
|
+
deliverReplyError = new Error('Gateway timeout');
|
|
266
|
+
|
|
267
|
+
const result = await deliverVerificationCodeToGuardian({
|
|
268
|
+
replyCallbackUrl: 'http://localhost:7830/deliver/telegram',
|
|
269
|
+
guardianChatId: 'guardian-chat-789',
|
|
270
|
+
requesterIdentifier: 'user-unknown-456',
|
|
271
|
+
verificationCode: '123456',
|
|
272
|
+
assistantId: 'self',
|
|
273
|
+
bearerToken: 'test-token',
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
expect(result.ok).toBe(false);
|
|
277
|
+
if (!result.ok) {
|
|
278
|
+
expect(result.reason).toBe('Gateway timeout');
|
|
279
|
+
}
|
|
280
|
+
// No calls should have been recorded (error thrown before push)
|
|
281
|
+
expect(deliverReplyCalls.length).toBe(0);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('notifies requester of approval', async () => {
|
|
285
|
+
await notifyRequesterOfApproval({
|
|
286
|
+
replyCallbackUrl: 'http://localhost:7830/deliver/telegram',
|
|
287
|
+
requesterChatId: 'chat-123',
|
|
288
|
+
assistantId: 'self',
|
|
289
|
+
bearerToken: 'test-token',
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
expect(deliverReplyCalls.length).toBe(1);
|
|
293
|
+
const call = deliverReplyCalls[0];
|
|
294
|
+
expect(call.payload.chatId).toBe('chat-123');
|
|
295
|
+
const text = call.payload.text as string;
|
|
296
|
+
expect(text).toContain('approved');
|
|
297
|
+
expect(text).toContain('verification code');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test('notifies requester of denial', async () => {
|
|
301
|
+
await notifyRequesterOfDenial({
|
|
302
|
+
replyCallbackUrl: 'http://localhost:7830/deliver/telegram',
|
|
303
|
+
requesterChatId: 'chat-123',
|
|
304
|
+
assistantId: 'self',
|
|
305
|
+
bearerToken: 'test-token',
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(deliverReplyCalls.length).toBe(1);
|
|
309
|
+
const call = deliverReplyCalls[0];
|
|
310
|
+
expect(call.payload.chatId).toBe('chat-123');
|
|
311
|
+
const text = call.payload.text as string;
|
|
312
|
+
expect(text).toContain('denied');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('notifies requester of delivery failure', async () => {
|
|
316
|
+
await notifyRequesterOfDeliveryFailure({
|
|
317
|
+
replyCallbackUrl: 'http://localhost:7830/deliver/telegram',
|
|
318
|
+
requesterChatId: 'chat-123',
|
|
319
|
+
assistantId: 'self',
|
|
320
|
+
bearerToken: 'test-token',
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
expect(deliverReplyCalls.length).toBe(1);
|
|
324
|
+
const call = deliverReplyCalls[0];
|
|
325
|
+
expect(call.payload.chatId).toBe('chat-123');
|
|
326
|
+
const text = call.payload.text as string;
|
|
327
|
+
expect(text).toContain('approved');
|
|
328
|
+
expect(text).toContain('unable to deliver');
|
|
329
|
+
expect(text).toContain('try again');
|
|
330
|
+
});
|
|
331
|
+
});
|
|
@@ -311,7 +311,7 @@ describe('AssetMaterializeTool visibility policy', () => {
|
|
|
311
311
|
const standardConv = createConversation({ title: 'standard-conv' });
|
|
312
312
|
const base64Content = Buffer.from('standard content').toString('base64');
|
|
313
313
|
const attachment = uploadAttachment('public.txt', 'text/plain', base64Content);
|
|
314
|
-
const msg = addMessage(standardConv.id, 'user', 'standard message');
|
|
314
|
+
const msg = await addMessage(standardConv.id, 'user', 'standard message');
|
|
315
315
|
linkAttachmentToMessage(msg.id, attachment.id, 0);
|
|
316
316
|
|
|
317
317
|
// Materialize from a different standard conversation
|
|
@@ -334,7 +334,7 @@ describe('AssetMaterializeTool visibility policy', () => {
|
|
|
334
334
|
const privateConv = createConversation({ title: 'private-conv', threadType: 'private' });
|
|
335
335
|
const base64Content = Buffer.from('private content').toString('base64');
|
|
336
336
|
const attachment = uploadAttachment('secret.txt', 'text/plain', base64Content);
|
|
337
|
-
const msg = addMessage(privateConv.id, 'user', 'private message');
|
|
337
|
+
const msg = await addMessage(privateConv.id, 'user', 'private message');
|
|
338
338
|
linkAttachmentToMessage(msg.id, attachment.id, 0);
|
|
339
339
|
|
|
340
340
|
// Materialize from the same private conversation
|
|
@@ -356,7 +356,7 @@ describe('AssetMaterializeTool visibility policy', () => {
|
|
|
356
356
|
const privateConv = createConversation({ title: 'private-conv', threadType: 'private' });
|
|
357
357
|
const base64Content = Buffer.from('private content').toString('base64');
|
|
358
358
|
const attachment = uploadAttachment('secret.txt', 'text/plain', base64Content);
|
|
359
|
-
const msg = addMessage(privateConv.id, 'user', 'private message');
|
|
359
|
+
const msg = await addMessage(privateConv.id, 'user', 'private message');
|
|
360
360
|
linkAttachmentToMessage(msg.id, attachment.id, 0);
|
|
361
361
|
|
|
362
362
|
// Attempt to materialize from a different conversation
|
|
@@ -380,7 +380,7 @@ describe('AssetMaterializeTool visibility policy', () => {
|
|
|
380
380
|
const privateConv = createConversation({ title: 'private-conv', threadType: 'private' });
|
|
381
381
|
const base64Content = Buffer.from('private content').toString('base64');
|
|
382
382
|
const attachment = uploadAttachment('confidential.pdf', 'application/pdf', base64Content);
|
|
383
|
-
const msg = addMessage(privateConv.id, 'user', 'private message');
|
|
383
|
+
const msg = await addMessage(privateConv.id, 'user', 'private message');
|
|
384
384
|
linkAttachmentToMessage(msg.id, attachment.id, 0);
|
|
385
385
|
|
|
386
386
|
// From a standard conversation
|
|
@@ -406,7 +406,7 @@ describe('AssetMaterializeTool visibility policy', () => {
|
|
|
406
406
|
const privateConv1 = createConversation({ title: 'private-conv-1', threadType: 'private' });
|
|
407
407
|
const base64Content = Buffer.from('private content').toString('base64');
|
|
408
408
|
const attachment = uploadAttachment('secret.txt', 'text/plain', base64Content);
|
|
409
|
-
const msg = addMessage(privateConv1.id, 'user', 'private message');
|
|
409
|
+
const msg = await addMessage(privateConv1.id, 'user', 'private message');
|
|
410
410
|
linkAttachmentToMessage(msg.id, attachment.id, 0);
|
|
411
411
|
|
|
412
412
|
// Attempt from a different private conversation
|
|
@@ -431,8 +431,8 @@ describe('AssetMaterializeTool visibility policy', () => {
|
|
|
431
431
|
const base64Content = Buffer.from('shared content').toString('base64');
|
|
432
432
|
const attachment = uploadAttachment('shared.txt', 'text/plain', base64Content);
|
|
433
433
|
|
|
434
|
-
const msg1 = addMessage(privateConv.id, 'user', 'private message');
|
|
435
|
-
const msg2 = addMessage(standardConv.id, 'user', 'standard message');
|
|
434
|
+
const msg1 = await addMessage(privateConv.id, 'user', 'private message');
|
|
435
|
+
const msg2 = await addMessage(standardConv.id, 'user', 'standard message');
|
|
436
436
|
linkAttachmentToMessage(msg1.id, attachment.id, 0);
|
|
437
437
|
linkAttachmentToMessage(msg2.id, attachment.id, 0);
|
|
438
438
|
|
|
@@ -219,14 +219,14 @@ describe('searchAttachments', () => {
|
|
|
219
219
|
describe('searchAttachments with conversation_id', () => {
|
|
220
220
|
beforeEach(resetTables);
|
|
221
221
|
|
|
222
|
-
test('returns only attachments linked to the specified conversation', () => {
|
|
222
|
+
test('returns only attachments linked to the specified conversation', async () => {
|
|
223
223
|
const png1 = uploadAttachment('in-conv.png', 'image/png', 'AAAA');
|
|
224
224
|
const png2 = uploadAttachment('other-conv.png', 'image/png', 'BBBB');
|
|
225
225
|
|
|
226
226
|
const conv1 = createConversation();
|
|
227
227
|
const conv2 = createConversation();
|
|
228
|
-
const msg1 = addMessage(conv1.id, 'user', 'First conv');
|
|
229
|
-
const msg2 = addMessage(conv2.id, 'user', 'Second conv');
|
|
228
|
+
const msg1 = await addMessage(conv1.id, 'user', 'First conv');
|
|
229
|
+
const msg2 = await addMessage(conv2.id, 'user', 'Second conv');
|
|
230
230
|
|
|
231
231
|
linkAttachmentToMessage(msg1.id, png1.id, 0);
|
|
232
232
|
linkAttachmentToMessage(msg2.id, png2.id, 0);
|
|
@@ -236,10 +236,10 @@ describe('searchAttachments with conversation_id', () => {
|
|
|
236
236
|
expect(results[0].id).toBe(png1.id);
|
|
237
237
|
});
|
|
238
238
|
|
|
239
|
-
test('returns empty when conversation has no attachments', () => {
|
|
239
|
+
test('returns empty when conversation has no attachments', async () => {
|
|
240
240
|
uploadAttachment('orphan.png', 'image/png', 'AAAA');
|
|
241
241
|
const conv = createConversation();
|
|
242
|
-
addMessage(conv.id, 'user', 'No attachments here');
|
|
242
|
+
await addMessage(conv.id, 'user', 'No attachments here');
|
|
243
243
|
|
|
244
244
|
const results = searchAttachments({ conversation_id: conv.id });
|
|
245
245
|
expect(results.length).toBe(0);
|
|
@@ -251,12 +251,12 @@ describe('searchAttachments with conversation_id', () => {
|
|
|
251
251
|
expect(results.length).toBe(0);
|
|
252
252
|
});
|
|
253
253
|
|
|
254
|
-
test('combines conversation_id with mime_type filter', () => {
|
|
254
|
+
test('combines conversation_id with mime_type filter', async () => {
|
|
255
255
|
const png = uploadAttachment('image.png', 'image/png', 'AAAA');
|
|
256
256
|
const pdf = uploadAttachment('doc.pdf', 'application/pdf', 'BBBB');
|
|
257
257
|
|
|
258
258
|
const conv = createConversation();
|
|
259
|
-
const msg = addMessage(conv.id, 'user', 'Both types');
|
|
259
|
+
const msg = await addMessage(conv.id, 'user', 'Both types');
|
|
260
260
|
|
|
261
261
|
linkAttachmentToMessage(msg.id, png.id, 0);
|
|
262
262
|
linkAttachmentToMessage(msg.id, pdf.id, 1);
|
|
@@ -266,12 +266,12 @@ describe('searchAttachments with conversation_id', () => {
|
|
|
266
266
|
expect(results[0].mimeType).toBe('image/png');
|
|
267
267
|
});
|
|
268
268
|
|
|
269
|
-
test('combines conversation_id with filename filter', () => {
|
|
269
|
+
test('combines conversation_id with filename filter', async () => {
|
|
270
270
|
const a = uploadAttachment('target.png', 'image/png', 'AAAA');
|
|
271
271
|
const b = uploadAttachment('other.png', 'image/png', 'BBBB');
|
|
272
272
|
|
|
273
273
|
const conv = createConversation();
|
|
274
|
-
const msg = addMessage(conv.id, 'user', 'Both');
|
|
274
|
+
const msg = await addMessage(conv.id, 'user', 'Both');
|
|
275
275
|
|
|
276
276
|
linkAttachmentToMessage(msg.id, a.id, 0);
|
|
277
277
|
linkAttachmentToMessage(msg.id, b.id, 1);
|
|
@@ -367,7 +367,7 @@ describe('AssetSearchTool visibility policy', () => {
|
|
|
367
367
|
test('attachments from standard threads are visible from any context', async () => {
|
|
368
368
|
const standardConv = createConversation({ title: 'standard-conv' });
|
|
369
369
|
const attachment = uploadAttachment('public.png', 'image/png', 'AAAA');
|
|
370
|
-
const msg = addMessage(standardConv.id, 'user', 'standard message');
|
|
370
|
+
const msg = await addMessage(standardConv.id, 'user', 'standard message');
|
|
371
371
|
linkAttachmentToMessage(msg.id, attachment.id, 0);
|
|
372
372
|
|
|
373
373
|
// Search from a different standard conversation
|
|
@@ -386,7 +386,7 @@ describe('AssetSearchTool visibility policy', () => {
|
|
|
386
386
|
test('attachments from private threads are visible within the same private thread', async () => {
|
|
387
387
|
const privateConv = createConversation({ title: 'private-conv', threadType: 'private' });
|
|
388
388
|
const attachment = uploadAttachment('secret.png', 'image/png', 'AAAA');
|
|
389
|
-
const msg = addMessage(privateConv.id, 'user', 'private message');
|
|
389
|
+
const msg = await addMessage(privateConv.id, 'user', 'private message');
|
|
390
390
|
linkAttachmentToMessage(msg.id, attachment.id, 0);
|
|
391
391
|
|
|
392
392
|
// Search from the same private conversation
|
|
@@ -404,7 +404,7 @@ describe('AssetSearchTool visibility policy', () => {
|
|
|
404
404
|
test('attachments from private threads are NOT visible from a different conversation', async () => {
|
|
405
405
|
const privateConv = createConversation({ title: 'private-conv', threadType: 'private' });
|
|
406
406
|
const attachment = uploadAttachment('secret.png', 'image/png', 'AAAA');
|
|
407
|
-
const msg = addMessage(privateConv.id, 'user', 'private message');
|
|
407
|
+
const msg = await addMessage(privateConv.id, 'user', 'private message');
|
|
408
408
|
linkAttachmentToMessage(msg.id, attachment.id, 0);
|
|
409
409
|
|
|
410
410
|
// Search from a different private conversation
|
|
@@ -423,7 +423,7 @@ describe('AssetSearchTool visibility policy', () => {
|
|
|
423
423
|
test('attachments from private threads are NOT visible from standard threads', async () => {
|
|
424
424
|
const privateConv = createConversation({ title: 'private-conv', threadType: 'private' });
|
|
425
425
|
const attachment = uploadAttachment('secret.png', 'image/png', 'AAAA');
|
|
426
|
-
const msg = addMessage(privateConv.id, 'user', 'private message');
|
|
426
|
+
const msg = await addMessage(privateConv.id, 'user', 'private message');
|
|
427
427
|
linkAttachmentToMessage(msg.id, attachment.id, 0);
|
|
428
428
|
|
|
429
429
|
// Search from a standard conversation
|
|
@@ -444,8 +444,8 @@ describe('AssetSearchTool visibility policy', () => {
|
|
|
444
444
|
const standardConv = createConversation({ title: 'standard-conv' });
|
|
445
445
|
const attachment = uploadAttachment('shared.png', 'image/png', 'AAAA');
|
|
446
446
|
|
|
447
|
-
const msg1 = addMessage(privateConv.id, 'user', 'private message');
|
|
448
|
-
const msg2 = addMessage(standardConv.id, 'user', 'standard message');
|
|
447
|
+
const msg1 = await addMessage(privateConv.id, 'user', 'private message');
|
|
448
|
+
const msg2 = await addMessage(standardConv.id, 'user', 'standard message');
|
|
449
449
|
linkAttachmentToMessage(msg1.id, attachment.id, 0);
|
|
450
450
|
linkAttachmentToMessage(msg2.id, attachment.id, 0);
|
|
451
451
|
|
|
@@ -206,10 +206,10 @@ describe('deleteAttachment', () => {
|
|
|
206
206
|
expect(result).toBe('not_found');
|
|
207
207
|
});
|
|
208
208
|
|
|
209
|
-
test('returns still_referenced when messages reference the attachment', () => {
|
|
209
|
+
test('returns still_referenced when messages reference the attachment', async () => {
|
|
210
210
|
const conv = createConversation();
|
|
211
|
-
const msg1 = addMessage(conv.id, 'user', 'First upload');
|
|
212
|
-
const msg2 = addMessage(conv.id, 'user', 'Duplicate upload');
|
|
211
|
+
const msg1 = await addMessage(conv.id, 'user', 'First upload');
|
|
212
|
+
const msg2 = await addMessage(conv.id, 'user', 'Duplicate upload');
|
|
213
213
|
|
|
214
214
|
// Dedup: both uploads return the same attachment row
|
|
215
215
|
const first = uploadAttachment('photo.png', 'image/png', 'SHAREDCONTENT1');
|
|
@@ -304,9 +304,9 @@ describe('getAttachmentById', () => {
|
|
|
304
304
|
describe('linkAttachmentToMessage + getAttachmentsForMessage', () => {
|
|
305
305
|
beforeEach(resetTables);
|
|
306
306
|
|
|
307
|
-
test('links attachment and retrieves it by message', () => {
|
|
307
|
+
test('links attachment and retrieves it by message', async () => {
|
|
308
308
|
const conv = createConversation();
|
|
309
|
-
const msg = addMessage(conv.id, 'assistant', 'Here is a chart');
|
|
309
|
+
const msg = await addMessage(conv.id, 'assistant', 'Here is a chart');
|
|
310
310
|
const stored = uploadAttachment('chart.png', 'image/png', 'iVBORw0K');
|
|
311
311
|
|
|
312
312
|
linkAttachmentToMessage(msg.id, stored.id, 0);
|
|
@@ -318,9 +318,9 @@ describe('linkAttachmentToMessage + getAttachmentsForMessage', () => {
|
|
|
318
318
|
expect(linked[0].dataBase64).toBe('iVBORw0K');
|
|
319
319
|
});
|
|
320
320
|
|
|
321
|
-
test('returns attachments in position order', () => {
|
|
321
|
+
test('returns attachments in position order', async () => {
|
|
322
322
|
const conv = createConversation();
|
|
323
|
-
const msg = addMessage(conv.id, 'assistant', 'Multiple files');
|
|
323
|
+
const msg = await addMessage(conv.id, 'assistant', 'Multiple files');
|
|
324
324
|
const a = uploadAttachment('first.txt', 'text/plain', 'AAAA');
|
|
325
325
|
const b = uploadAttachment('second.txt', 'text/plain', 'BBBB');
|
|
326
326
|
|
|
@@ -334,9 +334,9 @@ describe('linkAttachmentToMessage + getAttachmentsForMessage', () => {
|
|
|
334
334
|
expect(linked[1].originalFilename).toBe('second.txt');
|
|
335
335
|
});
|
|
336
336
|
|
|
337
|
-
test('returns empty for message with no attachments', () => {
|
|
337
|
+
test('returns empty for message with no attachments', async () => {
|
|
338
338
|
const conv = createConversation();
|
|
339
|
-
const msg = addMessage(conv.id, 'assistant', 'No attachments');
|
|
339
|
+
const msg = await addMessage(conv.id, 'assistant', 'No attachments');
|
|
340
340
|
|
|
341
341
|
const linked = getAttachmentsForMessage(msg.id);
|
|
342
342
|
expect(linked).toHaveLength(0);
|
|
@@ -357,9 +357,9 @@ describe('deleteOrphanAttachments', () => {
|
|
|
357
357
|
expect(removed).toBe(1);
|
|
358
358
|
});
|
|
359
359
|
|
|
360
|
-
test('preserves attachments that are still linked', () => {
|
|
360
|
+
test('preserves attachments that are still linked', async () => {
|
|
361
361
|
const conv = createConversation();
|
|
362
|
-
const msg = addMessage(conv.id, 'assistant', 'With attachment');
|
|
362
|
+
const msg = await addMessage(conv.id, 'assistant', 'With attachment');
|
|
363
363
|
const stored = uploadAttachment('linked.txt', 'text/plain', 'ZGF0YQ==');
|
|
364
364
|
linkAttachmentToMessage(msg.id, stored.id, 0);
|
|
365
365
|
|
|
@@ -370,9 +370,9 @@ describe('deleteOrphanAttachments', () => {
|
|
|
370
370
|
expect(fetched).not.toBeNull();
|
|
371
371
|
});
|
|
372
372
|
|
|
373
|
-
test('removes only orphans when mixed candidates provided', () => {
|
|
373
|
+
test('removes only orphans when mixed candidates provided', async () => {
|
|
374
374
|
const conv = createConversation();
|
|
375
|
-
const msg = addMessage(conv.id, 'assistant', 'Mixed');
|
|
375
|
+
const msg = await addMessage(conv.id, 'assistant', 'Mixed');
|
|
376
376
|
const linked = uploadAttachment('linked.txt', 'text/plain', 'AAAA');
|
|
377
377
|
const orphan = uploadAttachment('orphan.txt', 'text/plain', 'BBBB');
|
|
378
378
|
linkAttachmentToMessage(msg.id, linked.id, 0);
|