@vellumai/assistant 0.3.3 → 0.3.5
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/Dockerfile +2 -0
- package/README.md +45 -18
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +13 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +100 -0
- package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
- package/src/__tests__/approval-message-composer.test.ts +253 -0
- package/src/__tests__/call-domain.test.ts +12 -2
- package/src/__tests__/call-orchestrator.test.ts +391 -1
- package/src/__tests__/call-routes-http.test.ts +27 -2
- package/src/__tests__/channel-approval-routes.test.ts +397 -135
- package/src/__tests__/channel-approvals.test.ts +99 -3
- package/src/__tests__/channel-delivery-store.test.ts +30 -4
- package/src/__tests__/channel-guardian.test.ts +261 -22
- package/src/__tests__/channel-readiness-service.test.ts +257 -0
- package/src/__tests__/config-schema.test.ts +2 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/daemon-lifecycle.test.ts +636 -0
- package/src/__tests__/dictation-mode-detection.test.ts +63 -0
- package/src/__tests__/entity-search.test.ts +615 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +19 -13
- package/src/__tests__/handlers-twilio-config.test.ts +480 -0
- package/src/__tests__/ipc-snapshot.test.ts +63 -0
- package/src/__tests__/messaging-send-tool.test.ts +65 -0
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +4 -0
- package/src/__tests__/run-orchestrator.test.ts +22 -0
- package/src/__tests__/secret-scanner.test.ts +223 -0
- package/src/__tests__/session-runtime-assembly.test.ts +85 -1
- package/src/__tests__/shell-parser-property.test.ts +357 -2
- package/src/__tests__/sms-messaging-provider.test.ts +125 -0
- package/src/__tests__/system-prompt.test.ts +25 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +34 -1
- package/src/__tests__/twilio-routes.test.ts +39 -3
- package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
- package/src/__tests__/user-reference.test.ts +68 -0
- package/src/__tests__/web-search.test.ts +1 -1
- package/src/__tests__/work-item-output.test.ts +110 -0
- package/src/calls/call-domain.ts +8 -5
- package/src/calls/call-orchestrator.ts +85 -22
- package/src/calls/twilio-config.ts +17 -11
- package/src/calls/twilio-rest.ts +276 -0
- package/src/calls/twilio-routes.ts +39 -1
- package/src/cli/map.ts +6 -0
- package/src/commands/__tests__/cc-command-registry.test.ts +67 -0
- package/src/commands/cc-command-registry.ts +14 -1
- package/src/config/bundled-skills/claude-code/TOOLS.json +10 -3
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +199 -0
- package/src/config/bundled-skills/media-processing/TOOLS.json +320 -0
- package/src/config/bundled-skills/media-processing/services/capability-registry.ts +137 -0
- package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +280 -0
- package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +144 -0
- package/src/config/bundled-skills/media-processing/services/feedback-store.ts +136 -0
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +261 -0
- package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +95 -0
- package/src/config/bundled-skills/media-processing/services/timeline-service.ts +267 -0
- package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +301 -0
- package/src/config/bundled-skills/media-processing/tools/detect-events.ts +110 -0
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +190 -0
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
- package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
- package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +166 -0
- package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
- package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +300 -0
- package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +235 -0
- package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +142 -0
- package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +150 -0
- package/src/config/bundled-skills/messaging/SKILL.md +24 -5
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
- package/src/config/bundled-skills/twitter/SKILL.md +19 -3
- package/src/config/defaults.ts +2 -1
- package/src/config/schema.ts +9 -3
- package/src/config/skills.ts +5 -32
- package/src/config/system-prompt.ts +40 -0
- package/src/config/templates/IDENTITY.md +2 -2
- package/src/config/user-reference.ts +29 -0
- package/src/config/vellum-skills/catalog.json +58 -0
- package/src/config/vellum-skills/google-oauth-setup/SKILL.md +3 -3
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +3 -3
- package/src/config/vellum-skills/sms-setup/SKILL.md +118 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -1
- package/src/config/vellum-skills/twilio-setup/SKILL.md +76 -6
- package/src/daemon/auth-manager.ts +103 -0
- package/src/daemon/computer-use-session.ts +8 -1
- package/src/daemon/config-watcher.ts +253 -0
- package/src/daemon/handlers/config.ts +819 -22
- package/src/daemon/handlers/dictation.ts +182 -0
- package/src/daemon/handlers/identity.ts +14 -23
- package/src/daemon/handlers/index.ts +2 -0
- package/src/daemon/handlers/sessions.ts +2 -0
- package/src/daemon/handlers/shared.ts +3 -0
- package/src/daemon/handlers/skills.ts +6 -7
- package/src/daemon/handlers/work-items.ts +15 -7
- package/src/daemon/ipc-contract-inventory.json +10 -0
- package/src/daemon/ipc-contract.ts +114 -4
- package/src/daemon/ipc-handler.ts +87 -0
- package/src/daemon/lifecycle.ts +18 -4
- package/src/daemon/ride-shotgun-handler.ts +11 -1
- package/src/daemon/server.ts +111 -504
- package/src/daemon/session-agent-loop.ts +10 -15
- package/src/daemon/session-runtime-assembly.ts +115 -44
- package/src/daemon/session-tool-setup.ts +2 -0
- package/src/daemon/session.ts +19 -2
- package/src/inbound/public-ingress-urls.ts +3 -3
- package/src/memory/channel-guardian-store.ts +2 -1
- package/src/memory/db-connection.ts +28 -0
- package/src/memory/db-init.ts +1163 -0
- package/src/memory/db.ts +2 -2007
- package/src/memory/embedding-backend.ts +79 -11
- package/src/memory/indexer.ts +2 -0
- package/src/memory/job-handlers/media-processing.ts +100 -0
- package/src/memory/job-utils.ts +64 -4
- package/src/memory/jobs-store.ts +2 -1
- package/src/memory/jobs-worker.ts +11 -1
- package/src/memory/media-store.ts +759 -0
- package/src/memory/recall-cache.ts +107 -0
- package/src/memory/retriever.ts +36 -2
- package/src/memory/schema-migration.ts +984 -0
- package/src/memory/schema.ts +99 -0
- package/src/memory/search/entity.ts +208 -25
- package/src/memory/search/ranking.ts +6 -1
- package/src/memory/search/types.ts +26 -0
- package/src/messaging/provider-types.ts +2 -0
- package/src/messaging/providers/sms/adapter.ts +204 -0
- package/src/messaging/providers/sms/client.ts +93 -0
- package/src/messaging/providers/sms/types.ts +7 -0
- package/src/permissions/checker.ts +16 -2
- package/src/permissions/prompter.ts +14 -3
- package/src/permissions/trust-store.ts +7 -0
- package/src/runtime/approval-message-composer.ts +143 -0
- package/src/runtime/channel-approvals.ts +29 -7
- package/src/runtime/channel-guardian-service.ts +44 -18
- package/src/runtime/channel-readiness-service.ts +292 -0
- package/src/runtime/channel-readiness-types.ts +29 -0
- package/src/runtime/gateway-client.ts +2 -1
- package/src/runtime/http-server.ts +65 -28
- package/src/runtime/http-types.ts +3 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/channel-routes.ts +237 -103
- package/src/runtime/routes/run-routes.ts +7 -1
- package/src/runtime/run-orchestrator.ts +43 -3
- package/src/security/secret-scanner.ts +218 -0
- package/src/skills/frontmatter.ts +63 -0
- package/src/skills/slash-commands.ts +23 -0
- package/src/skills/vellum-catalog-remote.ts +107 -0
- package/src/tools/assets/materialize.ts +2 -2
- package/src/tools/browser/auto-navigate.ts +132 -24
- package/src/tools/browser/browser-manager.ts +67 -61
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/claude-code/claude-code.ts +55 -3
- package/src/tools/credentials/vault.ts +1 -1
- package/src/tools/execution-target.ts +11 -1
- package/src/tools/executor.ts +10 -2
- package/src/tools/network/web-search.ts +1 -1
- package/src/tools/skills/vellum-catalog.ts +61 -156
- package/src/tools/terminal/parser.ts +21 -5
- package/src/tools/types.ts +2 -0
- package/src/twitter/router.ts +1 -1
- package/src/util/platform.ts +43 -1
- package/src/util/retry.ts +4 -4
package/Dockerfile
CHANGED
|
@@ -42,11 +42,13 @@ RUN apt-get update && apt-get install -y \
|
|
|
42
42
|
fonts-freefont-ttf \
|
|
43
43
|
make \
|
|
44
44
|
g++ \
|
|
45
|
+
git \
|
|
45
46
|
sudo \
|
|
46
47
|
&& rm -rf /var/lib/apt/lists/*
|
|
47
48
|
|
|
48
49
|
# Copy bun binary from builder instead of re-installing
|
|
49
50
|
COPY --from=builder /root/.bun/bin/bun /usr/local/bin/bun
|
|
51
|
+
RUN ln -sf /usr/local/bin/bun /usr/local/bin/bunx
|
|
50
52
|
|
|
51
53
|
# Create non-root user that also has sudo access so it can like install stuff
|
|
52
54
|
RUN groupadd --system --gid 1001 assistant && \
|
package/README.md
CHANGED
|
@@ -46,7 +46,6 @@ cp .env.example .env
|
|
|
46
46
|
| `OLLAMA_BASE_URL` | No | `http://127.0.0.1:11434/v1` | Ollama base URL |
|
|
47
47
|
| `RUNTIME_HTTP_PORT` | No | — | Enable the HTTP server (required for gateway/web) |
|
|
48
48
|
| `RUNTIME_GATEWAY_ORIGIN_SECRET` | No | — | Dedicated secret for the `X-Gateway-Origin` proof header on `/channels/inbound`. When not set, falls back to the bearer token. Both gateway and runtime must share the same value. |
|
|
49
|
-
| `CHANNEL_APPROVALS_ENABLED` | No | `false` | Enable channel approval flow including interactive approval UX, guardian enforcement (`forceStrictSideEffects`, fail-closed denial), and approval prompt routing. Actor-role classification runs regardless, but enforcement requires this flag. |
|
|
50
49
|
| `VELLUM_DAEMON_SOCKET` | No | `~/.vellum/vellum.sock` | Override the daemon socket path |
|
|
51
50
|
|
|
52
51
|
## Usage
|
|
@@ -124,7 +123,7 @@ assistant/
|
|
|
124
123
|
|
|
125
124
|
## Channel Approval Flow
|
|
126
125
|
|
|
127
|
-
When the assistant needs tool-use confirmation during a channel session (e.g., Telegram), the approval flow intercepts the run and surfaces an interactive prompt to the user. This is
|
|
126
|
+
When the assistant needs tool-use confirmation during a channel session (e.g., Telegram), the approval flow intercepts the run and surfaces an interactive prompt to the user. This approval-aware path is always enabled whenever orchestrator + callback context are available.
|
|
128
127
|
|
|
129
128
|
### How it works
|
|
130
129
|
|
|
@@ -164,24 +163,17 @@ Channels that do not support rich inline approval UI (e.g., inline keyboards) re
|
|
|
164
163
|
|
|
165
164
|
### Enabling
|
|
166
165
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
```bash
|
|
170
|
-
CHANNEL_APPROVALS_ENABLED=true
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
When disabled (the default), channel messages follow the standard fire-and-forget processing path without approval interception.
|
|
166
|
+
Channel approvals are always enabled for channel traffic when orchestrator + callback context are available.
|
|
174
167
|
|
|
175
168
|
### Guardian-Specific Behavior
|
|
176
169
|
|
|
177
|
-
Guardian actor-role *classification* (determining whether a sender is guardian, non-guardian, or unverified) runs unconditionally.
|
|
170
|
+
Guardian actor-role *classification* (determining whether a sender is guardian, non-guardian, or unverified) runs unconditionally. Guardian *enforcement* for non-guardian/unverified actors (`forceStrictSideEffects`, fail-closed denial for unverified channels, and approval prompt routing to guardians) is always active when orchestrator + callback context are available.
|
|
178
171
|
|
|
179
172
|
| Flag / Behavior | Description |
|
|
180
173
|
|-----------------|-------------|
|
|
181
|
-
| `
|
|
182
|
-
|
|
|
183
|
-
| **Fail-closed no-
|
|
184
|
-
| **Fail-closed no-identity** | When `senderExternalUserId` is absent but a guardian binding exists for the channel, the actor is classified as `unverified_channel`. Only enforced when `CHANNEL_APPROVALS_ENABLED=true`. |
|
|
174
|
+
| `forceStrictSideEffects` | Automatically set on runs triggered by non-guardian or unverified-channel senders so all side-effect tools require approval. |
|
|
175
|
+
| **Fail-closed no-binding** | When no guardian binding exists for a channel, the sender is classified as `unverified_channel`. Any sensitive action is auto-denied with a notice that no guardian has been configured. |
|
|
176
|
+
| **Fail-closed no-identity** | When `senderExternalUserId` is absent, the actor is classified as `unverified_channel` (even if no guardian binding exists yet). |
|
|
185
177
|
| **Guardian-only approval** | Non-guardian senders cannot approve their own pending actions. Only the verified guardian can approve or deny. |
|
|
186
178
|
| **Expired approval auto-deny** | A proactive sweep runs every 60 seconds to find expired guardian approval requests (30-minute TTL). Expired approvals are auto-denied, and both the requester and guardian are notified. If a non-guardian interacts before the sweep runs, the expiry is also detected reactively. |
|
|
187
179
|
|
|
@@ -202,7 +194,7 @@ The `/channels/inbound` endpoint requires a valid `X-Gateway-Origin` header to p
|
|
|
202
194
|
|
|
203
195
|
## Twilio Setup Primitive
|
|
204
196
|
|
|
205
|
-
Twilio is the shared telephony provider for both voice calls and SMS messaging. Configuration is managed through the `twilio_config` IPC contract and the `twilio-setup` skill.
|
|
197
|
+
Twilio is the shared telephony provider for both voice calls and SMS messaging. Configuration is managed through the `twilio_config` IPC contract and the `twilio-setup` skill. For SMS-specific onboarding (including compliance verification and test sending), the `sms-setup` skill provides a guided conversational flow that layers on top of `twilio-setup`.
|
|
206
198
|
|
|
207
199
|
### `twilio_config` IPC Contract
|
|
208
200
|
|
|
@@ -216,8 +208,15 @@ The daemon handles `twilio_config` messages with the following actions:
|
|
|
216
208
|
| `provision_number` | Purchases a new phone number via the Twilio API. Accepts optional `areaCode` and `country` (ISO 3166-1 alpha-2, default `US`). Auto-assigns the number to the assistant (persists to config and secure storage) and configures Twilio webhooks (voice, status callback, SMS) when a public ingress URL is available. |
|
|
217
209
|
| `assign_number` | Assigns an existing Twilio phone number (E.164 format) to the assistant and auto-configures webhooks when ingress is available |
|
|
218
210
|
| `list_numbers` | Lists all incoming phone numbers on the Twilio account with their capabilities (voice, SMS) |
|
|
211
|
+
| `sms_compliance_status` | Returns the SMS compliance posture for the assigned phone number. Determines number type (toll-free vs local 10DLC) and retrieves toll-free verification status from Twilio. |
|
|
212
|
+
| `sms_submit_tollfree_verification` | Submits a new toll-free verification request to Twilio. Validates required fields and enum values. Defaults `businessType` to `SOLE_PROPRIETOR`. |
|
|
213
|
+
| `sms_update_tollfree_verification` | Updates an existing toll-free verification by SID. Requires `verificationSid`. |
|
|
214
|
+
| `sms_delete_tollfree_verification` | Deletes a toll-free verification by SID. Includes warning about queue priority reset. |
|
|
215
|
+
| `release_number` | Releases (deletes) a phone number from the Twilio account. Clears the number from config and secure storage. Includes warning about toll-free verification context loss. |
|
|
216
|
+
| `sms_send_test` | Sends a test SMS to the specified `phoneNumber` with the given `text`, polls Twilio for delivery status (up to 3 retries at 2-second intervals), and returns the result in `testResult`. Stores the last result in memory for use by `sms_doctor`. |
|
|
217
|
+
| `sms_doctor` | Runs a comprehensive SMS health diagnostic. Checks channel readiness, compliance/toll-free verification status, and the last `sms_send_test` result. Returns structured diagnostics in `diagnostics` with an overall `status` ("healthy", "degraded", or "unhealthy") and actionable `items`. |
|
|
219
218
|
|
|
220
|
-
Response type: `twilio_config_response` with `success`, `hasCredentials`, optional `phoneNumber`, optional `numbers` array, optional `error`,
|
|
219
|
+
Response type: `twilio_config_response` with `success`, `hasCredentials`, optional `phoneNumber`, optional `numbers` array, optional `error`, optional `warning` (for non-fatal webhook sync failures), optional `compliance` object (for compliance status actions, containing `numberType`, `verificationSid`, `verificationStatus`, `rejectionReason`, `rejectionReasons`, `errorCode`, `editAllowed`, `editExpiration`), optional `testResult` (for `sms_send_test`), and optional `diagnostics` (for `sms_doctor`).
|
|
221
220
|
|
|
222
221
|
### Ingress Webhook Reconciliation
|
|
223
222
|
|
|
@@ -259,6 +258,34 @@ Guardian bindings, verification challenges, and approval requests are all scoped
|
|
|
259
258
|
|
|
260
259
|
The channel guardian service generates verification challenge instructions with channel-appropriate wording. The `channelLabel()` function maps `sourceChannel` values to human-readable labels (e.g., `"telegram"` -> `"Telegram"`, `"sms"` -> `"SMS"`), so challenge prompts reference the correct channel name.
|
|
261
260
|
|
|
261
|
+
## Channel Readiness
|
|
262
|
+
|
|
263
|
+
The `channel_readiness` IPC contract provides a unified way to check whether a channel (SMS, Telegram, etc.) is fully configured and operational. It runs local checks (credential presence, phone number assignment, ingress config) synchronously and optional remote checks (API reachability) asynchronously with a 5-minute TTL cache.
|
|
264
|
+
|
|
265
|
+
### `channel_readiness` IPC Contract
|
|
266
|
+
|
|
267
|
+
| Action | Description |
|
|
268
|
+
|--------|-------------|
|
|
269
|
+
| `get` | Returns readiness snapshots for the specified channel (or all channels if omitted). Local checks always run; remote checks run only when `includeRemote=true` and cache is stale. |
|
|
270
|
+
| `refresh` | Invalidates the cache for the specified channel (or all channels), then returns fresh snapshots. |
|
|
271
|
+
|
|
272
|
+
Request fields: `action` (required), `channel` (optional filter), `assistantId` (optional), `includeRemote` (optional boolean).
|
|
273
|
+
|
|
274
|
+
Response type: `channel_readiness_response` with `success`, optional `snapshots` array (each with `channel`, `ready`, `checkedAt`, `stale`, `reasons`, `localChecks`, optional `remoteChecks`), and optional `error`.
|
|
275
|
+
|
|
276
|
+
### Built-in Channel Probes
|
|
277
|
+
|
|
278
|
+
- **SMS**: Checks Twilio credentials, phone number assignment, and public ingress URL.
|
|
279
|
+
- **Telegram**: Checks bot token, webhook secret, and public ingress URL.
|
|
280
|
+
|
|
281
|
+
### Key modules
|
|
282
|
+
|
|
283
|
+
| File | Purpose |
|
|
284
|
+
|------|---------|
|
|
285
|
+
| `src/runtime/channel-readiness-types.ts` | Shared types: `ChannelId`, `ReadinessCheckResult`, `ChannelReadinessSnapshot`, `ChannelProbe` |
|
|
286
|
+
| `src/runtime/channel-readiness-service.ts` | Service class with probe registration, cached readiness evaluation, and built-in SMS/Telegram probes |
|
|
287
|
+
| `src/daemon/handlers/config.ts` | `handleChannelReadiness` — IPC handler for `channel_readiness` messages |
|
|
288
|
+
|
|
262
289
|
## Database
|
|
263
290
|
|
|
264
291
|
SQLite via Drizzle ORM, stored at `~/.vellum/workspace/data/db/assistant.db`. Key tables include conversations, messages, tool invocations, attachments, memory segments (with FTS5), memory items, entities, reminders, and recurrence schedules (cron + RRULE).
|
|
@@ -293,9 +320,9 @@ The image runs as non-root user `assistant` (uid 1001) and exposes port `3001`.
|
|
|
293
320
|
| Symptom | Cause | Resolution |
|
|
294
321
|
|---------|-------|------------|
|
|
295
322
|
| 403 `GATEWAY_ORIGIN_REQUIRED` on `/channels/inbound` | Missing or invalid `X-Gateway-Origin` header | Ensure `RUNTIME_GATEWAY_ORIGIN_SECRET` is set to the same value on both gateway and runtime. If not using a dedicated secret, ensure the bearer token (`RUNTIME_BEARER_TOKEN` or `~/.vellum/http-token`) is shared. |
|
|
296
|
-
| Non-guardian actions silently denied | No guardian binding for the channel
|
|
323
|
+
| Non-guardian actions silently denied | No guardian binding for the channel. The system is fail-closed for unverified channels. | Run the guardian verification flow from the desktop UI to bind a guardian. |
|
|
297
324
|
| Guardian approval expired | The 30-minute TTL elapsed. The proactive sweep auto-denied the approval and notified both parties. | The requester must re-trigger the action. |
|
|
298
|
-
| `forceStrictSideEffects` unexpectedly active | The sender is classified as `non-guardian` or `unverified_channel`
|
|
325
|
+
| `forceStrictSideEffects` unexpectedly active | The sender is classified as `non-guardian` or `unverified_channel` | Verify the sender's `externalUserId` matches the guardian binding, or set up a guardian binding for the channel. |
|
|
299
326
|
|
|
300
327
|
### Invalid RRULE set expressions
|
|
301
328
|
|
package/package.json
CHANGED
|
@@ -405,6 +405,19 @@ function emitStruct(s: SwiftStruct): string {
|
|
|
405
405
|
lines.push(` public let ${p.swiftName}: ${p.swiftType}`);
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
+
// Emit public memberwise init (Swift only auto-generates internal inits for public structs)
|
|
409
|
+
if (s.properties.length > 0) {
|
|
410
|
+
lines.push('');
|
|
411
|
+
const params = s.properties
|
|
412
|
+
.map((p) => `${p.swiftName}: ${p.swiftType}${p.isOptional ? ' = nil' : ''}`)
|
|
413
|
+
.join(', ');
|
|
414
|
+
lines.push(` public init(${params}) {`);
|
|
415
|
+
for (const p of s.properties) {
|
|
416
|
+
lines.push(` self.${p.swiftName} = ${p.swiftName}`);
|
|
417
|
+
}
|
|
418
|
+
lines.push(' }');
|
|
419
|
+
}
|
|
420
|
+
|
|
408
421
|
if (needsCodingKeys(s.properties)) {
|
|
409
422
|
lines.push('');
|
|
410
423
|
lines.push(' private enum CodingKeys: String, CodingKey {');
|
|
@@ -601,6 +601,15 @@ exports[`IPC message snapshots ClientMessage types twilio_config serializes to e
|
|
|
601
601
|
}
|
|
602
602
|
`;
|
|
603
603
|
|
|
604
|
+
exports[`IPC message snapshots ClientMessage types channel_readiness serializes to expected JSON 1`] = `
|
|
605
|
+
{
|
|
606
|
+
"action": "get",
|
|
607
|
+
"channel": "sms",
|
|
608
|
+
"includeRemote": true,
|
|
609
|
+
"type": "channel_readiness",
|
|
610
|
+
}
|
|
611
|
+
`;
|
|
612
|
+
|
|
604
613
|
exports[`IPC message snapshots ClientMessage types guardian_verification serializes to expected JSON 1`] = `
|
|
605
614
|
{
|
|
606
615
|
"action": "create_challenge",
|
|
@@ -908,6 +917,20 @@ exports[`IPC message snapshots ClientMessage types tool_names_list serializes to
|
|
|
908
917
|
}
|
|
909
918
|
`;
|
|
910
919
|
|
|
920
|
+
exports[`IPC message snapshots ClientMessage types dictation_request serializes to expected JSON 1`] = `
|
|
921
|
+
{
|
|
922
|
+
"context": {
|
|
923
|
+
"appName": "Example App",
|
|
924
|
+
"bundleIdentifier": "com.example.app",
|
|
925
|
+
"cursorInTextField": true,
|
|
926
|
+
"selectedText": "some selected text",
|
|
927
|
+
"windowTitle": "Main Window",
|
|
928
|
+
},
|
|
929
|
+
"transcription": "Hello world",
|
|
930
|
+
"type": "dictation_request",
|
|
931
|
+
}
|
|
932
|
+
`;
|
|
933
|
+
|
|
911
934
|
exports[`IPC message snapshots ServerMessage types auth_result serializes to expected JSON 1`] = `
|
|
912
935
|
{
|
|
913
936
|
"success": true,
|
|
@@ -1323,6 +1346,14 @@ exports[`IPC message snapshots ServerMessage types task_routed serializes to exp
|
|
|
1323
1346
|
}
|
|
1324
1347
|
`;
|
|
1325
1348
|
|
|
1349
|
+
exports[`IPC message snapshots ServerMessage types ride_shotgun_progress serializes to expected JSON 1`] = `
|
|
1350
|
+
{
|
|
1351
|
+
"message": "Observing user activity...",
|
|
1352
|
+
"type": "ride_shotgun_progress",
|
|
1353
|
+
"watchId": "watch-shotgun-001",
|
|
1354
|
+
}
|
|
1355
|
+
`;
|
|
1356
|
+
|
|
1326
1357
|
exports[`IPC message snapshots ServerMessage types ride_shotgun_result serializes to expected JSON 1`] = `
|
|
1327
1358
|
{
|
|
1328
1359
|
"observationCount": 5,
|
|
@@ -1929,13 +1960,74 @@ exports[`IPC message snapshots ServerMessage types telegram_config_response seri
|
|
|
1929
1960
|
|
|
1930
1961
|
exports[`IPC message snapshots ServerMessage types twilio_config_response serializes to expected JSON 1`] = `
|
|
1931
1962
|
{
|
|
1963
|
+
"compliance": {
|
|
1964
|
+
"numberType": "toll_free",
|
|
1965
|
+
"verificationSid": "TF_VER_001",
|
|
1966
|
+
"verificationStatus": "TWILIO_APPROVED",
|
|
1967
|
+
},
|
|
1968
|
+
"diagnostics": {
|
|
1969
|
+
"actionItems": [],
|
|
1970
|
+
"compliance": {
|
|
1971
|
+
"detail": "Toll-free verification: TWILIO_APPROVED",
|
|
1972
|
+
"status": "TWILIO_APPROVED",
|
|
1973
|
+
},
|
|
1974
|
+
"overallStatus": "healthy",
|
|
1975
|
+
"readiness": {
|
|
1976
|
+
"issues": [],
|
|
1977
|
+
"ready": true,
|
|
1978
|
+
},
|
|
1979
|
+
},
|
|
1932
1980
|
"hasCredentials": true,
|
|
1933
1981
|
"phoneNumber": "+15551234567",
|
|
1934
1982
|
"success": true,
|
|
1983
|
+
"testResult": {
|
|
1984
|
+
"finalStatus": "delivered",
|
|
1985
|
+
"initialStatus": "queued",
|
|
1986
|
+
"messageSid": "SM-test-001",
|
|
1987
|
+
"to": "+15559876543",
|
|
1988
|
+
},
|
|
1935
1989
|
"type": "twilio_config_response",
|
|
1936
1990
|
}
|
|
1937
1991
|
`;
|
|
1938
1992
|
|
|
1993
|
+
exports[`IPC message snapshots ServerMessage types channel_readiness_response serializes to expected JSON 1`] = `
|
|
1994
|
+
{
|
|
1995
|
+
"snapshots": [
|
|
1996
|
+
{
|
|
1997
|
+
"channel": "sms",
|
|
1998
|
+
"checkedAt": 1700000000000,
|
|
1999
|
+
"localChecks": [
|
|
2000
|
+
{
|
|
2001
|
+
"message": "Twilio credentials are not configured",
|
|
2002
|
+
"name": "twilio_credentials",
|
|
2003
|
+
"passed": false,
|
|
2004
|
+
},
|
|
2005
|
+
{
|
|
2006
|
+
"message": "Phone number is assigned",
|
|
2007
|
+
"name": "phone_number",
|
|
2008
|
+
"passed": true,
|
|
2009
|
+
},
|
|
2010
|
+
{
|
|
2011
|
+
"message": "Public ingress URL is configured",
|
|
2012
|
+
"name": "ingress",
|
|
2013
|
+
"passed": true,
|
|
2014
|
+
},
|
|
2015
|
+
],
|
|
2016
|
+
"ready": false,
|
|
2017
|
+
"reasons": [
|
|
2018
|
+
{
|
|
2019
|
+
"code": "twilio_credentials",
|
|
2020
|
+
"text": "Twilio credentials are not configured",
|
|
2021
|
+
},
|
|
2022
|
+
],
|
|
2023
|
+
"stale": false,
|
|
2024
|
+
},
|
|
2025
|
+
],
|
|
2026
|
+
"success": true,
|
|
2027
|
+
"type": "channel_readiness_response",
|
|
2028
|
+
}
|
|
2029
|
+
`;
|
|
2030
|
+
|
|
1939
2031
|
exports[`IPC message snapshots ServerMessage types guardian_verification_response serializes to expected JSON 1`] = `
|
|
1940
2032
|
{
|
|
1941
2033
|
"instruction": "Send this code to the Telegram bot",
|
|
@@ -2499,3 +2591,11 @@ exports[`IPC message snapshots ServerMessage types tool_names_list_response seri
|
|
|
2499
2591
|
"type": "tool_names_list_response",
|
|
2500
2592
|
}
|
|
2501
2593
|
`;
|
|
2594
|
+
|
|
2595
|
+
exports[`IPC message snapshots ServerMessage types dictation_response serializes to expected JSON 1`] = `
|
|
2596
|
+
{
|
|
2597
|
+
"mode": "dictation",
|
|
2598
|
+
"text": "Hello world",
|
|
2599
|
+
"type": "dictation_response",
|
|
2600
|
+
}
|
|
2601
|
+
`;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard test: ensures no production hard-coded approval lifecycle copy creeps
|
|
3
|
+
* back into route/service files outside of the composer module.
|
|
4
|
+
*
|
|
5
|
+
* The composer file (`approval-message-composer.ts`) is intentionally excluded
|
|
6
|
+
* since that is where deterministic fallback copy legitimately lives.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { describe, test, expect } from 'bun:test';
|
|
12
|
+
|
|
13
|
+
const SCANNED_FILES = [
|
|
14
|
+
'runtime/channel-approvals.ts',
|
|
15
|
+
'runtime/routes/channel-routes.ts',
|
|
16
|
+
'runtime/channel-guardian-service.ts',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const BANNED_PATTERNS: { pattern: RegExp; description: string }[] = [
|
|
20
|
+
{ pattern: /The assistant wants to use/i, description: 'old standard prompt' },
|
|
21
|
+
{ pattern: /Do you want to allow this/i, description: 'old approval question' },
|
|
22
|
+
{ pattern: /['"`]I'm still waiting/i, description: 'old reminder prefix (string literal)' },
|
|
23
|
+
{ pattern: /['"`].*is requesting to run/i, description: 'old guardian prompt (string literal)' },
|
|
24
|
+
{ pattern: /['"`]Sent to guardian/i, description: 'old forwarding notice (string literal)' },
|
|
25
|
+
{ pattern: /['"`]Guardian verified successfully/i, description: 'old verify success (string literal)' },
|
|
26
|
+
{ pattern: /['"`]Verification failed/i, description: 'old verify failure (string literal)' },
|
|
27
|
+
{ pattern: /['"`]Your request has been sent/i, description: 'old request forwarded notice (string literal)' },
|
|
28
|
+
{ pattern: /['"`]No guardian is configured/i, description: 'old no-binding notice (string literal)' },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
describe('approval hardcoded copy guard', () => {
|
|
32
|
+
for (const file of SCANNED_FILES) {
|
|
33
|
+
test(`${file} does not contain banned approval copy literals`, () => {
|
|
34
|
+
const content = readFileSync(join(__dirname, '..', file), 'utf-8');
|
|
35
|
+
for (const { pattern, description: _description } of BANNED_PATTERNS) {
|
|
36
|
+
const match = content.match(pattern);
|
|
37
|
+
expect(match).toBeNull();
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
});
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
composeApprovalMessage,
|
|
4
|
+
getFallbackMessage,
|
|
5
|
+
} from '../runtime/approval-message-composer.js';
|
|
6
|
+
import type {
|
|
7
|
+
ApprovalMessageScenario,
|
|
8
|
+
ApprovalMessageContext,
|
|
9
|
+
} from '../runtime/approval-message-composer.js';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Every scenario must produce a non-empty string
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
const ALL_SCENARIOS: ApprovalMessageScenario[] = [
|
|
16
|
+
'standard_prompt',
|
|
17
|
+
'guardian_prompt',
|
|
18
|
+
'reminder_prompt',
|
|
19
|
+
'guardian_delivery_failed',
|
|
20
|
+
'guardian_request_forwarded',
|
|
21
|
+
'guardian_disambiguation',
|
|
22
|
+
'guardian_identity_mismatch',
|
|
23
|
+
'request_pending_guardian',
|
|
24
|
+
'guardian_decision_outcome',
|
|
25
|
+
'guardian_expired_requester',
|
|
26
|
+
'guardian_expired_guardian',
|
|
27
|
+
'guardian_verify_success',
|
|
28
|
+
'guardian_verify_failed',
|
|
29
|
+
'guardian_verify_challenge_setup',
|
|
30
|
+
'guardian_verify_status_bound',
|
|
31
|
+
'guardian_verify_status_unbound',
|
|
32
|
+
'guardian_deny_no_identity',
|
|
33
|
+
'guardian_deny_no_binding',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
describe('approval-message-composer', () => {
|
|
37
|
+
// -----------------------------------------------------------------------
|
|
38
|
+
// Fallback messages — every scenario produces non-empty output
|
|
39
|
+
// -----------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
describe('getFallbackMessage', () => {
|
|
42
|
+
for (const scenario of ALL_SCENARIOS) {
|
|
43
|
+
test(`scenario "${scenario}" produces a non-empty string`, () => {
|
|
44
|
+
const msg = getFallbackMessage({ scenario });
|
|
45
|
+
expect(typeof msg).toBe('string');
|
|
46
|
+
expect(msg.trim().length).toBeGreaterThan(0);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
test('standard_prompt includes toolName when provided', () => {
|
|
51
|
+
const msg = getFallbackMessage({
|
|
52
|
+
scenario: 'standard_prompt',
|
|
53
|
+
toolName: 'execute_shell',
|
|
54
|
+
});
|
|
55
|
+
expect(msg).toContain('execute_shell');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('guardian_prompt includes requester identifier and toolName', () => {
|
|
59
|
+
const msg = getFallbackMessage({
|
|
60
|
+
scenario: 'guardian_prompt',
|
|
61
|
+
toolName: 'write_file',
|
|
62
|
+
requesterIdentifier: 'alice',
|
|
63
|
+
});
|
|
64
|
+
expect(msg).toContain('alice');
|
|
65
|
+
expect(msg).toContain('write_file');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('guardian_delivery_failed includes toolName when provided', () => {
|
|
69
|
+
const msg = getFallbackMessage({
|
|
70
|
+
scenario: 'guardian_delivery_failed',
|
|
71
|
+
toolName: 'execute_shell',
|
|
72
|
+
});
|
|
73
|
+
expect(msg).toContain('execute_shell');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('guardian_request_forwarded includes toolName', () => {
|
|
77
|
+
const msg = getFallbackMessage({
|
|
78
|
+
scenario: 'guardian_request_forwarded',
|
|
79
|
+
toolName: 'execute_shell',
|
|
80
|
+
});
|
|
81
|
+
expect(msg).toContain('execute_shell');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('guardian_disambiguation includes pendingCount', () => {
|
|
85
|
+
const msg = getFallbackMessage({
|
|
86
|
+
scenario: 'guardian_disambiguation',
|
|
87
|
+
pendingCount: 3,
|
|
88
|
+
});
|
|
89
|
+
expect(msg).toContain('3');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('guardian_decision_outcome includes decision and toolName', () => {
|
|
93
|
+
const msg = getFallbackMessage({
|
|
94
|
+
scenario: 'guardian_decision_outcome',
|
|
95
|
+
decision: 'approved',
|
|
96
|
+
toolName: 'read_file',
|
|
97
|
+
});
|
|
98
|
+
expect(msg).toContain('approved');
|
|
99
|
+
expect(msg).toContain('read_file');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('guardian_expired_requester includes toolName', () => {
|
|
103
|
+
const msg = getFallbackMessage({
|
|
104
|
+
scenario: 'guardian_expired_requester',
|
|
105
|
+
toolName: 'deploy',
|
|
106
|
+
});
|
|
107
|
+
expect(msg).toContain('deploy');
|
|
108
|
+
expect(msg).toContain('expired');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('guardian_expired_guardian includes requester and toolName', () => {
|
|
112
|
+
const msg = getFallbackMessage({
|
|
113
|
+
scenario: 'guardian_expired_guardian',
|
|
114
|
+
requesterIdentifier: 'bob',
|
|
115
|
+
toolName: 'delete_file',
|
|
116
|
+
});
|
|
117
|
+
expect(msg).toContain('bob');
|
|
118
|
+
expect(msg).toContain('delete_file');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('guardian_verify_failed includes failureReason', () => {
|
|
122
|
+
const msg = getFallbackMessage({
|
|
123
|
+
scenario: 'guardian_verify_failed',
|
|
124
|
+
failureReason: 'Code did not match.',
|
|
125
|
+
});
|
|
126
|
+
expect(msg).toContain('Code did not match.');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('guardian_verify_challenge_setup includes verifyCommand and ttlSeconds', () => {
|
|
130
|
+
const msg = getFallbackMessage({
|
|
131
|
+
scenario: 'guardian_verify_challenge_setup',
|
|
132
|
+
verifyCommand: '/verify abc123',
|
|
133
|
+
ttlSeconds: 30,
|
|
134
|
+
});
|
|
135
|
+
expect(msg).toContain('/verify abc123');
|
|
136
|
+
expect(msg).toContain('30');
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// -----------------------------------------------------------------------
|
|
141
|
+
// composeApprovalMessage — layered source selection
|
|
142
|
+
// -----------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
describe('composeApprovalMessage', () => {
|
|
145
|
+
test('returns assistantPreface when provided (primary source)', () => {
|
|
146
|
+
const preface = 'The assistant already said something helpful.';
|
|
147
|
+
const msg = composeApprovalMessage({
|
|
148
|
+
scenario: 'standard_prompt',
|
|
149
|
+
toolName: 'execute_shell',
|
|
150
|
+
assistantPreface: preface,
|
|
151
|
+
});
|
|
152
|
+
expect(msg).toBe(preface);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('ignores empty assistantPreface and falls back to template', () => {
|
|
156
|
+
const msg = composeApprovalMessage({
|
|
157
|
+
scenario: 'standard_prompt',
|
|
158
|
+
toolName: 'execute_shell',
|
|
159
|
+
assistantPreface: '',
|
|
160
|
+
});
|
|
161
|
+
expect(msg).toContain('execute_shell');
|
|
162
|
+
expect(msg).not.toBe('');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('ignores whitespace-only assistantPreface', () => {
|
|
166
|
+
const msg = composeApprovalMessage({
|
|
167
|
+
scenario: 'standard_prompt',
|
|
168
|
+
toolName: 'execute_shell',
|
|
169
|
+
assistantPreface: ' ',
|
|
170
|
+
});
|
|
171
|
+
expect(msg).toContain('execute_shell');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('falls back to deterministic template when no assistantPreface', () => {
|
|
175
|
+
const msg = composeApprovalMessage({
|
|
176
|
+
scenario: 'guardian_prompt',
|
|
177
|
+
toolName: 'write_file',
|
|
178
|
+
requesterIdentifier: 'charlie',
|
|
179
|
+
});
|
|
180
|
+
expect(msg).toContain('charlie');
|
|
181
|
+
expect(msg).toContain('write_file');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('fallback matches getFallbackMessage output', () => {
|
|
185
|
+
const ctx: ApprovalMessageContext = {
|
|
186
|
+
scenario: 'reminder_prompt',
|
|
187
|
+
};
|
|
188
|
+
expect(composeApprovalMessage(ctx)).toBe(getFallbackMessage(ctx));
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// -----------------------------------------------------------------------
|
|
193
|
+
// Verification scenario resilience — composed messages contain key facts
|
|
194
|
+
// -----------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
describe('verification scenario resilience', () => {
|
|
197
|
+
test('guardian_verify_challenge_setup includes verify command and TTL', () => {
|
|
198
|
+
const msg = composeApprovalMessage({
|
|
199
|
+
scenario: 'guardian_verify_challenge_setup',
|
|
200
|
+
verifyCommand: '/guardian_verify abc123def456',
|
|
201
|
+
ttlSeconds: 600,
|
|
202
|
+
});
|
|
203
|
+
expect(typeof msg).toBe('string');
|
|
204
|
+
expect(msg.length).toBeGreaterThan(0);
|
|
205
|
+
expect(msg).toContain('/guardian_verify abc123def456');
|
|
206
|
+
expect(msg).toContain('600');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('guardian_verify_failed includes failure reason', () => {
|
|
210
|
+
const msg = composeApprovalMessage({
|
|
211
|
+
scenario: 'guardian_verify_failed',
|
|
212
|
+
failureReason: 'Too many attempts. Please try again later.',
|
|
213
|
+
});
|
|
214
|
+
expect(typeof msg).toBe('string');
|
|
215
|
+
expect(msg.length).toBeGreaterThan(0);
|
|
216
|
+
expect(msg).toContain('Too many attempts');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('guardian_verify_failed with invalid-or-expired reason includes that reason', () => {
|
|
220
|
+
const msg = composeApprovalMessage({
|
|
221
|
+
scenario: 'guardian_verify_failed',
|
|
222
|
+
failureReason: 'The verification code is invalid or has expired.',
|
|
223
|
+
});
|
|
224
|
+
expect(typeof msg).toBe('string');
|
|
225
|
+
expect(msg.length).toBeGreaterThan(0);
|
|
226
|
+
expect(msg).toContain('invalid or has expired');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('guardian_verify_success produces a non-empty success message', () => {
|
|
230
|
+
const msg = composeApprovalMessage({
|
|
231
|
+
scenario: 'guardian_verify_success',
|
|
232
|
+
});
|
|
233
|
+
expect(typeof msg).toBe('string');
|
|
234
|
+
expect(msg.length).toBeGreaterThan(0);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('guardian_verify_status_bound produces a non-empty message', () => {
|
|
238
|
+
const msg = composeApprovalMessage({
|
|
239
|
+
scenario: 'guardian_verify_status_bound',
|
|
240
|
+
});
|
|
241
|
+
expect(typeof msg).toBe('string');
|
|
242
|
+
expect(msg.length).toBeGreaterThan(0);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('guardian_verify_status_unbound produces a non-empty message', () => {
|
|
246
|
+
const msg = composeApprovalMessage({
|
|
247
|
+
scenario: 'guardian_verify_status_unbound',
|
|
248
|
+
});
|
|
249
|
+
expect(typeof msg).toBe('string');
|
|
250
|
+
expect(msg.length).toBeGreaterThan(0);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -34,10 +34,10 @@ mock.module('../util/logger.js', () => ({
|
|
|
34
34
|
}));
|
|
35
35
|
|
|
36
36
|
mock.module('../calls/twilio-config.js', () => ({
|
|
37
|
-
getTwilioConfig: () => ({
|
|
37
|
+
getTwilioConfig: (assistantId?: string) => ({
|
|
38
38
|
accountSid: 'AC_test',
|
|
39
39
|
authToken: 'test_token',
|
|
40
|
-
phoneNumber: '+15550001111',
|
|
40
|
+
phoneNumber: assistantId === 'ast-alpha' ? '+15550003333' : '+15550001111',
|
|
41
41
|
webhookBaseUrl: 'https://test.example.com',
|
|
42
42
|
wssBaseUrl: 'wss://test.example.com',
|
|
43
43
|
}),
|
|
@@ -97,6 +97,16 @@ describe('resolveCallerIdentity — strict implicit-default policy', () => {
|
|
|
97
97
|
}
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
+
test('assistant_number resolves from assistant-scoped Twilio number when assistantId is provided', async () => {
|
|
101
|
+
const result = await resolveCallerIdentity(makeConfig(), undefined, 'ast-alpha');
|
|
102
|
+
expect(result.ok).toBe(true);
|
|
103
|
+
if (result.ok) {
|
|
104
|
+
expect(result.mode).toBe('assistant_number');
|
|
105
|
+
expect(result.fromNumber).toBe('+15550003333');
|
|
106
|
+
expect(result.source).toBe('implicit_default');
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
100
110
|
test('explicit user_number succeeds when eligible', async () => {
|
|
101
111
|
const result = await resolveCallerIdentity(
|
|
102
112
|
makeConfig({ userNumber: '+15550002222' }),
|