Skip to content

Commit f12e9c4

Browse files
steipeteBenjamin Badejo
andcommitted
fix(codex): inject app-server client factories
Co-authored-by: Benjamin Badejo <[email protected]>
1 parent 0db0979 commit f12e9c4

8 files changed

Lines changed: 181 additions & 112 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai
3232

3333
### Fixes
3434

35+
- Codex app-server: inject native client factories per run and compaction attempt instead of using module-scope test state, avoiding temporal-dead-zone reads during cyclic startup. (#81148) Thanks @bdjben.
3536
- Plugin skills: replace generated Windows plugin-skill directories before publishing the current skill link, avoiding repeated `EINVAL` warnings from stale non-symlink entries. Fixes #81432. (#81446) Thanks @hclsys and @vincentkoc.
3637
- Channels/config: treat channel entries with only `enabled: true` as configured state so plugin-backed channels can auto-enable from an explicit on switch. Fixes #81323. (#81331) Thanks @EvanYao826 and @vincentkoc.
3738
- CLI/update: add an update finalization path for externally swapped core runtimes, running update-time doctor repair and plugin convergence from post-doctor config and install-record state before reporting completion. Thanks @shakkernerd.

extensions/codex/src/app-server/auth-profile-runtime-contract.test.ts

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,36 @@ import {
77
} from "openclaw/plugin-sdk/agent-harness";
88
import { AUTH_PROFILE_RUNTIME_CONTRACT } from "openclaw/plugin-sdk/agent-runtime-test-contracts";
99
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
10-
import { runCodexAppServerAttempt, __testing } from "./run-attempt.js";
10+
import type { CodexAppServerClientFactory } from "./client-factory.js";
11+
import { runCodexAppServerAttempt as runCodexAppServerAttemptImpl } from "./run-attempt.js";
1112
import { readCodexAppServerBinding, writeCodexAppServerBinding } from "./session-binding.js";
1213
import { createCodexTestModel } from "./test-support.js";
1314

15+
let codexAppServerClientFactoryForTest: CodexAppServerClientFactory | undefined;
16+
17+
type RunCodexAppServerAttemptOptions = NonNullable<
18+
Parameters<typeof runCodexAppServerAttemptImpl>[1]
19+
>;
20+
21+
function setCodexAppServerClientFactoryForTest(factory: CodexAppServerClientFactory): void {
22+
codexAppServerClientFactoryForTest = factory;
23+
}
24+
25+
function resetCodexAppServerClientFactoryForTest(): void {
26+
codexAppServerClientFactoryForTest = undefined;
27+
}
28+
29+
function runCodexAppServerAttempt(
30+
params: EmbeddedRunAttemptParams,
31+
options: RunCodexAppServerAttemptOptions = {},
32+
) {
33+
const clientFactory = options.clientFactory ?? codexAppServerClientFactoryForTest;
34+
return runCodexAppServerAttemptImpl(
35+
params,
36+
clientFactory ? { ...options, clientFactory } : options,
37+
);
38+
}
39+
1440
function createParams(sessionFile: string, workspaceDir: string): EmbeddedRunAttemptParams {
1541
return {
1642
prompt: AUTH_PROFILE_RUNTIME_CONTRACT.workspacePrompt,
@@ -85,29 +111,27 @@ function createCodexAuthProfileHarness(params: { startMethod: "thread/start" | "
85111
const seenAgentDirs: Array<string | undefined> = [];
86112
const requests: Array<{ method: string; params: unknown }> = [];
87113
let notify: (notification: unknown) => Promise<void> = async () => undefined;
88-
__testing.setCodexAppServerClientFactoryForTests(
89-
async (_startOptions, authProfileId, agentDir) => {
90-
seenAuthProfileIds.push(authProfileId);
91-
seenAgentDirs.push(agentDir);
92-
return {
93-
request: vi.fn(async (method: string, requestParams?: unknown) => {
94-
requests.push({ method, params: requestParams });
95-
if (method === params.startMethod) {
96-
return threadStartResult();
97-
}
98-
if (method === "turn/start") {
99-
return turnStartResult();
100-
}
101-
throw new Error(`unexpected method: ${method}`);
102-
}),
103-
addNotificationHandler: (handler: (notification: unknown) => Promise<void>) => {
104-
notify = handler;
105-
return () => undefined;
106-
},
107-
addRequestHandler: () => () => undefined,
108-
} as never;
109-
},
110-
);
114+
setCodexAppServerClientFactoryForTest(async (_startOptions, authProfileId, agentDir) => {
115+
seenAuthProfileIds.push(authProfileId);
116+
seenAgentDirs.push(agentDir);
117+
return {
118+
request: vi.fn(async (method: string, requestParams?: unknown) => {
119+
requests.push({ method, params: requestParams });
120+
if (method === params.startMethod) {
121+
return threadStartResult();
122+
}
123+
if (method === "turn/start") {
124+
return turnStartResult();
125+
}
126+
throw new Error(`unexpected method: ${method}`);
127+
}),
128+
addNotificationHandler: (handler: (notification: unknown) => Promise<void>) => {
129+
notify = handler;
130+
return () => undefined;
131+
},
132+
addRequestHandler: () => () => undefined,
133+
} as never;
134+
});
111135
return {
112136
seenAuthProfileIds,
113137
seenAgentDirs,
@@ -138,7 +162,7 @@ describe("Auth profile runtime contract - Codex app-server adapter", () => {
138162

139163
afterEach(async () => {
140164
abortAgentHarnessRun(AUTH_PROFILE_RUNTIME_CONTRACT.sessionId);
141-
__testing.resetCodexAppServerClientFactoryForTests();
165+
resetCodexAppServerClientFactoryForTest();
142166
await fs.rm(tmpDir, { recursive: true, force: true });
143167
});
144168

extensions/codex/src/app-server/client-factory.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,3 @@ export const defaultCodexAppServerClientFactory: CodexAppServerClientFactory = (
2222
import("./shared-client.js").then(({ getSharedCodexAppServerClient }) =>
2323
getSharedCodexAppServerClient({ startOptions, authProfileId, agentDir, config }),
2424
);
25-
26-
export function createCodexAppServerClientFactoryTestHooks(
27-
setFactory: (factory: CodexAppServerClientFactory) => void,
28-
) {
29-
return {
30-
setCodexAppServerClientFactoryForTests(factory: CodexAppServerClientFactory): void {
31-
setFactory(factory);
32-
},
33-
resetCodexAppServerClientFactoryForTests(): void {
34-
setFactory(defaultCodexAppServerClientFactory);
35-
},
36-
} as const;
37-
}

extensions/codex/src/app-server/compact.test.ts

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,35 @@ import os from "node:os";
33
import path from "node:path";
44
import type { HarnessContextEngine as ContextEngine } from "openclaw/plugin-sdk/agent-harness-runtime";
55
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6+
import type { CodexAppServerClientFactory } from "./client-factory.js";
67
import type { CodexAppServerClient } from "./client.js";
7-
import { maybeCompactCodexAppServerSession, __testing } from "./compact.js";
8+
import { maybeCompactCodexAppServerSession as maybeCompactCodexAppServerSessionImpl } from "./compact.js";
89
import type { CodexServerNotification } from "./protocol.js";
910
import { writeCodexAppServerBinding } from "./session-binding.js";
1011

1112
let tempDir: string;
13+
let codexAppServerClientFactoryForTest: CodexAppServerClientFactory | undefined;
14+
15+
type MaybeCompactOptions = NonNullable<Parameters<typeof maybeCompactCodexAppServerSessionImpl>[1]>;
16+
17+
function setCodexAppServerClientFactoryForTest(factory: CodexAppServerClientFactory): void {
18+
codexAppServerClientFactoryForTest = factory;
19+
}
20+
21+
function resetCodexAppServerClientFactoryForTest(): void {
22+
codexAppServerClientFactoryForTest = undefined;
23+
}
24+
25+
function maybeCompactCodexAppServerSession(
26+
params: Parameters<typeof maybeCompactCodexAppServerSessionImpl>[0],
27+
options: MaybeCompactOptions = {},
28+
) {
29+
const clientFactory = options.clientFactory ?? codexAppServerClientFactoryForTest;
30+
return maybeCompactCodexAppServerSessionImpl(
31+
params,
32+
clientFactory ? { ...options, clientFactory } : options,
33+
);
34+
}
1235

1336
async function writeTestBinding(options: { authProfileId?: string } = {}): Promise<string> {
1437
const sessionFile = path.join(tempDir, "session.jsonl");
@@ -49,13 +72,13 @@ describe("maybeCompactCodexAppServerSession", () => {
4972
});
5073

5174
afterEach(async () => {
52-
__testing.resetCodexAppServerClientFactoryForTests();
75+
resetCodexAppServerClientFactoryForTest();
5376
await fs.rm(tempDir, { recursive: true, force: true });
5477
});
5578

5679
it("waits for native app-server compaction before reporting success", async () => {
5780
const fake = createFakeCodexClient();
58-
__testing.setCodexAppServerClientFactoryForTests(async () => fake.client);
81+
setCodexAppServerClientFactoryForTest(async () => fake.client);
5982
const sessionFile = await writeTestBinding();
6083

6184
const pendingResult = startCompaction(sessionFile, { currentTokenCount: 123 });
@@ -88,7 +111,7 @@ describe("maybeCompactCodexAppServerSession", () => {
88111

89112
it("accepts native context-compaction item completion as success", async () => {
90113
const fake = createFakeCodexClient();
91-
__testing.setCodexAppServerClientFactoryForTests(async () => fake.client);
114+
setCodexAppServerClientFactoryForTest(async () => fake.client);
92115
const sessionFile = await writeTestBinding();
93116

94117
const pendingResult = startCompaction(sessionFile);
@@ -115,7 +138,7 @@ describe("maybeCompactCodexAppServerSession", () => {
115138
it("reuses the bound auth profile for native compaction", async () => {
116139
const fake = createFakeCodexClient();
117140
let seenAuthProfileId: string | undefined;
118-
__testing.setCodexAppServerClientFactoryForTests(async (_startOptions, authProfileId) => {
141+
setCodexAppServerClientFactoryForTest(async (_startOptions, authProfileId) => {
119142
seenAuthProfileId = authProfileId;
120143
return fake.client;
121144
});
@@ -137,7 +160,7 @@ describe("maybeCompactCodexAppServerSession", () => {
137160
it("fails closed when the persisted binding auth profile disagrees with the runtime request", async () => {
138161
const fake = createFakeCodexClient();
139162
const factory = vi.fn(async () => fake.client);
140-
__testing.setCodexAppServerClientFactoryForTests(factory);
163+
setCodexAppServerClientFactoryForTest(factory);
141164
const sessionFile = path.join(tempDir, "session.jsonl");
142165
await writeCodexAppServerBinding(sessionFile, {
143166
threadId: "thread-1",
@@ -163,7 +186,7 @@ describe("maybeCompactCodexAppServerSession", () => {
163186

164187
it("prefers owning context-engine compaction and records native status separately", async () => {
165188
const fake = createFakeCodexClient();
166-
__testing.setCodexAppServerClientFactoryForTests(async () => fake.client);
189+
setCodexAppServerClientFactoryForTest(async () => fake.client);
167190
const sessionFile = await writeTestBinding();
168191
const compact = vi.fn(async (_params: unknown) => ({
169192
ok: true,
@@ -260,7 +283,7 @@ describe("maybeCompactCodexAppServerSession", () => {
260283

261284
it("still runs native compaction when context-engine maintenance fails", async () => {
262285
const fake = createFakeCodexClient();
263-
__testing.setCodexAppServerClientFactoryForTests(async () => fake.client);
286+
setCodexAppServerClientFactoryForTest(async () => fake.client);
264287
const sessionFile = await writeTestBinding();
265288
const contextEngine: ContextEngine = {
266289
info: { id: "lossless-claw", name: "Lossless Claw", ownsCompaction: true },
@@ -307,7 +330,7 @@ describe("maybeCompactCodexAppServerSession", () => {
307330

308331
it("records native compaction status when primary compaction has no result payload", async () => {
309332
const fake = createFakeCodexClient();
310-
__testing.setCodexAppServerClientFactoryForTests(async () => fake.client);
333+
setCodexAppServerClientFactoryForTest(async () => fake.client);
311334
const sessionFile = await writeTestBinding();
312335
const contextEngine: ContextEngine = {
313336
info: { id: "lossless-claw", name: "Lossless Claw", ownsCompaction: true },
@@ -350,7 +373,7 @@ describe("maybeCompactCodexAppServerSession", () => {
350373

351374
it("reports context-engine compaction errors without skipping native compaction", async () => {
352375
const fake = createFakeCodexClient();
353-
__testing.setCodexAppServerClientFactoryForTests(async () => fake.client);
376+
setCodexAppServerClientFactoryForTest(async () => fake.client);
354377
const sessionFile = await writeTestBinding();
355378
const contextEngine: ContextEngine = {
356379
info: { id: "lossless-claw", name: "Lossless Claw", ownsCompaction: true },

extensions/codex/src/app-server/compact.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import {
77
type EmbeddedPiCompactResult,
88
} from "openclaw/plugin-sdk/agent-harness-runtime";
99
import {
10-
createCodexAppServerClientFactoryTestHooks,
1110
defaultCodexAppServerClientFactory,
11+
type CodexAppServerClientFactory,
1212
} from "./client-factory.js";
1313
import type { CodexAppServerClient, CodexServerNotificationHandler } from "./client.js";
1414
import { resolveCodexAppServerRuntimeOptions } from "./config.js";
@@ -30,11 +30,9 @@ type ContextEngineCompactResult = Awaited<
3030

3131
const DEFAULT_CODEX_COMPACTION_WAIT_TIMEOUT_MS = 5 * 60 * 1000;
3232

33-
let clientFactory = defaultCodexAppServerClientFactory;
34-
3533
export async function maybeCompactCodexAppServerSession(
3634
params: CompactEmbeddedPiSessionParams,
37-
options: { pluginConfig?: unknown } = {},
35+
options: { pluginConfig?: unknown; clientFactory?: CodexAppServerClientFactory } = {},
3836
): Promise<EmbeddedPiCompactResult | undefined> {
3937
const activeContextEngine = isActiveHarnessContextEngine(params.contextEngine)
4038
? params.contextEngine
@@ -107,7 +105,7 @@ export async function maybeCompactCodexAppServerSession(
107105

108106
async function compactCodexNativeThread(
109107
params: CompactEmbeddedPiSessionParams,
110-
options: { pluginConfig?: unknown } = {},
108+
options: { pluginConfig?: unknown; clientFactory?: CodexAppServerClientFactory } = {},
111109
): Promise<EmbeddedPiCompactResult | undefined> {
112110
const appServer = resolveCodexAppServerRuntimeOptions({ pluginConfig: options.pluginConfig });
113111
const binding = await readCodexAppServerBinding(params.sessionFile, { config: params.config });
@@ -123,6 +121,7 @@ async function compactCodexNativeThread(
123121
return { ok: false, compacted: false, reason: "auth profile mismatch for session binding" };
124122
}
125123

124+
const clientFactory = options.clientFactory ?? defaultCodexAppServerClientFactory;
126125
const client = await clientFactory(
127126
appServer.start,
128127
requestedAuthProfileId ?? binding.authProfileId,
@@ -370,7 +369,3 @@ function formatCompactionError(error: unknown): string {
370369
}
371370
return String(error);
372371
}
373-
374-
export const __testing = createCodexAppServerClientFactoryTestHooks((factory) => {
375-
clientFactory = factory;
376-
});

extensions/codex/src/app-server/run-attempt.context-engine.test.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,37 @@ import {
99
type HarnessContextEngine as ContextEngine,
1010
} from "openclaw/plugin-sdk/agent-harness-runtime";
1111
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
12+
import type { CodexAppServerClientFactory } from "./client-factory.js";
1213
import type { CodexServerNotification } from "./protocol.js";
13-
import { runCodexAppServerAttempt, __testing } from "./run-attempt.js";
14+
import { runCodexAppServerAttempt as runCodexAppServerAttemptImpl } from "./run-attempt.js";
1415
import { readCodexAppServerBinding, writeCodexAppServerBinding } from "./session-binding.js";
1516
import { createCodexTestModel } from "./test-support.js";
1617

1718
let tempDir: string;
19+
let codexAppServerClientFactoryForTest: CodexAppServerClientFactory | undefined;
20+
21+
type RunCodexAppServerAttemptOptions = NonNullable<
22+
Parameters<typeof runCodexAppServerAttemptImpl>[1]
23+
>;
24+
25+
function setCodexAppServerClientFactoryForTest(factory: CodexAppServerClientFactory): void {
26+
codexAppServerClientFactoryForTest = factory;
27+
}
28+
29+
function resetCodexAppServerClientFactoryForTest(): void {
30+
codexAppServerClientFactoryForTest = undefined;
31+
}
32+
33+
function runCodexAppServerAttempt(
34+
params: EmbeddedRunAttemptParams,
35+
options: RunCodexAppServerAttemptOptions = {},
36+
) {
37+
const clientFactory = options.clientFactory ?? codexAppServerClientFactoryForTest;
38+
return runCodexAppServerAttemptImpl(
39+
params,
40+
clientFactory ? { ...options, clientFactory } : options,
41+
);
42+
}
1843

1944
function createParams(sessionFile: string, workspaceDir: string): EmbeddedRunAttemptParams {
2045
return {
@@ -133,7 +158,7 @@ function createStartedThreadHarness(
133158
return {};
134159
});
135160

136-
__testing.setCodexAppServerClientFactoryForTests(
161+
setCodexAppServerClientFactoryForTest(
137162
async () =>
138163
({
139164
request,
@@ -258,7 +283,7 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
258283
});
259284

260285
afterEach(async () => {
261-
__testing.resetCodexAppServerClientFactoryForTests();
286+
resetCodexAppServerClientFactoryForTest();
262287
vi.restoreAllMocks();
263288
await fs.rm(tempDir, { recursive: true, force: true });
264289
});

0 commit comments

Comments
 (0)