Files
openclaw/src/commands/onboard-auth.test.ts

326 lines
10 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import type { OAuthCredentials } from "@mariozechner/pi-ai";
import { afterEach, describe, expect, it } from "vitest";
import {
applyAuthProfileConfig,
upsertApiKeyProfile,
writeOAuthCredentials,
} from "../plugins/provider-auth-helpers.js";
import {
createAuthTestLifecycle,
readAuthProfilesForAgent,
setupAuthTestEnv,
} from "./test-wizard-helpers.js";
describe("writeOAuthCredentials", () => {
const lifecycle = createAuthTestLifecycle([
"OPENCLAW_STATE_DIR",
"OPENCLAW_AGENT_DIR",
"PI_CODING_AGENT_DIR",
"OPENCLAW_OAUTH_DIR",
]);
let tempStateDir: string;
const authProfilePathFor = (dir: string) => path.join(dir, "auth-profiles.json");
afterEach(async () => {
await lifecycle.cleanup();
});
it("writes auth-profiles.json under OPENCLAW_AGENT_DIR when set", async () => {
const env = await setupAuthTestEnv("openclaw-oauth-");
lifecycle.setStateDir(env.stateDir);
const creds = {
refresh: "refresh-token",
access: "access-token",
expires: Date.now() + 60_000,
} satisfies OAuthCredentials;
await writeOAuthCredentials("openai-codex", creds);
const parsed = await readAuthProfilesForAgent<{
profiles?: Record<string, OAuthCredentials & { type?: string }>;
}>(env.agentDir);
expect(parsed.profiles?.["openai-codex:default"]).toMatchObject({
refresh: "refresh-token",
access: "access-token",
type: "oauth",
});
await expect(
fs.readFile(path.join(env.stateDir, "agents", "main", "agent", "auth-profiles.json"), "utf8"),
).rejects.toThrow();
});
it("writes OAuth credentials to all sibling agent dirs when syncSiblingAgents=true", async () => {
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-oauth-sync-"));
process.env.OPENCLAW_STATE_DIR = tempStateDir;
const mainAgentDir = path.join(tempStateDir, "agents", "main", "agent");
const kidAgentDir = path.join(tempStateDir, "agents", "kid", "agent");
const workerAgentDir = path.join(tempStateDir, "agents", "worker", "agent");
await fs.mkdir(mainAgentDir, { recursive: true });
await fs.mkdir(kidAgentDir, { recursive: true });
await fs.mkdir(workerAgentDir, { recursive: true });
process.env.OPENCLAW_AGENT_DIR = kidAgentDir;
process.env.PI_CODING_AGENT_DIR = kidAgentDir;
const creds = {
refresh: "refresh-sync",
access: "access-sync",
expires: Date.now() + 60_000,
} satisfies OAuthCredentials;
await writeOAuthCredentials("openai-codex", creds, undefined, {
syncSiblingAgents: true,
});
for (const dir of [mainAgentDir, kidAgentDir, workerAgentDir]) {
const raw = await fs.readFile(authProfilePathFor(dir), "utf8");
const parsed = JSON.parse(raw) as {
profiles?: Record<string, OAuthCredentials & { type?: string }>;
};
expect(parsed.profiles?.["openai-codex:default"]).toMatchObject({
refresh: "refresh-sync",
access: "access-sync",
type: "oauth",
});
}
});
it("writes OAuth credentials only to target dir by default", async () => {
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-oauth-nosync-"));
process.env.OPENCLAW_STATE_DIR = tempStateDir;
const mainAgentDir = path.join(tempStateDir, "agents", "main", "agent");
const kidAgentDir = path.join(tempStateDir, "agents", "kid", "agent");
await fs.mkdir(mainAgentDir, { recursive: true });
await fs.mkdir(kidAgentDir, { recursive: true });
process.env.OPENCLAW_AGENT_DIR = kidAgentDir;
process.env.PI_CODING_AGENT_DIR = kidAgentDir;
const creds = {
refresh: "refresh-kid",
access: "access-kid",
expires: Date.now() + 60_000,
} satisfies OAuthCredentials;
await writeOAuthCredentials("openai-codex", creds, kidAgentDir);
const kidRaw = await fs.readFile(authProfilePathFor(kidAgentDir), "utf8");
const kidParsed = JSON.parse(kidRaw) as {
profiles?: Record<string, OAuthCredentials & { type?: string }>;
};
expect(kidParsed.profiles?.["openai-codex:default"]).toMatchObject({
access: "access-kid",
type: "oauth",
});
await expect(fs.readFile(authProfilePathFor(mainAgentDir), "utf8")).rejects.toThrow();
});
it("syncs siblings from explicit agentDir outside OPENCLAW_STATE_DIR", async () => {
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-oauth-external-"));
process.env.OPENCLAW_STATE_DIR = tempStateDir;
// Create standard-layout agents tree *outside* OPENCLAW_STATE_DIR
const externalRoot = path.join(tempStateDir, "external", "agents");
const extMain = path.join(externalRoot, "main", "agent");
const extKid = path.join(externalRoot, "kid", "agent");
const extWorker = path.join(externalRoot, "worker", "agent");
await fs.mkdir(extMain, { recursive: true });
await fs.mkdir(extKid, { recursive: true });
await fs.mkdir(extWorker, { recursive: true });
const creds = {
refresh: "refresh-ext",
access: "access-ext",
expires: Date.now() + 60_000,
} satisfies OAuthCredentials;
await writeOAuthCredentials("openai-codex", creds, extKid, {
syncSiblingAgents: true,
});
// All siblings under the external root should have credentials
for (const dir of [extMain, extKid, extWorker]) {
const raw = await fs.readFile(authProfilePathFor(dir), "utf8");
const parsed = JSON.parse(raw) as {
profiles?: Record<string, OAuthCredentials & { type?: string }>;
};
expect(parsed.profiles?.["openai-codex:default"]).toMatchObject({
refresh: "refresh-ext",
access: "access-ext",
type: "oauth",
});
}
// Global state dir should NOT have credentials written
const globalMain = path.join(tempStateDir, "agents", "main", "agent");
await expect(fs.readFile(authProfilePathFor(globalMain), "utf8")).rejects.toThrow();
});
});
describe("upsertApiKeyProfile", () => {
const lifecycle = createAuthTestLifecycle([
"OPENCLAW_STATE_DIR",
"OPENCLAW_AGENT_DIR",
"PI_CODING_AGENT_DIR",
]);
afterEach(async () => {
await lifecycle.cleanup();
});
it("writes to OPENCLAW_AGENT_DIR when set", async () => {
const env = await setupAuthTestEnv("openclaw-minimax-", { agentSubdir: "custom-agent" });
lifecycle.setStateDir(env.stateDir);
upsertApiKeyProfile({ provider: "minimax", input: "sk-minimax-test" });
const parsed = await readAuthProfilesForAgent<{
profiles?: Record<string, { type?: string; provider?: string; key?: string }>;
}>(env.agentDir);
expect(parsed.profiles?.["minimax:default"]).toMatchObject({
type: "api_key",
provider: "minimax",
key: "sk-minimax-test",
});
await expect(
fs.readFile(path.join(env.stateDir, "agents", "main", "agent", "auth-profiles.json"), "utf8"),
).rejects.toThrow();
});
});
describe("applyAuthProfileConfig", () => {
it("promotes the newly selected profile to the front of auth.order", () => {
const next = applyAuthProfileConfig(
{
auth: {
profiles: {
"anthropic:default": { provider: "anthropic", mode: "api_key" },
},
order: { anthropic: ["anthropic:default"] },
},
},
{
profileId: "anthropic:work",
provider: "anthropic",
mode: "oauth",
},
);
expect(next.auth?.order?.anthropic).toEqual(["anthropic:work", "anthropic:default"]);
});
it("creates provider order when switching from legacy oauth to api_key without explicit order", () => {
const next = applyAuthProfileConfig(
{
auth: {
profiles: {
"kilocode:legacy": { provider: "kilocode", mode: "oauth" },
},
},
},
{
profileId: "kilocode:default",
provider: "kilocode",
mode: "api_key",
},
);
expect(next.auth?.order?.kilocode).toEqual(["kilocode:default", "kilocode:legacy"]);
});
it("repairs aliased auth.order keys instead of duplicating them", () => {
const next = applyAuthProfileConfig(
{
auth: {
profiles: {
"zai:default": { provider: "z.ai", mode: "api_key" },
},
order: { "z.ai": ["zai:default"] },
},
},
{
profileId: "zai:work",
provider: "z-ai",
mode: "oauth",
},
);
expect(next.auth?.order).toEqual({
zai: ["zai:work", "zai:default"],
});
});
it("merges split canonical and aliased auth.order entries for the same provider", () => {
const next = applyAuthProfileConfig(
{
auth: {
profiles: {
"zai:default": { provider: "z.ai", mode: "api_key" },
"zai:backup": { provider: "z-ai", mode: "token" },
},
order: {
zai: ["zai:default"],
"z.ai": ["zai:backup"],
},
},
},
{
profileId: "zai:work",
provider: "z-ai",
mode: "oauth",
},
);
expect(next.auth?.order).toEqual({
zai: ["zai:work", "zai:default", "zai:backup"],
});
});
it("keeps implicit round-robin when no mixed provider modes are present", () => {
const next = applyAuthProfileConfig(
{
auth: {
profiles: {
"kilocode:legacy": { provider: "kilocode", mode: "api_key" },
},
},
},
{
profileId: "kilocode:default",
provider: "kilocode",
mode: "api_key",
},
);
expect(next.auth?.order).toBeUndefined();
});
it("stores display metadata without overloading email", () => {
const next = applyAuthProfileConfig(
{},
{
profileId: "openai-codex:id-abc",
provider: "openai-codex",
mode: "oauth",
displayName: "Work account",
},
);
expect(next.auth?.profiles?.["openai-codex:id-abc"]).toEqual({
provider: "openai-codex",
mode: "oauth",
displayName: "Work account",
});
});
});