test: slim pi auth discovery tests

This commit is contained in:
Peter Steinberger
2026-04-24 10:50:43 +01:00
parent b453293349
commit ed51963c47
5 changed files with 143 additions and 129 deletions

View File

@@ -0,0 +1,72 @@
import fs from "node:fs";
import { isRecord } from "../utils.js";
import { resolveProviderEnvApiKeyCandidates } from "./model-auth-env-vars.js";
import { resolveEnvApiKey } from "./model-auth-env.js";
import type { PiCredentialMap } from "./pi-auth-credentials.js";
export function addEnvBackedPiCredentials(
credentials: PiCredentialMap,
env: NodeJS.ProcessEnv = process.env,
): PiCredentialMap {
const next = { ...credentials };
// pi-coding-agent hides providers from its registry when auth storage lacks
// a matching credential entry. Mirror env-backed provider auth here so
// live/model discovery sees the same providers runtime auth can use.
for (const provider of Object.keys(resolveProviderEnvApiKeyCandidates({ env }))) {
if (next[provider]) {
continue;
}
const resolved = resolveEnvApiKey(provider, env);
if (!resolved?.apiKey) {
continue;
}
next[provider] = {
type: "api_key",
key: resolved.apiKey,
};
}
return next;
}
export function scrubLegacyStaticAuthJsonEntriesForDiscovery(pathname: string): void {
if (process.env.OPENCLAW_AUTH_STORE_READONLY === "1") {
return;
}
if (!fs.existsSync(pathname)) {
return;
}
let parsed: unknown;
try {
parsed = JSON.parse(fs.readFileSync(pathname, "utf8")) as unknown;
} catch {
return;
}
if (!isRecord(parsed)) {
return;
}
let changed = false;
for (const [provider, value] of Object.entries(parsed)) {
if (!isRecord(value)) {
continue;
}
if (value.type !== "api_key") {
continue;
}
delete parsed[provider];
changed = true;
}
if (!changed) {
return;
}
if (Object.keys(parsed).length === 0) {
fs.rmSync(pathname, { force: true });
return;
}
fs.writeFileSync(pathname, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
fs.chmodSync(pathname, 0o600);
}

View File

@@ -0,0 +1,50 @@
import { resolveProviderSyntheticAuthWithPlugin } from "../plugins/provider-runtime.js";
import { resolveRuntimeSyntheticAuthProviderRefs } from "../plugins/synthetic-auth.runtime.js";
import {
ensureAuthProfileStore,
loadAuthProfileStoreForSecretsRuntime,
} from "./auth-profiles/store.js";
import { resolvePiCredentialMapFromStore, type PiCredentialMap } from "./pi-auth-credentials.js";
import { addEnvBackedPiCredentials } from "./pi-auth-discovery-core.js";
export type DiscoverAuthStorageOptions = {
readOnly?: boolean;
};
export function resolvePiCredentialsForDiscovery(
agentDir: string,
options?: DiscoverAuthStorageOptions,
): PiCredentialMap {
const store =
options?.readOnly === true
? loadAuthProfileStoreForSecretsRuntime(agentDir)
: ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false });
const credentials = addEnvBackedPiCredentials(resolvePiCredentialMapFromStore(store));
for (const provider of resolveRuntimeSyntheticAuthProviderRefs()) {
if (credentials[provider]) {
continue;
}
const resolved = resolveProviderSyntheticAuthWithPlugin({
provider,
context: {
config: undefined,
provider,
providerConfig: undefined,
},
});
const apiKey = resolved?.apiKey?.trim();
if (!apiKey) {
continue;
}
credentials[provider] = {
type: "api_key",
key: apiKey,
};
}
return credentials;
}
export {
addEnvBackedPiCredentials,
scrubLegacyStaticAuthJsonEntriesForDiscovery,
} from "./pi-auth-discovery-core.js";

View File

@@ -6,7 +6,7 @@ import { resolvePiCredentialMapFromStore } from "./pi-auth-credentials.js";
import {
addEnvBackedPiCredentials,
scrubLegacyStaticAuthJsonEntriesForDiscovery,
} from "./pi-model-discovery.js";
} from "./pi-auth-discovery-core.js";
vi.mock("./model-auth-env-vars.js", () => ({
resolveProviderEnvApiKeyCandidates: () => ({

View File

@@ -2,7 +2,6 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { saveAuthProfileStore } from "./auth-profiles/store.js";
const resolveRuntimeSyntheticAuthProviderRefs = vi.hoisted(() => vi.fn(() => ["claude-cli"]));
@@ -30,7 +29,7 @@ vi.mock("../plugins/provider-runtime.js", () => ({
resolveExternalAuthProfilesWithPlugins: () => [],
}));
let discoverAuthStorage: typeof import("./pi-model-discovery.js").discoverAuthStorage;
let resolvePiCredentialsForDiscovery: typeof import("./pi-auth-discovery.js").resolvePiCredentialsForDiscovery;
async function withAgentDir(run: (agentDir: string) => Promise<void>): Promise<void> {
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-pi-synthetic-auth-"));
@@ -43,7 +42,7 @@ async function withAgentDir(run: (agentDir: string) => Promise<void>): Promise<v
describe("pi model discovery synthetic auth", () => {
beforeAll(async () => {
({ discoverAuthStorage } = await import("./pi-model-discovery.js"));
({ resolvePiCredentialsForDiscovery } = await import("./pi-auth-discovery.js"));
});
beforeEach(() => {
@@ -57,17 +56,9 @@ describe("pi model discovery synthetic auth", () => {
vi.unstubAllEnvs();
});
it("mirrors plugin-owned synthetic cli auth into pi auth storage", async () => {
it("mirrors plugin-owned synthetic cli auth into pi credential discovery", async () => {
await withAgentDir(async (agentDir) => {
saveAuthProfileStore(
{
version: 1,
profiles: {},
},
agentDir,
);
const authStorage = discoverAuthStorage(agentDir);
const credentials = resolvePiCredentialsForDiscovery(agentDir, { readOnly: true });
expect(resolveRuntimeSyntheticAuthProviderRefs).toHaveBeenCalled();
expect(resolveProviderSyntheticAuthWithPlugin).toHaveBeenCalledWith({
@@ -78,8 +69,10 @@ describe("pi model discovery synthetic auth", () => {
providerConfig: undefined,
},
});
expect(authStorage.hasAuth("claude-cli")).toBe(true);
await expect(authStorage.getApiKey("claude-cli")).resolves.toBe("claude-cli-access-token");
expect(credentials["claude-cli"]).toEqual({
type: "api_key",
key: "claude-cli-access-token",
});
});
});
});

View File

@@ -1,4 +1,3 @@
import fs from "node:fs";
import path from "node:path";
import type { Api, Model } from "@mariozechner/pi-ai";
import * as PiCodingAgent from "@mariozechner/pi-coding-agent";
@@ -11,17 +10,14 @@ import {
applyProviderResolvedModelCompatWithPlugins,
applyProviderResolvedTransportWithPlugin,
normalizeProviderResolvedModelWithPlugin,
resolveProviderSyntheticAuthWithPlugin,
} from "../plugins/provider-runtime.js";
import { resolveRuntimeSyntheticAuthProviderRefs } from "../plugins/synthetic-auth.runtime.js";
import { isRecord } from "../utils.js";
import type { PiCredentialMap } from "./pi-auth-credentials.js";
import {
ensureAuthProfileStore,
loadAuthProfileStoreForSecretsRuntime,
} from "./auth-profiles/store.js";
import { resolveProviderEnvApiKeyCandidates } from "./model-auth-env-vars.js";
import { resolveEnvApiKey } from "./model-auth-env.js";
import { resolvePiCredentialMapFromStore, type PiCredentialMap } from "./pi-auth-credentials.js";
resolvePiCredentialsForDiscovery,
scrubLegacyStaticAuthJsonEntriesForDiscovery,
type DiscoverAuthStorageOptions,
} from "./pi-auth-discovery.js";
import { normalizeProviderId } from "./provider-id.js";
const PiAuthStorageClass = PiCodingAgent.AuthStorage;
@@ -168,49 +164,6 @@ function createOpenClawModelRegistry(
return registry;
}
export function scrubLegacyStaticAuthJsonEntriesForDiscovery(pathname: string): void {
if (process.env.OPENCLAW_AUTH_STORE_READONLY === "1") {
return;
}
if (!fs.existsSync(pathname)) {
return;
}
let parsed: unknown;
try {
parsed = JSON.parse(fs.readFileSync(pathname, "utf8")) as unknown;
} catch {
return;
}
if (!isRecord(parsed)) {
return;
}
let changed = false;
for (const [provider, value] of Object.entries(parsed)) {
if (!isRecord(value)) {
continue;
}
if (value.type !== "api_key") {
continue;
}
delete parsed[provider];
changed = true;
}
if (!changed) {
return;
}
if (Object.keys(parsed).length === 0) {
fs.rmSync(pathname, { force: true });
return;
}
fs.writeFileSync(pathname, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
fs.chmodSync(pathname, 0o600);
}
function createAuthStorage(AuthStorageLike: unknown, path: string, creds: PiCredentialMap) {
const withInMemory = AuthStorageLike as { inMemory?: (data?: unknown) => unknown };
if (typeof withInMemory.inMemory === "function") {
@@ -256,67 +209,6 @@ function createAuthStorage(AuthStorageLike: unknown, path: string, creds: PiCred
return withRuntimeOverride;
}
export function addEnvBackedPiCredentials(
credentials: PiCredentialMap,
env: NodeJS.ProcessEnv = process.env,
): PiCredentialMap {
const next = { ...credentials };
// pi-coding-agent hides providers from its registry when auth storage lacks
// a matching credential entry. Mirror env-backed provider auth here so
// live/model discovery sees the same providers runtime auth can use.
for (const provider of Object.keys(resolveProviderEnvApiKeyCandidates({ env }))) {
if (next[provider]) {
continue;
}
const resolved = resolveEnvApiKey(provider, env);
if (!resolved?.apiKey) {
continue;
}
next[provider] = {
type: "api_key",
key: resolved.apiKey,
};
}
return next;
}
type DiscoverAuthStorageOptions = {
readOnly?: boolean;
};
export function resolvePiCredentialsForDiscovery(
agentDir: string,
options?: DiscoverAuthStorageOptions,
): PiCredentialMap {
const store =
options?.readOnly === true
? loadAuthProfileStoreForSecretsRuntime(agentDir)
: ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false });
const credentials = addEnvBackedPiCredentials(resolvePiCredentialMapFromStore(store));
for (const provider of resolveRuntimeSyntheticAuthProviderRefs()) {
if (credentials[provider]) {
continue;
}
const resolved = resolveProviderSyntheticAuthWithPlugin({
provider,
context: {
config: undefined,
provider,
providerConfig: undefined,
},
});
const apiKey = resolved?.apiKey?.trim();
if (!apiKey) {
continue;
}
credentials[provider] = {
type: "api_key",
key: apiKey,
};
}
return credentials;
}
// Compatibility helpers for pi-coding-agent 0.50+ (discover* helpers removed).
export function discoverAuthStorage(
agentDir: string,
@@ -342,3 +234,10 @@ export function discoverModels(
options,
);
}
export {
addEnvBackedPiCredentials,
resolvePiCredentialsForDiscovery,
scrubLegacyStaticAuthJsonEntriesForDiscovery,
type DiscoverAuthStorageOptions,
} from "./pi-auth-discovery.js";