From 7a1e0c81b92b2ef81b5d81121e9875ce04a5ead1 Mon Sep 17 00:00:00 2001 From: clopen-set <33433326+clopen-set@users.noreply.github.com> Date: Tue, 14 Apr 2026 15:15:32 -0400 Subject: [PATCH] fix(environments): env-seed fallback for getPlatformUrl, revert H1 sync-on-switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under our invariant "each env has its own lockfile, and all assistants in that lockfile share a platform URL", the H1 workspace-config→lockfile sync on `vellum use`/`vellum wake` was load-bearing for nothing: switching the active assistant within a single env cannot change the platform URL. Revert that sync. When no lockfile is seeded yet, fall back to the current environment's seed URL instead of the hardcoded production default so `VELLUM_ENVIRONMENT=dev vellum …` targets `dev-platform.vellum.ai` out of the box. - Revert H1: `vellum use` no longer calls `syncActiveAssistantConfigToLockfile`, `vellum wake` no longer re-runs `syncConfigToLockfile`, and the helper itself is deleted (the hatch-time sync is still done by `syncConfigToLockfile`, unchanged). - `getPlatformUrl()` fallback: prefer `getCurrentEnvironment().platformUrl` over the hardcoded prod URL so non-prod CLI users get the right tenant before any assistant is registered. - Tests: drop the H1 sync-on-switch suite, add a dev-env seed fallback test, keep the existing prod fallback test. Co-Authored-By: Claude Opus 4.6 (1M context) --- cli/src/__tests__/platform-client.test.ts | 155 +--------------------- cli/src/commands/use.ts | 6 - cli/src/commands/wake.ts | 11 +- cli/src/lib/assistant-config.ts | 28 ---- cli/src/lib/platform-client.ts | 6 +- 5 files changed, 14 insertions(+), 192 deletions(-) diff --git a/cli/src/__tests__/platform-client.test.ts b/cli/src/__tests__/platform-client.test.ts index 92fbb0d4e8b..07ea2b4a3e0 100644 --- a/cli/src/__tests__/platform-client.test.ts +++ b/cli/src/__tests__/platform-client.test.ts @@ -1,7 +1,6 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { existsSync, - mkdirSync, mkdtempSync, readFileSync, rmSync, @@ -10,11 +9,6 @@ import { import { tmpdir } from "node:os"; import { join } from "node:path"; -import { - setActiveAssistant, - syncActiveAssistantConfigToLockfile, - syncConfigToLockfile, -} from "../lib/assistant-config.js"; import { clearPlatformToken, getPlatformUrl, @@ -193,153 +187,18 @@ describe("getPlatformUrl resolution order", () => { expect(getPlatformUrl()).toBe("https://env-after-blank.vellum.ai"); }); - test("falls back to production default when lockfile and env are unset", () => { + test("falls back to prod env seed URL when lockfile and VELLUM_PLATFORM_URL are unset (prod env)", () => { + // VELLUM_ENVIRONMENT is unset → production → prod seed URL. expect(getPlatformUrl()).toBe("https://platform.vellum.ai"); }); + test("falls back to dev env seed URL when VELLUM_ENVIRONMENT=dev", () => { + process.env.VELLUM_ENVIRONMENT = "dev"; + expect(getPlatformUrl()).toBe("https://dev-platform.vellum.ai"); + }); + test("trims whitespace from VELLUM_PLATFORM_URL", () => { process.env.VELLUM_PLATFORM_URL = " https://trimmed.vellum.ai "; expect(getPlatformUrl()).toBe("https://trimmed.vellum.ai"); }); }); - -describe("syncActiveAssistantConfigToLockfile on vellum use", () => { - let tempRoot: string; - let savedLockDir: string | undefined; - let savedEnv: string | undefined; - let savedPlatformUrl: string | undefined; - let savedBaseDataDir: string | undefined; - - beforeEach(() => { - savedLockDir = process.env.VELLUM_LOCKFILE_DIR; - savedEnv = process.env.VELLUM_ENVIRONMENT; - savedPlatformUrl = process.env.VELLUM_PLATFORM_URL; - savedBaseDataDir = process.env.BASE_DATA_DIR; - tempRoot = mkdtempSync(join(tmpdir(), "cli-active-sync-test-")); - process.env.VELLUM_LOCKFILE_DIR = tempRoot; - delete process.env.VELLUM_ENVIRONMENT; - delete process.env.VELLUM_PLATFORM_URL; - delete process.env.BASE_DATA_DIR; - }); - - afterEach(() => { - const restore = (name: string, value: string | undefined): void => { - if (value === undefined) delete process.env[name]; - else process.env[name] = value; - }; - restore("VELLUM_LOCKFILE_DIR", savedLockDir); - restore("VELLUM_ENVIRONMENT", savedEnv); - restore("VELLUM_PLATFORM_URL", savedPlatformUrl); - restore("BASE_DATA_DIR", savedBaseDataDir); - rmSync(tempRoot, { recursive: true, force: true }); - }); - - function writeWorkspaceConfig(instanceDir: string, platformUrl: string) { - const workspaceDir = join(instanceDir, ".vellum", "workspace"); - mkdirSync(workspaceDir, { recursive: true }); - writeFileSync( - join(workspaceDir, "config.json"), - JSON.stringify({ platform: { baseUrl: platformUrl } }, null, 2), - ); - } - - test("vellum use refreshes lockfile platformBaseUrl from the new active assistant's workspace config", () => { - // Set up two assistants with distinct platform URLs in their own - // per-instance workspace configs, mimicking the multi-tenant case - // where `alpha` targets prod and `beta` targets dev. - const alphaDir = join(tempRoot, "alpha-root"); - const betaDir = join(tempRoot, "beta-root"); - mkdirSync(alphaDir, { recursive: true }); - mkdirSync(betaDir, { recursive: true }); - writeWorkspaceConfig(alphaDir, "https://prod.vellum.ai"); - writeWorkspaceConfig(betaDir, "https://dev.vellum.ai"); - - // Seed the lockfile with two local entries + alpha as active. - writeFileSync( - join(tempRoot, ".vellum.lock.json"), - JSON.stringify( - { - activeAssistant: "alpha", - assistants: [ - { - assistantId: "alpha", - runtimeUrl: "http://127.0.0.1:7830", - cloud: "local", - resources: { - instanceDir: alphaDir, - daemonPort: 7821, - gatewayPort: 7830, - qdrantPort: 6333, - cesPort: 8090, - pidFile: join(alphaDir, ".vellum", "vellum.pid"), - }, - }, - { - assistantId: "beta", - runtimeUrl: "http://127.0.0.1:7831", - cloud: "local", - resources: { - instanceDir: betaDir, - daemonPort: 7822, - gatewayPort: 7831, - qdrantPort: 6334, - cesPort: 8091, - pidFile: join(betaDir, ".vellum", "vellum.pid"), - }, - }, - ], - }, - null, - 2, - ), - ); - - // Mimic the hatch-time sync for alpha so the lockfile has - // platformBaseUrl populated — this is what Fix G3 relies on. - process.env.BASE_DATA_DIR = alphaDir; - syncConfigToLockfile(); - delete process.env.BASE_DATA_DIR; - - expect(getPlatformUrl()).toBe("https://prod.vellum.ai"); - - // Simulate `vellum use beta`: switch active and run the new sync - // helper that use.ts now calls. - setActiveAssistant("beta"); - syncActiveAssistantConfigToLockfile("beta"); - - // getPlatformUrl must now return beta's tenant — the bug before this - // fix was that it kept returning the last-hatched assistant's URL. - expect(getPlatformUrl()).toBe("https://dev.vellum.ai"); - - // Switching back recovers alpha's URL. - setActiveAssistant("alpha"); - syncActiveAssistantConfigToLockfile("alpha"); - expect(getPlatformUrl()).toBe("https://prod.vellum.ai"); - }); - - test("syncActiveAssistantConfigToLockfile is a no-op for legacy entries without resources", () => { - // Legacy entry (no resources) — helper should skip silently without - // clobbering an existing platformBaseUrl in the lockfile. - writeFileSync( - join(tempRoot, ".vellum.lock.json"), - JSON.stringify( - { - activeAssistant: "legacy", - platformBaseUrl: "https://preexisting.vellum.ai", - assistants: [ - { - assistantId: "legacy", - runtimeUrl: "http://127.0.0.1:7830", - cloud: "remote", - }, - ], - }, - null, - 2, - ), - ); - - syncActiveAssistantConfigToLockfile("legacy"); - expect(getPlatformUrl()).toBe("https://preexisting.vellum.ai"); - }); -}); diff --git a/cli/src/commands/use.ts b/cli/src/commands/use.ts index 03b393d895c..f05eaa00f5e 100644 --- a/cli/src/commands/use.ts +++ b/cli/src/commands/use.ts @@ -2,7 +2,6 @@ import { findAssistantByName, getActiveAssistant, setActiveAssistant, - syncActiveAssistantConfigToLockfile, } from "../lib/assistant-config.js"; export async function use(): Promise { @@ -41,10 +40,5 @@ export async function use(): Promise { } setActiveAssistant(name); - // Refresh the lockfile's top-level platformBaseUrl from the newly-active - // assistant's workspace config so getPlatformUrl() — and downstream code - // like `vellum login`, ensureRegistration, and rollback — targets the - // right tenant. - syncActiveAssistantConfigToLockfile(name); console.log(`Active assistant set to '${name}'.`); } diff --git a/cli/src/commands/wake.ts b/cli/src/commands/wake.ts index 470427d4edf..76cb5d89f99 100644 --- a/cli/src/commands/wake.ts +++ b/cli/src/commands/wake.ts @@ -4,7 +4,6 @@ import { join } from "path"; import { resolveTargetAssistant, saveAssistantEntry, - syncConfigToLockfile, } from "../lib/assistant-config.js"; import { dockerResourceNames, wakeContainers } from "../lib/docker.js"; import { isProcessAlive, stopProcessByPidFile } from "../lib/process"; @@ -183,21 +182,17 @@ export async function wake(): Promise { } } - // Set BASE_DATA_DIR so ngrok and the config→lockfile sync read the - // correct instance's workspace config. Waking a different assistant must - // refresh the lockfile's top-level platformBaseUrl so getPlatformUrl() — - // and downstream code like `vellum login` and ensureRegistration — targets - // the tenant the woken assistant is configured for. + // Auto-start ngrok if webhook integrations (e.g. Telegram) are configured. + // Scope BASE_DATA_DIR to the woken instance so ngrok reads the correct + // instance config, then restore on any exit path. const prevBaseDataDir = process.env.BASE_DATA_DIR; process.env.BASE_DATA_DIR = resources.instanceDir; try { - // Auto-start ngrok if webhook integrations (e.g. Telegram) are configured. const ngrokChild = await maybeStartNgrokTunnel(resources.gatewayPort); if (ngrokChild?.pid) { const ngrokPidFile = join(resources.instanceDir, ".vellum", "ngrok.pid"); writeFileSync(ngrokPidFile, String(ngrokChild.pid)); } - syncConfigToLockfile(); } finally { if (prevBaseDataDir !== undefined) { process.env.BASE_DATA_DIR = prevBaseDataDir; diff --git a/cli/src/lib/assistant-config.ts b/cli/src/lib/assistant-config.ts index 6d69a2849fc..48cd8292671 100644 --- a/cli/src/lib/assistant-config.ts +++ b/cli/src/lib/assistant-config.ts @@ -499,31 +499,3 @@ export function syncConfigToLockfile(): void { // Config file unreadable — skip sync } } - -/** - * Sync the named assistant's workspace `config.json` to the lockfile's - * top-level `platformBaseUrl` field. Used by `vellum use` and `vellum wake` - * so `getPlatformUrl()` returns the tenant URL of the newly-active - * assistant rather than whichever assistant was most recently hatched. - * - * Temporarily scopes `BASE_DATA_DIR` to the target instance so - * {@link syncConfigToLockfile} reads from that instance's workspace - * config. No-op for legacy entries without `resources` (they share the - * legacy `~/.vellum/` workspace and the lockfile value is already correct). - */ -export function syncActiveAssistantConfigToLockfile(name: string): void { - const entry = findAssistantByName(name); - if (!entry?.resources) return; - - const prevBaseDataDir = process.env.BASE_DATA_DIR; - process.env.BASE_DATA_DIR = entry.resources.instanceDir; - try { - syncConfigToLockfile(); - } finally { - if (prevBaseDataDir !== undefined) { - process.env.BASE_DATA_DIR = prevBaseDataDir; - } else { - delete process.env.BASE_DATA_DIR; - } - } -} diff --git a/cli/src/lib/platform-client.ts b/cli/src/lib/platform-client.ts index 146ab7564b7..d25fd0c6453 100644 --- a/cli/src/lib/platform-client.ts +++ b/cli/src/lib/platform-client.ts @@ -27,14 +27,16 @@ function getPlatformTokenPath(): string { * know which instance to read from without first consulting the * lockfile anyway. * 2. `VELLUM_PLATFORM_URL` env var (explicit override, e.g. in CI). - * 3. Production default: `https://platform.vellum.ai`. + * 3. The current environment's seed URL (e.g. `https://dev-platform.vellum.ai` + * for `VELLUM_ENVIRONMENT=dev`, `https://platform.vellum.ai` for prod). + * This makes the CLI environment-aware when no lockfile entry exists yet. */ export function getPlatformUrl(): string { const lockfileUrl = getLockfilePlatformBaseUrl(); return ( lockfileUrl || process.env.VELLUM_PLATFORM_URL?.trim() || - "https://platform.vellum.ai" + getCurrentEnvironment().platformUrl ); }