mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:30:43 +00:00
fix(config): preserve source config during recovery
This commit is contained in:
@@ -714,6 +714,10 @@ config.channels = {
|
||||
botToken: "xoxb-bundled-channel-update-token",
|
||||
appToken: "xapp-bundled-channel-update-token",
|
||||
},
|
||||
feishu: {
|
||||
...(config.channels?.feishu || {}),
|
||||
enabled: mode === "feishu",
|
||||
},
|
||||
};
|
||||
|
||||
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
||||
@@ -841,6 +845,7 @@ npm install -g "openclaw@$BASELINE_VERSION" --omit=optional --no-fund --no-audit
|
||||
command -v openclaw >/dev/null
|
||||
baseline_root="$(package_root)"
|
||||
test -d "$baseline_root/dist/extensions/telegram"
|
||||
test -d "$baseline_root/dist/extensions/feishu"
|
||||
|
||||
echo "Replicating configured Telegram missing-runtime state..."
|
||||
write_config telegram
|
||||
@@ -889,6 +894,15 @@ cat /tmp/openclaw-update-slack.json
|
||||
assert_update_ok /tmp/openclaw-update-slack.json "$candidate_version"
|
||||
assert_dep_available slack @slack/web-api
|
||||
|
||||
echo "Mutating config to Feishu and rerunning same-version update path..."
|
||||
write_config feishu
|
||||
remove_runtime_dep feishu @larksuiteoapi/node-sdk
|
||||
assert_no_dep_available feishu @larksuiteoapi/node-sdk
|
||||
run_update_and_capture feishu /tmp/openclaw-update-feishu.json
|
||||
cat /tmp/openclaw-update-feishu.json
|
||||
assert_update_ok /tmp/openclaw-update-feishu.json "$candidate_version"
|
||||
assert_dep_available feishu @larksuiteoapi/node-sdk
|
||||
|
||||
echo "bundled channel runtime deps Docker update E2E passed"
|
||||
EOF
|
||||
then
|
||||
|
||||
@@ -219,6 +219,55 @@ describe("doctor bundled plugin runtime deps", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("repairs Feishu runtime deps from preserved source config", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-bundled-"));
|
||||
writeJson(path.join(root, "package.json"), { name: "openclaw" });
|
||||
writeBundledChannelPlugin(root, "feishu", { "@larksuiteoapi/node-sdk": "^1.61.0" });
|
||||
const installed: Array<{
|
||||
installRoot: string;
|
||||
missingSpecs: string[];
|
||||
installSpecs: string[];
|
||||
}> = [];
|
||||
const prompter = {
|
||||
shouldRepair: false,
|
||||
shouldForce: false,
|
||||
repairMode: {
|
||||
shouldRepair: false,
|
||||
shouldForce: false,
|
||||
nonInteractive: true,
|
||||
canPrompt: false,
|
||||
updateInProgress: true,
|
||||
},
|
||||
confirm: async () => false,
|
||||
confirmAutoFix: async () => false,
|
||||
confirmAggressiveAutoFix: async () => false,
|
||||
confirmRuntimeRepair: async () => false,
|
||||
select: async (_params: unknown, fallback: unknown) => fallback,
|
||||
} as DoctorPrompter;
|
||||
|
||||
await maybeRepairBundledPluginRuntimeDeps({
|
||||
runtime: { error: () => {} } as never,
|
||||
prompter,
|
||||
packageRoot: root,
|
||||
includeConfiguredChannels: true,
|
||||
config: {
|
||||
plugins: { enabled: true },
|
||||
channels: { feishu: { enabled: true } },
|
||||
},
|
||||
installDeps: (params) => {
|
||||
installed.push(params);
|
||||
},
|
||||
});
|
||||
|
||||
expect(installed).toEqual([
|
||||
{
|
||||
installRoot: root,
|
||||
missingSpecs: ["@larksuiteoapi/node-sdk@^1.61.0"],
|
||||
installSpecs: ["@larksuiteoapi/node-sdk@^1.61.0"],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("repairs missing deps into an external stage dir when configured", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-bundled-"));
|
||||
const stageDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-bundled-stage-"));
|
||||
|
||||
@@ -1257,9 +1257,16 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
let fallbackRaw: string | null = null;
|
||||
let fallbackParsed: unknown = {};
|
||||
let fallbackSourceConfig: OpenClawConfig = {};
|
||||
let fallbackHash = hashConfigRaw(null);
|
||||
|
||||
try {
|
||||
const raw = deps.fs.readFileSync(configPath, "utf-8");
|
||||
const rawHash = hashConfigRaw(raw);
|
||||
fallbackRaw = raw;
|
||||
fallbackHash = rawHash;
|
||||
const parsedRes = parseConfigJson5(raw, deps.json5);
|
||||
if (!parsedRes.ok) {
|
||||
return await finalizeReadConfigSnapshotInternalResult(deps, {
|
||||
@@ -1278,6 +1285,8 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
}),
|
||||
});
|
||||
}
|
||||
fallbackParsed = parsedRes.parsed;
|
||||
fallbackSourceConfig = coerceConfig(parsedRes.parsed);
|
||||
|
||||
// Resolve $include directives
|
||||
const recovered = await maybeRecoverSuspiciousConfigRead({
|
||||
@@ -1289,6 +1298,10 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
const effectiveRaw = recovered.raw;
|
||||
const effectiveParsed = recovered.parsed;
|
||||
const hash = hashConfigRaw(effectiveRaw);
|
||||
fallbackRaw = effectiveRaw;
|
||||
fallbackParsed = effectiveParsed;
|
||||
fallbackSourceConfig = coerceConfig(effectiveParsed);
|
||||
fallbackHash = hash;
|
||||
|
||||
let resolved: unknown;
|
||||
try {
|
||||
@@ -1329,6 +1342,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
const resolvedConfigRaw = readResolution.resolvedConfigRaw;
|
||||
const legacyResolution = resolveLegacyConfigForRead(resolvedConfigRaw, effectiveParsed);
|
||||
const effectiveConfigRaw = legacyResolution.effectiveConfigRaw;
|
||||
fallbackSourceConfig = coerceConfig(effectiveConfigRaw);
|
||||
const validated = validateConfigObjectWithPlugins(effectiveConfigRaw, { env: deps.env });
|
||||
if (!validated.ok) {
|
||||
return await finalizeReadConfigSnapshotInternalResult(deps, {
|
||||
@@ -1392,12 +1406,12 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
snapshot: createConfigFileSnapshot({
|
||||
path: configPath,
|
||||
exists: true,
|
||||
raw: null,
|
||||
parsed: {},
|
||||
sourceConfig: {},
|
||||
raw: fallbackRaw,
|
||||
parsed: fallbackParsed,
|
||||
sourceConfig: fallbackSourceConfig,
|
||||
valid: false,
|
||||
runtimeConfig: {},
|
||||
hash: hashConfigRaw(null),
|
||||
runtimeConfig: fallbackSourceConfig,
|
||||
hash: fallbackHash,
|
||||
issues: [{ path: "", message }],
|
||||
warnings: [],
|
||||
legacyIssues: [],
|
||||
|
||||
@@ -286,6 +286,37 @@ describe("config io write", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves parsed source config when snapshot validation throws", async () => {
|
||||
await withSuiteHome(async (home) => {
|
||||
const configPath = path.join(home, ".openclaw", "openclaw.json");
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
const original = {
|
||||
gateway: { mode: "local" },
|
||||
channels: { feishu: { enabled: true } },
|
||||
};
|
||||
const originalRaw = `${JSON.stringify(original, null, 2)}\n`;
|
||||
await fs.writeFile(configPath, originalRaw, "utf-8");
|
||||
mockLoadPluginManifestRegistry.mockImplementationOnce(() => {
|
||||
throw new Error("manifest registry unavailable");
|
||||
});
|
||||
|
||||
const io = createConfigIO({
|
||||
env: { VITEST: "true" } as NodeJS.ProcessEnv,
|
||||
homedir: () => home,
|
||||
logger: silentLogger,
|
||||
});
|
||||
|
||||
const snapshot = await io.readConfigFileSnapshot();
|
||||
|
||||
expect(snapshot.valid).toBe(false);
|
||||
expect(snapshot.raw).toBe(originalRaw);
|
||||
expect(snapshot.parsed).toEqual(original);
|
||||
expect(snapshot.sourceConfig).toEqual(original);
|
||||
expect(snapshot.config).toEqual(original);
|
||||
expect(snapshot.issues[0]?.message).toContain("manifest registry unavailable");
|
||||
});
|
||||
});
|
||||
|
||||
it("does not inject include-only $schema into the root config during partial writes", async () => {
|
||||
await withSuiteHome(async (home) => {
|
||||
const configPath = path.join(home, ".openclaw", "openclaw.json");
|
||||
|
||||
Reference in New Issue
Block a user