From f20a2957826e0ac871b58613fb2b2365c7e8360c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 27 Apr 2026 17:46:27 +0100 Subject: [PATCH] test: align release validation expectations --- src/commands/models.list.e2e.test.ts | 2 + ...gateway-codex-harness.live-helpers.test.ts | 17 ++++++ .../gateway-codex-harness.live-helpers.ts | 8 ++- .../server-startup-config.recovery.test.ts | 47 +++++++++++++++ src/gateway/server.cron.test.ts | 32 +--------- src/gateway/server.legacy-migration.test.ts | 58 ------------------- 6 files changed, 76 insertions(+), 88 deletions(-) delete mode 100644 src/gateway/server.legacy-migration.test.ts diff --git a/src/commands/models.list.e2e.test.ts b/src/commands/models.list.e2e.test.ts index aa4d113e502..d77c110f4f1 100644 --- a/src/commands/models.list.e2e.test.ts +++ b/src/commands/models.list.e2e.test.ts @@ -25,6 +25,7 @@ const loadStaticManifestCatalogRowsForList = vi.fn<() => Array Array>>(() => []); const hasProviderStaticCatalogForFilter = vi.fn().mockResolvedValue(false); const shouldSuppressBuiltInModel = vi.fn().mockReturnValue(false); +const shouldSuppressBuiltInModelFromManifest = vi.fn().mockReturnValue(false); const modelRegistryState = { models: [] as Array>, available: [] as Array>, @@ -203,6 +204,7 @@ vi.mock("./models/list.provider-index-catalog.js", () => ({ vi.mock("../agents/model-suppression.js", () => ({ shouldSuppressBuiltInModel, + shouldSuppressBuiltInModelFromManifest, })); function makeRuntime() { diff --git a/src/gateway/gateway-codex-harness.live-helpers.test.ts b/src/gateway/gateway-codex-harness.live-helpers.test.ts index f5c71fcfbd0..5e705647290 100644 --- a/src/gateway/gateway-codex-harness.live-helpers.test.ts +++ b/src/gateway/gateway-codex-harness.live-helpers.test.ts @@ -37,6 +37,23 @@ describe("gateway codex harness live helpers", () => { expect(isExpectedCodexStatusCommandText(text)).toBe(true); }); + it("accepts the current status card emitted by OpenAI Codex", () => { + const text = [ + "Current session status:", + "", + "- Model: `openai/gpt-5.5`", + "- Context: `22k/272k` tokens, `8%`", + "- Cache hit: `52%`", + "- Compactions: `0`", + "- Execution: `direct`", + "- Runtime: `OpenAI Codex`", + "- Think: `low`", + "- Active tasks: `1`", + ].join("\n"); + + expect(isExpectedCodexStatusCommandText(text)).toBe(true); + }); + it("rejects status prose for a different codex session", () => { const text = "OpenClaw is running on `openai/gpt-5.5` with low reasoning/text settings. Context is at `22k/272k` tokens, no compactions, and the current session is `agent:dev:other`."; diff --git a/src/gateway/gateway-codex-harness.live-helpers.ts b/src/gateway/gateway-codex-harness.live-helpers.ts index 1f2fb915522..07c4ce2bfc6 100644 --- a/src/gateway/gateway-codex-harness.live-helpers.ts +++ b/src/gateway/gateway-codex-harness.live-helpers.ts @@ -104,8 +104,14 @@ export function isExpectedCodexStatusCommandText(text: string): boolean { normalized.includes(" openai/") || normalized.includes("`codex/") || normalized.includes(" codex/"); + const isCurrentSessionStatus = + normalized.includes("current session status:") && + normalized.includes("runtime: `openai codex`") && + mentionsModel; - return mentionsOpenClawStatus && mentionsHarnessSession && mentionsModel; + return ( + isCurrentSessionStatus || (mentionsOpenClawStatus && mentionsHarnessSession && mentionsModel) + ); } export function isExpectedCodexModelsCommandText(text: string): boolean { diff --git a/src/gateway/server-startup-config.recovery.test.ts b/src/gateway/server-startup-config.recovery.test.ts index e2f9f0e5933..cdf33526347 100644 --- a/src/gateway/server-startup-config.recovery.test.ts +++ b/src/gateway/server-startup-config.recovery.test.ts @@ -260,6 +260,53 @@ describe("gateway startup config recovery", () => { expect(recoveryNotice.enqueueConfigRecoveryNotice).not.toHaveBeenCalled(); }); + it("rejects legacy config entries in Nix mode before recovery", async () => { + const legacySnapshot = buildTestConfigSnapshot({ + path: configPath, + exists: true, + raw: `${JSON.stringify({ + heartbeat: { model: "anthropic/claude-3-5-haiku-20241022", every: "30m" }, + })}\n`, + parsed: { + heartbeat: { model: "anthropic/claude-3-5-haiku-20241022", every: "30m" }, + }, + valid: false, + config: {} as OpenClawConfig, + issues: [ + { + path: "heartbeat", + message: + "top-level heartbeat is not a valid config path; use agents.defaults.heartbeat (cadence/target/model settings) or channels.defaults.heartbeat (showOk/showAlerts/useIndicator).", + }, + ], + legacyIssues: [ + { + path: "heartbeat", + message: + "top-level heartbeat is not a valid config path; use agents.defaults.heartbeat (cadence/target/model settings) or channels.defaults.heartbeat (showOk/showAlerts/useIndicator).", + }, + ], + }); + vi.mocked(configIo.readConfigFileSnapshotWithPluginMetadata).mockResolvedValueOnce({ + snapshot: legacySnapshot, + pluginMetadataSnapshot, + }); + vi.mocked(configIo, true).isNixMode = true; + + await expect( + loadGatewayStartupConfigSnapshot({ + minimalTestGateway: true, + log: { info: vi.fn(), warn: vi.fn() }, + }), + ).rejects.toThrow( + "Legacy config entries detected while running in Nix mode. Update your Nix config to the latest schema and restart.", + ); + + expect(configIo.recoverConfigFromLastKnownGood).not.toHaveBeenCalled(); + expect(configIo.recoverConfigFromJsonRootSuffix).not.toHaveBeenCalled(); + expect(recoveryNotice.enqueueConfigRecoveryNotice).not.toHaveBeenCalled(); + }); + it("continues startup in degraded mode for plugin-local startup invalidity", async () => { const invalidSnapshot = buildTestConfigSnapshot({ path: configPath, diff --git a/src/gateway/server.cron.test.ts b/src/gateway/server.cron.test.ts index 80b9a9f5199..d0fe19f22f7 100644 --- a/src/gateway/server.cron.test.ts +++ b/src/gateway/server.cron.test.ts @@ -1402,7 +1402,7 @@ describe("gateway server cron", () => { } }, 45_000); - test("ignores non-string cron.webhookToken values without crashing webhook delivery", async () => { + test("rejects malformed cron.webhookToken objects at startup", async () => { const { prevSkipCron } = await setupCronTestRun({ tempPrefix: "openclaw-gw-cron-webhook-secretinput-", cronEnabled: false, @@ -1416,33 +1416,7 @@ describe("gateway server cron", () => { }, }); - fetchWithSsrFGuardMock.mockClear(); - - const { server, ws } = await startServerWithClient(); - await connectOk(ws); - - try { - const notifyJobId = await addWebhookCronJob({ - ws, - name: "webhook secretinput object", - delivery: { mode: "webhook", to: "https://example.invalid/cron-finished" }, - }); - await runCronJobAndWaitForFinished(ws, notifyJobId); - const [notifyArgs] = fetchWithSsrFGuardMock.mock.calls[0] as unknown as [ - { - url?: string; - init?: { - method?: string; - headers?: Record; - }; - }, - ]; - expect(notifyArgs.url).toBe("https://example.invalid/cron-finished"); - expect(notifyArgs.init?.method).toBe("POST"); - expect(notifyArgs.init?.headers?.Authorization).toBeUndefined(); - expect(notifyArgs.init?.headers?.["Content-Type"]).toBe("application/json"); - } finally { - await cleanupCronTestRun({ ws, server, prevSkipCron }); - } + await expect(startServerWithClient()).rejects.toThrow("cron.webhookToken: Invalid input"); + await cleanupCronTestRun({ prevSkipCron }); }, 45_000); }); diff --git a/src/gateway/server.legacy-migration.test.ts b/src/gateway/server.legacy-migration.test.ts deleted file mode 100644 index 23f34b592c0..00000000000 --- a/src/gateway/server.legacy-migration.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { describe, expect, test } from "vitest"; -import { - getFreePort, - installGatewayTestHooks, - startGatewayServer, - testState, -} from "./test-helpers.js"; - -installGatewayTestHooks({ scope: "suite" }); - -async function expectHeartbeatValidationError(legacyParsed: Record) { - testState.legacyIssues = [ - { - path: "heartbeat", - message: - "top-level heartbeat is not a valid config path; use agents.defaults.heartbeat (cadence/target/model settings) or channels.defaults.heartbeat (showOk/showAlerts/useIndicator).", - }, - ]; - testState.legacyParsed = legacyParsed; - testState.migrationConfig = null; - testState.migrationChanges = []; - - let server: Awaited> | undefined; - let thrown: unknown; - try { - server = await startGatewayServer(await getFreePort()); - } catch (err) { - thrown = err; - } - - if (server) { - await server.close(); - } - - expect(thrown).toBeInstanceOf(Error); - const message = (thrown as Error).message; - expect(message).toContain("Invalid config at"); - expect(message).toContain( - "heartbeat: top-level heartbeat is not a valid config path; use agents.defaults.heartbeat (cadence/target/model settings) or channels.defaults.heartbeat (showOk/showAlerts/useIndicator).", - ); - expect(message).not.toContain("Legacy config entries detected but auto-migration failed."); -} - -describe("gateway startup legacy migration fallback", () => { - test("surfaces detailed validation errors when legacy entries have no migration output", async () => { - await expectHeartbeatValidationError({ - heartbeat: { model: "anthropic/claude-3-5-haiku-20241022", every: "30m" }, - }); - }); - - test("keeps detailed validation errors when heartbeat comes from include-resolved config", async () => { - // Simulate a parsed source that only contains include directives, while - // legacy heartbeat is surfaced from the resolved config. - await expectHeartbeatValidationError({ - $include: ["heartbeat.defaults.json"], - }); - }); -});