Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]
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, vi } from "vitest";
import {
applyAuthProfileConfig,
upsertApiKeyProfile,
writeOAuthCredentials,
} from "../plugins/provider-auth-helpers.js";
import {
createAuthTestLifecycle,
readAuthProfilesForAgent,
setupAuthTestEnv,
} from "./test-wizard-helpers.js";
const providerEnvVarsById = vi.hoisted(
(): Record<string, readonly string[]> => ({
"cloudflare-ai-gateway": ["CLOUDFLARE_AI_GATEWAY_API_KEY"],
byteplus: ["BYTEPLUS_API_KEY"],
moonshot: ["MOONSHOT_API_KEY"],
openai: ["OPENAI_API_KEY"],
opencode: ["OPENCODE_API_KEY"],
"opencode-go": ["OPENCODE_API_KEY"],
volcengine: ["VOLCANO_ENGINE_API_KEY"],
}),
);
vi.mock("../agents/agent-paths.js", () => ({
resolveOpenClawAgentDir: () => process.env.OPENCLAW_AGENT_DIR ?? "/tmp/openclaw-agent",
}));
vi.mock("../config/paths.js", () => ({
resolveStateDir: () => process.env.OPENCLAW_STATE_DIR ?? "/tmp/openclaw-state",
}));
vi.mock("../agents/auth-profiles/profiles.js", async () => {
const fs = await import("node:fs");
const path = await import("node:path");
return {
upsertAuthProfile: (params: { profileId: string; credential: unknown; agentDir?: string }) => {
const agentDir = params.agentDir ?? process.env.OPENCLAW_AGENT_DIR ?? "/tmp/openclaw-agent";
const file = path.join(agentDir, "auth-profiles.json");
fs.mkdirSync(agentDir, { recursive: true });
const existing = (() => {
try {
return JSON.parse(fs.readFileSync(file, "utf8")) as {
version?: number;
profiles?: Record<string, unknown>;
};
} catch {
return { version: 1, profiles: {} };
}
})();
fs.writeFileSync(
file,
`${JSON.stringify(
{
version: existing.version ?? 1,
profiles: {
...existing.profiles,
[params.profileId]: params.credential,
},
},
null,
2,
)}\n`,
);
},
};
});
vi.mock("../agents/provider-auth-aliases.js", () => ({
resolveProviderIdForAuth: (provider: string) => {
const normalized = provider.trim().toLowerCase();
if (normalized === "z.ai" || normalized === "z-ai") {
return "zai";
}
return normalized;
},
}));
vi.mock("../secrets/provider-env-vars.js", () => ({
getProviderEnvVars: vi.fn((provider: string) => providerEnvVarsById[provider] ?? []),
}));
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 secret refs", () => {
const lifecycle = createAuthTestLifecycle([
"OPENCLAW_STATE_DIR",
"OPENCLAW_AGENT_DIR",
"PI_CODING_AGENT_DIR",
"MOONSHOT_API_KEY",
"OPENAI_API_KEY",
"CLOUDFLARE_AI_GATEWAY_API_KEY",
"VOLCANO_ENGINE_API_KEY",
"BYTEPLUS_API_KEY",
"OPENCODE_API_KEY",
]);
type AuthProfileEntry = { key?: string; keyRef?: unknown; metadata?: unknown };
afterEach(async () => {
await lifecycle.cleanup();
});
async function readProfile(
agentDir: string,
profileId: string,
): Promise<AuthProfileEntry | undefined> {
const parsed = await readAuthProfilesForAgent<{
profiles?: Record<string, AuthProfileEntry>;
}>(agentDir);
return parsed.profiles?.[profileId];
}
it("handles plaintext, ref mode, and inline env-ref provider keys", async () => {
const env = await setupAuthTestEnv("openclaw-onboard-auth-credentials-");
lifecycle.setStateDir(env.stateDir);
process.env.MOONSHOT_API_KEY = "sk-moonshot-env"; // pragma: allowlist secret
process.env.OPENAI_API_KEY = "sk-openai-env"; // pragma: allowlist secret
upsertApiKeyProfile({
provider: "moonshot",
input: "sk-moonshot-env",
agentDir: env.agentDir,
});
upsertApiKeyProfile({ provider: "openai", input: "sk-openai-env", agentDir: env.agentDir });
expect(await readProfile(env.agentDir, "moonshot:default")).toMatchObject({
key: "sk-moonshot-env",
});
expect((await readProfile(env.agentDir, "moonshot:default"))?.keyRef).toBeUndefined();
expect(await readProfile(env.agentDir, "openai:default")).toMatchObject({
key: "sk-openai-env",
});
expect((await readProfile(env.agentDir, "openai:default"))?.keyRef).toBeUndefined();
upsertApiKeyProfile({
provider: "moonshot",
input: "sk-moonshot-env",
agentDir: env.agentDir,
options: { secretInputMode: "ref" }, // pragma: allowlist secret
});
upsertApiKeyProfile({
provider: "openai",
input: "sk-openai-env",
agentDir: env.agentDir,
options: { secretInputMode: "ref" }, // pragma: allowlist secret
});
upsertApiKeyProfile({
provider: "moonshot",
input: "${MOONSHOT_API_KEY}",
agentDir: env.agentDir,
profileId: "moonshot:inline",
});
process.env.MOONSHOT_API_KEY = "sk-moonshot-other"; // pragma: allowlist secret
upsertApiKeyProfile({
provider: "moonshot",
input: "sk-moonshot-plaintext",
agentDir: env.agentDir,
profileId: "moonshot:plain",
});
expect(await readProfile(env.agentDir, "moonshot:default")).toMatchObject({
keyRef: { source: "env", provider: "default", id: "MOONSHOT_API_KEY" },
});
expect((await readProfile(env.agentDir, "moonshot:default"))?.key).toBeUndefined();
expect(await readProfile(env.agentDir, "openai:default")).toMatchObject({
keyRef: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
});
expect((await readProfile(env.agentDir, "openai:default"))?.key).toBeUndefined();
expect(await readProfile(env.agentDir, "moonshot:inline")).toMatchObject({
keyRef: { source: "env", provider: "default", id: "MOONSHOT_API_KEY" },
});
expect(await readProfile(env.agentDir, "moonshot:plain")).toMatchObject({
key: "sk-moonshot-plaintext",
});
expect((await readProfile(env.agentDir, "moonshot:plain"))?.keyRef).toBeUndefined();
});
it("stores provider-specific env refs and metadata in ref mode", async () => {
const env = await setupAuthTestEnv("openclaw-onboard-auth-credentials-provider-ref-");
lifecycle.setStateDir(env.stateDir);
process.env.CLOUDFLARE_AI_GATEWAY_API_KEY = "cf-secret"; // pragma: allowlist secret
process.env.VOLCANO_ENGINE_API_KEY = "volcengine-secret"; // pragma: allowlist secret
process.env.BYTEPLUS_API_KEY = "byteplus-secret"; // pragma: allowlist secret
process.env.OPENCODE_API_KEY = "sk-opencode-env"; // pragma: allowlist secret
upsertApiKeyProfile({
provider: "cloudflare-ai-gateway",
input: "cf-secret",
agentDir: env.agentDir,
options: { secretInputMode: "ref" }, // pragma: allowlist secret
metadata: {
accountId: "account-1",
gatewayId: "gateway-1",
},
});
for (const [provider, input] of [
["volcengine", "volcengine-secret"],
["byteplus", "byteplus-secret"],
["opencode", "sk-opencode-env"],
["opencode-go", "sk-opencode-env"],
] as const) {
upsertApiKeyProfile({
provider,
input,
agentDir: env.agentDir,
options: { secretInputMode: "ref" }, // pragma: allowlist secret
});
}
expect(await readProfile(env.agentDir, "cloudflare-ai-gateway:default")).toMatchObject({
keyRef: { source: "env", provider: "default", id: "CLOUDFLARE_AI_GATEWAY_API_KEY" },
metadata: { accountId: "account-1", gatewayId: "gateway-1" },
});
expect((await readProfile(env.agentDir, "cloudflare-ai-gateway:default"))?.key).toBeUndefined();
expect(await readProfile(env.agentDir, "volcengine:default")).toMatchObject({
keyRef: { source: "env", provider: "default", id: "VOLCANO_ENGINE_API_KEY" },
});
expect(await readProfile(env.agentDir, "byteplus:default")).toMatchObject({
keyRef: { source: "env", provider: "default", id: "BYTEPLUS_API_KEY" },
});
expect(await readProfile(env.agentDir, "opencode:default")).toMatchObject({
keyRef: { source: "env", provider: "default", id: "OPENCODE_API_KEY" },
});
expect(await readProfile(env.agentDir, "opencode-go:default")).toMatchObject({
keyRef: { source: "env", provider: "default", id: "OPENCODE_API_KEY" },
});
});
});
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",
});
});
});
¤ Dauer der Verarbeitung: 0.50 Sekunden
(vorverarbeitet am 2026-04-27)
¤
*© Formatika GbR, Deutschland
|
|