Skip to content
Closed
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
4 changes: 2 additions & 2 deletions __tests__/api/agent-server-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe("agent server config", () => {

it("prefills the settings form from environment defaults when local settings are empty", () => {
vi.stubEnv("VITE_BACKEND_BASE_URL", "https://env-agent.example.com/");
vi.stubEnv("VITE_SESSION_API_KEY", "env-session-key");
vi.stubEnv("VITE_LOCAL_BACKEND_API_KEY", "env-session-key");

expect(getAgentServerFormDefaults()).toEqual({
baseUrl: "https://env-agent.example.com",
Expand All @@ -75,7 +75,7 @@ describe("agent server config", () => {

it("lets saved interface settings override environment defaults", () => {
vi.stubEnv("VITE_BACKEND_BASE_URL", "https://env-agent.example.com");
vi.stubEnv("VITE_SESSION_API_KEY", "env-session-key");
vi.stubEnv("VITE_LOCAL_BACKEND_API_KEY", "env-session-key");

saveAgentServerConfig({
baseUrl: "https://saved-agent.example.com/",
Expand Down
6 changes: 3 additions & 3 deletions __tests__/api/backend-registry/storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ describe("backend-registry storage", () => {
});

it("fills a missing API key on the default Local backend from env defaults", () => {
vi.stubEnv("VITE_SESSION_API_KEY", "fresh-session-key");
vi.stubEnv("VITE_LOCAL_BACKEND_API_KEY", "fresh-session-key");
window.localStorage.setItem(
BACKENDS_STORAGE_KEY,
JSON.stringify([
Expand Down Expand Up @@ -123,7 +123,7 @@ describe("backend-registry storage", () => {


it("refreshes a stale API key on the default Local backend from env defaults", () => {
vi.stubEnv("VITE_SESSION_API_KEY", "fresh-session-key");
vi.stubEnv("VITE_LOCAL_BACKEND_API_KEY", "fresh-session-key");
window.localStorage.setItem(
BACKENDS_STORAGE_KEY,
JSON.stringify([
Expand Down Expand Up @@ -152,7 +152,7 @@ describe("backend-registry storage", () => {
});

it("does not fill the default Local backend API key after its host is edited", () => {
vi.stubEnv("VITE_SESSION_API_KEY", "fresh-session-key");
vi.stubEnv("VITE_LOCAL_BACKEND_API_KEY", "fresh-session-key");
window.localStorage.setItem(
BACKENDS_STORAGE_KEY,
JSON.stringify([
Expand Down
4 changes: 2 additions & 2 deletions __tests__/api/to-app-conversation-session-key.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ afterEach(() => {
});

describe("toAppConversation session_api_key hydration", () => {
it("prefers the configured VITE_SESSION_API_KEY over a stale stored default-local apiKey", () => {
vi.stubEnv("VITE_SESSION_API_KEY", "fresh-session-key");
it("prefers the configured VITE_LOCAL_BACKEND_API_KEY over a stale stored default-local apiKey", () => {
vi.stubEnv("VITE_LOCAL_BACKEND_API_KEY", "fresh-session-key");

setRegisteredBackends([
{
Expand Down
14 changes: 7 additions & 7 deletions __tests__/scripts/dev-extra-backend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { afterEach, describe, expect, it } from "vitest";
import { buildExtraBackendConfig } from "../../scripts/dev-extra-backend.mjs";
import {
buildSafeDevConfig,
resetPersistedSessionApiKeyCache,
resetPersistedApiKeyCache,
} from "../../scripts/dev-safe.mjs";

const repoRoot = path.resolve(
Expand All @@ -29,17 +29,17 @@ describe("buildExtraBackendConfig", () => {
const dir = keyDirs.pop();
if (dir) rmSync(dir, { recursive: true, force: true });
}
resetPersistedSessionApiKeyCache();
resetPersistedApiKeyCache();
});

function isolatedKeyPath(): string {
const dir = mkdtempSync(path.join(tmpdir(), "extra-backend-key-"));
keyDirs.push(dir);
return path.join(dir, "session-api-key.txt");
return path.join(dir, "local-backend-api-key.txt");
}

it("defaults to ports 18002/18003 distinct from the bundled instance", () => {
const env = { OH_SESSION_API_KEY_PATH: isolatedKeyPath() };
const env = { OH_LOCAL_BACKEND_API_KEY_PATH: isolatedKeyPath() };
const bundled = buildSafeDevConfig(repoRoot, env);
const extra = buildExtraBackendConfig(repoRoot, env);

Expand All @@ -55,7 +55,7 @@ describe("buildExtraBackendConfig", () => {
const config = buildExtraBackendConfig(repoRoot, {
OH_CANVAS_EXTRA_BACKEND_PORT: "29000",
OH_CANVAS_EXTRA_VSCODE_PORT: "29001",
OH_SESSION_API_KEY_PATH: isolatedKeyPath(),
OH_LOCAL_BACKEND_API_KEY_PATH: isolatedKeyPath(),
});

expect(config.backendPort).toBe(29000);
Expand All @@ -66,7 +66,7 @@ describe("buildExtraBackendConfig", () => {
it("shares state dir, conversations, bash events, and secret key with the bundled config", () => {
const env = {
OH_CANVAS_SAFE_STATE_DIR: "/tmp/canvas-state",
OH_SESSION_API_KEY_PATH: isolatedKeyPath(),
OH_LOCAL_BACKEND_API_KEY_PATH: isolatedKeyPath(),
};
const bundled = buildSafeDevConfig(repoRoot, env);
const extra = buildExtraBackendConfig(repoRoot, env);
Expand All @@ -82,7 +82,7 @@ describe("buildExtraBackendConfig", () => {
expect(() =>
buildExtraBackendConfig(repoRoot, {
OH_CANVAS_EXTRA_BACKEND_PORT: "not-a-port",
OH_SESSION_API_KEY_PATH: isolatedKeyPath(),
OH_LOCAL_BACKEND_API_KEY_PATH: isolatedKeyPath(),
}),
).toThrow(/Invalid port/);
});
Expand Down
100 changes: 68 additions & 32 deletions __tests__/scripts/dev-safe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ import {
validateLocalAgentServerPath,
findFreePort,
findFreePorts,
getOrCreatePersistedSessionApiKey,
resetPersistedSessionApiKeyCache,
getOrCreatePersistedLocalBackendApiKey,
resetPersistedApiKeyCache,
} from "../../scripts/dev-safe.mjs";
import {
existsSync,
mkdtempSync,
mkdirSync,
readFileSync,
Expand Down Expand Up @@ -183,17 +184,17 @@ describe("buildSafeDevConfigAsync", () => {
rmSync(keyTmp, { recursive: true, force: true });
keyTmp = null;
}
resetPersistedSessionApiKeyCache();
resetPersistedApiKeyCache();
});

function tempKeyPath(): string {
keyTmp = mkdtempSync(path.join(tmpdir(), "dev-safe-async-key-"));
return path.join(keyTmp, "session-api-key.txt");
return path.join(keyTmp, "local-backend-api-key.txt");
}

it("returns config with dynamically allocated ports", async () => {
const config = await buildSafeDevConfigAsync(repoRoot, {
OH_SESSION_API_KEY_PATH: tempKeyPath(),
OH_LOCAL_BACKEND_API_KEY_PATH: tempKeyPath(),
});

expect(typeof config.backendPort).toBe("number");
Expand All @@ -219,7 +220,7 @@ describe("buildSafeDevConfigAsync", () => {
// Request the busy port via env var
const config = await buildSafeDevConfigAsync(repoRoot, {
OH_CANVAS_SAFE_BACKEND_PORT: busyPort.toString(),
OH_SESSION_API_KEY_PATH: tempKeyPath(),
OH_LOCAL_BACKEND_API_KEY_PATH: tempKeyPath(),
});

// Backend port should NOT be busyPort since it's taken
Expand Down Expand Up @@ -494,19 +495,19 @@ describe("buildSafeDevConfig", () => {
rmSync(keyTmp, { recursive: true, force: true });
keyTmp = null;
}
resetPersistedSessionApiKeyCache();
resetPersistedApiKeyCache();
});

function tempKeyPath(): string {
keyTmp = mkdtempSync(path.join(tmpdir(), "dev-safe-key-"));
return path.join(keyTmp, "session-api-key.txt");
return path.join(keyTmp, "local-backend-api-key.txt");
}

it("builds isolated default paths and ports", () => {
const cwd = "/workspace/project/agent-canvas";

const config = buildSafeDevConfig(cwd, {
OH_SESSION_API_KEY_PATH: tempKeyPath(),
OH_LOCAL_BACKEND_API_KEY_PATH: tempKeyPath(),
});

expect(config.backendPort).toBe(18000);
Expand Down Expand Up @@ -539,7 +540,7 @@ describe("buildSafeDevConfig", () => {
OH_CANVAS_SAFE_VSCODE_PORT: "19010",
OH_CANVAS_SAFE_STATE_DIR: ".tmp/dev-safe",
VITE_WORKING_DIR: "/workspace/custom-repo",
OH_SESSION_API_KEY_PATH: tempKeyPath(),
OH_LOCAL_BACKEND_API_KEY_PATH: tempKeyPath(),
});

expect(config.backendPort).toBe(19000);
Expand All @@ -550,83 +551,83 @@ describe("buildSafeDevConfig", () => {
expect(config.workingDir).toBe("/workspace/custom-repo");
});

it("falls back to the persisted session key file when no env override is set", () => {
it("falls back to the persisted local-backend key file when no env override is set", () => {
const keyPath = tempKeyPath();
const config = buildSafeDevConfig("/workspace/project/agent-canvas", {
OH_SESSION_API_KEY_PATH: keyPath,
OH_LOCAL_BACKEND_API_KEY_PATH: keyPath,
});

// A fresh hex key was generated and persisted.
expect(config.sessionApiKey).toMatch(/^[a-f0-9]{64}$/);
expect(readFileSync(keyPath, "utf8").trim()).toBe(config.sessionApiKey);
expect(config.localBackendApiKey).toMatch(/^[a-f0-9]{64}$/);
expect(readFileSync(keyPath, "utf8").trim()).toBe(config.localBackendApiKey);
});

it("reuses the same key across config builds, simulating restarts", () => {
const keyPath = tempKeyPath();

const first = buildSafeDevConfig("/workspace/project/agent-canvas", {
OH_SESSION_API_KEY_PATH: keyPath,
OH_LOCAL_BACKEND_API_KEY_PATH: keyPath,
});

// Simulate a fresh process by clearing the in-memory cache; the file
// on disk is what should make the key stable.
resetPersistedSessionApiKeyCache();
resetPersistedApiKeyCache();

const second = buildSafeDevConfig("/workspace/project/agent-canvas", {
OH_SESSION_API_KEY_PATH: keyPath,
OH_LOCAL_BACKEND_API_KEY_PATH: keyPath,
});

expect(second.sessionApiKey).toBe(first.sessionApiKey);
expect(second.localBackendApiKey).toBe(first.localBackendApiKey);
});

it("env-provided session keys take precedence over the persisted file", () => {
it("LOCAL_BACKEND_API_KEY takes precedence over the persisted file", () => {
const keyPath = tempKeyPath();
// Pre-seed the file with one key.
mkdirSync(path.dirname(keyPath), { recursive: true });
writeFileSync(keyPath, "persisted-key-value\n");

const config = buildSafeDevConfig("/workspace/project/agent-canvas", {
SESSION_API_KEY: "env-key-wins",
OH_SESSION_API_KEY_PATH: keyPath,
LOCAL_BACKEND_API_KEY: "env-key-wins",
OH_LOCAL_BACKEND_API_KEY_PATH: keyPath,
});

expect(config.sessionApiKey).toBe("env-key-wins");
expect(config.localBackendApiKey).toBe("env-key-wins");
// The file is left untouched.
expect(readFileSync(keyPath, "utf8").trim()).toBe("persisted-key-value");
});
});

describe("getOrCreatePersistedSessionApiKey", () => {
describe("getOrCreatePersistedLocalBackendApiKey", () => {
let dir: string | null = null;

afterEach(() => {
if (dir) {
rmSync(dir, { recursive: true, force: true });
dir = null;
}
resetPersistedSessionApiKeyCache();
resetPersistedApiKeyCache();
});

function tempPath(): string {
dir = mkdtempSync(path.join(tmpdir(), "session-key-"));
return path.join(dir, "nested", "session-api-key.txt");
dir = mkdtempSync(path.join(tmpdir(), "local-backend-key-"));
return path.join(dir, "nested", "local-backend-api-key.txt");
}

it("creates the file (and parent dirs) with a hex key on first call", () => {
const filePath = tempPath();
const key = getOrCreatePersistedSessionApiKey(filePath);
const key = getOrCreatePersistedLocalBackendApiKey(filePath);

expect(key).toMatch(/^[a-f0-9]{64}$/);
expect(readFileSync(filePath, "utf8").trim()).toBe(key);
});

it("returns the existing key on subsequent calls (after cache reset)", () => {
const filePath = tempPath();
const first = getOrCreatePersistedSessionApiKey(filePath);
const first = getOrCreatePersistedLocalBackendApiKey(filePath);

resetPersistedSessionApiKeyCache();
resetPersistedApiKeyCache();

const second = getOrCreatePersistedSessionApiKey(filePath);
const second = getOrCreatePersistedLocalBackendApiKey(filePath);
expect(second).toBe(first);
});

Expand All @@ -635,7 +636,7 @@ describe("getOrCreatePersistedSessionApiKey", () => {
mkdirSync(path.dirname(filePath), { recursive: true });
writeFileSync(filePath, " abcdef1234 \n");

const key = getOrCreatePersistedSessionApiKey(filePath);
const key = getOrCreatePersistedLocalBackendApiKey(filePath);
expect(key).toBe("abcdef1234");
});

Expand All @@ -644,10 +645,45 @@ describe("getOrCreatePersistedSessionApiKey", () => {
mkdirSync(path.dirname(filePath), { recursive: true });
writeFileSync(filePath, " \n");

const key = getOrCreatePersistedSessionApiKey(filePath);
const key = getOrCreatePersistedLocalBackendApiKey(filePath);
expect(key).toMatch(/^[a-f0-9]{64}$/);
expect(readFileSync(filePath, "utf8").trim()).toBe(key);
});

it("migrates a legacy sibling session-api-key.txt on first run", () => {
const filePath = tempPath();
const legacyPath = path.join(
path.dirname(filePath),
"session-api-key.txt",
);
mkdirSync(path.dirname(filePath), { recursive: true });
writeFileSync(legacyPath, "legacy-session-key\n");

const key = getOrCreatePersistedLocalBackendApiKey(filePath);

expect(key).toBe("legacy-session-key");
expect(readFileSync(filePath, "utf8").trim()).toBe("legacy-session-key");
// The legacy file has been renamed (not copied) so a second run sees
// only the new file.
expect(existsSync(legacyPath)).toBe(false);
});

it("does not migrate when the target file already exists", () => {
const filePath = tempPath();
const legacyPath = path.join(
path.dirname(filePath),
"session-api-key.txt",
);
mkdirSync(path.dirname(filePath), { recursive: true });
writeFileSync(legacyPath, "legacy-session-key\n");
writeFileSync(filePath, "current-key\n");

const key = getOrCreatePersistedLocalBackendApiKey(filePath);

expect(key).toBe("current-key");
// Legacy file is left untouched.
expect(readFileSync(legacyPath, "utf8").trim()).toBe("legacy-session-key");
});
});

describe("buildNpmScriptCommand", () => {
Expand Down
9 changes: 4 additions & 5 deletions __tests__/scripts/dev-static.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,18 @@ import { describe, expect, it } from "vitest";
import { buildAutomationBackendEnv } from "../../scripts/dev-static.mjs";

describe("dev-static", () => {
it("passes the agent-server session key to the automation backend", () => {
it("uses the unified local-backend key for both automation auth and the agent-server callback", () => {
const env = buildAutomationBackendEnv({
agentServerPort: 18000,
ingressPort: 8000,
localApiKey: "automation-local-key",
sessionApiKey: "agent-session-key",
localBackendApiKey: "local-backend-key",
stateDir: "/tmp/agent-canvas-state",
});

expect(env).toMatchObject({
AUTOMATION_AGENT_SERVER_URL: "http://localhost:18000",
AUTOMATION_AGENT_SERVER_API_KEY: "agent-session-key",
AUTOMATION_LOCAL_API_KEY: "automation-local-key",
AUTOMATION_AGENT_SERVER_API_KEY: "local-backend-key",
AUTOMATION_LOCAL_API_KEY: "local-backend-key",
});
});
});
Loading
Loading