@vellumai/assistant 0.3.15 → 0.3.18
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 +211 -12
- package/Dockerfile +1 -1
- package/README.md +11 -5
- package/docs/architecture/http-token-refresh.md +274 -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 +328 -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 +19 -15
- package/src/__tests__/checker.test.ts +103 -48
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +2 -2
- package/src/__tests__/config-watcher.test.ts +356 -0
- package/src/__tests__/conversation-pairing.test.ts +127 -27
- 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 +425 -0
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +71 -0
- package/src/__tests__/guardian-action-store.test.ts +182 -0
- package/src/__tests__/guardian-action-sweep.test.ts +9 -9
- package/src/__tests__/guardian-dispatch.test.ts +120 -0
- 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 +23 -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 +281 -0
- package/src/__tests__/notification-broadcaster.test.ts +115 -4
- package/src/__tests__/notification-decision-strategy.test.ts +138 -1
- package/src/__tests__/notification-deep-link.test.ts +44 -1
- package/src/__tests__/notification-guardian-path.test.ts +157 -0
- package/src/__tests__/notification-routing-intent.test.ts +11 -1
- package/src/__tests__/notification-thread-candidate-validation.test.ts +215 -0
- 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 +38 -22
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +489 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +405 -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 +323 -0
- package/src/__tests__/update-template-contract.test.ts +24 -0
- package/src/__tests__/voice-session-bridge.test.ts +109 -9
- package/src/agent/loop.ts +2 -2
- package/src/amazon/client.ts +2 -3
- package/src/calls/call-controller.ts +241 -39
- 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/guardian-dispatch.ts +8 -0
- 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 +8 -6
- package/src/cli/core-commands.ts +43 -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 +15 -0
- package/src/config/templates/USER.md +5 -1
- package/src/config/types.ts +1 -0
- package/src/config/update-bulletin-format.ts +54 -0
- package/src/config/update-bulletin-state.ts +49 -0
- package/src/config/update-bulletin-template-path.ts +6 -0
- package/src/config/update-bulletin.ts +97 -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 +4 -2
- package/src/daemon/connection-policy.ts +21 -1
- package/src/daemon/daemon-control.ts +219 -8
- 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/identity.ts +45 -25
- 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/workspace.ts +12 -1
- package/src/daemon/ipc-contract-inventory.json +6 -1
- package/src/daemon/ipc-contract.ts +5 -1
- package/src/daemon/lifecycle.ts +314 -266
- package/src/daemon/recording-intent.ts +0 -41
- package/src/daemon/response-tier.ts +2 -2
- package/src/daemon/server.ts +31 -9
- 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 +546 -59
- 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 +60 -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 +35 -567
- package/src/memory/channel-guardian-store.ts +63 -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 +44 -983
- package/src/memory/db-connection.ts +3 -0
- package/src/memory/db-init.ts +33 -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 +136 -0
- package/src/memory/guardian-action-store.ts +418 -5
- 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 +521 -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/032-guardian-delivery-conversation-index.ts +15 -0
- package/src/memory/migrations/032-notification-delivery-thread-decision.ts +20 -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 +10 -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 +4 -0
- package/src/memory/schema.ts +31 -8
- package/src/memory/search/semantic.ts +8 -90
- package/src/notifications/README.md +159 -18
- package/src/notifications/broadcaster.ts +69 -33
- package/src/notifications/conversation-pairing.ts +99 -21
- package/src/notifications/decision-engine.ts +176 -8
- package/src/notifications/deliveries-store.ts +39 -8
- package/src/notifications/emit-signal.ts +1 -0
- package/src/notifications/preferences-store.ts +7 -7
- package/src/notifications/thread-candidates.ts +269 -0
- package/src/notifications/types.ts +19 -0
- package/src/permissions/checker.ts +1 -16
- package/src/permissions/defaults.ts +25 -5
- 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 +271 -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/version.ts +29 -2
- 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,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for M4: Verification success → trusted contact activation.
|
|
3
|
+
*
|
|
4
|
+
* When a requester successfully verifies their identity (enters the correct
|
|
5
|
+
* 6-digit code from an identity-bound outbound session), the system should:
|
|
6
|
+
* 1. Upsert an active member record in assistant_ingress_members
|
|
7
|
+
* 2. Allow subsequent messages through the ACL check
|
|
8
|
+
* 3. Scope the member correctly (no cross-assistant leakage)
|
|
9
|
+
* 4. Reactivate previously revoked members on re-verification
|
|
10
|
+
* 5. NOT create a guardian binding (trusted contacts are not guardians)
|
|
11
|
+
*/
|
|
12
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
|
|
16
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Test isolation: in-memory SQLite via temp directory
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
const testDir = mkdtempSync(join(tmpdir(), 'trusted-contact-verify-test-'));
|
|
23
|
+
|
|
24
|
+
mock.module('../util/platform.js', () => ({
|
|
25
|
+
getRootDir: () => testDir,
|
|
26
|
+
getDataDir: () => testDir,
|
|
27
|
+
isMacOS: () => process.platform === 'darwin',
|
|
28
|
+
isLinux: () => process.platform === 'linux',
|
|
29
|
+
isWindows: () => process.platform === 'win32',
|
|
30
|
+
getSocketPath: () => join(testDir, 'test.sock'),
|
|
31
|
+
getPidPath: () => join(testDir, 'test.pid'),
|
|
32
|
+
getDbPath: () => join(testDir, 'test.db'),
|
|
33
|
+
getLogPath: () => join(testDir, 'test.log'),
|
|
34
|
+
ensureDataDir: () => {},
|
|
35
|
+
normalizeAssistantId: (id: string) => id === 'self' ? 'self' : id,
|
|
36
|
+
readHttpToken: () => 'test-bearer-token',
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
mock.module('../util/logger.js', () => ({
|
|
40
|
+
getLogger: () => new Proxy({} as Record<string, unknown>, {
|
|
41
|
+
get: () => () => {},
|
|
42
|
+
}),
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
import {
|
|
46
|
+
createBinding,
|
|
47
|
+
getActiveBinding,
|
|
48
|
+
} from '../memory/channel-guardian-store.js';
|
|
49
|
+
import { getDb, initializeDb, resetDb } from '../memory/db.js';
|
|
50
|
+
import {
|
|
51
|
+
findMember,
|
|
52
|
+
revokeMember,
|
|
53
|
+
upsertMember,
|
|
54
|
+
} from '../memory/ingress-member-store.js';
|
|
55
|
+
import {
|
|
56
|
+
createOutboundSession,
|
|
57
|
+
validateAndConsumeChallenge,
|
|
58
|
+
} from '../runtime/channel-guardian-service.js';
|
|
59
|
+
|
|
60
|
+
initializeDb();
|
|
61
|
+
|
|
62
|
+
afterAll(() => {
|
|
63
|
+
resetDb();
|
|
64
|
+
try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Helpers
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
function resetTables(): void {
|
|
72
|
+
const db = getDb();
|
|
73
|
+
db.run('DELETE FROM channel_guardian_verification_challenges');
|
|
74
|
+
db.run('DELETE FROM channel_guardian_bindings');
|
|
75
|
+
db.run('DELETE FROM channel_guardian_rate_limits');
|
|
76
|
+
db.run('DELETE FROM assistant_ingress_members');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Tests
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
describe('trusted contact verification → member activation', () => {
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
resetTables();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('successful verification creates active member with allow policy', () => {
|
|
89
|
+
// Simulate M3: guardian approves, outbound session created for the requester
|
|
90
|
+
const session = createOutboundSession({
|
|
91
|
+
assistantId: 'self',
|
|
92
|
+
channel: 'telegram',
|
|
93
|
+
expectedExternalUserId: 'requester-user-123',
|
|
94
|
+
expectedChatId: 'requester-chat-123',
|
|
95
|
+
identityBindingStatus: 'bound',
|
|
96
|
+
destinationAddress: 'requester-chat-123',
|
|
97
|
+
verificationPurpose: 'trusted_contact',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Requester enters the 6-digit code
|
|
101
|
+
const result = validateAndConsumeChallenge(
|
|
102
|
+
'self',
|
|
103
|
+
'telegram',
|
|
104
|
+
session.secret,
|
|
105
|
+
'requester-user-123',
|
|
106
|
+
'requester-chat-123',
|
|
107
|
+
'requester_username',
|
|
108
|
+
'Requester Name',
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(result.success).toBe(true);
|
|
112
|
+
if (result.success) {
|
|
113
|
+
expect(result.verificationType).toBe('trusted_contact');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Simulate the member upsert that inbound-message-handler performs on success
|
|
117
|
+
upsertMember({
|
|
118
|
+
assistantId: 'self',
|
|
119
|
+
sourceChannel: 'telegram',
|
|
120
|
+
externalUserId: 'requester-user-123',
|
|
121
|
+
externalChatId: 'requester-chat-123',
|
|
122
|
+
status: 'active',
|
|
123
|
+
policy: 'allow',
|
|
124
|
+
displayName: 'Requester Name',
|
|
125
|
+
username: 'requester_username',
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Verify: active member record exists
|
|
129
|
+
const member = findMember({
|
|
130
|
+
assistantId: 'self',
|
|
131
|
+
sourceChannel: 'telegram',
|
|
132
|
+
externalUserId: 'requester-user-123',
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(member).not.toBeNull();
|
|
136
|
+
expect(member!.status).toBe('active');
|
|
137
|
+
expect(member!.policy).toBe('allow');
|
|
138
|
+
expect(member!.externalUserId).toBe('requester-user-123');
|
|
139
|
+
expect(member!.externalChatId).toBe('requester-chat-123');
|
|
140
|
+
expect(member!.displayName).toBe('Requester Name');
|
|
141
|
+
expect(member!.username).toBe('requester_username');
|
|
142
|
+
expect(member!.assistantId).toBe('self');
|
|
143
|
+
expect(member!.sourceChannel).toBe('telegram');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('post-verify message is accepted (ACL check passes)', () => {
|
|
147
|
+
// Create and verify a trusted contact
|
|
148
|
+
const session = createOutboundSession({
|
|
149
|
+
assistantId: 'self',
|
|
150
|
+
channel: 'telegram',
|
|
151
|
+
expectedExternalUserId: 'requester-user-456',
|
|
152
|
+
expectedChatId: 'requester-chat-456',
|
|
153
|
+
identityBindingStatus: 'bound',
|
|
154
|
+
destinationAddress: 'requester-chat-456',
|
|
155
|
+
verificationPurpose: 'trusted_contact',
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
validateAndConsumeChallenge(
|
|
159
|
+
'self', 'telegram', session.secret,
|
|
160
|
+
'requester-user-456', 'requester-chat-456',
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// Simulate member upsert on verification success
|
|
164
|
+
upsertMember({
|
|
165
|
+
assistantId: 'self',
|
|
166
|
+
sourceChannel: 'telegram',
|
|
167
|
+
externalUserId: 'requester-user-456',
|
|
168
|
+
externalChatId: 'requester-chat-456',
|
|
169
|
+
status: 'active',
|
|
170
|
+
policy: 'allow',
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Simulate the ACL check that inbound-message-handler performs
|
|
174
|
+
const member = findMember({
|
|
175
|
+
assistantId: 'self',
|
|
176
|
+
sourceChannel: 'telegram',
|
|
177
|
+
externalUserId: 'requester-user-456',
|
|
178
|
+
externalChatId: 'requester-chat-456',
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(member).not.toBeNull();
|
|
182
|
+
expect(member!.status).toBe('active');
|
|
183
|
+
expect(member!.policy).toBe('allow');
|
|
184
|
+
// ACL check passes: member exists, is active, and has allow policy
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('no cross-assistant leakage (member scoped correctly)', () => {
|
|
188
|
+
// Create member for assistant 'self'
|
|
189
|
+
const session = createOutboundSession({
|
|
190
|
+
assistantId: 'self',
|
|
191
|
+
channel: 'telegram',
|
|
192
|
+
expectedExternalUserId: 'user-cross-test',
|
|
193
|
+
expectedChatId: 'chat-cross-test',
|
|
194
|
+
identityBindingStatus: 'bound',
|
|
195
|
+
destinationAddress: 'chat-cross-test',
|
|
196
|
+
verificationPurpose: 'trusted_contact',
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
validateAndConsumeChallenge(
|
|
200
|
+
'self', 'telegram', session.secret,
|
|
201
|
+
'user-cross-test', 'chat-cross-test',
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
upsertMember({
|
|
205
|
+
assistantId: 'self',
|
|
206
|
+
sourceChannel: 'telegram',
|
|
207
|
+
externalUserId: 'user-cross-test',
|
|
208
|
+
externalChatId: 'chat-cross-test',
|
|
209
|
+
status: 'active',
|
|
210
|
+
policy: 'allow',
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Member should be found for 'self'
|
|
214
|
+
const selfMember = findMember({
|
|
215
|
+
assistantId: 'self',
|
|
216
|
+
sourceChannel: 'telegram',
|
|
217
|
+
externalUserId: 'user-cross-test',
|
|
218
|
+
});
|
|
219
|
+
expect(selfMember).not.toBeNull();
|
|
220
|
+
expect(selfMember!.status).toBe('active');
|
|
221
|
+
|
|
222
|
+
// Member should NOT be found for a different assistant
|
|
223
|
+
const otherMember = findMember({
|
|
224
|
+
assistantId: 'other-assistant',
|
|
225
|
+
sourceChannel: 'telegram',
|
|
226
|
+
externalUserId: 'user-cross-test',
|
|
227
|
+
});
|
|
228
|
+
expect(otherMember).toBeNull();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('re-verification of previously revoked member reactivates them', () => {
|
|
232
|
+
// Create and activate a member
|
|
233
|
+
const member = upsertMember({
|
|
234
|
+
assistantId: 'self',
|
|
235
|
+
sourceChannel: 'telegram',
|
|
236
|
+
externalUserId: 'user-revoked',
|
|
237
|
+
externalChatId: 'chat-revoked',
|
|
238
|
+
status: 'active',
|
|
239
|
+
policy: 'allow',
|
|
240
|
+
displayName: 'Revoked User',
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Revoke the member
|
|
244
|
+
const revoked = revokeMember(member.id, 'testing revocation');
|
|
245
|
+
expect(revoked).not.toBeNull();
|
|
246
|
+
expect(revoked!.status).toBe('revoked');
|
|
247
|
+
|
|
248
|
+
// Verify the member is indeed revoked (ACL would reject)
|
|
249
|
+
const revokedMember = findMember({
|
|
250
|
+
assistantId: 'self',
|
|
251
|
+
sourceChannel: 'telegram',
|
|
252
|
+
externalUserId: 'user-revoked',
|
|
253
|
+
});
|
|
254
|
+
expect(revokedMember).not.toBeNull();
|
|
255
|
+
expect(revokedMember!.status).toBe('revoked');
|
|
256
|
+
|
|
257
|
+
// Guardian re-approves, new outbound session created
|
|
258
|
+
const session = createOutboundSession({
|
|
259
|
+
assistantId: 'self',
|
|
260
|
+
channel: 'telegram',
|
|
261
|
+
expectedExternalUserId: 'user-revoked',
|
|
262
|
+
expectedChatId: 'chat-revoked',
|
|
263
|
+
identityBindingStatus: 'bound',
|
|
264
|
+
destinationAddress: 'chat-revoked',
|
|
265
|
+
verificationPurpose: 'trusted_contact',
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Requester enters the new code
|
|
269
|
+
const result = validateAndConsumeChallenge(
|
|
270
|
+
'self', 'telegram', session.secret,
|
|
271
|
+
'user-revoked', 'chat-revoked',
|
|
272
|
+
);
|
|
273
|
+
expect(result.success).toBe(true);
|
|
274
|
+
if (result.success) {
|
|
275
|
+
expect(result.verificationType).toBe('trusted_contact');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// upsertMember reactivates the existing record
|
|
279
|
+
upsertMember({
|
|
280
|
+
assistantId: 'self',
|
|
281
|
+
sourceChannel: 'telegram',
|
|
282
|
+
externalUserId: 'user-revoked',
|
|
283
|
+
externalChatId: 'chat-revoked',
|
|
284
|
+
status: 'active',
|
|
285
|
+
policy: 'allow',
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Verify: member is now active again
|
|
289
|
+
const reactivated = findMember({
|
|
290
|
+
assistantId: 'self',
|
|
291
|
+
sourceChannel: 'telegram',
|
|
292
|
+
externalUserId: 'user-revoked',
|
|
293
|
+
});
|
|
294
|
+
expect(reactivated).not.toBeNull();
|
|
295
|
+
expect(reactivated!.status).toBe('active');
|
|
296
|
+
expect(reactivated!.policy).toBe('allow');
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('trusted contact verification does NOT create a guardian binding', () => {
|
|
300
|
+
// Ensure there's an existing guardian binding we want to preserve
|
|
301
|
+
createBinding({
|
|
302
|
+
assistantId: 'self',
|
|
303
|
+
channel: 'telegram',
|
|
304
|
+
guardianExternalUserId: 'guardian-user-original',
|
|
305
|
+
guardianDeliveryChatId: 'guardian-chat-original',
|
|
306
|
+
verifiedVia: 'challenge',
|
|
307
|
+
metadataJson: null,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Create an outbound session for a requester (different user than guardian)
|
|
311
|
+
const session = createOutboundSession({
|
|
312
|
+
assistantId: 'self',
|
|
313
|
+
channel: 'telegram',
|
|
314
|
+
expectedExternalUserId: 'requester-user-789',
|
|
315
|
+
expectedChatId: 'requester-chat-789',
|
|
316
|
+
identityBindingStatus: 'bound',
|
|
317
|
+
destinationAddress: 'requester-chat-789',
|
|
318
|
+
verificationPurpose: 'trusted_contact',
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const result = validateAndConsumeChallenge(
|
|
322
|
+
'self', 'telegram', session.secret,
|
|
323
|
+
'requester-user-789', 'requester-chat-789',
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
expect(result.success).toBe(true);
|
|
327
|
+
if (result.success) {
|
|
328
|
+
expect(result.verificationType).toBe('trusted_contact');
|
|
329
|
+
// Should NOT have a bindingId — no guardian binding created
|
|
330
|
+
expect('bindingId' in result).toBe(false);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// The original guardian binding should remain intact
|
|
334
|
+
const binding = getActiveBinding('self', 'telegram');
|
|
335
|
+
expect(binding).not.toBeNull();
|
|
336
|
+
expect(binding!.guardianExternalUserId).toBe('guardian-user-original');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('guardian inbound verification still creates binding (backward compat)', () => {
|
|
340
|
+
// Create an inbound challenge (no expected identity — guardian flow)
|
|
341
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
342
|
+
const { createVerificationChallenge } = require('../runtime/channel-guardian-service.js');
|
|
343
|
+
const { secret } = createVerificationChallenge('self', 'telegram');
|
|
344
|
+
|
|
345
|
+
const result = validateAndConsumeChallenge(
|
|
346
|
+
'self', 'telegram', secret,
|
|
347
|
+
'guardian-user', 'guardian-chat',
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
expect(result.success).toBe(true);
|
|
351
|
+
if (result.success && result.verificationType === 'guardian') {
|
|
352
|
+
expect(result.bindingId).toBeDefined();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Guardian binding should be created
|
|
356
|
+
const binding = getActiveBinding('self', 'telegram');
|
|
357
|
+
expect(binding).not.toBeNull();
|
|
358
|
+
expect(binding!.guardianExternalUserId).toBe('guardian-user');
|
|
359
|
+
});
|
|
360
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
appendReleaseBlock,
|
|
5
|
+
extractReleaseIds,
|
|
6
|
+
hasReleaseBlock,
|
|
7
|
+
releaseMarker,
|
|
8
|
+
} from '../config/update-bulletin-format.js';
|
|
9
|
+
|
|
10
|
+
describe('releaseMarker', () => {
|
|
11
|
+
test('returns an HTML comment with the version embedded', () => {
|
|
12
|
+
expect(releaseMarker('1.2.3')).toBe('<!-- vellum-update-release:1.2.3 -->');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('handles pre-release / build-metadata versions', () => {
|
|
16
|
+
expect(releaseMarker('2.0.0-beta.1+build.42')).toBe(
|
|
17
|
+
'<!-- vellum-update-release:2.0.0-beta.1+build.42 -->',
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('hasReleaseBlock', () => {
|
|
23
|
+
const content = [
|
|
24
|
+
'<!-- vellum-update-release:1.0.0 -->',
|
|
25
|
+
'## 1.0.0',
|
|
26
|
+
'Initial release.',
|
|
27
|
+
'',
|
|
28
|
+
'<!-- vellum-update-release:1.1.0 -->',
|
|
29
|
+
'## 1.1.0',
|
|
30
|
+
'Second release.',
|
|
31
|
+
].join('\n');
|
|
32
|
+
|
|
33
|
+
test('returns true when the marker is present', () => {
|
|
34
|
+
expect(hasReleaseBlock(content, '1.0.0')).toBe(true);
|
|
35
|
+
expect(hasReleaseBlock(content, '1.1.0')).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('returns false when the marker is absent', () => {
|
|
39
|
+
expect(hasReleaseBlock(content, '2.0.0')).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('returns false for empty content', () => {
|
|
43
|
+
expect(hasReleaseBlock('', '1.0.0')).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('appendReleaseBlock', () => {
|
|
48
|
+
test('appends to empty content', () => {
|
|
49
|
+
const result = appendReleaseBlock('', '1.0.0', '## 1.0.0\nInitial release.');
|
|
50
|
+
expect(result).toBe(
|
|
51
|
+
'<!-- vellum-update-release:1.0.0 -->\n## 1.0.0\nInitial release.\n',
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('preserves prior blocks when appending', () => {
|
|
56
|
+
const existing =
|
|
57
|
+
'<!-- vellum-update-release:1.0.0 -->\n## 1.0.0\nFirst.\n';
|
|
58
|
+
const result = appendReleaseBlock(existing, '1.1.0', '## 1.1.0\nSecond.');
|
|
59
|
+
|
|
60
|
+
// Prior block is untouched
|
|
61
|
+
expect(result).toContain('<!-- vellum-update-release:1.0.0 -->');
|
|
62
|
+
expect(result).toContain('## 1.0.0\nFirst.');
|
|
63
|
+
|
|
64
|
+
// New block is appended
|
|
65
|
+
expect(result).toContain('<!-- vellum-update-release:1.1.0 -->');
|
|
66
|
+
expect(result).toContain('## 1.1.0\nSecond.');
|
|
67
|
+
|
|
68
|
+
// New block comes after old block
|
|
69
|
+
const oldIdx = result.indexOf('<!-- vellum-update-release:1.0.0 -->');
|
|
70
|
+
const newIdx = result.indexOf('<!-- vellum-update-release:1.1.0 -->');
|
|
71
|
+
expect(newIdx).toBeGreaterThan(oldIdx);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('inserts separator when existing content lacks trailing newline', () => {
|
|
75
|
+
const existing = '<!-- vellum-update-release:1.0.0 -->\nFirst.';
|
|
76
|
+
const result = appendReleaseBlock(existing, '1.1.0', 'Second.');
|
|
77
|
+
|
|
78
|
+
// Double newline separates the blocks when there was no trailing newline
|
|
79
|
+
expect(result).toContain('First.\n\n<!-- vellum-update-release:1.1.0 -->');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('extractReleaseIds', () => {
|
|
84
|
+
test('returns all version strings from multiple markers', () => {
|
|
85
|
+
const content = [
|
|
86
|
+
'<!-- vellum-update-release:1.0.0 -->',
|
|
87
|
+
'Block one.',
|
|
88
|
+
'<!-- vellum-update-release:1.1.0 -->',
|
|
89
|
+
'Block two.',
|
|
90
|
+
'<!-- vellum-update-release:2.0.0-rc.1 -->',
|
|
91
|
+
'Block three.',
|
|
92
|
+
].join('\n');
|
|
93
|
+
|
|
94
|
+
expect(extractReleaseIds(content)).toEqual([
|
|
95
|
+
'1.0.0',
|
|
96
|
+
'1.1.0',
|
|
97
|
+
'2.0.0-rc.1',
|
|
98
|
+
]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('returns empty array for empty content', () => {
|
|
102
|
+
expect(extractReleaseIds('')).toEqual([]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('returns empty array when no markers are present', () => {
|
|
106
|
+
expect(extractReleaseIds('Just some text\nwith no markers.')).toEqual([]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('handles duplicate markers', () => {
|
|
110
|
+
const content = [
|
|
111
|
+
'<!-- vellum-update-release:1.0.0 -->',
|
|
112
|
+
'Block one.',
|
|
113
|
+
'<!-- vellum-update-release:1.0.0 -->',
|
|
114
|
+
'Duplicate block.',
|
|
115
|
+
].join('\n');
|
|
116
|
+
|
|
117
|
+
expect(extractReleaseIds(content)).toEqual(['1.0.0', '1.0.0']);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
const store = new Map<string, string>();
|
|
4
|
+
|
|
5
|
+
mock.module('../memory/checkpoints.js', () => ({
|
|
6
|
+
getMemoryCheckpoint: mock((key: string) => store.get(key) ?? null),
|
|
7
|
+
setMemoryCheckpoint: mock((key: string, value: string) => store.set(key, value)),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
getActiveReleases,
|
|
12
|
+
setActiveReleases,
|
|
13
|
+
getCompletedReleases,
|
|
14
|
+
setCompletedReleases,
|
|
15
|
+
isReleaseCompleted,
|
|
16
|
+
markReleasesCompleted,
|
|
17
|
+
addActiveRelease,
|
|
18
|
+
} = await import('../config/update-bulletin-state.js');
|
|
19
|
+
|
|
20
|
+
describe('update-bulletin-state', () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
store.clear();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('empty/default state', () => {
|
|
26
|
+
it('returns empty array when no active releases checkpoint exists', () => {
|
|
27
|
+
expect(getActiveReleases()).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('returns empty array when no completed releases checkpoint exists', () => {
|
|
31
|
+
expect(getCompletedReleases()).toEqual([]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('isReleaseCompleted returns false when no completed releases exist', () => {
|
|
35
|
+
expect(isReleaseCompleted('1.0.0')).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('corrupt checkpoint content', () => {
|
|
40
|
+
it('returns empty array for invalid JSON in active releases', () => {
|
|
41
|
+
store.set('updates:active_releases', 'not-json{{{');
|
|
42
|
+
expect(getActiveReleases()).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('returns empty array for invalid JSON in completed releases', () => {
|
|
46
|
+
store.set('updates:completed_releases', '}{broken');
|
|
47
|
+
expect(getCompletedReleases()).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('returns empty array when checkpoint contains a non-array JSON value', () => {
|
|
51
|
+
store.set('updates:active_releases', '"just-a-string"');
|
|
52
|
+
expect(getActiveReleases()).toEqual([]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('filters out non-string values from the array', () => {
|
|
56
|
+
store.set('updates:active_releases', '["1.0.0", 42, null, "2.0.0"]');
|
|
57
|
+
expect(getActiveReleases()).toEqual(['1.0.0', '2.0.0']);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('round-trip serialization', () => {
|
|
62
|
+
it('write then read returns same data for active releases', () => {
|
|
63
|
+
const releases = ['1.0.0', '2.0.0', '3.0.0'];
|
|
64
|
+
setActiveReleases(releases);
|
|
65
|
+
expect(getActiveReleases()).toEqual(releases);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('write then read returns same data for completed releases', () => {
|
|
69
|
+
const releases = ['0.9.0', '1.0.0'];
|
|
70
|
+
setCompletedReleases(releases);
|
|
71
|
+
expect(getCompletedReleases()).toEqual(releases);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('isReleaseCompleted returns true for a completed release', () => {
|
|
75
|
+
setCompletedReleases(['1.0.0', '2.0.0']);
|
|
76
|
+
expect(isReleaseCompleted('1.0.0')).toBe(true);
|
|
77
|
+
expect(isReleaseCompleted('2.0.0')).toBe(true);
|
|
78
|
+
expect(isReleaseCompleted('3.0.0')).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('dedupe behavior', () => {
|
|
83
|
+
it('setActiveReleases deduplicates entries', () => {
|
|
84
|
+
setActiveReleases(['1.0.0', '2.0.0', '1.0.0', '2.0.0', '1.0.0']);
|
|
85
|
+
expect(getActiveReleases()).toEqual(['1.0.0', '2.0.0']);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('setCompletedReleases deduplicates entries', () => {
|
|
89
|
+
setCompletedReleases(['a', 'b', 'a']);
|
|
90
|
+
expect(getCompletedReleases()).toEqual(['a', 'b']);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('addActiveRelease does not duplicate an existing release', () => {
|
|
94
|
+
setActiveReleases(['1.0.0']);
|
|
95
|
+
addActiveRelease('1.0.0');
|
|
96
|
+
expect(getActiveReleases()).toEqual(['1.0.0']);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('markReleasesCompleted does not duplicate existing entries', () => {
|
|
100
|
+
setCompletedReleases(['1.0.0']);
|
|
101
|
+
markReleasesCompleted(['1.0.0', '2.0.0']);
|
|
102
|
+
expect(getCompletedReleases()).toEqual(['1.0.0', '2.0.0']);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('sort behavior', () => {
|
|
107
|
+
it('active releases are sorted alphabetically', () => {
|
|
108
|
+
setActiveReleases(['c-release', 'a-release', 'b-release']);
|
|
109
|
+
expect(getActiveReleases()).toEqual(['a-release', 'b-release', 'c-release']);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('completed releases are sorted alphabetically', () => {
|
|
113
|
+
setCompletedReleases(['3.0.0', '1.0.0', '2.0.0']);
|
|
114
|
+
expect(getCompletedReleases()).toEqual(['1.0.0', '2.0.0', '3.0.0']);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('addActiveRelease maintains sorted order', () => {
|
|
118
|
+
setActiveReleases(['a', 'c']);
|
|
119
|
+
addActiveRelease('b');
|
|
120
|
+
expect(getActiveReleases()).toEqual(['a', 'b', 'c']);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('markReleasesCompleted maintains sorted order', () => {
|
|
124
|
+
setCompletedReleases(['c']);
|
|
125
|
+
markReleasesCompleted(['a', 'b']);
|
|
126
|
+
expect(getCompletedReleases()).toEqual(['a', 'b', 'c']);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|