Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 7 additions & 148 deletions cli/src/__tests__/platform-client.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
Expand All @@ -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,
Expand Down Expand Up @@ -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 <name> 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");
});
});
6 changes: 0 additions & 6 deletions cli/src/commands/use.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
findAssistantByName,
getActiveAssistant,
setActiveAssistant,
syncActiveAssistantConfigToLockfile,
} from "../lib/assistant-config.js";

export async function use(): Promise<void> {
Expand Down Expand Up @@ -41,10 +40,5 @@ export async function use(): Promise<void> {
}

setActiveAssistant(name);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resync lockfile URL when selecting active assistant

After this change, vellum use only calls setActiveAssistant(name) and no longer refreshes platformBaseUrl from the selected assistant's workspace config, so getPlatformUrl() can keep returning the previously persisted tenant URL. This is user-visible when assistants in one lockfile have different platform.baseUrl values (e.g. created/edited via config overrides): switching active assistants then running platform-facing commands (login/registration/upgrade paths that call getPlatformUrl) can target the wrong host.

Useful? React with 👍 / 👎.

// 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}'.`);
}
11 changes: 3 additions & 8 deletions cli/src/commands/wake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -183,21 +182,17 @@ export async function wake(): Promise<void> {
}
}

// 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);
Comment on lines 188 to 191

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Sync lockfile URL during wake for legacy lockfiles

The wake path still scopes BASE_DATA_DIR to the target instance but no longer performs a config→lockfile sync in that scope, so existing lockfiles without platformBaseUrl never get backfilled from the assistant's workspace config. In that state, getPlatformUrl() falls through to environment seed defaults, which can misroute subsequent CLI platform calls for assistants configured with a non-seed platform URL until they are re-hatched.

Useful? React with 👍 / 👎.

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;
Expand Down
28 changes: 0 additions & 28 deletions cli/src/lib/assistant-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
6 changes: 4 additions & 2 deletions cli/src/lib/platform-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}

Expand Down