@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,426 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Mock getDataDir to use a temp directory
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
let testDataDir: string;
|
|
17
|
+
|
|
18
|
+
mock.module("../util/platform.js", () => ({
|
|
19
|
+
getDataDir: () => testDataDir,
|
|
20
|
+
getProjectDir: () => testDataDir,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// Re-import app-store after mocking so it uses our temp dir
|
|
24
|
+
const {
|
|
25
|
+
slugify,
|
|
26
|
+
generateAppDirName,
|
|
27
|
+
validateDirName,
|
|
28
|
+
resolveAppDir,
|
|
29
|
+
getAppDirPath,
|
|
30
|
+
getAppsDir,
|
|
31
|
+
createApp,
|
|
32
|
+
getApp,
|
|
33
|
+
updateApp,
|
|
34
|
+
deleteApp,
|
|
35
|
+
} = await import("../memory/app-store.js");
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Helpers
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
function freshTempDir(): string {
|
|
42
|
+
return join(
|
|
43
|
+
tmpdir(),
|
|
44
|
+
`vellum-app-dir-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function makeAppParams(name: string) {
|
|
49
|
+
return {
|
|
50
|
+
name,
|
|
51
|
+
schemaJson: "{}",
|
|
52
|
+
htmlDefinition: "<h1>Hello</h1>",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Setup / teardown
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
testDataDir = freshTempDir();
|
|
62
|
+
mkdirSync(join(testDataDir, "apps"), { recursive: true });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
if (existsSync(testDataDir)) {
|
|
67
|
+
rmSync(testDataDir, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// slugify()
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
describe("slugify()", () => {
|
|
76
|
+
test("normal names", () => {
|
|
77
|
+
expect(slugify("My Cool App")).toBe("my-cool-app");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("special characters are replaced with hyphens", () => {
|
|
81
|
+
expect(slugify("hello@world!#$%")).toBe("hello-world");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("unicode characters are replaced", () => {
|
|
85
|
+
expect(slugify("café résumé")).toBe("caf-r-sum");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("emoji-only names produce fallback slug", () => {
|
|
89
|
+
const result = slugify("🚀🎉");
|
|
90
|
+
expect(result).toMatch(/^app-[a-f0-9]{8}$/);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("empty string produces fallback slug", () => {
|
|
94
|
+
const result = slugify("");
|
|
95
|
+
expect(result).toMatch(/^app-[a-f0-9]{8}$/);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("very long names are truncated to 60 chars", () => {
|
|
99
|
+
const longName = "a".repeat(100);
|
|
100
|
+
const result = slugify(longName);
|
|
101
|
+
expect(result.length).toBeLessThanOrEqual(60);
|
|
102
|
+
expect(result).toBe("a".repeat(60));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("truncation removes trailing hyphens", () => {
|
|
106
|
+
// Create a name where position 60 lands on a hyphen sequence
|
|
107
|
+
const name = "a".repeat(58) + "--bbb";
|
|
108
|
+
const result = slugify(name);
|
|
109
|
+
expect(result.length).toBeLessThanOrEqual(60);
|
|
110
|
+
expect(result).not.toMatch(/-$/);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("names with only hyphens produce fallback slug", () => {
|
|
114
|
+
const result = slugify("---");
|
|
115
|
+
expect(result).toMatch(/^app-[a-f0-9]{8}$/);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("leading and trailing hyphens are stripped", () => {
|
|
119
|
+
expect(slugify("-hello-world-")).toBe("hello-world");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("consecutive hyphens are collapsed", () => {
|
|
123
|
+
expect(slugify("hello---world")).toBe("hello-world");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// generateAppDirName()
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
describe("generateAppDirName()", () => {
|
|
132
|
+
test("returns base slug when no collision", () => {
|
|
133
|
+
const result = generateAppDirName("My App", new Set());
|
|
134
|
+
expect(result).toBe("my-app");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("appends -2 on first collision", () => {
|
|
138
|
+
const result = generateAppDirName("My App", new Set(["my-app"]));
|
|
139
|
+
expect(result).toBe("my-app-2");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("escalates numeric suffix on multiple collisions", () => {
|
|
143
|
+
const existing = new Set(["my-app", "my-app-2", "my-app-3"]);
|
|
144
|
+
const result = generateAppDirName("My App", existing);
|
|
145
|
+
expect(result).toBe("my-app-4");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("collision with truncated names still deduplicates", () => {
|
|
149
|
+
const longName = "a".repeat(100);
|
|
150
|
+
const base = slugify(longName);
|
|
151
|
+
const existing = new Set([base]);
|
|
152
|
+
const result = generateAppDirName(longName, existing);
|
|
153
|
+
expect(result).toBe(`${base}-2`);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// createApp() — directory named after slug
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
describe("createApp()", () => {
|
|
162
|
+
test("directory is named after slug, not UUID", () => {
|
|
163
|
+
const app = createApp(makeAppParams("My Test App"));
|
|
164
|
+
const appsDir = getAppsDir();
|
|
165
|
+
|
|
166
|
+
// dirName should be the slug
|
|
167
|
+
expect(app.dirName).toBe("my-test-app");
|
|
168
|
+
|
|
169
|
+
// JSON file should be named after slug
|
|
170
|
+
expect(existsSync(join(appsDir, "my-test-app.json"))).toBe(true);
|
|
171
|
+
|
|
172
|
+
// Directory should be named after slug
|
|
173
|
+
expect(existsSync(join(appsDir, "my-test-app"))).toBe(true);
|
|
174
|
+
|
|
175
|
+
// UUID-named files should NOT exist
|
|
176
|
+
expect(existsSync(join(appsDir, `${app.id}.json`))).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("dirName is persisted in JSON", () => {
|
|
180
|
+
const app = createApp(makeAppParams("Slug Test"));
|
|
181
|
+
const appsDir = getAppsDir();
|
|
182
|
+
const jsonPath = join(appsDir, "slug-test.json");
|
|
183
|
+
const persisted = JSON.parse(readFileSync(jsonPath, "utf-8"));
|
|
184
|
+
expect(persisted.dirName).toBe("slug-test");
|
|
185
|
+
expect(persisted.id).toBe(app.id);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("index.html is in the slugified directory", () => {
|
|
189
|
+
createApp(makeAppParams("Html App"));
|
|
190
|
+
const appsDir = getAppsDir();
|
|
191
|
+
const indexPath = join(appsDir, "html-app", "index.html");
|
|
192
|
+
expect(existsSync(indexPath)).toBe(true);
|
|
193
|
+
expect(readFileSync(indexPath, "utf-8")).toBe("<h1>Hello</h1>");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("deduplicates dirNames across multiple creates", () => {
|
|
197
|
+
const app1 = createApp(makeAppParams("Duplicate"));
|
|
198
|
+
const app2 = createApp(makeAppParams("Duplicate"));
|
|
199
|
+
expect(app1.dirName).toBe("duplicate");
|
|
200
|
+
expect(app2.dirName).toBe("duplicate-2");
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// createApp() + updateApp() — frozen dirName invariant
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
describe("frozen dirName invariant", () => {
|
|
209
|
+
test("renaming an app does NOT change its directory name", () => {
|
|
210
|
+
const app = createApp(makeAppParams("Original Name"));
|
|
211
|
+
const appsDir = getAppsDir();
|
|
212
|
+
|
|
213
|
+
expect(app.dirName).toBe("original-name");
|
|
214
|
+
expect(existsSync(join(appsDir, "original-name.json"))).toBe(true);
|
|
215
|
+
|
|
216
|
+
// Rename the app
|
|
217
|
+
const updated = updateApp(app.id, { name: "New Name" });
|
|
218
|
+
expect(updated.name).toBe("New Name");
|
|
219
|
+
|
|
220
|
+
// Directory and files should still be at original slug
|
|
221
|
+
expect(existsSync(join(appsDir, "original-name.json"))).toBe(true);
|
|
222
|
+
expect(existsSync(join(appsDir, "original-name"))).toBe(true);
|
|
223
|
+
|
|
224
|
+
// New name slug should NOT exist as files
|
|
225
|
+
expect(existsSync(join(appsDir, "new-name.json"))).toBe(false);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// getApp() — lookup by UUID with slugified dirs
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
describe("getApp()", () => {
|
|
234
|
+
test("lookup by UUID works with slugified dirs", () => {
|
|
235
|
+
const created = createApp(makeAppParams("Lookup App"));
|
|
236
|
+
const fetched = getApp(created.id);
|
|
237
|
+
expect(fetched).not.toBeNull();
|
|
238
|
+
expect(fetched!.id).toBe(created.id);
|
|
239
|
+
expect(fetched!.name).toBe("Lookup App");
|
|
240
|
+
expect(fetched!.htmlDefinition).toBe("<h1>Hello</h1>");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("backward compat: works when JSON has no dirName (uses id as fallback)", () => {
|
|
244
|
+
const appsDir = getAppsDir();
|
|
245
|
+
const fakeId = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
|
|
246
|
+
|
|
247
|
+
// Write a JSON file with no dirName, using the UUID as the filename
|
|
248
|
+
const appData = {
|
|
249
|
+
id: fakeId,
|
|
250
|
+
name: "Legacy App",
|
|
251
|
+
schemaJson: "{}",
|
|
252
|
+
createdAt: Date.now(),
|
|
253
|
+
updatedAt: Date.now(),
|
|
254
|
+
};
|
|
255
|
+
writeFileSync(
|
|
256
|
+
join(appsDir, `${fakeId}.json`),
|
|
257
|
+
JSON.stringify(appData, null, 2),
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// Create directory with index.html
|
|
261
|
+
const appDir = join(appsDir, fakeId);
|
|
262
|
+
mkdirSync(appDir, { recursive: true });
|
|
263
|
+
writeFileSync(join(appDir, "index.html"), "<p>legacy</p>", "utf-8");
|
|
264
|
+
|
|
265
|
+
const fetched = getApp(fakeId);
|
|
266
|
+
expect(fetched).not.toBeNull();
|
|
267
|
+
expect(fetched!.id).toBe(fakeId);
|
|
268
|
+
expect(fetched!.name).toBe("Legacy App");
|
|
269
|
+
expect(fetched!.htmlDefinition).toBe("<p>legacy</p>");
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// deleteApp() — cleans up slugified files
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
describe("deleteApp()", () => {
|
|
278
|
+
test("cleans up slugified directory, JSON, and preview files", () => {
|
|
279
|
+
const app = createApp({
|
|
280
|
+
...makeAppParams("Delete Me"),
|
|
281
|
+
preview: "base64-preview-data",
|
|
282
|
+
});
|
|
283
|
+
const appsDir = getAppsDir();
|
|
284
|
+
|
|
285
|
+
// Verify files exist before deletion
|
|
286
|
+
expect(existsSync(join(appsDir, "delete-me.json"))).toBe(true);
|
|
287
|
+
expect(existsSync(join(appsDir, "delete-me"))).toBe(true);
|
|
288
|
+
expect(existsSync(join(appsDir, "delete-me.preview"))).toBe(true);
|
|
289
|
+
|
|
290
|
+
deleteApp(app.id);
|
|
291
|
+
|
|
292
|
+
// All files should be gone
|
|
293
|
+
expect(existsSync(join(appsDir, "delete-me.json"))).toBe(false);
|
|
294
|
+
expect(existsSync(join(appsDir, "delete-me"))).toBe(false);
|
|
295
|
+
expect(existsSync(join(appsDir, "delete-me.preview"))).toBe(false);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// resolveAppDir() — validation rejects malicious dirName values
|
|
301
|
+
// When a JSON file has an invalid dirName, resolveAppDir defensively falls
|
|
302
|
+
// back to using the app ID instead of the malicious dirName. The
|
|
303
|
+
// validateDirName() call inside the try/catch causes the invalid entry to
|
|
304
|
+
// be skipped, and the function returns the safe fallback.
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
describe("resolveAppDir() validation", () => {
|
|
308
|
+
test("falls back to id when dirName contains path traversal (..)", () => {
|
|
309
|
+
const appsDir = getAppsDir();
|
|
310
|
+
const fakeId = "bbbbbbbb-cccc-dddd-eeee-ffffffffffff";
|
|
311
|
+
writeFileSync(
|
|
312
|
+
join(appsDir, `${fakeId}.json`),
|
|
313
|
+
JSON.stringify({ id: fakeId, name: "Evil", dirName: "../etc" }),
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// Should NOT use the malicious dirName — falls back to id
|
|
317
|
+
const result = resolveAppDir(fakeId);
|
|
318
|
+
expect(result.dirName).toBe(fakeId);
|
|
319
|
+
expect(result.appDir).toBe(join(appsDir, fakeId));
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("falls back to id when dirName contains forward slash", () => {
|
|
323
|
+
const appsDir = getAppsDir();
|
|
324
|
+
const fakeId = "cccccccc-dddd-eeee-ffff-aaaaaaaaaaaa";
|
|
325
|
+
writeFileSync(
|
|
326
|
+
join(appsDir, `${fakeId}.json`),
|
|
327
|
+
JSON.stringify({ id: fakeId, name: "Evil", dirName: "foo/bar" }),
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
const result = resolveAppDir(fakeId);
|
|
331
|
+
expect(result.dirName).toBe(fakeId);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("falls back to id when dirName contains backslash", () => {
|
|
335
|
+
const appsDir = getAppsDir();
|
|
336
|
+
const fakeId = "dddddddd-eeee-ffff-aaaa-bbbbbbbbbbbb";
|
|
337
|
+
writeFileSync(
|
|
338
|
+
join(appsDir, `${fakeId}.json`),
|
|
339
|
+
JSON.stringify({ id: fakeId, name: "Evil", dirName: "foo\\bar" }),
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
const result = resolveAppDir(fakeId);
|
|
343
|
+
expect(result.dirName).toBe(fakeId);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("falls back to id when dirName is empty string", () => {
|
|
347
|
+
const appsDir = getAppsDir();
|
|
348
|
+
const fakeId = "eeeeeeee-ffff-aaaa-bbbb-cccccccccccc";
|
|
349
|
+
writeFileSync(
|
|
350
|
+
join(appsDir, `${fakeId}.json`),
|
|
351
|
+
JSON.stringify({ id: fakeId, name: "Evil", dirName: "" }),
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
// Empty string is falsy, so `parsed.dirName || id` falls back to the app ID.
|
|
355
|
+
// This prevents appDir from resolving to the apps root directory.
|
|
356
|
+
const result = resolveAppDir(fakeId);
|
|
357
|
+
expect(result.dirName).toBe(fakeId);
|
|
358
|
+
expect(result.appDir).toBe(join(appsDir, fakeId));
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
// validateDirName() — direct unit tests
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
|
|
366
|
+
describe("validateDirName()", () => {
|
|
367
|
+
test("accepts valid slug names", () => {
|
|
368
|
+
expect(() => validateDirName("my-cool-app")).not.toThrow();
|
|
369
|
+
expect(() => validateDirName("app-123")).not.toThrow();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("rejects git pathspec metacharacters", () => {
|
|
373
|
+
expect(() => validateDirName("app*")).toThrow("git pathspec");
|
|
374
|
+
expect(() => validateDirName("app?")).toThrow("git pathspec");
|
|
375
|
+
expect(() => validateDirName("app[0]")).toThrow("git pathspec");
|
|
376
|
+
expect(() => validateDirName("app:foo")).toThrow("git pathspec");
|
|
377
|
+
expect(() => validateDirName("app(1)")).toThrow("git pathspec");
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("rejects path traversal", () => {
|
|
381
|
+
expect(() => validateDirName("..")).toThrow("Invalid dirName");
|
|
382
|
+
expect(() => validateDirName("foo/bar")).toThrow("Invalid dirName");
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// ---------------------------------------------------------------------------
|
|
387
|
+
// getAppDirPath() — returns correct path
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
describe("getAppDirPath()", () => {
|
|
391
|
+
test("returns correct path for slugified apps", () => {
|
|
392
|
+
const app = createApp(makeAppParams("Path Test"));
|
|
393
|
+
const appsDir = getAppsDir();
|
|
394
|
+
const result = getAppDirPath(app.id);
|
|
395
|
+
expect(result).toBe(join(appsDir, "path-test"));
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("returns correct path for legacy apps (no dirName)", () => {
|
|
399
|
+
const appsDir = getAppsDir();
|
|
400
|
+
const fakeId = "11111111-2222-3333-4444-555555555555";
|
|
401
|
+
// Write a legacy JSON with no dirName
|
|
402
|
+
writeFileSync(
|
|
403
|
+
join(appsDir, `${fakeId}.json`),
|
|
404
|
+
JSON.stringify({ id: fakeId, name: "Legacy" }),
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
const result = getAppDirPath(fakeId);
|
|
408
|
+
expect(result).toBe(join(appsDir, fakeId));
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// ---------------------------------------------------------------------------
|
|
413
|
+
// Guard test: no file outside app-store.ts constructs getAppsDir() + appId
|
|
414
|
+
// (Note: the primary guard test is in app-dir-path-guard.test.ts; this is
|
|
415
|
+
// a complementary check that we can import and use the guard-relevant
|
|
416
|
+
// functions without issues.)
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
|
|
419
|
+
describe("guard: getAppsDir + appId path construction", () => {
|
|
420
|
+
test("app-dir-path-guard.test.ts exists and covers this concern", () => {
|
|
421
|
+
// This is a meta-test to ensure the guard test file is present
|
|
422
|
+
expect(existsSync(join(__dirname, "app-dir-path-guard.test.ts"))).toBe(
|
|
423
|
+
true,
|
|
424
|
+
);
|
|
425
|
+
});
|
|
426
|
+
});
|