fix(doctor): preserve upgrade ownership through metadata

Preserve shipped legacy default-agent ownership when Wizard metadata replaces the config object, and make the direct multi-agent upgrade fixture explicitly own heartbeat, system-agent, auth inheritance, fixed session storage, and ambient channels.

This keeps authored current configurations fail-closed while allowing Doctor contributions and updater restarts to consume the migrated legacy owner.

Evidence: 244 focused ownership assertions; dedicated pre-fix regression; JSON and shell fixture proof; targeted OXLint; clean structured autoreview; exact-head CI https://github.com/openclaw/openclaw/actions/runs/31691304326 (unrelated gateway shard passed on exact rerun).

This merge signs the exact reviewed r21 tree on the isolated r20 base. It does not promote, tag, publish, or release.
This commit is contained in:
Peter Steinberger
2026-08-13 03:50:52 -07:00
parent 807f5926f4
commit b3d9bd687e
4 changed files with 125 additions and 24 deletions
+48 -4
View File
@@ -233,11 +233,28 @@ describe("scripts/lib/openclaw-test-state", () => {
source: "env",
},
});
expect(payload.config.channels.discord.enabled).toBe(true);
expect(payload.config.channels.discord.dm).toStrictEqual({
allowFrom: ["111111111111111111"],
policy: "allowlist",
expect(payload.config.agents.ownership).toBe("explicit");
expect(payload.config.agents.defaults).toMatchObject({
authInheritance: { agentId: "main" },
heartbeat: { agentId: "main" },
sessionStore: { agentId: "main" },
systemAgent: { agentId: "main" },
});
expect(payload.config.bindings).toEqual([
{ agentId: "main", match: { channel: "discord", accountId: "*" } },
{ agentId: "main", match: { channel: "telegram", accountId: "*" } },
{ agentId: "main", match: { channel: "whatsapp", accountId: "*" } },
]);
expect(Object.keys(payload.config.agents.entries)).toEqual(["main", "ops"]);
expect(payload.config.agents).not.toHaveProperty("list");
for (const agent of Object.values(payload.config.agents.entries)) {
expect(agent).not.toHaveProperty("default");
}
expect(payload.config.channels.discord.enabled).toBe(true);
expect(payload.config.channels.discord.dmPolicy).toBe("allowlist");
expect(payload.config.channels.discord.allowFrom).toEqual(["111111111111111111"]);
expect(payload.config.channels.discord.dm?.policy).toBeUndefined();
expect(payload.config.channels.discord.dm?.allowFrom).toBeUndefined();
expect(payload.config.channels.telegram.enabled).toBe(true);
expect(payload.config.channels.whatsapp.enabled).toBe(true);
} finally {
@@ -273,6 +290,33 @@ describe("scripts/lib/openclaw-test-state", () => {
expect(payload.secretKey).toMatch(secretKeyPattern);
expect(payload.config).toStrictEqual({});
const upgradeProbe = await execFileAsync("bash", [
"-lc",
`${cleanupTestStateHomeTrap()}; export OPENCLAW_TEST_STATE_TMPDIR=${shellQuote(path.join(tempRoot, "upgrade-function-tmp"))}; source ${shellQuote(snippetFile)}; openclaw_test_state_create "upgrade case" upgrade-survivor; node -e 'const fs=require("node:fs"); process.stdout.write(fs.readFileSync(process.env.OPENCLAW_CONFIG_PATH,"utf8"));'`,
]);
const upgradeConfig = JSON.parse(upgradeProbe.stdout);
expect(upgradeConfig.agents.ownership).toBe("explicit");
expect(upgradeConfig.agents.defaults).toMatchObject({
authInheritance: { agentId: "main" },
heartbeat: { agentId: "main" },
sessionStore: { agentId: "main" },
systemAgent: { agentId: "main" },
});
expect(upgradeConfig.bindings).toEqual([
{ agentId: "main", match: { channel: "discord", accountId: "*" } },
{ agentId: "main", match: { channel: "telegram", accountId: "*" } },
{ agentId: "main", match: { channel: "whatsapp", accountId: "*" } },
]);
expect(Object.keys(upgradeConfig.agents.entries)).toEqual(["main", "ops"]);
expect(upgradeConfig.agents).not.toHaveProperty("list");
for (const agent of Object.values(upgradeConfig.agents.entries)) {
expect(agent).not.toHaveProperty("default");
}
expect(upgradeConfig.channels.discord.dmPolicy).toBe("allowlist");
expect(upgradeConfig.channels.discord.allowFrom).toEqual(["111111111111111111"]);
expect(upgradeConfig.channels.discord.dm?.policy).toBeUndefined();
expect(upgradeConfig.channels.discord.dm?.allowFrom).toBeUndefined();
const trailingTmpDir = path.join(tempRoot, "function-trailing-tmp");
const trailingProbe = await execFileAsync("bash", [
"-lc",