mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
fix(cli): honor explicit system agent in configure (#125377)
* fix(cli): honor explicit system agent in configure * fix(cli): persist explicit agent workspace * fix(cli): preserve authored agent key casing * fix(cli): preserve configured system agent key * fix(configure): preserve legacy onboarding owner * fix(configure): preserve workspace ownership semantics * fix(configure): retain migrated legacy owner * fix(configure): retain legacy owner marker --------- Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
This commit is contained in:
committed by
GitHub
parent
47af5a0960
commit
5dcc91df99
@@ -1,5 +1,6 @@
|
||||
// Configure wizard tests keep workspace-owned effects on the configured default agent.
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { retainLegacyDefaultAgentId } from "../config/legacy.default-agent-owner.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
|
||||
@@ -159,6 +160,192 @@ describe("runConfigureWizard default-agent ownership", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the configured system agent when ownership is explicit", async () => {
|
||||
const baseConfig = {
|
||||
agents: {
|
||||
ownership: "explicit",
|
||||
defaults: {
|
||||
workspace: "/tmp/global-workspace",
|
||||
systemAgent: { agentId: "main" },
|
||||
},
|
||||
entries: {
|
||||
MAIN: {
|
||||
agentDir: "/tmp/main-agent",
|
||||
},
|
||||
ops: {
|
||||
agentDir: "/tmp/ops-agent",
|
||||
workspace: "/tmp/ops-workspace",
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
mocks.state.snapshot = {
|
||||
exists: true,
|
||||
valid: true,
|
||||
hash: "config-hash",
|
||||
config: baseConfig,
|
||||
sourceConfig: baseConfig,
|
||||
issues: [],
|
||||
};
|
||||
mocks.text.mockResolvedValue("/tmp/new-main-workspace");
|
||||
|
||||
await runConfigureWizard(
|
||||
{ command: "configure", sections: ["workspace", "plugins", "skills"] },
|
||||
runtime,
|
||||
);
|
||||
|
||||
expect(mocks.commitConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
nextConfig: expect.objectContaining({
|
||||
agents: expect.objectContaining({
|
||||
defaults: expect.objectContaining({ workspace: "/tmp/global-workspace" }),
|
||||
entries: {
|
||||
MAIN: {
|
||||
agentDir: "/tmp/main-agent",
|
||||
workspace: "/tmp/new-main-workspace",
|
||||
},
|
||||
ops: {
|
||||
agentDir: "/tmp/ops-agent",
|
||||
workspace: "/tmp/ops-workspace",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(mocks.setupPluginConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ workspaceDir: "/tmp/new-main-workspace" }),
|
||||
);
|
||||
expect(mocks.setupSkills).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
"/tmp/new-main-workspace",
|
||||
runtime,
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(mocks.ensureWorkspaceAndSessions).toHaveBeenCalledWith(
|
||||
"/tmp/new-main-workspace",
|
||||
runtime,
|
||||
expect.objectContaining({ agentId: "main" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves the legacy default owner when system-agent ownership is not explicit", async () => {
|
||||
const baseConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: "/tmp/global-workspace",
|
||||
systemAgent: { agentId: "main" },
|
||||
},
|
||||
entries: {
|
||||
MAIN: { agentDir: "/tmp/main-agent" },
|
||||
ops: {
|
||||
default: true,
|
||||
agentDir: "/tmp/ops-agent",
|
||||
workspace: "/tmp/ops-workspace",
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
mocks.state.snapshot = {
|
||||
exists: true,
|
||||
valid: true,
|
||||
hash: "config-hash",
|
||||
config: baseConfig,
|
||||
sourceConfig: baseConfig,
|
||||
issues: [],
|
||||
};
|
||||
mocks.text.mockResolvedValue("/tmp/new-ops-workspace");
|
||||
|
||||
await runConfigureWizard(
|
||||
{ command: "configure", sections: ["workspace", "plugins", "skills"] },
|
||||
runtime,
|
||||
);
|
||||
|
||||
expect(mocks.setupPluginConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ workspaceDir: "/tmp/new-ops-workspace" }),
|
||||
);
|
||||
expect(mocks.setupSkills).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
"/tmp/new-ops-workspace",
|
||||
runtime,
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(mocks.ensureWorkspaceAndSessions).toHaveBeenCalledWith(
|
||||
"/tmp/new-ops-workspace",
|
||||
runtime,
|
||||
expect.objectContaining({ agentId: "ops" }),
|
||||
);
|
||||
expect(mocks.commitConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
nextConfig: expect.objectContaining({
|
||||
agents: expect.objectContaining({
|
||||
entries: expect.objectContaining({
|
||||
MAIN: expect.not.objectContaining({ workspace: "/tmp/new-ops-workspace" }),
|
||||
ops: expect.objectContaining({ workspace: "/tmp/new-ops-workspace" }),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps a workspace-less legacy owner on the global workspace", async () => {
|
||||
const baseConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: "/tmp/global-workspace",
|
||||
systemAgent: { agentId: "main" },
|
||||
},
|
||||
entries: {
|
||||
main: { agentDir: "/tmp/main-agent" },
|
||||
ops: {
|
||||
agentDir: "/tmp/ops-agent",
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
retainLegacyDefaultAgentId(baseConfig, "ops");
|
||||
mocks.state.snapshot = {
|
||||
exists: true,
|
||||
valid: true,
|
||||
hash: "config-hash",
|
||||
config: baseConfig,
|
||||
sourceConfig: baseConfig,
|
||||
issues: [],
|
||||
};
|
||||
mocks.text.mockResolvedValue("/tmp/new-global-workspace");
|
||||
|
||||
await runConfigureWizard(
|
||||
{ command: "configure", sections: ["workspace", "plugins", "skills"] },
|
||||
runtime,
|
||||
);
|
||||
|
||||
expect(mocks.commitConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
nextConfig: expect.objectContaining({
|
||||
agents: expect.objectContaining({
|
||||
defaults: expect.objectContaining({ workspace: "/tmp/new-global-workspace" }),
|
||||
entries: baseConfig.agents.entries,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(mocks.setupPluginConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ workspaceDir: "/tmp/new-global-workspace" }),
|
||||
);
|
||||
expect(mocks.setupSkills).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
"/tmp/new-global-workspace",
|
||||
runtime,
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(mocks.ensureWorkspaceAndSessions).toHaveBeenCalledWith(
|
||||
"/tmp/new-global-workspace",
|
||||
runtime,
|
||||
expect.objectContaining({ agentId: "ops" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not persist an unprovisionable workspace", async () => {
|
||||
mocks.ensureWorkspaceAndSessions.mockRejectedValueOnce(new Error("workspace is unwritable"));
|
||||
|
||||
|
||||
@@ -8,12 +8,14 @@ import { formatCliCommand } from "../cli/command-format.js";
|
||||
import { formatPortRangeHint } from "../cli/error-format.js";
|
||||
import { parsePort } from "../cli/shared/parse-port.js";
|
||||
import { readConfigFileSnapshotForWrite, resolveGatewayPort } from "../config/config.js";
|
||||
import { inheritLegacyDefaultAgentId } from "../config/legacy.default-agent-owner.js";
|
||||
import { logConfigUpdated } from "../config/logging.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { resolveGatewayProbeAuthSafeWithSecretInputs } from "../gateway/probe-auth.js";
|
||||
import { formatWindowsGatewayFirewallGuidance } from "../infra/windows-gateway-firewall-diagnostics.js";
|
||||
import { commitConfigWithPendingPluginInstalls } from "../plugins/install-record-commit.js";
|
||||
import { resolvePluginContributionOwners } from "../plugins/plugin-registry.js";
|
||||
import { normalizeAgentId } from "../routing/session-key.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { createLazyImportLoader } from "../shared/lazy-promise.js";
|
||||
@@ -44,6 +46,7 @@ import { healthCommand } from "./health.js";
|
||||
import {
|
||||
ensureOnboardingAgentWorkspace,
|
||||
resolveOnboardingAgentTarget,
|
||||
resolveSystemAgentOnboardingTarget,
|
||||
} from "./onboard-agent-target.js";
|
||||
import { setupChannels } from "./onboard-channels.js";
|
||||
import {
|
||||
@@ -608,7 +611,12 @@ export async function runConfigureWizard(
|
||||
};
|
||||
didSetGatewayMode = true;
|
||||
}
|
||||
const resolveSetupTarget = () => resolveOnboardingAgentTarget(nextConfig);
|
||||
// Configure keeps legacy default-owner semantics; only explicit fleets opt into
|
||||
// the System Agent target used unconditionally by setup and recovery callers.
|
||||
const resolveSetupTarget = () =>
|
||||
nextConfig.agents?.ownership === "explicit"
|
||||
? resolveSystemAgentOnboardingTarget(nextConfig)
|
||||
: resolveOnboardingAgentTarget(inheritLegacyDefaultAgentId(baseConfig, nextConfig));
|
||||
let workspaceDir = resolveSetupTarget().workspaceDir;
|
||||
let gatewayPort = resolveGatewayPort(baseConfig);
|
||||
|
||||
@@ -665,16 +673,24 @@ export async function runConfigureWizard(
|
||||
}
|
||||
}
|
||||
const target = resolveSetupTarget();
|
||||
const targetEntry = nextConfig.agents?.entries?.[target.agentId];
|
||||
const authoredEntryKey = Object.keys(nextConfig.agents?.entries ?? {}).find(
|
||||
(key) => normalizeAgentId(key) === target.agentId,
|
||||
);
|
||||
const targetEntry = authoredEntryKey
|
||||
? nextConfig.agents?.entries?.[authoredEntryKey]
|
||||
: undefined;
|
||||
// Explicit fleets own workspace at the selected entry even when it inherited
|
||||
// the global default; legacy owners stay global until they author an override.
|
||||
nextConfig =
|
||||
targetEntry?.workspace !== undefined
|
||||
targetEntry?.workspace !== undefined ||
|
||||
(nextConfig.agents?.ownership === "explicit" && targetEntry !== undefined)
|
||||
? {
|
||||
...nextConfig,
|
||||
agents: {
|
||||
...nextConfig.agents,
|
||||
entries: {
|
||||
...nextConfig.agents?.entries,
|
||||
[target.agentId]: { ...targetEntry, workspace: workspaceDir },
|
||||
[authoredEntryKey ?? target.agentId]: { ...targetEntry, workspace: workspaceDir },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -7,13 +7,34 @@ import { retainLegacyDefaultAgentId } from "../config/legacy.default-agent-owner
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { withEnvAsync } from "../test-utils/env.js";
|
||||
import {
|
||||
applyOnboardingPrimaryModel,
|
||||
ensureOnboardingAgentWorkspace,
|
||||
resolveOnboardingAgentTarget,
|
||||
resolveSystemAgentOnboardingTarget,
|
||||
} from "./onboard-agent-target.js";
|
||||
|
||||
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||||
|
||||
describe("onboarding agent target", () => {
|
||||
it("preserves an uppercase authored entry key when applying the primary model", () => {
|
||||
const config = {
|
||||
agents: {
|
||||
ownership: "explicit" as const,
|
||||
entries: {
|
||||
MAIN: { model: "openai/old" },
|
||||
},
|
||||
},
|
||||
};
|
||||
const target = resolveOnboardingAgentTarget(config, "main");
|
||||
|
||||
expect(applyOnboardingPrimaryModel(config, target, "openai/new").agents?.entries).toEqual({
|
||||
MAIN: {
|
||||
model: { primary: "openai/new" },
|
||||
models: { "openai/new": {} },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the retained compatibility owner after the marker is removed", () => {
|
||||
const config = retainLegacyDefaultAgentId(
|
||||
{ agents: { entries: { main: {}, ops: { workspace: "/srv/ops" } } } },
|
||||
@@ -26,6 +47,30 @@ describe("onboarding agent target", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves shared system-agent setup to the configured system agent on a legacy roster", () => {
|
||||
const config = {
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: "/srv/global",
|
||||
systemAgent: { agentId: "main" },
|
||||
},
|
||||
entries: {
|
||||
main: { workspace: "/srv/main" },
|
||||
ops: { default: true, workspace: "/srv/ops" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(resolveOnboardingAgentTarget(config)).toMatchObject({
|
||||
agentId: "ops",
|
||||
workspaceDir: "/srv/ops",
|
||||
});
|
||||
expect(resolveSystemAgentOnboardingTarget(config)).toMatchObject({
|
||||
agentId: "main",
|
||||
workspaceDir: "/srv/main",
|
||||
});
|
||||
});
|
||||
|
||||
it("provisions the configured default agent workspace and sessions", async () => {
|
||||
const stateDir = tempDirs.make("openclaw-onboard-target-");
|
||||
const globalWorkspace = path.join(stateDir, "global-workspace");
|
||||
|
||||
@@ -61,7 +61,10 @@ export function applyOnboardingPrimaryModel(
|
||||
target: OnboardingAgentTarget,
|
||||
model: string,
|
||||
): OpenClawConfig {
|
||||
const entry = config.agents?.entries?.[target.agentId];
|
||||
const authoredEntryKey = Object.keys(config.agents?.entries ?? {}).find(
|
||||
(key) => normalizeAgentId(key) === target.agentId,
|
||||
);
|
||||
const entry = authoredEntryKey ? config.agents?.entries?.[authoredEntryKey] : undefined;
|
||||
if (entry?.model === undefined) {
|
||||
return applyPrimaryModel(config, model);
|
||||
}
|
||||
@@ -77,7 +80,7 @@ export function applyOnboardingPrimaryModel(
|
||||
...config.agents,
|
||||
entries: {
|
||||
...config.agents?.entries,
|
||||
[target.agentId]: {
|
||||
[authoredEntryKey ?? target.agentId]: {
|
||||
...entry,
|
||||
model: {
|
||||
...(fallbackValues.length > 0 ? { fallbacks: fallbackValues } : {}),
|
||||
|
||||
Reference in New Issue
Block a user