test: align release validation expectations

This commit is contained in:
Peter Steinberger
2026-04-27 17:46:27 +01:00
parent efc3a52947
commit f20a295782
6 changed files with 76 additions and 88 deletions

View File

@@ -25,6 +25,7 @@ const loadStaticManifestCatalogRowsForList = vi.fn<() => Array<Record<string, un
const loadProviderIndexCatalogRowsForList = vi.fn<() => Array<Record<string, unknown>>>(() => []);
const hasProviderStaticCatalogForFilter = vi.fn().mockResolvedValue(false);
const shouldSuppressBuiltInModel = vi.fn().mockReturnValue(false);
const shouldSuppressBuiltInModelFromManifest = vi.fn().mockReturnValue(false);
const modelRegistryState = {
models: [] as Array<Record<string, unknown>>,
available: [] as Array<Record<string, unknown>>,
@@ -203,6 +204,7 @@ vi.mock("./models/list.provider-index-catalog.js", () => ({
vi.mock("../agents/model-suppression.js", () => ({
shouldSuppressBuiltInModel,
shouldSuppressBuiltInModelFromManifest,
}));
function makeRuntime() {

View File

@@ -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`.";

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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<string, string>;
};
},
];
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);
});

View File

@@ -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<string, unknown>) {
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<ReturnType<typeof startGatewayServer>> | 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"],
});
});
});