mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-03 19:04:05 +00:00
refactor: share startup recovery test helpers
This commit is contained in:
@@ -134,6 +134,77 @@ function buildDefaultSnapshot(): ConfigFileSnapshot {
|
||||
});
|
||||
}
|
||||
|
||||
function buildRuntimeSnapshot(
|
||||
sourceConfig: OpenClawConfig,
|
||||
runtimeConfig: OpenClawConfig = sourceConfig,
|
||||
): ConfigFileSnapshot {
|
||||
return {
|
||||
...buildTestConfigSnapshot({
|
||||
path: configPath,
|
||||
exists: true,
|
||||
raw: `${JSON.stringify(sourceConfig)}\n`,
|
||||
parsed: sourceConfig,
|
||||
valid: true,
|
||||
config: runtimeConfig,
|
||||
issues: [],
|
||||
legacyIssues: [],
|
||||
}),
|
||||
sourceConfig,
|
||||
resolved: sourceConfig,
|
||||
runtimeConfig,
|
||||
config: runtimeConfig,
|
||||
} satisfies ConfigFileSnapshot;
|
||||
}
|
||||
|
||||
function mockStartupSnapshot(snapshot: ConfigFileSnapshot) {
|
||||
vi.mocked(configIo.readConfigFileSnapshotWithPluginMetadata).mockResolvedValueOnce({
|
||||
snapshot,
|
||||
pluginMetadataSnapshot,
|
||||
});
|
||||
}
|
||||
|
||||
function buildInvalidConfigSnapshot(params: {
|
||||
rawConfig: unknown;
|
||||
config?: OpenClawConfig;
|
||||
issues: ConfigFileSnapshot["issues"];
|
||||
warnings?: ConfigFileSnapshot["warnings"];
|
||||
legacyIssues?: ConfigFileSnapshot["legacyIssues"];
|
||||
}) {
|
||||
return buildTestConfigSnapshot({
|
||||
path: configPath,
|
||||
exists: true,
|
||||
raw: `${JSON.stringify(params.rawConfig)}\n`,
|
||||
parsed: params.rawConfig,
|
||||
valid: false,
|
||||
config: params.config ?? (params.rawConfig as OpenClawConfig),
|
||||
issues: params.issues,
|
||||
warnings: params.warnings,
|
||||
legacyIssues: params.legacyIssues ?? [],
|
||||
});
|
||||
}
|
||||
|
||||
function testStartupLog() {
|
||||
return { info: vi.fn(), warn: vi.fn() };
|
||||
}
|
||||
|
||||
function loadTestStartup(params: {
|
||||
minimalTestGateway?: boolean;
|
||||
log?: ReturnType<typeof testStartupLog>;
|
||||
initialSnapshotRead?: Parameters<
|
||||
typeof loadGatewayStartupConfigSnapshot
|
||||
>[0]["initialSnapshotRead"];
|
||||
}) {
|
||||
return loadGatewayStartupConfigSnapshot({
|
||||
minimalTestGateway: params.minimalTestGateway ?? true,
|
||||
log: params.log ?? testStartupLog(),
|
||||
initialSnapshotRead: params.initialSnapshotRead,
|
||||
});
|
||||
}
|
||||
|
||||
async function expectStartupRejects(message: string | RegExp, minimalTestGateway = true) {
|
||||
await expect(loadTestStartup({ minimalTestGateway })).rejects.toThrow(message);
|
||||
}
|
||||
|
||||
function installConfigIoMockDefaults() {
|
||||
const readSnapshot = vi.mocked(configIo.readConfigFileSnapshot);
|
||||
const readSnapshotWithPluginMetadata = vi.mocked(
|
||||
@@ -203,30 +274,12 @@ describe("gateway startup config validation", () => {
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const snapshot = {
|
||||
...buildTestConfigSnapshot({
|
||||
path: configPath,
|
||||
exists: true,
|
||||
raw: `${JSON.stringify(sourceConfig)}\n`,
|
||||
parsed: sourceConfig,
|
||||
valid: true,
|
||||
config: runtimeConfig,
|
||||
issues: [],
|
||||
legacyIssues: [],
|
||||
}),
|
||||
sourceConfig,
|
||||
resolved: sourceConfig,
|
||||
runtimeConfig,
|
||||
config: runtimeConfig,
|
||||
} satisfies ConfigFileSnapshot;
|
||||
vi.mocked(configIo.readConfigFileSnapshotWithPluginMetadata).mockResolvedValueOnce({
|
||||
snapshot,
|
||||
pluginMetadataSnapshot,
|
||||
});
|
||||
const log = { info: vi.fn(), warn: vi.fn() };
|
||||
const snapshot = buildRuntimeSnapshot(sourceConfig, runtimeConfig);
|
||||
mockStartupSnapshot(snapshot);
|
||||
const log = testStartupLog();
|
||||
|
||||
await expect(
|
||||
loadGatewayStartupConfigSnapshot({
|
||||
loadTestStartup({
|
||||
minimalTestGateway: false,
|
||||
log,
|
||||
}),
|
||||
@@ -257,10 +310,10 @@ describe("gateway startup config validation", () => {
|
||||
issues: [],
|
||||
legacyIssues: [],
|
||||
});
|
||||
const log = { info: vi.fn(), warn: vi.fn() };
|
||||
const log = testStartupLog();
|
||||
|
||||
await expect(
|
||||
loadGatewayStartupConfigSnapshot({
|
||||
loadTestStartup({
|
||||
minimalTestGateway: false,
|
||||
log,
|
||||
initialSnapshotRead: {
|
||||
@@ -312,35 +365,17 @@ describe("gateway startup config validation", () => {
|
||||
telegram: { enabled: true },
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
const initialSnapshot = {
|
||||
...buildTestConfigSnapshot({
|
||||
path: configPath,
|
||||
exists: true,
|
||||
raw: `${JSON.stringify(sourceConfig)}\n`,
|
||||
parsed: sourceConfig,
|
||||
valid: true,
|
||||
config: sourceConfig,
|
||||
issues: [],
|
||||
legacyIssues: [],
|
||||
}),
|
||||
sourceConfig,
|
||||
resolved: sourceConfig,
|
||||
runtimeConfig: sourceConfig,
|
||||
config: sourceConfig,
|
||||
} satisfies ConfigFileSnapshot;
|
||||
vi.mocked(configIo.readConfigFileSnapshotWithPluginMetadata).mockResolvedValueOnce({
|
||||
snapshot: initialSnapshot,
|
||||
pluginMetadataSnapshot,
|
||||
});
|
||||
const initialSnapshot = buildRuntimeSnapshot(sourceConfig);
|
||||
mockStartupSnapshot(initialSnapshot);
|
||||
applyPluginAutoEnable.mockReturnValueOnce({
|
||||
config: autoEnabledConfig,
|
||||
changes: ["Telegram configured, enabled automatically."],
|
||||
autoEnabledReasons: {},
|
||||
});
|
||||
const log = { info: vi.fn(), warn: vi.fn() };
|
||||
const log = testStartupLog();
|
||||
|
||||
await expect(
|
||||
loadGatewayStartupConfigSnapshot({
|
||||
loadTestStartup({
|
||||
minimalTestGateway: false,
|
||||
log,
|
||||
}),
|
||||
@@ -394,36 +429,18 @@ describe("gateway startup config validation", () => {
|
||||
allow: ["telegram"],
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
const snapshot = {
|
||||
...buildTestConfigSnapshot({
|
||||
path: configPath,
|
||||
exists: true,
|
||||
raw: `${JSON.stringify(sourceConfig)}\n`,
|
||||
parsed: sourceConfig,
|
||||
valid: true,
|
||||
config: sourceConfig,
|
||||
issues: [],
|
||||
legacyIssues: [],
|
||||
}),
|
||||
sourceConfig,
|
||||
resolved: sourceConfig,
|
||||
runtimeConfig: sourceConfig,
|
||||
config: sourceConfig,
|
||||
} satisfies ConfigFileSnapshot;
|
||||
vi.mocked(configIo.readConfigFileSnapshotWithPluginMetadata).mockResolvedValueOnce({
|
||||
snapshot,
|
||||
pluginMetadataSnapshot,
|
||||
});
|
||||
const snapshot = buildRuntimeSnapshot(sourceConfig);
|
||||
mockStartupSnapshot(snapshot);
|
||||
applyPluginAutoEnable.mockReturnValueOnce({
|
||||
config: autoEnabledConfig,
|
||||
changes: ["Telegram configured, enabled automatically."],
|
||||
autoEnabledReasons: {},
|
||||
});
|
||||
configMocks.isNixMode.value = true;
|
||||
const log = { info: vi.fn(), warn: vi.fn() };
|
||||
const log = testStartupLog();
|
||||
|
||||
await expect(
|
||||
loadGatewayStartupConfigSnapshot({
|
||||
loadTestStartup({
|
||||
minimalTestGateway: false,
|
||||
log,
|
||||
}),
|
||||
@@ -449,33 +466,19 @@ describe("gateway startup config validation", () => {
|
||||
const invalidSnapshot = buildSnapshot({ valid: false, raw: "{ invalid json" });
|
||||
vi.mocked(configIo.readConfigFileSnapshot).mockResolvedValueOnce(invalidSnapshot);
|
||||
|
||||
await expect(
|
||||
loadGatewayStartupConfigSnapshot({
|
||||
minimalTestGateway: true,
|
||||
log: { info: vi.fn(), warn: vi.fn() },
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
await expectStartupRejects(
|
||||
`Invalid config at ${configPath}.\ngateway.mode: Expected 'local' or 'remote'\nRun "openclaw doctor --fix" to repair, then retry.\nIf startup is still blocked, inspect the adjacent .bak backup before restoring it manually.`,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not suggest doctor repair for plugin packaging compiled-output failures", async () => {
|
||||
const invalidSnapshot = buildTestConfigSnapshot({
|
||||
path: configPath,
|
||||
exists: true,
|
||||
raw: `${JSON.stringify({
|
||||
gateway: { mode: "local" },
|
||||
plugins: { slots: { memory: "source-only-pack" } },
|
||||
})}\n`,
|
||||
parsed: {
|
||||
gateway: { mode: "local" },
|
||||
plugins: { slots: { memory: "source-only-pack" } },
|
||||
},
|
||||
valid: false,
|
||||
config: {
|
||||
gateway: { mode: "local" },
|
||||
plugins: { slots: { memory: "source-only-pack" } },
|
||||
} as OpenClawConfig,
|
||||
const rawConfig = {
|
||||
gateway: { mode: "local" },
|
||||
plugins: { slots: { memory: "source-only-pack" } },
|
||||
};
|
||||
const invalidSnapshot = buildInvalidConfigSnapshot({
|
||||
rawConfig,
|
||||
config: rawConfig as OpenClawConfig,
|
||||
issues: [
|
||||
{
|
||||
path: "plugins.slots.memory",
|
||||
@@ -489,14 +492,10 @@ describe("gateway startup config validation", () => {
|
||||
"plugin source-only-pack: installed plugin package requires compiled runtime output for TypeScript entry index.ts: expected ./dist/index.js. This is a plugin packaging issue, not a local config problem.",
|
||||
},
|
||||
],
|
||||
legacyIssues: [],
|
||||
});
|
||||
vi.mocked(configIo.readConfigFileSnapshot).mockResolvedValueOnce(invalidSnapshot);
|
||||
|
||||
const start = loadGatewayStartupConfigSnapshot({
|
||||
minimalTestGateway: true,
|
||||
log: { info: vi.fn(), warn: vi.fn() },
|
||||
});
|
||||
const start = loadTestStartup({});
|
||||
await expect(start).rejects.toThrow(
|
||||
`Invalid config at ${configPath}.\nplugins.slots.memory: plugin not found: source-only-pack\nThis is a plugin packaging issue, not a local config problem.\nUpdate or reinstall the plugin after the publisher ships compiled JavaScript, or disable/uninstall the plugin until then.`,
|
||||
);
|
||||
@@ -506,22 +505,13 @@ describe("gateway startup config validation", () => {
|
||||
});
|
||||
|
||||
it("keeps doctor repair guidance for mixed plugin packaging and core invalidity", async () => {
|
||||
const invalidSnapshot = buildTestConfigSnapshot({
|
||||
path: configPath,
|
||||
exists: true,
|
||||
raw: `${JSON.stringify({
|
||||
gateway: { mode: "invalid" },
|
||||
plugins: { slots: { memory: "source-only-pack" } },
|
||||
})}\n`,
|
||||
parsed: {
|
||||
gateway: { mode: "invalid" },
|
||||
plugins: { slots: { memory: "source-only-pack" } },
|
||||
},
|
||||
valid: false,
|
||||
config: {
|
||||
gateway: { mode: "invalid" },
|
||||
plugins: { slots: { memory: "source-only-pack" } },
|
||||
} as unknown as OpenClawConfig,
|
||||
const rawConfig = {
|
||||
gateway: { mode: "invalid" },
|
||||
plugins: { slots: { memory: "source-only-pack" } },
|
||||
};
|
||||
const invalidSnapshot = buildInvalidConfigSnapshot({
|
||||
rawConfig,
|
||||
config: rawConfig as unknown as OpenClawConfig,
|
||||
issues: [
|
||||
{
|
||||
path: "plugins.slots.memory",
|
||||
@@ -539,29 +529,17 @@ describe("gateway startup config validation", () => {
|
||||
"plugin source-only-pack: installed plugin package requires compiled runtime output for TypeScript entry index.ts: expected ./dist/index.js.",
|
||||
},
|
||||
],
|
||||
legacyIssues: [],
|
||||
});
|
||||
vi.mocked(configIo.readConfigFileSnapshot).mockResolvedValueOnce(invalidSnapshot);
|
||||
|
||||
await expect(
|
||||
loadGatewayStartupConfigSnapshot({
|
||||
minimalTestGateway: true,
|
||||
log: { info: vi.fn(), warn: vi.fn() },
|
||||
}),
|
||||
).rejects.toThrow('Run "openclaw doctor --fix" to repair, then retry.');
|
||||
await expectStartupRejects('Run "openclaw doctor --fix" to repair, then retry.');
|
||||
});
|
||||
|
||||
it("rejects legacy config entries in Nix mode", async () => {
|
||||
const legacySnapshot = buildTestConfigSnapshot({
|
||||
path: configPath,
|
||||
exists: true,
|
||||
raw: `${JSON.stringify({
|
||||
heartbeat: { model: "anthropic/claude-3-5-haiku-20241022", every: "30m" },
|
||||
})}\n`,
|
||||
parsed: {
|
||||
const legacySnapshot = buildInvalidConfigSnapshot({
|
||||
rawConfig: {
|
||||
heartbeat: { model: "anthropic/claude-3-5-haiku-20241022", every: "30m" },
|
||||
},
|
||||
valid: false,
|
||||
config: {} as OpenClawConfig,
|
||||
issues: [
|
||||
{
|
||||
@@ -578,51 +556,26 @@ describe("gateway startup config validation", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
vi.mocked(configIo.readConfigFileSnapshotWithPluginMetadata).mockResolvedValueOnce({
|
||||
snapshot: legacySnapshot,
|
||||
pluginMetadataSnapshot,
|
||||
});
|
||||
mockStartupSnapshot(legacySnapshot);
|
||||
configMocks.isNixMode.value = true;
|
||||
|
||||
await expect(
|
||||
loadGatewayStartupConfigSnapshot({
|
||||
minimalTestGateway: true,
|
||||
log: { info: vi.fn(), warn: vi.fn() },
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
await expectStartupRejects(
|
||||
"Legacy config entries detected while running in Nix mode. Update your Nix config to the latest schema and restart.",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects plugin-local startup invalidity without degraded startup", async () => {
|
||||
const invalidSnapshot = buildTestConfigSnapshot({
|
||||
path: configPath,
|
||||
exists: true,
|
||||
raw: `${JSON.stringify({
|
||||
gateway: { mode: "local" },
|
||||
plugins: {
|
||||
entries: {
|
||||
feishu: { enabled: true },
|
||||
},
|
||||
},
|
||||
})}\n`,
|
||||
parsed: {
|
||||
gateway: { mode: "local" },
|
||||
plugins: {
|
||||
entries: {
|
||||
feishu: { enabled: true },
|
||||
},
|
||||
const rawConfig = {
|
||||
gateway: { mode: "local" },
|
||||
plugins: {
|
||||
entries: {
|
||||
feishu: { enabled: true },
|
||||
},
|
||||
},
|
||||
valid: false,
|
||||
config: {
|
||||
gateway: { mode: "local" },
|
||||
plugins: {
|
||||
entries: {
|
||||
feishu: { enabled: true },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
};
|
||||
const invalidSnapshot = buildInvalidConfigSnapshot({
|
||||
rawConfig,
|
||||
config: rawConfig as OpenClawConfig,
|
||||
issues: [
|
||||
{
|
||||
path: "plugins.entries.feishu",
|
||||
@@ -630,46 +583,23 @@ describe("gateway startup config validation", () => {
|
||||
"plugin feishu: plugin requires OpenClaw >=2026.4.23, but this host is 2026.4.22; skipping load",
|
||||
},
|
||||
],
|
||||
legacyIssues: [],
|
||||
});
|
||||
vi.mocked(configIo.readConfigFileSnapshot).mockResolvedValueOnce(invalidSnapshot);
|
||||
await expect(
|
||||
loadGatewayStartupConfigSnapshot({
|
||||
minimalTestGateway: true,
|
||||
log: { info: vi.fn(), warn: vi.fn() },
|
||||
}),
|
||||
).rejects.toThrow(`Invalid config at ${configPath}.`);
|
||||
await expectStartupRejects(`Invalid config at ${configPath}.`);
|
||||
});
|
||||
|
||||
it("keeps mixed plugin and core startup invalidity fatal", async () => {
|
||||
const invalidSnapshot = buildTestConfigSnapshot({
|
||||
path: configPath,
|
||||
exists: true,
|
||||
raw: `${JSON.stringify({
|
||||
gateway: { mode: "invalid" },
|
||||
plugins: {
|
||||
entries: {
|
||||
feishu: { enabled: true },
|
||||
},
|
||||
},
|
||||
})}\n`,
|
||||
parsed: {
|
||||
gateway: { mode: "invalid" },
|
||||
plugins: {
|
||||
entries: {
|
||||
feishu: { enabled: true },
|
||||
},
|
||||
const rawConfig = {
|
||||
gateway: { mode: "invalid" },
|
||||
plugins: {
|
||||
entries: {
|
||||
feishu: { enabled: true },
|
||||
},
|
||||
},
|
||||
valid: false,
|
||||
config: {
|
||||
gateway: { mode: "invalid" },
|
||||
plugins: {
|
||||
entries: {
|
||||
feishu: { enabled: true },
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig,
|
||||
};
|
||||
const invalidSnapshot = buildInvalidConfigSnapshot({
|
||||
rawConfig,
|
||||
config: rawConfig as unknown as OpenClawConfig,
|
||||
issues: [
|
||||
{
|
||||
path: "gateway.mode",
|
||||
@@ -680,16 +610,10 @@ describe("gateway startup config validation", () => {
|
||||
message: "invalid config: must be string",
|
||||
},
|
||||
],
|
||||
legacyIssues: [],
|
||||
});
|
||||
vi.mocked(configIo.readConfigFileSnapshot).mockResolvedValueOnce(invalidSnapshot);
|
||||
|
||||
await expect(
|
||||
loadGatewayStartupConfigSnapshot({
|
||||
minimalTestGateway: true,
|
||||
log: { info: vi.fn(), warn: vi.fn() },
|
||||
}),
|
||||
).rejects.toThrow(`Invalid config at ${configPath}.`);
|
||||
await expectStartupRejects(`Invalid config at ${configPath}.`);
|
||||
});
|
||||
|
||||
it("rejects stale model provider api enum values during startup", async () => {
|
||||
@@ -721,12 +645,8 @@ describe("gateway startup config validation", () => {
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
const invalidSnapshot = buildTestConfigSnapshot({
|
||||
path: configPath,
|
||||
exists: true,
|
||||
raw: `${JSON.stringify(config)}\n`,
|
||||
parsed: config,
|
||||
valid: false,
|
||||
const invalidSnapshot = buildInvalidConfigSnapshot({
|
||||
rawConfig: config,
|
||||
config,
|
||||
issues: [
|
||||
{
|
||||
@@ -740,15 +660,9 @@ describe("gateway startup config validation", () => {
|
||||
'Invalid option: expected one of "openai-completions"|"openai-responses"|"openai-chatgpt-responses"|"anthropic-messages"|"google-generative-ai"|"github-copilot"|"bedrock-converse-stream"|"ollama"|"azure-openai-responses"',
|
||||
},
|
||||
],
|
||||
legacyIssues: [],
|
||||
});
|
||||
vi.mocked(configIo.readConfigFileSnapshot).mockResolvedValueOnce(invalidSnapshot);
|
||||
await expect(
|
||||
loadGatewayStartupConfigSnapshot({
|
||||
minimalTestGateway: false,
|
||||
log: { info: vi.fn(), warn: vi.fn() },
|
||||
}),
|
||||
).rejects.toThrow(`Invalid config at ${configPath}.`);
|
||||
await expectStartupRejects(`Invalid config at ${configPath}.`, false);
|
||||
|
||||
expect(configMutate.replaceConfigFile).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -760,11 +674,6 @@ describe("gateway startup config validation", () => {
|
||||
});
|
||||
vi.mocked(configIo.readConfigFileSnapshot).mockResolvedValueOnce(invalidSnapshot);
|
||||
|
||||
await expect(
|
||||
loadGatewayStartupConfigSnapshot({
|
||||
minimalTestGateway: true,
|
||||
log: { info: vi.fn(), warn: vi.fn() },
|
||||
}),
|
||||
).rejects.toThrow(`Invalid config at ${configPath}.`);
|
||||
await expectStartupRejects(`Invalid config at ${configPath}.`);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user