mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-15 13:19:24 +02:00
* test: add pi codex runtime contract coverage * test: expand pi codex tool runtime contracts * test: tighten tool runtime contracts * test: reset tool contract param cache * test: document codex tool middleware fixture * test: type pi tool contract events * test: satisfy pi tool contract test types * test: cover tool media telemetry contracts * test: reset plugin runtime after tool contracts * test: add auth profile runtime contracts * test: strengthen auth profile runtime contracts * test: clarify auth profile contract fixtures * test: expand auth profile contract matrix * test: assert unrelated cli auth isolation * test: expand auth profile contract matrix * test: tighten auth profile contract expectations * test: add outcome fallback runtime contracts * test: strengthen outcome fallback contracts * test: isolate outcome fallback contracts * test: cover codex terminal outcome signals * test: expand terminal fallback contracts * test: add delivery no reply runtime contracts * test: document json no-reply delivery gap * test: align delivery contract fixtures * test: add transcript repair runtime contracts * test: tighten transcript repair contracts * test: add prompt overlay runtime contracts * test: tighten prompt overlay contract scope * test: type prompt overlay contracts * test: add schema normalization runtime contracts * test: clarify schema normalization contract gaps * test: simplify schema normalization contracts * test: tighten schema normalization contract gaps * test: cover compaction schema contract * test: satisfy schema contract lint * test: add transport params runtime contracts * test: tighten transport params contract scope * test: isolate transport params contracts * test: lock exact transport defaults * feat: add agent runtime plan foundation * fix: preserve codex harness auth profiles * fix: route followup delivery through runtime plan * fix: normalize parameter-free openai tool schemas * fix: satisfy runtime plan type checks * fix: narrow followup delivery runtime planning * fix: apply codex app-server auth profiles * fix: classify codex terminal outcomes * fix: prevent harness auth leakage into unrelated cli providers * feat: expand agent runtime plan policy contract * fix: route pi runtime policy through runtime plan * fix: route codex runtime policy through runtime plan * fix: route fallback outcome classification through runtime plan * refactor: make runtime plan contracts topology-safe * fix: restore runtime plan test type coverage * fix: align runtime plan schema contract assertions * fix: stabilize incomplete turn runtime tests * fix: stabilize codex native web search test * fix: preserve codex auth profile secret refs * fix: keep runtime resolved refs canonical * fix: preserve permissive nested openai schemas * fix: accept Codex auth provider aliases * test: update media-only groups mock * fix: resolve runtime plan rebase checks * fix: resolve runtime plan rebase checks --------- Co-authored-by: Eva <eva@100yen.org> Co-authored-by: Peter Steinberger <steipete@gmail.com>
367 lines
12 KiB
TypeScript
367 lines
12 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { upsertAuthProfile } from "openclaw/plugin-sdk/provider-auth";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
applyCodexAppServerAuthProfile,
|
|
bridgeCodexAppServerStartOptions,
|
|
refreshCodexAppServerAuthTokens,
|
|
} from "./auth-bridge.js";
|
|
|
|
const oauthMocks = vi.hoisted(() => ({
|
|
refreshOpenAICodexToken: vi.fn(),
|
|
}));
|
|
|
|
const providerRuntimeMocks = vi.hoisted(() => ({
|
|
formatProviderAuthProfileApiKeyWithPlugin: vi.fn(),
|
|
refreshProviderOAuthCredentialWithPlugin: vi.fn(
|
|
async (params: { context: { refresh: string } }) => {
|
|
const refreshed = await oauthMocks.refreshOpenAICodexToken(params.context.refresh);
|
|
return refreshed
|
|
? {
|
|
...params.context,
|
|
...refreshed,
|
|
type: "oauth",
|
|
provider: "openai-codex",
|
|
}
|
|
: undefined;
|
|
},
|
|
),
|
|
}));
|
|
|
|
vi.mock("@mariozechner/pi-ai/oauth", () => ({
|
|
getOAuthApiKey: vi.fn(),
|
|
getOAuthProviders: () => [],
|
|
loginOpenAICodex: vi.fn(),
|
|
refreshOpenAICodexToken: oauthMocks.refreshOpenAICodexToken,
|
|
}));
|
|
|
|
vi.mock("../../../../src/plugins/provider-runtime.runtime.js", () => ({
|
|
formatProviderAuthProfileApiKeyWithPlugin:
|
|
providerRuntimeMocks.formatProviderAuthProfileApiKeyWithPlugin,
|
|
refreshProviderOAuthCredentialWithPlugin:
|
|
providerRuntimeMocks.refreshProviderOAuthCredentialWithPlugin,
|
|
}));
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
oauthMocks.refreshOpenAICodexToken.mockReset();
|
|
providerRuntimeMocks.formatProviderAuthProfileApiKeyWithPlugin.mockReset();
|
|
providerRuntimeMocks.refreshProviderOAuthCredentialWithPlugin.mockClear();
|
|
});
|
|
|
|
describe("bridgeCodexAppServerStartOptions", () => {
|
|
it("leaves Codex app-server start options unchanged", async () => {
|
|
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
|
|
const startOptions = {
|
|
transport: "stdio" as const,
|
|
command: "codex",
|
|
args: ["app-server"],
|
|
headers: { authorization: "Bearer dev-token" },
|
|
env: { CODEX_HOME: "/tmp/source-codex-home", EXISTING: "1" },
|
|
clearEnv: ["FOO"],
|
|
};
|
|
try {
|
|
await expect(
|
|
bridgeCodexAppServerStartOptions({
|
|
startOptions,
|
|
agentDir,
|
|
authProfileId: "openai-codex:default",
|
|
}),
|
|
).resolves.toBe(startOptions);
|
|
await expect(fs.access(path.join(agentDir, "harness-auth"))).rejects.toMatchObject({
|
|
code: "ENOENT",
|
|
});
|
|
} finally {
|
|
await fs.rm(agentDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("applies an OpenAI Codex OAuth profile through app-server login", async () => {
|
|
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
|
|
const request = vi.fn(async () => ({ type: "chatgptAuthTokens" }));
|
|
try {
|
|
upsertAuthProfile({
|
|
agentDir,
|
|
profileId: "openai-codex:work",
|
|
credential: {
|
|
type: "oauth",
|
|
provider: "openai-codex",
|
|
access: "access-token",
|
|
refresh: "refresh-token",
|
|
expires: Date.now() + 24 * 60 * 60_000,
|
|
accountId: "account-123",
|
|
email: "codex@example.test",
|
|
},
|
|
});
|
|
|
|
await applyCodexAppServerAuthProfile({
|
|
client: { request } as never,
|
|
agentDir,
|
|
authProfileId: "openai-codex:work",
|
|
});
|
|
|
|
expect(request).toHaveBeenCalledWith("account/login/start", {
|
|
type: "chatgptAuthTokens",
|
|
accessToken: "access-token",
|
|
chatgptAccountId: "account-123",
|
|
chatgptPlanType: null,
|
|
});
|
|
} finally {
|
|
await fs.rm(agentDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("refreshes an expired OpenAI Codex OAuth profile before app-server login", async () => {
|
|
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
|
|
const request = vi.fn(async () => ({ type: "chatgptAuthTokens" }));
|
|
oauthMocks.refreshOpenAICodexToken.mockResolvedValueOnce({
|
|
access: "fresh-access-token",
|
|
refresh: "fresh-refresh-token",
|
|
expires: Date.now() + 60_000,
|
|
accountId: "account-456",
|
|
});
|
|
try {
|
|
upsertAuthProfile({
|
|
agentDir,
|
|
profileId: "openai-codex:work",
|
|
credential: {
|
|
type: "oauth",
|
|
provider: "openai-codex",
|
|
access: "expired-access-token",
|
|
refresh: "refresh-token",
|
|
expires: Date.now() - 60_000,
|
|
accountId: "account-123",
|
|
email: "codex@example.test",
|
|
},
|
|
});
|
|
|
|
await applyCodexAppServerAuthProfile({
|
|
client: { request } as never,
|
|
agentDir,
|
|
authProfileId: "openai-codex:work",
|
|
});
|
|
|
|
expect(oauthMocks.refreshOpenAICodexToken).toHaveBeenCalledWith("refresh-token");
|
|
expect(request).toHaveBeenCalledWith("account/login/start", {
|
|
type: "chatgptAuthTokens",
|
|
accessToken: "fresh-access-token",
|
|
chatgptAccountId: "account-456",
|
|
chatgptPlanType: null,
|
|
});
|
|
} finally {
|
|
await fs.rm(agentDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("applies an OpenAI Codex api-key profile backed by a secret ref", async () => {
|
|
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
|
|
const request = vi.fn(async () => ({ type: "apiKey" }));
|
|
vi.stubEnv("OPENAI_CODEX_API_KEY", "ref-backed-api-key");
|
|
try {
|
|
upsertAuthProfile({
|
|
agentDir,
|
|
profileId: "openai-codex:work",
|
|
credential: {
|
|
type: "api_key",
|
|
provider: "openai-codex",
|
|
keyRef: { source: "env", provider: "default", id: "OPENAI_CODEX_API_KEY" },
|
|
},
|
|
});
|
|
|
|
await applyCodexAppServerAuthProfile({
|
|
client: { request } as never,
|
|
agentDir,
|
|
authProfileId: "openai-codex:work",
|
|
});
|
|
|
|
expect(request).toHaveBeenCalledWith("account/login/start", {
|
|
type: "apiKey",
|
|
apiKey: "ref-backed-api-key",
|
|
});
|
|
} finally {
|
|
await fs.rm(agentDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("applies an OpenAI Codex token profile backed by a secret ref", async () => {
|
|
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
|
|
const request = vi.fn(async () => ({ type: "chatgptAuthTokens" }));
|
|
vi.stubEnv("OPENAI_CODEX_TOKEN", "ref-backed-access-token");
|
|
try {
|
|
upsertAuthProfile({
|
|
agentDir,
|
|
profileId: "openai-codex:work",
|
|
credential: {
|
|
type: "token",
|
|
provider: "openai-codex",
|
|
tokenRef: { source: "env", provider: "default", id: "OPENAI_CODEX_TOKEN" },
|
|
email: "codex@example.test",
|
|
},
|
|
});
|
|
|
|
await applyCodexAppServerAuthProfile({
|
|
client: { request } as never,
|
|
agentDir,
|
|
authProfileId: "openai-codex:work",
|
|
});
|
|
|
|
expect(request).toHaveBeenCalledWith("account/login/start", {
|
|
type: "chatgptAuthTokens",
|
|
accessToken: "ref-backed-access-token",
|
|
chatgptAccountId: "codex@example.test",
|
|
chatgptPlanType: null,
|
|
});
|
|
} finally {
|
|
await fs.rm(agentDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("accepts a legacy Codex auth-provider alias for app-server login", async () => {
|
|
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
|
|
const request = vi.fn(async () => ({ type: "chatgptAuthTokens" }));
|
|
try {
|
|
upsertAuthProfile({
|
|
agentDir,
|
|
profileId: "openai-codex:work",
|
|
credential: {
|
|
type: "token",
|
|
provider: "codex-cli",
|
|
token: "legacy-access-token",
|
|
email: "legacy-codex@example.test",
|
|
},
|
|
});
|
|
|
|
await applyCodexAppServerAuthProfile({
|
|
client: { request } as never,
|
|
agentDir,
|
|
authProfileId: "openai-codex:work",
|
|
});
|
|
|
|
expect(request).toHaveBeenCalledWith("account/login/start", {
|
|
type: "chatgptAuthTokens",
|
|
accessToken: "legacy-access-token",
|
|
chatgptAccountId: "legacy-codex@example.test",
|
|
chatgptPlanType: null,
|
|
});
|
|
} finally {
|
|
await fs.rm(agentDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("answers app-server ChatGPT token refresh requests from the bound profile", async () => {
|
|
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
|
|
oauthMocks.refreshOpenAICodexToken.mockResolvedValueOnce({
|
|
access: "refreshed-access-token",
|
|
refresh: "refreshed-refresh-token",
|
|
expires: Date.now() + 60_000,
|
|
accountId: "account-789",
|
|
});
|
|
try {
|
|
upsertAuthProfile({
|
|
agentDir,
|
|
profileId: "openai-codex:work",
|
|
credential: {
|
|
type: "oauth",
|
|
provider: "openai-codex",
|
|
access: "stale-access-token",
|
|
refresh: "refresh-token",
|
|
expires: Date.now() + 60_000,
|
|
accountId: "account-123",
|
|
email: "codex@example.test",
|
|
},
|
|
});
|
|
|
|
await expect(
|
|
refreshCodexAppServerAuthTokens({
|
|
agentDir,
|
|
authProfileId: "openai-codex:work",
|
|
}),
|
|
).resolves.toEqual({
|
|
accessToken: "refreshed-access-token",
|
|
chatgptAccountId: "account-789",
|
|
chatgptPlanType: null,
|
|
});
|
|
expect(oauthMocks.refreshOpenAICodexToken).toHaveBeenCalledWith("refresh-token");
|
|
} finally {
|
|
await fs.rm(agentDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("accepts a refreshed Codex OAuth credential when the stored provider is a legacy alias", async () => {
|
|
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
|
|
oauthMocks.refreshOpenAICodexToken.mockResolvedValueOnce({
|
|
access: "refreshed-alias-access-token",
|
|
refresh: "refreshed-alias-refresh-token",
|
|
expires: Date.now() + 60_000,
|
|
accountId: "account-alias",
|
|
});
|
|
try {
|
|
upsertAuthProfile({
|
|
agentDir,
|
|
profileId: "openai-codex:work",
|
|
credential: {
|
|
type: "oauth",
|
|
provider: "codex-cli",
|
|
access: "stale-alias-access-token",
|
|
refresh: "alias-refresh-token",
|
|
expires: Date.now() + 60_000,
|
|
accountId: "account-legacy",
|
|
email: "legacy-codex@example.test",
|
|
},
|
|
});
|
|
|
|
await expect(
|
|
refreshCodexAppServerAuthTokens({
|
|
agentDir,
|
|
authProfileId: "openai-codex:work",
|
|
}),
|
|
).resolves.toEqual({
|
|
accessToken: "refreshed-alias-access-token",
|
|
chatgptAccountId: "account-alias",
|
|
chatgptPlanType: null,
|
|
});
|
|
expect(oauthMocks.refreshOpenAICodexToken).toHaveBeenCalledWith("alias-refresh-token");
|
|
} finally {
|
|
await fs.rm(agentDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("preserves a stored ChatGPT plan type when building token login params", async () => {
|
|
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
|
|
const request = vi.fn(async () => ({ type: "chatgptAuthTokens" }));
|
|
try {
|
|
upsertAuthProfile({
|
|
agentDir,
|
|
profileId: "openai-codex:work",
|
|
credential: {
|
|
type: "oauth",
|
|
provider: "openai-codex",
|
|
access: "access-token",
|
|
refresh: "refresh-token",
|
|
expires: Date.now() + 24 * 60 * 60_000,
|
|
accountId: "account-123",
|
|
email: "codex@example.test",
|
|
chatgptPlanType: "pro",
|
|
} as never,
|
|
});
|
|
|
|
await applyCodexAppServerAuthProfile({
|
|
client: { request } as never,
|
|
agentDir,
|
|
authProfileId: "openai-codex:work",
|
|
});
|
|
|
|
expect(request).toHaveBeenCalledWith("account/login/start", {
|
|
type: "chatgptAuthTokens",
|
|
accessToken: "access-token",
|
|
chatgptAccountId: "account-123",
|
|
chatgptPlanType: "pro",
|
|
});
|
|
} finally {
|
|
await fs.rm(agentDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|