From bff6a6a9c1f90c45a5f46ce6e4f43a7637e03dce Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 30 Mar 2026 01:02:13 +0100 Subject: [PATCH] test(config): align optimistic write helpers --- src/cli/config-cli.test.ts | 4 ++ src/cli/plugins-cli-test-helpers.ts | 37 +++++++++++------ src/cli/update-cli.test.ts | 39 +++++++++--------- src/commands/agents.add.test.ts | 5 +++ src/commands/agents.bind.test-support.ts | 18 +++++++++ src/commands/agents.identity.test.ts | 16 ++++++-- src/commands/channels.mock-harness.ts | 13 +++++- src/commands/channels.resolve.test.ts | 14 +++++-- src/commands/channels/capabilities.test.ts | 16 +++++--- src/commands/configure.wizard.test.ts | 47 +++++++++++++--------- src/gateway/startup-auth.test.ts | 25 +++++++----- 11 files changed, 157 insertions(+), 77 deletions(-) diff --git a/src/cli/config-cli.test.ts b/src/cli/config-cli.test.ts index f1b77534a9e..edc1bedddb9 100644 --- a/src/cli/config-cli.test.ts +++ b/src/cli/config-cli.test.ts @@ -23,6 +23,10 @@ vi.mock("../config/config.js", () => ({ readConfigFileSnapshot: () => mockReadConfigFileSnapshot(), writeConfigFile: (cfg: OpenClawConfig, options?: { unsetPaths?: string[][] }) => mockWriteConfigFile(cfg, options), + replaceConfigFile: (params: { + nextConfig: OpenClawConfig; + writeOptions?: { unsetPaths?: string[][] }; + }) => mockWriteConfigFile(params.nextConfig, params.writeOptions), })); vi.mock("../secrets/resolve.js", () => ({ diff --git a/src/cli/plugins-cli-test-helpers.ts b/src/cli/plugins-cli-test-helpers.ts index 5d65624f159..a3d303169c5 100644 --- a/src/cli/plugins-cli-test-helpers.ts +++ b/src/cli/plugins-cli-test-helpers.ts @@ -8,6 +8,9 @@ export const readConfigFileSnapshot = vi.fn(); export const writeConfigFile = vi.fn<(config: OpenClawConfig) => Promise>( async () => undefined, ); +export const replaceConfigFile = vi.fn( + async (params: { nextConfig: OpenClawConfig }) => await writeConfigFile(params.nextConfig), +); export const resolveStateDir = vi.fn(() => "/tmp/openclaw-state"); export const installPluginFromMarketplace = vi.fn(); export const listMarketplacePlugins = vi.fn(); @@ -42,6 +45,7 @@ vi.mock("../config/config.js", () => ({ loadConfig: () => loadConfig(), readConfigFileSnapshot: (...args: unknown[]) => readConfigFileSnapshot(...args), writeConfigFile: (config: OpenClawConfig) => writeConfigFile(config), + replaceConfigFile: (params: { nextConfig: OpenClawConfig }) => replaceConfigFile(params), })); vi.mock("../config/paths.js", () => ({ @@ -142,6 +146,7 @@ export function resetPluginsCliTestState() { loadConfig.mockReset(); readConfigFileSnapshot.mockReset(); writeConfigFile.mockReset(); + replaceConfigFile.mockReset(); resolveStateDir.mockReset(); installPluginFromMarketplace.mockReset(); listMarketplacePlugins.mockReset(); @@ -164,20 +169,28 @@ export function resetPluginsCliTestState() { recordHookInstall.mockReset(); loadConfig.mockReturnValue({} as OpenClawConfig); - readConfigFileSnapshot.mockResolvedValue({ - path: "/tmp/openclaw-config.json5", - exists: true, - raw: "{}", - parsed: {}, - resolved: {}, - valid: true, - config: {} as OpenClawConfig, - hash: "mock", - issues: [], - warnings: [], - legacyIssues: [], + readConfigFileSnapshot.mockImplementation(async () => { + const config = loadConfig(); + return { + path: "/tmp/openclaw-config.json5", + exists: true, + raw: "{}", + parsed: config, + resolved: config, + sourceConfig: config, + runtimeConfig: config, + valid: true, + config, + hash: "mock", + issues: [], + warnings: [], + legacyIssues: [], + }; }); writeConfigFile.mockResolvedValue(undefined); + replaceConfigFile.mockImplementation( + async (params: { nextConfig: OpenClawConfig }) => await writeConfigFile(params.nextConfig), + ); resolveStateDir.mockReturnValue("/tmp/openclaw-state"); resolveMarketplaceInstallShortcut.mockResolvedValue(null); installPluginFromMarketplace.mockResolvedValue({ diff --git a/src/cli/update-cli.test.ts b/src/cli/update-cli.test.ts index 5f41d27299f..a784ac8766e 100644 --- a/src/cli/update-cli.test.ts +++ b/src/cli/update-cli.test.ts @@ -49,8 +49,8 @@ vi.mock("../infra/openclaw-root.js", () => ({ vi.mock("../config/config.js", () => ({ readConfigFileSnapshot: vi.fn(), + replaceConfigFile: vi.fn(), resolveGatewayPort: vi.fn(() => 18789), - writeConfigFile: vi.fn(), })); vi.mock("../infra/update-check.js", async (importOriginal) => { @@ -149,7 +149,7 @@ vi.mock("../runtime.js", () => ({ const { runGatewayUpdate } = await import("../infra/update-runner.js"); const { resolveOpenClawPackageRoot } = await import("../infra/openclaw-root.js"); -const { readConfigFileSnapshot, writeConfigFile } = await import("../config/config.js"); +const { readConfigFileSnapshot, replaceConfigFile } = await import("../config/config.js"); const { checkUpdateStatus, fetchNpmPackageTargetStatus, fetchNpmTagVersion, resolveNpmChannelTag } = await import("../infra/update-check.js"); const { runCommandWithTimeout } = await import("../process/exec.js"); @@ -419,7 +419,7 @@ describe("update-cli", () => { await updateCommand({ dryRun: true, channel: "beta" }); }, assert: () => { - expect(writeConfigFile).not.toHaveBeenCalled(); + expect(replaceConfigFile).not.toHaveBeenCalled(); expect(runGatewayUpdate).not.toHaveBeenCalled(); expect(runDaemonInstall).not.toHaveBeenCalled(); expect(runRestartScript).not.toHaveBeenCalled(); @@ -555,11 +555,11 @@ describe("update-cli", () => { } if (expectedPersistedChannel !== undefined) { - expect(writeConfigFile).toHaveBeenCalled(); - const writeCall = vi.mocked(writeConfigFile).mock.calls[0]?.[0] as { - update?: { channel?: string }; - }; - expect(writeCall?.update?.channel).toBe(expectedPersistedChannel); + expect(replaceConfigFile).toHaveBeenCalled(); + const writeCall = vi.mocked(replaceConfigFile).mock.calls[0]?.[0] as + | { nextConfig?: { update?: { channel?: string } } } + | undefined; + expect(writeCall?.nextConfig?.update?.channel).toBe(expectedPersistedChannel); } }, ); @@ -706,7 +706,7 @@ describe("update-cli", () => { await updateCommand({ yes: true, tag: "2026.3.23-2" }); expect(defaultRuntime.exit).toHaveBeenCalledWith(1); - expect(writeConfigFile).not.toHaveBeenCalled(); + expect(replaceConfigFile).not.toHaveBeenCalled(); const logs = vi.mocked(defaultRuntime.log).mock.calls.map((call) => String(call[0])); expect(logs.join("\n")).toContain("global install verify"); expect(logs.join("\n")).toContain("expected installed version 2026.3.23-2, found 2026.3.23"); @@ -813,16 +813,19 @@ describe("update-cli", () => { call[0][2] === "-g", ); expect(installCallIndex).toBeGreaterThanOrEqual(0); - expect(writeConfigFile).toHaveBeenCalledTimes(1); - expect(writeConfigFile).toHaveBeenCalledWith({ - update: { - channel: "beta", + expect(replaceConfigFile).toHaveBeenCalledTimes(1); + expect(replaceConfigFile).toHaveBeenCalledWith({ + nextConfig: { + update: { + channel: "beta", + }, }, + baseHash: undefined, }); expect( vi.mocked(runCommandWithTimeout).mock.invocationCallOrder[installCallIndex] ?? 0, ).toBeLessThan( - vi.mocked(writeConfigFile).mock.invocationCallOrder[0] ?? Number.MAX_SAFE_INTEGER, + vi.mocked(replaceConfigFile).mock.invocationCallOrder[0] ?? Number.MAX_SAFE_INTEGER, ); }); @@ -852,7 +855,7 @@ describe("update-cli", () => { await updateCommand({ channel: "beta", yes: true }); - expect(writeConfigFile).not.toHaveBeenCalled(); + expect(replaceConfigFile).not.toHaveBeenCalled(); expect(defaultRuntime.exit).toHaveBeenCalledWith(1); }); @@ -877,10 +880,10 @@ describe("update-cli", () => { await updateCommand({ channel: "beta", yes: true }); - const lastWrite = vi.mocked(writeConfigFile).mock.calls.at(-1)?.[0] as - | { update?: { channel?: string } } + const lastWrite = vi.mocked(replaceConfigFile).mock.calls.at(-1)?.[0] as + | { nextConfig?: { update?: { channel?: string } } } | undefined; - expect(lastWrite?.update?.channel).toBe("beta"); + expect(lastWrite?.nextConfig?.update?.channel).toBe("beta"); }); it.each([ diff --git a/src/commands/agents.add.test.ts b/src/commands/agents.add.test.ts index 56184eb5849..f1c61288ecd 100644 --- a/src/commands/agents.add.test.ts +++ b/src/commands/agents.add.test.ts @@ -3,6 +3,9 @@ import { baseConfigSnapshot, createTestRuntime } from "./test-runtime-config-hel const readConfigFileSnapshotMock = vi.hoisted(() => vi.fn()); const writeConfigFileMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined)); +const replaceConfigFileMock = vi.hoisted(() => + vi.fn(async (params: { nextConfig: unknown }) => await writeConfigFileMock(params.nextConfig)), +); const wizardMocks = vi.hoisted(() => ({ createClackPrompter: vi.fn(), @@ -12,6 +15,7 @@ vi.mock("../config/config.js", async (importOriginal) => ({ ...(await importOriginal()), readConfigFileSnapshot: readConfigFileSnapshotMock, writeConfigFile: writeConfigFileMock, + replaceConfigFile: replaceConfigFileMock, })); vi.mock("../wizard/clack-prompter.js", () => ({ @@ -27,6 +31,7 @@ describe("agents add command", () => { beforeEach(() => { readConfigFileSnapshotMock.mockClear(); writeConfigFileMock.mockClear(); + replaceConfigFileMock.mockClear(); wizardMocks.createClackPrompter.mockClear(); runtime.log.mockClear(); runtime.error.mockClear(); diff --git a/src/commands/agents.bind.test-support.ts b/src/commands/agents.bind.test-support.ts index be8947d1e55..39d6965da39 100644 --- a/src/commands/agents.bind.test-support.ts +++ b/src/commands/agents.bind.test-support.ts @@ -1,9 +1,25 @@ import { vi } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; import { mergeMockedModule } from "../test-utils/vitest-module-mocks.js"; import { createTestRuntime } from "./test-runtime-config-helpers.js"; +type ReplaceConfigFileResult = Awaited< + ReturnType<(typeof import("../config/config.js"))["replaceConfigFile"]> +>; + export const readConfigFileSnapshotMock = vi.fn(); export const writeConfigFileMock = vi.fn().mockResolvedValue(undefined); +export const replaceConfigFileMock = vi.fn( + async (params: { nextConfig: OpenClawConfig }): Promise => { + await writeConfigFileMock(params.nextConfig); + return { + path: "/tmp/openclaw.json", + previousHash: null, + snapshot: {} as never, + nextConfig: params.nextConfig, + }; + }, +); vi.mock("../config/config.js", async (importOriginal) => { return await mergeMockedModule( @@ -11,6 +27,7 @@ vi.mock("../config/config.js", async (importOriginal) => { () => ({ readConfigFileSnapshot: readConfigFileSnapshotMock, writeConfigFile: writeConfigFileMock, + replaceConfigFile: replaceConfigFileMock, }), ); }); @@ -25,6 +42,7 @@ export async function loadFreshAgentsCommandModuleForTest() { export function resetAgentsBindTestHarness(): void { readConfigFileSnapshotMock.mockClear(); writeConfigFileMock.mockClear(); + replaceConfigFileMock.mockClear(); runtime.log.mockClear(); runtime.error.mockClear(); runtime.exit.mockClear(); diff --git a/src/commands/agents.identity.test.ts b/src/commands/agents.identity.test.ts index 5a02753a32c..2663d631cab 100644 --- a/src/commands/agents.identity.test.ts +++ b/src/commands/agents.identity.test.ts @@ -4,15 +4,22 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { makeTempWorkspace } from "../test-helpers/workspace.js"; import { baseConfigSnapshot, createTestRuntime } from "./test-runtime-config-helpers.js"; -const configMocks = vi.hoisted(() => ({ - readConfigFileSnapshot: vi.fn(), - writeConfigFile: vi.fn().mockResolvedValue(undefined), -})); +const configMocks = vi.hoisted(() => { + const writeConfigFile = vi.fn().mockResolvedValue(undefined); + return { + readConfigFileSnapshot: vi.fn(), + writeConfigFile, + replaceConfigFile: vi.fn(async (params: { nextConfig: unknown }) => { + await writeConfigFile(params.nextConfig); + }), + }; +}); vi.mock("../config/config.js", async (importOriginal) => ({ ...(await importOriginal()), readConfigFileSnapshot: configMocks.readConfigFileSnapshot, writeConfigFile: configMocks.writeConfigFile, + replaceConfigFile: configMocks.replaceConfigFile, })); import { agentsSetIdentityCommand } from "./agents.js"; @@ -52,6 +59,7 @@ describe("agents set-identity command", () => { beforeEach(() => { configMocks.readConfigFileSnapshot.mockClear(); configMocks.writeConfigFile.mockClear(); + configMocks.replaceConfigFile.mockClear(); runtime.log.mockClear(); runtime.error.mockClear(); runtime.exit.mockClear(); diff --git a/src/commands/channels.mock-harness.ts b/src/commands/channels.mock-harness.ts index a069f61c855..368604e4fef 100644 --- a/src/commands/channels.mock-harness.ts +++ b/src/commands/channels.mock-harness.ts @@ -5,12 +5,20 @@ function buildBundledPluginModuleId(pluginId: string, artifactBasename: string): return ["..", "..", "extensions", pluginId, artifactBasename].join("/"); } +const readConfigFileSnapshotMock = vi.fn() as unknown as MockFn; +const writeConfigFileMock = vi.fn().mockResolvedValue(undefined) as unknown as MockFn; +const replaceConfigFileMock = vi.fn(async (params: { nextConfig: unknown }) => { + await writeConfigFileMock(params.nextConfig); +}) as unknown as MockFn; + export const configMocks: { readConfigFileSnapshot: MockFn; writeConfigFile: MockFn; + replaceConfigFile: MockFn; } = { - readConfigFileSnapshot: vi.fn() as unknown as MockFn, - writeConfigFile: vi.fn().mockResolvedValue(undefined) as unknown as MockFn, + readConfigFileSnapshot: readConfigFileSnapshotMock, + writeConfigFile: writeConfigFileMock, + replaceConfigFile: replaceConfigFileMock, }; export const offsetMocks: { @@ -25,6 +33,7 @@ vi.mock("../config/config.js", async (importOriginal) => { ...actual, readConfigFileSnapshot: configMocks.readConfigFileSnapshot, writeConfigFile: configMocks.writeConfigFile, + replaceConfigFile: configMocks.replaceConfigFile, }; }); diff --git a/src/commands/channels.resolve.test.ts b/src/commands/channels.resolve.test.ts index c7f9f0dd84f..014fa2da059 100644 --- a/src/commands/channels.resolve.test.ts +++ b/src/commands/channels.resolve.test.ts @@ -4,8 +4,9 @@ const mocks = vi.hoisted(() => ({ resolveCommandSecretRefsViaGateway: vi.fn(), getChannelsCommandSecretTargetIds: vi.fn(() => []), loadConfig: vi.fn(), + readConfigFileSnapshot: vi.fn(), applyPluginAutoEnable: vi.fn(), - writeConfigFile: vi.fn(), + replaceConfigFile: vi.fn(), resolveMessageChannelSelection: vi.fn(), resolveInstallableChannelPlugin: vi.fn(), getChannelPlugin: vi.fn(), @@ -21,7 +22,8 @@ vi.mock("../cli/command-secret-targets.js", () => ({ vi.mock("../config/config.js", () => ({ loadConfig: mocks.loadConfig, - writeConfigFile: mocks.writeConfigFile, + readConfigFileSnapshot: mocks.readConfigFileSnapshot, + replaceConfigFile: mocks.replaceConfigFile, })); vi.mock("../config/plugin-auto-enable.js", () => ({ @@ -52,8 +54,9 @@ describe("channelsResolveCommand", () => { beforeEach(() => { vi.clearAllMocks(); mocks.loadConfig.mockReturnValue({ channels: {} }); + mocks.readConfigFileSnapshot.mockResolvedValue({ hash: "config-1" }); mocks.applyPluginAutoEnable.mockImplementation(({ config }) => ({ config, changes: [] })); - mocks.writeConfigFile.mockResolvedValue(undefined); + mocks.replaceConfigFile.mockResolvedValue(undefined); mocks.resolveCommandSecretRefsViaGateway.mockResolvedValue({ resolvedConfig: { channels: {} }, diagnostics: [], @@ -106,7 +109,10 @@ describe("channelsResolveCommand", () => { allowInstall: true, }), ); - expect(mocks.writeConfigFile).toHaveBeenCalledWith(installedCfg); + expect(mocks.replaceConfigFile).toHaveBeenCalledWith({ + nextConfig: installedCfg, + baseHash: "config-1", + }); expect(resolveTargets).toHaveBeenCalledWith( expect.objectContaining({ cfg: installedCfg, diff --git a/src/commands/channels/capabilities.test.ts b/src/commands/channels/capabilities.test.ts index 01d8eea6a22..3efc571762e 100644 --- a/src/commands/channels/capabilities.test.ts +++ b/src/commands/channels/capabilities.test.ts @@ -10,7 +10,8 @@ const logs: string[] = []; const errors: string[] = []; const resolveDefaultAccountId = () => DEFAULT_ACCOUNT_ID; const mocks = vi.hoisted(() => ({ - writeConfigFile: vi.fn(), + readConfigFileSnapshot: vi.fn(), + replaceConfigFile: vi.fn(), resolveInstallableChannelPlugin: vi.fn(), })); @@ -30,7 +31,8 @@ vi.mock("../../config/config.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, - writeConfigFile: mocks.writeConfigFile, + readConfigFileSnapshot: mocks.readConfigFileSnapshot, + replaceConfigFile: mocks.replaceConfigFile, }; }); @@ -95,7 +97,8 @@ describe("channelsCapabilitiesCommand", () => { beforeEach(() => { resetOutput(); vi.clearAllMocks(); - mocks.writeConfigFile.mockResolvedValue(undefined); + mocks.readConfigFileSnapshot.mockResolvedValue({ hash: "config-1" }); + mocks.replaceConfigFile.mockResolvedValue(undefined); mocks.resolveInstallableChannelPlugin.mockResolvedValue({ cfg: { channels: {} }, configChanged: false, @@ -211,11 +214,12 @@ describe("channelsCapabilitiesCommand", () => { allowInstall: true, }), ); - expect(mocks.writeConfigFile).toHaveBeenCalledWith( - expect.objectContaining({ + expect(mocks.replaceConfigFile).toHaveBeenCalledWith({ + nextConfig: expect.objectContaining({ plugins: { entries: { whatsapp: { enabled: true } } }, }), - ); + baseHash: "config-1", + }); expect(logs.join("\n")).toContain("Probe: linked"); }); }); diff --git a/src/commands/configure.wizard.test.ts b/src/commands/configure.wizard.test.ts index 6c83aa05a1e..2c33b4d6501 100644 --- a/src/commands/configure.wizard.test.ts +++ b/src/commands/configure.wizard.test.ts @@ -1,26 +1,32 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; -const mocks = vi.hoisted(() => ({ - clackIntro: vi.fn(), - clackOutro: vi.fn(), - clackSelect: vi.fn(), - clackText: vi.fn(), - clackConfirm: vi.fn(), - resolveSearchProviderOptions: vi.fn(), - setupSearch: vi.fn(), - readConfigFileSnapshot: vi.fn(), - writeConfigFile: vi.fn(), - resolveGatewayPort: vi.fn(), - ensureControlUiAssetsBuilt: vi.fn(), - createClackPrompter: vi.fn(), - note: vi.fn(), - printWizardHeader: vi.fn(), - probeGatewayReachable: vi.fn(), - waitForGatewayReachable: vi.fn(), - resolveControlUiLinks: vi.fn(), - summarizeExistingConfig: vi.fn(), -})); +const mocks = vi.hoisted(() => { + const writeConfigFile = vi.fn(); + return { + clackIntro: vi.fn(), + clackOutro: vi.fn(), + clackSelect: vi.fn(), + clackText: vi.fn(), + clackConfirm: vi.fn(), + resolveSearchProviderOptions: vi.fn(), + setupSearch: vi.fn(), + readConfigFileSnapshot: vi.fn(), + writeConfigFile, + replaceConfigFile: vi.fn(async (params: { nextConfig: unknown }) => { + await writeConfigFile(params.nextConfig); + }), + resolveGatewayPort: vi.fn(), + ensureControlUiAssetsBuilt: vi.fn(), + createClackPrompter: vi.fn(), + note: vi.fn(), + printWizardHeader: vi.fn(), + probeGatewayReachable: vi.fn(), + waitForGatewayReachable: vi.fn(), + resolveControlUiLinks: vi.fn(), + summarizeExistingConfig: vi.fn(), + }; +}); vi.mock("@clack/prompts", () => ({ intro: mocks.clackIntro, @@ -34,6 +40,7 @@ vi.mock("../config/config.js", () => ({ CONFIG_PATH: "~/.openclaw/openclaw.json", readConfigFileSnapshot: mocks.readConfigFileSnapshot, writeConfigFile: mocks.writeConfigFile, + replaceConfigFile: mocks.replaceConfigFile, resolveGatewayPort: mocks.resolveGatewayPort, })); diff --git a/src/gateway/startup-auth.test.ts b/src/gateway/startup-auth.test.ts index fce94fea1cf..19eaa0bb81d 100644 --- a/src/gateway/startup-auth.test.ts +++ b/src/gateway/startup-auth.test.ts @@ -3,14 +3,14 @@ import type { OpenClawConfig } from "../config/config.js"; import { expectGeneratedTokenPersistedToGatewayAuth } from "../test-utils/auth-token-assertions.js"; const mocks = vi.hoisted(() => ({ - writeConfigFile: vi.fn(async (_cfg: OpenClawConfig) => {}), + replaceConfigFile: vi.fn(async (_params: { nextConfig: OpenClawConfig }) => {}), })); vi.mock("../config/config.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, - writeConfigFile: mocks.writeConfigFile, + replaceConfigFile: mocks.replaceConfigFile, }; }); @@ -36,12 +36,12 @@ describe("ensureGatewayStartupAuth", () => { expect(result.persistedGeneratedToken).toBe(false); expect(result.auth.mode).toBe("token"); expect(result.auth.token).toBe(result.generatedToken); - expect(mocks.writeConfigFile).not.toHaveBeenCalled(); + expect(mocks.replaceConfigFile).not.toHaveBeenCalled(); } beforeEach(async () => { vi.restoreAllMocks(); - mocks.writeConfigFile.mockClear(); + mocks.replaceConfigFile.mockClear(); await loadFreshStartupAuthModuleForTest(); }); @@ -55,7 +55,7 @@ describe("ensureGatewayStartupAuth", () => { expect(result.generatedToken).toBeUndefined(); expect(result.persistedGeneratedToken).toBe(false); expect(result.auth.mode).toBe(mode); - expect(mocks.writeConfigFile).not.toHaveBeenCalled(); + expect(mocks.replaceConfigFile).not.toHaveBeenCalled(); } async function expectResolvedToken(params: { @@ -77,7 +77,7 @@ describe("ensureGatewayStartupAuth", () => { if ("expectedConfiguredToken" in params) { expect(result.cfg.gateway?.auth?.token).toEqual(params.expectedConfiguredToken); } - expect(mocks.writeConfigFile).not.toHaveBeenCalled(); + expect(mocks.replaceConfigFile).not.toHaveBeenCalled(); } function createMissingGatewayTokenSecretRefConfig(): OpenClawConfig { @@ -106,11 +106,14 @@ describe("ensureGatewayStartupAuth", () => { expect(result.generatedToken).toMatch(/^[0-9a-f]{48}$/); expect(result.persistedGeneratedToken).toBe(true); expect(result.auth.mode).toBe("token"); - expect(mocks.writeConfigFile).toHaveBeenCalledTimes(1); + expect(mocks.replaceConfigFile).toHaveBeenCalledTimes(1); + const persistedParams = mocks.replaceConfigFile.mock.calls[0]?.[0] as + | { nextConfig: OpenClawConfig } + | undefined; expectGeneratedTokenPersistedToGatewayAuth({ generatedToken: result.generatedToken, authToken: result.auth.token, - persistedConfig: mocks.writeConfigFile.mock.calls[0]?.[0], + persistedConfig: persistedParams?.nextConfig, }); }); @@ -236,7 +239,7 @@ describe("ensureGatewayStartupAuth", () => { persist: true, }), ).rejects.toThrow(/MISSING_GW_TOKEN/i); - expect(mocks.writeConfigFile).not.toHaveBeenCalled(); + expect(mocks.replaceConfigFile).not.toHaveBeenCalled(); }); it("requires explicit gateway.auth.mode when token and password are both configured", async () => { @@ -254,7 +257,7 @@ describe("ensureGatewayStartupAuth", () => { persist: true, }), ).rejects.toThrow(/gateway\.auth\.mode is unset/i); - expect(mocks.writeConfigFile).not.toHaveBeenCalled(); + expect(mocks.replaceConfigFile).not.toHaveBeenCalled(); }); it("uses OPENCLAW_GATEWAY_PASSWORD without resolving configured password SecretRef", async () => { @@ -357,7 +360,7 @@ describe("ensureGatewayStartupAuth", () => { expect(result.persistedGeneratedToken).toBe(false); expect(result.auth.mode).toBe("token"); expect(result.auth.token).toBe("from-config"); - expect(mocks.writeConfigFile).not.toHaveBeenCalled(); + expect(mocks.replaceConfigFile).not.toHaveBeenCalled(); }); it("keeps generated token ephemeral when runtime override flips explicit non-token mode", async () => {