@vellumai/assistant 0.5.6 → 0.5.7
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/.env.example +16 -2
- package/ARCHITECTURE.md +6 -75
- package/Dockerfile +1 -1
- package/README.md +0 -2
- package/bun.lock +0 -414
- package/docs/architecture/keychain-broker.md +45 -240
- package/docs/architecture/security.md +0 -17
- package/docs/credential-execution-service.md +2 -2
- package/node_modules/@vellumai/ces-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +119 -0
- package/node_modules/@vellumai/credential-storage/package.json +1 -0
- package/node_modules/@vellumai/egress-proxy/package.json +1 -0
- package/package.json +2 -3
- package/src/__tests__/actor-token-service.test.ts +0 -114
- package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
- package/src/__tests__/browser-skill-endstate.test.ts +6 -5
- package/src/__tests__/btw-routes.test.ts +0 -39
- package/src/__tests__/call-domain.test.ts +0 -128
- package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
- package/src/__tests__/channel-approval-routes.test.ts +0 -5
- package/src/__tests__/channel-readiness-service.test.ts +1 -60
- package/src/__tests__/checker.test.ts +4 -2
- package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -1
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
- package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
- package/src/__tests__/conversation-skill-tools.test.ts +0 -54
- package/src/__tests__/conversation-title-service.test.ts +87 -0
- package/src/__tests__/credential-execution-feature-gates.test.ts +28 -14
- package/src/__tests__/credential-execution-managed-contract.test.ts +33 -18
- package/src/__tests__/credential-security-e2e.test.ts +0 -66
- package/src/__tests__/credential-security-invariants.test.ts +4 -45
- package/src/__tests__/credentials-cli.test.ts +78 -0
- package/src/__tests__/db-migration-rollback.test.ts +2015 -1
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
- package/src/__tests__/guardian-routing-state.test.ts +0 -5
- package/src/__tests__/host-shell-tool.test.ts +6 -7
- package/src/__tests__/http-user-message-parity.test.ts +3 -103
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
- package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
- package/src/__tests__/intent-routing.test.ts +0 -13
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
- package/src/__tests__/keychain-broker-client.test.ts +161 -22
- package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
- package/src/__tests__/migration-export-http.test.ts +2 -2
- package/src/__tests__/migration-import-commit-http.test.ts +2 -2
- package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
- package/src/__tests__/migration-validate-http.test.ts +2 -2
- package/src/__tests__/non-member-access-request.test.ts +0 -5
- package/src/__tests__/notification-decision-fallback.test.ts +4 -0
- package/src/__tests__/notification-decision-identity.test.ts +4 -0
- package/src/__tests__/permission-types.test.ts +1 -0
- package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
- package/src/__tests__/qdrant-manager.test.ts +28 -2
- package/src/__tests__/registry.test.ts +0 -6
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
- package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
- package/src/__tests__/secure-keys.test.ts +83 -263
- package/src/__tests__/shell-identity.test.ts +96 -6
- package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
- package/src/__tests__/skill-feature-flags.test.ts +46 -45
- package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
- package/src/__tests__/skill-load-inline-command.test.ts +8 -12
- package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
- package/src/__tests__/skill-load-tool.test.ts +0 -2
- package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
- package/src/__tests__/skills.test.ts +0 -2
- package/src/__tests__/slack-inbound-verification.test.ts +0 -4
- package/src/__tests__/suggestion-routes.test.ts +1 -32
- package/src/__tests__/system-prompt.test.ts +0 -1
- package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
- package/src/__tests__/update-bulletin.test.ts +0 -2
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -6
- package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
- package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +218 -0
- package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
- package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
- package/src/calls/audio-store.test.ts +97 -0
- package/src/calls/audio-store.ts +205 -0
- package/src/calls/call-controller.ts +85 -7
- package/src/calls/call-domain.ts +3 -0
- package/src/calls/call-store.ts +10 -3
- package/src/calls/fish-audio-client.ts +117 -0
- package/src/calls/relay-server.ts +27 -0
- package/src/calls/twilio-routes.ts +2 -1
- package/src/calls/types.ts +1 -0
- package/src/calls/voice-ingress-preflight.ts +0 -42
- package/src/calls/voice-quality.ts +26 -5
- package/src/calls/voice-session-bridge.ts +6 -12
- package/src/cli/commands/config.ts +1 -4
- package/src/cli/commands/credentials.ts +34 -4
- package/src/cli/commands/oauth/index.ts +7 -0
- package/src/cli/commands/oauth/platform.ts +179 -0
- package/src/cli/commands/platform.ts +3 -3
- package/src/config/assistant-feature-flags.ts +186 -5
- package/src/config/bundled-skills/messaging/SKILL.md +5 -5
- package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
- package/src/config/bundled-skills/settings/TOOLS.json +2 -2
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
- package/src/config/bundled-tool-registry.ts +1 -11
- package/src/config/env-registry.ts +1 -1
- package/src/config/env.ts +8 -14
- package/src/config/feature-flag-registry.json +48 -8
- package/src/config/loader.ts +98 -31
- package/src/config/schema.ts +4 -13
- package/src/config/schemas/calls.ts +13 -0
- package/src/config/schemas/fish-audio.ts +39 -0
- package/src/config/schemas/security.ts +0 -4
- package/src/config/types.ts +0 -1
- package/src/contacts/contact-store.ts +39 -0
- package/src/contacts/types.ts +2 -0
- package/src/credential-execution/approval-bridge.ts +1 -0
- package/src/credential-execution/executable-discovery.ts +28 -4
- package/src/credential-execution/feature-gates.ts +16 -0
- package/src/credential-execution/process-manager.ts +38 -0
- package/src/daemon/assistant-attachments.ts +9 -0
- package/src/daemon/config-watcher.ts +5 -0
- package/src/daemon/conversation-tool-setup.ts +0 -105
- package/src/daemon/conversation.ts +10 -1
- package/src/daemon/handlers/config-vercel.ts +92 -0
- package/src/daemon/handlers/skills.ts +2 -15
- package/src/daemon/install-symlink.ts +195 -0
- package/src/daemon/lifecycle.ts +227 -51
- package/src/daemon/message-types/conversations.ts +3 -4
- package/src/daemon/message-types/diagnostics.ts +3 -22
- package/src/daemon/message-types/messages.ts +0 -2
- package/src/daemon/message-types/upgrades.ts +8 -0
- package/src/daemon/server.ts +30 -92
- package/src/events/domain-events.ts +2 -1
- package/src/inbound/platform-callback-registration.ts +3 -3
- package/src/instrument.ts +8 -5
- package/src/memory/conversation-title-service.ts +50 -1
- package/src/memory/db-init.ts +12 -0
- package/src/memory/items-extractor.ts +15 -1
- package/src/memory/job-handlers/conversation-starters.ts +4 -1
- package/src/memory/jobs-store.ts +30 -5
- package/src/memory/jobs-worker.ts +31 -7
- package/src/memory/migrations/001-job-deferrals.ts +19 -0
- package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
- package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
- package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
- package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
- package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
- package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
- package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
- package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
- package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
- package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
- package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
- package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
- package/src/memory/migrations/116-messages-fts.ts +106 -1
- package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
- package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
- package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
- package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
- package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
- package/src/memory/migrations/141-rename-verification-table.ts +54 -0
- package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
- package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
- package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
- package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
- package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
- package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
- package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
- package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
- package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
- package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
- package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
- package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
- package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
- package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/migrations/registry.ts +90 -0
- package/src/memory/migrations/validate-migration-state.ts +137 -11
- package/src/memory/qdrant-circuit-breaker.ts +9 -0
- package/src/memory/qdrant-manager.ts +64 -7
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/contacts.ts +1 -0
- package/src/notifications/decision-engine.ts +4 -1
- package/src/oauth/connection-resolver.ts +6 -4
- package/src/permissions/checker.ts +0 -38
- package/src/permissions/shell-identity.ts +76 -22
- package/src/permissions/types.ts +4 -2
- package/src/platform/client.ts +35 -7
- package/src/prompts/persona-resolver.ts +138 -0
- package/src/prompts/system-prompt.ts +36 -4
- package/src/prompts/templates/users/default.md +1 -0
- package/src/providers/registry.ts +27 -40
- package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
- package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
- package/src/runtime/auth/external-assistant-id.ts +13 -59
- package/src/runtime/auth/route-policy.ts +15 -1
- package/src/runtime/auth/token-service.ts +43 -138
- package/src/runtime/channel-readiness-service.ts +1 -16
- package/src/runtime/http-server.ts +27 -2
- package/src/runtime/middleware/error-handler.ts +1 -9
- package/src/runtime/routes/audio-routes.ts +40 -0
- package/src/runtime/routes/btw-routes.ts +0 -17
- package/src/runtime/routes/conversation-query-routes.ts +63 -1
- package/src/runtime/routes/conversation-routes.ts +4 -44
- package/src/runtime/routes/diagnostics-routes.ts +1 -477
- package/src/runtime/routes/identity-routes.ts +18 -29
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
- package/src/runtime/routes/integrations/vercel.ts +89 -0
- package/src/runtime/routes/log-export-routes.ts +5 -0
- package/src/runtime/routes/memory-item-routes.ts +24 -6
- package/src/runtime/routes/migration-rollback-routes.ts +209 -0
- package/src/runtime/routes/migration-routes.ts +17 -1
- package/src/runtime/routes/notification-routes.ts +58 -0
- package/src/runtime/routes/schedule-routes.ts +65 -0
- package/src/runtime/routes/settings-routes.ts +41 -1
- package/src/runtime/routes/tts-routes.ts +86 -0
- package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
- package/src/runtime/routes/workspace-commit-routes.ts +62 -0
- package/src/runtime/routes/workspace-routes.test.ts +22 -1
- package/src/runtime/routes/workspace-routes.ts +1 -1
- package/src/runtime/routes/workspace-utils.ts +86 -2
- package/src/security/ces-credential-client.ts +59 -22
- package/src/security/ces-rpc-credential-backend.ts +85 -0
- package/src/security/credential-backend.ts +12 -88
- package/src/security/keychain-broker-client.ts +10 -2
- package/src/security/secure-keys.ts +94 -113
- package/src/skills/catalog-install.ts +13 -7
- package/src/telemetry/usage-telemetry-reporter.ts +4 -2
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/executor.ts +0 -4
- package/src/tools/network/script-proxy/session-manager.ts +19 -4
- package/src/tools/network/web-fetch.ts +3 -1
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/types.ts +0 -8
- package/src/util/errors.ts +0 -12
- package/src/util/platform.ts +3 -50
- package/src/workspace/git-service.ts +5 -2
- package/src/workspace/migrations/001-avatar-rename.ts +15 -0
- package/src/workspace/migrations/003-seed-device-id.ts +17 -1
- package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
- package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
- package/src/workspace/migrations/006-services-config.ts +49 -0
- package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
- package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
- package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
- package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
- package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
- package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
- package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
- package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
- package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
- package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
- package/src/workspace/migrations/017-seed-persona-dirs.ts +95 -0
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +23 -1
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/workspace/migrations/runner.ts +106 -2
- package/src/workspace/migrations/types.ts +4 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
- package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
- package/src/__tests__/diagnostics-export.test.ts +0 -288
- package/src/__tests__/local-gateway-health.test.ts +0 -209
- package/src/__tests__/secret-ingress-handler.test.ts +0 -120
- package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
- package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
- package/src/__tests__/swarm-orchestrator.test.ts +0 -463
- package/src/__tests__/swarm-plan-validator.test.ts +0 -384
- package/src/__tests__/swarm-recursion.test.ts +0 -197
- package/src/__tests__/swarm-router-planner.test.ts +0 -234
- package/src/__tests__/swarm-tool.test.ts +0 -185
- package/src/__tests__/swarm-worker-backend.test.ts +0 -144
- package/src/__tests__/swarm-worker-runner.test.ts +0 -288
- package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
- package/src/commands/cc-command-registry.ts +0 -248
- package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
- package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
- package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
- package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
- package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
- package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
- package/src/config/schemas/swarm.ts +0 -82
- package/src/logfire.ts +0 -135
- package/src/runtime/local-gateway-health.ts +0 -275
- package/src/security/secret-ingress.ts +0 -68
- package/src/swarm/backend-claude-code.ts +0 -225
- package/src/swarm/checkpoint.ts +0 -137
- package/src/swarm/graph-utils.ts +0 -53
- package/src/swarm/index.ts +0 -55
- package/src/swarm/limits.ts +0 -66
- package/src/swarm/orchestrator.ts +0 -424
- package/src/swarm/plan-validator.ts +0 -117
- package/src/swarm/router-planner.ts +0 -162
- package/src/swarm/router-prompts.ts +0 -39
- package/src/swarm/synthesizer.ts +0 -81
- package/src/swarm/types.ts +0 -72
- package/src/swarm/worker-backend.ts +0 -131
- package/src/swarm/worker-prompts.ts +0 -80
- package/src/swarm/worker-runner.ts +0 -170
- package/src/tools/claude-code/claude-code.ts +0 -610
- package/src/tools/swarm/delegate.ts +0 -205
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* tools, even when conversation history contains old markers for those skills.
|
|
4
4
|
*/
|
|
5
5
|
import * as realFs from "node:fs";
|
|
6
|
-
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
7
7
|
|
|
8
8
|
import type { SkillSummary, SkillToolManifest } from "../config/skills.js";
|
|
9
9
|
import { RiskLevel } from "../permissions/types.js";
|
|
@@ -38,23 +38,6 @@ mock.module("../config/loader.js", () => ({
|
|
|
38
38
|
invalidateConfigCache: () => {},
|
|
39
39
|
}));
|
|
40
40
|
|
|
41
|
-
mock.module("../config/assistant-feature-flags.js", () => ({
|
|
42
|
-
isAssistantFeatureFlagEnabled: (
|
|
43
|
-
key: string,
|
|
44
|
-
config: Record<string, unknown>,
|
|
45
|
-
) => {
|
|
46
|
-
const vals = (
|
|
47
|
-
config as {
|
|
48
|
-
assistantFeatureFlagValues?: Record<string, boolean>;
|
|
49
|
-
}
|
|
50
|
-
).assistantFeatureFlagValues;
|
|
51
|
-
if (vals && typeof vals[key] === "boolean") return vals[key];
|
|
52
|
-
return true; // default enabled
|
|
53
|
-
},
|
|
54
|
-
loadDefaultsRegistry: () => ({}),
|
|
55
|
-
getAssistantFeatureFlagDefaults: () => ({}),
|
|
56
|
-
}));
|
|
57
|
-
|
|
58
41
|
mock.module("../config/skill-state.js", () => ({
|
|
59
42
|
skillFlagKey: (skill: { featureFlag?: string }) =>
|
|
60
43
|
skill.featureFlag
|
|
@@ -62,6 +45,24 @@ mock.module("../config/skill-state.js", () => ({
|
|
|
62
45
|
: undefined,
|
|
63
46
|
}));
|
|
64
47
|
|
|
48
|
+
// Mock assistant-feature-flags to avoid loading the real module (which
|
|
49
|
+
// triggers file I/O and env-registry imports that hang in test context).
|
|
50
|
+
let _mockOverrides: Record<string, boolean> = {};
|
|
51
|
+
mock.module("../config/assistant-feature-flags.js", () => ({
|
|
52
|
+
isAssistantFeatureFlagEnabled: (key: string, _config: unknown): boolean => {
|
|
53
|
+
const explicit = _mockOverrides[key];
|
|
54
|
+
if (typeof explicit === "boolean") return explicit;
|
|
55
|
+
return true; // undeclared flags default to enabled
|
|
56
|
+
},
|
|
57
|
+
clearFeatureFlagOverridesCache: () => {
|
|
58
|
+
_mockOverrides = {};
|
|
59
|
+
},
|
|
60
|
+
_setOverridesForTesting: (overrides: Record<string, boolean>) => {
|
|
61
|
+
_mockOverrides = { ...overrides };
|
|
62
|
+
},
|
|
63
|
+
getAssistantFeatureFlagDefaults: () => ({}),
|
|
64
|
+
}));
|
|
65
|
+
|
|
65
66
|
mock.module("../skills/active-skill-tools.js", () => {
|
|
66
67
|
const parseMarkers = (messages: Message[]) => {
|
|
67
68
|
const skillLoadUseIds = new Set<string>();
|
|
@@ -216,6 +217,10 @@ mock.module("../util/logger.js", () => ({
|
|
|
216
217
|
|
|
217
218
|
const { projectSkillTools, resetSkillToolProjection } =
|
|
218
219
|
await import("../daemon/conversation-skill-tools.js");
|
|
220
|
+
const { _setOverridesForTesting } =
|
|
221
|
+
(await import("../config/assistant-feature-flags.js")) as {
|
|
222
|
+
_setOverridesForTesting: (o: Record<string, boolean>) => void;
|
|
223
|
+
};
|
|
219
224
|
|
|
220
225
|
// ---------------------------------------------------------------------------
|
|
221
226
|
// Helpers
|
|
@@ -289,9 +294,14 @@ describe("projectSkillTools feature flag enforcement", () => {
|
|
|
289
294
|
mockUnregisteredSkillIds = [];
|
|
290
295
|
mockSkillRefCount = new Map();
|
|
291
296
|
currentConfig = {};
|
|
297
|
+
_setOverridesForTesting({});
|
|
292
298
|
resetSkillToolProjection();
|
|
293
299
|
});
|
|
294
300
|
|
|
301
|
+
afterEach(() => {
|
|
302
|
+
_setOverridesForTesting({});
|
|
303
|
+
});
|
|
304
|
+
|
|
295
305
|
test("no skill tools projected for flag OFF skill even with old markers", () => {
|
|
296
306
|
mockCatalog = [makeSkill(DECLARED_SKILL_ID, DECLARED_SKILL_ID)];
|
|
297
307
|
mockManifests = {
|
|
@@ -302,10 +312,8 @@ describe("projectSkillTools feature flag enforcement", () => {
|
|
|
302
312
|
const history = buildHistoryWithMarker(DECLARED_SKILL_ID);
|
|
303
313
|
const prevActive = new Map<string, string>();
|
|
304
314
|
|
|
305
|
-
// Feature flag is OFF
|
|
306
|
-
|
|
307
|
-
assistantFeatureFlagValues: { [DECLARED_FLAG_KEY]: false },
|
|
308
|
-
};
|
|
315
|
+
// Feature flag is OFF — use protected directory override
|
|
316
|
+
_setOverridesForTesting({ [DECLARED_FLAG_KEY]: false });
|
|
309
317
|
|
|
310
318
|
const result = projectSkillTools(history, {
|
|
311
319
|
previouslyActiveSkillIds: prevActive,
|
|
@@ -325,10 +333,8 @@ describe("projectSkillTools feature flag enforcement", () => {
|
|
|
325
333
|
const history = buildHistoryWithMarker(DECLARED_SKILL_ID);
|
|
326
334
|
const prevActive = new Map<string, string>();
|
|
327
335
|
|
|
328
|
-
// Feature flag is ON
|
|
329
|
-
|
|
330
|
-
assistantFeatureFlagValues: { [DECLARED_FLAG_KEY]: true },
|
|
331
|
-
};
|
|
336
|
+
// Feature flag is ON — use protected directory override
|
|
337
|
+
_setOverridesForTesting({ [DECLARED_FLAG_KEY]: true });
|
|
332
338
|
|
|
333
339
|
const result = projectSkillTools(history, {
|
|
334
340
|
previouslyActiveSkillIds: prevActive,
|
|
@@ -419,9 +425,7 @@ describe("projectSkillTools feature flag enforcement", () => {
|
|
|
419
425
|
const prevActive = new Map<string, string>();
|
|
420
426
|
|
|
421
427
|
// Declared skill is OFF, plain-skill is undeclared with no persisted override so remains ON.
|
|
422
|
-
|
|
423
|
-
assistantFeatureFlagValues: { [DECLARED_FLAG_KEY]: false },
|
|
424
|
-
};
|
|
428
|
+
_setOverridesForTesting({ [DECLARED_FLAG_KEY]: false });
|
|
425
429
|
|
|
426
430
|
const result = projectSkillTools(history, {
|
|
427
431
|
previouslyActiveSkillIds: prevActive,
|
|
@@ -40,8 +40,6 @@ mock.module("../util/platform.js", () => ({
|
|
|
40
40
|
getWorkspacePromptPath: (file: string) => join(TEST_DIR, file),
|
|
41
41
|
readSessionToken: () => null,
|
|
42
42
|
normalizeAssistantId: (id: string) => id,
|
|
43
|
-
readLockfile: () => null,
|
|
44
|
-
writeLockfile: () => {},
|
|
45
43
|
}));
|
|
46
44
|
|
|
47
45
|
const noopLogger = {
|
|
@@ -38,10 +38,6 @@ mock.module("../util/logger.js", () => ({
|
|
|
38
38
|
}),
|
|
39
39
|
}));
|
|
40
40
|
|
|
41
|
-
mock.module("../security/secret-ingress.js", () => ({
|
|
42
|
-
checkIngressForSecrets: () => ({ blocked: false }),
|
|
43
|
-
}));
|
|
44
|
-
|
|
45
41
|
mock.module("../config/env.js", () => ({
|
|
46
42
|
isHttpAuthDisabled: () => true,
|
|
47
43
|
getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* Unit tests for the GET /v1/suggestion endpoint (handleGetSuggestion).
|
|
3
3
|
*
|
|
4
4
|
* Validates happy path, all null-return paths, caching, staleness check,
|
|
5
|
-
* quote stripping,
|
|
6
|
-
* and modelIntent verification.
|
|
5
|
+
* quote stripping, empty response rejection, and modelIntent verification.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
import { describe, expect, mock, test } from "bun:test";
|
|
@@ -293,36 +292,6 @@ describe("GET /v1/suggestion", () => {
|
|
|
293
292
|
expect(body.suggestion).toBe("Sure, let's go!");
|
|
294
293
|
});
|
|
295
294
|
|
|
296
|
-
test("truncates long suggestions at word boundary", async () => {
|
|
297
|
-
// A 60-char string that will exceed the 50-char limit
|
|
298
|
-
const longText =
|
|
299
|
-
"This is a really long suggestion that goes well beyond fifty chars";
|
|
300
|
-
const provider = makeMockProvider(longText);
|
|
301
|
-
mockGetConfiguredProvider.mockImplementation(async () => provider);
|
|
302
|
-
mockGetConversationByKey.mockImplementation(() => ({
|
|
303
|
-
conversationId: "conv-test",
|
|
304
|
-
}));
|
|
305
|
-
mockGetMessages.mockImplementation(() => [
|
|
306
|
-
{
|
|
307
|
-
id: "msg-asst-1",
|
|
308
|
-
conversationId: "conv-test",
|
|
309
|
-
role: "assistant",
|
|
310
|
-
content: JSON.stringify([{ type: "text", text: "Hello there" }]),
|
|
311
|
-
createdAt: Date.now(),
|
|
312
|
-
metadata: null,
|
|
313
|
-
},
|
|
314
|
-
]);
|
|
315
|
-
|
|
316
|
-
const url = makeUrl({ conversationKey: "test-key" });
|
|
317
|
-
const deps = makeDeps();
|
|
318
|
-
const res = await handleGetSuggestion(url, deps);
|
|
319
|
-
const body = (await res.json()) as { suggestion: string };
|
|
320
|
-
|
|
321
|
-
expect(body.suggestion.length).toBeLessThanOrEqual(50);
|
|
322
|
-
// Should end at a word boundary (no partial words)
|
|
323
|
-
expect(body.suggestion).not.toMatch(/\s$/);
|
|
324
|
-
});
|
|
325
|
-
|
|
326
295
|
test("rejects empty LLM response", async () => {
|
|
327
296
|
const provider = makeMockProvider("");
|
|
328
297
|
mockGetConfiguredProvider.mockImplementation(async () => provider);
|
|
@@ -330,7 +330,7 @@ describe("ToolExecutor → real shell allowlist integration", () => {
|
|
|
330
330
|
expect(patterns).toContain("action:git");
|
|
331
331
|
});
|
|
332
332
|
|
|
333
|
-
test("pipeline command produces
|
|
333
|
+
test("pipeline command produces exact + action-key options", async () => {
|
|
334
334
|
const { prompter, getAllowlist } = makeCapturingPrompter();
|
|
335
335
|
const executor = new ToolExecutor(prompter);
|
|
336
336
|
|
|
@@ -343,9 +343,11 @@ describe("ToolExecutor → real shell allowlist integration", () => {
|
|
|
343
343
|
const allowlist = getAllowlist();
|
|
344
344
|
expect(allowlist).toBeDefined();
|
|
345
345
|
|
|
346
|
-
// Pipelines
|
|
347
|
-
expect(allowlist!.length).
|
|
346
|
+
// Pipelines now produce exact option + action key options
|
|
347
|
+
expect(allowlist!.length).toBeGreaterThanOrEqual(2);
|
|
348
348
|
expect(allowlist![0].pattern).toBe("cat file.txt | grep error");
|
|
349
349
|
expect(allowlist![0].description).toContain("compound");
|
|
350
|
+
// Action keys from the first segment before the pipe
|
|
351
|
+
expect(allowlist!.some((o) => o.pattern.startsWith("action:"))).toBe(true);
|
|
350
352
|
});
|
|
351
353
|
});
|
|
@@ -41,11 +41,6 @@ mock.module("../util/logger.js", () => ({
|
|
|
41
41
|
}),
|
|
42
42
|
}));
|
|
43
43
|
|
|
44
|
-
// Mock security check to always pass
|
|
45
|
-
mock.module("../security/secret-ingress.js", () => ({
|
|
46
|
-
checkIngressForSecrets: () => ({ blocked: false }),
|
|
47
|
-
}));
|
|
48
|
-
|
|
49
44
|
mock.module("../config/env.js", () => ({
|
|
50
45
|
isHttpAuthDisabled: () => true,
|
|
51
46
|
getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
|
|
@@ -36,10 +36,6 @@ mock.module("../util/logger.js", () => ({
|
|
|
36
36
|
}),
|
|
37
37
|
}));
|
|
38
38
|
|
|
39
|
-
mock.module("../security/secret-ingress.js", () => ({
|
|
40
|
-
checkIngressForSecrets: () => ({ blocked: false }),
|
|
41
|
-
}));
|
|
42
|
-
|
|
43
39
|
mock.module("../config/env.js", () => ({
|
|
44
40
|
isHttpAuthDisabled: () => true,
|
|
45
41
|
getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
|
|
@@ -66,8 +66,6 @@ mock.module("../util/platform.js", () => ({
|
|
|
66
66
|
getSandboxWorkingDir: () => "",
|
|
67
67
|
getInterfacesDir: () => "",
|
|
68
68
|
getClipboardCommand: () => null,
|
|
69
|
-
readLockfile: () => null,
|
|
70
|
-
writeLockfile: () => {},
|
|
71
69
|
readPlatformToken: () => null,
|
|
72
70
|
readSessionToken: () => null,
|
|
73
71
|
getTCPPort: () => 8765,
|
|
@@ -61,9 +61,7 @@ const platformOverrides: Record<string, (...args: unknown[]) => unknown> = {
|
|
|
61
61
|
getSessionTokenPath: () => join(TEST_DIR, "session-token"),
|
|
62
62
|
readSessionToken: () => null,
|
|
63
63
|
getClipboardCommand: () => null,
|
|
64
|
-
readLockfile: () => null,
|
|
65
64
|
normalizeAssistantId: (id: unknown) => String(id),
|
|
66
|
-
writeLockfile: () => {},
|
|
67
65
|
getEmbeddingModelsDir: () => join(TEST_DIR, "embedding-models"),
|
|
68
66
|
getTCPPort: () => 8765,
|
|
69
67
|
isTCPEnabled: () => false,
|
|
@@ -141,7 +139,6 @@ interface TestConfig {
|
|
|
141
139
|
permissions: { mode: "strict" | "workspace" };
|
|
142
140
|
skills: { load: { extraDirs: string[] } };
|
|
143
141
|
sandbox: { enabled: boolean };
|
|
144
|
-
assistantFeatureFlagValues?: Record<string, boolean>;
|
|
145
142
|
[key: string]: unknown;
|
|
146
143
|
}
|
|
147
144
|
|
|
@@ -149,9 +146,6 @@ const testConfig: TestConfig = {
|
|
|
149
146
|
permissions: { mode: "workspace" },
|
|
150
147
|
skills: { load: { extraDirs: [] } },
|
|
151
148
|
sandbox: { enabled: true },
|
|
152
|
-
assistantFeatureFlagValues: {
|
|
153
|
-
"feature_flags.inline-skill-commands.enabled": true,
|
|
154
|
-
},
|
|
155
149
|
};
|
|
156
150
|
|
|
157
151
|
mock.module("../config/loader.js", () => ({
|
|
@@ -169,6 +163,8 @@ mock.module("../config/loader.js", () => ({
|
|
|
169
163
|
|
|
170
164
|
await import("../tools/skills/load.js");
|
|
171
165
|
const { getTool } = await import("../tools/registry.js");
|
|
166
|
+
const { _setOverridesForTesting } =
|
|
167
|
+
await import("../config/assistant-feature-flags.js");
|
|
172
168
|
|
|
173
169
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
174
170
|
|
|
@@ -228,16 +224,17 @@ describe("vellum-self-knowledge inline command expansion", () => {
|
|
|
228
224
|
) => mockRunInlineCommand(command, workingDir),
|
|
229
225
|
}));
|
|
230
226
|
|
|
231
|
-
// Enable the feature flag
|
|
232
|
-
|
|
227
|
+
// Enable the feature flag via protected directory override
|
|
228
|
+
_setOverridesForTesting({
|
|
233
229
|
"feature_flags.inline-skill-commands.enabled": true,
|
|
234
|
-
};
|
|
230
|
+
});
|
|
235
231
|
testConfig.skills = { load: { extraDirs: [] } };
|
|
236
232
|
|
|
237
233
|
installSelfKnowledgeSkill();
|
|
238
234
|
});
|
|
239
235
|
|
|
240
236
|
afterEach(() => {
|
|
237
|
+
_setOverridesForTesting({});
|
|
241
238
|
if (existsSync(TEST_DIR)) {
|
|
242
239
|
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
243
240
|
}
|
|
@@ -70,12 +70,6 @@ mock.module("../config/loader.js", () => ({
|
|
|
70
70
|
}),
|
|
71
71
|
}));
|
|
72
72
|
|
|
73
|
-
// ── Secret ingress mock ────────────────────────────────────────────
|
|
74
|
-
|
|
75
|
-
mock.module("../security/secret-ingress.js", () => ({
|
|
76
|
-
checkIngressForSecrets: () => ({ blocked: false }),
|
|
77
|
-
}));
|
|
78
|
-
|
|
79
73
|
// ── Assistant event hub mock ───────────────────────────────────────
|
|
80
74
|
|
|
81
75
|
mock.module("../runtime/assistant-event-hub.js", () => ({
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Mock state
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
const isAvailableFn = mock((): boolean => true);
|
|
8
|
+
const brokerSetFn = mock(
|
|
9
|
+
async (
|
|
10
|
+
_account: string,
|
|
11
|
+
_value: string,
|
|
12
|
+
): Promise<{ status: string; code?: string; message?: string }> => ({
|
|
13
|
+
status: "ok",
|
|
14
|
+
}),
|
|
15
|
+
);
|
|
16
|
+
const createBrokerClientFn = mock(() => ({
|
|
17
|
+
isAvailable: isAvailableFn,
|
|
18
|
+
set: brokerSetFn,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
const listKeysFn = mock((): string[] => []);
|
|
22
|
+
const getKeyFn = mock((_account: string): string | undefined => undefined);
|
|
23
|
+
const deleteKeyFn = mock(
|
|
24
|
+
(_account: string): "deleted" | "not-found" | "error" => "deleted",
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Mock modules — before importing module under test
|
|
29
|
+
//
|
|
30
|
+
// The logger is mocked with a silent Proxy to suppress pino output in tests.
|
|
31
|
+
// The broker client and encrypted store are mocked to control migration
|
|
32
|
+
// behavior without touching real keychain or filesystem state.
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
mock.module("../util/logger.js", () => ({
|
|
36
|
+
getLogger: () =>
|
|
37
|
+
new Proxy({} as Record<string, unknown>, {
|
|
38
|
+
get: () => () => {},
|
|
39
|
+
}),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
mock.module("../security/keychain-broker-client.js", () => ({
|
|
43
|
+
createBrokerClient: createBrokerClientFn,
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
mock.module("../security/encrypted-store.js", () => ({
|
|
47
|
+
listKeys: listKeysFn,
|
|
48
|
+
getKey: getKeyFn,
|
|
49
|
+
deleteKey: deleteKeyFn,
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
// Import after mocking
|
|
53
|
+
import { migrateCredentialsToKeychainMigration } from "../workspace/migrations/015-migrate-credentials-to-keychain.js";
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Helpers
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
const WORKSPACE_DIR = "/mock-home/.vellum/workspace";
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Tests
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
describe("015-migrate-credentials-to-keychain migration", () => {
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
isAvailableFn.mockClear();
|
|
68
|
+
brokerSetFn.mockClear();
|
|
69
|
+
createBrokerClientFn.mockClear();
|
|
70
|
+
listKeysFn.mockClear();
|
|
71
|
+
getKeyFn.mockClear();
|
|
72
|
+
deleteKeyFn.mockClear();
|
|
73
|
+
|
|
74
|
+
// Defaults: mac production build
|
|
75
|
+
process.env.VELLUM_DESKTOP_APP = "1";
|
|
76
|
+
delete process.env.VELLUM_DEV;
|
|
77
|
+
|
|
78
|
+
isAvailableFn.mockReturnValue(true);
|
|
79
|
+
brokerSetFn.mockResolvedValue({ status: "ok" });
|
|
80
|
+
listKeysFn.mockReturnValue([]);
|
|
81
|
+
getKeyFn.mockReturnValue(undefined);
|
|
82
|
+
deleteKeyFn.mockReturnValue("deleted");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("has correct migration id", () => {
|
|
86
|
+
expect(migrateCredentialsToKeychainMigration.id).toBe(
|
|
87
|
+
"015-migrate-credentials-to-keychain",
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("skips when VELLUM_DESKTOP_APP is not set", async () => {
|
|
92
|
+
delete process.env.VELLUM_DESKTOP_APP;
|
|
93
|
+
|
|
94
|
+
await migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR);
|
|
95
|
+
|
|
96
|
+
expect(createBrokerClientFn).not.toHaveBeenCalled();
|
|
97
|
+
expect(listKeysFn).not.toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("skips when VELLUM_DESKTOP_APP is not '1'", async () => {
|
|
101
|
+
process.env.VELLUM_DESKTOP_APP = "0";
|
|
102
|
+
|
|
103
|
+
await migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR);
|
|
104
|
+
|
|
105
|
+
expect(createBrokerClientFn).not.toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("skips when VELLUM_DEV=1", async () => {
|
|
109
|
+
process.env.VELLUM_DEV = "1";
|
|
110
|
+
|
|
111
|
+
await migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR);
|
|
112
|
+
|
|
113
|
+
expect(createBrokerClientFn).not.toHaveBeenCalled();
|
|
114
|
+
expect(listKeysFn).not.toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test(
|
|
118
|
+
"throws when broker is not available after max retry attempts",
|
|
119
|
+
async () => {
|
|
120
|
+
isAvailableFn.mockReturnValue(false);
|
|
121
|
+
|
|
122
|
+
await expect(
|
|
123
|
+
migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR),
|
|
124
|
+
).rejects.toThrow(
|
|
125
|
+
"Keychain broker not available after waiting — credential migration will be retried on next startup",
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// Should have retried isAvailable multiple times
|
|
129
|
+
expect(isAvailableFn.mock.calls.length).toBeGreaterThan(1);
|
|
130
|
+
|
|
131
|
+
// Should not proceed to list or migrate keys
|
|
132
|
+
expect(listKeysFn).not.toHaveBeenCalled();
|
|
133
|
+
expect(brokerSetFn).not.toHaveBeenCalled();
|
|
134
|
+
},
|
|
135
|
+
{ timeout: 10_000 },
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
test("succeeds when broker becomes available after retry", async () => {
|
|
139
|
+
// Broker unavailable for first 3 calls, then available
|
|
140
|
+
let callCount = 0;
|
|
141
|
+
isAvailableFn.mockImplementation(() => {
|
|
142
|
+
callCount++;
|
|
143
|
+
return callCount > 3;
|
|
144
|
+
});
|
|
145
|
+
listKeysFn.mockReturnValue(["retry-key"]);
|
|
146
|
+
getKeyFn.mockReturnValue("retry-secret");
|
|
147
|
+
brokerSetFn.mockResolvedValue({ status: "ok" });
|
|
148
|
+
|
|
149
|
+
await migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR);
|
|
150
|
+
|
|
151
|
+
// Should have called isAvailable 4 times (3 false + 1 true)
|
|
152
|
+
expect(isAvailableFn).toHaveBeenCalledTimes(4);
|
|
153
|
+
|
|
154
|
+
// Should have proceeded with migration
|
|
155
|
+
expect(brokerSetFn).toHaveBeenCalledWith("retry-key", "retry-secret");
|
|
156
|
+
expect(deleteKeyFn).toHaveBeenCalledWith("retry-key");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("no-ops when encrypted store has no keys", async () => {
|
|
160
|
+
listKeysFn.mockReturnValue([]);
|
|
161
|
+
|
|
162
|
+
await migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR);
|
|
163
|
+
|
|
164
|
+
expect(brokerSetFn).not.toHaveBeenCalled();
|
|
165
|
+
expect(deleteKeyFn).not.toHaveBeenCalled();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("successfully migrates keys from encrypted store to keychain", async () => {
|
|
169
|
+
listKeysFn.mockReturnValue(["account-a", "account-b"]);
|
|
170
|
+
getKeyFn.mockImplementation((account: string) => {
|
|
171
|
+
if (account === "account-a") return "secret-a";
|
|
172
|
+
if (account === "account-b") return "secret-b";
|
|
173
|
+
return undefined;
|
|
174
|
+
});
|
|
175
|
+
brokerSetFn.mockResolvedValue({ status: "ok" });
|
|
176
|
+
|
|
177
|
+
await migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR);
|
|
178
|
+
|
|
179
|
+
// Should have called broker.set for each key
|
|
180
|
+
expect(brokerSetFn).toHaveBeenCalledTimes(2);
|
|
181
|
+
expect(brokerSetFn).toHaveBeenCalledWith("account-a", "secret-a");
|
|
182
|
+
expect(brokerSetFn).toHaveBeenCalledWith("account-b", "secret-b");
|
|
183
|
+
|
|
184
|
+
// Should have deleted each key from encrypted store after successful migration
|
|
185
|
+
expect(deleteKeyFn).toHaveBeenCalledTimes(2);
|
|
186
|
+
expect(deleteKeyFn).toHaveBeenCalledWith("account-a");
|
|
187
|
+
expect(deleteKeyFn).toHaveBeenCalledWith("account-b");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("continues on individual key failure and migrates others", async () => {
|
|
191
|
+
listKeysFn.mockReturnValue(["fail-key", "ok-key"]);
|
|
192
|
+
getKeyFn.mockImplementation((account: string) => {
|
|
193
|
+
if (account === "fail-key") return "fail-secret";
|
|
194
|
+
if (account === "ok-key") return "ok-secret";
|
|
195
|
+
return undefined;
|
|
196
|
+
});
|
|
197
|
+
brokerSetFn.mockImplementation(async (account: string) => {
|
|
198
|
+
if (account === "fail-key") {
|
|
199
|
+
return {
|
|
200
|
+
status: "rejected" as const,
|
|
201
|
+
code: "UNKNOWN",
|
|
202
|
+
message: "broker rejected",
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return { status: "ok" as const };
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
await migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR);
|
|
209
|
+
|
|
210
|
+
// fail-key should NOT have been deleted (broker rejected it)
|
|
211
|
+
expect(deleteKeyFn).not.toHaveBeenCalledWith("fail-key");
|
|
212
|
+
|
|
213
|
+
// ok-key should have been migrated and deleted
|
|
214
|
+
expect(brokerSetFn).toHaveBeenCalledWith("ok-key", "ok-secret");
|
|
215
|
+
expect(deleteKeyFn).toHaveBeenCalledWith("ok-key");
|
|
216
|
+
expect(deleteKeyFn).toHaveBeenCalledTimes(1);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("handles getKey returning undefined for a listed key", async () => {
|
|
220
|
+
listKeysFn.mockReturnValue(["ghost-key", "real-key"]);
|
|
221
|
+
getKeyFn.mockImplementation((account: string) => {
|
|
222
|
+
if (account === "ghost-key") return undefined;
|
|
223
|
+
if (account === "real-key") return "real-secret";
|
|
224
|
+
return undefined;
|
|
225
|
+
});
|
|
226
|
+
brokerSetFn.mockResolvedValue({ status: "ok" });
|
|
227
|
+
|
|
228
|
+
await migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR);
|
|
229
|
+
|
|
230
|
+
// ghost-key should not be sent to broker or deleted
|
|
231
|
+
expect(brokerSetFn).not.toHaveBeenCalledWith(
|
|
232
|
+
"ghost-key",
|
|
233
|
+
expect.anything(),
|
|
234
|
+
);
|
|
235
|
+
expect(deleteKeyFn).not.toHaveBeenCalledWith("ghost-key");
|
|
236
|
+
|
|
237
|
+
// real-key should be migrated
|
|
238
|
+
expect(brokerSetFn).toHaveBeenCalledWith("real-key", "real-secret");
|
|
239
|
+
expect(deleteKeyFn).toHaveBeenCalledWith("real-key");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("handles broker unreachable status for individual keys", async () => {
|
|
243
|
+
listKeysFn.mockReturnValue(["key-1"]);
|
|
244
|
+
getKeyFn.mockReturnValue("secret-1");
|
|
245
|
+
brokerSetFn.mockResolvedValue({ status: "unreachable" });
|
|
246
|
+
|
|
247
|
+
await migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR);
|
|
248
|
+
|
|
249
|
+
// Should not delete when broker is unreachable
|
|
250
|
+
expect(deleteKeyFn).not.toHaveBeenCalled();
|
|
251
|
+
});
|
|
252
|
+
});
|