@vellumai/assistant 0.7.2 → 0.7.3
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 +16 -1
- package/docs/architecture/memory.md +5 -2
- package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
- package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
- package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
- package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
- package/openapi.yaml +449 -22
- package/package.json +1 -1
- package/src/__tests__/app-control-flow.test.ts +21 -11
- package/src/__tests__/assistant-event-hub.test.ts +48 -0
- package/src/__tests__/assistant-event.test.ts +0 -10
- package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
- package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
- package/src/__tests__/auto-analysis-end-to-end.test.ts +62 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
- package/src/__tests__/call-conversation-messages.test.ts +8 -2
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
- package/src/__tests__/channel-readiness-service.test.ts +4 -2
- package/src/__tests__/config-loader-backfill.test.ts +379 -0
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
- package/src/__tests__/config-watcher.test.ts +140 -69
- package/src/__tests__/context-search-agent-runner.test.ts +61 -3
- package/src/__tests__/context-search-conversations-source.test.ts +0 -24
- package/src/__tests__/context-search-fanout.test.ts +0 -1
- package/src/__tests__/context-search-memory-source.test.ts +3 -7
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
- package/src/__tests__/context-search-pkb-source.test.ts +0 -1
- package/src/__tests__/context-search-workspace-source.test.ts +0 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +6 -0
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
- package/src/__tests__/conversation-agent-loop.test.ts +454 -5
- package/src/__tests__/conversation-error.test.ts +150 -3
- package/src/__tests__/conversation-process-callsite.test.ts +43 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +6 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +65 -0
- package/src/__tests__/conversation-slash-unknown.test.ts +6 -0
- package/src/__tests__/conversation-speed-override.test.ts +0 -3
- package/src/__tests__/conversation-store.test.ts +0 -18
- package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +404 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +2 -5
- package/src/__tests__/conversation-workspace-injection.test.ts +6 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -0
- package/src/__tests__/credentials-cli.test.ts +7 -0
- package/src/__tests__/cu-unified-flow.test.ts +176 -10
- package/src/__tests__/date-context.test.ts +164 -2
- package/src/__tests__/disk-pressure-guard.test.ts +262 -0
- package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
- package/src/__tests__/disk-pressure-policy.test.ts +241 -0
- package/src/__tests__/disk-pressure-routes.test.ts +379 -0
- package/src/__tests__/disk-pressure-tools.test.ts +277 -0
- package/src/__tests__/disk-usage.test.ts +150 -0
- package/src/__tests__/events-client-registration.test.ts +52 -0
- package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
- package/src/__tests__/file-write-tool.test.ts +4 -10
- package/src/__tests__/filing-service.test.ts +3 -4
- package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
- package/src/__tests__/heartbeat-service.test.ts +260 -11
- package/src/__tests__/host-app-control-proxy.test.ts +195 -25
- package/src/__tests__/host-bash-proxy.test.ts +227 -34
- package/src/__tests__/host-bash-routes.test.ts +178 -13
- package/src/__tests__/host-cu-proxy.test.ts +210 -3
- package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
- package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
- package/src/__tests__/host-file-proxy.test.ts +268 -6
- package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
- package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
- package/src/__tests__/http-user-message-parity.test.ts +107 -1
- package/src/__tests__/injector-chain.test.ts +18 -6
- package/src/__tests__/injector-disk-pressure.test.ts +224 -0
- package/src/__tests__/managed-profile-guard.test.ts +18 -0
- package/src/__tests__/mcp-abort-signal.test.ts +130 -0
- package/src/__tests__/memory-admin-recall.test.ts +3 -11
- package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
- package/src/__tests__/normalize-onboarding.test.ts +180 -0
- package/src/__tests__/oauth-connect-routes.test.ts +316 -0
- package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
- package/src/__tests__/onboarding-persona-write.test.ts +308 -0
- package/src/__tests__/openai-provider.test.ts +45 -8
- package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
- package/src/__tests__/platform-callback-registration.test.ts +21 -4
- package/src/__tests__/platform.test.ts +2 -1
- package/src/__tests__/playbook-execution.test.ts +0 -43
- package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
- package/src/__tests__/provider-tool-name.test.ts +23 -0
- package/src/__tests__/relay-server.test.ts +15 -4
- package/src/__tests__/runtime-events-sse.test.ts +4 -8
- package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
- package/src/__tests__/secret-ingress-http.test.ts +0 -1
- package/src/__tests__/suggestion-routes.test.ts +46 -0
- package/src/__tests__/twilio-validation.test.ts +2 -2
- package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
- package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
- package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
- package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +90 -0
- package/src/approvals/guardian-decision-primitive.ts +13 -0
- package/src/approvals/guardian-request-resolvers.ts +16 -17
- package/src/backup/snapshot-lock.ts +2 -27
- package/src/bundler/compiler-tools.ts +3 -2
- package/src/calls/call-conversation-messages.ts +46 -10
- package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
- package/src/cli/commands/bash.ts +35 -108
- package/src/cli/commands/contacts.ts +64 -25
- package/src/cli/commands/credentials.ts +56 -0
- package/src/cli/commands/memory-v2.ts +7 -6
- package/src/cli/commands/oauth/__tests__/connect.test.ts +437 -1
- package/src/cli/commands/oauth/connect.ts +127 -1
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
- package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
- package/src/cli/commands/platform/index.ts +16 -7
- package/src/cli/commands/status.ts +57 -0
- package/src/cli/program.ts +4 -2
- package/src/config/assistant-feature-flags.ts +13 -3
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
- package/src/config/env.ts +0 -8
- package/src/config/feature-flag-registry.json +27 -3
- package/src/config/loader.ts +127 -8
- package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
- package/src/config/schemas/call-site-catalog.ts +14 -0
- package/src/config/schemas/channels.ts +0 -5
- package/src/config/schemas/heartbeat.ts +1 -1
- package/src/config/schemas/llm.ts +2 -0
- package/src/config/schemas/memory-lifecycle.ts +13 -0
- package/src/config/schemas/memory-v2.ts +75 -11
- package/src/config/schemas/platform.ts +43 -3
- package/src/config/schemas/services.ts +28 -0
- package/src/config/seed-inference-profiles.ts +230 -33
- package/src/contacts/contact-store.ts +0 -25
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
- package/src/daemon/assistant-attachments.ts +4 -4
- package/src/daemon/config-watcher.ts +85 -57
- package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
- package/src/daemon/conversation-agent-loop.ts +170 -33
- package/src/daemon/conversation-error.ts +87 -15
- package/src/daemon/conversation-lifecycle.ts +1 -3
- package/src/daemon/conversation-process.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +26 -0
- package/src/daemon/conversation-store.ts +2 -2
- package/src/daemon/conversation-surfaces.ts +195 -15
- package/src/daemon/conversation-tool-setup.ts +57 -14
- package/src/daemon/conversation.ts +17 -22
- package/src/daemon/date-context.ts +71 -22
- package/src/daemon/disk-pressure-background-gate.ts +73 -0
- package/src/daemon/disk-pressure-guard.ts +343 -0
- package/src/daemon/disk-pressure-policy.ts +163 -0
- package/src/daemon/handlers/shared.ts +0 -1
- package/src/daemon/handlers/skills.ts +3 -4
- package/src/daemon/host-app-control-proxy.ts +137 -41
- package/src/daemon/host-bash-proxy.ts +46 -21
- package/src/daemon/host-cu-proxy.ts +49 -3
- package/src/daemon/host-file-proxy.ts +43 -7
- package/src/daemon/host-transfer-proxy.ts +95 -4
- package/src/daemon/lifecycle.ts +79 -28
- package/src/daemon/meet-host-supervisor.ts +4 -4
- package/src/daemon/meet-manifest-loader.ts +0 -1
- package/src/daemon/memory-v2-startup.ts +14 -4
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/conversations.ts +4 -0
- package/src/daemon/message-types/disk-pressure.ts +9 -0
- package/src/daemon/message-types/messages.ts +3 -0
- package/src/daemon/profiler-run-store.ts +5 -5
- package/src/daemon/tool-setup-types.ts +2 -2
- package/src/documents/document-store.ts +85 -0
- package/src/filing/filing-service.ts +30 -5
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
- package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
- package/src/heartbeat/heartbeat-run-store.ts +13 -0
- package/src/heartbeat/heartbeat-service.ts +205 -31
- package/src/home/feed-scheduler.ts +18 -0
- package/src/inbound/platform-callback-registration.ts +8 -15
- package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
- package/src/ipc/assistant-server.ts +56 -2
- package/src/ipc/gateway-client.ts +37 -3
- package/src/live-voice/live-voice-archive.ts +4 -4
- package/src/live-voice/protocol.ts +5 -7
- package/src/media/image-service.ts +1 -7
- package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +52 -22
- package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
- package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
- package/src/memory/admin.ts +5 -9
- package/src/memory/context-search/agent-runner.ts +19 -2
- package/src/memory/context-search/sources/conversations.ts +2 -11
- package/src/memory/context-search/sources/memory-v2.ts +5 -4
- package/src/memory/context-search/sources/memory.ts +0 -1
- package/src/memory/context-search/types.ts +0 -1
- package/src/memory/conversation-crud.ts +4 -12
- package/src/memory/db-init.ts +2 -0
- package/src/memory/embedding-runtime-manager.ts +119 -5
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +32 -21
- package/src/memory/graph/conversation-graph-memory.ts +42 -54
- package/src/memory/graph/extraction.ts +1 -3
- package/src/memory/graph/graph-search.test.ts +10 -67
- package/src/memory/graph/graph-search.ts +1 -20
- package/src/memory/graph/retriever.test.ts +6 -0
- package/src/memory/graph/retriever.ts +6 -10
- package/src/memory/indexer.ts +54 -45
- package/src/memory/job-handlers/backfill.ts +2 -11
- package/src/memory/job-handlers/cleanup.ts +43 -0
- package/src/memory/job-handlers/embedding.ts +6 -8
- package/src/memory/job-handlers/summarization.ts +2 -7
- package/src/memory/jobs-store.ts +48 -0
- package/src/memory/jobs-worker.ts +81 -43
- package/src/memory/memory-v2-activation-log-store.ts +32 -14
- package/src/memory/memory-v2-concept-frequency.ts +169 -0
- package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/pkb/pkb-search.test.ts +6 -0
- package/src/memory/qdrant-client.ts +0 -13
- package/src/memory/rerank-local.ts +374 -0
- package/src/memory/search/semantic.ts +6 -67
- package/src/memory/trace-event-store.ts +1 -17
- package/src/memory/v2/__tests__/activation.test.ts +311 -250
- package/src/memory/v2/__tests__/consolidation-job.test.ts +40 -8
- package/src/memory/v2/__tests__/injection.test.ts +157 -167
- package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
- package/src/memory/v2/__tests__/qdrant.test.ts +16 -0
- package/src/memory/v2/__tests__/reranker.test.ts +338 -0
- package/src/memory/v2/__tests__/sim.test.ts +5 -199
- package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
- package/src/memory/v2/__tests__/static-context.test.ts +76 -1
- package/src/memory/v2/activation.ts +149 -156
- package/src/memory/v2/consolidation-job.ts +62 -12
- package/src/memory/v2/injection.ts +47 -60
- package/src/memory/v2/prompts/consolidation.ts +36 -1
- package/src/memory/v2/qdrant.ts +99 -0
- package/src/memory/v2/reranker.ts +177 -0
- package/src/memory/v2/sim.ts +10 -84
- package/src/memory/v2/skill-content.ts +4 -3
- package/src/memory/v2/skill-store.ts +82 -59
- package/src/memory/v2/static-context.ts +22 -0
- package/src/memory/v2/types.ts +10 -10
- package/src/notifications/copy-composer.ts +13 -0
- package/src/notifications/signal.ts +4 -0
- package/src/oauth/AGENTS.md +3 -1
- package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
- package/src/oauth/connect-orchestrator.ts +2 -0
- package/src/oauth/connection-resolver.test.ts +66 -1
- package/src/oauth/connection-resolver.ts +55 -1
- package/src/oauth/oauth-connect-state.ts +77 -0
- package/src/oauth/seed-providers.ts +58 -1
- package/src/plugins/defaults/injectors.ts +35 -2
- package/src/plugins/defaults/memory-retrieval.ts +5 -6
- package/src/plugins/types.ts +7 -0
- package/src/proactive-artifact/aux-message-injector.ts +74 -0
- package/src/proactive-artifact/decision.test.ts +226 -0
- package/src/proactive-artifact/decision.ts +165 -0
- package/src/proactive-artifact/index.ts +7 -0
- package/src/proactive-artifact/job.test.ts +867 -0
- package/src/proactive-artifact/job.ts +352 -0
- package/src/proactive-artifact/message-copy.ts +41 -0
- package/src/proactive-artifact/trigger-state.test.ts +277 -0
- package/src/proactive-artifact/trigger-state.ts +119 -0
- package/src/prompts/normalize-onboarding.ts +80 -0
- package/src/prompts/persona-resolver.ts +101 -9
- package/src/prompts/system-prompt.ts +21 -7
- package/src/prompts/templates/BOOTSTRAP.md +13 -5
- package/src/providers/__tests__/retry-callsite.test.ts +222 -1
- package/src/providers/model-intents.ts +7 -0
- package/src/providers/openrouter/client.ts +8 -0
- package/src/providers/retry.ts +50 -0
- package/src/providers/types.ts +1 -0
- package/src/runtime/__tests__/agent-wake.test.ts +456 -3
- package/src/runtime/agent-wake.ts +238 -100
- package/src/runtime/assistant-event-hub.ts +36 -6
- package/src/runtime/assistant-event.ts +0 -1
- package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
- package/src/runtime/auth/route-policy.ts +14 -1
- package/src/runtime/auth/same-actor.ts +216 -0
- package/src/runtime/channel-retry-sweep.ts +65 -1
- package/src/runtime/guardian-reply-router.ts +10 -0
- package/src/runtime/local-actor-identity.ts +52 -11
- package/src/runtime/pending-interactions.ts +8 -0
- package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
- package/src/runtime/routes/client-routes.ts +20 -2
- package/src/runtime/routes/contact-routes.ts +0 -25
- package/src/runtime/routes/conversation-routes.ts +35 -26
- package/src/runtime/routes/debug-bash-routes.ts +163 -0
- package/src/runtime/routes/disk-pressure-routes.ts +121 -0
- package/src/runtime/routes/document-pdf-renderer.ts +6 -2
- package/src/runtime/routes/documents-routes.ts +2 -75
- package/src/runtime/routes/events-routes.ts +41 -9
- package/src/runtime/routes/host-bash-routes.ts +23 -3
- package/src/runtime/routes/host-cu-routes.ts +33 -6
- package/src/runtime/routes/host-file-routes.ts +32 -6
- package/src/runtime/routes/host-transfer-routes.ts +79 -16
- package/src/runtime/routes/identity-routes.ts +7 -138
- package/src/runtime/routes/inbound-message-handler.ts +77 -12
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -0
- package/src/runtime/routes/index.ts +6 -0
- package/src/runtime/routes/memory-item-routes.test.ts +41 -15
- package/src/runtime/routes/memory-v2-routes.ts +33 -0
- package/src/runtime/routes/oauth-connect-routes.ts +153 -0
- package/src/runtime/verification-outbound-actions.ts +4 -4
- package/src/schedule/run-script.ts +37 -5
- package/src/schedule/scheduler.ts +20 -1
- package/src/security/encrypted-store.ts +2 -0
- package/src/security/secure-keys.ts +55 -0
- package/src/skills/remote-skill-policy.ts +4 -10
- package/src/subagent/index.ts +1 -7
- package/src/subagent/manager.ts +1 -15
- package/src/tasks/task-runner.ts +0 -1
- package/src/tasks/task-store.ts +0 -3
- package/src/tools/background-tool-registry.ts +17 -3
- package/src/tools/host-filesystem/edit.test.ts +151 -0
- package/src/tools/host-filesystem/edit.ts +43 -1
- package/src/tools/host-filesystem/read.test.ts +129 -0
- package/src/tools/host-filesystem/read.ts +43 -1
- package/src/tools/host-filesystem/transfer.test.ts +127 -2
- package/src/tools/host-filesystem/transfer.ts +56 -11
- package/src/tools/host-filesystem/write.test.ts +134 -0
- package/src/tools/host-filesystem/write.ts +43 -1
- package/src/tools/host-terminal/host-shell.ts +13 -6
- package/src/tools/mcp/mcp-tool-factory.ts +2 -1
- package/src/tools/memory/register.test.ts +12 -9
- package/src/tools/memory/register.ts +1 -2
- package/src/tools/provider-tool-name.ts +28 -0
- package/src/tools/registry.ts +30 -9
- package/src/tools/terminal/shell.ts +9 -1
- package/src/tools/tool-approval-handler.ts +31 -6
- package/src/tools/types.ts +24 -2
- package/src/tts/provider-catalog.ts +3 -5
- package/src/util/disk-usage.ts +138 -0
- package/src/util/platform.ts +21 -11
- package/src/util/process-liveness.ts +26 -0
- package/src/workspace/heartbeat-service.ts +19 -0
- package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
- package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
- package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +72 -0
- package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
- package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
- package/src/memory/v2/skill-qdrant.ts +0 -404
- package/src/signals/bash.ts +0 -198
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// ── Module mocks (must precede imports) ───────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
type OrchestrateOptions = {
|
|
6
|
+
service: string;
|
|
7
|
+
clientId: string;
|
|
8
|
+
clientSecret?: string;
|
|
9
|
+
callbackTransport?: string;
|
|
10
|
+
requestedScopes?: string[];
|
|
11
|
+
isInteractive: boolean;
|
|
12
|
+
onDeferredComplete?: (r: {
|
|
13
|
+
success: boolean;
|
|
14
|
+
service: string;
|
|
15
|
+
accountInfo?: string;
|
|
16
|
+
grantedScopes?: string[];
|
|
17
|
+
error?: string;
|
|
18
|
+
}) => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let capturedOnDeferredComplete: OrchestrateOptions["onDeferredComplete"] | undefined;
|
|
22
|
+
let mockOrchestrateResult: Record<string, unknown> = {
|
|
23
|
+
success: true,
|
|
24
|
+
deferred: true,
|
|
25
|
+
authorizeUrl: "https://accounts.google.com/o/oauth2/auth?client_id=test",
|
|
26
|
+
state: "test-state-uuid-abc123",
|
|
27
|
+
service: "google",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
mock.module("../oauth/connect-orchestrator.js", () => ({
|
|
31
|
+
orchestrateOAuthConnect: async (opts: OrchestrateOptions) => {
|
|
32
|
+
capturedOnDeferredComplete = opts.onDeferredComplete;
|
|
33
|
+
return mockOrchestrateResult;
|
|
34
|
+
},
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
mock.module("../util/logger.js", () => ({
|
|
38
|
+
getLogger: () =>
|
|
39
|
+
new Proxy({} as Record<string, unknown>, {
|
|
40
|
+
get: () => () => {},
|
|
41
|
+
}),
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
// NOTE: Do NOT mock oauth-connect-state — use the real module so we can
|
|
45
|
+
// verify state transitions via getOAuthConnectState.
|
|
46
|
+
|
|
47
|
+
// ── Import SUT after mocks ─────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
const { ROUTES } = await import("../runtime/routes/oauth-connect-routes.js");
|
|
50
|
+
const { BadRequestError, InternalError, NotFoundError } = await import(
|
|
51
|
+
"../runtime/routes/errors.js"
|
|
52
|
+
);
|
|
53
|
+
const { _clearAllOAuthConnectStates, getOAuthConnectState } = await import(
|
|
54
|
+
"../oauth/oauth-connect-state.js"
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
function findRoute(operationId: string) {
|
|
60
|
+
const route = ROUTES.find((r) => r.operationId === operationId);
|
|
61
|
+
if (!route) throw new Error(`Route ${operationId} not found`);
|
|
62
|
+
return route;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Tests ──────────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
describe("oauth-connect-routes", () => {
|
|
68
|
+
describe("POST internal/oauth/connect/start", () => {
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
capturedOnDeferredComplete = undefined;
|
|
71
|
+
mockOrchestrateResult = {
|
|
72
|
+
success: true,
|
|
73
|
+
deferred: true,
|
|
74
|
+
authorizeUrl: "https://accounts.google.com/o/oauth2/auth?client_id=test",
|
|
75
|
+
state: "test-state-uuid-abc123",
|
|
76
|
+
service: "google",
|
|
77
|
+
};
|
|
78
|
+
_clearAllOAuthConnectStates();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("happy path returns auth_url and state, sets pending in state map", async () => {
|
|
82
|
+
const result = await findRoute("internal_oauth_connect_start").handler({
|
|
83
|
+
body: {
|
|
84
|
+
service: "google",
|
|
85
|
+
clientId: "my-client-id",
|
|
86
|
+
callbackTransport: "gateway",
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
expect(result).toEqual({
|
|
90
|
+
auth_url: "https://accounts.google.com/o/oauth2/auth?client_id=test",
|
|
91
|
+
state: "test-state-uuid-abc123",
|
|
92
|
+
});
|
|
93
|
+
// State map should have pending entry
|
|
94
|
+
expect(getOAuthConnectState("test-state-uuid-abc123")).toMatchObject({
|
|
95
|
+
status: "pending",
|
|
96
|
+
service: "google",
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("invalid callbackTransport throws BadRequestError", async () => {
|
|
101
|
+
await expect(
|
|
102
|
+
findRoute("internal_oauth_connect_start").handler({
|
|
103
|
+
body: {
|
|
104
|
+
service: "google",
|
|
105
|
+
clientId: "my-client-id",
|
|
106
|
+
callbackTransport: "ftp",
|
|
107
|
+
},
|
|
108
|
+
}),
|
|
109
|
+
).rejects.toBeInstanceOf(BadRequestError);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("missing clientId throws BadRequestError", async () => {
|
|
113
|
+
await expect(
|
|
114
|
+
findRoute("internal_oauth_connect_start").handler({
|
|
115
|
+
body: {
|
|
116
|
+
service: "google",
|
|
117
|
+
callbackTransport: "gateway",
|
|
118
|
+
},
|
|
119
|
+
}),
|
|
120
|
+
).rejects.toBeInstanceOf(BadRequestError);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("missing service throws BadRequestError", async () => {
|
|
124
|
+
await expect(
|
|
125
|
+
findRoute("internal_oauth_connect_start").handler({
|
|
126
|
+
body: {
|
|
127
|
+
clientId: "my-client-id",
|
|
128
|
+
callbackTransport: "gateway",
|
|
129
|
+
},
|
|
130
|
+
}),
|
|
131
|
+
).rejects.toBeInstanceOf(BadRequestError);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("orchestrator returns success:false throws InternalError", async () => {
|
|
135
|
+
mockOrchestrateResult = {
|
|
136
|
+
success: false,
|
|
137
|
+
error: "provider configuration error",
|
|
138
|
+
deferred: false,
|
|
139
|
+
};
|
|
140
|
+
await expect(
|
|
141
|
+
findRoute("internal_oauth_connect_start").handler({
|
|
142
|
+
body: {
|
|
143
|
+
service: "google",
|
|
144
|
+
clientId: "my-client-id",
|
|
145
|
+
callbackTransport: "gateway",
|
|
146
|
+
},
|
|
147
|
+
}),
|
|
148
|
+
).rejects.toBeInstanceOf(InternalError);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("loopback callbackTransport is also accepted", async () => {
|
|
152
|
+
const result = await findRoute("internal_oauth_connect_start").handler({
|
|
153
|
+
body: {
|
|
154
|
+
service: "google",
|
|
155
|
+
clientId: "my-client-id",
|
|
156
|
+
callbackTransport: "loopback",
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
expect(result).toMatchObject({
|
|
160
|
+
auth_url: "https://accounts.google.com/o/oauth2/auth?client_id=test",
|
|
161
|
+
state: "test-state-uuid-abc123",
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("success:true, deferred:false throws InternalError (synchronous completion not supported via daemon route)", async () => {
|
|
166
|
+
// The daemon-owned route requires a deferred flow so the CLI can poll for status.
|
|
167
|
+
// When the orchestrator returns { success: true, deferred: false } (e.g., already
|
|
168
|
+
// authenticated), the handler has no auth_url or state to return and throws an
|
|
169
|
+
// InternalError rather than silently returning a malformed response.
|
|
170
|
+
mockOrchestrateResult = {
|
|
171
|
+
success: true,
|
|
172
|
+
deferred: false,
|
|
173
|
+
service: "google",
|
|
174
|
+
grantedScopes: [],
|
|
175
|
+
};
|
|
176
|
+
await expect(
|
|
177
|
+
findRoute("internal_oauth_connect_start").handler({
|
|
178
|
+
body: {
|
|
179
|
+
service: "google",
|
|
180
|
+
clientId: "my-client-id",
|
|
181
|
+
callbackTransport: "gateway",
|
|
182
|
+
},
|
|
183
|
+
}),
|
|
184
|
+
).rejects.toBeInstanceOf(InternalError);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("GET internal/oauth/connect/status/:state", () => {
|
|
189
|
+
beforeEach(() => {
|
|
190
|
+
_clearAllOAuthConnectStates();
|
|
191
|
+
capturedOnDeferredComplete = undefined;
|
|
192
|
+
mockOrchestrateResult = {
|
|
193
|
+
success: true,
|
|
194
|
+
deferred: true,
|
|
195
|
+
authorizeUrl: "https://accounts.google.com/o/oauth2/auth?client_id=test",
|
|
196
|
+
state: "test-state-uuid-abc123",
|
|
197
|
+
service: "google",
|
|
198
|
+
};
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("returns pending after start", async () => {
|
|
202
|
+
await findRoute("internal_oauth_connect_start").handler({
|
|
203
|
+
body: {
|
|
204
|
+
service: "google",
|
|
205
|
+
clientId: "my-client-id",
|
|
206
|
+
callbackTransport: "gateway",
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
const result = findRoute("internal_oauth_connect_status").handler({
|
|
210
|
+
pathParams: { state: "test-state-uuid-abc123" },
|
|
211
|
+
});
|
|
212
|
+
expect(result).toMatchObject({ status: "pending", service: "google" });
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("returns complete after onDeferredComplete fires with success", async () => {
|
|
216
|
+
await findRoute("internal_oauth_connect_start").handler({
|
|
217
|
+
body: {
|
|
218
|
+
service: "google",
|
|
219
|
+
clientId: "my-client-id",
|
|
220
|
+
callbackTransport: "gateway",
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
// Fire the onDeferredComplete callback manually
|
|
224
|
+
capturedOnDeferredComplete?.({
|
|
225
|
+
success: true,
|
|
226
|
+
service: "google",
|
|
227
|
+
accountInfo: "user@example.com",
|
|
228
|
+
});
|
|
229
|
+
const result = findRoute("internal_oauth_connect_status").handler({
|
|
230
|
+
pathParams: { state: "test-state-uuid-abc123" },
|
|
231
|
+
});
|
|
232
|
+
expect(result).toMatchObject({
|
|
233
|
+
status: "complete",
|
|
234
|
+
service: "google",
|
|
235
|
+
account_info: "user@example.com",
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("returns error after onDeferredComplete fires with failure", async () => {
|
|
240
|
+
await findRoute("internal_oauth_connect_start").handler({
|
|
241
|
+
body: {
|
|
242
|
+
service: "google",
|
|
243
|
+
clientId: "my-client-id",
|
|
244
|
+
callbackTransport: "gateway",
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
capturedOnDeferredComplete?.({
|
|
248
|
+
success: false,
|
|
249
|
+
service: "google",
|
|
250
|
+
error: "exchange failed",
|
|
251
|
+
});
|
|
252
|
+
const result = findRoute("internal_oauth_connect_status").handler({
|
|
253
|
+
pathParams: { state: "test-state-uuid-abc123" },
|
|
254
|
+
});
|
|
255
|
+
expect(result).toMatchObject({
|
|
256
|
+
status: "error",
|
|
257
|
+
service: "google",
|
|
258
|
+
error: "exchange failed",
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("throws NotFoundError for unknown state", () => {
|
|
263
|
+
expect(() =>
|
|
264
|
+
findRoute("internal_oauth_connect_status").handler({
|
|
265
|
+
pathParams: { state: "nonexistent-state" },
|
|
266
|
+
}),
|
|
267
|
+
).toThrow(NotFoundError);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("returns complete with granted_scopes after onDeferredComplete fires with grantedScopes", async () => {
|
|
271
|
+
await findRoute("internal_oauth_connect_start").handler({
|
|
272
|
+
body: {
|
|
273
|
+
service: "google",
|
|
274
|
+
clientId: "my-client-id",
|
|
275
|
+
callbackTransport: "gateway",
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
// Fire the onDeferredComplete callback with grantedScopes
|
|
279
|
+
capturedOnDeferredComplete?.({
|
|
280
|
+
success: true,
|
|
281
|
+
service: "google",
|
|
282
|
+
accountInfo: "user@example.com",
|
|
283
|
+
grantedScopes: ["https://www.googleapis.com/auth/calendar", "https://www.googleapis.com/auth/gmail.readonly"],
|
|
284
|
+
});
|
|
285
|
+
const result = findRoute("internal_oauth_connect_status").handler({
|
|
286
|
+
pathParams: { state: "test-state-uuid-abc123" },
|
|
287
|
+
}) as Record<string, unknown>;
|
|
288
|
+
expect(result).toMatchObject({
|
|
289
|
+
status: "complete",
|
|
290
|
+
service: "google",
|
|
291
|
+
account_info: "user@example.com",
|
|
292
|
+
granted_scopes: ["https://www.googleapis.com/auth/calendar", "https://www.googleapis.com/auth/gmail.readonly"],
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("complete without accountInfo does not include account_info field", async () => {
|
|
297
|
+
await findRoute("internal_oauth_connect_start").handler({
|
|
298
|
+
body: {
|
|
299
|
+
service: "google",
|
|
300
|
+
clientId: "my-client-id",
|
|
301
|
+
callbackTransport: "gateway",
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
capturedOnDeferredComplete?.({
|
|
305
|
+
success: true,
|
|
306
|
+
service: "google",
|
|
307
|
+
// No accountInfo
|
|
308
|
+
});
|
|
309
|
+
const result = findRoute("internal_oauth_connect_status").handler({
|
|
310
|
+
pathParams: { state: "test-state-uuid-abc123" },
|
|
311
|
+
}) as Record<string, unknown>;
|
|
312
|
+
expect(result.status).toBe("complete");
|
|
313
|
+
expect(result.account_info).toBeUndefined();
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
});
|
|
@@ -2,8 +2,26 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
|
|
3
3
|
import { PROVIDER_SEED_DATA } from "../oauth/seed-providers.js";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Allowed CDN prefixes for ``logoUrl``. Mirrors the source registry in
|
|
7
|
+
* ``clients/shared/Resources/integration-logos-manifest.json``:
|
|
8
|
+
*
|
|
9
|
+
* - Simple Icons (CC0) is the default for most providers.
|
|
10
|
+
* - thesvg via jsDelivr is the documented fallback for brands Simple Icons
|
|
11
|
+
* doesn't host (e.g. Salesforce, which Simple Icons removed for
|
|
12
|
+
* trademark reasons). Same source is already used for the bundled PDFs
|
|
13
|
+
* of figma/github/gmail/linear/notion/outlook/slack.
|
|
14
|
+
*
|
|
15
|
+
* Adding another CDN should be a deliberate choice — extend this list
|
|
16
|
+
* and update the manifest in tandem.
|
|
17
|
+
*/
|
|
18
|
+
const ALLOWED_LOGO_URL_PREFIXES = [
|
|
19
|
+
"https://cdn.simpleicons.org/",
|
|
20
|
+
"https://cdn.jsdelivr.net/gh/glincker/thesvg@",
|
|
21
|
+
];
|
|
22
|
+
|
|
5
23
|
describe("PROVIDER_SEED_DATA logo URLs", () => {
|
|
6
|
-
test("every well-known provider has a
|
|
24
|
+
test("every well-known provider has a recognised CDN logoUrl", () => {
|
|
7
25
|
const missing: string[] = [];
|
|
8
26
|
const invalid: Array<{ provider: string; logoUrl: string }> = [];
|
|
9
27
|
|
|
@@ -12,7 +30,11 @@ describe("PROVIDER_SEED_DATA logo URLs", () => {
|
|
|
12
30
|
missing.push(key);
|
|
13
31
|
continue;
|
|
14
32
|
}
|
|
15
|
-
if (
|
|
33
|
+
if (
|
|
34
|
+
!ALLOWED_LOGO_URL_PREFIXES.some((prefix) =>
|
|
35
|
+
seed.logoUrl!.startsWith(prefix),
|
|
36
|
+
)
|
|
37
|
+
) {
|
|
16
38
|
invalid.push({ provider: key, logoUrl: seed.logoUrl });
|
|
17
39
|
}
|
|
18
40
|
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `writeOnboardingSection` in persona-resolver.
|
|
3
|
+
*
|
|
4
|
+
* The function writes a managed `## Onboarding Context` section to the
|
|
5
|
+
* guardian persona file (with a fallback chain). These tests stub
|
|
6
|
+
* `util/platform.js` and `contacts/contact-store.js` to control the
|
|
7
|
+
* write target and verify idempotency, fallback, and field omission.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
existsSync,
|
|
12
|
+
mkdirSync,
|
|
13
|
+
mkdtempSync,
|
|
14
|
+
readFileSync,
|
|
15
|
+
rmSync,
|
|
16
|
+
writeFileSync,
|
|
17
|
+
} from "node:fs";
|
|
18
|
+
import { tmpdir } from "node:os";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import {
|
|
21
|
+
afterAll,
|
|
22
|
+
afterEach,
|
|
23
|
+
beforeAll,
|
|
24
|
+
beforeEach,
|
|
25
|
+
describe,
|
|
26
|
+
expect,
|
|
27
|
+
mock,
|
|
28
|
+
test,
|
|
29
|
+
} from "bun:test";
|
|
30
|
+
|
|
31
|
+
// ── Mock state ────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
let mockWorkspaceDir: string = "";
|
|
34
|
+
let mockVellumGuardian: {
|
|
35
|
+
contact: { userFile: string | null };
|
|
36
|
+
channel: Record<string, unknown>;
|
|
37
|
+
} | null = null;
|
|
38
|
+
|
|
39
|
+
// ── Mock modules (must precede imports from the module under test) ──
|
|
40
|
+
|
|
41
|
+
mock.module("../util/logger.js", () => ({
|
|
42
|
+
getLogger: () =>
|
|
43
|
+
new Proxy({} as Record<string, unknown>, {
|
|
44
|
+
get: (_target, prop) =>
|
|
45
|
+
prop === "child"
|
|
46
|
+
? () =>
|
|
47
|
+
new Proxy({} as Record<string, unknown>, { get: () => () => {} })
|
|
48
|
+
: () => {},
|
|
49
|
+
}),
|
|
50
|
+
getCliLogger: () => ({}),
|
|
51
|
+
truncateForLog: (v: string) => v,
|
|
52
|
+
initLogger: () => {},
|
|
53
|
+
pruneOldLogFiles: () => 0,
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
mock.module("../util/platform.js", () => ({
|
|
57
|
+
getWorkspaceDir: () => mockWorkspaceDir,
|
|
58
|
+
getWorkspacePromptPath: (file: string) => join(mockWorkspaceDir, file),
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
mock.module("../contacts/contact-store.js", () => ({
|
|
62
|
+
findContactByChannelExternalId: () => null,
|
|
63
|
+
findGuardianForChannel: (channelType: string) =>
|
|
64
|
+
channelType === "vellum" ? mockVellumGuardian : null,
|
|
65
|
+
listGuardianChannels: () => null,
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
// Import AFTER mocks so the module under test binds to the stubbed
|
|
69
|
+
// implementations.
|
|
70
|
+
import { writeOnboardingSection } from "../prompts/persona-resolver.js";
|
|
71
|
+
|
|
72
|
+
// ── Temp workspace scaffold ───────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
let testRoot: string;
|
|
75
|
+
|
|
76
|
+
function workspacePath(file: string): string {
|
|
77
|
+
return join(mockWorkspaceDir, file);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
beforeAll(() => {
|
|
81
|
+
testRoot = mkdtempSync(join(tmpdir(), "onboarding-persona-write-test-"));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterAll(() => {
|
|
85
|
+
rmSync(testRoot, { recursive: true, force: true });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
mockWorkspaceDir = mkdtempSync(join(testRoot, "ws-"));
|
|
90
|
+
mockVellumGuardian = null;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
afterEach(() => {
|
|
94
|
+
rmSync(mockWorkspaceDir, { recursive: true, force: true });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ── Tests ─────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
describe("writeOnboardingSection", () => {
|
|
100
|
+
test("writes section to guardian persona file when it exists", () => {
|
|
101
|
+
mockVellumGuardian = {
|
|
102
|
+
contact: { userFile: "alice.md" },
|
|
103
|
+
channel: {},
|
|
104
|
+
};
|
|
105
|
+
const guardianPath = workspacePath("users/alice.md");
|
|
106
|
+
mkdirSync(workspacePath("users"), { recursive: true });
|
|
107
|
+
writeFileSync(guardianPath, "# User Profile\n\n- **Name:** Alice\n");
|
|
108
|
+
|
|
109
|
+
writeOnboardingSection({
|
|
110
|
+
preferredName: "Alice",
|
|
111
|
+
commonWork: ["builds code, apps, or tools", "plans and coordinates work"],
|
|
112
|
+
dailyTools: ["GitHub", "Linear", "Slack"],
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const content = readFileSync(guardianPath, "utf-8");
|
|
116
|
+
expect(content).toContain("- **Name:** Alice");
|
|
117
|
+
expect(content).toContain("## Onboarding Context");
|
|
118
|
+
expect(content).toContain("- **Preferred name:** Alice");
|
|
119
|
+
expect(content).toContain(
|
|
120
|
+
"- **Common work:** builds code, apps, or tools; plans and coordinates work",
|
|
121
|
+
);
|
|
122
|
+
expect(content).toContain("- **Daily tools:** GitHub, Linear, Slack");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("falls back to users/default.md when guardian path is null", () => {
|
|
126
|
+
mockVellumGuardian = null;
|
|
127
|
+
mkdirSync(workspacePath("users"), { recursive: true });
|
|
128
|
+
writeFileSync(
|
|
129
|
+
workspacePath("users/default.md"),
|
|
130
|
+
"# User Profile\n\n- **Name:** Default User\n",
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
writeOnboardingSection({
|
|
134
|
+
preferredName: "Alice",
|
|
135
|
+
commonWork: [],
|
|
136
|
+
dailyTools: ["Slack"],
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const content = readFileSync(workspacePath("users/default.md"), "utf-8");
|
|
140
|
+
expect(content).toContain("- **Name:** Default User");
|
|
141
|
+
expect(content).toContain("## Onboarding Context");
|
|
142
|
+
expect(content).toContain("- **Preferred name:** Alice");
|
|
143
|
+
expect(content).toContain("- **Daily tools:** Slack");
|
|
144
|
+
|
|
145
|
+
// USER.md should not be created
|
|
146
|
+
expect(existsSync(workspacePath("USER.md"))).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("falls back to USER.md when no users/ files exist", () => {
|
|
150
|
+
mockVellumGuardian = null;
|
|
151
|
+
|
|
152
|
+
writeOnboardingSection({
|
|
153
|
+
preferredName: "Alice",
|
|
154
|
+
commonWork: [],
|
|
155
|
+
dailyTools: [],
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(existsSync(workspacePath("USER.md"))).toBe(true);
|
|
159
|
+
const content = readFileSync(workspacePath("USER.md"), "utf-8");
|
|
160
|
+
expect(content).toContain("## Onboarding Context");
|
|
161
|
+
expect(content).toContain("- **Preferred name:** Alice");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("creates file with header + section when target doesn't exist", () => {
|
|
165
|
+
mockVellumGuardian = null;
|
|
166
|
+
|
|
167
|
+
writeOnboardingSection({
|
|
168
|
+
preferredName: "Alice",
|
|
169
|
+
commonWork: ["builds code, apps, or tools"],
|
|
170
|
+
dailyTools: ["GitHub", "Linear", "Slack"],
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const content = readFileSync(workspacePath("USER.md"), "utf-8");
|
|
174
|
+
expect(content).toContain("# User Profile");
|
|
175
|
+
expect(content).toContain("## Onboarding Context");
|
|
176
|
+
expect(content).toContain("- **Preferred name:** Alice");
|
|
177
|
+
expect(content).toContain("- **Common work:** builds code, apps, or tools");
|
|
178
|
+
expect(content).toContain("- **Daily tools:** GitHub, Linear, Slack");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("idempotent: calling twice produces the same file content", () => {
|
|
182
|
+
mockVellumGuardian = null;
|
|
183
|
+
const normalized = {
|
|
184
|
+
preferredName: "Alice",
|
|
185
|
+
commonWork: ["builds code, apps, or tools"],
|
|
186
|
+
dailyTools: ["GitHub", "Linear"],
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
writeOnboardingSection(normalized);
|
|
190
|
+
const first = readFileSync(workspacePath("USER.md"), "utf-8");
|
|
191
|
+
|
|
192
|
+
writeOnboardingSection(normalized);
|
|
193
|
+
const second = readFileSync(workspacePath("USER.md"), "utf-8");
|
|
194
|
+
|
|
195
|
+
expect(first).toBe(second);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("replaces existing onboarding section with updated data", () => {
|
|
199
|
+
mockVellumGuardian = null;
|
|
200
|
+
|
|
201
|
+
writeOnboardingSection({
|
|
202
|
+
preferredName: "Alice",
|
|
203
|
+
commonWork: ["builds code, apps, or tools"],
|
|
204
|
+
dailyTools: ["GitHub"],
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
writeOnboardingSection({
|
|
208
|
+
preferredName: "Bob",
|
|
209
|
+
commonWork: ["writes docs, emails, or content"],
|
|
210
|
+
dailyTools: ["Notion", "Slack"],
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const content = readFileSync(workspacePath("USER.md"), "utf-8");
|
|
214
|
+
expect(content).toContain("- **Preferred name:** Bob");
|
|
215
|
+
expect(content).toContain(
|
|
216
|
+
"- **Common work:** writes docs, emails, or content",
|
|
217
|
+
);
|
|
218
|
+
expect(content).toContain("- **Daily tools:** Notion, Slack");
|
|
219
|
+
// Old values should be gone
|
|
220
|
+
expect(content).not.toContain("**Preferred name:** Alice");
|
|
221
|
+
expect(content).not.toContain("GitHub");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("preserves content outside the managed section", () => {
|
|
225
|
+
mockVellumGuardian = null;
|
|
226
|
+
writeFileSync(
|
|
227
|
+
workspacePath("USER.md"),
|
|
228
|
+
"# User Profile\n\n- **Name:** Alice\n- **Role:** Engineer\n",
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
writeOnboardingSection({
|
|
232
|
+
preferredName: "Alice",
|
|
233
|
+
commonWork: [],
|
|
234
|
+
dailyTools: ["GitHub"],
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const content = readFileSync(workspacePath("USER.md"), "utf-8");
|
|
238
|
+
expect(content).toContain("- **Name:** Alice");
|
|
239
|
+
expect(content).toContain("- **Role:** Engineer");
|
|
240
|
+
expect(content).toContain("## Onboarding Context");
|
|
241
|
+
expect(content).toContain("- **Daily tools:** GitHub");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("omits empty fields", () => {
|
|
245
|
+
mockVellumGuardian = null;
|
|
246
|
+
|
|
247
|
+
writeOnboardingSection({
|
|
248
|
+
commonWork: [],
|
|
249
|
+
dailyTools: [],
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const content = readFileSync(workspacePath("USER.md"), "utf-8");
|
|
253
|
+
expect(content).toContain("## Onboarding Context");
|
|
254
|
+
expect(content).not.toContain("Preferred name");
|
|
255
|
+
expect(content).not.toContain("Common work");
|
|
256
|
+
expect(content).not.toContain("Daily tools");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("omits preferredName when undefined", () => {
|
|
260
|
+
mockVellumGuardian = null;
|
|
261
|
+
|
|
262
|
+
writeOnboardingSection({
|
|
263
|
+
preferredName: undefined,
|
|
264
|
+
commonWork: ["builds code, apps, or tools"],
|
|
265
|
+
dailyTools: ["GitHub"],
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const content = readFileSync(workspacePath("USER.md"), "utf-8");
|
|
269
|
+
expect(content).not.toContain("Preferred name");
|
|
270
|
+
expect(content).toContain("- **Common work:** builds code, apps, or tools");
|
|
271
|
+
expect(content).toContain("- **Daily tools:** GitHub");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("preserves content after onboarding section when followed by another heading", () => {
|
|
275
|
+
mockVellumGuardian = null;
|
|
276
|
+
writeFileSync(
|
|
277
|
+
workspacePath("USER.md"),
|
|
278
|
+
[
|
|
279
|
+
"# User Profile",
|
|
280
|
+
"",
|
|
281
|
+
"- **Name:** Alice",
|
|
282
|
+
"",
|
|
283
|
+
"## Onboarding Context",
|
|
284
|
+
"",
|
|
285
|
+
"- **Preferred name:** Alice",
|
|
286
|
+
"",
|
|
287
|
+
"## Preferences",
|
|
288
|
+
"",
|
|
289
|
+
"- Likes dark mode",
|
|
290
|
+
"",
|
|
291
|
+
].join("\n"),
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
writeOnboardingSection({
|
|
295
|
+
preferredName: "Bob",
|
|
296
|
+
commonWork: [],
|
|
297
|
+
dailyTools: ["Slack"],
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const content = readFileSync(workspacePath("USER.md"), "utf-8");
|
|
301
|
+
expect(content).toContain("- **Preferred name:** Bob");
|
|
302
|
+
expect(content).toContain("- **Daily tools:** Slack");
|
|
303
|
+
expect(content).toContain("## Preferences");
|
|
304
|
+
expect(content).toContain("- Likes dark mode");
|
|
305
|
+
// Old preferred name should be gone from the onboarding section
|
|
306
|
+
expect(content).not.toContain("**Preferred name:** Alice");
|
|
307
|
+
});
|
|
308
|
+
});
|