@vellumai/assistant 0.8.4 → 0.8.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/ARCHITECTURE.md +2 -2
- package/docs/browser-use-architecture-phase2.md +1 -1
- package/knip.json +2 -1
- package/openapi.yaml +809 -11
- package/package.json +1 -1
- package/src/__tests__/anthropic-provider.test.ts +34 -37
- package/src/__tests__/assistant-event-hub-self-exclusion.test.ts +293 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +3 -3
- package/src/__tests__/audit-log-rotation.test.ts +70 -16
- package/src/__tests__/background-workers-disk-pressure.test.ts +3 -3
- package/src/__tests__/btw-routes.test.ts +2 -3
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/cancel-resolves-conversation-key.test.ts +1 -1
- package/src/__tests__/channel-guardian.test.ts +3 -3
- package/src/__tests__/checker.test.ts +6 -15
- package/src/__tests__/compaction-events.test.ts +1 -0
- package/src/__tests__/compactor-call-site-logging.test.ts +214 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +5 -11
- package/src/__tests__/computer-use-tools.test.ts +2 -4
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +197 -2
- package/src/__tests__/conversation-agent-loop.test.ts +163 -122
- package/src/__tests__/conversation-app-control-instantiation.test.ts +2 -5
- package/src/__tests__/conversation-clear-safety.test.ts +25 -25
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +1 -1
- package/src/__tests__/conversation-disk-view-integration.test.ts +2 -2
- package/src/__tests__/conversation-error.test.ts +31 -0
- package/src/__tests__/conversation-fork-crud.test.ts +178 -15
- package/src/__tests__/conversation-lifecycle.test.ts +52 -11
- package/src/__tests__/{conversation-load-cleaned-at.test.ts → conversation-load-history-stripped.test.ts} +13 -13
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -0
- package/src/__tests__/conversation-routes-disk-view.test.ts +109 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +35 -0
- package/src/__tests__/conversation-skill-tools.test.ts +2 -5
- package/src/__tests__/conversation-store.test.ts +1 -1
- package/src/__tests__/conversation-sync-tags.test.ts +99 -32
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/credential-execution-feature-gates.test.ts +9 -7
- package/src/__tests__/credential-execution-tools.test.ts +6 -6
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/credential-vault-unit.test.ts +2 -2
- package/src/__tests__/dynamic-page-surface.test.ts +2 -2
- package/src/__tests__/email-html-renderer.test.ts +12 -0
- package/src/__tests__/gateway-flag-listener.test.ts +237 -0
- package/src/__tests__/gemini-provider.test.ts +78 -0
- package/src/__tests__/guardian-dispatch.test.ts +0 -1
- package/src/__tests__/guardian-outbound-http.test.ts +7 -5
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -0
- package/src/__tests__/heartbeat-service.test.ts +4 -0
- package/src/__tests__/host-shell-tool.test.ts +1 -1
- package/src/__tests__/init-feature-flag-overrides.test.ts +5 -6
- package/src/__tests__/list-messages-tool-merge.test.ts +70 -11
- package/src/__tests__/llm-request-log-call-site.test.ts +136 -0
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +26 -0
- package/src/__tests__/llm-resolver.test.ts +77 -9
- package/src/__tests__/llm-usage-store.test.ts +66 -0
- package/src/__tests__/logger.test.ts +89 -0
- package/src/__tests__/mcp-abort-signal.test.ts +2 -2
- package/src/__tests__/media-generate-image.test.ts +31 -0
- package/src/__tests__/memory-v2-static-injector.test.ts +7 -7
- package/src/__tests__/model-intents.test.ts +2 -4
- package/src/__tests__/notification-guardian-path.test.ts +0 -1
- package/src/__tests__/onboarding-template-contract.test.ts +1 -1
- package/src/__tests__/openai-provider.test.ts +46 -0
- package/src/__tests__/openai-responses-provider.test.ts +114 -12
- package/src/__tests__/pending-interactions-resolved-event.test.ts +0 -1
- package/src/__tests__/platform-bash-auto-approve.test.ts +2 -2
- package/src/__tests__/platform.test.ts +2 -2
- package/src/__tests__/plugin-api-tool-definition.test.ts +92 -0
- package/src/__tests__/plugin-bootstrap.test.ts +2 -2
- package/src/__tests__/plugin-tool-contribution.test.ts +13 -6
- package/src/__tests__/plugin-types.test.ts +3 -2
- package/src/__tests__/prechat-onboarding-contract.test.ts +131 -98
- package/src/__tests__/pricing.test.ts +12 -0
- package/src/__tests__/prune-jobs-changes-parser.test.ts +61 -0
- package/src/__tests__/registry.test.ts +2 -8
- package/src/__tests__/require-fresh-approval.test.ts +2 -2
- package/src/__tests__/runtime-events-sse-bilingual.test.ts +154 -0
- package/src/__tests__/shell-tool-proxy-mode.test.ts +1 -1
- package/src/__tests__/skill-feature-flags.test.ts +2 -2
- package/src/__tests__/skill-projection-feature-flag.test.ts +4 -7
- package/src/__tests__/skill-projection.benchmark.test.ts +2 -6
- package/src/__tests__/skill-tool-factory.test.ts +1 -1
- package/src/__tests__/subagent-notify-parent.test.ts +1 -1
- package/src/__tests__/suggestion-routes.test.ts +1 -0
- package/src/__tests__/sync-message-contract.test.ts +59 -0
- package/src/__tests__/system-prompt.test.ts +145 -131
- package/src/__tests__/terminal-tools.test.ts +1 -1
- package/src/__tests__/tool-approval-handler.test.ts +1 -5
- package/src/__tests__/tool-execute-pipeline.test.ts +2 -2
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -5
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +15 -5
- package/src/__tests__/tool-executor.test.ts +9 -62
- package/src/__tests__/tool-grant-request-escalation.test.ts +1 -6
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -6
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
- package/src/__tests__/ui-file-upload-surface.test.ts +2 -2
- package/src/__tests__/usage-routes.test.ts +3 -0
- package/src/__tests__/verification-control-plane-policy.test.ts +2 -2
- package/src/__tests__/workspace-git-service.test.ts +6 -5
- package/src/__tests__/workspace-migration-089-move-memory-tree-out-of-v3.test.ts +86 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +146 -0
- package/src/acp/prepare-agent-env.ts +78 -0
- package/src/acp/session-manager.ts +1 -1
- package/src/agent/loop.ts +8 -0
- package/src/api/README.md +5 -0
- package/src/api/index.ts +4 -0
- package/src/api/package.json +10 -0
- package/src/background-wake/background-wake-routes.test.ts +233 -0
- package/src/background-wake/runtime-registry.ts +24 -0
- package/src/cli/commands/__tests__/browser.test.ts +23 -5
- package/src/cli/commands/__tests__/domain-register.test.ts +110 -0
- package/src/cli/commands/__tests__/domain-status.test.ts +33 -33
- package/src/cli/commands/__tests__/inference-send.test.ts +108 -5
- package/src/cli/commands/__tests__/memory-v2-compare-render.test.ts +98 -0
- package/src/cli/commands/__tests__/memory-v2.test.ts +1 -0
- package/src/cli/commands/__tests__/memory-v3-render.test.ts +340 -0
- package/src/cli/commands/browser.ts +247 -0
- package/src/cli/commands/domain.ts +91 -41
- package/src/cli/commands/inference.ts +93 -40
- package/src/cli/commands/memory-v2-compare-render.ts +115 -0
- package/src/cli/commands/memory-v2.ts +176 -1
- package/src/cli/commands/memory-v3-render.ts +344 -0
- package/src/cli/commands/memory-v3.ts +316 -0
- package/src/cli/program.ts +2 -0
- package/src/config/assistant-feature-flags.ts +21 -9
- package/src/config/bundled-skills/document-editor/SKILL.md +11 -2
- package/src/config/bundled-skills/document-editor/TOOLS.json +18 -0
- package/src/config/bundled-skills/document-editor/tools/document-open.ts +12 -0
- package/src/config/bundled-skills/image-studio/SKILL.md +4 -0
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -2
- package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +13 -8
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +10 -3
- package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +16 -14
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +7 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +7 -2
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/call-site-defaults.ts +7 -6
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +213 -1
- package/src/config/schemas/call-site-catalog.ts +21 -7
- package/src/config/schemas/llm.ts +12 -1
- package/src/config/schemas/memory-v2.ts +246 -0
- package/src/config/schemas/memory.ts +2 -1
- package/src/context/compactor.ts +52 -0
- package/src/conversations/__tests__/message-consolidation.test.ts +350 -0
- package/src/conversations/message-consolidation.ts +404 -0
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +1 -1
- package/src/daemon/__tests__/meet-manifest-loader.test.ts +1 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +2 -13
- package/src/daemon/conversation-agent-loop.ts +126 -76
- package/src/daemon/conversation-error.ts +31 -1
- package/src/daemon/conversation-lifecycle.ts +27 -22
- package/src/daemon/conversation-runtime-assembly.ts +10 -9
- package/src/daemon/conversation-tool-setup.ts +63 -3
- package/src/daemon/conversation-usage.ts +2 -0
- package/src/daemon/conversation.ts +14 -29
- package/src/daemon/disk-pressure-guard.ts +14 -2
- package/src/daemon/handlers/config-model.test.ts +1 -0
- package/src/daemon/handlers/conversations.ts +11 -3
- package/src/daemon/host-browser-proxy.ts +5 -5
- package/src/daemon/host-cu-proxy.ts +4 -4
- package/src/daemon/host-file-proxy.ts +4 -4
- package/src/daemon/host-proxy-base.ts +4 -4
- package/src/daemon/host-transfer-proxy.ts +10 -10
- package/src/daemon/lifecycle.ts +23 -20
- package/src/daemon/meet-manifest-loader.ts +1 -7
- package/src/daemon/message-types/conversations.ts +6 -9
- package/src/daemon/message-types/home.ts +1 -13
- package/src/daemon/message-types/messages.ts +6 -14
- package/src/daemon/message-types/sync.ts +14 -0
- package/src/daemon/shutdown-handlers.ts +24 -5
- package/src/daemon/switch-inference-profile-tool.ts +52 -0
- package/src/daemon/tool-setup-types.ts +13 -0
- package/src/events/relationship-state-updated.ts +25 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
- package/src/home/home-greeting.ts +0 -9
- package/src/home/suggested-prompts.ts +0 -9
- package/src/ipc/gateway-flag-listener.ts +123 -0
- package/src/ipc/skill-routes/registries.ts +8 -12
- package/src/memory/__tests__/db-async-query.test.ts +165 -0
- package/src/memory/__tests__/db-maintenance.test.ts +115 -0
- package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +241 -0
- package/src/memory/__tests__/jobs-store-job-classes.test.ts +28 -1
- package/src/memory/__tests__/memory-retrospective-job.test.ts +7 -0
- package/src/memory/auto-analysis-enqueue.ts +5 -1
- package/src/memory/conversation-crud.ts +71 -70
- package/src/memory/conversation-starters-cadence.ts +3 -1
- package/src/memory/conversation-title-service.ts +19 -3
- package/src/memory/db-async-query.ts +214 -0
- package/src/memory/db-init.ts +10 -0
- package/src/memory/db-maintenance.ts +30 -21
- package/src/memory/graph/bootstrap.ts +8 -1
- package/src/memory/graph/capability-seed.ts +7 -3
- package/src/memory/graph/conversation-graph-memory.ts +100 -17
- package/src/memory/graph/extraction.ts +1 -5
- package/src/memory/graph/graph-search.ts +7 -1
- package/src/memory/indexer.ts +28 -18
- package/src/memory/job-handlers/cleanup.ts +76 -18
- package/src/memory/job-handlers/conversation-starters.ts +1 -4
- package/src/memory/jobs/embed-pkb-file.ts +6 -1
- package/src/memory/jobs-store.ts +14 -0
- package/src/memory/jobs-worker.ts +55 -22
- package/src/memory/llm-request-log-source-clickhouse.ts +42 -2
- package/src/memory/llm-request-log-source-local.ts +7 -0
- package/src/memory/llm-request-log-source.ts +9 -2
- package/src/memory/llm-request-log-store.ts +43 -1
- package/src/memory/llm-usage-store.ts +24 -0
- package/src/memory/memory-retrospective-enqueue.ts +8 -1
- package/src/memory/memory-retrospective-job.ts +5 -0
- package/src/memory/memory-v2-activation-log-store.ts +15 -6
- package/src/memory/migrations/260-rename-cleaned-at.ts +44 -0
- package/src/memory/migrations/261-llm-usage-add-raw-usage.ts +36 -0
- package/src/memory/migrations/262-memory-v3-coactivation.ts +57 -0
- package/src/memory/migrations/263-memory-v3-auto-edges.ts +50 -0
- package/src/memory/migrations/264-llm-request-log-call-site.ts +29 -0
- package/src/memory/migrations/index.ts +17 -0
- package/src/memory/migrations/registry.ts +33 -0
- package/src/memory/schema/conversations.ts +1 -1
- package/src/memory/schema/infrastructure.ts +21 -0
- package/src/memory/tool-usage-store.ts +36 -8
- package/src/memory/v2/__tests__/consolidation-job.test.ts +1 -0
- package/src/memory/v2/__tests__/harness-compare.test.ts +186 -0
- package/src/memory/v2/__tests__/harness-metrics.test.ts +74 -0
- package/src/memory/v2/__tests__/harness-oracle.test.ts +257 -0
- package/src/memory/v2/__tests__/harness-replay-input.test.ts +225 -0
- package/src/memory/v2/__tests__/harness-runner.test.ts +109 -0
- package/src/memory/v2/__tests__/injection.test.ts +127 -98
- package/src/memory/v2/__tests__/qdrant.test.ts +36 -0
- package/src/memory/v2/__tests__/router.test.ts +171 -3
- package/src/memory/v2/harness/compare.ts +57 -0
- package/src/memory/v2/harness/metrics.ts +124 -0
- package/src/memory/v2/harness/oracle.ts +145 -0
- package/src/memory/v2/harness/replay-input.ts +224 -0
- package/src/memory/v2/harness/retriever.ts +74 -0
- package/src/memory/v2/harness/router-retriever.ts +43 -0
- package/src/memory/v2/harness/runner.ts +106 -0
- package/src/memory/v2/harness/trace.ts +58 -0
- package/src/memory/v2/injection.ts +21 -15
- package/src/memory/v2/prompts/router.ts +26 -1
- package/src/memory/v2/qdrant.ts +14 -2
- package/src/memory/v2/router.ts +171 -18
- package/src/memory/v3/__tests__/coactivation-store.test.ts +422 -0
- package/src/memory/v3/__tests__/consolidation-job.test.ts +468 -0
- package/src/memory/v3/__tests__/edge-learning-job.test.ts +324 -0
- package/src/memory/v3/__tests__/edges.test.ts +563 -0
- package/src/memory/v3/__tests__/filter.test.ts +512 -0
- package/src/memory/v3/__tests__/gate.test.ts +574 -0
- package/src/memory/v3/__tests__/index-composition.test.ts +233 -0
- package/src/memory/v3/__tests__/loop.test.ts +530 -0
- package/src/memory/v3/__tests__/retriever.test.ts +226 -0
- package/src/memory/v3/__tests__/scouts.test.ts +440 -0
- package/src/memory/v3/__tests__/shadow-middleware.test.ts +312 -0
- package/src/memory/v3/__tests__/system-prompts.test.ts +154 -0
- package/src/memory/v3/__tests__/traversal.test.ts +469 -0
- package/src/memory/v3/__tests__/tree-index.test.ts +280 -0
- package/src/memory/v3/__tests__/tree-store.test.ts +529 -0
- package/src/memory/v3/__tests__/tree-walk.test.ts +707 -0
- package/src/memory/v3/__tests__/validate.test.ts +245 -0
- package/src/memory/v3/auto-edges.ts +223 -0
- package/src/memory/v3/coactivation-store.ts +124 -0
- package/src/memory/v3/consolidation-job.ts +323 -0
- package/src/memory/v3/edge-learning-job.ts +160 -0
- package/src/memory/v3/edges.ts +249 -0
- package/src/memory/v3/filter.ts +281 -0
- package/src/memory/v3/gate.ts +334 -0
- package/src/memory/v3/index-composition.ts +113 -0
- package/src/memory/v3/llm-capture.ts +46 -0
- package/src/memory/v3/loop.ts +382 -0
- package/src/memory/v3/maintenance.ts +144 -0
- package/src/memory/v3/prompt-context.ts +33 -0
- package/src/memory/v3/prompts/consolidation.ts +458 -0
- package/src/memory/v3/prompts/system-prompts.ts +196 -0
- package/src/memory/v3/retriever.ts +33 -0
- package/src/memory/v3/scouts.ts +420 -0
- package/src/memory/v3/shadow-middleware.ts +305 -0
- package/src/memory/v3/traversal.ts +206 -0
- package/src/memory/v3/tree-index.ts +237 -0
- package/src/memory/v3/tree-store.ts +394 -0
- package/src/memory/v3/tree-walk.ts +351 -0
- package/src/memory/v3/types.ts +65 -0
- package/src/memory/v3/validate.ts +300 -0
- package/src/notifications/adapters/macos.ts +18 -1
- package/src/notifications/adapters/platform.ts +1 -1
- package/src/notifications/decision-engine.ts +1 -4
- package/src/notifications/emit-signal.ts +29 -49
- package/src/permissions/prompter.ts +3 -3
- package/src/permissions/question-prompter.ts +5 -2
- package/src/permissions/secret-prompter.ts +2 -2
- package/src/plugin-api/index.ts +4 -0
- package/src/plugin-api/types.ts +7 -33
- package/src/plugins/defaults/index.ts +6 -0
- package/src/plugins/defaults/injectors.ts +18 -11
- package/src/plugins/external-plugin-loader.ts +5 -68
- package/src/plugins/types.ts +11 -16
- package/src/proactive-artifact/aux-message-injector.ts +17 -4
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +3 -9
- package/src/prompts/persona-resolver.ts +36 -21
- package/src/prompts/sections.ts +39 -7
- package/src/prompts/system-prompt.ts +50 -185
- package/src/prompts/templates/BOOTSTRAP.md +2 -2
- package/src/prompts/templates/system-sections.ts +230 -8
- package/src/providers/__tests__/connection-model-compat.test.ts +234 -0
- package/src/providers/__tests__/retry-callsite.test.ts +85 -5
- package/src/providers/anthropic/client.ts +32 -66
- package/src/providers/call-site-routing.ts +14 -2
- package/src/providers/connection-model-compat.ts +38 -0
- package/src/providers/connection-resolution.ts +16 -2
- package/src/providers/gemini/client.ts +49 -6
- package/src/providers/inference/adapter-factory.ts +3 -0
- package/src/providers/minimax/client.ts +106 -0
- package/src/providers/model-catalog.ts +43 -0
- package/src/providers/model-intents.ts +1 -1
- package/src/providers/openai/chat-completions-provider.ts +6 -3
- package/src/providers/openai/codex-models.ts +18 -0
- package/src/providers/openai/responses-provider.ts +78 -21
- package/src/providers/provider-send-message.ts +7 -1
- package/src/providers/retry.ts +34 -3
- package/src/providers/thinking-config.ts +26 -1
- package/src/providers/usage-tracking.ts +2 -0
- package/src/runtime/AGENTS.md +2 -2
- package/src/runtime/agent-wake.ts +1 -0
- package/src/runtime/assistant-event-hub.ts +76 -6
- package/src/runtime/auth/route-policy.ts +36 -0
- package/src/runtime/btw-sidechain.ts +0 -6
- package/src/runtime/http-types.ts +0 -2
- package/src/runtime/migrations/vbundle-builder.ts +10 -3
- package/src/runtime/pending-interactions.ts +0 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +106 -0
- package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +25 -6
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +512 -0
- package/src/runtime/routes/acp-routes.test.ts +255 -6
- package/src/runtime/routes/acp-routes.ts +8 -1
- package/src/runtime/routes/avatar-routes.ts +10 -10
- package/src/runtime/routes/background-wake-routes.ts +188 -0
- package/src/runtime/routes/browser-tabs-routes.ts +200 -0
- package/src/runtime/routes/btw-routes.ts +0 -6
- package/src/runtime/routes/conversation-cli-routes.ts +1 -1
- package/src/runtime/routes/conversation-list-routes.ts +12 -4
- package/src/runtime/routes/conversation-management-routes.ts +77 -20
- package/src/runtime/routes/conversation-query-routes.ts +142 -36
- package/src/runtime/routes/conversation-routes.ts +252 -410
- package/src/runtime/routes/conversation-starter-routes.ts +6 -3
- package/src/runtime/routes/disk-pressure-routes.ts +1 -1
- package/src/runtime/routes/domain-routes.ts +60 -10
- package/src/runtime/routes/email-routes.ts +5 -2
- package/src/runtime/routes/events-routes.ts +54 -10
- package/src/runtime/routes/group-routes.ts +24 -8
- package/src/runtime/routes/host-browser-routes.ts +10 -2
- package/src/runtime/routes/host-cu-routes.ts +2 -2
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +96 -3
- package/src/runtime/routes/index.ts +8 -0
- package/src/runtime/routes/inference-profile-session-handler.ts +22 -12
- package/src/runtime/routes/inference-profile-session-routes.ts +7 -1
- package/src/runtime/routes/llm-call-sites-routes.ts +32 -5
- package/src/runtime/routes/memory-item-routes.ts +8 -3
- package/src/runtime/routes/memory-v2-routes.ts +215 -5
- package/src/runtime/routes/memory-v3-routes.ts +316 -0
- package/src/runtime/routes/migration-routes.ts +21 -24
- package/src/runtime/routes/plugins-routes.ts +337 -0
- package/src/runtime/routes/rename-conversation-routes.ts +6 -2
- package/src/runtime/routes/secret-routes.ts +25 -5
- package/src/runtime/routes/settings-routes.ts +12 -11
- package/src/runtime/routes/slack-channel-routes.ts +5 -4
- package/src/runtime/routes/workspace-routes.ts +25 -10
- package/src/runtime/sync/resource-sync-events.ts +106 -38
- package/src/runtime/sync/sync-publisher.test.ts +49 -0
- package/src/runtime/sync/sync-publisher.ts +2 -1
- package/src/runtime/verification-outbound-actions.ts +73 -1
- package/src/telemetry/types.ts +12 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
- package/src/telemetry/usage-telemetry-reporter.ts +1 -0
- package/src/tools/acp/spawn.test.ts +119 -0
- package/src/tools/acp/spawn.ts +15 -2
- package/src/tools/apps/definitions.ts +2 -8
- package/src/tools/ask-question/ask-question-tool.test.ts +3 -3
- package/src/tools/ask-question/ask-question-tool.ts +38 -45
- package/src/tools/browser/__tests__/pinned-tabs.test.ts +70 -0
- package/src/tools/browser/browser-execution.ts +16 -3
- package/src/tools/browser/cdp-client/__tests__/browser-tabs-factory.test.ts +402 -0
- package/src/tools/browser/cdp-client/__tests__/types.test.ts +3 -0
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +12 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +27 -1
- package/src/tools/browser/cdp-client/factory.ts +100 -17
- package/src/tools/browser/cdp-client/local-cdp-client.ts +12 -0
- package/src/tools/browser/cdp-client/types.ts +65 -0
- package/src/tools/browser/pinned-tabs.ts +96 -40
- package/src/tools/computer-use/definitions.ts +22 -78
- package/src/tools/credential-execution/make-authenticated-request.ts +3 -9
- package/src/tools/credential-execution/manage-secure-command-tool.ts +3 -9
- package/src/tools/credential-execution/run-authenticated-command.ts +3 -9
- package/src/tools/credentials/vault.ts +3 -9
- package/src/tools/document/document-tool.ts +59 -0
- package/src/tools/execution-target.ts +21 -23
- package/src/tools/executor.ts +6 -1
- package/src/tools/filesystem/edit.ts +3 -9
- package/src/tools/filesystem/list.ts +3 -9
- package/src/tools/filesystem/read.ts +3 -9
- package/src/tools/filesystem/write.ts +3 -9
- package/src/tools/host-filesystem/edit.ts +3 -9
- package/src/tools/host-filesystem/read.ts +3 -9
- package/src/tools/host-filesystem/transfer.ts +3 -9
- package/src/tools/host-filesystem/write.ts +3 -9
- package/src/tools/host-terminal/host-shell.ts +3 -9
- package/src/tools/mcp/mcp-tool-factory.ts +1 -8
- package/src/tools/memory/register.test.ts +1 -1
- package/src/tools/memory/register.ts +4 -9
- package/src/tools/network/web-fetch.ts +3 -9
- package/src/tools/network/web-search.ts +25 -32
- package/src/tools/registry.ts +7 -23
- package/src/tools/schema-transforms.ts +1 -1
- package/src/tools/skills/execute.ts +3 -9
- package/src/tools/skills/load.ts +3 -9
- package/src/tools/skills/skill-tool-factory.ts +1 -8
- package/src/tools/subagent/notify-parent.ts +3 -9
- package/src/tools/system/request-permission.ts +3 -9
- package/src/tools/terminal/shell.ts +3 -9
- package/src/tools/tool-defaults.ts +94 -0
- package/src/tools/types.ts +27 -98
- package/src/tools/ui-surface/definitions.ts +6 -22
- package/src/usage/pricing.ts +23 -0
- package/src/usage/types.ts +12 -0
- package/src/util/logger.ts +16 -7
- package/src/util/platform.ts +7 -2
- package/src/util/sqlite3-runtime.ts +65 -0
- package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +1 -0
- package/src/workspace/migrations/089-move-memory-tree-out-of-v3.ts +86 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/__tests__/compaction-strip-metadata-clear.test.ts +0 -206
- package/src/__tests__/message-complete-display-id.test.ts +0 -175
- package/src/daemon/query-complexity-router.ts +0 -75
- package/src/prompts/cache-boundary.ts +0 -8
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `assistant/src/memory/tree-store.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Coverage matrix:
|
|
5
|
+
* - slugify: lowercase / kebab-case / ascii / 80-char cap / empty fallback.
|
|
6
|
+
* - validateNodeId: accept set, reject set (path-traversal, malformed shapes),
|
|
7
|
+
* reserved `_root` accepted.
|
|
8
|
+
* - readNode / writeNode round-trip: frontmatter survives, body preserved.
|
|
9
|
+
* - children refs parse for both `page:` and `node:` forms.
|
|
10
|
+
* - malformed YAML / unknown frontmatter keys throw.
|
|
11
|
+
* - readNode on missing file: returns null.
|
|
12
|
+
* - writeNode atomicity: no orphan tmp on success, parent dirs created.
|
|
13
|
+
* - listNodes: walks subdirectories, returns nested ids in `/`-form, excludes
|
|
14
|
+
* hidden dirs / non-.md / temp files, missing dir → [].
|
|
15
|
+
* - deleteNode: nested-id round-trip, idempotent on missing.
|
|
16
|
+
* - renderNodeContent: frontmatter + body shape.
|
|
17
|
+
* - No change to memory/concepts/ (v3 lives under memory/tree/).
|
|
18
|
+
*
|
|
19
|
+
* Tests use temp workspaces under `os.tmpdir()`; they never touch `~/.vellum/`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
existsSync,
|
|
24
|
+
mkdirSync,
|
|
25
|
+
mkdtempSync,
|
|
26
|
+
readdirSync,
|
|
27
|
+
readFileSync,
|
|
28
|
+
rmSync,
|
|
29
|
+
writeFileSync,
|
|
30
|
+
} from "node:fs";
|
|
31
|
+
import { tmpdir } from "node:os";
|
|
32
|
+
import { join } from "node:path";
|
|
33
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
deleteNode,
|
|
37
|
+
getTreeDir,
|
|
38
|
+
listNodes,
|
|
39
|
+
readNode,
|
|
40
|
+
renderNodeContent,
|
|
41
|
+
ROOT_NODE_ID,
|
|
42
|
+
slugify,
|
|
43
|
+
validateNodeId,
|
|
44
|
+
writeNode,
|
|
45
|
+
} from "../tree-store.js";
|
|
46
|
+
import type { TreeNode } from "../types.js";
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Helpers
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
let workspaceDir: string;
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
workspaceDir = mkdtempSync(join(tmpdir(), "vellum-tree-store-test-"));
|
|
56
|
+
// Mirror the workspace migration so readNode / writeNode have a target dir.
|
|
57
|
+
mkdirSync(getTreeDir(workspaceDir), { recursive: true });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
if (existsSync(workspaceDir)) {
|
|
62
|
+
rmSync(workspaceDir, { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
function makeNode(overrides: Partial<TreeNode> = {}): TreeNode {
|
|
67
|
+
return {
|
|
68
|
+
id: "people",
|
|
69
|
+
frontmatter: {
|
|
70
|
+
children: ["page:people/alice", "node:people/colleagues"],
|
|
71
|
+
routing_hints: "for work relationships see people/colleagues",
|
|
72
|
+
summary: "People I know.",
|
|
73
|
+
},
|
|
74
|
+
body: "The people branch of the memory tree.\n",
|
|
75
|
+
...overrides,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// slugify
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
describe("slugify", () => {
|
|
84
|
+
test("lowercases ASCII letters", () => {
|
|
85
|
+
expect(slugify("AliceBob")).toBe("alicebob");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("converts spaces and punctuation to single hyphens", () => {
|
|
89
|
+
expect(slugify("Alice's Preferred IDE!")).toBe("alice-s-preferred-ide");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("collapses runs of separators to one hyphen", () => {
|
|
93
|
+
expect(slugify("foo ___ bar")).toBe("foo-bar");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("trims leading and trailing hyphens", () => {
|
|
97
|
+
expect(slugify("---hello world---")).toBe("hello-world");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("collapses '/' to hyphen — slugify produces a single segment", () => {
|
|
101
|
+
expect(slugify("People/Colleagues")).toBe("people-colleagues");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("caps slug length at 80 chars and re-trims trailing hyphen", () => {
|
|
105
|
+
const long = "a".repeat(120);
|
|
106
|
+
const slug = slugify(long);
|
|
107
|
+
expect(slug.length).toBe(80);
|
|
108
|
+
expect(slug.endsWith("-")).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("falls back to a unique placeholder for empty inputs", () => {
|
|
112
|
+
const a = slugify("");
|
|
113
|
+
const b = slugify("!!!");
|
|
114
|
+
expect(a).toMatch(/^node-[a-f0-9]{8}$/);
|
|
115
|
+
expect(b).toMatch(/^node-[a-f0-9]{8}$/);
|
|
116
|
+
expect(a).not.toBe(b);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// validateNodeId
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
describe("validateNodeId", () => {
|
|
125
|
+
test.each([
|
|
126
|
+
["people"],
|
|
127
|
+
["a"],
|
|
128
|
+
["people-colleagues"],
|
|
129
|
+
["people/alice"],
|
|
130
|
+
["people/colleagues/alice"],
|
|
131
|
+
["a/b/c/d/e"],
|
|
132
|
+
[ROOT_NODE_ID],
|
|
133
|
+
])("accepts %p", (id) => {
|
|
134
|
+
expect(() => validateNodeId(id)).not.toThrow();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test.each([
|
|
138
|
+
["empty string", ""],
|
|
139
|
+
["leading slash", "/people"],
|
|
140
|
+
["trailing slash", "people/"],
|
|
141
|
+
["double slash", "people//alice"],
|
|
142
|
+
["dot-dot segment", "people/../alice"],
|
|
143
|
+
["pure dot-dot", ".."],
|
|
144
|
+
["leading dot segment", ".hidden/alice"],
|
|
145
|
+
["backslash", "people\\alice"],
|
|
146
|
+
["null byte", "people\0evil"],
|
|
147
|
+
["whitespace", "people alice"],
|
|
148
|
+
["uppercase", "People"],
|
|
149
|
+
["non-ascii", "café"],
|
|
150
|
+
["leading hyphen", "-people"],
|
|
151
|
+
["non-alphanumeric", "people!"],
|
|
152
|
+
["leading underscore (only _root reserved)", "_other"],
|
|
153
|
+
])("rejects %s (%p)", (_label, id) => {
|
|
154
|
+
expect(() => validateNodeId(id)).toThrow(/Invalid tree-node id/);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("rejects ids longer than 200 chars", () => {
|
|
158
|
+
expect(() => validateNodeId("a".repeat(201))).toThrow(
|
|
159
|
+
/Invalid tree-node id/,
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("rejects segments longer than 80 chars even if total is under 200", () => {
|
|
164
|
+
expect(() => validateNodeId("a".repeat(81))).toThrow(
|
|
165
|
+
/Invalid tree-node id/,
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// readNode / writeNode round-trip
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
describe("writeNode + readNode round-trip", () => {
|
|
175
|
+
test("round-trips frontmatter and body verbatim", async () => {
|
|
176
|
+
const node = makeNode();
|
|
177
|
+
await writeNode(workspaceDir, node);
|
|
178
|
+
|
|
179
|
+
const read = await readNode(workspaceDir, node.id);
|
|
180
|
+
expect(read).not.toBeNull();
|
|
181
|
+
expect(read!.id).toBe(node.id);
|
|
182
|
+
expect(read!.frontmatter.children).toEqual(node.frontmatter.children);
|
|
183
|
+
expect(read!.frontmatter.routing_hints).toBe(
|
|
184
|
+
node.frontmatter.routing_hints,
|
|
185
|
+
);
|
|
186
|
+
expect(read!.frontmatter.summary).toBe(node.frontmatter.summary);
|
|
187
|
+
expect(read!.body).toBe(node.body);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("children parse for both page: and node: reference forms", async () => {
|
|
191
|
+
const node = makeNode({
|
|
192
|
+
id: "mixed",
|
|
193
|
+
frontmatter: {
|
|
194
|
+
children: ["page:procs/git-flow", "node:procs", "page:alice"],
|
|
195
|
+
},
|
|
196
|
+
body: "mixed refs\n",
|
|
197
|
+
});
|
|
198
|
+
await writeNode(workspaceDir, node);
|
|
199
|
+
|
|
200
|
+
const read = await readNode(workspaceDir, "mixed");
|
|
201
|
+
expect(read!.frontmatter.children).toEqual([
|
|
202
|
+
"page:procs/git-flow",
|
|
203
|
+
"node:procs",
|
|
204
|
+
"page:alice",
|
|
205
|
+
]);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("the children list IS the DAG edge — a page may be referenced by multiple parents", async () => {
|
|
209
|
+
await writeNode(
|
|
210
|
+
workspaceDir,
|
|
211
|
+
makeNode({
|
|
212
|
+
id: "team-a",
|
|
213
|
+
frontmatter: { children: ["page:people/alice"] },
|
|
214
|
+
body: "team a\n",
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
217
|
+
await writeNode(
|
|
218
|
+
workspaceDir,
|
|
219
|
+
makeNode({
|
|
220
|
+
id: "team-b",
|
|
221
|
+
frontmatter: { children: ["page:people/alice"] },
|
|
222
|
+
body: "team b\n",
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const a = await readNode(workspaceDir, "team-a");
|
|
227
|
+
const b = await readNode(workspaceDir, "team-b");
|
|
228
|
+
expect(a!.frontmatter.children).toContain("page:people/alice");
|
|
229
|
+
expect(b!.frontmatter.children).toContain("page:people/alice");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("renders frontmatter at the top with --- delimiters", async () => {
|
|
233
|
+
const node = makeNode();
|
|
234
|
+
await writeNode(workspaceDir, node);
|
|
235
|
+
|
|
236
|
+
const raw = readFileSync(
|
|
237
|
+
join(getTreeDir(workspaceDir), `${node.id}.md`),
|
|
238
|
+
"utf-8",
|
|
239
|
+
);
|
|
240
|
+
expect(raw.startsWith("---\n")).toBe(true);
|
|
241
|
+
expect(raw.split("---").length).toBeGreaterThanOrEqual(3);
|
|
242
|
+
expect(raw).toContain("The people branch");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("preserves an empty body", async () => {
|
|
246
|
+
const node = makeNode({ body: "" });
|
|
247
|
+
await writeNode(workspaceDir, node);
|
|
248
|
+
|
|
249
|
+
const read = await readNode(workspaceDir, node.id);
|
|
250
|
+
expect(read!.body).toBe("");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("preserves multiline body with embedded YAML-looking lines", async () => {
|
|
254
|
+
const tricky = "key: value\n---\nnot-frontmatter\n";
|
|
255
|
+
const node = makeNode({ id: "tricky", body: tricky });
|
|
256
|
+
await writeNode(workspaceDir, node);
|
|
257
|
+
|
|
258
|
+
const read = await readNode(workspaceDir, node.id);
|
|
259
|
+
expect(read!.body).toBe(tricky);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("defaults children to [] for a node with empty frontmatter", async () => {
|
|
263
|
+
const node = makeNode({
|
|
264
|
+
id: "bare",
|
|
265
|
+
frontmatter: { children: [] },
|
|
266
|
+
body: "bare\n",
|
|
267
|
+
});
|
|
268
|
+
await writeNode(workspaceDir, node);
|
|
269
|
+
|
|
270
|
+
const read = await readNode(workspaceDir, "bare");
|
|
271
|
+
expect(read!.frontmatter.children).toEqual([]);
|
|
272
|
+
expect(read!.frontmatter.routing_hints).toBeUndefined();
|
|
273
|
+
expect(read!.frontmatter.summary).toBeUndefined();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("readNode returns null for an id that does not exist", async () => {
|
|
277
|
+
const result = await readNode(workspaceDir, "nonexistent");
|
|
278
|
+
expect(result).toBeNull();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("readNode parses a hand-written node with no frontmatter as empty frontmatter + full body", async () => {
|
|
282
|
+
const id = "no-frontmatter";
|
|
283
|
+
const body = "Just some prose, no YAML.\n";
|
|
284
|
+
writeFileSync(join(getTreeDir(workspaceDir), `${id}.md`), body, "utf-8");
|
|
285
|
+
|
|
286
|
+
const read = await readNode(workspaceDir, id);
|
|
287
|
+
expect(read).not.toBeNull();
|
|
288
|
+
expect(read!.frontmatter.children).toEqual([]);
|
|
289
|
+
expect(read!.body).toBe(body);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("readNode throws on malformed YAML frontmatter", async () => {
|
|
293
|
+
const id = "bad-yaml";
|
|
294
|
+
// Unclosed bracket inside the frontmatter block — invalid YAML.
|
|
295
|
+
const raw = "---\nchildren: [unterminated\n---\nbody\n";
|
|
296
|
+
writeFileSync(join(getTreeDir(workspaceDir), `${id}.md`), raw, "utf-8");
|
|
297
|
+
|
|
298
|
+
await expect(readNode(workspaceDir, id)).rejects.toThrow();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("readNode throws on unknown frontmatter keys instead of silently dropping them", async () => {
|
|
302
|
+
const id = "extra-keys";
|
|
303
|
+
const raw = "---\nchildren: []\nunknown_field: oops\n---\nbody\n";
|
|
304
|
+
writeFileSync(join(getTreeDir(workspaceDir), `${id}.md`), raw, "utf-8");
|
|
305
|
+
|
|
306
|
+
await expect(readNode(workspaceDir, id)).rejects.toThrow();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("writeNode overwrites an existing node", async () => {
|
|
310
|
+
await writeNode(workspaceDir, makeNode({ body: "first\n" }));
|
|
311
|
+
await writeNode(workspaceDir, makeNode({ body: "second\n" }));
|
|
312
|
+
|
|
313
|
+
const read = await readNode(workspaceDir, "people");
|
|
314
|
+
expect(read!.body).toBe("second\n");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("writeNode creates parent directories for nested ids", async () => {
|
|
318
|
+
const node = makeNode({ id: "people/colleagues" });
|
|
319
|
+
await writeNode(workspaceDir, node);
|
|
320
|
+
|
|
321
|
+
const filePath = join(getTreeDir(workspaceDir), "people", "colleagues.md");
|
|
322
|
+
expect(existsSync(filePath)).toBe(true);
|
|
323
|
+
|
|
324
|
+
const read = await readNode(workspaceDir, "people/colleagues");
|
|
325
|
+
expect(read!.id).toBe("people/colleagues");
|
|
326
|
+
expect(read!.body).toBe(node.body);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("writeNode round-trips deeply nested ids", async () => {
|
|
330
|
+
const node = makeNode({ id: "people/colleagues/alice" });
|
|
331
|
+
await writeNode(workspaceDir, node);
|
|
332
|
+
|
|
333
|
+
const read = await readNode(workspaceDir, "people/colleagues/alice");
|
|
334
|
+
expect(read!.id).toBe("people/colleagues/alice");
|
|
335
|
+
expect(read!.frontmatter.children).toEqual(node.frontmatter.children);
|
|
336
|
+
expect(read!.body).toBe(node.body);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("writeNode + readNode round-trip the reserved _root id", async () => {
|
|
340
|
+
const node = makeNode({
|
|
341
|
+
id: ROOT_NODE_ID,
|
|
342
|
+
frontmatter: { children: ["node:people"] },
|
|
343
|
+
body: "root of the tree\n",
|
|
344
|
+
});
|
|
345
|
+
await writeNode(workspaceDir, node);
|
|
346
|
+
|
|
347
|
+
const read = await readNode(workspaceDir, ROOT_NODE_ID);
|
|
348
|
+
expect(read!.id).toBe(ROOT_NODE_ID);
|
|
349
|
+
expect(read!.frontmatter.children).toEqual(["node:people"]);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("writeNode rejects malicious ids and writes nothing at the escape target", async () => {
|
|
353
|
+
await expect(
|
|
354
|
+
writeNode(workspaceDir, makeNode({ id: "../escape" })),
|
|
355
|
+
).rejects.toThrow(/Invalid tree-node id/);
|
|
356
|
+
|
|
357
|
+
// `../escape` would resolve to `<workspace>/memory/v3/escape.md`. Confirm
|
|
358
|
+
// the validation throw fired before any I/O — no file at that target.
|
|
359
|
+
expect(existsSync(join(workspaceDir, "memory", "v3", "escape.md"))).toBe(
|
|
360
|
+
false,
|
|
361
|
+
);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("readNode rejects malicious ids", async () => {
|
|
365
|
+
await expect(readNode(workspaceDir, "../escape")).rejects.toThrow(
|
|
366
|
+
/Invalid tree-node id/,
|
|
367
|
+
);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("successful write produces no orphan tmp files", async () => {
|
|
371
|
+
await writeNode(workspaceDir, makeNode());
|
|
372
|
+
|
|
373
|
+
const remaining = readdirSync(getTreeDir(workspaceDir));
|
|
374
|
+
const orphanTmps = remaining.filter((name) => name.includes(".tmp."));
|
|
375
|
+
expect(orphanTmps).toEqual([]);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("does not touch memory/concepts/", async () => {
|
|
379
|
+
await writeNode(workspaceDir, makeNode({ id: "people/colleagues" }));
|
|
380
|
+
|
|
381
|
+
expect(existsSync(join(workspaceDir, "memory", "concepts"))).toBe(false);
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
// renderNodeContent
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
|
|
389
|
+
describe("renderNodeContent", () => {
|
|
390
|
+
test("emits frontmatter block followed by body", () => {
|
|
391
|
+
const rendered = renderNodeContent(makeNode());
|
|
392
|
+
expect(rendered.startsWith("---\n")).toBe(true);
|
|
393
|
+
expect(rendered).toContain("children:");
|
|
394
|
+
expect(rendered).toContain("page:people/alice");
|
|
395
|
+
expect(rendered.endsWith("The people branch of the memory tree.\n")).toBe(
|
|
396
|
+
true,
|
|
397
|
+
);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("keeps the explicit children key even when empty", () => {
|
|
401
|
+
const rendered = renderNodeContent(
|
|
402
|
+
makeNode({ frontmatter: { children: [] }, body: "x\n" }),
|
|
403
|
+
);
|
|
404
|
+
expect(rendered).toContain("children: []");
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
// listNodes
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
describe("listNodes", () => {
|
|
413
|
+
test("returns ids (filename minus .md) for every node on disk", async () => {
|
|
414
|
+
await writeNode(workspaceDir, makeNode({ id: "alice" }));
|
|
415
|
+
await writeNode(workspaceDir, makeNode({ id: "bob" }));
|
|
416
|
+
await writeNode(workspaceDir, makeNode({ id: "carol" }));
|
|
417
|
+
|
|
418
|
+
const ids = await listNodes(workspaceDir);
|
|
419
|
+
expect(ids).toEqual(["alice", "bob", "carol"]);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test("excludes non-.md files in the tree directory", async () => {
|
|
423
|
+
await writeNode(workspaceDir, makeNode({ id: "alice" }));
|
|
424
|
+
|
|
425
|
+
const treeDir = getTreeDir(workspaceDir);
|
|
426
|
+
writeFileSync(join(treeDir, "README.txt"), "ignore me", "utf-8");
|
|
427
|
+
writeFileSync(join(treeDir, "image.png"), "fake", "utf-8");
|
|
428
|
+
writeFileSync(join(treeDir, ".hidden"), "fake", "utf-8");
|
|
429
|
+
|
|
430
|
+
const ids = await listNodes(workspaceDir);
|
|
431
|
+
expect(ids).toEqual(["alice"]);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("walks subdirectories and returns nested ids in '/'-form", async () => {
|
|
435
|
+
await writeNode(workspaceDir, makeNode({ id: "alice" }));
|
|
436
|
+
await writeNode(workspaceDir, makeNode({ id: "people/bob" }));
|
|
437
|
+
await writeNode(workspaceDir, makeNode({ id: "people/carol" }));
|
|
438
|
+
await writeNode(workspaceDir, makeNode({ id: "arcs/2025-04/cutover" }));
|
|
439
|
+
|
|
440
|
+
const ids = await listNodes(workspaceDir);
|
|
441
|
+
expect(ids).toEqual([
|
|
442
|
+
"alice",
|
|
443
|
+
"arcs/2025-04/cutover",
|
|
444
|
+
"people/bob",
|
|
445
|
+
"people/carol",
|
|
446
|
+
]);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test("skips hidden subdirectories and non-.md files inside nested dirs", async () => {
|
|
450
|
+
await writeNode(workspaceDir, makeNode({ id: "people/alice" }));
|
|
451
|
+
|
|
452
|
+
const treeDir = getTreeDir(workspaceDir);
|
|
453
|
+
mkdirSync(join(treeDir, ".git"), { recursive: true });
|
|
454
|
+
writeFileSync(join(treeDir, ".git", "config.md"), "fake", "utf-8");
|
|
455
|
+
writeFileSync(join(treeDir, "people", "notes.txt"), "ignore", "utf-8");
|
|
456
|
+
|
|
457
|
+
const ids = await listNodes(workspaceDir);
|
|
458
|
+
expect(ids).toEqual(["people/alice"]);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test("skips orphaned .tmp.* files at any depth", async () => {
|
|
462
|
+
const treeDir = getTreeDir(workspaceDir);
|
|
463
|
+
await writeNode(workspaceDir, makeNode({ id: "people/alice" }));
|
|
464
|
+
|
|
465
|
+
writeFileSync(
|
|
466
|
+
join(treeDir, "alice.md.tmp.123.abc-def"),
|
|
467
|
+
"stranded",
|
|
468
|
+
"utf-8",
|
|
469
|
+
);
|
|
470
|
+
writeFileSync(
|
|
471
|
+
join(treeDir, "people", "bob.md.tmp.123.abc-def"),
|
|
472
|
+
"stranded",
|
|
473
|
+
"utf-8",
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
const ids = await listNodes(workspaceDir);
|
|
477
|
+
expect(ids).toEqual(["people/alice"]);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test("returns [] when the tree directory does not exist", async () => {
|
|
481
|
+
rmSync(getTreeDir(workspaceDir), { recursive: true, force: true });
|
|
482
|
+
|
|
483
|
+
const ids = await listNodes(workspaceDir);
|
|
484
|
+
expect(ids).toEqual([]);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test("returns [] when the tree directory is empty", async () => {
|
|
488
|
+
const ids = await listNodes(workspaceDir);
|
|
489
|
+
expect(ids).toEqual([]);
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
// ---------------------------------------------------------------------------
|
|
494
|
+
// deleteNode
|
|
495
|
+
// ---------------------------------------------------------------------------
|
|
496
|
+
|
|
497
|
+
describe("deleteNode", () => {
|
|
498
|
+
test("removes the node from disk", async () => {
|
|
499
|
+
const node = makeNode();
|
|
500
|
+
await writeNode(workspaceDir, node);
|
|
501
|
+
expect(await readNode(workspaceDir, node.id)).not.toBeNull();
|
|
502
|
+
|
|
503
|
+
await deleteNode(workspaceDir, node.id);
|
|
504
|
+
expect(await readNode(workspaceDir, node.id)).toBeNull();
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test("removes nested nodes", async () => {
|
|
508
|
+
const node = makeNode({ id: "people/colleagues" });
|
|
509
|
+
await writeNode(workspaceDir, node);
|
|
510
|
+
|
|
511
|
+
await deleteNode(workspaceDir, "people/colleagues");
|
|
512
|
+
expect(await readNode(workspaceDir, "people/colleagues")).toBeNull();
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test("is idempotent — deleting a missing node does not throw", async () => {
|
|
516
|
+
await deleteNode(workspaceDir, "never-existed");
|
|
517
|
+
await deleteNode(workspaceDir, "never-existed");
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
test("does not affect other nodes", async () => {
|
|
521
|
+
await writeNode(workspaceDir, makeNode({ id: "alice" }));
|
|
522
|
+
await writeNode(workspaceDir, makeNode({ id: "bob" }));
|
|
523
|
+
|
|
524
|
+
await deleteNode(workspaceDir, "alice");
|
|
525
|
+
|
|
526
|
+
expect(await readNode(workspaceDir, "alice")).toBeNull();
|
|
527
|
+
expect(await readNode(workspaceDir, "bob")).not.toBeNull();
|
|
528
|
+
});
|
|
529
|
+
});
|