/** * When IS_PLATFORM=true and no config.json exists yet, loadConfig() must * write all managed-capable service modes as "managed" instead of the * per-service schema default. When IS_PLATFORM is absent/false, or when * config.json already exists, the Zod schema defaults and existing values * are preserved unchanged — note that `google-oauth` and `notion-oauth` * default to "managed" at the schema level (per JARVIS-966), while every * other managed-capable service defaults to "your-own". */ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs"; import { join } from "node:path"; import { afterAll, afterEach, beforeEach, describe, expect, mock, test, } from "bun:test"; // --------------------------------------------------------------------------- // Mocks — declared before imports that depend on platform/logger // --------------------------------------------------------------------------- const WORKSPACE_DIR = process.env.VELLUM_WORKSPACE_DIR!; const CONFIG_PATH = join(WORKSPACE_DIR, "config.json"); afterAll(() => { mock.restore(); }); import { invalidateConfigCache, loadConfig } from "../config/loader.js"; import { applyContextDefaultsToRawConfig } from "../runtime/routes/conversation-query-routes.js"; import { setStorePathForTesting } from "./encrypted-store-test-helpers.js"; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function ensureTestDir(): void { const dirs = [ WORKSPACE_DIR, join(WORKSPACE_DIR, "data"), join(WORKSPACE_DIR, "data", "memory"), join(WORKSPACE_DIR, "data", "memory", "knowledge"), join(WORKSPACE_DIR, "data", "logs"), ]; for (const dir of dirs) { if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } } } function resetWorkspace(): void { if (existsSync(WORKSPACE_DIR)) { for (const name of readdirSync(WORKSPACE_DIR)) { rmSync(join(WORKSPACE_DIR, name), { recursive: true, force: true }); } } ensureTestDir(); } function readConfig(): Record { return JSON.parse(readFileSync(CONFIG_PATH, "utf-8")); } // When IS_PLATFORM=true, every managed-capable service defaults to "managed". // Without IS_PLATFORM, services split by Zod schema default: google/notion-oauth // resolve to "managed" (per JARVIS-966), the rest resolve to "your-own". // web-search is deliberately absent: `provider` is its only axis, so the // platform context injects no mode for it. image-generation is also absent: // its managed axis is the provider ("vellum" proxies through the platform), // so the platform context fills `provider: "vellum"` rather than a mode. const MANAGED_SERVICES = [ "google-oauth", "outlook-oauth", "linear-oauth", "github-oauth", "notion-oauth", ] as const; /** * Services whose Zod schema default is `"managed"` rather than `"your-own"`. * For these, the fresh-write and in-memory paths produce `"managed"` even * when IS_PLATFORM is false/unset — the schema default applies regardless of * deployment context. See `assistant/src/config/schemas/services.ts` and * JARVIS-966: managed mode is the optimization target for new users on * managed-capable infra, since the BYO flow requires a Google Cloud / * Notion integration setup that most users never complete. */ const SCHEMA_MANAGED_DEFAULT_SERVICES = [ "google-oauth", "notion-oauth", ] as const; // web-search is absent from both lists: it carries no mode at all. // image-generation and web-search are absent from both lists: they carry // no mode at all. const SCHEMA_YOUR_OWN_DEFAULT_SERVICES = [ "outlook-oauth", "linear-oauth", "github-oauth", ] as const; // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe("platform-managed config defaults", () => { const originalIsPlatform = process.env.IS_PLATFORM; beforeEach(() => { resetWorkspace(); setStorePathForTesting(join(WORKSPACE_DIR, "keys.enc")); invalidateConfigCache(); }); afterEach(() => { setStorePathForTesting(null); invalidateConfigCache(); // Restore env to its original value if (originalIsPlatform === undefined) { delete process.env.IS_PLATFORM; } else { process.env.IS_PLATFORM = originalIsPlatform; } }); test("IS_PLATFORM=true, no config file → all 6 managed service modes written as 'managed', image-generation provider written as 'vellum'", () => { process.env.IS_PLATFORM = "true"; loadConfig(); expect(existsSync(CONFIG_PATH)).toBe(true); const written = readConfig() as { services?: Record }; expect(written.services).toBeDefined(); const services = written.services!; for (const svc of MANAGED_SERVICES) { expect((services[svc] as { mode?: string })?.mode).toBe("managed"); } expect((services["web-search"] as { provider?: string })?.provider).toBe( "inference-provider-native", ); // image-generation's managed axis is its provider: the platform context // fills "vellum" and never injects a managed mode (the persisted mode is // the schema default). const imageGen = services["image-generation"] as { provider?: string; mode?: string; }; expect(imageGen?.provider).toBe("vellum"); expect(imageGen?.mode).not.toBe("managed"); }); test("IS_PLATFORM=false, no config file → service modes follow schema defaults (your-own except google/notion-oauth which are managed)", () => { process.env.IS_PLATFORM = "false"; loadConfig(); expect(existsSync(CONFIG_PATH)).toBe(true); const written = readConfig() as { services?: Record }; expect(written.services).toBeDefined(); const services = written.services!; for (const svc of SCHEMA_YOUR_OWN_DEFAULT_SERVICES) { expect((services[svc] as { mode?: string })?.mode).toBe("your-own"); } for (const svc of SCHEMA_MANAGED_DEFAULT_SERVICES) { expect((services[svc] as { mode?: string })?.mode).toBe("managed"); } }); test("IS_PLATFORM unset, no config file → service modes follow schema defaults (your-own except google/notion-oauth which are managed)", () => { delete process.env.IS_PLATFORM; loadConfig(); expect(existsSync(CONFIG_PATH)).toBe(true); const written = readConfig() as { services?: Record }; expect(written.services).toBeDefined(); const services = written.services!; for (const svc of SCHEMA_YOUR_OWN_DEFAULT_SERVICES) { expect((services[svc] as { mode?: string })?.mode).toBe("your-own"); } for (const svc of SCHEMA_MANAGED_DEFAULT_SERVICES) { expect((services[svc] as { mode?: string })?.mode).toBe("managed"); } }); test("IS_PLATFORM=true, config file already exists → existing service mode values are preserved", () => { process.env.IS_PLATFORM = "true"; // Write an existing config with image-generation mode explicitly set to "your-own" writeFileSync( CONFIG_PATH, JSON.stringify( { services: { "image-generation": { mode: "your-own" }, }, }, null, 2, ) + "\n", ); const config = loadConfig(); const written = readConfig() as { services?: Record }; expect(written.services).toBeDefined(); // The raw value must be preserved — backfill path, not fresh-write path. // (Migration 134 rewrites this legacy shape at daemon startup; the loader // itself must simply leave it alone.) expect( (written.services!["image-generation"] as { mode?: string })?.mode, ).toBe("your-own"); // The parsed config strips the legacy key and carries the filled // provider; no mode survives the schema. expect(config.services["image-generation"]).not.toHaveProperty("mode"); expect(config.services["image-generation"].provider).toBe("vellum"); }); test("IS_PLATFORM=true, config file with explicit notion-oauth mode='your-own' → preserved (schema default is 'managed')", () => { process.env.IS_PLATFORM = "true"; // notion-oauth's schema default is "managed" (per JARVIS-966), but an // explicit user choice of "your-own" must win — the fill-defaults pass // must never override an explicit value, even one that contradicts the // schema default. Mirror of the image-generation "your-own preserved" // test above, but for a service whose schema default is "managed". writeFileSync( CONFIG_PATH, JSON.stringify( { services: { "notion-oauth": { mode: "your-own" }, }, }, null, 2, ) + "\n", ); const config = loadConfig(); const written = readConfig() as { services?: Record }; expect(written.services).toBeDefined(); expect((written.services!["notion-oauth"] as { mode?: string })?.mode).toBe( "your-own", ); expect(config.services["notion-oauth"].mode).toBe("your-own"); }); test("IS_PLATFORM=true, config file exists without a services key → in-memory config has all managed modes", () => { // Regression guard for the platform-managed boot order: by the time // `loadConfig()` runs, lifecycle steps such as `seedInferenceProfiles` // have already written `config.json` (with `llm.profiles` etc.), so // `configFileExisted` is true even on a brand-new platform-managed // assistant. Deployment-context defaults must still be applied to the // in-memory config for any leaf keys that are absent from disk. process.env.IS_PLATFORM = "true"; writeFileSync( CONFIG_PATH, JSON.stringify( { llm: { profiles: { balanced: { provider: "anthropic", model: "claude-sonnet-4.5" }, }, activeProfile: "balanced", }, }, null, 2, ) + "\n", ); const config = loadConfig(); // In-memory config has the deployment-context defaults applied for the // missing service-mode fields. for (const svc of MANAGED_SERVICES) { expect( (config.services as unknown as Record)[svc]! .mode, ).toBe("managed"); } // ...and for image-generation's provider, whose fill is "vellum". expect(config.services["image-generation"].provider).toBe("vellum"); // The on-disk file is NOT modified by the fill pass — disk reflects only // what was already there. Existing-file branch never re-writes config.json. const onDisk = readConfig() as Record; expect(onDisk["services"]).toBeUndefined(); }); test("IS_PLATFORM=true, config file exists with an explicit image-generation provider → provider wins over the vellum fill", () => { process.env.IS_PLATFORM = "true"; // User has an explicit BYOK image-generation provider on disk. The fill // pass targets exactly that leaf (`provider: "vellum"`), so the explicit // value must win and no mode is injected — the effective mode is the // schema default. writeFileSync( CONFIG_PATH, JSON.stringify( { services: { "image-generation": { provider: "openai" }, }, }, null, 2, ) + "\n", ); const config = loadConfig(); const imageGen = ( config.services as unknown as Record< string, { mode?: string; provider?: string } > )["image-generation"]!; expect(imageGen.provider).toBe("openai"); expect(imageGen).not.toHaveProperty("mode"); }); test("IS_PLATFORM=false, config file exists without services key → in-memory config keeps schema defaults (your-own except google/notion-oauth which are managed)", () => { // Sanity guard: deployment-context defaults are a no-op when IS_PLATFORM // is not enabled, regardless of whether config.json existed. The Zod // schema defaults still apply, so google-oauth and notion-oauth resolve // to "managed" while the remaining services resolve to "your-own". process.env.IS_PLATFORM = "false"; writeFileSync( CONFIG_PATH, JSON.stringify( { llm: { profiles: { balanced: { provider: "anthropic", model: "claude-sonnet-4.5" }, }, activeProfile: "balanced", }, }, null, 2, ) + "\n", ); const config = loadConfig(); for (const svc of SCHEMA_YOUR_OWN_DEFAULT_SERVICES) { expect( (config.services as unknown as Record)[svc]! .mode, ).toBe("your-own"); } for (const svc of SCHEMA_MANAGED_DEFAULT_SERVICES) { expect( (config.services as unknown as Record)[svc]! .mode, ).toBe("managed"); } }); }); /** * Regression guard for the `handleGetConfig` route handler in * `assistant/src/runtime/routes/conversation-query-routes.ts`. That handler * returns the raw on-disk JSON to clients (macOS, web, CLI) via * `GET /v1/config`, but first layers deployment-context defaults on top * via the `applyContextDefaultsToRawConfig` helper. * * macOS's `loadServiceModes(config:)` only updates `inferenceMode` when * `services.inference.mode` is present in the response — without the fill * pass, freshly-hatched platform-managed assistants would have no `services` * key on disk (only `llm.profiles` from `seedInferenceProfiles`) and macOS * would fall back to its `@Published` default of "your-own". The helper is * also responsible for guarding against `loadRawConfig()` returning a * non-object payload from a malformed-but-parseable `config.json`. */ describe("GET /v1/config handler — context-default fill on raw response", () => { const originalIsPlatform = process.env.IS_PLATFORM; afterEach(() => { if (originalIsPlatform === undefined) { delete process.env.IS_PLATFORM; } else { process.env.IS_PLATFORM = originalIsPlatform; } }); test("IS_PLATFORM=true, raw config has no services key → response includes managed defaults", () => { process.env.IS_PLATFORM = "true"; // Mirrors the real-world fresh-hatch state: lifecycle wrote // `llm.profiles` to disk, but never persisted any service modes. const raw: Record = { llm: { profiles: { balanced: { provider: "anthropic", model: "claude-sonnet-4.5" }, }, activeProfile: "balanced", }, }; const result = applyContextDefaultsToRawConfig(raw) as Record< string, unknown >; const services = result["services"] as Record< string, { mode?: string; provider?: string } >; expect(services).toBeDefined(); for (const svc of MANAGED_SERVICES) { expect(services[svc]!.mode).toBe("managed"); } // image-generation is filled by provider, not mode. expect(services["image-generation"]!.provider).toBe("vellum"); expect(services["image-generation"]!.mode).toBeUndefined(); }); test("IS_PLATFORM=true, raw config has explicit services.image-generation.mode='your-own' → preserved", () => { process.env.IS_PLATFORM = "true"; // A legacy raw shape: mode with no provider leaf. Migration 134 rewrites // it (pinning provider gemini) before any real request reaches this // function, so the fill's only obligation here is to be non-destructive: // existing keys pass through untouched, absent leaves get the platform // default. const raw: Record = { services: { "image-generation": { mode: "your-own" }, }, }; const result = applyContextDefaultsToRawConfig(raw) as Record< string, unknown >; const services = result["services"] as Record< string, { mode?: string; provider?: string } >; expect(services["image-generation"]!.mode).toBe("your-own"); expect(services["image-generation"]!.provider).toBe("vellum"); // web-search is not context-filled: a filled `mode` would override BYOK // configs on every load. expect(services["web-search"]).toBeUndefined(); // inference.mode is a legacy backwards-compat wire field — synthesized // here for old macOS clients (SettingsStore.swift) that still read it. expect(services["inference"]!.mode).toBe("managed"); }); test("IS_PLATFORM=false, raw config has no services key → response is unchanged", () => { process.env.IS_PLATFORM = "false"; const raw: Record = { llm: { profiles: { balanced: { provider: "anthropic", model: "claude-sonnet-4.5" }, }, }, }; const result = applyContextDefaultsToRawConfig(raw) as Record< string, unknown >; expect(result["services"]).toBeUndefined(); }); test("IS_PLATFORM=true, raw config has an explicit image-generation provider → preserved, no mode injected", () => { process.env.IS_PLATFORM = "true"; // User set image-generation.provider explicitly. The fill targets that // same leaf (`provider: "vellum"`), so the explicit value wins and no // mode is injected. const raw: Record = { services: { "image-generation": { provider: "openai" }, }, }; const result = applyContextDefaultsToRawConfig(raw) as Record< string, unknown >; const services = result["services"] as Record< string, { mode?: string; provider?: string } >; expect(services["image-generation"]!.provider).toBe("openai"); expect(services["image-generation"]!.mode).toBeUndefined(); // services.inference.mode is synthesized as a legacy wire-only field for // older macOS clients during the rollout window (Phase 1.2 schema removal // landed before the macOS Providers UI ships). expect(services["inference"]!.mode).toBe("managed"); }); test("IS_PLATFORM=true, raw config has no inference subtree → synthesizes legacy mode='managed'", () => { process.env.IS_PLATFORM = "true"; const raw: Record = { llm: { profiles: { balanced: { provider: "anthropic", model: "claude-sonnet-4.5" }, }, }, }; const result = applyContextDefaultsToRawConfig(raw) as Record< string, unknown >; const services = result["services"] as Record; expect(services["inference"]!.mode).toBe("managed"); }); test("IS_PLATFORM=true, raw config has explicit services.inference.mode='your-own' → preserved (legacy override)", () => { process.env.IS_PLATFORM = "true"; // Pre-migration upgrade: workspace config still carries the legacy // mode value. The synthesis only fills when absent, so an explicit // disk value wins until migration 076 strips it. const raw: Record = { services: { inference: { mode: "your-own" }, }, }; const result = applyContextDefaultsToRawConfig(raw) as Record< string, unknown >; const services = result["services"] as Record; expect(services["inference"]!.mode).toBe("your-own"); }); test("IS_PLATFORM=false, raw config has no inference subtree → no synthesis", () => { process.env.IS_PLATFORM = "false"; const raw: Record = { llm: {}, }; const result = applyContextDefaultsToRawConfig(raw) as Record< string, unknown >; expect(result["services"]).toBeUndefined(); }); // ------------------------------------------------------------------------- // Malformed-but-parseable config.json — must not 500 the GET endpoint. // // `loadRawConfig()` is typed `Record` but `JSON.parse` // will happily return `null`, primitives, or arrays for a syntactically // valid file like `null` / `42` / `[]`. The helper must return those // payloads unchanged rather than throwing inside // `fillContextDefaultsForMissingKeys`. // ------------------------------------------------------------------------- test("IS_PLATFORM=true, raw config is null → returned unchanged (no throw)", () => { process.env.IS_PLATFORM = "true"; expect(applyContextDefaultsToRawConfig(null)).toBe(null); }); test("IS_PLATFORM=true, raw config is a primitive number → returned unchanged (no throw)", () => { process.env.IS_PLATFORM = "true"; expect(applyContextDefaultsToRawConfig(42)).toBe(42); }); test("IS_PLATFORM=true, raw config is an array → returned unchanged (no throw)", () => { process.env.IS_PLATFORM = "true"; const raw: unknown[] = [{ foo: "bar" }]; const result = applyContextDefaultsToRawConfig(raw); expect(result).toBe(raw); // No `services` key was synthesized onto the array. expect((result as { services?: unknown }).services).toBeUndefined(); }); test("IS_PLATFORM=true, raw config is a string → returned unchanged (no throw)", () => { process.env.IS_PLATFORM = "true"; expect(applyContextDefaultsToRawConfig("not-an-object")).toBe( "not-an-object", ); }); test("IS_PLATFORM=false, raw config is null → returned unchanged (no throw)", () => { // Sanity check: when there are no context defaults to apply, the helper // also short-circuits cleanly on non-object payloads. process.env.IS_PLATFORM = "false"; expect(applyContextDefaultsToRawConfig(null)).toBe(null); }); });