mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 20:00:42 +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
|
### 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.
|
- 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.
|
- 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.
|
- 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 { 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 { CodexAppServerClient, MIN_CODEX_APP_SERVER_VERSION } from "./client.js";
|
||||||
import { createClientHarness } from "./test-support.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 listCodexAppServerModels: typeof import("./models.js").listCodexAppServerModels;
|
||||||
|
let clearSharedCodexAppServerClient: typeof import("./shared-client.js").clearSharedCodexAppServerClient;
|
||||||
let resetSharedCodexAppServerClientForTests: typeof import("./shared-client.js").resetSharedCodexAppServerClientForTests;
|
let resetSharedCodexAppServerClientForTests: typeof import("./shared-client.js").resetSharedCodexAppServerClientForTests;
|
||||||
|
|
||||||
async function sendInitializeResult(
|
async function sendInitializeResult(
|
||||||
@@ -36,7 +38,8 @@ async function sendEmptyModelList(harness: ReturnType<typeof createClientHarness
|
|||||||
describe("shared Codex app-server client", () => {
|
describe("shared Codex app-server client", () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
({ listCodexAppServerModels } = await import("./models.js"));
|
({ listCodexAppServerModels } = await import("./models.js"));
|
||||||
({ resetSharedCodexAppServerClientForTests } = await import("./shared-client.js"));
|
({ clearSharedCodexAppServerClient, resetSharedCodexAppServerClientForTests } =
|
||||||
|
await import("./shared-client.js"));
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -187,4 +190,77 @@ describe("shared Codex app-server client", () => {
|
|||||||
|
|
||||||
expect(second.process.kill).not.toHaveBeenCalled();
|
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);
|
client.addCloseHandler(clearSharedClientIfCurrent);
|
||||||
try {
|
try {
|
||||||
await client.initialize();
|
await client.initialize();
|
||||||
return client;
|
return client;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Startup failures happen before callers own the shared client, so close
|
// Startup failures happen before callers own the shared client, so close
|
||||||
// the child here instead of leaving a rejected daemon attached to stdio.
|
// the child here instead of leaving a rejected daemon attached to stdio.
|
||||||
client.close();
|
client.close();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user