mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(config): record why best-effort reads fell back (#125010)
readSourceConfigBestEffortFromContext collapsed three distinct
failures — parse error, $include resolution failure, any other read
error — into {} or a root-only config with zero recorded fact. A
corrupt config (or an update.channel living in an unresolvable
$include) silently flipped the update-channel decision and doctor-lint
input to defaults, indistinguishable from a missing file. The main
snapshot path logs an actionable message for the same conditions.
Root cause: 'best effort' legitimized the fallback value AND the
silence. Each degradation branch now warns once with the config path
and cause; the fallback values are unchanged.
Regression: unparseable config yields {} plus a recorded
'best-effort read ignored unparseable config' warning — fails pre-fix
(silent).
This commit is contained in:
committed by
GitHub
parent
4b2aa935f3
commit
7393a1f64d
@@ -154,6 +154,29 @@ describe("readBestEffortConfig", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("records why an unparseable config was ignored by best-effort reads", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const configPath = `${home}/.openclaw/openclaw.json`;
|
||||
await fs.mkdir(`${home}/.openclaw`, { recursive: true });
|
||||
await fs.writeFile(configPath, "{ definitely not json", "utf-8");
|
||||
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
|
||||
try {
|
||||
const config = await readSourceConfigBestEffort();
|
||||
|
||||
// The fallback value stays {} — but the degradation is recorded.
|
||||
expect(config).toEqual({});
|
||||
expect(
|
||||
warn.mock.calls.some(([line]) =>
|
||||
String(line).includes("best-effort read ignored unparseable config"),
|
||||
),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
warn.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves Windows case-insensitive env lookup in isolated reads", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const mixedCaseKey = "OpenClaw_Config_Path";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import {
|
||||
includeContributionOwnsAgentRoster,
|
||||
includeContributionOwnsBindings,
|
||||
@@ -454,21 +455,31 @@ export async function readSourceConfigBestEffortFromContext(
|
||||
if (!deps.fs.existsSync(configPath)) {
|
||||
return {};
|
||||
}
|
||||
// Best-effort legitimizes the fallback value, not the silence: consumers
|
||||
// (update-channel selection, doctor lint) act on the result, so each
|
||||
// degradation records why the real config was not used.
|
||||
try {
|
||||
const raw = deps.fs.readFileSync(configPath, "utf-8");
|
||||
const parsed = parseConfigJson5(raw, deps.json5);
|
||||
if (!parsed.ok) {
|
||||
deps.logger.warn(
|
||||
`Config (${configPath}): best-effort read ignored unparseable config: ${parsed.error}`,
|
||||
);
|
||||
return {};
|
||||
}
|
||||
let resolved: unknown;
|
||||
try {
|
||||
resolved = resolveConfigIncludesForRead(parsed.parsed, configPath, deps);
|
||||
} catch {
|
||||
} catch (err) {
|
||||
deps.logger.warn(
|
||||
`Config (${configPath}): best-effort read skipped $include resolution: ${formatErrorMessage(err)}`,
|
||||
);
|
||||
return coerceConfig(parsed.parsed);
|
||||
}
|
||||
const resolution = resolveConfigForRead(resolved, deps.env, deps.lowerPrecedenceEnv);
|
||||
return coerceConfig(resolution.resolvedConfigRaw);
|
||||
} catch {
|
||||
} catch (err) {
|
||||
deps.logger.warn(`Config (${configPath}): best-effort read failed: ${formatErrorMessage(err)}`);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user