@vellumai/assistant 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +54 -54
- package/docs/architecture/integrations.md +62 -67
- package/docs/credential-execution-service.md +3 -3
- package/package.json +1 -1
- package/src/__tests__/agent-loop.test.ts +111 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
- package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
- package/src/__tests__/app-dir-path-guard.test.ts +78 -0
- package/src/__tests__/app-executors.test.ts +1 -291
- package/src/__tests__/app-git-history.test.ts +4 -4
- package/src/__tests__/app-routes-csp.test.ts +1 -0
- package/src/__tests__/app-store-dir-names.test.ts +426 -0
- package/src/__tests__/attachments-store.test.ts +169 -21
- package/src/__tests__/attachments.test.ts +115 -1
- package/src/__tests__/btw-routes.test.ts +1 -0
- package/src/__tests__/canonical-guardian-store.test.ts +38 -0
- package/src/__tests__/channel-reply-delivery.test.ts +55 -0
- package/src/__tests__/checker.test.ts +54 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
- package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
- package/src/__tests__/compaction.benchmark.test.ts +2 -1
- package/src/__tests__/config-schema-cmd.test.ts +68 -21
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +149 -5
- package/src/__tests__/conversation-agent-loop.test.ts +290 -2
- package/src/__tests__/conversation-attachments.test.ts +17 -19
- package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
- package/src/__tests__/conversation-disk-view.test.ts +810 -0
- package/src/__tests__/conversation-error.test.ts +1 -1
- package/src/__tests__/conversation-fork-crud.test.ts +551 -0
- package/src/__tests__/conversation-fork-route.test.ts +386 -0
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
- package/src/__tests__/conversation-media-retry.test.ts +8 -2
- package/src/__tests__/conversation-queue.test.ts +36 -1
- package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
- package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
- package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
- package/src/__tests__/conversation-skill-tools.test.ts +4 -9
- package/src/__tests__/conversation-slash-commands.test.ts +149 -0
- package/src/__tests__/conversation-store.test.ts +24 -21
- package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
- package/src/__tests__/conversation-title-service.test.ts +137 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
- package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
- package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
- package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
- package/src/__tests__/credential-security-invariants.test.ts +3 -0
- package/src/__tests__/credential-vault-unit.test.ts +5 -10
- package/src/__tests__/cu-unified-flow.test.ts +1 -0
- package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
- package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
- package/src/__tests__/diagnostics-export.test.ts +70 -1
- package/src/__tests__/first-greeting.test.ts +80 -0
- package/src/__tests__/gateway-only-guard.test.ts +1 -0
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
- package/src/__tests__/history-repair.test.ts +32 -10
- package/src/__tests__/http-conversation-lineage.test.ts +251 -0
- package/src/__tests__/image-source-path-reinject.test.ts +136 -0
- package/src/__tests__/llm-context-normalization.test.ts +1116 -0
- package/src/__tests__/llm-context-route-provider.test.ts +217 -0
- package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
- package/src/__tests__/media-generate-image.test.ts +47 -94
- package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
- package/src/__tests__/memory-recall-quality.test.ts +5 -5
- package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
- package/src/__tests__/migration-export-http.test.ts +3 -1
- package/src/__tests__/migration-import-commit-http.test.ts +18 -4
- package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
- package/src/__tests__/mime-builder.test.ts +3 -2
- package/src/__tests__/non-member-access-request.test.ts +12 -1
- package/src/__tests__/notification-decision-identity.test.ts +52 -0
- package/src/__tests__/oauth-apps-routes.test.ts +103 -0
- package/src/__tests__/oauth-store.test.ts +115 -0
- package/src/__tests__/provider-error-scenarios.test.ts +1 -3
- package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
- package/src/__tests__/recording-handler.test.ts +17 -0
- package/src/__tests__/registry.test.ts +3 -8
- package/src/__tests__/relay-server.test.ts +1 -1
- package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
- package/src/__tests__/schema-transforms.test.ts +165 -5
- package/src/__tests__/server-history-render.test.ts +2 -2
- package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
- package/src/__tests__/slack-inbound-verification.test.ts +2 -2
- package/src/__tests__/starter-task-flow.test.ts +1 -0
- package/src/__tests__/suggestion-routes.test.ts +443 -0
- package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
- package/src/__tests__/swarm-recursion.test.ts +1 -0
- package/src/__tests__/swarm-tool.test.ts +1 -0
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
- package/src/__tests__/top-level-renderer.test.ts +22 -0
- package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
- package/src/__tests__/web-fetch.test.ts +6 -2
- package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
- package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
- package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
- package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
- package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
- package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
- package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
- package/src/agent/attachments.ts +27 -1
- package/src/agent/loop.ts +29 -1
- package/src/avatar/traits-png-sync.ts +80 -25
- package/src/bundler/app-bundler.ts +4 -4
- package/src/calls/call-domain.ts +1 -0
- package/src/calls/voice-session-bridge.ts +1 -0
- package/src/cli/commands/auth.ts +92 -0
- package/src/cli/commands/avatar.ts +7 -6
- package/src/cli/commands/config.ts +2 -0
- package/src/cli/commands/oauth/providers.ts +29 -0
- package/src/cli/program.ts +12 -0
- package/src/cli.ts +15 -48
- package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
- package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
- package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
- package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
- package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
- package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
- package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
- package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
- package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
- package/src/config/bundled-tool-registry.ts +2 -14
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/loader.ts +64 -0
- package/src/config/raw-config-utils.ts +30 -0
- package/src/config/schema-utils.ts +28 -7
- package/src/config/schema.ts +8 -0
- package/src/config/schemas/elevenlabs.ts +18 -0
- package/src/config/schemas/memory-lifecycle.ts +4 -2
- package/src/config/schemas/memory-storage.ts +1 -1
- package/src/config/schemas/services.ts +8 -6
- package/src/contacts/contact-store.ts +13 -6
- package/src/contacts/contacts-write.ts +0 -1
- package/src/context/window-manager.ts +13 -2
- package/src/daemon/conversation-agent-loop-handlers.ts +48 -7
- package/src/daemon/conversation-agent-loop.ts +56 -19
- package/src/daemon/conversation-attachments.ts +18 -36
- package/src/daemon/conversation-error.ts +2 -1
- package/src/daemon/conversation-history.ts +18 -4
- package/src/daemon/conversation-lifecycle.ts +39 -15
- package/src/daemon/conversation-messaging.ts +70 -26
- package/src/daemon/conversation-process.ts +58 -34
- package/src/daemon/conversation-runtime-assembly.ts +21 -38
- package/src/daemon/conversation-slash.ts +121 -256
- package/src/daemon/conversation-surfaces.ts +143 -20
- package/src/daemon/conversation-tool-setup.ts +0 -6
- package/src/daemon/conversation-workspace.ts +21 -1
- package/src/daemon/conversation.ts +51 -29
- package/src/daemon/first-greeting.ts +35 -0
- package/src/daemon/handlers/config-embeddings.ts +148 -0
- package/src/daemon/handlers/config-model.ts +71 -26
- package/src/daemon/handlers/conversations.ts +0 -23
- package/src/daemon/handlers/recording.ts +26 -21
- package/src/daemon/host-cu-proxy.ts +2 -2
- package/src/daemon/lifecycle.ts +106 -64
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/conversations.ts +19 -0
- package/src/daemon/message-types/messages.ts +1 -0
- package/src/daemon/message-types/shared.ts +2 -0
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/daemon/message-types/upgrades.ts +23 -0
- package/src/daemon/server.ts +83 -12
- package/src/daemon/shutdown-handlers.ts +8 -5
- package/src/daemon/startup-error.ts +9 -0
- package/src/daemon/tool-side-effects.ts +11 -28
- package/src/events/tool-permission-telemetry-listener.ts +1 -3
- package/src/instrument.ts +0 -4
- package/src/media/app-icon-generator.ts +2 -2
- package/src/memory/app-git-service.ts +28 -16
- package/src/memory/app-store.ts +230 -41
- package/src/memory/attachments-store.ts +558 -130
- package/src/memory/conversation-attention-store.ts +70 -0
- package/src/memory/conversation-crud.ts +442 -3
- package/src/memory/conversation-directories.ts +125 -0
- package/src/memory/conversation-disk-view.ts +390 -0
- package/src/memory/conversation-key-store.ts +17 -5
- package/src/memory/conversation-queries.ts +5 -1
- package/src/memory/conversation-title-service.ts +21 -49
- package/src/memory/db-init.ts +28 -0
- package/src/memory/embedding-backend.ts +42 -53
- package/src/memory/embedding-gemini.test.ts +4 -4
- package/src/memory/embedding-local.ts +1 -3
- package/src/memory/embedding-ollama.ts +1 -3
- package/src/memory/embedding-openai.ts +1 -3
- package/src/memory/indexer.ts +9 -7
- package/src/memory/items-extractor.ts +42 -13
- package/src/memory/job-handlers/conversation-starters.ts +6 -1
- package/src/memory/job-handlers/embedding.test.ts +1 -4
- package/src/memory/llm-request-log-store.ts +100 -1
- package/src/memory/migrations/102-alter-table-columns.ts +5 -0
- package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
- package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
- package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
- package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
- package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
- package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
- package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
- package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
- package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
- package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
- package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
- package/src/memory/migrations/index.ts +7 -0
- package/src/memory/migrations/registry.ts +13 -0
- package/src/memory/retriever.test.ts +601 -2
- package/src/memory/retriever.ts +85 -9
- package/src/memory/schema/conversations.ts +6 -0
- package/src/memory/schema/infrastructure.ts +13 -7
- package/src/memory/schema/oauth.ts +6 -0
- package/src/messaging/providers/gmail/mime-builder.ts +3 -1
- package/src/notifications/copy-composer.ts +26 -0
- package/src/notifications/decision-engine.ts +14 -1
- package/src/notifications/emit-signal.ts +1 -1
- package/src/notifications/signal.ts +36 -0
- package/src/oauth/byo-connection.test.ts +1 -45
- package/src/oauth/byo-connection.ts +2 -8
- package/src/oauth/connect-orchestrator.ts +15 -11
- package/src/oauth/connection-resolver.test.ts +191 -0
- package/src/oauth/connection-resolver.ts +66 -38
- package/src/oauth/connection.ts +0 -1
- package/src/oauth/oauth-store.ts +97 -47
- package/src/oauth/platform-connection.test.ts +0 -1
- package/src/oauth/platform-connection.ts +11 -3
- package/src/oauth/seed-providers.ts +78 -3
- package/src/oauth/token-persistence.ts +16 -10
- package/src/permissions/checker.ts +71 -8
- package/src/prompts/templates/BOOTSTRAP.md +2 -0
- package/src/providers/anthropic/client.ts +8 -1
- package/src/providers/failover.ts +4 -1
- package/src/providers/gemini/client.ts +50 -0
- package/src/providers/model-catalog.ts +92 -0
- package/src/providers/model-intents.ts +29 -20
- package/src/providers/openai/client.ts +49 -0
- package/src/providers/types.ts +2 -0
- package/src/runtime/access-request-helper.ts +16 -7
- package/src/runtime/auth/credential-service.ts +3 -1
- package/src/runtime/auth/route-policy.ts +14 -1
- package/src/runtime/btw-sidechain.ts +101 -0
- package/src/runtime/channel-reply-delivery.ts +17 -1
- package/src/runtime/http-router.ts +3 -1
- package/src/runtime/http-server.ts +196 -141
- package/src/runtime/http-types.ts +1 -0
- package/src/runtime/migrations/vbundle-builder.ts +5 -1
- package/src/runtime/routes/access-request-decision.ts +41 -0
- package/src/runtime/routes/app-management-routes.ts +6 -3
- package/src/runtime/routes/app-routes.ts +7 -3
- package/src/runtime/routes/approval-routes.ts +1 -0
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
- package/src/runtime/routes/attachment-routes.ts +45 -15
- package/src/runtime/routes/btw-routes.ts +21 -61
- package/src/runtime/routes/conversation-management-routes.ts +68 -0
- package/src/runtime/routes/conversation-query-routes.ts +180 -10
- package/src/runtime/routes/conversation-routes.ts +222 -28
- package/src/runtime/routes/conversation-starter-routes.ts +9 -11
- package/src/runtime/routes/diagnostics-routes.ts +1 -0
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
- package/src/runtime/routes/llm-context-normalization.ts +1199 -0
- package/src/runtime/routes/log-export-routes.ts +3 -0
- package/src/runtime/routes/memory-item-routes.test.ts +34 -0
- package/src/runtime/routes/memory-item-routes.ts +4 -0
- package/src/runtime/routes/migration-routes.ts +4 -1
- package/src/runtime/routes/oauth-apps.ts +291 -0
- package/src/runtime/routes/secret-routes.ts +28 -1
- package/src/runtime/routes/settings-routes.ts +14 -0
- package/src/runtime/routes/trace-event-routes.ts +4 -1
- package/src/schedule/schedule-store.ts +9 -21
- package/src/security/secure-keys.ts +21 -0
- package/src/signals/bash.ts +1 -1
- package/src/swarm/backend-claude-code.ts +3 -6
- package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
- package/src/telemetry/usage-telemetry-reporter.ts +3 -1
- package/src/tools/AGENTS.md +6 -10
- package/src/tools/apps/executors.ts +17 -232
- package/src/tools/claude-code/claude-code.ts +2 -3
- package/src/tools/credentials/vault.ts +7 -12
- package/src/tools/host-filesystem/read.ts +13 -10
- package/src/tools/network/__tests__/web-search.test.ts +4 -2
- package/src/tools/schedule/list.ts +2 -7
- package/src/tools/schema-transforms.ts +5 -0
- package/src/tools/shared/filesystem/format-diff.ts +2 -7
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/tool-manifest.ts +0 -6
- package/src/tools/ui-surface/definitions.ts +2 -2
- package/src/util/device-id.ts +28 -5
- package/src/util/platform.ts +6 -0
- package/src/util/pricing.ts +1 -0
- package/src/util/retry.ts +1 -3
- package/src/workspace/migrations/002-backfill-installation-id.ts +23 -12
- package/src/workspace/migrations/003-seed-device-id.ts +3 -4
- package/src/workspace/migrations/006-services-config.ts +5 -0
- package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
- package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
- package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
- package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
- package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
- package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
- package/src/workspace/migrations/registry.ts +10 -0
- package/src/workspace/top-level-renderer.ts +12 -0
- package/src/__tests__/asset-materialize-tool.test.ts +0 -523
- package/src/__tests__/asset-search-tool.test.ts +0 -536
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
- package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
- package/src/__tests__/media-visibility-policy.test.ts +0 -190
- package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
- package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
- package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
- package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
- package/src/daemon/media-visibility-policy.ts +0 -59
- package/src/tools/assets/materialize.ts +0 -248
- package/src/tools/assets/search.ts +0 -400
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
realpathSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
12
|
+
|
|
13
|
+
import { createAssistantMessage } from "../agent/message-types.js";
|
|
14
|
+
import type { Conversation } from "../daemon/conversation.js";
|
|
15
|
+
import { persistUserMessage } from "../daemon/conversation-messaging.js";
|
|
16
|
+
import {
|
|
17
|
+
addMessage,
|
|
18
|
+
getConversation,
|
|
19
|
+
provenanceFromTrustContext,
|
|
20
|
+
} from "../memory/conversation-crud.js";
|
|
21
|
+
import {
|
|
22
|
+
getConversationDirPath,
|
|
23
|
+
syncMessageToDisk,
|
|
24
|
+
} from "../memory/conversation-disk-view.js";
|
|
25
|
+
import {
|
|
26
|
+
getConversationByKey,
|
|
27
|
+
getOrCreateConversation as getOrCreateConversationMapping,
|
|
28
|
+
} from "../memory/conversation-key-store.js";
|
|
29
|
+
import { getDb, initializeDb, resetDb } from "../memory/db.js";
|
|
30
|
+
import { AssistantEventHub } from "../runtime/assistant-event-hub.js";
|
|
31
|
+
import type { AuthContext } from "../runtime/auth/types.js";
|
|
32
|
+
import { handleSendMessage } from "../runtime/routes/conversation-routes.js";
|
|
33
|
+
|
|
34
|
+
const testDir = realpathSync(
|
|
35
|
+
mkdtempSync(join(tmpdir(), "conversation-routes-disk-view-test-")),
|
|
36
|
+
);
|
|
37
|
+
const workspaceDir = join(testDir, "workspace");
|
|
38
|
+
const conversationsDir = join(workspaceDir, "conversations");
|
|
39
|
+
mkdirSync(conversationsDir, { recursive: true });
|
|
40
|
+
|
|
41
|
+
mock.module("../util/platform.js", () => ({
|
|
42
|
+
getRootDir: () => testDir,
|
|
43
|
+
getDataDir: () => join(testDir, "data"),
|
|
44
|
+
getWorkspaceDir: () => workspaceDir,
|
|
45
|
+
getConversationsDir: () => conversationsDir,
|
|
46
|
+
isMacOS: () => process.platform === "darwin",
|
|
47
|
+
isLinux: () => process.platform === "linux",
|
|
48
|
+
isWindows: () => process.platform === "win32",
|
|
49
|
+
getPidPath: () => join(testDir, "test.pid"),
|
|
50
|
+
getDbPath: () => join(testDir, "test.db"),
|
|
51
|
+
getLogPath: () => join(testDir, "test.log"),
|
|
52
|
+
ensureDataDir: () => {},
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
mock.module("../util/logger.js", () => ({
|
|
56
|
+
getLogger: () =>
|
|
57
|
+
new Proxy({} as Record<string, unknown>, {
|
|
58
|
+
get: () => () => {},
|
|
59
|
+
}),
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
mock.module("../config/loader.js", () => ({
|
|
63
|
+
getConfig: () => ({
|
|
64
|
+
ui: {},
|
|
65
|
+
model: "test",
|
|
66
|
+
provider: "test",
|
|
67
|
+
memory: { enabled: false },
|
|
68
|
+
rateLimit: { maxRequestsPerMinute: 0 },
|
|
69
|
+
secretDetection: { enabled: false },
|
|
70
|
+
contextWindow: { maxInputTokens: 200000 },
|
|
71
|
+
services: {
|
|
72
|
+
inference: {
|
|
73
|
+
mode: "your-own",
|
|
74
|
+
provider: "anthropic",
|
|
75
|
+
model: "claude-opus-4-6",
|
|
76
|
+
},
|
|
77
|
+
"image-generation": {
|
|
78
|
+
mode: "your-own",
|
|
79
|
+
provider: "gemini",
|
|
80
|
+
model: "gemini-3.1-flash-image-preview",
|
|
81
|
+
},
|
|
82
|
+
"web-search": { mode: "your-own", provider: "inference-provider-native" },
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
initializeDb();
|
|
88
|
+
|
|
89
|
+
const conversationInstances = new Map<string, Conversation>();
|
|
90
|
+
|
|
91
|
+
const authContext: AuthContext = {
|
|
92
|
+
subject: "svc_gateway:self",
|
|
93
|
+
principalType: "svc_gateway",
|
|
94
|
+
assistantId: "self",
|
|
95
|
+
scopeProfile: "gateway_service_v1",
|
|
96
|
+
scopes: new Set([
|
|
97
|
+
"chat.read",
|
|
98
|
+
"chat.write",
|
|
99
|
+
"approval.read",
|
|
100
|
+
"approval.write",
|
|
101
|
+
"settings.read",
|
|
102
|
+
"settings.write",
|
|
103
|
+
"attachments.read",
|
|
104
|
+
"attachments.write",
|
|
105
|
+
"calls.read",
|
|
106
|
+
"calls.write",
|
|
107
|
+
"feature_flags.read",
|
|
108
|
+
"feature_flags.write",
|
|
109
|
+
]),
|
|
110
|
+
policyEpoch: 1,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
function resetTables(): void {
|
|
114
|
+
const db = getDb();
|
|
115
|
+
db.run("DELETE FROM messages");
|
|
116
|
+
db.run("DELETE FROM conversations");
|
|
117
|
+
db.run("DELETE FROM conversation_keys");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function resetConversationsDir(): void {
|
|
121
|
+
rmSync(conversationsDir, { recursive: true, force: true });
|
|
122
|
+
mkdirSync(conversationsDir, { recursive: true });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function createFakeConversation(conversationId: string): Conversation {
|
|
126
|
+
const conversation = {
|
|
127
|
+
conversationId,
|
|
128
|
+
processing: false,
|
|
129
|
+
currentRequestId: undefined as string | undefined,
|
|
130
|
+
abortController: null as AbortController | null,
|
|
131
|
+
trustContext: undefined as unknown,
|
|
132
|
+
turnChannelContext: null as
|
|
133
|
+
| {
|
|
134
|
+
userMessageChannel: string;
|
|
135
|
+
assistantMessageChannel: string;
|
|
136
|
+
}
|
|
137
|
+
| null,
|
|
138
|
+
turnInterfaceContext: null as
|
|
139
|
+
| {
|
|
140
|
+
userMessageInterface: string;
|
|
141
|
+
assistantMessageInterface: string;
|
|
142
|
+
}
|
|
143
|
+
| null,
|
|
144
|
+
messages: [] as Array<unknown>,
|
|
145
|
+
hostBashProxy: undefined as unknown,
|
|
146
|
+
hostFileProxy: undefined as unknown,
|
|
147
|
+
hostCuProxy: undefined as unknown,
|
|
148
|
+
usageStats: { inputTokens: 0, outputTokens: 0, estimatedCost: 0 },
|
|
149
|
+
memoryPolicy: {
|
|
150
|
+
scopeId: "default",
|
|
151
|
+
includeDefaultFallback: false,
|
|
152
|
+
strictSideEffects: false,
|
|
153
|
+
},
|
|
154
|
+
isProcessing(this: { processing: boolean }) {
|
|
155
|
+
return this.processing;
|
|
156
|
+
},
|
|
157
|
+
setChannelCapabilities: () => {},
|
|
158
|
+
setAssistantId: () => {},
|
|
159
|
+
setTrustContext(this: { trustContext: unknown }, ctx: unknown) {
|
|
160
|
+
this.trustContext = ctx;
|
|
161
|
+
},
|
|
162
|
+
setAuthContext: () => {},
|
|
163
|
+
setCommandIntent: () => {},
|
|
164
|
+
setTurnChannelContext(
|
|
165
|
+
this: {
|
|
166
|
+
turnChannelContext: {
|
|
167
|
+
userMessageChannel: string;
|
|
168
|
+
assistantMessageChannel: string;
|
|
169
|
+
} | null;
|
|
170
|
+
},
|
|
171
|
+
ctx: { userMessageChannel: string; assistantMessageChannel: string },
|
|
172
|
+
) {
|
|
173
|
+
this.turnChannelContext = ctx;
|
|
174
|
+
},
|
|
175
|
+
getTurnChannelContext(this: {
|
|
176
|
+
turnChannelContext: {
|
|
177
|
+
userMessageChannel: string;
|
|
178
|
+
assistantMessageChannel: string;
|
|
179
|
+
} | null;
|
|
180
|
+
}) {
|
|
181
|
+
return this.turnChannelContext;
|
|
182
|
+
},
|
|
183
|
+
setTurnInterfaceContext(
|
|
184
|
+
this: {
|
|
185
|
+
turnInterfaceContext: {
|
|
186
|
+
userMessageInterface: string;
|
|
187
|
+
assistantMessageInterface: string;
|
|
188
|
+
} | null;
|
|
189
|
+
},
|
|
190
|
+
ctx: {
|
|
191
|
+
userMessageInterface: string;
|
|
192
|
+
assistantMessageInterface: string;
|
|
193
|
+
},
|
|
194
|
+
) {
|
|
195
|
+
this.turnInterfaceContext = ctx;
|
|
196
|
+
},
|
|
197
|
+
getTurnInterfaceContext(this: {
|
|
198
|
+
turnInterfaceContext: {
|
|
199
|
+
userMessageInterface: string;
|
|
200
|
+
assistantMessageInterface: string;
|
|
201
|
+
} | null;
|
|
202
|
+
}) {
|
|
203
|
+
return this.turnInterfaceContext;
|
|
204
|
+
},
|
|
205
|
+
ensureActorScopedHistory: async () => {},
|
|
206
|
+
updateClient: () => {},
|
|
207
|
+
setHostBashProxy(this: { hostBashProxy: unknown }, proxy: unknown) {
|
|
208
|
+
this.hostBashProxy = proxy;
|
|
209
|
+
},
|
|
210
|
+
setHostFileProxy(this: { hostFileProxy: unknown }, proxy: unknown) {
|
|
211
|
+
this.hostFileProxy = proxy;
|
|
212
|
+
},
|
|
213
|
+
setHostCuProxy(this: { hostCuProxy: unknown }, proxy: unknown) {
|
|
214
|
+
this.hostCuProxy = proxy;
|
|
215
|
+
},
|
|
216
|
+
addPreactivatedSkillId: () => {},
|
|
217
|
+
hasAnyPendingConfirmation: () => false,
|
|
218
|
+
hasPendingConfirmation: () => false,
|
|
219
|
+
denyAllPendingConfirmations: () => {},
|
|
220
|
+
emitConfirmationStateChanged: () => {},
|
|
221
|
+
emitActivityState: () => {},
|
|
222
|
+
enqueueMessage: () => ({ queued: true, requestId: crypto.randomUUID() }),
|
|
223
|
+
getQueueDepth: () => 0,
|
|
224
|
+
handleConfirmationResponse: () => {},
|
|
225
|
+
handleSecretResponse: () => {},
|
|
226
|
+
getMessages(this: { messages: Array<unknown> }) {
|
|
227
|
+
return this.messages as never[];
|
|
228
|
+
},
|
|
229
|
+
persistUserMessage(
|
|
230
|
+
this: Conversation,
|
|
231
|
+
content: string,
|
|
232
|
+
attachments: Array<{
|
|
233
|
+
id: string;
|
|
234
|
+
filename: string;
|
|
235
|
+
mimeType: string;
|
|
236
|
+
data: string;
|
|
237
|
+
extractedText?: string;
|
|
238
|
+
filePath?: string;
|
|
239
|
+
}>,
|
|
240
|
+
requestId?: string,
|
|
241
|
+
metadata?: Record<string, unknown>,
|
|
242
|
+
displayContent?: string,
|
|
243
|
+
): Promise<string> {
|
|
244
|
+
return persistUserMessage(
|
|
245
|
+
this as Parameters<typeof persistUserMessage>[0],
|
|
246
|
+
content,
|
|
247
|
+
attachments,
|
|
248
|
+
requestId,
|
|
249
|
+
metadata,
|
|
250
|
+
displayContent,
|
|
251
|
+
);
|
|
252
|
+
},
|
|
253
|
+
async runAgentLoop(
|
|
254
|
+
this: {
|
|
255
|
+
conversationId: string;
|
|
256
|
+
turnChannelContext: {
|
|
257
|
+
userMessageChannel: string;
|
|
258
|
+
assistantMessageChannel: string;
|
|
259
|
+
} | null;
|
|
260
|
+
turnInterfaceContext: {
|
|
261
|
+
userMessageInterface: string;
|
|
262
|
+
assistantMessageInterface: string;
|
|
263
|
+
} | null;
|
|
264
|
+
trustContext: unknown;
|
|
265
|
+
messages: Array<unknown>;
|
|
266
|
+
processing: boolean;
|
|
267
|
+
abortController: AbortController | null;
|
|
268
|
+
currentRequestId?: string;
|
|
269
|
+
},
|
|
270
|
+
_content: string,
|
|
271
|
+
_userMessageId: string,
|
|
272
|
+
onEvent: (msg: Record<string, unknown>) => void,
|
|
273
|
+
): Promise<void> {
|
|
274
|
+
const assistantText = "Synthetic assistant reply";
|
|
275
|
+
const assistantMessage = createAssistantMessage(assistantText);
|
|
276
|
+
const assistantMetadata = {
|
|
277
|
+
...provenanceFromTrustContext(this.trustContext as never),
|
|
278
|
+
...(this.turnChannelContext
|
|
279
|
+
? {
|
|
280
|
+
userMessageChannel: this.turnChannelContext.userMessageChannel,
|
|
281
|
+
assistantMessageChannel:
|
|
282
|
+
this.turnChannelContext.assistantMessageChannel,
|
|
283
|
+
}
|
|
284
|
+
: {}),
|
|
285
|
+
...(this.turnInterfaceContext
|
|
286
|
+
? {
|
|
287
|
+
userMessageInterface:
|
|
288
|
+
this.turnInterfaceContext.userMessageInterface,
|
|
289
|
+
assistantMessageInterface:
|
|
290
|
+
this.turnInterfaceContext.assistantMessageInterface,
|
|
291
|
+
}
|
|
292
|
+
: {}),
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const persistedAssistant = await addMessage(
|
|
296
|
+
this.conversationId,
|
|
297
|
+
"assistant",
|
|
298
|
+
JSON.stringify(assistantMessage.content),
|
|
299
|
+
assistantMetadata,
|
|
300
|
+
);
|
|
301
|
+
this.messages.push(assistantMessage);
|
|
302
|
+
|
|
303
|
+
const conversationRow = getConversation(this.conversationId);
|
|
304
|
+
if (conversationRow) {
|
|
305
|
+
syncMessageToDisk(
|
|
306
|
+
this.conversationId,
|
|
307
|
+
persistedAssistant.id,
|
|
308
|
+
conversationRow.createdAt,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
onEvent({
|
|
313
|
+
type: "assistant_text_delta",
|
|
314
|
+
text: assistantText,
|
|
315
|
+
conversationId: this.conversationId,
|
|
316
|
+
});
|
|
317
|
+
onEvent({
|
|
318
|
+
type: "message_complete",
|
|
319
|
+
conversationId: this.conversationId,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
this.processing = false;
|
|
323
|
+
this.abortController = null;
|
|
324
|
+
this.currentRequestId = undefined;
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
return conversation as unknown as Conversation;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function getOrCreateFakeConversation(conversationId: string): Conversation {
|
|
332
|
+
const existing = conversationInstances.get(conversationId);
|
|
333
|
+
if (existing) return existing;
|
|
334
|
+
const created = createFakeConversation(conversationId);
|
|
335
|
+
conversationInstances.set(conversationId, created);
|
|
336
|
+
return created;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function waitFor<T>(
|
|
340
|
+
getter: () => T | undefined,
|
|
341
|
+
timeoutMs = 3000,
|
|
342
|
+
): Promise<T> {
|
|
343
|
+
const deadline = Date.now() + timeoutMs;
|
|
344
|
+
while (Date.now() < deadline) {
|
|
345
|
+
const value = getter();
|
|
346
|
+
if (value !== undefined) return value;
|
|
347
|
+
await Bun.sleep(20);
|
|
348
|
+
}
|
|
349
|
+
throw new Error("Timed out waiting for expected disk-view output");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
beforeEach(() => {
|
|
353
|
+
resetTables();
|
|
354
|
+
resetConversationsDir();
|
|
355
|
+
conversationInstances.clear();
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
afterAll(() => {
|
|
359
|
+
resetDb();
|
|
360
|
+
try {
|
|
361
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
362
|
+
} catch {
|
|
363
|
+
/* best effort */
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe("conversationKey send path disk-view regression", () => {
|
|
368
|
+
test("first send on a fresh conversationKey creates disk-view dir and writes user+assistant records", async () => {
|
|
369
|
+
const conversationKey = `fresh-conv-key-${crypto.randomUUID()}`;
|
|
370
|
+
const content = "Please persist this first turn.";
|
|
371
|
+
|
|
372
|
+
const response = await handleSendMessage(
|
|
373
|
+
new Request("http://localhost/v1/messages", {
|
|
374
|
+
method: "POST",
|
|
375
|
+
headers: { "Content-Type": "application/json" },
|
|
376
|
+
body: JSON.stringify({
|
|
377
|
+
conversationKey,
|
|
378
|
+
content,
|
|
379
|
+
sourceChannel: "vellum",
|
|
380
|
+
interface: "macos",
|
|
381
|
+
}),
|
|
382
|
+
}),
|
|
383
|
+
{
|
|
384
|
+
sendMessageDeps: {
|
|
385
|
+
getOrCreateConversation: async (conversationId: string) =>
|
|
386
|
+
getOrCreateFakeConversation(conversationId),
|
|
387
|
+
assistantEventHub: new AssistantEventHub(),
|
|
388
|
+
resolveAttachments: () => [],
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
authContext,
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
expect(response.status).toBe(202);
|
|
395
|
+
const body = (await response.json()) as {
|
|
396
|
+
accepted: boolean;
|
|
397
|
+
conversationId: string;
|
|
398
|
+
messageId: string;
|
|
399
|
+
};
|
|
400
|
+
expect(body.accepted).toBe(true);
|
|
401
|
+
expect(body.conversationId).toBeDefined();
|
|
402
|
+
expect(body.messageId).toBeDefined();
|
|
403
|
+
|
|
404
|
+
// Verify the real key store mapping is reused after the first send.
|
|
405
|
+
const mapping = getOrCreateConversationMapping(conversationKey);
|
|
406
|
+
expect(mapping.created).toBe(false);
|
|
407
|
+
expect(mapping.conversationId).toBe(body.conversationId);
|
|
408
|
+
expect(getConversationByKey(conversationKey)?.conversationId).toBe(
|
|
409
|
+
body.conversationId,
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
const conversationRow = getConversation(body.conversationId);
|
|
413
|
+
expect(conversationRow).not.toBeNull();
|
|
414
|
+
const conversationDir = getConversationDirPath(
|
|
415
|
+
body.conversationId,
|
|
416
|
+
conversationRow!.createdAt,
|
|
417
|
+
);
|
|
418
|
+
const metaPath = join(conversationDir, "meta.json");
|
|
419
|
+
const messagesPath = join(conversationDir, "messages.jsonl");
|
|
420
|
+
|
|
421
|
+
expect(existsSync(conversationDir)).toBe(true);
|
|
422
|
+
expect(existsSync(metaPath)).toBe(true);
|
|
423
|
+
|
|
424
|
+
const lines = await waitFor(() => {
|
|
425
|
+
if (!existsSync(messagesPath)) return undefined;
|
|
426
|
+
const raw = readFileSync(messagesPath, "utf-8").trim();
|
|
427
|
+
if (!raw) return undefined;
|
|
428
|
+
const parsed = raw
|
|
429
|
+
.split("\n")
|
|
430
|
+
.map((line) => JSON.parse(line) as { role: string; content?: string });
|
|
431
|
+
return parsed.length >= 2 ? parsed : undefined;
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
expect(lines[0]?.role).toBe("user");
|
|
435
|
+
expect(lines[0]?.content).toBe(content);
|
|
436
|
+
expect(lines[1]?.role).toBe("assistant");
|
|
437
|
+
expect(lines[1]?.content).toBe("Synthetic assistant reply");
|
|
438
|
+
});
|
|
439
|
+
});
|
|
@@ -216,7 +216,7 @@ describe("handleSendMessage canonical guardian reply interception", () => {
|
|
|
216
216
|
expect(runAgentLoop).toHaveBeenCalledTimes(0);
|
|
217
217
|
});
|
|
218
218
|
|
|
219
|
-
test("passes
|
|
219
|
+
test("passes empty pendingRequestIds array when no canonical hints are found", async () => {
|
|
220
220
|
listPendingByDestinationMock.mockReturnValue([]);
|
|
221
221
|
listCanonicalMock.mockReturnValue([]);
|
|
222
222
|
routeGuardianReplyMock.mockResolvedValue({
|
|
@@ -279,7 +279,7 @@ describe("handleSendMessage canonical guardian reply interception", () => {
|
|
|
279
279
|
expect(routeGuardianReplyMock).toHaveBeenCalledTimes(1);
|
|
280
280
|
const routerCall = (routeGuardianReplyMock as any).mock
|
|
281
281
|
.calls[0][0] as Record<string, unknown>;
|
|
282
|
-
expect(routerCall.pendingRequestIds).
|
|
282
|
+
expect(routerCall.pendingRequestIds).toEqual([]);
|
|
283
283
|
expect(persistUserMessage).toHaveBeenCalledTimes(1);
|
|
284
284
|
expect(runAgentLoop).toHaveBeenCalledTimes(1);
|
|
285
285
|
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Tests for slash command interception in the POST /v1/messages handler.
|
|
3
3
|
*
|
|
4
4
|
* Validates that:
|
|
5
|
-
* - Built-in slash commands (/status, /
|
|
5
|
+
* - Built-in slash commands (/status, /models, /commands) are intercepted and
|
|
6
6
|
* do NOT trigger the agent loop.
|
|
7
7
|
* - Regular messages pass through to the agent loop unchanged.
|
|
8
8
|
*/
|
|
@@ -122,12 +122,7 @@ mock.module("../daemon/conversation-process.js", () => ({
|
|
|
122
122
|
configuredProviders: ["anthropic", "ollama"],
|
|
123
123
|
}),
|
|
124
124
|
isModelSlashCommand: (content: string) => {
|
|
125
|
-
|
|
126
|
-
return (
|
|
127
|
-
trimmed === "/model" ||
|
|
128
|
-
trimmed === "/models" ||
|
|
129
|
-
trimmed.startsWith("/model ")
|
|
130
|
-
);
|
|
125
|
+
return content.trim() === "/models";
|
|
131
126
|
},
|
|
132
127
|
}));
|
|
133
128
|
|
|
@@ -1065,9 +1065,24 @@ describe("buildTurnContextBlock (channel-only)", () => {
|
|
|
1065
1065
|
},
|
|
1066
1066
|
undefined,
|
|
1067
1067
|
);
|
|
1068
|
-
expect(block).
|
|
1069
|
-
|
|
1068
|
+
expect(block).toContain("<turn_context>");
|
|
1069
|
+
expect(block).toContain("channel: telegram");
|
|
1070
|
+
expect(block).toContain("response_discretion:");
|
|
1071
|
+
expect(block).toContain("</turn_context>");
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
test("omits response_discretion for vellum channel", () => {
|
|
1075
|
+
const block = buildTurnContextBlock(
|
|
1076
|
+
{
|
|
1077
|
+
turnContext: {
|
|
1078
|
+
userMessageChannel: "vellum",
|
|
1079
|
+
assistantMessageChannel: "vellum",
|
|
1080
|
+
},
|
|
1081
|
+
conversationOriginChannel: "vellum",
|
|
1082
|
+
},
|
|
1083
|
+
undefined,
|
|
1070
1084
|
);
|
|
1085
|
+
expect(block).not.toContain("response_discretion:");
|
|
1071
1086
|
});
|
|
1072
1087
|
|
|
1073
1088
|
test('uses "unknown" when conversationOriginChannel is null', () => {
|
|
@@ -1779,14 +1779,9 @@ describe("bundled skill: claude-code", () => {
|
|
|
1779
1779
|
|
|
1780
1780
|
const APP_BUILDER_TOOL_NAMES = [
|
|
1781
1781
|
"app_create",
|
|
1782
|
-
"app_list",
|
|
1783
|
-
"app_query",
|
|
1784
|
-
"app_update",
|
|
1785
1782
|
"app_delete",
|
|
1786
|
-
"
|
|
1787
|
-
"
|
|
1788
|
-
"app_file_edit",
|
|
1789
|
-
"app_file_write",
|
|
1783
|
+
"app_generate_icon",
|
|
1784
|
+
"app_refresh",
|
|
1790
1785
|
] as const;
|
|
1791
1786
|
|
|
1792
1787
|
describe("bundled skill: app-builder", () => {
|
|
@@ -1803,7 +1798,7 @@ describe("bundled skill: app-builder", () => {
|
|
|
1803
1798
|
sessionState = new Map<string, string>();
|
|
1804
1799
|
});
|
|
1805
1800
|
|
|
1806
|
-
test("app-builder skill activation registers all
|
|
1801
|
+
test("app-builder skill activation registers all 4 canonical non-proxy tools in allowedToolNames", () => {
|
|
1807
1802
|
mockCatalog = [
|
|
1808
1803
|
makeSkill("app-builder", "/path/to/bundled-skills/app-builder"),
|
|
1809
1804
|
];
|
|
@@ -1862,7 +1857,7 @@ describe("bundled skill: app-builder", () => {
|
|
|
1862
1857
|
|
|
1863
1858
|
const tools = mockRegisteredTools.get("app-builder");
|
|
1864
1859
|
expect(tools).toBeDefined();
|
|
1865
|
-
expect(tools!.length).toBe(
|
|
1860
|
+
expect(tools!.length).toBe(4);
|
|
1866
1861
|
|
|
1867
1862
|
// All tools should have skill origin metadata
|
|
1868
1863
|
for (const tool of tools!) {
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
resolveSlash,
|
|
5
|
+
type SlashContext,
|
|
6
|
+
} from "../daemon/conversation-slash.js";
|
|
7
|
+
|
|
8
|
+
function makeSlashContext(
|
|
9
|
+
overrides: Partial<SlashContext> = {},
|
|
10
|
+
): SlashContext {
|
|
11
|
+
return {
|
|
12
|
+
messageCount: 4,
|
|
13
|
+
inputTokens: 1024,
|
|
14
|
+
outputTokens: 256,
|
|
15
|
+
maxInputTokens: 200000,
|
|
16
|
+
model: "claude-opus-4-6",
|
|
17
|
+
provider: "anthropic",
|
|
18
|
+
estimatedCost: 0.03,
|
|
19
|
+
...overrides,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function resolveCommandsLines(context?: SlashContext): Promise<string[]> {
|
|
24
|
+
const result = await resolveSlash("/commands", context);
|
|
25
|
+
expect(result.kind).toBe("unknown");
|
|
26
|
+
if (result.kind !== "unknown") {
|
|
27
|
+
throw new Error("Expected /commands to resolve to kind=unknown");
|
|
28
|
+
}
|
|
29
|
+
return result.message.split("\n");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("resolveSlash /commands interface-aware help", () => {
|
|
33
|
+
test("renders desktop command help for macOS", async () => {
|
|
34
|
+
const lines = await resolveCommandsLines(
|
|
35
|
+
makeSlashContext({ userMessageInterface: "macos" }),
|
|
36
|
+
);
|
|
37
|
+
expect(lines).toEqual([
|
|
38
|
+
"/commands — List all available commands",
|
|
39
|
+
"/models — List all available models",
|
|
40
|
+
"/status — Show conversation status and context usage",
|
|
41
|
+
"/btw — Ask a side question while the assistant is working",
|
|
42
|
+
"/fork — Fork the current conversation into a new branch",
|
|
43
|
+
"/pair — Generate pairing info for connecting a mobile device",
|
|
44
|
+
]);
|
|
45
|
+
expect(lines).not.toContain(
|
|
46
|
+
"/model — Switch the active model",
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("renders iOS command help with /fork but without /pair", async () => {
|
|
51
|
+
const lines = await resolveCommandsLines(
|
|
52
|
+
makeSlashContext({ userMessageInterface: "ios" }),
|
|
53
|
+
);
|
|
54
|
+
expect(lines).toEqual([
|
|
55
|
+
"/commands — List all available commands",
|
|
56
|
+
"/models — List all available models",
|
|
57
|
+
"/status — Show conversation status and context usage",
|
|
58
|
+
"/btw — Ask a side question while the assistant is working",
|
|
59
|
+
"/fork — Fork the current conversation into a new branch",
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("renders explicit cli command help without /pair", async () => {
|
|
64
|
+
const lines = await resolveCommandsLines(
|
|
65
|
+
makeSlashContext({ userMessageInterface: "cli" }),
|
|
66
|
+
);
|
|
67
|
+
expect(lines).toEqual([
|
|
68
|
+
"/commands — List all available commands",
|
|
69
|
+
"/models — List all available models",
|
|
70
|
+
"/status — Show conversation status and context usage",
|
|
71
|
+
"/btw — Ask a side question while the assistant is working",
|
|
72
|
+
]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("keeps legacy fallback help when no interface is provided", async () => {
|
|
76
|
+
const lines = await resolveCommandsLines(makeSlashContext());
|
|
77
|
+
expect(lines).toEqual([
|
|
78
|
+
"/commands — List all available commands",
|
|
79
|
+
"/models — List all available models",
|
|
80
|
+
"/pair — Generate pairing info for connecting a mobile device",
|
|
81
|
+
"/status — Show conversation status and context usage",
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("keeps context-free fallback without /status", async () => {
|
|
86
|
+
const lines = await resolveCommandsLines();
|
|
87
|
+
expect(lines).toEqual([
|
|
88
|
+
"/commands — List all available commands",
|
|
89
|
+
"/models — List all available models",
|
|
90
|
+
"/pair — Generate pairing info for connecting a mobile device",
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("resolveSlash command contract", () => {
|
|
96
|
+
test("keeps unsupported slash forms as passthrough", async () => {
|
|
97
|
+
const slashForms = [
|
|
98
|
+
"/commands foo",
|
|
99
|
+
"/models foo",
|
|
100
|
+
"/status foo",
|
|
101
|
+
"/pair foo",
|
|
102
|
+
"/btw",
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
for (const input of slashForms) {
|
|
106
|
+
const result = await resolveSlash(
|
|
107
|
+
input,
|
|
108
|
+
makeSlashContext({ userMessageInterface: "macos" }),
|
|
109
|
+
);
|
|
110
|
+
expect(result).toEqual({ kind: "passthrough", content: input });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("rejects /pair on iOS interfaces", async () => {
|
|
115
|
+
const result = await resolveSlash(
|
|
116
|
+
"/pair",
|
|
117
|
+
makeSlashContext({ userMessageInterface: "ios" }),
|
|
118
|
+
);
|
|
119
|
+
expect(result.kind).toBe("unknown");
|
|
120
|
+
if (result.kind !== "unknown") {
|
|
121
|
+
throw new Error("Expected /pair on iOS to resolve to kind=unknown");
|
|
122
|
+
}
|
|
123
|
+
expect(result.message).toContain("only available in the macOS desktop app");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("keeps /pair rejected for explicit non-macOS interfaces", async () => {
|
|
127
|
+
const result = await resolveSlash(
|
|
128
|
+
"/pair",
|
|
129
|
+
makeSlashContext({ userMessageInterface: "cli" }),
|
|
130
|
+
);
|
|
131
|
+
expect(result.kind).toBe("unknown");
|
|
132
|
+
if (result.kind !== "unknown") {
|
|
133
|
+
throw new Error("Expected /pair on cli to resolve to kind=unknown");
|
|
134
|
+
}
|
|
135
|
+
expect(result.message).toContain("only available in the macOS desktop app");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("keeps /pair handling enabled on macOS interfaces", async () => {
|
|
139
|
+
const result = await resolveSlash(
|
|
140
|
+
"/pair",
|
|
141
|
+
makeSlashContext({ userMessageInterface: "macos" }),
|
|
142
|
+
);
|
|
143
|
+
expect(result.kind).toBe("unknown");
|
|
144
|
+
if (result.kind !== "unknown") {
|
|
145
|
+
throw new Error("Expected /pair on macOS to resolve to kind=unknown");
|
|
146
|
+
}
|
|
147
|
+
expect(result.message).toContain("Pairing is not available");
|
|
148
|
+
});
|
|
149
|
+
});
|