From 2d0e25c23aa70c31657eaa5d032d48231ed75abf Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 8 Apr 2026 18:15:10 +0100 Subject: [PATCH] fix: pass system prompt to codex cli --- CHANGELOG.md | 1 + docs/.generated/config-baseline.sha256 | 4 +- docs/gateway/cli-backends.md | 9 ++++ extensions/openai/cli-backend.ts | 3 ++ src/agents/cli-backends.test.ts | 16 ++++++- src/agents/cli-runner.helpers.test.ts | 40 +++++++++++++++++ src/agents/cli-runner.spawn.test.ts | 36 +++++++++++++++ src/agents/cli-runner.test-support.ts | 3 ++ src/agents/cli-runner/bundle-mcp.ts | 30 +------------ src/agents/cli-runner/execute.ts | 12 +++++ src/agents/cli-runner/helpers.ts | 45 ++++++++++++++++++- src/agents/cli-runner/toml-inline.ts | 36 +++++++++++++++ ...nfig.providers.volcengine-byteplus.test.ts | 16 +++++++ src/config/schema.base.generated.ts | 6 +++ src/config/types.agent-defaults.ts | 4 ++ src/config/zod-schema.core.ts | 2 + src/gateway/server-restart-sentinel.test.ts | 14 ++++-- 17 files changed, 239 insertions(+), 38 deletions(-) create mode 100644 src/agents/cli-runner/toml-inline.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a7fed652c19..c5d28db609e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai - Android/pairing: clear stale setup-code auth on new QR scans, bootstrap operator and node sessions from fresh pairing, prefer stored device tokens after bootstrap handoff, and pause pairing auto-retry while the app is backgrounded so scan-once Android pairing recovers reliably again. (#63199) Thanks @obviyus. - Auto-reply/NO_REPLY: strip glued leading `NO_REPLY` tokens before reply normalization and ACP-visible streaming so silent sentinel text no longer leaks into user-visible replies while preserving substantive `NO_REPLY ...` text. Thanks @frankekn. - Gateway/sessions: clear auto-fallback-pinned model overrides on `/reset` and `/new` while still preserving explicit user model selections, including legacy sessions created before override-source tracking existed. (#63155) Thanks @frankekn. +- Codex CLI: pass OpenClaw's system prompt through Codex's `model_instructions_file` config override so fresh Codex CLI sessions receive the same prompt guidance as Claude CLI sessions. ## 2026.4.8 diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index 5c629cab7dc..18239a3d1c4 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -1,4 +1,4 @@ -6092701439f9f56624f508eb2b240cb48375264c2667a99cb7e7823cb0ef18d1 config-baseline.json -065f474b340fc22b19358cb298131037cbb2a3411ef0b6f765072bbaafedf751 config-baseline.core.json +0a75b57f5dbb0bb1488eacb47111ee22ff42dd3747bfe07bb69c9445d5e55c3e config-baseline.json +ff15bb8b4231fc80174249ae89bcb61439d7adda5ee6be95e4d304680253a59f config-baseline.core.json 7f42b22b46c487d64aaac46001ba9d9096cf7bf0b1c263a54d39946303ff5018 config-baseline.channel.json 483d4f3c1d516719870ad6f2aba6779b9950f85471ee77b9994a077a7574a892 config-baseline.plugin.json diff --git a/docs/gateway/cli-backends.md b/docs/gateway/cli-backends.md index b9c24930cb2..ee1ecfee73e 100644 --- a/docs/gateway/cli-backends.md +++ b/docs/gateway/cli-backends.md @@ -124,6 +124,9 @@ The provider id becomes the left side of your model ref: sessionMode: "existing", sessionIdFields: ["session_id", "conversation_id"], systemPromptArg: "--system", + // Codex-style CLIs can point at a prompt file instead: + // systemPromptFileConfigArg: "-c", + // systemPromptFileConfigKey: "model_instructions_file", systemPromptWhen: "first", imageArg: "--image", imageMode: "repeat", @@ -150,6 +153,12 @@ told us OpenClaw-style Claude CLI usage is allowed again, so OpenClaw treats a new policy. +The bundled OpenAI `codex-cli` backend passes OpenClaw's system prompt through +Codex's `model_instructions_file` config override (`-c +model_instructions_file="..."`). Codex does not expose a Claude-style +`--append-system-prompt` flag, so OpenClaw writes the assembled prompt to a +temporary file for each fresh Codex CLI session. + ## Sessions - If the CLI supports sessions, set `sessionArg` (e.g. `--session-id`) or diff --git a/extensions/openai/cli-backend.ts b/extensions/openai/cli-backend.ts index 30874ccfe77..1d9c7ccc285 100644 --- a/extensions/openai/cli-backend.ts +++ b/extensions/openai/cli-backend.ts @@ -38,6 +38,9 @@ export function buildOpenAICodexCliBackend(): CliBackendPlugin { modelArg: "--model", sessionIdFields: ["thread_id"], sessionMode: "existing", + systemPromptFileConfigArg: "-c", + systemPromptFileConfigKey: "model_instructions_file", + systemPromptWhen: "first", imageArg: "--image", imageMode: "repeat", reliability: { diff --git a/src/agents/cli-backends.test.ts b/src/agents/cli-backends.test.ts index 26554069deb..e118f34cc1f 100644 --- a/src/agents/cli-backends.test.ts +++ b/src/agents/cli-backends.test.ts @@ -1,9 +1,10 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import type { CliBackendConfig } from "../config/types.js"; import type { CliBundleMcpMode } from "../plugins/types.js"; let createEmptyPluginRegistry: typeof import("../plugins/registry.js").createEmptyPluginRegistry; +let resetPluginRuntimeStateForTest: typeof import("../plugins/runtime.js").resetPluginRuntimeStateForTest; let setActivePluginRegistry: typeof import("../plugins/runtime.js").setActivePluginRegistry; let normalizeClaudeBackendConfig: typeof import("./cli-backends.js").normalizeClaudeBackendConfig; let resolveCliBackendConfig: typeof import("./cli-backends.js").resolveCliBackendConfig; @@ -96,11 +97,16 @@ beforeAll(async () => { vi.doUnmock("../plugins/setup-registry.js"); vi.doUnmock("../plugins/cli-backends.runtime.js"); ({ createEmptyPluginRegistry } = await import("../plugins/registry.js")); - ({ setActivePluginRegistry } = await import("../plugins/runtime.js")); + ({ resetPluginRuntimeStateForTest, setActivePluginRegistry } = + await import("../plugins/runtime.js")); ({ normalizeClaudeBackendConfig, resolveCliBackendConfig, resolveCliBackendLiveTest } = await import("./cli-backends.js")); }); +afterEach(() => { + resetPluginRuntimeStateForTest(); +}); + beforeEach(() => { const registry = createEmptyPluginRegistry(); registry.cliBackends = [ @@ -177,6 +183,9 @@ beforeEach(() => { "--skip-git-repo-check", ], resumeArgs: ["exec", "resume", "{sessionId}", "--dangerously-bypass-approvals-and-sandbox"], + systemPromptFileConfigArg: "-c", + systemPromptFileConfigKey: "model_instructions_file", + systemPromptWhen: "first", reliability: { watchdog: { fresh: { @@ -657,6 +666,9 @@ describe("resolveCliBackendConfig google-gemini-cli defaults", () => { expect(resolved).not.toBeNull(); expect(resolved?.bundleMcp).toBe(true); expect(resolved?.bundleMcpMode).toBe("codex-config-overrides"); + expect(resolved?.config.systemPromptFileConfigArg).toBe("-c"); + expect(resolved?.config.systemPromptFileConfigKey).toBe("model_instructions_file"); + expect(resolved?.config.systemPromptWhen).toBe("first"); }); }); diff --git a/src/agents/cli-runner.helpers.test.ts b/src/agents/cli-runner.helpers.test.ts index 5e921d8cd0b..7f4be0140c5 100644 --- a/src/agents/cli-runner.helpers.test.ts +++ b/src/agents/cli-runner.helpers.test.ts @@ -10,6 +10,7 @@ import { prepareCliPromptImagePayload, resolveCliRunQueueKey, writeCliImages, + writeCliSystemPromptFile, } from "./cli-runner/helpers.js"; import * as promptImageUtils from "./pi-embedded-runner/run/images.js"; import type { SandboxFsBridge } from "./sandbox/fs-bridge.js"; @@ -143,6 +144,23 @@ describe("buildCliArgs", () => { ).toEqual(["-p", "--append-system-prompt", "Stable prefix\nDynamic suffix"]); }); + it("passes Codex system prompts via a model instructions file config override", () => { + expect( + buildCliArgs({ + backend: { + command: "codex", + systemPromptFileConfigArg: "-c", + systemPromptFileConfigKey: "model_instructions_file", + }, + baseArgs: ["exec", "--json"], + modelId: "gpt-5.4", + systemPrompt: "Stable prefix", + systemPromptFilePath: "/tmp/openclaw/system-prompt.md", + useResume: false, + }), + ).toEqual(["exec", "--json", "-c", 'model_instructions_file="/tmp/openclaw/system-prompt.md"']); + }); + it("replaces prompt placeholders before falling back to a trailing positional prompt", () => { expect( buildCliArgs({ @@ -412,6 +430,28 @@ describe("writeCliImages", () => { }); }); +describe("writeCliSystemPromptFile", () => { + it("writes stripped system prompts to a private temp file", async () => { + const written = await writeCliSystemPromptFile({ + backend: { + command: "codex", + systemPromptFileConfigKey: "model_instructions_file", + }, + systemPrompt: `Stable prefix${SYSTEM_PROMPT_CACHE_BOUNDARY}Dynamic suffix`, + }); + + try { + expect(written.filePath).toContain("openclaw-cli-system-prompt-"); + await expect(fs.readFile(written.filePath ?? "", "utf-8")).resolves.toBe( + "Stable prefix\nDynamic suffix", + ); + } finally { + await written.cleanup(); + } + await expect(fs.access(written.filePath ?? "")).rejects.toMatchObject({ code: "ENOENT" }); + }); +}); + describe("resolveCliRunQueueKey", () => { it("scopes Claude CLI serialization to the workspace for fresh runs", () => { expect( diff --git a/src/agents/cli-runner.spawn.test.ts b/src/agents/cli-runner.spawn.test.ts index 9dfbabb1f30..cbec0e093c0 100644 --- a/src/agents/cli-runner.spawn.test.ts +++ b/src/agents/cli-runner.spawn.test.ts @@ -51,6 +51,9 @@ function buildPreparedCliRunContext(params: { input: "arg" as const, modelArg: "--model", sessionMode: "existing" as const, + systemPromptFileConfigArg: "-c", + systemPromptFileConfigKey: "model_instructions_file", + systemPromptWhen: "first" as const, serialize: true, }; const backend = { ...baseBackend, ...params.backend }; @@ -221,6 +224,39 @@ describe("runCliAgent spawn path", () => { expect(input.scopeKey).toContain("thread-123"); }); + it("passes Codex system prompts through model_instructions_file", async () => { + let promptFileText = ""; + supervisorSpawnMock.mockImplementationOnce(async (...args: unknown[]) => { + const input = (args[0] ?? {}) as { argv?: string[] }; + const configArgIndex = input.argv?.indexOf("-c") ?? -1; + expect(configArgIndex).toBeGreaterThanOrEqual(0); + const configArg = input.argv?.[configArgIndex + 1] ?? ""; + const match = /^model_instructions_file="(.+)"$/.exec(configArg); + expect(match?.[1]).toBeTruthy(); + promptFileText = await fs.readFile(match?.[1] ?? "", "utf-8"); + return createManagedRun({ + reason: "exit", + exitCode: 0, + exitSignal: null, + durationMs: 50, + stdout: "ok", + stderr: "", + timedOut: false, + noOutputTimedOut: false, + }); + }); + + await executePreparedCliRun( + buildPreparedCliRunContext({ + provider: "codex-cli", + model: "gpt-5.4", + runId: "run-codex-system-prompt-file", + }), + ); + + expect(promptFileText).toBe("You are a helpful assistant."); + }); + it("cancels the managed CLI run when the abort signal fires", async () => { const abortController = new AbortController(); let resolveWait!: (value: { diff --git a/src/agents/cli-runner.test-support.ts b/src/agents/cli-runner.test-support.ts index 193ac6772a1..782f9f208ca 100644 --- a/src/agents/cli-runner.test-support.ts +++ b/src/agents/cli-runner.test-support.ts @@ -135,6 +135,9 @@ function buildOpenAICodexCliBackendFixture(): CliBackendPlugin { modelArg: "--model", sessionIdFields: ["thread_id"], sessionMode: "existing", + systemPromptFileConfigArg: "-c", + systemPromptFileConfigKey: "model_instructions_file", + systemPromptWhen: "first", imageArg: "--image", imageMode: "repeat", reliability: { diff --git a/src/agents/cli-runner/bundle-mcp.ts b/src/agents/cli-runner/bundle-mcp.ts index 2ceaa26786a..43ace46f9c3 100644 --- a/src/agents/cli-runner/bundle-mcp.ts +++ b/src/agents/cli-runner/bundle-mcp.ts @@ -16,6 +16,7 @@ import { normalizeOptionalLowercaseString, normalizeOptionalString, } from "../../shared/string-coerce.js"; +import { serializeTomlInlineValue } from "./toml-inline.js"; type PreparedCliBundleMcpConfig = { backend: CliBackendConfig; @@ -204,35 +205,6 @@ function normalizeGeminiServerConfig( return next; } -function escapeTomlString(value: string): string { - return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"'); -} - -function formatTomlKey(key: string): string { - return /^[A-Za-z0-9_-]+$/.test(key) ? key : `"${escapeTomlString(key)}"`; -} - -function serializeTomlInlineValue(value: unknown): string { - if (typeof value === "string") { - return `"${escapeTomlString(value)}"`; - } - if (typeof value === "number" || typeof value === "bigint") { - return String(value); - } - if (typeof value === "boolean") { - return value ? "true" : "false"; - } - if (Array.isArray(value)) { - return `[${value.map((entry) => serializeTomlInlineValue(entry)).join(", ")}]`; - } - if (isRecord(value)) { - return `{ ${Object.entries(value) - .map(([key, entry]) => `${formatTomlKey(key)} = ${serializeTomlInlineValue(entry)}`) - .join(", ")} }`; - } - throw new Error(`Unsupported TOML value for Codex MCP config: ${String(value)}`); -} - function injectCodexMcpConfigArgs(args: string[] | undefined, config: BundleMcpConfig): string[] { const overrides = serializeTomlInlineValue( Object.fromEntries( diff --git a/src/agents/cli-runner/execute.ts b/src/agents/cli-runner/execute.ts index 96b74ad4b7e..739a50f4fb6 100644 --- a/src/agents/cli-runner/execute.ts +++ b/src/agents/cli-runner/execute.ts @@ -25,6 +25,7 @@ import { resolvePromptInput, resolveSessionIdToSend, resolveSystemPromptUsage, + writeCliSystemPromptFile, } from "./helpers.js"; import { cliBackendLog, @@ -113,6 +114,13 @@ export async function executePreparedCliRun( isNewSession: isNew, systemPrompt: context.systemPrompt, }); + const systemPromptFile = + !useResume && systemPromptArg + ? await writeCliSystemPromptFile({ + backend, + systemPrompt: systemPromptArg, + }) + : undefined; let prompt = prependBootstrapPromptWarning(params.prompt, context.bootstrapPromptWarningLines, { preserveExactPrompt: context.heartbeatPrompt, @@ -144,6 +152,7 @@ export async function executePreparedCliRun( modelId: context.normalizedModel, sessionId: resolvedSessionId, systemPrompt: systemPromptArg, + systemPromptFilePath: systemPromptFile?.filePath, imagePaths, promptArg: argsPrompt, useResume, @@ -350,6 +359,9 @@ export async function executePreparedCliRun( }); }); } finally { + if (systemPromptFile) { + await systemPromptFile.cleanup(); + } if (cleanupImages) { await cleanupImages(); } diff --git a/src/agents/cli-runner/helpers.ts b/src/agents/cli-runner/helpers.ts index 1a06fdb6fc9..664a0381dad 100644 --- a/src/agents/cli-runner/helpers.ts +++ b/src/agents/cli-runner/helpers.ts @@ -27,6 +27,7 @@ import { stripSystemPromptCacheBoundary } from "../system-prompt-cache-boundary. import { buildSystemPromptParams } from "../system-prompt-params.js"; import { buildAgentSystemPrompt } from "../system-prompt.js"; import { sanitizeImageBlocks } from "../tool-images.js"; +import { formatTomlConfigOverride } from "./toml-inline.js"; export { buildCliSupervisorScopeKey, resolveCliNoOutputTimeoutMs } from "./reliability.js"; const CLI_RUN_QUEUE = new KeyedAsyncQueue(); @@ -153,7 +154,10 @@ export function resolveSystemPromptUsage(params: { if (when === "first" && !params.isNewSession) { return null; } - if (!params.backend.systemPromptArg?.trim()) { + if ( + !params.backend.systemPromptArg?.trim() && + !params.backend.systemPromptFileConfigKey?.trim() + ) { return null; } return systemPrompt; @@ -280,6 +284,29 @@ export async function writeCliImages(params: { return { paths, cleanup }; } +export async function writeCliSystemPromptFile(params: { + backend: CliBackendConfig; + systemPrompt: string; +}): Promise<{ filePath?: string; cleanup: () => Promise }> { + if (!params.backend.systemPromptFileConfigKey?.trim()) { + return { cleanup: async () => {} }; + } + const tempDir = await fs.mkdtemp( + path.join(resolvePreferredOpenClawTmpDir(), "openclaw-cli-system-prompt-"), + ); + const filePath = path.join(tempDir, "system-prompt.md"); + await fs.writeFile(filePath, stripSystemPromptCacheBoundary(params.systemPrompt), { + encoding: "utf-8", + mode: 0o600, + }); + return { + filePath, + cleanup: async () => { + await fs.rm(tempDir, { recursive: true, force: true }); + }, + }; +} + export async function prepareCliPromptImagePayload(params: { backend: CliBackendConfig; prompt: string; @@ -328,6 +355,7 @@ export function buildCliArgs(params: { modelId: string; sessionId?: string; systemPrompt?: string | null; + systemPromptFilePath?: string; imagePaths?: string[]; promptArg?: string; useResume: boolean; @@ -336,7 +364,20 @@ export function buildCliArgs(params: { if (params.backend.modelArg && params.modelId) { args.push(params.backend.modelArg, params.modelId); } - if (!params.useResume && params.systemPrompt && params.backend.systemPromptArg) { + if ( + !params.useResume && + params.systemPrompt && + params.systemPromptFilePath && + params.backend.systemPromptFileConfigKey + ) { + args.push( + params.backend.systemPromptFileConfigArg ?? "-c", + formatTomlConfigOverride( + params.backend.systemPromptFileConfigKey, + params.systemPromptFilePath, + ), + ); + } else if (!params.useResume && params.systemPrompt && params.backend.systemPromptArg) { args.push(params.backend.systemPromptArg, stripSystemPromptCacheBoundary(params.systemPrompt)); } if (!params.useResume && params.sessionId) { diff --git a/src/agents/cli-runner/toml-inline.ts b/src/agents/cli-runner/toml-inline.ts new file mode 100644 index 00000000000..d1212c448f2 --- /dev/null +++ b/src/agents/cli-runner/toml-inline.ts @@ -0,0 +1,36 @@ +function escapeTomlString(value: string): string { + return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"'); +} + +function formatTomlKey(key: string): string { + return /^[A-Za-z0-9_-]+$/.test(key) ? key : `"${escapeTomlString(key)}"`; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +export function serializeTomlInlineValue(value: unknown): string { + if (typeof value === "string") { + return `"${escapeTomlString(value)}"`; + } + if (typeof value === "number" || typeof value === "bigint") { + return String(value); + } + if (typeof value === "boolean") { + return value ? "true" : "false"; + } + if (Array.isArray(value)) { + return `[${value.map((entry) => serializeTomlInlineValue(entry)).join(", ")}]`; + } + if (isRecord(value)) { + return `{ ${Object.entries(value) + .map(([key, entry]) => `${formatTomlKey(key)} = ${serializeTomlInlineValue(entry)}`) + .join(", ")} }`; + } + throw new Error(`Unsupported TOML inline value: ${String(value)}`); +} + +export function formatTomlConfigOverride(key: string, value: unknown): string { + return `${key}=${serializeTomlInlineValue(value)}`; +} diff --git a/src/agents/models-config.providers.volcengine-byteplus.test.ts b/src/agents/models-config.providers.volcengine-byteplus.test.ts index 5bdf51e6f58..38f83abb2cd 100644 --- a/src/agents/models-config.providers.volcengine-byteplus.test.ts +++ b/src/agents/models-config.providers.volcengine-byteplus.test.ts @@ -1,5 +1,20 @@ import { beforeAll, describe, expect, it, vi } from "vitest"; +async function resetProviderRuntimeState() { + const [ + { clearPluginManifestRegistryCache }, + { resetProviderRuntimeHookCacheForTest }, + { resetPluginLoaderTestStateForTest }, + ] = await Promise.all([ + import("../plugins/manifest-registry.js"), + import("../plugins/provider-runtime.js"), + import("../plugins/loader.test-fixtures.js"), + ]); + resetPluginLoaderTestStateForTest(); + clearPluginManifestRegistryCache(); + resetProviderRuntimeHookCacheForTest(); +} + let createProviderAuthResolver: typeof import("./models-config.providers.secrets.js").createProviderAuthResolver; async function loadSecretsModule() { @@ -7,6 +22,7 @@ async function loadSecretsModule() { vi.doUnmock("../plugins/provider-runtime.js"); vi.doUnmock("../secrets/provider-env-vars.js"); vi.resetModules(); + await resetProviderRuntimeState(); ({ createProviderAuthResolver } = await import("./models-config.providers.secrets.js")); } diff --git a/src/config/schema.base.generated.ts b/src/config/schema.base.generated.ts index 6036f5699a3..ad783e254a8 100644 --- a/src/config/schema.base.generated.ts +++ b/src/config/schema.base.generated.ts @@ -3425,6 +3425,12 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { systemPromptArg: { type: "string", }, + systemPromptFileConfigArg: { + type: "string", + }, + systemPromptFileConfigKey: { + type: "string", + }, systemPromptMode: { anyOf: [ { diff --git a/src/config/types.agent-defaults.ts b/src/config/types.agent-defaults.ts index 78411439e6e..6578683bcb6 100644 --- a/src/config/types.agent-defaults.ts +++ b/src/config/types.agent-defaults.ts @@ -79,6 +79,10 @@ export type CliBackendConfig = { sessionIdFields?: string[]; /** Flag used to pass system prompt. */ systemPromptArg?: string; + /** Config override flag used to pass a system prompt file (e.g. -c). */ + systemPromptFileConfigArg?: string; + /** Config override key used to pass a system prompt file. */ + systemPromptFileConfigKey?: string; /** System prompt behavior (append vs replace). */ systemPromptMode?: "append" | "replace"; /** When to send system prompt. */ diff --git a/src/config/zod-schema.core.ts b/src/config/zod-schema.core.ts index 93b568b15cc..95df00820f3 100644 --- a/src/config/zod-schema.core.ts +++ b/src/config/zod-schema.core.ts @@ -535,6 +535,8 @@ export const CliBackendSchema = z .optional(), sessionIdFields: z.array(z.string()).optional(), systemPromptArg: z.string().optional(), + systemPromptFileConfigArg: z.string().optional(), + systemPromptFileConfigKey: z.string().optional(), systemPromptMode: z.union([z.literal("append"), z.literal("replace")]).optional(), systemPromptWhen: z .union([z.literal("first"), z.literal("always"), z.literal("never")]) diff --git a/src/gateway/server-restart-sentinel.test.ts b/src/gateway/server-restart-sentinel.test.ts index fbe6f556853..13e20b5cdd8 100644 --- a/src/gateway/server-restart-sentinel.test.ts +++ b/src/gateway/server-restart-sentinel.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { mergeMockedModule } from "../test-utils/vitest-module-mocks.js"; const mocks = vi.hoisted(() => ({ @@ -110,6 +110,10 @@ vi.mock("../logging/subsystem.js", () => ({ const { scheduleRestartSentinelWake } = await import("./server-restart-sentinel.js"); describe("scheduleRestartSentinelWake", () => { + afterEach(() => { + vi.useRealTimers(); + }); + beforeEach(() => { vi.useRealTimers(); mocks.consumeRestartSentinel.mockResolvedValue({ @@ -178,7 +182,9 @@ describe("scheduleRestartSentinelWake", () => { .mockResolvedValueOnce([{ channel: "whatsapp", messageId: "msg-2" }]); const wakePromise = scheduleRestartSentinelWake({ deps: {} as never }); - await vi.runAllTimersAsync(); + await Promise.resolve(); + await Promise.resolve(); + await vi.advanceTimersByTimeAsync(750); await wakePromise; expect(mocks.enqueueDelivery).toHaveBeenCalledTimes(1); @@ -218,7 +224,9 @@ describe("scheduleRestartSentinelWake", () => { .mockRejectedValueOnce(new Error("transport still not ready")); const wakePromise = scheduleRestartSentinelWake({ deps: {} as never }); - await vi.runAllTimersAsync(); + await Promise.resolve(); + await Promise.resolve(); + await vi.advanceTimersByTimeAsync(750); await wakePromise; expect(mocks.enqueueDelivery).toHaveBeenCalledTimes(1);