mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
test(codex): cover websocket token rotation (#70328) (thanks @Lucenx9)
This commit is contained in:
@@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Codex harness: rotate the shared app-server websocket client when the configured bearer token changes, so auth-token refreshes reconnect with the new `Authorization` header instead of reusing a stale socket. (#70328) Thanks @Lucenx9.
|
||||
- Config/models: merge provider-scoped model allowlist updates and protect model/provider map writes from accidental full replacement, adding `config set --merge` for additive updates and `--replace` for intentional clobbers. Fixes #65920, #68392, and #68653.
|
||||
- Agents/Pi auth: preserve AWS SDK-authenticated Bedrock runs for IMDS and task-role setups, clear stale refresh timers on sentinel fallback, and log unexpected runtime-auth prep failures instead of silently leaving the provider unauthenticated. Thanks @wirjo.
|
||||
- Config/gateway: restore last-known-good config on critical clobber signatures such as missing metadata, missing `gateway.mode`, or sharp size drops, preventing gateway crash loops when a valid backup exists. Fixes #70336.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { WebSocketServer, type RawData } from "ws";
|
||||
import { CodexAppServerClient, MIN_CODEX_APP_SERVER_VERSION } from "./client.js";
|
||||
import { createClientHarness } from "./test-support.js";
|
||||
|
||||
@@ -16,6 +17,7 @@ vi.mock("openclaw/plugin-sdk/provider-auth", () => ({
|
||||
}));
|
||||
|
||||
let listCodexAppServerModels: typeof import("./models.js").listCodexAppServerModels;
|
||||
let clearSharedCodexAppServerClient: typeof import("./shared-client.js").clearSharedCodexAppServerClient;
|
||||
let resetSharedCodexAppServerClientForTests: typeof import("./shared-client.js").resetSharedCodexAppServerClientForTests;
|
||||
|
||||
async function sendInitializeResult(
|
||||
@@ -36,7 +38,8 @@ async function sendEmptyModelList(harness: ReturnType<typeof createClientHarness
|
||||
describe("shared Codex app-server client", () => {
|
||||
beforeAll(async () => {
|
||||
({ listCodexAppServerModels } = await import("./models.js"));
|
||||
({ resetSharedCodexAppServerClientForTests } = await import("./shared-client.js"));
|
||||
({ clearSharedCodexAppServerClient, resetSharedCodexAppServerClientForTests } =
|
||||
await import("./shared-client.js"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -187,4 +190,77 @@ describe("shared Codex app-server client", () => {
|
||||
|
||||
expect(second.process.kill).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses a fresh websocket Authorization header after shared-client token rotation", async () => {
|
||||
const server = new WebSocketServer({ host: "127.0.0.1", port: 0 });
|
||||
const authHeaders: Array<string | undefined> = [];
|
||||
server.on("connection", (socket, request) => {
|
||||
authHeaders.push(request.headers.authorization);
|
||||
socket.on("message", (data) => {
|
||||
const message = JSON.parse(rawDataToText(data)) as { id?: number; method?: string };
|
||||
if (message.method === "initialize") {
|
||||
socket.send(
|
||||
JSON.stringify({ id: message.id, result: { userAgent: "openclaw/0.118.0" } }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (message.method === "model/list") {
|
||||
socket.send(JSON.stringify({ id: message.id, result: { data: [] } }));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
await new Promise<void>((resolve) => server.once("listening", resolve));
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
throw new Error("expected websocket test server port");
|
||||
}
|
||||
const url = `ws://127.0.0.1:${address.port}`;
|
||||
|
||||
await expect(
|
||||
listCodexAppServerModels({
|
||||
timeoutMs: 1000,
|
||||
startOptions: {
|
||||
transport: "websocket",
|
||||
command: "codex",
|
||||
args: [],
|
||||
url,
|
||||
authToken: "tok-first",
|
||||
headers: {},
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual({ models: [] });
|
||||
await expect(
|
||||
listCodexAppServerModels({
|
||||
timeoutMs: 1000,
|
||||
startOptions: {
|
||||
transport: "websocket",
|
||||
command: "codex",
|
||||
args: [],
|
||||
url,
|
||||
authToken: "tok-second",
|
||||
headers: {},
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual({ models: [] });
|
||||
|
||||
expect(authHeaders).toEqual(["Bearer tok-first", "Bearer tok-second"]);
|
||||
} finally {
|
||||
clearSharedCodexAppServerClient();
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
server.close((error) => (error ? reject(error) : resolve())),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function rawDataToText(data: RawData): string {
|
||||
if (Array.isArray(data)) {
|
||||
return Buffer.concat(data).toString("utf8");
|
||||
}
|
||||
if (data instanceof ArrayBuffer) {
|
||||
return Buffer.from(new Uint8Array(data)).toString("utf8");
|
||||
}
|
||||
return Buffer.from(data).toString("utf8");
|
||||
}
|
||||
|
||||
@@ -48,10 +48,10 @@ export async function getSharedCodexAppServerClient(options?: {
|
||||
client.addCloseHandler(clearSharedClientIfCurrent);
|
||||
try {
|
||||
await client.initialize();
|
||||
return client;
|
||||
} catch (error) {
|
||||
// Startup failures happen before callers own the shared client, so close
|
||||
// the child here instead of leaving a rejected daemon attached to stdio.
|
||||
return client;
|
||||
} catch (error) {
|
||||
// Startup failures happen before callers own the shared client, so close
|
||||
// the child here instead of leaving a rejected daemon attached to stdio.
|
||||
client.close();
|
||||
throw error;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user