Enable Codex native code mode for OpenClaw harness runs (#80001)

* fix(codex): enable native code mode in harness

* test(codex): update code mode prompt snapshots

* test(codex): align code mode thread config expectations

* chore(protocol): refresh generated Swift agent params

* fix(codex): enable code-mode-only harness threads

* test(discord): fix test mock type assertions

* test: fix remaining test type assertions

* test(matrix): guard avatar loader test callback
This commit is contained in:
pashpashpash
2026-05-10 16:18:03 -07:00
committed by GitHub
parent 8bee6f58d4
commit 0e8a7e12da
14 changed files with 241 additions and 60 deletions

View File

@@ -25,6 +25,7 @@ Docs: https://docs.openclaw.ai
- Discord/voice: default test and source installs to the pure-JS `opusscript` decoder by ignoring optional native `@discordjs/opus` builds, avoiding slow native addon compiles outside dedicated voice-performance lanes.
- Discord/voice: add an opt-in native `@discordjs/opus` install script and decoder preference for live voice-performance lanes without charging unrelated Docker/tests for native addon builds.
- Gateway/skills: add an opt-in private skill archive upload install path gated by `skills.install.allowUploadedArchives`, so trusted Gateway clients can stage and install zip-backed skills only when operators explicitly enable the code-install surface. (#74430) Thanks @samzong.
- Codex app-server: enable Codex native code-mode-only for harness threads so deferred OpenClaw dynamic tools run through Codex's own searchable code execution surface instead of a PI-style wrapper.
- Dependencies: refresh workspace pins and patch targets, including ACPX `@agentclientprotocol/claude-agent-acp` `0.33.1`, Codex ACP `0.14.0`, Baileys `7.0.0-rc10`, Google GenAI `2.0.1`, OpenAI `6.37.0`, AWS SDK `3.1045.0`, Kysely `0.29.0`, Tlon skill `0.3.6`, Aimock `1.19.5`, and tsdown `0.22.0`.
- Agents/compaction: preserve scoped background exec/process session references across embedded compaction and after-turn runtime contexts without exposing sessions from unrelated scopes. Fixes #79284. (#79307) Thanks @TurboTheTurtle.
- Agents/process: tell agents to inspect background sessions with `process log` before sending interactive input and to use `waitingForInput`/`stdinWritable` hints from `log`/`poll`.

View File

@@ -716,6 +716,7 @@ public struct AgentParams: Codable, Sendable {
public let bootstrapcontextmode: AnyCodable?
public let bootstrapcontextrunkind: AnyCodable?
public let acpturnsource: String?
public let internalruntimehandoffid: String?
public let internalevents: [[String: AnyCodable]]?
public let inputprovenance: [String: AnyCodable]?
public let voicewaketrigger: String?
@@ -752,6 +753,7 @@ public struct AgentParams: Codable, Sendable {
bootstrapcontextmode: AnyCodable?,
bootstrapcontextrunkind: AnyCodable?,
acpturnsource: String?,
internalruntimehandoffid: String?,
internalevents: [[String: AnyCodable]]?,
inputprovenance: [String: AnyCodable]?,
voicewaketrigger: String?,
@@ -787,6 +789,7 @@ public struct AgentParams: Codable, Sendable {
self.bootstrapcontextmode = bootstrapcontextmode
self.bootstrapcontextrunkind = bootstrapcontextrunkind
self.acpturnsource = acpturnsource
self.internalruntimehandoffid = internalruntimehandoffid
self.internalevents = internalevents
self.inputprovenance = inputprovenance
self.voicewaketrigger = voicewaketrigger
@@ -824,6 +827,7 @@ public struct AgentParams: Codable, Sendable {
case bootstrapcontextmode = "bootstrapContextMode"
case bootstrapcontextrunkind = "bootstrapContextRunKind"
case acpturnsource = "acpTurnSource"
case internalruntimehandoffid = "internalRuntimeHandoffId"
case internalevents = "internalEvents"
case inputprovenance = "inputProvenance"
case voicewaketrigger = "voiceWakeTrigger"

View File

@@ -21,6 +21,11 @@ Do not configure `openai-codex/gpt-*` model refs. `openai-codex` is the auth
profile provider for Codex OAuth or Codex API-key profiles, not the model
provider prefix for new agent config.
OpenClaw starts Codex app-server threads with Codex native code mode and
code-mode-only enabled. That keeps deferred/searchable OpenClaw dynamic tools
inside Codex's own code execution and tool-search surface instead of adding a
PI-style tool-search wrapper on top of Codex.
For the broader model/provider/runtime split, start with
[Agent runtimes](/concepts/agent-runtimes). The short version is:
`openai/gpt-5.5` is the model ref, `codex` is the runtime, and Telegram,

View File

@@ -81,6 +81,7 @@ describe("codex plugin", () => {
});
it("registers with capture APIs that do not expose conversation binding hooks yet", () => {
const registerProvider = vi.fn();
const api = createTestPluginApi({
id: "codex",
name: "Codex",
@@ -91,7 +92,7 @@ describe("codex plugin", () => {
registerAgentHarness: vi.fn(),
registerCommand: vi.fn(),
registerMediaUnderstandingProvider: vi.fn(),
registerProvider: vi.fn(),
registerProvider,
on: vi.fn(),
}) as ReturnType<typeof createTestPluginApi> & {
onConversationBindingResolved?: ReturnType<typeof vi.fn>;
@@ -99,8 +100,8 @@ describe("codex plugin", () => {
delete (api as { onConversationBindingResolved?: unknown }).onConversationBindingResolved;
plugin.register(api);
expect(api.registerProvider).toHaveBeenCalledTimes(1);
expect(api.registerProvider.mock.calls[0]?.[0].id).toBe("codex");
expect(registerProvider).toHaveBeenCalledTimes(1);
expect(registerProvider.mock.calls[0]?.[0].id).toBe("codex");
});
it("only claims the codex provider by default", () => {

View File

@@ -3791,6 +3791,11 @@ describe("runCodexAppServerAttempt", () => {
"features.codex_hooks": true,
"hooks.PreToolUse": [],
};
const expectedConfig = {
...config,
"features.code_mode": true,
"features.code_mode_only": true,
};
await startOrResumeThread({
client: { request } as never,
@@ -3811,8 +3816,8 @@ describe("runCodexAppServerAttempt", () => {
const requestCalls = request.mock.calls as unknown as Array<[string, { config?: unknown }]>;
expect(requestCalls.map(([method]) => method)).toEqual(["thread/start", "thread/resume"]);
expect(requestCalls[0]?.[1].config).toEqual(config);
expect(requestCalls[1]?.[1].config).toEqual(config);
expect(requestCalls[0]?.[1].config).toEqual(expectedConfig);
expect(requestCalls[1]?.[1].config).toEqual(expectedConfig);
});
it("merges native hook relay config with plugin app config when starting a thread", async () => {
@@ -3856,6 +3861,8 @@ describe("runCodexAppServerAttempt", () => {
expect(requestCalls.map(([method]) => method)).toEqual(["thread/start"]);
expect(requestCalls[0]?.[1].config).toEqual({
"features.codex_hooks": true,
"features.code_mode": true,
"features.code_mode_only": true,
hooks: { PreToolUse: [] },
...createPluginAppConfigPatch(),
});
@@ -3921,9 +3928,15 @@ describe("runCodexAppServerAttempt", () => {
expect(requestCalls.map(([method]) => method)).toEqual(["thread/start", "thread/resume"]);
expect(requestCalls[0]?.[1].config).toEqual({
"features.codex_hooks": true,
"features.code_mode": true,
"features.code_mode_only": true,
...createPluginAppConfigPatch(),
});
expect(requestCalls[1]?.[1].config).toEqual({ "features.codex_hooks": true });
expect(requestCalls[1]?.[1].config).toEqual({
"features.codex_hooks": true,
"features.code_mode": true,
"features.code_mode_only": true,
});
});
it("starts a new plugin app thread when full binding revalidation removes an app", async () => {
@@ -3979,6 +3992,8 @@ describe("runCodexAppServerAttempt", () => {
const requestCalls = request.mock.calls as unknown as Array<[string, { config?: unknown }]>;
expect(requestCalls.map(([method]) => method)).toEqual(["thread/start"]);
expect(requestCalls[0]?.[1].config).toEqual({
"features.code_mode": true,
"features.code_mode_only": true,
apps: {
_default: {
enabled: false,
@@ -4030,7 +4045,10 @@ describe("runCodexAppServerAttempt", () => {
const requestCalls = request.mock.calls as unknown as Array<[string, { config?: unknown }]>;
expect(requestCalls.map(([method]) => method)).toEqual(["thread/resume"]);
expect("config" in (requestCalls[0]?.[1] ?? {})).toBe(false);
expect(requestCalls[0]?.[1].config).toEqual({
"features.code_mode": true,
"features.code_mode_only": true,
});
const binding = await readCodexAppServerBinding(sessionFile);
expect(binding?.threadId).toBe("thread-existing");
expect(binding?.pluginAppsFingerprint).toBe("plugin-apps-config-1");
@@ -4081,7 +4099,11 @@ describe("runCodexAppServerAttempt", () => {
expect(buildPluginThreadConfig).toHaveBeenCalledTimes(1);
const requestCalls = request.mock.calls as unknown as Array<[string, { config?: unknown }]>;
expect(requestCalls.map(([method]) => method)).toEqual(["thread/start"]);
expect(requestCalls[0]?.[1].config).toEqual(createPluginAppConfigPatch());
expect(requestCalls[0]?.[1].config).toEqual({
...createPluginAppConfigPatch(),
"features.code_mode": true,
"features.code_mode_only": true,
});
const binding = await readCodexAppServerBinding(sessionFile);
expect(binding?.threadId).toBe("thread-recovered");
expect(binding?.pluginAppsFingerprint).toBe("plugin-apps-config-1");
@@ -4139,7 +4161,10 @@ describe("runCodexAppServerAttempt", () => {
expect(buildPluginThreadConfig).toHaveBeenCalledTimes(1);
const requestCalls = request.mock.calls as unknown as Array<[string, { config?: unknown }]>;
expect(requestCalls.map(([method]) => method)).toEqual(["thread/resume"]);
expect("config" in (requestCalls[0]?.[1] ?? {})).toBe(false);
expect(requestCalls[0]?.[1].config).toEqual({
"features.code_mode": true,
"features.code_mode_only": true,
});
});
it("rebuilds a partial plugin app binding after another plugin recovers", async () => {
@@ -4186,7 +4211,11 @@ describe("runCodexAppServerAttempt", () => {
expect(buildPluginThreadConfig).toHaveBeenCalledTimes(1);
const requestCalls = request.mock.calls as unknown as Array<[string, { config?: unknown }]>;
expect(requestCalls.map(([method]) => method)).toEqual(["thread/start"]);
expect(requestCalls[0]?.[1].config).toEqual(createTwoPluginAppConfigPatch());
expect(requestCalls[0]?.[1].config).toEqual({
...createTwoPluginAppConfigPatch(),
"features.code_mode": true,
"features.code_mode_only": true,
});
const binding = await readCodexAppServerBinding(sessionFile);
expect(binding?.threadId).toBe("thread-recovered");
expect(binding?.pluginAppsFingerprint).toBe("plugin-apps-config-2");
@@ -4242,7 +4271,11 @@ describe("runCodexAppServerAttempt", () => {
expect(buildPluginThreadConfig).toHaveBeenCalledTimes(1);
const requestCalls = request.mock.calls as unknown as Array<[string, { config?: unknown }]>;
expect(requestCalls.map(([method]) => method)).toEqual(["thread/start"]);
expect(requestCalls[0]?.[1].config).toEqual(createTwoCalendarAppConfigPatch());
expect(requestCalls[0]?.[1].config).toEqual({
...createTwoCalendarAppConfigPatch(),
"features.code_mode": true,
"features.code_mode_only": true,
});
const binding = await readCodexAppServerBinding(sessionFile);
expect(binding?.threadId).toBe("thread-recovered");
expect(binding?.pluginAppsFingerprint).toBe("plugin-apps-config-calendar-2");
@@ -4285,7 +4318,11 @@ describe("runCodexAppServerAttempt", () => {
const requestCalls = request.mock.calls as unknown as Array<[string, { config?: unknown }]>;
expect(requestCalls.map(([method]) => method)).toEqual(["thread/start"]);
expect(requestCalls[0]?.[1].config).toEqual(createPluginAppConfigPatch());
expect(requestCalls[0]?.[1].config).toEqual({
...createPluginAppConfigPatch(),
"features.code_mode": true,
"features.code_mode_only": true,
});
const binding = await readCodexAppServerBinding(sessionFile);
expect(binding?.threadId).toBe("thread-plugins");
expect(binding?.pluginAppsFingerprint).toBe("plugin-apps-config-1");
@@ -4349,6 +4386,11 @@ describe("runCodexAppServerAttempt", () => {
model: "gpt-5.4-codex",
approvalPolicy: "on-request",
approvalsReviewer: "guardian_subagent",
config: expect.objectContaining({
"features.codex_hooks": true,
"features.code_mode": true,
"features.code_mode_only": true,
}),
sandbox: "danger-full-access",
serviceTier: "priority",
developerInstructions: expect.stringContaining(CODEX_GPT5_BEHAVIOR_CONTRACT),
@@ -4451,6 +4493,10 @@ describe("runCodexAppServerAttempt", () => {
model: "gpt-5.4-codex",
approvalPolicy: "on-request",
approvalsReviewer: "guardian_subagent",
config: {
"features.code_mode": true,
"features.code_mode_only": true,
},
sandbox: "danger-full-access",
serviceTier: "flex",
developerInstructions: expect.stringContaining(CODEX_GPT5_BEHAVIOR_CONTRACT),

View File

@@ -47,6 +47,41 @@ function createAppServerOptions() {
} as const;
}
describe("Codex app-server native code mode config", () => {
it("enables Codex code-mode-only on thread/start without clobbering other config", () => {
const request = buildThreadStartParams(createAttemptParams({ provider: "openai" }), {
cwd: "/repo",
dynamicTools: [],
appServer: createAppServerOptions() as never,
developerInstructions: "test instructions",
config: {
"features.codex_hooks": true,
apps: { _default: { enabled: false } },
},
});
expect(request.config).toEqual({
"features.codex_hooks": true,
apps: { _default: { enabled: false } },
"features.code_mode": true,
"features.code_mode_only": true,
});
});
it("enables Codex code-mode-only on thread/resume", () => {
const request = buildThreadResumeParams(createAttemptParams({ provider: "openai" }), {
threadId: "thread-1",
appServer: createAppServerOptions() as never,
developerInstructions: "test instructions",
});
expect(request.config).toEqual({
"features.code_mode": true,
"features.code_mode_only": true,
});
});
});
describe("Codex app-server model provider selection", () => {
it.each(["openai", "openai-codex"])(
"omits public %s modelProvider when forwarding native Codex auth on thread/start",

View File

@@ -44,6 +44,11 @@ export type CodexPluginThreadConfigProvider = {
build: () => Promise<CodexPluginThreadConfig>;
};
const CODEX_CODE_MODE_THREAD_CONFIG: JsonObject = {
"features.code_mode": true,
"features.code_mode_only": true,
};
export async function startOrResumeThread(params: {
client: CodexAppServerClient;
params: EmbeddedRunAttemptParams;
@@ -304,7 +309,7 @@ export function buildThreadStartParams(
sandbox: options.appServer.sandbox,
...(options.appServer.serviceTier ? { serviceTier: options.appServer.serviceTier } : {}),
serviceName: "OpenClaw",
...(options.config ? { config: options.config } : {}),
config: buildCodexRuntimeThreadConfig(options.config),
developerInstructions: options.developerInstructions ?? buildDeveloperInstructions(params),
dynamicTools: options.dynamicTools,
experimentalRawEvents: true,
@@ -337,12 +342,20 @@ export function buildThreadResumeParams(
approvalsReviewer: options.appServer.approvalsReviewer,
sandbox: options.appServer.sandbox,
...(options.appServer.serviceTier ? { serviceTier: options.appServer.serviceTier } : {}),
...(options.config ? { config: options.config } : {}),
config: buildCodexRuntimeThreadConfig(options.config),
developerInstructions: options.developerInstructions ?? buildDeveloperInstructions(params),
persistExtendedHistory: true,
};
}
function buildCodexRuntimeThreadConfig(config: JsonObject | undefined): JsonObject {
return (
mergeCodexThreadConfigs(config, CODEX_CODE_MODE_THREAD_CONFIG) ?? {
...CODEX_CODE_MODE_THREAD_CONFIG,
}
);
}
export function buildTurnStartParams(
params: EmbeddedRunAttemptParams,
options: {

View File

@@ -87,6 +87,29 @@ const {
const enableAllActions = () => true;
const DISCORD_TEST_CFG = EMPTY_DISCORD_TEST_CONFIG;
type MockCallSource = { mock: { calls: Array<Array<unknown>> } };
function mockCall(source: MockCallSource, label: string, callIndex = 0): Array<unknown> {
const call = source.mock.calls[callIndex];
if (!call) {
throw new Error(`expected ${label} call ${callIndex}`);
}
return call;
}
function mockObjectArg(
source: MockCallSource,
label: string,
callIndex: number,
argIndex: number,
): Record<string, unknown> {
const value = mockCall(source, label, callIndex)[argIndex];
if (!value || typeof value !== "object") {
throw new Error(`expected ${label} call ${callIndex} argument ${argIndex} to be an object`);
}
return value as Record<string, unknown>;
}
function handleMessagingAction(
action: string,
params: Record<string, unknown>,
@@ -489,13 +512,14 @@ describe("handleDiscordMessagingAction", () => {
{ mediaAccess, mediaLocalRoots: ["/tmp/agent-root"], mediaReadFile },
);
expect(sendMessageDiscord).toHaveBeenCalledTimes(1);
const [, , sendOptions] = sendMessageDiscord.mock.calls[0] ?? [];
expect(sendMessageDiscord.mock.calls[0]?.[0]).toBe("channel:123");
expect(sendMessageDiscord.mock.calls[0]?.[1]).toBe("hello");
expect(sendOptions?.mediaAccess).toBe(mediaAccess);
expect(sendOptions?.mediaUrl).toBe("/tmp/image.png");
expect(sendOptions?.mediaLocalRoots).toEqual(["/tmp/agent-root"]);
expect(sendOptions?.mediaReadFile).toBe(mediaReadFile);
const call = mockCall(sendMessageDiscord, "sendMessageDiscord");
const sendOptions = mockObjectArg(sendMessageDiscord, "sendMessageDiscord", 0, 2);
expect(call[0]).toBe("channel:123");
expect(call[1]).toBe("hello");
expect(sendOptions.mediaAccess).toBe(mediaAccess);
expect(sendOptions.mediaUrl).toBe("/tmp/image.png");
expect(sendOptions.mediaLocalRoots).toEqual(["/tmp/agent-root"]);
expect(sendOptions.mediaReadFile).toBe(mediaReadFile);
});
it("allows media-only message sends", async () => {
@@ -511,11 +535,13 @@ describe("handleDiscordMessagingAction", () => {
{ mediaLocalRoots: ["/tmp/agent-root"] },
);
expect(sendMessageDiscord).toHaveBeenCalledTimes(1);
const [, content, sendOptions] = sendMessageDiscord.mock.calls[0] ?? [];
expect(sendMessageDiscord.mock.calls[0]?.[0]).toBe("channel:123");
const call = mockCall(sendMessageDiscord, "sendMessageDiscord");
const sendOptions = mockObjectArg(sendMessageDiscord, "sendMessageDiscord", 0, 2);
expect(call[0]).toBe("channel:123");
const content = call[1];
expect(content).toBe("");
expect(sendOptions?.mediaUrl).toBe("/tmp/image.png");
expect(sendOptions?.mediaLocalRoots).toEqual(["/tmp/agent-root"]);
expect(sendOptions.mediaUrl).toBe("/tmp/image.png");
expect(sendOptions.mediaLocalRoots).toEqual(["/tmp/agent-root"]);
});
it("ignores empty components objects for regular media sends", async () => {
@@ -537,11 +563,13 @@ describe("handleDiscordMessagingAction", () => {
expect(sendDiscordComponentMessage).not.toHaveBeenCalled();
expect(sendMessageDiscord).toHaveBeenCalledTimes(1);
const [, content, sendOptions] = sendMessageDiscord.mock.calls[0] ?? [];
expect(sendMessageDiscord.mock.calls[0]?.[0]).toBe("channel:123");
const call = mockCall(sendMessageDiscord, "sendMessageDiscord");
const sendOptions = mockObjectArg(sendMessageDiscord, "sendMessageDiscord", 0, 2);
expect(call[0]).toBe("channel:123");
const content = call[1];
expect(content).toBe("hello");
expect(sendOptions?.mediaUrl).toBe("/tmp/image.png");
expect(sendOptions?.mediaLocalRoots).toEqual(["/tmp/agent-root"]);
expect(sendOptions.mediaUrl).toBe("/tmp/image.png");
expect(sendOptions.mediaLocalRoots).toEqual(["/tmp/agent-root"]);
});
it("forwards the optional filename into sendMessageDiscord", async () => {
@@ -557,11 +585,13 @@ describe("handleDiscordMessagingAction", () => {
enableAllActions,
);
expect(sendMessageDiscord).toHaveBeenCalledTimes(1);
const [, content, sendOptions] = sendMessageDiscord.mock.calls[0] ?? [];
expect(sendMessageDiscord.mock.calls[0]?.[0]).toBe("channel:123");
const call = mockCall(sendMessageDiscord, "sendMessageDiscord");
const sendOptions = mockObjectArg(sendMessageDiscord, "sendMessageDiscord", 0, 2);
expect(call[0]).toBe("channel:123");
const content = call[1];
expect(content).toBe("hello");
expect(sendOptions?.mediaUrl).toBe("/tmp/generated-image");
expect(sendOptions?.filename).toBe("image.png");
expect(sendOptions.mediaUrl).toBe("/tmp/generated-image");
expect(sendOptions.filename).toBe("image.png");
});
it("rejects voice messages that include content", async () => {
@@ -670,9 +700,10 @@ describe("handleDiscordGuildAction", () => {
cfg,
accountId: "work",
});
expect(result.details?.ok).toBe(true);
expect(result.details?.status).toBe("online");
expect(result.details?.activities).toEqual([]);
const details = result.details as Record<string, unknown>;
expect(details.ok).toBe(true);
expect(details.status).toBe("online");
expect(details.activities).toEqual([]);
});
});
@@ -963,10 +994,11 @@ describe("handleDiscordModerationAction", () => {
moderationEnabled,
);
expect(timeoutMemberDiscord).toHaveBeenCalledTimes(1);
expect(timeoutMemberDiscord.mock.calls[0]?.[0].guildId).toBe("G1");
expect(timeoutMemberDiscord.mock.calls[0]?.[0].userId).toBe("U1");
expect(timeoutMemberDiscord.mock.calls[0]?.[0].durationMinutes).toBe(5);
expect(timeoutMemberDiscord.mock.calls[0]?.[1]).toEqual({
const params = mockObjectArg(timeoutMemberDiscord, "timeoutMemberDiscord", 0, 0);
expect(params.guildId).toBe("G1");
expect(params.userId).toBe("U1");
expect(params.durationMinutes).toBe(5);
expect(mockCall(timeoutMemberDiscord, "timeoutMemberDiscord")[1]).toEqual({
cfg: DISCORD_TEST_CFG,
accountId: "ops",
});
@@ -990,9 +1022,13 @@ describe("handleDiscordAction per-account gating", () => {
cfg,
);
expect(timeoutMemberDiscord).toHaveBeenCalledTimes(1);
expect(timeoutMemberDiscord.mock.calls[0]?.[0].guildId).toBe("G1");
expect(timeoutMemberDiscord.mock.calls[0]?.[0].userId).toBe("U1");
expect(timeoutMemberDiscord.mock.calls[0]?.[1]).toEqual({ cfg, accountId: "ops" });
const params = mockObjectArg(timeoutMemberDiscord, "timeoutMemberDiscord", 0, 0);
expect(params.guildId).toBe("G1");
expect(params.userId).toBe("U1");
expect(mockCall(timeoutMemberDiscord, "timeoutMemberDiscord")[1]).toEqual({
cfg,
accountId: "ops",
});
});
it("blocks moderation when account omits it", async () => {
@@ -1075,8 +1111,12 @@ describe("handleDiscordAction per-account gating", () => {
);
expect(createChannelDiscord).toHaveBeenCalledTimes(1);
expect(createChannelDiscord.mock.calls[0]?.[0].guildId).toBe("G1");
expect(createChannelDiscord.mock.calls[0]?.[0].name).toBe("alerts");
expect(createChannelDiscord.mock.calls[0]?.[1]).toEqual({ cfg, accountId: "ops" });
const params = mockObjectArg(createChannelDiscord, "createChannelDiscord", 0, 0);
expect(params.guildId).toBe("G1");
expect(params.name).toBe("alerts");
expect(mockCall(createChannelDiscord, "createChannelDiscord")[1]).toEqual({
cfg,
accountId: "ops",
});
});
});

View File

@@ -29,15 +29,25 @@ vi.mock("./typing.js", () => ({
}));
type SetStatusFn = (patch: Record<string, unknown>) => void;
type MockCallSource = { mock: { calls: Array<Array<unknown>> } };
function statusPatches(setStatus: { mock: { calls: Array<[Record<string, unknown>]> } }) {
return setStatus.mock.calls.map(([patch]) => patch);
function mockCall(source: MockCallSource, label: string, callIndex = 0): Array<unknown> {
const call = source.mock.calls[callIndex];
if (!call) {
throw new Error(`expected ${label} call ${callIndex}`);
}
return call;
}
function expectStatusPatch(
setStatus: { mock: { calls: Array<[Record<string, unknown>]> } },
expected: Record<string, unknown>,
) {
function mockCalls(source: MockCallSource): Array<Array<unknown>> {
return source.mock.calls;
}
function statusPatches(setStatus: MockCallSource) {
return setStatus.mock.calls.map(([patch]) => patch as Record<string, unknown>);
}
function expectStatusPatch(setStatus: MockCallSource, expected: Record<string, unknown>) {
expect(
statusPatches(setStatus).some((patch) =>
Object.entries(expected).every(([key, value]) => patch[key] === value),
@@ -184,7 +194,10 @@ describe("createDiscordMessageHandler queue behavior", () => {
await flushQueueWork();
expect(earlyTypingMocks.createDiscordRestClient).toHaveBeenCalledTimes(1);
const [restClientParams] = earlyTypingMocks.createDiscordRestClient.mock.calls[0] ?? [];
const [restClientParams] = mockCall(
earlyTypingMocks.createDiscordRestClient,
"createDiscordRestClient",
);
expect((restClientParams as { accountId?: unknown } | undefined)?.accountId).toBe("default");
expect((restClientParams as { token?: unknown } | undefined)?.token).toBe("test-token");
expect(earlyTypingMocks.sendTyping).toHaveBeenCalledWith({
@@ -364,8 +377,9 @@ describe("createDiscordMessageHandler queue behavior", () => {
await expect(handler(duplicate as never, {} as never)).resolves.toBeUndefined();
await flushQueueWork();
expect(processDiscordMessageMock).toHaveBeenCalledTimes(1);
const runtimeError = params.runtime.error as unknown as MockCallSource;
expect(params.runtime.error).toHaveBeenCalledTimes(1);
expect(String(params.runtime.error.mock.calls[0]?.[0])).toContain(
expect(String(mockCall(runtimeError, "runtime.error")[0])).toContain(
"discord message run failed: DiscordRetryableInboundError: retry me",
);
@@ -392,8 +406,9 @@ describe("createDiscordMessageHandler queue behavior", () => {
await expect(handler(duplicate as never, {} as never)).resolves.toBeUndefined();
await flushQueueWork();
expect(processDiscordMessageMock).toHaveBeenCalledTimes(1);
const runtimeError = params.runtime.error as unknown as MockCallSource;
expect(params.runtime.error).toHaveBeenCalledTimes(1);
expect(String(params.runtime.error.mock.calls[0]?.[0])).toContain(
expect(String(mockCall(runtimeError, "runtime.error")[0])).toContain(
"discord message run failed: Error: post-send failure",
);
@@ -444,8 +459,9 @@ describe("createDiscordMessageHandler queue behavior", () => {
expect(processDiscordMessageMock).toHaveBeenCalledTimes(1);
expect(capturedAbortSignals).toEqual([undefined]);
const runtimeError = params.runtime.error as unknown as MockCallSource;
expect(
params.runtime.error.mock.calls.some(([message]) => String(message).includes("timed out")),
mockCalls(runtimeError).some(([message]) => String(message).includes("timed out")),
).toBe(false);
firstRun.resolve();

View File

@@ -320,7 +320,7 @@ describe("discord component interactions", () => {
expect(dispatchReplyMock).toHaveBeenCalledTimes(1);
const dispatchParams = dispatchReplyMock.mock.calls[0]?.[0] as DispatchParams | undefined;
expect(typeof dispatchParams?.dispatcherOptions.responsePrefixContextProvider).toBe("function");
expect(typeof dispatchParams?.replyOptions.onModelSelected).toBe("function");
expect(typeof dispatchParams?.replyOptions?.onModelSelected).toBe("function");
expect(resolveDiscordComponentEntry({ id: "btn_1" })).toBeNull();
});

View File

@@ -159,15 +159,19 @@ describe("runMatrixStartupMaintenance", () => {
if (!profileSyncParams) {
throw new Error("profile sync params missing");
}
const loadAvatarFromUrl = profileSyncParams.loadAvatarFromUrl;
if (!loadAvatarFromUrl) {
throw new Error("profile sync params missing loadAvatarFromUrl");
}
expect(profileSyncParams).toStrictEqual({
client: params.client,
userId: "@bot:example.org",
displayName: "Ops Bot",
avatarUrl: "https://example.org/avatar.png",
loadAvatarFromUrl: profileSyncParams.loadAvatarFromUrl,
loadAvatarFromUrl,
});
await expect(
profileSyncParams.loadAvatarFromUrl("https://example.org/new-avatar.png", 123),
loadAvatarFromUrl("https://example.org/new-avatar.png", 123),
).resolves.toStrictEqual({
buffer: Buffer.from("avatar"),
contentType: "image/png",
@@ -180,7 +184,11 @@ describe("runMatrixStartupMaintenance", () => {
{ avatarUrl: "mxc://avatar" },
);
expect(params.replaceConfigFile).toHaveBeenCalledWith(updatedCfg as never);
expect(params.logVerboseMessage).toHaveBeenCalledWith(
const logVerboseMessage = params.logVerboseMessage;
if (!logVerboseMessage) {
throw new Error("expected logVerboseMessage");
}
expect(logVerboseMessage).toHaveBeenCalledWith(
"matrix: persisted converted avatar URL for account ops (mxc://avatar)",
);
});

View File

@@ -76,6 +76,8 @@
"approvalPolicy": "never",
"approvalsReviewer": "user",
"config": {
"features.code_mode": true,
"features.code_mode_only": true,
"instructions": "OpenClaw loaded these user-editable workspace files. Treat them as project/user context. Codex loads AGENTS.md natively, so AGENTS.md is not repeated here.\n\n# Project Context\n\nThe following project context files have been loaded:\nSOUL.md: persona/tone. Follow it unless higher-priority instructions override.\n\n## /tmp/openclaw-happy-path/workspace/SOUL.md\n\n<SOUL.md contents will be here>\n\n## /tmp/openclaw-happy-path/workspace/TOOLS.md\n\n<TOOLS.md contents will be here>\n\n## /tmp/openclaw-happy-path/workspace/HEARTBEAT.md\n\n<HEARTBEAT.md contents will be here>"
},
"cwd": "/tmp/openclaw-happy-path/workspace",
@@ -112,6 +114,8 @@
"approvalPolicy": "never",
"approvalsReviewer": "user",
"config": {
"features.code_mode": true,
"features.code_mode_only": true,
"instructions": "OpenClaw loaded these user-editable workspace files. Treat them as project/user context. Codex loads AGENTS.md natively, so AGENTS.md is not repeated here.\n\n# Project Context\n\nThe following project context files have been loaded:\nSOUL.md: persona/tone. Follow it unless higher-priority instructions override.\n\n## /tmp/openclaw-happy-path/workspace/SOUL.md\n\n<SOUL.md contents will be here>\n\n## /tmp/openclaw-happy-path/workspace/TOOLS.md\n\n<TOOLS.md contents will be here>\n\n## /tmp/openclaw-happy-path/workspace/HEARTBEAT.md\n\n<HEARTBEAT.md contents will be here>"
},
"developerInstructions": "<see Reconstructed Model-Bound Prompt Layers>",

View File

@@ -76,6 +76,8 @@
"approvalPolicy": "never",
"approvalsReviewer": "user",
"config": {
"features.code_mode": true,
"features.code_mode_only": true,
"instructions": "OpenClaw loaded these user-editable workspace files. Treat them as project/user context. Codex loads AGENTS.md natively, so AGENTS.md is not repeated here.\n\n# Project Context\n\nThe following project context files have been loaded:\nSOUL.md: persona/tone. Follow it unless higher-priority instructions override.\n\n## /tmp/openclaw-happy-path/workspace/SOUL.md\n\n<SOUL.md contents will be here>\n\n## /tmp/openclaw-happy-path/workspace/TOOLS.md\n\n<TOOLS.md contents will be here>\n\n## /tmp/openclaw-happy-path/workspace/HEARTBEAT.md\n\n<HEARTBEAT.md contents will be here>"
},
"cwd": "/tmp/openclaw-happy-path/workspace",
@@ -112,6 +114,8 @@
"approvalPolicy": "never",
"approvalsReviewer": "user",
"config": {
"features.code_mode": true,
"features.code_mode_only": true,
"instructions": "OpenClaw loaded these user-editable workspace files. Treat them as project/user context. Codex loads AGENTS.md natively, so AGENTS.md is not repeated here.\n\n# Project Context\n\nThe following project context files have been loaded:\nSOUL.md: persona/tone. Follow it unless higher-priority instructions override.\n\n## /tmp/openclaw-happy-path/workspace/SOUL.md\n\n<SOUL.md contents will be here>\n\n## /tmp/openclaw-happy-path/workspace/TOOLS.md\n\n<TOOLS.md contents will be here>\n\n## /tmp/openclaw-happy-path/workspace/HEARTBEAT.md\n\n<HEARTBEAT.md contents will be here>"
},
"developerInstructions": "<see Reconstructed Model-Bound Prompt Layers>",

View File

@@ -76,6 +76,8 @@
"approvalPolicy": "never",
"approvalsReviewer": "user",
"config": {
"features.code_mode": true,
"features.code_mode_only": true,
"instructions": "OpenClaw loaded these user-editable workspace files. Treat them as project/user context. Codex loads AGENTS.md natively, so AGENTS.md is not repeated here.\n\n# Project Context\n\nThe following project context files have been loaded:\nSOUL.md: persona/tone. Follow it unless higher-priority instructions override.\n\n## /tmp/openclaw-happy-path/workspace/SOUL.md\n\n<SOUL.md contents will be here>\n\n## /tmp/openclaw-happy-path/workspace/TOOLS.md\n\n<TOOLS.md contents will be here>\n\n## /tmp/openclaw-happy-path/workspace/HEARTBEAT.md\n\n<HEARTBEAT.md contents will be here>"
},
"cwd": "/tmp/openclaw-happy-path/workspace",
@@ -113,6 +115,8 @@
"approvalPolicy": "never",
"approvalsReviewer": "user",
"config": {
"features.code_mode": true,
"features.code_mode_only": true,
"instructions": "OpenClaw loaded these user-editable workspace files. Treat them as project/user context. Codex loads AGENTS.md natively, so AGENTS.md is not repeated here.\n\n# Project Context\n\nThe following project context files have been loaded:\nSOUL.md: persona/tone. Follow it unless higher-priority instructions override.\n\n## /tmp/openclaw-happy-path/workspace/SOUL.md\n\n<SOUL.md contents will be here>\n\n## /tmp/openclaw-happy-path/workspace/TOOLS.md\n\n<TOOLS.md contents will be here>\n\n## /tmp/openclaw-happy-path/workspace/HEARTBEAT.md\n\n<HEARTBEAT.md contents will be here>"
},
"developerInstructions": "<see Reconstructed Model-Bound Prompt Layers>",