test: share oauth test helpers

This commit is contained in:
Peter Steinberger
2026-04-18 22:46:49 +01:00
parent 310d2db312
commit e89e214516
7 changed files with 104 additions and 200 deletions

View File

@@ -4,19 +4,18 @@ import path from "node:path";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { FILE_LOCK_TIMEOUT_ERROR_CODE, type FileLockTimeoutError } from "../../infra/file-lock.js";
import { captureEnv } from "../../test-utils/env.js";
import {
OAUTH_AGENT_ENV_KEYS,
createExpiredOauthStore,
resolveApiKeyForProfileInTest,
} from "./oauth-test-utils.js";
import { resolveAuthStorePath, resolveOAuthRefreshLockPath } from "./paths.js";
import { clearRuntimeAuthProfileStoreSnapshots, saveAuthProfileStore } from "./store.js";
import type { AuthProfileStore, OAuthCredential } from "./types.js";
import type { OAuthCredential } from "./types.js";
let resolveApiKeyForProfile: typeof import("./oauth.js").resolveApiKeyForProfile;
let resetOAuthRefreshQueuesForTest: typeof import("./oauth.js").resetOAuthRefreshQueuesForTest;
function resolveApiKeyForProfileInTest(
params: Omit<Parameters<typeof resolveApiKeyForProfile>[0], "cfg">,
) {
return resolveApiKeyForProfile({ cfg: {}, ...params });
}
const { withFileLockMock } = vi.hoisted(() => ({
withFileLockMock: vi.fn(
async <T>(_filePath: string, _options: unknown, run: () => Promise<T>) => await run(),
@@ -78,24 +77,6 @@ vi.mock("./external-cli-sync.js", () => ({
existing !== incoming,
}));
function createExpiredOauthStore(params: {
profileId: string;
provider: string;
}): AuthProfileStore {
return {
version: 1,
profiles: {
[params.profileId]: {
type: "oauth",
provider: params.provider,
access: "stale-access",
refresh: "stale-refresh",
expires: Date.now() - 60_000,
} satisfies OAuthCredential,
},
};
}
function createLockTimeoutError(lockPath: string): FileLockTimeoutError {
return Object.assign(new Error(`file lock timeout for ${lockPath.slice(0, -5)}`), {
code: FILE_LOCK_TIMEOUT_ERROR_CODE as typeof FILE_LOCK_TIMEOUT_ERROR_CODE,
@@ -104,11 +85,7 @@ function createLockTimeoutError(lockPath: string): FileLockTimeoutError {
}
describe("OAuth refresh lock timeout classification", () => {
const envSnapshot = captureEnv([
"OPENCLAW_STATE_DIR",
"OPENCLAW_AGENT_DIR",
"PI_CODING_AGENT_DIR",
]);
const envSnapshot = captureEnv(OAUTH_AGENT_ENV_KEYS);
let tempRoot = "";
let agentDir = "";
let caseIndex = 0;
@@ -155,7 +132,7 @@ describe("OAuth refresh lock timeout classification", () => {
});
try {
await resolveApiKeyForProfileInTest({
await resolveApiKeyForProfileInTest(resolveApiKeyForProfile, {
store,
profileId,
agentDir,
@@ -194,7 +171,7 @@ describe("OAuth refresh lock timeout classification", () => {
});
try {
await resolveApiKeyForProfileInTest({
await resolveApiKeyForProfileInTest(resolveApiKeyForProfile, {
store,
profileId,
agentDir,

View File

@@ -4,19 +4,18 @@ import path from "node:path";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { resetFileLockStateForTest } from "../../infra/file-lock.js";
import { captureEnv } from "../../test-utils/env.js";
import {
OAUTH_AGENT_ENV_KEYS,
createExpiredOauthStore,
resolveApiKeyForProfileInTest,
} from "./oauth-test-utils.js";
import { resolveApiKeyForProfile, resetOAuthRefreshQueuesForTest } from "./oauth.js";
import {
clearRuntimeAuthProfileStoreSnapshots,
ensureAuthProfileStore,
saveAuthProfileStore,
} from "./store.js";
import type { AuthProfileStore, OAuthCredential } from "./types.js";
function resolveApiKeyForProfileInTest(
params: Omit<Parameters<typeof resolveApiKeyForProfile>[0], "cfg">,
) {
return resolveApiKeyForProfile({ cfg: {}, ...params });
}
import type { OAuthCredential } from "./types.js";
const {
refreshProviderOAuthCredentialWithPluginMock,
@@ -81,30 +80,8 @@ vi.mock("./external-cli-sync.js", () => ({
existing !== incoming,
}));
function createExpiredOauthStore(params: {
profileId: string;
provider: string;
}): AuthProfileStore {
return {
version: 1,
profiles: {
[params.profileId]: {
type: "oauth",
provider: params.provider,
access: "stale-access",
refresh: "stale-refresh",
expires: Date.now() - 60_000,
} satisfies OAuthCredential,
},
};
}
describe("OAuth refresh in-process queue", () => {
const envSnapshot = captureEnv([
"OPENCLAW_STATE_DIR",
"OPENCLAW_AGENT_DIR",
"PI_CODING_AGENT_DIR",
]);
const envSnapshot = captureEnv(OAUTH_AGENT_ENV_KEYS);
let tempRoot = "";
let agentDir = "";
let caseIndex = 0;
@@ -163,12 +140,12 @@ describe("OAuth refresh in-process queue", () => {
});
const [first, second] = await Promise.all([
resolveApiKeyForProfileInTest({
resolveApiKeyForProfileInTest(resolveApiKeyForProfile, {
store: ensureAuthProfileStore(agentDir),
profileId,
agentDir,
}).catch((e) => e),
resolveApiKeyForProfileInTest({
resolveApiKeyForProfileInTest(resolveApiKeyForProfile, {
store: ensureAuthProfileStore(agentDir),
profileId,
agentDir,
@@ -230,7 +207,7 @@ describe("OAuth refresh in-process queue", () => {
const results = await Promise.all(
Array.from({ length: 10 }, () =>
resolveApiKeyForProfileInTest({
resolveApiKeyForProfileInTest(resolveApiKeyForProfile, {
store: ensureAuthProfileStore(agentDir),
profileId,
agentDir,

View File

@@ -0,0 +1,54 @@
import type { resolveApiKeyForProfile } from "./oauth.js";
import type { AuthProfileStore, OAuthCredential } from "./types.js";
export const OAUTH_AGENT_ENV_KEYS = [
"OPENCLAW_STATE_DIR",
"OPENCLAW_AGENT_DIR",
"PI_CODING_AGENT_DIR",
];
export function resolveApiKeyForProfileInTest(
resolver: typeof resolveApiKeyForProfile,
params: Omit<Parameters<typeof resolveApiKeyForProfile>[0], "cfg">,
) {
return resolver({ cfg: {}, ...params });
}
export function oauthCred(params: {
provider: string;
access: string;
refresh: string;
expires: number;
accountId?: string;
email?: string;
}): OAuthCredential {
return { type: "oauth", ...params };
}
export function storeWith(profileId: string, cred: OAuthCredential): AuthProfileStore {
return { version: 1, profiles: { [profileId]: cred } };
}
export function createExpiredOauthStore(params: {
profileId: string;
provider: string;
access?: string;
refresh?: string;
accountId?: string;
email?: string;
}): AuthProfileStore {
return {
version: 1,
profiles: {
[params.profileId]: {
type: "oauth",
provider: params.provider,
access: params.access ?? "cached-access-token",
refresh: params.refresh ?? "refresh-token",
expires: Date.now() - 60_000,
accountId: params.accountId,
email: params.email,
} satisfies OAuthCredential,
},
};
}

View File

@@ -4,6 +4,12 @@ import path from "node:path";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { resetFileLockStateForTest } from "../../infra/file-lock.js";
import { captureEnv } from "../../test-utils/env.js";
import {
OAUTH_AGENT_ENV_KEYS,
oauthCred,
resolveApiKeyForProfileInTest,
storeWith,
} from "./oauth-test-utils.js";
import { resolveApiKeyForProfile, resetOAuthRefreshQueuesForTest } from "./oauth.js";
import {
clearRuntimeAuthProfileStoreSnapshots,
@@ -17,12 +23,6 @@ import type { AuthProfileStore, OAuthCredential } from "./types.js";
// sub-agent store. Unit tests cover policy variants; this suite proves each
// production branch refuses a mismatched accountId.
function resolveApiKeyForProfileInTest(
params: Omit<Parameters<typeof resolveApiKeyForProfile>[0], "cfg">,
) {
return resolveApiKeyForProfile({ cfg: {}, ...params });
}
const {
refreshProviderOAuthCredentialWithPluginMock,
formatProviderAuthProfileApiKeyWithPluginMock,
@@ -86,27 +86,8 @@ vi.mock("./external-cli-sync.js", () => ({
existing !== incoming,
}));
function oauthCred(params: {
provider: string;
access: string;
refresh: string;
expires: number;
accountId?: string;
email?: string;
}): OAuthCredential {
return { type: "oauth", ...params };
}
function storeWith(profileId: string, cred: OAuthCredential): AuthProfileStore {
return { version: 1, profiles: { [profileId]: cred } };
}
describe("OAuth credential adoption is identity-gated", () => {
const envSnapshot = captureEnv([
"OPENCLAW_STATE_DIR",
"OPENCLAW_AGENT_DIR",
"PI_CODING_AGENT_DIR",
]);
const envSnapshot = captureEnv(OAUTH_AGENT_ENV_KEYS);
let tempRoot = "";
let caseIndex = 0;
let mainAgentDir = "";
@@ -183,7 +164,7 @@ describe("OAuth credential adoption is identity-gated", () => {
mainAgentDir,
);
const result = await resolveApiKeyForProfileInTest({
const result = await resolveApiKeyForProfileInTest(resolveApiKeyForProfile, {
store: ensureAuthProfileStore(subAgentDir),
profileId,
agentDir: subAgentDir,
@@ -252,7 +233,7 @@ describe("OAuth credential adoption is identity-gated", () => {
}) as never,
);
const result = await resolveApiKeyForProfileInTest({
const result = await resolveApiKeyForProfileInTest(resolveApiKeyForProfile, {
store: ensureAuthProfileStore(subAgentDir),
profileId,
agentDir: subAgentDir,
@@ -333,7 +314,7 @@ describe("OAuth credential adoption is identity-gated", () => {
});
await expect(
resolveApiKeyForProfileInTest({
resolveApiKeyForProfileInTest(resolveApiKeyForProfile, {
store: ensureAuthProfileStore(subAgentDir),
profileId,
agentDir: subAgentDir,

View File

@@ -4,12 +4,17 @@ import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { resetFileLockStateForTest } from "../../infra/file-lock.js";
import { captureEnv } from "../../test-utils/env.js";
import {
OAUTH_AGENT_ENV_KEYS,
createExpiredOauthStore,
resolveApiKeyForProfileInTest,
} from "./oauth-test-utils.js";
import {
clearRuntimeAuthProfileStoreSnapshots,
ensureAuthProfileStore,
saveAuthProfileStore,
} from "./store.js";
import type { AuthProfileStore, OAuthCredential } from "./types.js";
import type { OAuthCredential } from "./types.js";
let resolveApiKeyForProfile: typeof import("./oauth.js").resolveApiKeyForProfile;
let resetOAuthRefreshQueuesForTest: typeof import("./oauth.js").resetOAuthRefreshQueuesForTest;
@@ -18,12 +23,6 @@ async function loadOAuthModuleForTest() {
({ resolveApiKeyForProfile, resetOAuthRefreshQueuesForTest } = await import("./oauth.js"));
}
function resolveApiKeyForProfileInTest(
params: Omit<Parameters<typeof resolveApiKeyForProfile>[0], "cfg">,
) {
return resolveApiKeyForProfile({ cfg: {}, ...params });
}
const {
refreshProviderOAuthCredentialWithPluginMock,
formatProviderAuthProfileApiKeyWithPluginMock,
@@ -80,36 +79,8 @@ vi.mock("./external-cli-sync.js", () => ({
existing !== incoming,
}));
function createExpiredOauthStore(params: {
profileId: string;
provider: string;
access?: string;
refresh?: string;
accountId?: string;
email?: string;
}): AuthProfileStore {
return {
version: 1,
profiles: {
[params.profileId]: {
type: "oauth",
provider: params.provider,
access: params.access ?? "cached-access-token",
refresh: params.refresh ?? "refresh-token",
expires: Date.now() - 60_000,
accountId: params.accountId,
email: params.email,
} satisfies OAuthCredential,
},
};
}
describe("resolveApiKeyForProfile cross-agent refresh coordination (#26322)", () => {
const envSnapshot = captureEnv([
"OPENCLAW_STATE_DIR",
"OPENCLAW_AGENT_DIR",
"PI_CODING_AGENT_DIR",
]);
const envSnapshot = captureEnv(OAUTH_AGENT_ENV_KEYS);
let tempRoot = "";
let mainAgentDir = "";
@@ -183,7 +154,7 @@ describe("resolveApiKeyForProfile cross-agent refresh coordination (#26322)", ()
// performed; the remaining agents adopt the resulting fresh credentials.
const results = await Promise.all(
subAgents.map((agentDir) =>
resolveApiKeyForProfileInTest({
resolveApiKeyForProfileInTest(resolveApiKeyForProfile, {
store: ensureAuthProfileStore(agentDir),
profileId,
agentDir,

View File

@@ -5,6 +5,11 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi }
import { resetFileLockStateForTest } from "../../infra/file-lock.js";
import { captureEnv } from "../../test-utils/env.js";
import { __testing as externalAuthTesting } from "./external-auth.js";
import {
OAUTH_AGENT_ENV_KEYS,
createExpiredOauthStore,
resolveApiKeyForProfileInTest,
} from "./oauth-test-utils.js";
import { resolveApiKeyForProfile, resetOAuthRefreshQueuesForTest } from "./oauth.js";
import {
clearRuntimeAuthProfileStoreSnapshots,
@@ -13,12 +18,6 @@ import {
} from "./store.js";
import type { AuthProfileStore, OAuthCredential } from "./types.js";
function resolveApiKeyForProfileInTest(
params: Omit<Parameters<typeof resolveApiKeyForProfile>[0], "cfg">,
) {
return resolveApiKeyForProfile({ cfg: {}, ...params });
}
const {
refreshProviderOAuthCredentialWithPluginMock,
formatProviderAuthProfileApiKeyWithPluginMock,
@@ -85,36 +84,8 @@ vi.mock("./external-cli-sync.js", () => ({
existing !== incoming,
}));
function createExpiredOauthStore(params: {
profileId: string;
provider: string;
access?: string;
refresh?: string;
accountId?: string;
email?: string;
}): AuthProfileStore {
return {
version: 1,
profiles: {
[params.profileId]: {
type: "oauth",
provider: params.provider,
access: params.access ?? "cached-access-token",
refresh: params.refresh ?? "refresh-token",
expires: Date.now() - 60_000,
accountId: params.accountId,
email: params.email,
} satisfies OAuthCredential,
},
};
}
describe("resolveApiKeyForProfile OAuth refresh mirror-to-main (#26322)", () => {
const envSnapshot = captureEnv([
"OPENCLAW_STATE_DIR",
"OPENCLAW_AGENT_DIR",
"PI_CODING_AGENT_DIR",
]);
const envSnapshot = captureEnv(OAUTH_AGENT_ENV_KEYS);
let tempRoot = "";
let caseIndex = 0;
let mainAgentDir = "";
@@ -178,7 +149,7 @@ describe("resolveApiKeyForProfile OAuth refresh mirror-to-main (#26322)", () =>
}) as never,
);
const result = await resolveApiKeyForProfileInTest({
const result = await resolveApiKeyForProfileInTest(resolveApiKeyForProfile, {
store: ensureAuthProfileStore(subAgentDir),
profileId,
agentDir: subAgentDir,
@@ -222,7 +193,7 @@ describe("resolveApiKeyForProfile OAuth refresh mirror-to-main (#26322)", () =>
// Main-agent refresh uses undefined agentDir; the mirror path is a no-op
// (local == main). Just make sure the main store still reflects the refresh
// and no double-write happens.
const result = await resolveApiKeyForProfileInTest({
const result = await resolveApiKeyForProfileInTest(resolveApiKeyForProfile, {
store: ensureAuthProfileStore(undefined),
profileId,
agentDir: undefined,
@@ -274,7 +245,7 @@ describe("resolveApiKeyForProfile OAuth refresh mirror-to-main (#26322)", () =>
// Refresh mock intentionally left as default-undefined — it should not
// be called, the pre-refresh adopt wins.
const result = await resolveApiKeyForProfileInTest({
const result = await resolveApiKeyForProfileInTest(resolveApiKeyForProfile, {
store: ensureAuthProfileStore(subAgentDir),
profileId,
agentDir: subAgentDir,
@@ -337,7 +308,7 @@ describe("resolveApiKeyForProfile OAuth refresh mirror-to-main (#26322)", () =>
throw new Error("upstream 503 service unavailable");
});
const result = await resolveApiKeyForProfileInTest({
const result = await resolveApiKeyForProfileInTest(resolveApiKeyForProfile, {
store: ensureAuthProfileStore(subAgentDir),
profileId,
agentDir: subAgentDir,
@@ -380,7 +351,7 @@ describe("resolveApiKeyForProfile OAuth refresh mirror-to-main (#26322)", () =>
}) as never,
);
const result = await resolveApiKeyForProfileInTest({
const result = await resolveApiKeyForProfileInTest(resolveApiKeyForProfile, {
store: ensureAuthProfileStore(subAgentDir),
profileId,
agentDir: subAgentDir,

View File

@@ -4,6 +4,7 @@ import path from "node:path";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { resetFileLockStateForTest } from "../../infra/file-lock.js";
import { captureEnv } from "../../test-utils/env.js";
import { OAUTH_AGENT_ENV_KEYS, createExpiredOauthStore } from "./oauth-test-utils.js";
import {
clearRuntimeAuthProfileStoreSnapshots,
ensureAuthProfileStore,
@@ -77,36 +78,8 @@ async function readPersistedStore(agentDir: string): Promise<AuthProfileStore> {
) as AuthProfileStore;
}
function createExpiredOauthStore(params: {
profileId: string;
provider: string;
access?: string;
refresh?: string;
accountId?: string;
email?: string;
}): AuthProfileStore {
return {
version: 1,
profiles: {
[params.profileId]: {
type: "oauth",
provider: params.provider,
access: params.access ?? "cached-access-token",
refresh: params.refresh ?? "refresh-token",
expires: Date.now() - 60_000,
accountId: params.accountId,
email: params.email,
},
},
};
}
describe("resolveApiKeyForProfile openai-codex refresh fallback", () => {
const envSnapshot = captureEnv([
"OPENCLAW_STATE_DIR",
"OPENCLAW_AGENT_DIR",
"PI_CODING_AGENT_DIR",
]);
const envSnapshot = captureEnv(OAUTH_AGENT_ENV_KEYS);
let tempRoot = "";
let agentDir = "";
let caseIndex = 0;