@vellumai/assistant 0.9.0 → 0.9.1-staging.1
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 +18 -34
- package/bun.lock +7 -8
- package/docs/activation-funnel-telemetry.md +4 -4
- package/docs/architecture/security.md +29 -28
- package/docs/stt-provider-onboarding.md +3 -5
- package/docs/workflows-testing.md +13 -44
- package/docs/workflows.md +3 -5
- package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
- package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
- package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
- package/node_modules/@vellumai/gateway-client/src/index.ts +17 -6
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
- package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
- package/openapi.yaml +135 -59
- package/package.json +2 -1
- package/scripts/sync-llm-catalog.ts +6 -15
- package/scripts/sync-web-search-catalog.ts +3 -11
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +14 -26
- package/src/__tests__/agent-loop-compaction-strip.test.ts +240 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
- package/src/__tests__/app-dir-path-guard.test.ts +0 -1
- package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
- package/src/__tests__/avatar-identity-sync.test.ts +2 -27
- package/src/__tests__/btw-routes.test.ts +6 -8
- package/src/__tests__/checker.test.ts +0 -3
- package/src/__tests__/config-loader-backfill.test.ts +103 -6
- package/src/__tests__/config-watcher.test.ts +0 -18
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
- package/src/__tests__/credential-broker.test.ts +449 -1
- package/src/__tests__/credential-execution-tools.test.ts +0 -1
- package/src/__tests__/credential-prompt-route.test.ts +3 -4
- package/src/__tests__/credential-routes.test.ts +360 -0
- package/src/__tests__/credential-security-invariants.test.ts +2 -13
- package/src/__tests__/dynamic-page-surface.test.ts +101 -1
- package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
- package/src/__tests__/gateway-only-guard.test.ts +3 -7
- package/src/__tests__/identity-routes.test.ts +0 -189
- package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
- package/src/__tests__/invite-redemption-service.test.ts +4 -4
- package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
- package/src/__tests__/llm-catalog-parity.test.ts +0 -22
- package/src/__tests__/llm-resolver.test.ts +49 -24
- package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
- package/src/__tests__/onboarding-persona-write.test.ts +1 -1
- package/src/__tests__/persona-resolver.test.ts +11 -14
- package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
- package/src/__tests__/registry.test.ts +2 -7
- package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
- package/src/__tests__/schedule-routes.test.ts +0 -30
- package/src/__tests__/schedule-tools.test.ts +2 -18
- package/src/__tests__/skill-execute-input.test.ts +46 -1
- package/src/__tests__/skill-runtime-path.test.ts +2 -3
- package/src/__tests__/subagent-tools.test.ts +116 -0
- package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
- package/src/__tests__/token-manager.test.ts +519 -0
- package/src/__tests__/tool-executor.test.ts +0 -79
- package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
- package/src/__tests__/trusted-contact-verification.test.ts +6 -6
- package/src/__tests__/voice-invite-redemption.test.ts +2 -2
- package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
- package/src/__tests__/workspace-greetings.test.ts +152 -0
- package/src/agent/loop.ts +25 -5
- package/src/api/README.md +6 -6
- package/src/api/responses/conversation-message.ts +2 -4
- package/src/api/responses/home.ts +0 -4
- package/src/approvals/guardian-request-resolvers.ts +2 -2
- package/src/calls/relay-access-wait.ts +1 -1
- package/src/calls/voice-session-bridge.ts +2 -2
- package/src/cli/commands/plugins.ts +143 -2
- package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +313 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +253 -2
- package/src/cli/lib/diff-plugin.ts +346 -0
- package/src/cli/lib/install-from-github.ts +105 -17
- package/src/cli/lib/merge-plugin-tree.ts +228 -0
- package/src/cli/lib/plugin-fingerprint.ts +14 -0
- package/src/cli/lib/upgrade-plugin.ts +270 -10
- package/src/cli/program.ts +0 -2
- package/src/config/bundled-skills/subagent/SKILL.md +4 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
- package/src/config/bundled-skills/workflows/SKILL.md +0 -1
- package/src/config/bundled-tool-registry.ts +2 -7
- package/src/config/call-site-defaults.ts +12 -2
- package/src/config/feature-flag-registry.json +1 -17
- package/src/config/inference-profile-validation.ts +26 -0
- package/src/config/loader.ts +4 -0
- package/src/config/profile-order.ts +28 -0
- package/src/config/schemas/elevenlabs.ts +0 -1
- package/src/config/schemas/platform.ts +0 -8
- package/src/config/seed-inference-profiles.ts +25 -20
- package/src/contacts/contact-store.ts +87 -96
- package/src/contacts/contacts-write.ts +5 -21
- package/src/context/compactor.ts +2 -2
- package/src/credential-execution/process-manager.ts +55 -14
- package/src/credential-execution/prompted-credential.ts +2 -3
- package/src/daemon/config-watcher.ts +0 -4
- package/src/daemon/conversation-agent-loop.ts +15 -4
- package/src/daemon/conversation-slash.ts +2 -23
- package/src/daemon/conversation-tool-setup.ts +11 -3
- package/src/daemon/conversation.ts +2 -0
- package/src/daemon/handlers/config-channels.ts +20 -16
- package/src/daemon/handlers/config-slack-channel.ts +2 -3
- package/src/daemon/lifecycle.ts +0 -7
- package/src/daemon/message-types/conversations.ts +3 -3
- package/src/daemon/message-types/sync.ts +0 -1
- package/src/daemon/orphan-reaper.test.ts +0 -19
- package/src/daemon/orphan-reaper.ts +2 -24
- package/src/daemon/server.ts +0 -10
- package/src/home/relationship-state.ts +2 -4
- package/src/memory/__tests__/memory-retrospective-job.test.ts +195 -401
- package/src/memory/bookmark-crud.ts +1 -2
- package/src/memory/db-init.ts +7 -17
- package/src/memory/embedding-backend.ts +23 -0
- package/src/memory/embedding-billing-breaker.ts +96 -0
- package/src/memory/jobs-store.ts +25 -13
- package/src/memory/jobs-worker.ts +52 -0
- package/src/memory/memory-retrospective-constants.ts +4 -4
- package/src/memory/memory-retrospective-job.ts +19 -227
- package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +62 -0
- package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +311 -0
- package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/run-migrations.ts +41 -0
- package/src/memory/migrations/validate-migration-state.ts +1 -1
- package/src/memory/schema/contacts.ts +0 -4
- package/src/messaging/providers/slack/adapter.ts +1 -1
- package/src/notifications/adapters/shared.ts +29 -0
- package/src/notifications/adapters/slack.ts +5 -32
- package/src/notifications/adapters/telegram.ts +2 -20
- package/src/notifications/broadcaster.ts +10 -1
- package/src/notifications/home-feed-side-effect.ts +4 -3
- package/src/notifications/notification-utils.ts +17 -19
- package/src/notifications/types.ts +7 -0
- package/src/oauth/AGENTS.md +5 -24
- package/src/plugin-api/constants.ts +1 -1
- package/src/plugin-api/index.ts +6 -1
- package/src/plugin-api/model-profiles.ts +33 -0
- package/src/plugin-api/types.ts +50 -2
- package/src/plugins/defaults/index.ts +25 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +54 -2
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +107 -7
- package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
- package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
- package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
- package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
- package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +1 -1
- package/src/prompts/persona-resolver.ts +2 -2
- package/src/runtime/AGENTS.md +0 -1
- package/src/runtime/actor-trust-resolver.ts +12 -44
- package/src/runtime/btw-sidechain.ts +3 -6
- package/src/runtime/channel-approval-types.ts +18 -45
- package/src/runtime/channel-invite-transports/telegram.ts +4 -4
- package/src/runtime/channel-verification-service.ts +4 -3
- package/src/runtime/invite-redemption-service.ts +3 -3
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +218 -1
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
- package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
- package/src/runtime/routes/btw-routes.ts +1 -27
- package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +2 -2
- package/src/runtime/routes/credential-routes.ts +40 -16
- package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
- package/src/runtime/routes/identity-routes.ts +1 -296
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +1 -1
- package/src/runtime/routes/plugins-routes.ts +171 -5
- package/src/runtime/routes/schedule-routes.ts +0 -22
- package/src/runtime/routes/workflow-routes.test.ts +4 -43
- package/src/runtime/routes/workflow-routes.ts +0 -28
- package/src/runtime/routes/workspace-greetings.ts +55 -0
- package/src/runtime/sync/resource-sync-events.ts +1 -11
- package/src/schedule/inference-profile.ts +2 -14
- package/src/subagent/manager.ts +6 -0
- package/src/subagent/types.ts +6 -0
- package/src/tools/AGENTS.md +3 -3
- package/src/tools/browser/browser-execution.ts +1 -1
- package/src/tools/network/web-search-error.ts +1 -1
- package/src/tools/permission-checker.ts +1 -1
- package/src/tools/schedule/create.ts +3 -9
- package/src/tools/schedule/update.ts +2 -10
- package/src/tools/side-effects.ts +2 -17
- package/src/tools/skills/execute.ts +34 -0
- package/src/tools/subagent/spawn.ts +34 -9
- package/src/tools/tool-approval-handler.ts +1 -3
- package/src/tools/tool-manifest.ts +0 -2
- package/src/tools/ui-surface/definitions.ts +39 -1
- package/src/tools/workflows/run-workflow.test.ts +8 -18
- package/src/util/platform.ts +2 -2
- package/src/workflows/capabilities.ts +2 -3
- package/src/workflows/run-manager.test.ts +0 -25
- package/src/workflows/run-manager.ts +2 -24
- package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
- package/src/__tests__/credential-security-e2e.test.ts +0 -362
- package/src/__tests__/credential-vault-unit.test.ts +0 -1528
- package/src/__tests__/credential-vault.test.ts +0 -1706
- package/src/__tests__/identity-intro-cache.test.ts +0 -315
- package/src/__tests__/secret-onetime-send.test.ts +0 -182
- package/src/cli/commands/__tests__/task.test.ts +0 -914
- package/src/cli/commands/task.ts +0 -771
- package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
- package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
- package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
- package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
- package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
- package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
- package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
- package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
- package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
- package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
- package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
- package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
- package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
- package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
- package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
- package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
- package/src/memory/preloaded-apps.ts +0 -116
- package/src/runtime/routes/identity-intro-cache.ts +0 -172
- package/src/tools/credentials/vault.ts +0 -712
|
@@ -22,57 +22,6 @@ mock.module("../util/logger.js", () => ({
|
|
|
22
22
|
}),
|
|
23
23
|
}));
|
|
24
24
|
|
|
25
|
-
const checkpointStore = new Map<string, string>();
|
|
26
|
-
|
|
27
|
-
mock.module("../memory/checkpoints.js", () => ({
|
|
28
|
-
getMemoryCheckpoint: (key: string) => checkpointStore.get(key) ?? null,
|
|
29
|
-
setMemoryCheckpoint: (key: string, value: string) => {
|
|
30
|
-
checkpointStore.set(key, value);
|
|
31
|
-
},
|
|
32
|
-
}));
|
|
33
|
-
|
|
34
|
-
const getConfiguredProviderCalls: string[] = [];
|
|
35
|
-
const mockProvider = { name: "mock-provider" };
|
|
36
|
-
|
|
37
|
-
mock.module("../providers/provider-send-message.js", () => ({
|
|
38
|
-
getConfiguredProvider: mock(async (callSite: string) => {
|
|
39
|
-
getConfiguredProviderCalls.push(callSite);
|
|
40
|
-
return mockProvider;
|
|
41
|
-
}),
|
|
42
|
-
}));
|
|
43
|
-
|
|
44
|
-
type SidechainCall = {
|
|
45
|
-
callSite?: string;
|
|
46
|
-
content: string;
|
|
47
|
-
maxTokens?: number;
|
|
48
|
-
systemPrompt?: string;
|
|
49
|
-
tools: unknown[];
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
type SidechainResult = {
|
|
53
|
-
text: string;
|
|
54
|
-
hadTextDeltas: false;
|
|
55
|
-
response: { content: [] };
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const sidechainCalls: SidechainCall[] = [];
|
|
59
|
-
let sidechainText = "";
|
|
60
|
-
let sidechainResultPromise: Promise<SidechainResult> | null = null;
|
|
61
|
-
|
|
62
|
-
mock.module("../runtime/btw-sidechain.js", () => ({
|
|
63
|
-
runBtwSidechain: mock(async (params: SidechainCall) => {
|
|
64
|
-
sidechainCalls.push(params);
|
|
65
|
-
if (sidechainResultPromise) {
|
|
66
|
-
return sidechainResultPromise;
|
|
67
|
-
}
|
|
68
|
-
return {
|
|
69
|
-
text: sidechainText,
|
|
70
|
-
hadTextDeltas: false,
|
|
71
|
-
response: { content: [] },
|
|
72
|
-
};
|
|
73
|
-
}),
|
|
74
|
-
}));
|
|
75
|
-
|
|
76
25
|
import {
|
|
77
26
|
handleDetailedHealth,
|
|
78
27
|
handleReadyz,
|
|
@@ -86,17 +35,6 @@ import {
|
|
|
86
35
|
selectHatchedAtFromStats,
|
|
87
36
|
} from "../workspace/hatched-date.js";
|
|
88
37
|
|
|
89
|
-
function createDeferred<T>(): {
|
|
90
|
-
promise: Promise<T>;
|
|
91
|
-
resolve: (value: T) => void;
|
|
92
|
-
} {
|
|
93
|
-
let resolve!: (value: T) => void;
|
|
94
|
-
const promise = new Promise<T>((res) => {
|
|
95
|
-
resolve = res;
|
|
96
|
-
});
|
|
97
|
-
return { promise, resolve };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
38
|
// ── Env helpers ─────────────────────────────────────────────────────────
|
|
101
39
|
|
|
102
40
|
let savedEnv: Record<string, string | undefined>;
|
|
@@ -194,11 +132,6 @@ beforeEach(() => {
|
|
|
194
132
|
rmSync(getHatchedSidecarPath(), { force: true });
|
|
195
133
|
rmSync(join(getWorkspaceDir(), "IDENTITY.md"), { force: true });
|
|
196
134
|
rmSync(join(getWorkspaceDir(), "SOUL.md"), { force: true });
|
|
197
|
-
checkpointStore.clear();
|
|
198
|
-
getConfiguredProviderCalls.length = 0;
|
|
199
|
-
sidechainCalls.length = 0;
|
|
200
|
-
sidechainText = "";
|
|
201
|
-
sidechainResultPromise = null;
|
|
202
135
|
});
|
|
203
136
|
|
|
204
137
|
afterEach(() => {
|
|
@@ -564,125 +497,3 @@ describe("identity routes — createdAt selection", () => {
|
|
|
564
497
|
expect(existsSync(getHatchedSidecarPath())).toBe(false);
|
|
565
498
|
});
|
|
566
499
|
});
|
|
567
|
-
|
|
568
|
-
describe("identity routes — intro greetings", () => {
|
|
569
|
-
test("returns fallback immediately, generates personalized greetings in the background, then reuses the cache", async () => {
|
|
570
|
-
const workspaceDir = getWorkspaceDir();
|
|
571
|
-
writeFileSync(
|
|
572
|
-
join(workspaceDir, "IDENTITY.md"),
|
|
573
|
-
[
|
|
574
|
-
"# Identity",
|
|
575
|
-
"",
|
|
576
|
-
"- **Name:** Example Assistant",
|
|
577
|
-
"- **Personality:** enjoys crisp, useful hellos",
|
|
578
|
-
"",
|
|
579
|
-
"Identity sentinel: chartreuse compass.",
|
|
580
|
-
].join("\n"),
|
|
581
|
-
"utf-8",
|
|
582
|
-
);
|
|
583
|
-
writeFileSync(
|
|
584
|
-
join(workspaceDir, "SOUL.md"),
|
|
585
|
-
[
|
|
586
|
-
"# Soul",
|
|
587
|
-
"",
|
|
588
|
-
"Soul sentinel: copper lighthouse.",
|
|
589
|
-
"",
|
|
590
|
-
"Keep greetings warm and specific.",
|
|
591
|
-
].join("\n"),
|
|
592
|
-
"utf-8",
|
|
593
|
-
);
|
|
594
|
-
const deferredSidechain = createDeferred<SidechainResult>();
|
|
595
|
-
sidechainResultPromise = deferredSidechain.promise;
|
|
596
|
-
|
|
597
|
-
const route = ROUTES.find(
|
|
598
|
-
(candidate) => candidate.operationId === "identity_intro",
|
|
599
|
-
);
|
|
600
|
-
expect(route).toBeDefined();
|
|
601
|
-
|
|
602
|
-
const body = route!.handler({
|
|
603
|
-
queryParams: { localHour: "8", localMinute: "15" },
|
|
604
|
-
}) as {
|
|
605
|
-
greetings: string[];
|
|
606
|
-
text: string;
|
|
607
|
-
source: string;
|
|
608
|
-
refreshing: boolean;
|
|
609
|
-
};
|
|
610
|
-
|
|
611
|
-
expect(body).toEqual({
|
|
612
|
-
greetings: [
|
|
613
|
-
"What are we working on?",
|
|
614
|
-
"I'm here whenever you need me.",
|
|
615
|
-
"What's on your mind?",
|
|
616
|
-
"Ready when you are.",
|
|
617
|
-
],
|
|
618
|
-
text: "What are we working on?",
|
|
619
|
-
source: "fallback",
|
|
620
|
-
refreshing: true,
|
|
621
|
-
});
|
|
622
|
-
expect(getConfiguredProviderCalls).toEqual([]);
|
|
623
|
-
expect(sidechainCalls).toEqual([]);
|
|
624
|
-
|
|
625
|
-
await Promise.resolve();
|
|
626
|
-
await Promise.resolve();
|
|
627
|
-
|
|
628
|
-
expect(getConfiguredProviderCalls).toEqual(["emptyStateGreeting"]);
|
|
629
|
-
expect(sidechainCalls).toHaveLength(1);
|
|
630
|
-
expect(sidechainCalls[0]?.callSite).toBe("emptyStateGreeting");
|
|
631
|
-
expect(sidechainCalls[0]?.tools).toEqual([]);
|
|
632
|
-
expect(sidechainCalls[0]?.content).toContain("Generate 5 short");
|
|
633
|
-
expect(sidechainCalls[0]?.content).not.toContain("Current time of day:");
|
|
634
|
-
expect(sidechainCalls[0]?.content).toContain(
|
|
635
|
-
"do not mention the current time",
|
|
636
|
-
);
|
|
637
|
-
expect(sidechainCalls[0]?.content).toMatch(
|
|
638
|
-
/Current user-local time for subtle tone only: morning \(08:15\)\.$/,
|
|
639
|
-
);
|
|
640
|
-
expect(sidechainCalls[0]?.content).toContain("JSON array");
|
|
641
|
-
expect(sidechainCalls[0]?.systemPrompt).toContain(
|
|
642
|
-
"Identity sentinel: chartreuse compass.",
|
|
643
|
-
);
|
|
644
|
-
expect(sidechainCalls[0]?.systemPrompt).toContain(
|
|
645
|
-
"Soul sentinel: copper lighthouse.",
|
|
646
|
-
);
|
|
647
|
-
deferredSidechain.resolve({
|
|
648
|
-
text: JSON.stringify([
|
|
649
|
-
"Charting the next useful thing?",
|
|
650
|
-
"I brought the compass. Where to?",
|
|
651
|
-
"Ready to make this lighter.",
|
|
652
|
-
"Morning momentum?",
|
|
653
|
-
"Five options, one good start.",
|
|
654
|
-
"A useful next step?",
|
|
655
|
-
]),
|
|
656
|
-
hadTextDeltas: false,
|
|
657
|
-
response: { content: [] },
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
await sidechainResultPromise;
|
|
661
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
662
|
-
|
|
663
|
-
sidechainCalls.length = 0;
|
|
664
|
-
getConfiguredProviderCalls.length = 0;
|
|
665
|
-
|
|
666
|
-
const cachedBody = (await route!.handler({})) as {
|
|
667
|
-
greetings: string[];
|
|
668
|
-
text: string;
|
|
669
|
-
source: string;
|
|
670
|
-
refreshing: boolean;
|
|
671
|
-
};
|
|
672
|
-
|
|
673
|
-
expect(cachedBody).toEqual({
|
|
674
|
-
greetings: [
|
|
675
|
-
"Charting the next useful thing?",
|
|
676
|
-
"I brought the compass. Where to?",
|
|
677
|
-
"Ready to make this lighter.",
|
|
678
|
-
"Five options, one good start.",
|
|
679
|
-
"A useful next step?",
|
|
680
|
-
],
|
|
681
|
-
text: "Charting the next useful thing?",
|
|
682
|
-
source: "cache",
|
|
683
|
-
refreshing: false,
|
|
684
|
-
});
|
|
685
|
-
expect(getConfiguredProviderCalls).toEqual([]);
|
|
686
|
-
expect(sidechainCalls).toEqual([]);
|
|
687
|
-
});
|
|
688
|
-
});
|
|
@@ -173,7 +173,7 @@ describe("inbound invite redemption intercept", () => {
|
|
|
173
173
|
// Verify the user is now an active member
|
|
174
174
|
const result = findContactChannel({
|
|
175
175
|
channelType: "telegram",
|
|
176
|
-
|
|
176
|
+
address: "user-invite-123",
|
|
177
177
|
});
|
|
178
178
|
expect(result).not.toBeNull();
|
|
179
179
|
expect(result!.channel.status).toBe("active");
|
|
@@ -203,7 +203,7 @@ describe("inbound invite redemption intercept", () => {
|
|
|
203
203
|
// Verify the user was NOT made a member
|
|
204
204
|
const result = findContactChannel({
|
|
205
205
|
channelType: "telegram",
|
|
206
|
-
|
|
206
|
+
address: "user-invite-123",
|
|
207
207
|
});
|
|
208
208
|
expect(result).toBeNull();
|
|
209
209
|
});
|
|
@@ -400,7 +400,7 @@ describe("inbound invite redemption intercept", () => {
|
|
|
400
400
|
// its ID as the invite's contactId (satisfies the FK constraint).
|
|
401
401
|
const existing = findContactChannel({
|
|
402
402
|
channelType: "telegram",
|
|
403
|
-
|
|
403
|
+
address: "user-invite-123",
|
|
404
404
|
externalChatId: "chat-invite-test",
|
|
405
405
|
});
|
|
406
406
|
const targetContactId = existing!.contact.id;
|
|
@@ -422,7 +422,7 @@ describe("inbound invite redemption intercept", () => {
|
|
|
422
422
|
|
|
423
423
|
const result = findContactChannel({
|
|
424
424
|
channelType: "telegram",
|
|
425
|
-
|
|
425
|
+
address: "user-invite-123",
|
|
426
426
|
externalChatId: "chat-invite-test",
|
|
427
427
|
});
|
|
428
428
|
expect(result).not.toBeNull();
|
|
@@ -83,7 +83,7 @@ describe("invite-redemption-service", () => {
|
|
|
83
83
|
|
|
84
84
|
const result = findContactChannel({
|
|
85
85
|
channelType: "telegram",
|
|
86
|
-
|
|
86
|
+
address: "user-1",
|
|
87
87
|
});
|
|
88
88
|
|
|
89
89
|
expect(result).not.toBeNull();
|
|
@@ -112,7 +112,7 @@ describe("invite-redemption-service", () => {
|
|
|
112
112
|
|
|
113
113
|
const result = findContactChannel({
|
|
114
114
|
channelType: "telegram",
|
|
115
|
-
|
|
115
|
+
address: "code-user-1",
|
|
116
116
|
});
|
|
117
117
|
|
|
118
118
|
expect(result).not.toBeNull();
|
|
@@ -325,7 +325,7 @@ describe("invite-redemption-service", () => {
|
|
|
325
325
|
// Verify the redeemer's Telegram ID is now bound to Mom's contact
|
|
326
326
|
const result = findContactChannel({
|
|
327
327
|
channelType: "telegram",
|
|
328
|
-
|
|
328
|
+
address: "guardian-tg-id",
|
|
329
329
|
});
|
|
330
330
|
expect(result).not.toBeNull();
|
|
331
331
|
expect(result!.contact.id).toBe(momContact.id);
|
|
@@ -417,7 +417,7 @@ describe("invite-redemption-service", () => {
|
|
|
417
417
|
// Verify the redeemer's Telegram ID is now bound to Mom's contact
|
|
418
418
|
const result = findContactChannel({
|
|
419
419
|
channelType: "telegram",
|
|
420
|
-
|
|
420
|
+
address: "guardian-code-id",
|
|
421
421
|
});
|
|
422
422
|
expect(result).not.toBeNull();
|
|
423
423
|
expect(result!.contact.id).toBe(momContact.id);
|
|
@@ -54,9 +54,7 @@ describe("LLM call-site catalog", () => {
|
|
|
54
54
|
|
|
55
55
|
test("CALL_SITE_DEFAULTS covers every LLMCallSite enum value", () => {
|
|
56
56
|
const defaultIds = new Set(Object.keys(CALL_SITE_DEFAULTS));
|
|
57
|
-
const missing = LLMCallSiteEnum.options.filter(
|
|
58
|
-
(id) => !defaultIds.has(id),
|
|
59
|
-
);
|
|
57
|
+
const missing = LLMCallSiteEnum.options.filter((id) => !defaultIds.has(id));
|
|
60
58
|
expect(missing).toEqual([]);
|
|
61
59
|
});
|
|
62
60
|
|
|
@@ -68,11 +66,12 @@ describe("LLM call-site catalog", () => {
|
|
|
68
66
|
expect(extra).toEqual([]);
|
|
69
67
|
});
|
|
70
68
|
|
|
71
|
-
test("every CALL_SITE_DEFAULTS
|
|
69
|
+
test("every CALL_SITE_DEFAULTS profile, when present, is a non-empty string", () => {
|
|
72
70
|
for (const [, config] of Object.entries(CALL_SITE_DEFAULTS)) {
|
|
73
|
-
|
|
71
|
+
// A call site may omit `profile` to inherit the workspace default config.
|
|
72
|
+
if (config.profile === undefined) continue;
|
|
74
73
|
expect(typeof config.profile).toBe("string");
|
|
75
|
-
expect(config.profile
|
|
74
|
+
expect(config.profile.length).toBeGreaterThan(0);
|
|
76
75
|
}
|
|
77
76
|
});
|
|
78
77
|
});
|
|
@@ -30,13 +30,6 @@ function getRepoRoot(): string {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
const META_JSON_PATH = join(getRepoRoot(), "meta", "llm-provider-catalog.json");
|
|
33
|
-
const SWIFTPM_MIRROR_PATH = join(
|
|
34
|
-
getRepoRoot(),
|
|
35
|
-
"clients",
|
|
36
|
-
"shared",
|
|
37
|
-
"Resources",
|
|
38
|
-
"llm-provider-catalog.json",
|
|
39
|
-
);
|
|
40
33
|
|
|
41
34
|
interface ClientCatalogCredentialsGuide {
|
|
42
35
|
description: string;
|
|
@@ -457,19 +450,4 @@ describe("LLM catalog parity: daemon vs client", () => {
|
|
|
457
450
|
longContextMode: "native-model",
|
|
458
451
|
});
|
|
459
452
|
});
|
|
460
|
-
|
|
461
|
-
// -----------------------------------------------------------------------
|
|
462
|
-
// Mirror byte-equality
|
|
463
|
-
// -----------------------------------------------------------------------
|
|
464
|
-
|
|
465
|
-
test("SwiftPM mirror is byte-identical to meta/ copy", () => {
|
|
466
|
-
// `sync-llm-catalog.ts` writes both files from the same serializer; this
|
|
467
|
-
// guard catches any case where one copy is regenerated without the other.
|
|
468
|
-
// Byte equality is required because SwiftPM bundles the resource verbatim
|
|
469
|
-
// into `VellumAssistantShared` while the meta/ JSON is consumed as a
|
|
470
|
-
// cross-package artifact (web codegen, etc.).
|
|
471
|
-
const metaBytes = readFileSync(META_JSON_PATH);
|
|
472
|
-
const swiftPmBytes = readFileSync(SWIFTPM_MIRROR_PATH);
|
|
473
|
-
expect(swiftPmBytes.equals(metaBytes)).toBe(true);
|
|
474
|
-
});
|
|
475
453
|
});
|
|
@@ -670,30 +670,6 @@ describe("resolveCallSiteConfig", () => {
|
|
|
670
670
|
expect(resolved.effort).toBe("medium");
|
|
671
671
|
});
|
|
672
672
|
|
|
673
|
-
test("workflowLeaf defaults to the cost-optimized profile", () => {
|
|
674
|
-
const llm = LLMSchema.parse({
|
|
675
|
-
default: fullDefault,
|
|
676
|
-
profiles: {
|
|
677
|
-
balanced: {
|
|
678
|
-
model: "claude-sonnet-4-7",
|
|
679
|
-
effort: "medium",
|
|
680
|
-
},
|
|
681
|
-
"cost-optimized": {
|
|
682
|
-
model: "claude-haiku-4-5-20251001",
|
|
683
|
-
effort: "low",
|
|
684
|
-
},
|
|
685
|
-
},
|
|
686
|
-
});
|
|
687
|
-
|
|
688
|
-
expect(resolveDefaultProfileKey("workflowLeaf", llm)).toBe(
|
|
689
|
-
"cost-optimized",
|
|
690
|
-
);
|
|
691
|
-
const resolved = resolveCallSiteConfig("workflowLeaf", llm);
|
|
692
|
-
expect(resolved.model).toBe("claude-haiku-4-5-20251001");
|
|
693
|
-
expect(resolved.effort).toBe("low");
|
|
694
|
-
expect(resolved.thinking.enabled).toBe(false);
|
|
695
|
-
});
|
|
696
|
-
|
|
697
673
|
test("explicit callSites config overrides CALL_SITE_DEFAULTS", () => {
|
|
698
674
|
const llm = LLMSchema.parse({
|
|
699
675
|
default: fullDefault,
|
|
@@ -1437,3 +1413,52 @@ describe("resolveCallSiteConfig logitBias provenance", () => {
|
|
|
1437
1413
|
expect(resolveCallSiteConfig("mainAgent", llm).logitBias).toBeUndefined();
|
|
1438
1414
|
});
|
|
1439
1415
|
});
|
|
1416
|
+
|
|
1417
|
+
describe("resolveCallSiteConfig — workflowLeaf default", () => {
|
|
1418
|
+
test("inherits the workspace default config rather than pinning cost-optimized", () => {
|
|
1419
|
+
const llm = LLMSchema.parse({
|
|
1420
|
+
default: fullDefault,
|
|
1421
|
+
profiles: {
|
|
1422
|
+
"cost-optimized": {
|
|
1423
|
+
provider: "anthropic",
|
|
1424
|
+
model: "claude-haiku-4-5-20251001",
|
|
1425
|
+
},
|
|
1426
|
+
},
|
|
1427
|
+
});
|
|
1428
|
+
const resolved = resolveCallSiteConfig("workflowLeaf", llm);
|
|
1429
|
+
// No pinned profile → the model comes from llm.default, NOT the
|
|
1430
|
+
// `cost-optimized` profile (which is uncredentialed on a BYOK install).
|
|
1431
|
+
expect(resolved.model).toBe("claude-opus-4-7");
|
|
1432
|
+
// Call-site tuning still applies.
|
|
1433
|
+
expect(resolved.effort).toBe("low");
|
|
1434
|
+
expect(resolved.thinking?.enabled).toBe(false);
|
|
1435
|
+
// The call site has no implicit default profile key.
|
|
1436
|
+
expect(resolveDefaultProfileKey("workflowLeaf", llm)).toBeUndefined();
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
test("honors an explicit workflowLeaf call-site override", () => {
|
|
1440
|
+
const llm = LLMSchema.parse({
|
|
1441
|
+
default: fullDefault,
|
|
1442
|
+
profiles: {
|
|
1443
|
+
cheap: { provider: "anthropic", model: "claude-haiku-4-5-20251001" },
|
|
1444
|
+
},
|
|
1445
|
+
callSites: { workflowLeaf: { profile: "cheap" } },
|
|
1446
|
+
});
|
|
1447
|
+
expect(resolveCallSiteConfig("workflowLeaf", llm).model).toBe(
|
|
1448
|
+
"claude-haiku-4-5-20251001",
|
|
1449
|
+
);
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
test("honors a per-call override profile (an explicit per-leaf profile)", () => {
|
|
1453
|
+
const llm = LLMSchema.parse({
|
|
1454
|
+
default: fullDefault,
|
|
1455
|
+
profiles: {
|
|
1456
|
+
fancy: { provider: "anthropic", model: "claude-sonnet-4-7" },
|
|
1457
|
+
},
|
|
1458
|
+
});
|
|
1459
|
+
expect(
|
|
1460
|
+
resolveCallSiteConfig("workflowLeaf", llm, { overrideProfile: "fancy" })
|
|
1461
|
+
.model,
|
|
1462
|
+
).toBe("claude-sonnet-4-7");
|
|
1463
|
+
});
|
|
1464
|
+
});
|
|
@@ -3,17 +3,15 @@ import { describe, expect, test } from "bun:test";
|
|
|
3
3
|
import { PROVIDER_SEED_DATA } from "../oauth/seed-providers.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Allowed CDN prefixes for ``logoUrl
|
|
7
|
-
* ``
|
|
6
|
+
* Allowed CDN prefixes for the ``logoUrl`` field on ``PROVIDER_SEED_DATA``
|
|
7
|
+
* (``assistant/src/oauth/seed-providers.ts``):
|
|
8
8
|
*
|
|
9
9
|
* - Simple Icons (CC0) is the default for most providers.
|
|
10
10
|
* - thesvg via jsDelivr is the documented fallback for brands Simple Icons
|
|
11
11
|
* doesn't host (e.g. Salesforce, which Simple Icons removed for
|
|
12
|
-
* trademark reasons).
|
|
13
|
-
* of figma/github/gmail/linear/notion/outlook/slack.
|
|
12
|
+
* trademark reasons).
|
|
14
13
|
*
|
|
15
|
-
* Adding another CDN should be a deliberate choice — extend this list
|
|
16
|
-
* and update the manifest in tandem.
|
|
14
|
+
* Adding another CDN should be a deliberate choice — extend this list.
|
|
17
15
|
*/
|
|
18
16
|
const ALLOWED_LOGO_URL_PREFIXES = [
|
|
19
17
|
"https://cdn.simpleicons.org/",
|
|
@@ -59,7 +59,7 @@ mock.module("../util/platform.js", () => ({
|
|
|
59
59
|
}));
|
|
60
60
|
|
|
61
61
|
mock.module("../contacts/contact-store.js", () => ({
|
|
62
|
-
|
|
62
|
+
findContactByAddress: () => null,
|
|
63
63
|
findGuardianForChannel: (channelType: string) =>
|
|
64
64
|
channelType === "vellum" ? mockVellumGuardian : null,
|
|
65
65
|
listGuardianChannels: () => null,
|
|
@@ -32,18 +32,14 @@ import {
|
|
|
32
32
|
// ── Mock state ────────────────────────────────────────────────────
|
|
33
33
|
|
|
34
34
|
let mockWorkspaceDir: string = "";
|
|
35
|
-
let mockVellumGuardian:
|
|
36
|
-
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
| null
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
contact: { userFile: string | null };
|
|
44
|
-
channels: Record<string, unknown>[];
|
|
45
|
-
}
|
|
46
|
-
| null = null;
|
|
35
|
+
let mockVellumGuardian: {
|
|
36
|
+
contact: { userFile: string | null };
|
|
37
|
+
channel: Record<string, unknown>;
|
|
38
|
+
} | null = null;
|
|
39
|
+
let mockAnyGuardian: {
|
|
40
|
+
contact: { userFile: string | null };
|
|
41
|
+
channels: Record<string, unknown>[];
|
|
42
|
+
} | null = null;
|
|
47
43
|
|
|
48
44
|
// ── Mock modules (must precede imports from the module under test) ──
|
|
49
45
|
|
|
@@ -52,7 +48,7 @@ mock.module("../util/platform.js", () => ({
|
|
|
52
48
|
}));
|
|
53
49
|
|
|
54
50
|
mock.module("../contacts/contact-store.js", () => ({
|
|
55
|
-
|
|
51
|
+
findContactByAddress: () => null,
|
|
56
52
|
findGuardianForChannel: (channelType: string) =>
|
|
57
53
|
channelType === "vellum" ? mockVellumGuardian : null,
|
|
58
54
|
listGuardianChannels: () => mockAnyGuardian,
|
|
@@ -138,7 +134,8 @@ describe("ensureGuardianPersonaFile", () => {
|
|
|
138
134
|
const userFile = "alice.md";
|
|
139
135
|
const dir = join(mockWorkspaceDir, "users");
|
|
140
136
|
const filePath = join(dir, userFile);
|
|
141
|
-
const existingContent =
|
|
137
|
+
const existingContent =
|
|
138
|
+
"# Existing user notes\n\n- Likes sparkling water\n";
|
|
142
139
|
|
|
143
140
|
mkdirSync(dir, { recursive: true });
|
|
144
141
|
writeFileSync(filePath, existingContent, "utf-8");
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
3
|
+
|
|
4
|
+
import { invalidateConfigCache } from "../config/loader.js";
|
|
5
|
+
import { getModelProfiles } from "../plugin-api/index.js";
|
|
6
|
+
import { getWorkspaceConfigPath } from "../util/platform.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `getModelProfiles()` — the runtime handle a plugin (e.g. a model router) calls
|
|
10
|
+
* to learn which inference profiles a workspace defines. These tests pin its
|
|
11
|
+
* contract: presentation ordering (`profileOrder` then alphabetical tail), mix
|
|
12
|
+
* profiles included and flagged via `isMix`, disabled profiles included and
|
|
13
|
+
* flagged via `isDisabled`, and the per-field fallbacks (label → key,
|
|
14
|
+
* description → null, isActive → `llm.activeProfile`).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
function writeFixtureConfig(config: Record<string, unknown>): void {
|
|
18
|
+
writeFileSync(getWorkspaceConfigPath(), JSON.stringify(config), "utf-8");
|
|
19
|
+
invalidateConfigCache();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("getModelProfiles", () => {
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
invalidateConfigCache();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("orders profiles by profileOrder then the remaining keys alphabetically", () => {
|
|
28
|
+
// GIVEN a workspace whose profileOrder names some keys, with a duplicate
|
|
29
|
+
// ("balanced") and a key that resolves to no profile ("ghost").
|
|
30
|
+
writeFixtureConfig({
|
|
31
|
+
llm: {
|
|
32
|
+
profiles: { zeta: {}, alpha: {}, balanced: {}, "cost-optimized": {} },
|
|
33
|
+
profileOrder: ["balanced", "cost-optimized", "balanced", "ghost"],
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
// WHEN the profiles are listed.
|
|
37
|
+
const keys = getModelProfiles().map((p) => p.key);
|
|
38
|
+
// THEN profileOrder keys come first (deduped, existing-only), then the rest
|
|
39
|
+
// alphabetically.
|
|
40
|
+
expect(keys).toEqual(["balanced", "cost-optimized", "alpha", "zeta"]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("includes mix profiles and flags them via isMix", () => {
|
|
44
|
+
// GIVEN a workspace with a weighted mix profile alongside plain ones.
|
|
45
|
+
writeFixtureConfig({
|
|
46
|
+
llm: {
|
|
47
|
+
profiles: {
|
|
48
|
+
alpha: {},
|
|
49
|
+
beta: {},
|
|
50
|
+
blend: {
|
|
51
|
+
mix: [
|
|
52
|
+
{ profile: "alpha", weight: 1 },
|
|
53
|
+
{ profile: "beta", weight: 1 },
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
profileOrder: ["alpha", "beta", "blend"],
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
// WHEN the profiles are listed.
|
|
61
|
+
const flags = getModelProfiles().map((p) => [p.key, p.isMix]);
|
|
62
|
+
// THEN the mix profile is present and flagged isMix, while plain profiles are not.
|
|
63
|
+
expect(flags).toEqual([
|
|
64
|
+
["alpha", false],
|
|
65
|
+
["beta", false],
|
|
66
|
+
["blend", true],
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("marks an active mix profile via isActive", () => {
|
|
71
|
+
// GIVEN a workspace whose activeProfile is itself a weighted mix.
|
|
72
|
+
writeFixtureConfig({
|
|
73
|
+
llm: {
|
|
74
|
+
profiles: {
|
|
75
|
+
alpha: {},
|
|
76
|
+
blend: {
|
|
77
|
+
mix: [
|
|
78
|
+
{ profile: "alpha", weight: 1 },
|
|
79
|
+
{ profile: "beta", weight: 1 },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
beta: {},
|
|
83
|
+
},
|
|
84
|
+
profileOrder: ["alpha", "blend", "beta"],
|
|
85
|
+
activeProfile: "blend",
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
// WHEN the profiles are listed.
|
|
89
|
+
const active = getModelProfiles()
|
|
90
|
+
.filter((p) => p.isActive)
|
|
91
|
+
.map((p) => p.key);
|
|
92
|
+
// THEN the active mix profile is the one flagged isActive.
|
|
93
|
+
expect(active).toEqual(["blend"]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("includes disabled profiles and flags them via isDisabled", () => {
|
|
97
|
+
// GIVEN a workspace with one active and one disabled profile.
|
|
98
|
+
writeFixtureConfig({
|
|
99
|
+
llm: {
|
|
100
|
+
profiles: {
|
|
101
|
+
alpha: { status: "active" },
|
|
102
|
+
beta: { status: "disabled" },
|
|
103
|
+
},
|
|
104
|
+
profileOrder: ["alpha", "beta"],
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
// WHEN the profiles are listed.
|
|
108
|
+
const flags = getModelProfiles().map((p) => [p.key, p.isDisabled]);
|
|
109
|
+
// THEN the disabled profile is present and marked isDisabled.
|
|
110
|
+
expect(flags).toEqual([
|
|
111
|
+
["alpha", false],
|
|
112
|
+
["beta", true],
|
|
113
|
+
]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("falls back to the key when a profile has no label", () => {
|
|
117
|
+
// GIVEN a workspace where one profile sets a label and one does not.
|
|
118
|
+
writeFixtureConfig({
|
|
119
|
+
llm: {
|
|
120
|
+
profiles: { balanced: { label: "Balanced" }, terse: {} },
|
|
121
|
+
profileOrder: ["balanced", "terse"],
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
// WHEN the profiles are listed.
|
|
125
|
+
const byKey = Object.fromEntries(
|
|
126
|
+
getModelProfiles().map((p) => [p.key, p.label]),
|
|
127
|
+
);
|
|
128
|
+
// THEN the unlabeled profile's label falls back to its key.
|
|
129
|
+
expect(byKey.balanced).toBe("Balanced");
|
|
130
|
+
expect(byKey.terse).toBe("terse");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("marks the workspace active profile via isActive", () => {
|
|
134
|
+
// GIVEN a workspace whose activeProfile is "beta".
|
|
135
|
+
writeFixtureConfig({
|
|
136
|
+
llm: {
|
|
137
|
+
profiles: { alpha: {}, beta: {} },
|
|
138
|
+
profileOrder: ["alpha", "beta"],
|
|
139
|
+
activeProfile: "beta",
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
// WHEN the profiles are listed.
|
|
143
|
+
const flags = getModelProfiles().map((p) => [p.key, p.isActive]);
|
|
144
|
+
// THEN only the active profile is marked isActive.
|
|
145
|
+
expect(flags).toEqual([
|
|
146
|
+
["alpha", false],
|
|
147
|
+
["beta", true],
|
|
148
|
+
]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("reports description as null when a profile sets none", () => {
|
|
152
|
+
// GIVEN a workspace where one profile has a description and one does not.
|
|
153
|
+
writeFixtureConfig({
|
|
154
|
+
llm: {
|
|
155
|
+
profiles: {
|
|
156
|
+
documented: { description: "Cheaper models, slower" },
|
|
157
|
+
bare: {},
|
|
158
|
+
},
|
|
159
|
+
profileOrder: ["documented", "bare"],
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
// WHEN the profiles are listed.
|
|
163
|
+
const byKey = Object.fromEntries(
|
|
164
|
+
getModelProfiles().map((p) => [p.key, p.description]),
|
|
165
|
+
);
|
|
166
|
+
// THEN a missing description is reported as null.
|
|
167
|
+
expect(byKey.documented).toBe("Cheaper models, slower");
|
|
168
|
+
expect(byKey.bare).toBeNull();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("returns an empty list when the workspace defines no profiles", () => {
|
|
172
|
+
// GIVEN a workspace with no profiles defined.
|
|
173
|
+
writeFixtureConfig({ llm: { profiles: {}, profileOrder: [] } });
|
|
174
|
+
// WHEN the profiles are listed.
|
|
175
|
+
// THEN the result is empty.
|
|
176
|
+
expect(getModelProfiles()).toEqual([]);
|
|
177
|
+
});
|
|
178
|
+
});
|