mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
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:
@@ -127,17 +127,20 @@ function scenarioConfig(scenario: string, options: TestStateOptions = {}) {
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
ownership: "explicit",
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "openai/gpt-5.6-luna",
|
||||
},
|
||||
contextTokens: 64000,
|
||||
skills: ["memory"],
|
||||
authInheritance: { agentId: "main" },
|
||||
heartbeat: { agentId: "main" },
|
||||
sessionStore: { agentId: "main" },
|
||||
systemAgent: { agentId: "main" },
|
||||
},
|
||||
entries: {
|
||||
main: {
|
||||
default: true,
|
||||
name: "Main",
|
||||
workspace: "~/workspace",
|
||||
model: {
|
||||
@@ -157,6 +160,11 @@ function scenarioConfig(scenario: string, options: TestStateOptions = {}) {
|
||||
},
|
||||
},
|
||||
},
|
||||
bindings: [
|
||||
{ agentId: "main", match: { channel: "discord", accountId: "*" } },
|
||||
{ agentId: "main", match: { channel: "telegram", accountId: "*" } },
|
||||
{ agentId: "main", match: { channel: "whatsapp", accountId: "*" } },
|
||||
],
|
||||
skills: {
|
||||
allowBundled: ["memory", "openclaw-testing"],
|
||||
limits: {
|
||||
@@ -177,10 +185,8 @@ function scenarioConfig(scenario: string, options: TestStateOptions = {}) {
|
||||
discord: {
|
||||
enabled: true,
|
||||
token: { source: "env", provider: "default", id: "DISCORD_BOT_TOKEN" },
|
||||
dm: {
|
||||
policy: "allowlist",
|
||||
allowFrom: ["111111111111111111"],
|
||||
},
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: ["111111111111111111"],
|
||||
groupPolicy: "allowlist",
|
||||
guilds: {
|
||||
"222222222222222222": {
|
||||
@@ -473,6 +479,7 @@ OPENCLAW_TEST_STATE_JSON
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
"ownership": "explicit",
|
||||
"defaults": {
|
||||
"model": {
|
||||
"primary": "openai/gpt-5.6-luna"
|
||||
@@ -481,14 +488,21 @@ OPENCLAW_TEST_STATE_JSON
|
||||
"skills": [
|
||||
"memory"
|
||||
],
|
||||
"authInheritance": {
|
||||
"agentId": "main"
|
||||
},
|
||||
"heartbeat": {
|
||||
"agentId": "main"
|
||||
},
|
||||
"sessionStore": {
|
||||
"agentId": "main"
|
||||
},
|
||||
"systemAgent": {
|
||||
"agentId": "main"
|
||||
}
|
||||
},
|
||||
"list": [
|
||||
{
|
||||
"id": "main",
|
||||
"default": true,
|
||||
"entries": {
|
||||
"main": {
|
||||
"name": "Main",
|
||||
"workspace": "~/workspace",
|
||||
"model": {
|
||||
@@ -500,8 +514,7 @@ OPENCLAW_TEST_STATE_JSON
|
||||
],
|
||||
"contextTokens": 64000
|
||||
},
|
||||
{
|
||||
"id": "ops",
|
||||
"ops": {
|
||||
"name": "Ops",
|
||||
"workspace": "~/workspace/ops",
|
||||
"model": {
|
||||
@@ -509,8 +522,31 @@ OPENCLAW_TEST_STATE_JSON
|
||||
},
|
||||
"fastModeDefault": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"bindings": [
|
||||
{
|
||||
"agentId": "main",
|
||||
"match": {
|
||||
"channel": "discord",
|
||||
"accountId": "*"
|
||||
}
|
||||
},
|
||||
{
|
||||
"agentId": "main",
|
||||
"match": {
|
||||
"channel": "telegram",
|
||||
"accountId": "*"
|
||||
}
|
||||
},
|
||||
{
|
||||
"agentId": "main",
|
||||
"match": {
|
||||
"channel": "whatsapp",
|
||||
"accountId": "*"
|
||||
}
|
||||
}
|
||||
],
|
||||
"skills": {
|
||||
"allowBundled": [
|
||||
"memory",
|
||||
@@ -549,12 +585,10 @@ OPENCLAW_TEST_STATE_JSON
|
||||
"provider": "default",
|
||||
"id": "DISCORD_BOT_TOKEN"
|
||||
},
|
||||
"dm": {
|
||||
"policy": "allowlist",
|
||||
"allowFrom": [
|
||||
"111111111111111111"
|
||||
]
|
||||
},
|
||||
"dmPolicy": "allowlist",
|
||||
"allowFrom": [
|
||||
"111111111111111111"
|
||||
],
|
||||
"groupPolicy": "allowlist",
|
||||
"guilds": {
|
||||
"222222222222222222": {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { stylePromptTitle } from "../../packages/terminal-core/src/prompt-style.
|
||||
import { resolveAgentEffectiveModelPrimary, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { DEFAULT_AGENT_WORKSPACE_DIR, ensureAgentWorkspace } from "../agents/workspace.js";
|
||||
import { printClawBanner } from "../cli/claw-banner.js";
|
||||
import { inheritLegacyDefaultAgentId } from "../config/legacy.default-agent-owner.js";
|
||||
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
|
||||
import { resolveConfigPath, resolveStateDir } from "../config/paths.js";
|
||||
import { resolveSessionTranscriptsDirForAgent } from "../config/sessions/paths.js";
|
||||
@@ -190,7 +191,7 @@ export function applyWizardMetadata(
|
||||
): OpenClawConfig {
|
||||
const commit =
|
||||
normalizeOptionalString(process.env.GIT_COMMIT) ?? normalizeOptionalString(process.env.GIT_SHA);
|
||||
return {
|
||||
return inheritLegacyDefaultAgentId(cfg, {
|
||||
...cfg,
|
||||
wizard: {
|
||||
...cfg.wizard,
|
||||
@@ -200,7 +201,7 @@ export function applyWizardMetadata(
|
||||
lastRunCommand: params.command,
|
||||
lastRunMode: params.mode,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** Formats the no-GUI SSH tunnel hint for opening the Control UI remotely. */
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// Wizard metadata tests cover config provenance without losing migration ownership.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { tryResolveLegacyCompatibilityAgentId } from "../config/legacy.default-agent-owner.js";
|
||||
import { migratePersistedImplicitMainRoster } from "../config/legacy.roster.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { applyWizardMetadata } from "./onboard-helpers.js";
|
||||
|
||||
describe("applyWizardMetadata", () => {
|
||||
it("preserves the migrated legacy owner across the config replacement", () => {
|
||||
const cfg = migratePersistedImplicitMainRoster({
|
||||
agents: {
|
||||
list: [{ id: "main", default: true }, { id: "ops" }],
|
||||
},
|
||||
}).config as OpenClawConfig;
|
||||
expect(tryResolveLegacyCompatibilityAgentId(cfg)).toBe("main");
|
||||
|
||||
const result = applyWizardMetadata(cfg, { command: "doctor", mode: "local" });
|
||||
|
||||
expect(result).not.toBe(cfg);
|
||||
expect(tryResolveLegacyCompatibilityAgentId(result)).toBe("main");
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user