import fs from "node:fs/promises"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { clearConfigCache, clearRuntimeConfigSnapshot, loadConfig, writeConfigFile, } from "../config/config.js"; import { withTempHome } from "../config/home-env.test-harness.js"; import { captureEnv, withEnvAsync } from "../test-utils/env.js"; import { asConfig, EMPTY_LOADABLE_PLUGIN_ORIGINS, loadAuthStoreWithProfiles, SECRETS_RUNTIME_INTEGRATION_TIMEOUT_MS, } from "./runtime.integration.test-helpers.js"; import { activateSecretsRuntimeSnapshot, clearSecretsRuntimeSnapshot, getActiveSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot, } from "./runtime.js"; vi.unmock("../version.js"); describe("secrets runtime snapshot gateway-auth integration", () => { let envSnapshot: ReturnType; beforeEach(() => { envSnapshot = captureEnv([ "OPENCLAW_BUNDLED_PLUGINS_DIR", "OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE", "OPENCLAW_VERSION", ]); delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; process.env.OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE = "1"; delete process.env.OPENCLAW_VERSION; }); afterEach(() => { vi.restoreAllMocks(); envSnapshot.restore(); clearSecretsRuntimeSnapshot(); clearRuntimeConfigSnapshot(); clearConfigCache(); }); it("fails fast at startup when gateway auth SecretRef is active and unresolved", async () => { await withEnvAsync( { OPENCLAW_BUNDLED_PLUGINS_DIR: undefined, OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: "1", OPENCLAW_VERSION: undefined, }, async () => { await expect( prepareSecretsRuntimeSnapshot({ config: asConfig({ gateway: { auth: { mode: "token", token: { source: "env", provider: "default", id: "MISSING_GATEWAY_AUTH_TOKEN", }, }, }, }), env: {}, agentDirs: ["/tmp/openclaw-agent-main"], loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS, loadAuthStore: () => ({ version: 1, profiles: {} }), }), ).rejects.toThrow(/MISSING_GATEWAY_AUTH_TOKEN/i); }, ); }); it( "keeps last-known-good runtime snapshot active when reload introduces unresolved active gateway auth refs", async () => { await withTempHome("openclaw-secrets-runtime-gateway-auth-reload-lkg-", async (home) => { const initialTokenRef = { source: "env", provider: "default", id: "GATEWAY_AUTH_TOKEN", } as const; const missingTokenRef = { source: "env", provider: "default", id: "MISSING_GATEWAY_AUTH_TOKEN", } as const; const prepared = await prepareSecretsRuntimeSnapshot({ config: asConfig({ gateway: { auth: { mode: "token", token: initialTokenRef, }, }, }), env: { GATEWAY_AUTH_TOKEN: "gateway-runtime-token", }, agentDirs: ["/tmp/openclaw-agent-main"], loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS, loadAuthStore: () => loadAuthStoreWithProfiles({}), }); activateSecretsRuntimeSnapshot(prepared); expect(loadConfig().gateway?.auth?.token).toBe("gateway-runtime-token"); await expect( writeConfigFile({ ...loadConfig(), gateway: { auth: { mode: "token", token: missingTokenRef, }, }, }), ).rejects.toThrow(/runtime snapshot refresh failed: .*MISSING_GATEWAY_AUTH_TOKEN/i); const activeAfterFailure = getActiveSecretsRuntimeSnapshot(); expect(activeAfterFailure).not.toBeNull(); expect(loadConfig().gateway?.auth?.token).toBe("gateway-runtime-token"); expect(activeAfterFailure?.sourceConfig.gateway?.auth?.token).toEqual(initialTokenRef); const persistedConfig = JSON.parse( await fs.readFile(path.join(home, ".openclaw", "openclaw.json"), "utf8"), ) as OpenClawConfig; expect(persistedConfig.gateway?.auth?.token).toEqual(missingTokenRef); }); }, SECRETS_RUNTIME_INTEGRATION_TIMEOUT_MS, ); });