mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(onboard): keep setup effects on the default agent (#112738)
* fix(onboard): align default agent setup ownership * chore(onboard): remove stale target assignments * fix(onboard): preserve workspace provisioning boundary
This commit is contained in:
committed by
GitHub
parent
0724dfda21
commit
bf922f59fe
@@ -272,11 +272,11 @@ describe("agents add command", () => {
|
||||
expect(writeConfigFileMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips catalog validation when checking the interactive wizard model config", async () => {
|
||||
it("uses the explicit agent target and skips catalog validation", async () => {
|
||||
readConfigFileSnapshotMock.mockResolvedValue({
|
||||
...baseConfigSnapshot,
|
||||
config: { agents: { list: [] } },
|
||||
sourceConfig: { agents: { list: [] } },
|
||||
config: { agents: { entries: {} } },
|
||||
sourceConfig: { agents: { entries: {} } },
|
||||
});
|
||||
wizardMocks.createClackPrompter.mockReturnValue({
|
||||
intro: vi.fn(),
|
||||
@@ -297,6 +297,11 @@ describe("agents add command", () => {
|
||||
validateCatalog: false,
|
||||
}),
|
||||
);
|
||||
expect(onboardHelpersMocks.ensureWorkspaceAndSessions).toHaveBeenCalledWith(
|
||||
"/tmp/openclaw-jon",
|
||||
runtime,
|
||||
expect.objectContaining({ agentId: "jon" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("copies only portable auth profiles when seeding a new agent store", async () => {
|
||||
|
||||
@@ -36,8 +36,11 @@ import { requireValidConfigFileSnapshot } from "./agents.command-shared.js";
|
||||
import { applyAgentConfig, listAgentEntries } from "./agents.config.js";
|
||||
import { promptAuthChoiceGrouped } from "./auth-choice-prompt.js";
|
||||
import { applyAuthChoice, warnIfModelConfigLooksOff } from "./auth-choice.js";
|
||||
import {
|
||||
ensureOnboardingAgentWorkspace,
|
||||
resolveOnboardingAgentTarget,
|
||||
} from "./onboard-agent-target.js";
|
||||
import { setupChannels } from "./onboard-channels.js";
|
||||
import { ensureWorkspaceAndSessions } from "./onboard-helpers.js";
|
||||
import type { ChannelChoice } from "./onboard-types.js";
|
||||
|
||||
type AgentsAddOptions = {
|
||||
@@ -410,17 +413,17 @@ export async function agentsAddCommand(
|
||||
});
|
||||
nextConfig = committed.config;
|
||||
logConfigUpdated(runtime);
|
||||
await ensureWorkspaceAndSessions(workspaceDir, runtime, {
|
||||
const target = resolveOnboardingAgentTarget(nextConfig, agentId);
|
||||
await ensureOnboardingAgentWorkspace(target, runtime, {
|
||||
skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap),
|
||||
skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles,
|
||||
agentId,
|
||||
});
|
||||
|
||||
const payload = {
|
||||
agentId,
|
||||
agentId: target.agentId,
|
||||
name: agentName,
|
||||
workspace: workspaceDir,
|
||||
agentDir,
|
||||
workspace: target.workspaceDir,
|
||||
agentDir: target.agentDir,
|
||||
};
|
||||
if (opts.json) {
|
||||
writeRuntimeJson(runtime, payload);
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
// Configure wizard tests keep workspace-owned effects on the configured default agent.
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
state: { snapshot: undefined as unknown },
|
||||
commitConfig: vi.fn(),
|
||||
ensureWorkspaceAndSessions: vi.fn(),
|
||||
setupPluginConfig: vi.fn(),
|
||||
setupSkills: vi.fn(),
|
||||
text: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../config/config.js", () => ({
|
||||
createConfigIO: () => ({
|
||||
readConfigFileSnapshotForWrite: async () => ({ snapshot: mocks.state.snapshot }),
|
||||
}),
|
||||
readConfigFileSnapshotForWrite: async () => ({
|
||||
snapshot: mocks.state.snapshot,
|
||||
writeOptions: {
|
||||
expectedConfigPath: "/tmp/openclaw.json",
|
||||
ownedConfigPathForWrite: "/tmp/openclaw.json",
|
||||
},
|
||||
}),
|
||||
resolveGatewayPort: () => 18789,
|
||||
}));
|
||||
|
||||
vi.mock("../config/logging.js", () => ({ logConfigUpdated: vi.fn() }));
|
||||
|
||||
vi.mock("../plugins/install-record-commit.js", () => ({
|
||||
commitConfigWithPendingPluginInstalls: mocks.commitConfig,
|
||||
}));
|
||||
|
||||
vi.mock("../wizard/clack-prompter.js", () => ({
|
||||
createClackPrompter: () => ({
|
||||
intro: vi.fn(),
|
||||
outro: vi.fn(),
|
||||
note: vi.fn(),
|
||||
select: vi.fn(),
|
||||
multiselect: vi.fn(),
|
||||
text: vi.fn(),
|
||||
confirm: vi.fn(),
|
||||
progress: vi.fn(() => ({ update: vi.fn(), stop: vi.fn() })),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../wizard/setup.plugin-config.js", () => ({
|
||||
configurePluginConfig: mocks.setupPluginConfig,
|
||||
}));
|
||||
|
||||
vi.mock("./configure.shared.js", () => ({
|
||||
CONFIGURE_SECTION_OPTIONS: [],
|
||||
confirm: vi.fn(),
|
||||
intro: vi.fn(),
|
||||
outro: vi.fn(),
|
||||
select: vi.fn(),
|
||||
text: mocks.text,
|
||||
}));
|
||||
|
||||
vi.mock("./onboard-helpers.js", () => ({
|
||||
DEFAULT_WORKSPACE: "/tmp/default-workspace",
|
||||
applyWizardMetadata: (config: OpenClawConfig) => config,
|
||||
ensureWorkspaceAndSessions: mocks.ensureWorkspaceAndSessions,
|
||||
guardCancel: (value: unknown) => value,
|
||||
probeGatewayReachable: vi.fn(),
|
||||
resolveAdvertisedControlUiLinks: vi.fn(),
|
||||
resolveLocalControlUiProbeLinks: vi.fn(),
|
||||
summarizeExistingConfig: vi.fn(() => ""),
|
||||
waitForGatewayReachable: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./onboard-skills.js", () => ({ setupSkills: mocks.setupSkills }));
|
||||
|
||||
import { runConfigureWizard } from "./configure.wizard.js";
|
||||
|
||||
const runtime = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
} as unknown as RuntimeEnv;
|
||||
|
||||
describe("runConfigureWizard default-agent ownership", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
const baseConfig = {
|
||||
agents: {
|
||||
defaults: { workspace: "/tmp/global-workspace" },
|
||||
entries: {
|
||||
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");
|
||||
mocks.setupPluginConfig.mockImplementation(
|
||||
async ({ config }: { config: OpenClawConfig }) => config,
|
||||
);
|
||||
mocks.setupSkills.mockImplementation(async (config: OpenClawConfig) => config);
|
||||
mocks.commitConfig.mockImplementation(
|
||||
async ({ nextConfig }: { nextConfig: OpenClawConfig }) => ({ config: nextConfig }),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the concrete default-agent workspace for provisioning, plugins, and skills", async () => {
|
||||
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: expect.objectContaining({
|
||||
ops: expect.objectContaining({ workspace: "/tmp/new-ops-workspace" }),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
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" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not persist an unprovisionable workspace", async () => {
|
||||
mocks.ensureWorkspaceAndSessions.mockRejectedValueOnce(new Error("workspace is unwritable"));
|
||||
|
||||
await expect(
|
||||
runConfigureWizard(
|
||||
{ command: "configure", sections: ["workspace", "plugins", "skills"] },
|
||||
runtime,
|
||||
),
|
||||
).rejects.toThrow("workspace is unwritable");
|
||||
|
||||
expect(mocks.setupPluginConfig).not.toHaveBeenCalled();
|
||||
expect(mocks.setupSkills).not.toHaveBeenCalled();
|
||||
expect(mocks.commitConfig).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -46,11 +46,14 @@ import {
|
||||
} from "./configure.shared.js";
|
||||
import { formatHealthCheckFailure } from "./health-format.js";
|
||||
import { healthCommand } from "./health.js";
|
||||
import {
|
||||
ensureOnboardingAgentWorkspace,
|
||||
resolveOnboardingAgentTarget,
|
||||
} from "./onboard-agent-target.js";
|
||||
import { setupChannels } from "./onboard-channels.js";
|
||||
import {
|
||||
applyWizardMetadata,
|
||||
DEFAULT_WORKSPACE,
|
||||
ensureWorkspaceAndSessions,
|
||||
guardCancel,
|
||||
probeGatewayReachable,
|
||||
resolveAdvertisedControlUiLinks,
|
||||
@@ -540,10 +543,8 @@ export async function runConfigureWizard(
|
||||
};
|
||||
didSetGatewayMode = true;
|
||||
}
|
||||
let workspaceDir =
|
||||
nextConfig.agents?.defaults?.workspace ??
|
||||
baseConfig.agents?.defaults?.workspace ??
|
||||
DEFAULT_WORKSPACE;
|
||||
const resolveSetupTarget = () => resolveOnboardingAgentTarget(nextConfig);
|
||||
let workspaceDir = resolveSetupTarget().workspaceDir;
|
||||
let gatewayPort = resolveGatewayPort(baseConfig);
|
||||
|
||||
const persistConfig = async () => {
|
||||
@@ -630,17 +631,34 @@ export async function runConfigureWizard(
|
||||
);
|
||||
}
|
||||
}
|
||||
nextConfig = {
|
||||
...nextConfig,
|
||||
agents: {
|
||||
...nextConfig.agents,
|
||||
defaults: {
|
||||
...nextConfig.agents?.defaults,
|
||||
workspace: workspaceDir,
|
||||
},
|
||||
},
|
||||
};
|
||||
await ensureWorkspaceAndSessions(workspaceDir, runtime, {
|
||||
const target = resolveSetupTarget();
|
||||
const targetEntry = nextConfig.agents?.entries?.[target.agentId];
|
||||
nextConfig =
|
||||
targetEntry?.workspace !== undefined
|
||||
? {
|
||||
...nextConfig,
|
||||
agents: {
|
||||
...nextConfig.agents,
|
||||
entries: {
|
||||
...nextConfig.agents?.entries,
|
||||
[target.agentId]: { ...targetEntry, workspace: workspaceDir },
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
...nextConfig,
|
||||
agents: {
|
||||
...nextConfig.agents,
|
||||
defaults: {
|
||||
...nextConfig.agents?.defaults,
|
||||
workspace: workspaceDir,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const provisionWorkspace = async () => {
|
||||
await ensureOnboardingAgentWorkspace(resolveSetupTarget(), runtime, {
|
||||
skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap),
|
||||
skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles,
|
||||
});
|
||||
@@ -684,6 +702,7 @@ export async function runConfigureWizard(
|
||||
|
||||
if (selected.includes("workspace")) {
|
||||
await configureWorkspace();
|
||||
await provisionWorkspace();
|
||||
}
|
||||
|
||||
if (selected.includes("model")) {
|
||||
@@ -709,13 +728,17 @@ export async function runConfigureWizard(
|
||||
nextConfig = await configurePluginConfig({
|
||||
config: nextConfig,
|
||||
prompter,
|
||||
workspaceDir: resolveUserPath(workspaceDir),
|
||||
workspaceDir: resolveSetupTarget().workspaceDir,
|
||||
});
|
||||
}
|
||||
|
||||
if (selected.includes("skills")) {
|
||||
const wsDir = resolveUserPath(workspaceDir);
|
||||
nextConfig = await setupSkills(nextConfig, wsDir, runtime, prompter);
|
||||
nextConfig = await setupSkills(
|
||||
nextConfig,
|
||||
resolveSetupTarget().workspaceDir,
|
||||
runtime,
|
||||
prompter,
|
||||
);
|
||||
}
|
||||
|
||||
await persistConfig();
|
||||
@@ -744,6 +767,7 @@ export async function runConfigureWizard(
|
||||
|
||||
if (choice === "workspace") {
|
||||
await configureWorkspace();
|
||||
await provisionWorkspace();
|
||||
await persistConfig();
|
||||
}
|
||||
|
||||
@@ -775,14 +799,18 @@ export async function runConfigureWizard(
|
||||
nextConfig = await configurePluginConfig({
|
||||
config: nextConfig,
|
||||
prompter,
|
||||
workspaceDir: resolveUserPath(workspaceDir),
|
||||
workspaceDir: resolveSetupTarget().workspaceDir,
|
||||
});
|
||||
await persistConfig();
|
||||
}
|
||||
|
||||
if (choice === "skills") {
|
||||
const wsDir = resolveUserPath(workspaceDir);
|
||||
nextConfig = await setupSkills(nextConfig, wsDir, runtime, prompter);
|
||||
nextConfig = await setupSkills(
|
||||
nextConfig,
|
||||
resolveSetupTarget().workspaceDir,
|
||||
runtime,
|
||||
prompter,
|
||||
);
|
||||
await persistConfig();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// Model picker tests read the configured target agent without rewriting global defaults.
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
|
||||
vi.mock("./model-picker.runtime.js", () => ({
|
||||
modelPickerRuntime: { resolvePluginProviders: () => [] },
|
||||
}));
|
||||
|
||||
import { promptDefaultModel } from "./model-picker.js";
|
||||
|
||||
describe("promptDefaultModel default-agent ownership", () => {
|
||||
it("offers the resolved agent override as the current model", async () => {
|
||||
const config = {
|
||||
agents: {
|
||||
defaults: { model: "openai/global-model" },
|
||||
entries: {
|
||||
ops: {
|
||||
default: true,
|
||||
model: "anthropic/ops-model",
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
const select = vi.fn(async (params: { options: Array<{ value: string; label: string }> }) => {
|
||||
const keep = params.options[0];
|
||||
expect(keep?.label).toContain("anthropic/ops-model");
|
||||
return keep?.value;
|
||||
});
|
||||
const prompter = {
|
||||
select,
|
||||
progress: vi.fn(() => ({ stop: vi.fn(), update: vi.fn() })),
|
||||
} as unknown as WizardPrompter;
|
||||
|
||||
await expect(
|
||||
promptDefaultModel({
|
||||
config,
|
||||
prompter,
|
||||
agentId: "ops",
|
||||
agentDir: "/tmp/ops-agent",
|
||||
workspaceDir: "/tmp/ops-workspace",
|
||||
loadCatalog: false,
|
||||
}),
|
||||
).resolves.toEqual({});
|
||||
expect(select).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
// Onboarding target tests keep workspace, auth directory, and sessions on one agent owner.
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { withEnvAsync } from "../test-utils/env.js";
|
||||
import {
|
||||
ensureOnboardingAgentWorkspace,
|
||||
resolveOnboardingAgentTarget,
|
||||
} from "./onboard-agent-target.js";
|
||||
|
||||
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||||
|
||||
describe("onboarding agent target", () => {
|
||||
it("provisions the configured default agent workspace and sessions", async () => {
|
||||
const stateDir = tempDirs.make("openclaw-onboard-target-");
|
||||
const globalWorkspace = path.join(stateDir, "global-workspace");
|
||||
const opsWorkspace = path.join(stateDir, "ops-workspace");
|
||||
const runtime = { log: vi.fn() } as unknown as RuntimeEnv;
|
||||
|
||||
await withEnvAsync({ OPENCLAW_STATE_DIR: stateDir }, async () => {
|
||||
const config = {
|
||||
agents: {
|
||||
defaults: { workspace: globalWorkspace },
|
||||
entries: { ops: { default: true, workspace: opsWorkspace } },
|
||||
},
|
||||
};
|
||||
const target = resolveOnboardingAgentTarget(config);
|
||||
|
||||
expect(target).toEqual({
|
||||
agentId: "ops",
|
||||
agentDir: path.join(stateDir, "agents", "ops", "agent"),
|
||||
workspaceDir: opsWorkspace,
|
||||
});
|
||||
expect(resolveOnboardingAgentTarget(config, " OPS ")).toEqual(target);
|
||||
await ensureOnboardingAgentWorkspace(target, runtime, { skipBootstrap: true });
|
||||
|
||||
expect((await fs.stat(opsWorkspace)).isDirectory()).toBe(true);
|
||||
expect((await fs.stat(path.join(stateDir, "agents", "ops", "sessions"))).isDirectory()).toBe(
|
||||
true,
|
||||
);
|
||||
await expect(fs.access(globalWorkspace)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
await expect(
|
||||
fs.access(path.join(stateDir, "agents", "main", "sessions")),
|
||||
).rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
// Resolves one concrete agent owner for onboarding auth, model, workspace, and session effects.
|
||||
import {
|
||||
resolveAgentDir,
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveDefaultAgentId,
|
||||
} from "../agents/agent-scope-config.js";
|
||||
import {
|
||||
normalizeAgentModelMapForConfig,
|
||||
normalizeAgentModelRefForConfig,
|
||||
resolveAgentModelFallbackValues,
|
||||
} from "../config/model-input.js";
|
||||
import type { OptionalBootstrapFileName } from "../config/types.agent-defaults.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { applyPrimaryModel } from "../plugins/provider-model-primary.js";
|
||||
import { normalizeAgentId } from "../routing/session-key.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { ensureWorkspaceAndSessions } from "./onboard-helpers.js";
|
||||
|
||||
export type OnboardingAgentTarget = {
|
||||
agentId: string;
|
||||
agentDir: string;
|
||||
workspaceDir: string;
|
||||
};
|
||||
|
||||
export function resolveOnboardingAgentTarget(
|
||||
config: OpenClawConfig,
|
||||
explicitAgentId?: string,
|
||||
): OnboardingAgentTarget {
|
||||
const agentId = normalizeAgentId(explicitAgentId ?? resolveDefaultAgentId(config));
|
||||
return {
|
||||
agentId,
|
||||
agentDir: resolveAgentDir(config, agentId),
|
||||
workspaceDir: resolveAgentWorkspaceDir(config, agentId),
|
||||
};
|
||||
}
|
||||
|
||||
export async function ensureOnboardingAgentWorkspace(
|
||||
target: OnboardingAgentTarget,
|
||||
runtime: RuntimeEnv,
|
||||
options?: {
|
||||
skipBootstrap?: boolean;
|
||||
skipOptionalBootstrapFiles?: OptionalBootstrapFileName[];
|
||||
},
|
||||
): Promise<{ bootstrapPending: boolean }> {
|
||||
return ensureWorkspaceAndSessions(target.workspaceDir, runtime, {
|
||||
...options,
|
||||
agentId: target.agentId,
|
||||
});
|
||||
}
|
||||
|
||||
export function applyOnboardingPrimaryModel(
|
||||
config: OpenClawConfig,
|
||||
target: OnboardingAgentTarget,
|
||||
model: string,
|
||||
): OpenClawConfig {
|
||||
const entry = config.agents?.entries?.[target.agentId];
|
||||
if (entry?.model === undefined) {
|
||||
return applyPrimaryModel(config, model);
|
||||
}
|
||||
|
||||
const primary = normalizeAgentModelRefForConfig(model);
|
||||
const fallbackValues = resolveAgentModelFallbackValues(entry.model).map((fallback) =>
|
||||
normalizeAgentModelRefForConfig(fallback),
|
||||
);
|
||||
const models = normalizeAgentModelMapForConfig(entry.models ?? {});
|
||||
return {
|
||||
...config,
|
||||
agents: {
|
||||
...config.agents,
|
||||
entries: {
|
||||
...config.agents?.entries,
|
||||
[target.agentId]: {
|
||||
...entry,
|
||||
model: {
|
||||
...(fallbackValues.length > 0 ? { fallbacks: fallbackValues } : {}),
|
||||
primary,
|
||||
},
|
||||
models: {
|
||||
...models,
|
||||
[primary]: models[primary] ?? {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// Non-interactive setup tests keep provisioning and output on the configured default agent.
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
commitConfig: vi.fn(),
|
||||
ensureWorkspaceAndSessions: vi.fn(),
|
||||
logConfigUpdated: vi.fn(),
|
||||
logJson: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../config/config.js", () => ({
|
||||
resolveGatewayPort: () => 18789,
|
||||
}));
|
||||
|
||||
vi.mock("../../config/logging.js", () => ({
|
||||
logConfigUpdated: mocks.logConfigUpdated,
|
||||
}));
|
||||
|
||||
vi.mock("../onboard-helpers.js", () => ({
|
||||
DEFAULT_WORKSPACE: "/tmp/default-workspace",
|
||||
applyWizardMetadata: (config: OpenClawConfig) => config,
|
||||
ensureWorkspaceAndSessions: mocks.ensureWorkspaceAndSessions,
|
||||
resolveLocalControlUiProbeLinks: vi.fn(),
|
||||
waitForGatewayReachable: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./config-write.js", () => ({
|
||||
commitNonInteractiveOnboardConfig: mocks.commitConfig,
|
||||
}));
|
||||
|
||||
vi.mock("./local/gateway-config.js", () => ({
|
||||
applyNonInteractiveGatewayConfig: ({ nextConfig }: { nextConfig: OpenClawConfig }) => ({
|
||||
nextConfig,
|
||||
port: 18789,
|
||||
bind: "loopback",
|
||||
authMode: "token",
|
||||
tailscaleMode: "off",
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("./local/output.js", () => ({
|
||||
logNonInteractiveOnboardingFailure: vi.fn(),
|
||||
logNonInteractiveOnboardingJson: mocks.logJson,
|
||||
}));
|
||||
|
||||
vi.mock("./local/skills-config.js", () => ({
|
||||
applyNonInteractiveSkillsConfig: ({ nextConfig }: { nextConfig: OpenClawConfig }) => nextConfig,
|
||||
}));
|
||||
|
||||
import { runNonInteractiveLocalSetup } from "./local.js";
|
||||
|
||||
const runtime = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
} as unknown as RuntimeEnv;
|
||||
|
||||
describe("runNonInteractiveLocalSetup default-agent ownership", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.commitConfig.mockImplementation(
|
||||
async ({ nextConfig }: { nextConfig: OpenClawConfig }) => nextConfig,
|
||||
);
|
||||
});
|
||||
|
||||
it("provisions and reports the keyed default agent while preserving the global workspace", async () => {
|
||||
const baseConfig = {
|
||||
agents: {
|
||||
defaults: { workspace: "/tmp/global-workspace" },
|
||||
entries: {
|
||||
ops: {
|
||||
default: true,
|
||||
agentDir: "/tmp/ops-agent",
|
||||
workspace: "/tmp/ops-workspace",
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
await runNonInteractiveLocalSetup({
|
||||
opts: {
|
||||
nonInteractive: true,
|
||||
mode: "local",
|
||||
workspace: "/tmp/global-workspace",
|
||||
authChoice: "skip",
|
||||
skipHooks: true,
|
||||
skipSkills: true,
|
||||
skipHealth: true,
|
||||
installDaemon: false,
|
||||
json: true,
|
||||
},
|
||||
runtime,
|
||||
baseConfig,
|
||||
});
|
||||
|
||||
expect(mocks.commitConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
nextConfig: expect.objectContaining({
|
||||
agents: expect.objectContaining({
|
||||
defaults: expect.objectContaining({ workspace: "/tmp/global-workspace" }),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(mocks.ensureWorkspaceAndSessions).toHaveBeenCalledWith(
|
||||
"/tmp/ops-workspace",
|
||||
runtime,
|
||||
expect.objectContaining({ agentId: "ops" }),
|
||||
);
|
||||
expect(mocks.logJson).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ workspaceDir: "/tmp/ops-workspace" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,10 @@ import { resolveGatewayAuthToken } from "../../gateway/auth-token-resolution.js"
|
||||
import { resolveConfiguredSecretInputWithFallback } from "../../gateway/resolve-configured-secret-input-string.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
import { DEFAULT_GATEWAY_DAEMON_RUNTIME } from "../daemon-runtime.js";
|
||||
import {
|
||||
ensureOnboardingAgentWorkspace,
|
||||
resolveOnboardingAgentTarget,
|
||||
} from "../onboard-agent-target.js";
|
||||
import {
|
||||
applyLocalSetupWorkspaceConfig,
|
||||
applySkipBootstrapConfig,
|
||||
@@ -20,7 +24,6 @@ import {
|
||||
import {
|
||||
applyWizardMetadata,
|
||||
DEFAULT_WORKSPACE,
|
||||
ensureWorkspaceAndSessions,
|
||||
resolveLocalControlUiProbeLinks,
|
||||
waitForGatewayReachable,
|
||||
} from "../onboard-helpers.js";
|
||||
@@ -177,7 +180,6 @@ export async function runNonInteractiveLocalSetup(params: {
|
||||
defaultWorkspaceDir: DEFAULT_WORKSPACE,
|
||||
});
|
||||
const workspaceConflict = resolveOnboardingWorkspaceConflict(baseConfig, requestedWorkspaceDir);
|
||||
const workspaceDir = workspaceConflict?.currentWorkspaceDir ?? requestedWorkspaceDir;
|
||||
if (workspaceConflict) {
|
||||
runtime.error(
|
||||
[
|
||||
@@ -196,12 +198,13 @@ export async function runNonInteractiveLocalSetup(params: {
|
||||
if (opts.skipBootstrap) {
|
||||
nextConfig = applySkipBootstrapConfig(nextConfig);
|
||||
}
|
||||
const authTarget = resolveOnboardingAgentTarget(nextConfig);
|
||||
|
||||
const inferredAuthChoice = opts.authChoice
|
||||
? undefined
|
||||
: (await import("./local/auth-choice-inference.js")).inferAuthChoiceFromFlags(opts, {
|
||||
config: nextConfig,
|
||||
workspaceDir,
|
||||
workspaceDir: authTarget.workspaceDir,
|
||||
env: process.env,
|
||||
});
|
||||
if (!opts.authChoice && inferredAuthChoice && inferredAuthChoice.matches.length > 1) {
|
||||
@@ -228,7 +231,7 @@ export async function runNonInteractiveLocalSetup(params: {
|
||||
opts,
|
||||
runtime,
|
||||
baseConfig,
|
||||
workspaceDir,
|
||||
target: authTarget,
|
||||
});
|
||||
if (!nextConfigAfterAuth) {
|
||||
return;
|
||||
@@ -262,7 +265,8 @@ export async function runNonInteractiveLocalSetup(params: {
|
||||
});
|
||||
logConfigUpdated(runtime);
|
||||
|
||||
await ensureWorkspaceAndSessions(workspaceDir, runtime, {
|
||||
const finalTarget = resolveOnboardingAgentTarget(nextConfig);
|
||||
await ensureOnboardingAgentWorkspace(finalTarget, runtime, {
|
||||
skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap),
|
||||
skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles,
|
||||
});
|
||||
@@ -403,7 +407,7 @@ export async function runNonInteractiveLocalSetup(params: {
|
||||
opts,
|
||||
runtime,
|
||||
mode,
|
||||
workspaceDir,
|
||||
workspaceDir: finalTarget.workspaceDir,
|
||||
authChoice,
|
||||
gateway: {
|
||||
port: gatewayResult.port,
|
||||
|
||||
@@ -100,6 +100,12 @@ function createRuntime() {
|
||||
};
|
||||
}
|
||||
|
||||
const target = {
|
||||
agentId: "main",
|
||||
agentDir: "/tmp/main-agent",
|
||||
workspaceDir: "/tmp/workspace",
|
||||
};
|
||||
|
||||
type MockCalls = { mock: { calls: Array<Array<unknown>> } };
|
||||
|
||||
function mockCall(mock: MockCalls, callIndex = 0): Array<unknown> {
|
||||
@@ -150,6 +156,7 @@ describe("applyNonInteractivePluginProviderChoice", () => {
|
||||
opts: {} as never,
|
||||
runtime: runtime as never,
|
||||
baseConfig: { agents: { defaults: {} } } as OpenClawConfig,
|
||||
target,
|
||||
resolveApiKey: vi.fn(),
|
||||
toApiKeyCredential: vi.fn(),
|
||||
});
|
||||
@@ -162,7 +169,12 @@ describe("applyNonInteractivePluginProviderChoice", () => {
|
||||
expect(providersInput.onlyPluginIds).toEqual(["vllm"]);
|
||||
expect(providersInput.includeUntrustedWorkspacePlugins).toBe(false);
|
||||
expect(resolveProviderPluginChoice).toHaveBeenCalledOnce();
|
||||
expect(runNonInteractive).toHaveBeenCalledOnce();
|
||||
expect(runNonInteractive).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentDir: target.agentDir,
|
||||
workspaceDir: target.workspaceDir,
|
||||
}),
|
||||
);
|
||||
expect(result).toEqual({ plugins: { allow: ["vllm"] } });
|
||||
});
|
||||
|
||||
@@ -210,6 +222,7 @@ describe("applyNonInteractivePluginProviderChoice", () => {
|
||||
opts: { groqApiKey: "groq-key" } as never,
|
||||
runtime: runtime as never,
|
||||
baseConfig: { agents: { defaults: {} } } as OpenClawConfig,
|
||||
target,
|
||||
resolveApiKey: vi.fn(),
|
||||
toApiKeyCredential: vi.fn(),
|
||||
});
|
||||
@@ -263,6 +276,7 @@ describe("applyNonInteractivePluginProviderChoice", () => {
|
||||
opts: {} as never,
|
||||
runtime: runtime as never,
|
||||
baseConfig: { agents: { defaults: {} } } as OpenClawConfig,
|
||||
target,
|
||||
resolveApiKey: vi.fn(),
|
||||
toApiKeyCredential: vi.fn(),
|
||||
});
|
||||
@@ -286,6 +300,7 @@ describe("applyNonInteractivePluginProviderChoice", () => {
|
||||
opts: {} as never,
|
||||
runtime: runtime as never,
|
||||
baseConfig: { agents: { defaults: {} } } as OpenClawConfig,
|
||||
target,
|
||||
resolveApiKey: vi.fn(),
|
||||
toApiKeyCredential: vi.fn(),
|
||||
});
|
||||
@@ -313,6 +328,7 @@ describe("applyNonInteractivePluginProviderChoice", () => {
|
||||
opts: {} as never,
|
||||
runtime: runtime as never,
|
||||
baseConfig: { agents: { defaults: {} } } as OpenClawConfig,
|
||||
target,
|
||||
resolveApiKey: vi.fn(),
|
||||
toApiKeyCredential: vi.fn(),
|
||||
});
|
||||
@@ -354,6 +370,7 @@ describe("applyNonInteractivePluginProviderChoice", () => {
|
||||
opts: {} as never,
|
||||
runtime: runtime as never,
|
||||
baseConfig: { agents: { defaults: {} } } as OpenClawConfig,
|
||||
target,
|
||||
resolveApiKey: vi.fn(),
|
||||
toApiKeyCredential: vi.fn(),
|
||||
});
|
||||
@@ -376,6 +393,7 @@ describe("applyNonInteractivePluginProviderChoice", () => {
|
||||
opts: {} as never,
|
||||
runtime: runtime as never,
|
||||
baseConfig: { agents: { defaults: {} } } as OpenClawConfig,
|
||||
target,
|
||||
resolveApiKey: vi.fn(),
|
||||
toApiKeyCredential: vi.fn(),
|
||||
});
|
||||
@@ -413,6 +431,7 @@ describe("applyNonInteractivePluginProviderChoice", () => {
|
||||
opts: {} as never,
|
||||
runtime: runtime as never,
|
||||
baseConfig: { agents: { defaults: {} } } as OpenClawConfig,
|
||||
target,
|
||||
resolveApiKey: vi.fn(),
|
||||
toApiKeyCredential: vi.fn(),
|
||||
});
|
||||
@@ -465,6 +484,7 @@ describe("applyNonInteractivePluginProviderChoice", () => {
|
||||
opts: {} as never,
|
||||
runtime: runtime as never,
|
||||
baseConfig: { agents: { defaults: {} } } as OpenClawConfig,
|
||||
target,
|
||||
resolveApiKey: vi.fn(),
|
||||
toApiKeyCredential: vi.fn(),
|
||||
});
|
||||
@@ -500,6 +520,7 @@ describe("applyNonInteractivePluginProviderChoice", () => {
|
||||
opts: {} as never,
|
||||
runtime: runtime as never,
|
||||
baseConfig: { agents: { defaults: {} } } as OpenClawConfig,
|
||||
target,
|
||||
resolveApiKey: vi.fn(),
|
||||
toApiKeyCredential: vi.fn(),
|
||||
});
|
||||
|
||||
@@ -4,13 +4,7 @@
|
||||
* This path resolves trusted plugin providers, delegates setup to their
|
||||
* non-interactive method, and installs runtime plugins required by the model.
|
||||
*/
|
||||
import {
|
||||
resolveAgentDir,
|
||||
resolveDefaultAgentId,
|
||||
resolveAgentWorkspaceDir,
|
||||
} from "../../../agents/agent-scope.js";
|
||||
import type { ApiKeyCredential } from "../../../agents/auth-profiles/types.js";
|
||||
import { resolveDefaultAgentWorkspaceDir } from "../../../agents/workspace.js";
|
||||
import { resolveAgentModelPrimaryValue } from "../../../config/model-input.js";
|
||||
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
|
||||
import { enablePluginInConfig } from "../../../plugins/enable.js";
|
||||
@@ -33,6 +27,7 @@ import {
|
||||
} from "../../codex-runtime-plugin-install.js";
|
||||
import { ensureCopilotRuntimePluginForModelSelection } from "../../copilot-runtime-plugin-install.js";
|
||||
import { createNonInteractiveLoggingPrompter } from "../../non-interactive-prompter.js";
|
||||
import type { OnboardingAgentTarget } from "../../onboard-agent-target.js";
|
||||
import type { OnboardOptions } from "../../onboard-types.js";
|
||||
|
||||
const PROVIDER_PLUGIN_CHOICE_PREFIX = "provider-plugin:";
|
||||
@@ -53,6 +48,7 @@ export async function applyNonInteractivePluginProviderChoice(params: {
|
||||
opts: OnboardOptions;
|
||||
runtime: RuntimeEnv;
|
||||
baseConfig: OpenClawConfig;
|
||||
target: OnboardingAgentTarget;
|
||||
resolveApiKey: (input: ProviderResolveNonInteractiveApiKeyParams) => Promise<{
|
||||
key: string;
|
||||
source: "profile" | "env" | "flag";
|
||||
@@ -62,10 +58,7 @@ export async function applyNonInteractivePluginProviderChoice(params: {
|
||||
input: ProviderNonInteractiveApiKeyCredentialParams,
|
||||
) => ApiKeyCredential | null;
|
||||
}): Promise<OpenClawConfig | null | undefined> {
|
||||
const agentId = resolveDefaultAgentId(params.nextConfig);
|
||||
const agentDir = resolveAgentDir(params.nextConfig, agentId);
|
||||
const workspaceDir =
|
||||
resolveAgentWorkspaceDir(params.nextConfig, agentId) ?? resolveDefaultAgentWorkspaceDir();
|
||||
const { agentDir, workspaceDir } = params.target;
|
||||
let nextConfig = params.nextConfig;
|
||||
const prefixedProviderId = params.authChoice.startsWith(PROVIDER_PLUGIN_CHOICE_PREFIX)
|
||||
? params.authChoice.slice(PROVIDER_PLUGIN_CHOICE_PREFIX.length).split(":", 1)[0]?.trim()
|
||||
|
||||
@@ -53,6 +53,12 @@ function createRuntime() {
|
||||
};
|
||||
}
|
||||
|
||||
const target = {
|
||||
agentId: "main",
|
||||
agentDir: "/tmp/main-agent",
|
||||
workspaceDir: "/tmp/workspace",
|
||||
};
|
||||
|
||||
describe("applyNonInteractiveAuthChoice", () => {
|
||||
it("rejects an unknown auth choice and lists the valid choices", async () => {
|
||||
const runtime = createRuntime();
|
||||
@@ -64,6 +70,7 @@ describe("applyNonInteractiveAuthChoice", () => {
|
||||
opts: {} as never,
|
||||
runtime: runtime as never,
|
||||
baseConfig: nextConfig,
|
||||
target,
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
@@ -85,9 +92,13 @@ describe("applyNonInteractiveAuthChoice", () => {
|
||||
opts: {} as never,
|
||||
runtime: runtime as never,
|
||||
baseConfig: nextConfig,
|
||||
target,
|
||||
});
|
||||
|
||||
expect(result).toBe(resolvedConfig);
|
||||
expect(applyNonInteractivePluginProviderChoice).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ target }),
|
||||
);
|
||||
expect(runtime.error).not.toHaveBeenCalled();
|
||||
expect(runtime.exit).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -104,6 +115,7 @@ describe("applyNonInteractiveAuthChoice", () => {
|
||||
opts: {} as never,
|
||||
runtime: runtime as never,
|
||||
baseConfig: nextConfig,
|
||||
target,
|
||||
});
|
||||
|
||||
expect(result).toBe(resolvedConfig);
|
||||
@@ -123,6 +135,7 @@ describe("applyNonInteractiveAuthChoice", () => {
|
||||
opts: {} as never,
|
||||
runtime: runtime as never,
|
||||
baseConfig: nextConfig,
|
||||
target,
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
@@ -146,6 +159,7 @@ describe("applyNonInteractiveAuthChoice", () => {
|
||||
opts: {} as never,
|
||||
runtime: runtime as never,
|
||||
baseConfig: nextConfig,
|
||||
target,
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
@@ -169,6 +183,7 @@ describe("applyNonInteractiveAuthChoice", () => {
|
||||
opts: {} as never,
|
||||
runtime: runtime as never,
|
||||
baseConfig: nextConfig,
|
||||
target,
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
@@ -198,6 +213,7 @@ describe("applyNonInteractiveAuthChoice", () => {
|
||||
} as never,
|
||||
runtime: runtime as never,
|
||||
baseConfig: nextConfig,
|
||||
target,
|
||||
});
|
||||
|
||||
expect(result?.models?.providers?.["custom-models-custom-local"]?.apiKey).toEqual({
|
||||
@@ -214,6 +230,7 @@ describe("applyNonInteractiveAuthChoice", () => {
|
||||
expect(apiKeyParams?.flagName).toBe("--custom-api-key");
|
||||
expect(apiKeyParams?.envVar).toBe("CUSTOM_API_KEY");
|
||||
expect(apiKeyParams?.envVarName).toBe("CUSTOM_API_KEY");
|
||||
expect(apiKeyParams?.agentDir).toBe(target.agentDir);
|
||||
expect(apiKeyParams?.secretInputMode).toBe("ref");
|
||||
});
|
||||
|
||||
@@ -232,6 +249,7 @@ describe("applyNonInteractiveAuthChoice", () => {
|
||||
} as never,
|
||||
runtime: runtime as never,
|
||||
baseConfig: nextConfig,
|
||||
target,
|
||||
});
|
||||
|
||||
expect(result?.models?.providers?.["custom-models-custom-local"]?.api).toBe("openai-responses");
|
||||
@@ -252,6 +270,7 @@ describe("applyNonInteractiveAuthChoice", () => {
|
||||
} as never,
|
||||
runtime: runtime as never,
|
||||
baseConfig: nextConfig,
|
||||
target,
|
||||
});
|
||||
|
||||
expect(result?.models?.providers?.["custom-models-custom-local"]?.models?.[0]?.input).toEqual([
|
||||
@@ -274,6 +293,7 @@ describe("applyNonInteractiveAuthChoice", () => {
|
||||
} as never,
|
||||
runtime: runtime as never,
|
||||
baseConfig: nextConfig,
|
||||
target,
|
||||
});
|
||||
|
||||
expect(result?.models?.providers?.["custom-models-custom-local"]?.models?.[0]?.input).toEqual([
|
||||
@@ -297,6 +317,7 @@ describe("applyNonInteractiveAuthChoice", () => {
|
||||
} as never,
|
||||
runtime: runtime as never,
|
||||
baseConfig: nextConfig,
|
||||
target,
|
||||
});
|
||||
|
||||
expect(result?.models?.providers?.["custom-models-custom-local"]?.models?.[0]?.input).toEqual([
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import { formatAuthChoiceChoicesForCli } from "../../auth-choice-options.js";
|
||||
import { normalizeSecretInputModeInput } from "../../auth-choice.apply-helpers.js";
|
||||
import { normalizeApiKeyTokenProviderAuthChoice } from "../../auth-choice.apply.api-providers.js";
|
||||
import type { OnboardingAgentTarget } from "../../onboard-agent-target.js";
|
||||
import {
|
||||
applyCustomApiConfig,
|
||||
CustomApiError,
|
||||
@@ -44,7 +45,7 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
opts: OnboardOptions;
|
||||
runtime: RuntimeEnv;
|
||||
baseConfig: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
target: OnboardingAgentTarget;
|
||||
}): Promise<OpenClawConfig | null> {
|
||||
const { opts, runtime, baseConfig } = params;
|
||||
let authChoice = normalizeApiKeyTokenProviderAuthChoice({
|
||||
@@ -93,6 +94,7 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
const resolveApiKey = (input: Parameters<typeof resolveNonInteractiveApiKey>[0]) =>
|
||||
resolveNonInteractiveApiKey({
|
||||
...input,
|
||||
agentDir: params.target.agentDir,
|
||||
secretInputMode: requestedSecretInputMode,
|
||||
});
|
||||
const toApiKeyCredential = (paramsLocal: {
|
||||
@@ -141,7 +143,7 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
if (
|
||||
isDeprecatedAuthChoice(authChoice, {
|
||||
config: nextConfig,
|
||||
workspaceDir: params.workspaceDir,
|
||||
workspaceDir: params.target.workspaceDir,
|
||||
env: process.env,
|
||||
})
|
||||
) {
|
||||
@@ -149,7 +151,7 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
// either plugin dispatch or built-in setup handling.
|
||||
const replacement = resolveDeprecatedAuthChoiceReplacement(authChoice, {
|
||||
config: nextConfig,
|
||||
workspaceDir: params.workspaceDir,
|
||||
workspaceDir: params.target.workspaceDir,
|
||||
env: process.env,
|
||||
});
|
||||
if (replacement) {
|
||||
@@ -159,7 +161,7 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
runtime.error(
|
||||
formatDeprecatedNonInteractiveAuthChoiceError(authChoice, {
|
||||
config: nextConfig,
|
||||
workspaceDir: params.workspaceDir,
|
||||
workspaceDir: params.target.workspaceDir,
|
||||
env: process.env,
|
||||
})!,
|
||||
);
|
||||
@@ -170,14 +172,14 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
|
||||
const deprecatedChoice = resolveManifestDeprecatedProviderAuthChoice(authChoice as string, {
|
||||
config: nextConfig,
|
||||
workspaceDir: params.workspaceDir,
|
||||
workspaceDir: params.target.workspaceDir,
|
||||
env: process.env,
|
||||
});
|
||||
const deprecatedInstallChoice = deprecatedChoice
|
||||
? undefined
|
||||
: resolveDeprecatedProviderInstallCatalogEntry(authChoice as string, {
|
||||
config: nextConfig,
|
||||
workspaceDir: params.workspaceDir,
|
||||
workspaceDir: params.target.workspaceDir,
|
||||
env: process.env,
|
||||
includeUntrustedWorkspacePlugins: false,
|
||||
});
|
||||
@@ -196,7 +198,7 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
includeLegacyAliases: false,
|
||||
includeSkip: true,
|
||||
config: nextConfig,
|
||||
workspaceDir: params.workspaceDir,
|
||||
workspaceDir: params.target.workspaceDir,
|
||||
env: process.env,
|
||||
}).split("|"),
|
||||
...GENERIC_NON_INTERACTIVE_AUTH_CHOICES,
|
||||
@@ -216,6 +218,7 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
opts,
|
||||
runtime,
|
||||
baseConfig,
|
||||
target: params.target,
|
||||
resolveApiKey: (input) =>
|
||||
resolveApiKey({
|
||||
...input,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Model picker flow lets users select provider models for config defaults.
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { sortUniqueStrings } from "@openclaw/normalization-core/string-normalization";
|
||||
import { resolveDefaultAgentDir } from "../agents/agent-scope.js";
|
||||
import { resolveAgentConfig, resolveDefaultAgentDir } from "../agents/agent-scope.js";
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||
import { resolveAgentHarnessPolicy } from "../agents/harness/policy.js";
|
||||
import {
|
||||
@@ -107,6 +107,7 @@ type PromptDefaultModelParams = {
|
||||
loadCatalog?: boolean;
|
||||
browseCatalogOnDemand?: boolean;
|
||||
preferredProvider?: string;
|
||||
agentId?: string;
|
||||
agentDir?: string;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
@@ -137,6 +138,26 @@ function resolveConfiguredModelKeys(cfg: OpenClawConfig): string[] {
|
||||
.filter((key) => key.length > 0);
|
||||
}
|
||||
|
||||
function resolveModelPickerConfig(cfg: OpenClawConfig, agentId?: string): OpenClawConfig {
|
||||
const agent = agentId ? resolveAgentConfig(cfg, agentId) : undefined;
|
||||
if (agent?.model === undefined && agent?.models === undefined) {
|
||||
return cfg;
|
||||
}
|
||||
return {
|
||||
...cfg,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
...(agent.model !== undefined ? { model: agent.model } : {}),
|
||||
...(agent.models !== undefined
|
||||
? { models: { ...cfg.agents?.defaults?.models, ...agent.models } }
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function toPickerCatalogEntry(
|
||||
row: ReturnType<typeof loadStaticManifestCatalogRowsForList>[number],
|
||||
): ModelCatalogEntry {
|
||||
@@ -783,6 +804,7 @@ export async function promptDefaultModel(
|
||||
params: PromptDefaultModelParams,
|
||||
): Promise<PromptDefaultModelResult> {
|
||||
const cfg = params.config;
|
||||
const pickerConfig = resolveModelPickerConfig(cfg, params.agentId);
|
||||
const pickerAgentDir = resolvePickerAgentDir({
|
||||
cfg,
|
||||
...(params.agentDir !== undefined ? { agentDir: params.agentDir } : {}),
|
||||
@@ -798,10 +820,10 @@ export async function promptDefaultModel(
|
||||
const preferredProvider = preferredProviderRaw
|
||||
? normalizeProviderId(preferredProviderRaw)
|
||||
: undefined;
|
||||
const configuredRaw = resolveConfiguredModelRaw(cfg);
|
||||
const configuredRaw = resolveConfiguredModelRaw(pickerConfig);
|
||||
const useStaticModelNormalization = !loadCatalog || browseCatalogOnDemand;
|
||||
const resolved = resolveConfiguredModelRef({
|
||||
cfg,
|
||||
cfg: pickerConfig,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
allowPluginNormalization: useStaticModelNormalization ? false : undefined,
|
||||
@@ -954,7 +976,7 @@ export async function promptDefaultModel(
|
||||
}
|
||||
|
||||
const aliasIndex = buildModelAliasIndex({
|
||||
cfg,
|
||||
cfg: pickerConfig,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
});
|
||||
const hasAuth = createProviderAuthChecker({
|
||||
@@ -968,7 +990,7 @@ export async function promptDefaultModel(
|
||||
env: params.env,
|
||||
});
|
||||
const models = await resolvePickerLogicalCatalog({
|
||||
cfg,
|
||||
cfg: pickerConfig,
|
||||
catalog,
|
||||
routeVariants: catalogSnapshot.routeVariants,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
@@ -1733,4 +1755,5 @@ function mergeFallbackSelection(params: {
|
||||
}
|
||||
return fallbacks;
|
||||
}
|
||||
|
||||
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
|
||||
|
||||
@@ -348,11 +348,18 @@ describe("applySystemAgentSetup transaction boundaries", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps an existing fleet workspace for unconfirmed setup apply", async () => {
|
||||
it("keeps the fleet workspace and provisions the configured default agent", async () => {
|
||||
const config = {
|
||||
agents: {
|
||||
defaults: { workspace: "/tmp/current-workspace" },
|
||||
entries: { main: {}, ops: {} },
|
||||
entries: {
|
||||
main: {},
|
||||
ops: {
|
||||
default: true,
|
||||
agentDir: "/agents/ops",
|
||||
workspace: "/tmp/ops-workspace",
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
mocks.state.initialSnapshot = snapshot("probe", config);
|
||||
@@ -369,11 +376,11 @@ describe("applySystemAgentSetup transaction boundaries", () => {
|
||||
);
|
||||
|
||||
expect(mocks.state.persistedConfig?.agents?.defaults?.workspace).toBe("/tmp/current-workspace");
|
||||
expect(mocks.state.persistedConfig?.agents?.entries).toEqual({ main: {}, ops: {} });
|
||||
expect(mocks.state.persistedConfig?.agents?.entries).toEqual(config.agents.entries);
|
||||
expect(mocks.ensureWorkspace).toHaveBeenCalledWith(
|
||||
"/tmp/current-workspace",
|
||||
"/tmp/ops-workspace",
|
||||
runtime,
|
||||
expect.any(Object),
|
||||
expect.objectContaining({ agentId: "ops" }),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Applies OpenClaw's conversational setup: config, workspace files, gateway.
|
||||
import { isDeepStrictEqual } from "node:util";
|
||||
import { listAgentEntries } from "../agents/agent-scope-config.js";
|
||||
import { resolveOnboardingAgentTarget } from "../commands/onboard-agent-target.js";
|
||||
import {
|
||||
readConfigFileSnapshot,
|
||||
readConfigFileSnapshotWithPluginMetadata,
|
||||
@@ -416,10 +417,6 @@ export async function applySystemAgentSetup(
|
||||
};
|
||||
}
|
||||
|
||||
const effectiveWorkspace =
|
||||
workspaceConflict && !params.allowWorkspaceChange
|
||||
? workspaceConflict.currentWorkspaceDir
|
||||
: workspace;
|
||||
let candidate = applyLocalSetupWorkspaceConfig(setupBaseConfig, workspace, {
|
||||
allowWorkspaceChange: allowWorkspaceWrite,
|
||||
preserveWorkspace,
|
||||
@@ -448,7 +445,6 @@ export async function applySystemAgentSetup(
|
||||
mode: "local",
|
||||
}),
|
||||
settings: gateway.settings,
|
||||
workspace: effectiveWorkspace,
|
||||
};
|
||||
};
|
||||
const committed = await commit(
|
||||
@@ -493,7 +489,6 @@ export async function applySystemAgentSetup(
|
||||
nextConfig: finalizedConfig,
|
||||
result: {
|
||||
settings: setupCandidate.settings,
|
||||
workspace: setupCandidate.workspace,
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -505,7 +500,7 @@ export async function applySystemAgentSetup(
|
||||
if (!settings) {
|
||||
throw new Error("OpenClaw setup committed without resolved Gateway settings.");
|
||||
}
|
||||
const effectiveWorkspace = setupResult.workspace;
|
||||
const onboardingTarget = resolveOnboardingAgentTarget(nextConfig);
|
||||
if (params.expectedInferenceRoute) {
|
||||
const afterRead = await readConfigFileSnapshotWithPluginMetadata();
|
||||
const afterSnapshot = afterRead.snapshot;
|
||||
@@ -533,7 +528,7 @@ export async function applySystemAgentSetup(
|
||||
}
|
||||
|
||||
const lines: string[] = [
|
||||
`Workspace: ${shortenHomePath(effectiveWorkspace)}`,
|
||||
`Workspace: ${shortenHomePath(onboardingTarget.workspaceDir)}`,
|
||||
model ? `Default model: ${model}` : undefined,
|
||||
].filter((line): line is string => line !== undefined);
|
||||
|
||||
@@ -561,9 +556,10 @@ export async function applySystemAgentSetup(
|
||||
|
||||
const workspaceResult = await runCommittedFollowUp(
|
||||
async () =>
|
||||
await onboardHelpers.ensureWorkspaceAndSessions(effectiveWorkspace, runtime, {
|
||||
await onboardHelpers.ensureWorkspaceAndSessions(onboardingTarget.workspaceDir, runtime, {
|
||||
skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap),
|
||||
skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles,
|
||||
agentId: onboardingTarget.agentId,
|
||||
}),
|
||||
(error) => lines.push(`Workspace files: ${formatErrorMessage(error)}`),
|
||||
);
|
||||
@@ -600,7 +596,7 @@ export async function applySystemAgentSetup(
|
||||
await refreshPluginRegistryAfterConfigMutation({
|
||||
config: nextConfig,
|
||||
reason: "source-changed",
|
||||
workspaceDir: effectiveWorkspace,
|
||||
workspaceDir: onboardingTarget.workspaceDir,
|
||||
traceCommand: "openclaw-setup",
|
||||
logger: {
|
||||
warn: (message) => lines.push(message),
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
// Classic setup tests keep every workspace-owned effect on the configured default agent.
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import type { WizardPrompter } from "./prompts.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
readSnapshot: vi.fn(),
|
||||
writeConfig: vi.fn(),
|
||||
ensureWorkspaceAndSessions: vi.fn(),
|
||||
setupSkills: vi.fn(),
|
||||
setupOfficialPlugins: vi.fn(),
|
||||
setupRecommendations: vi.fn(),
|
||||
setupPluginConfig: vi.fn(),
|
||||
finalizeSetup: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./navigation-prompter.js", () => ({
|
||||
runWizardWithPromptNavigation: async (
|
||||
prompter: WizardPrompter,
|
||||
run: (prompter: WizardPrompter) => Promise<void>,
|
||||
) => await run(prompter),
|
||||
}));
|
||||
|
||||
vi.mock("./setup.shared.js", () => ({
|
||||
readSetupConfigFileSnapshot: mocks.readSnapshot,
|
||||
readValidSetupConfigFile: vi.fn(),
|
||||
requireRiskAcknowledgement: async ({ config }: { config: OpenClawConfig }) => config,
|
||||
resolveQuickstartGatewayDefaults: () => ({
|
||||
hasExisting: false,
|
||||
port: 18789,
|
||||
bind: "loopback",
|
||||
authMode: "token",
|
||||
tailscaleMode: "off",
|
||||
}),
|
||||
writeWizardConfigFile: mocks.writeConfig,
|
||||
}));
|
||||
|
||||
vi.mock("./setup.migration-import.js", () => ({
|
||||
detectSetupMigrationSources: vi.fn(async () => []),
|
||||
listSetupMigrationOptions: vi.fn(async () => []),
|
||||
runSetupMigrationImport: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./setup.model-auth.js", () => ({
|
||||
runSetupModelAuthStep: async ({ config }: { config: OpenClawConfig }) => ({
|
||||
config,
|
||||
authProfiles: [],
|
||||
persistAuthProfiles: async () => {},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("./setup.workspace.js", () => ({
|
||||
resolveSetupWorkspaceSelection: async () => ({
|
||||
workspaceDir: "/tmp/global-workspace",
|
||||
allowWorkspaceChange: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("./setup.secret-input.js", () => ({
|
||||
resolveSetupSecretInputString: vi.fn(async () => undefined),
|
||||
}));
|
||||
|
||||
vi.mock("./setup.gateway-config.js", () => ({
|
||||
configureGatewayForSetup: async ({ nextConfig }: { nextConfig: OpenClawConfig }) => ({
|
||||
nextConfig,
|
||||
settings: {
|
||||
port: 18789,
|
||||
bind: "loopback",
|
||||
authMode: "token",
|
||||
gatewayToken: "test-token",
|
||||
tailscaleMode: "off",
|
||||
tailscaleResetOnExit: false,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("./setup.memory-import.js", () => ({
|
||||
runSetupMemoryImportStep: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./setup.official-plugins.js", () => ({
|
||||
setupOfficialPluginInstalls: mocks.setupOfficialPlugins,
|
||||
}));
|
||||
|
||||
vi.mock("./setup.app-recommendations.js", () => ({
|
||||
setupAppRecommendations: mocks.setupRecommendations,
|
||||
}));
|
||||
|
||||
vi.mock("./setup.plugin-config.js", () => ({
|
||||
setupPluginConfig: mocks.setupPluginConfig,
|
||||
}));
|
||||
|
||||
vi.mock("./setup.finalize.js", () => ({
|
||||
finalizeSetupWizard: mocks.finalizeSetup,
|
||||
}));
|
||||
|
||||
vi.mock("../commands/onboard-helpers.js", () => ({
|
||||
DEFAULT_WORKSPACE: "/tmp/default-workspace",
|
||||
applyWizardMetadata: (config: OpenClawConfig) => config,
|
||||
ensureWorkspaceAndSessions: mocks.ensureWorkspaceAndSessions,
|
||||
printWizardHeader: vi.fn(),
|
||||
probeGatewayReachable: vi.fn(async () => ({ ok: false })),
|
||||
summarizeExistingConfig: vi.fn(() => ""),
|
||||
}));
|
||||
|
||||
vi.mock("../commands/onboard-skills.js", () => ({ setupSkills: mocks.setupSkills }));
|
||||
vi.mock("../config/config.js", () => ({ resolveGatewayPort: () => 18789 }));
|
||||
vi.mock("../config/logging.js", () => ({ logConfigUpdated: vi.fn() }));
|
||||
vi.mock("../plugins/status.js", () => ({
|
||||
buildPluginCompatibilitySnapshotNotices: vi.fn(() => []),
|
||||
formatPluginCompatibilityNotice: vi.fn(),
|
||||
}));
|
||||
|
||||
import { runSetupWizard } from "./setup.js";
|
||||
|
||||
const runtime = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
} as unknown as RuntimeEnv;
|
||||
|
||||
const prompter = {
|
||||
intro: vi.fn(),
|
||||
outro: vi.fn(),
|
||||
note: vi.fn(),
|
||||
select: vi.fn(),
|
||||
multiselect: vi.fn(),
|
||||
text: vi.fn(),
|
||||
confirm: vi.fn(),
|
||||
progress: vi.fn(() => ({ update: vi.fn(), stop: vi.fn() })),
|
||||
} as unknown as WizardPrompter;
|
||||
|
||||
describe("runSetupWizard default-agent ownership", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
const config = {
|
||||
wizard: { securityAcknowledgedAt: "2026-07-01T00:00:00.000Z" },
|
||||
agents: {
|
||||
defaults: { workspace: "/tmp/global-workspace" },
|
||||
entries: {
|
||||
ops: {
|
||||
default: true,
|
||||
agentDir: "/tmp/ops-agent",
|
||||
workspace: "/tmp/ops-workspace",
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
mocks.readSnapshot.mockResolvedValue({
|
||||
exists: true,
|
||||
valid: true,
|
||||
config,
|
||||
sourceConfig: config,
|
||||
issues: [],
|
||||
});
|
||||
mocks.writeConfig.mockImplementation(async (nextConfig: OpenClawConfig) => nextConfig);
|
||||
mocks.setupSkills.mockImplementation(async (nextConfig: OpenClawConfig) => nextConfig);
|
||||
mocks.setupOfficialPlugins.mockImplementation(
|
||||
async ({ config: nextConfig }: { config: OpenClawConfig }) => nextConfig,
|
||||
);
|
||||
mocks.setupRecommendations.mockImplementation(
|
||||
async ({ config: nextConfig }: { config: OpenClawConfig }) => ({ config: nextConfig }),
|
||||
);
|
||||
mocks.setupPluginConfig.mockImplementation(
|
||||
async ({ config: nextConfig }: { config: OpenClawConfig }) => nextConfig,
|
||||
);
|
||||
mocks.finalizeSetup.mockResolvedValue({ launchedTui: false });
|
||||
});
|
||||
|
||||
it("uses the keyed default-agent workspace for all classic agent-owned effects", async () => {
|
||||
await runSetupWizard(
|
||||
{
|
||||
acceptRisk: true,
|
||||
flow: "advanced",
|
||||
mode: "local",
|
||||
workspace: "/tmp/global-workspace",
|
||||
authChoice: "skip",
|
||||
installDaemon: false,
|
||||
skipChannels: true,
|
||||
skipSearch: true,
|
||||
skipHealth: true,
|
||||
skipHooks: true,
|
||||
skipUi: true,
|
||||
},
|
||||
runtime,
|
||||
prompter,
|
||||
);
|
||||
|
||||
expect(mocks.writeConfig).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
agents: expect.objectContaining({
|
||||
defaults: expect.objectContaining({ workspace: "/tmp/global-workspace" }),
|
||||
}),
|
||||
}),
|
||||
expect.any(Object),
|
||||
);
|
||||
const target = {
|
||||
agentId: "ops",
|
||||
agentDir: "/tmp/ops-agent",
|
||||
workspaceDir: "/tmp/ops-workspace",
|
||||
};
|
||||
expect(mocks.ensureWorkspaceAndSessions).toHaveBeenCalledWith(
|
||||
target.workspaceDir,
|
||||
runtime,
|
||||
expect.objectContaining({ agentId: target.agentId }),
|
||||
);
|
||||
expect(mocks.setupSkills).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
target.workspaceDir,
|
||||
runtime,
|
||||
prompter,
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(mocks.setupOfficialPlugins).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ workspaceDir: target.workspaceDir }),
|
||||
);
|
||||
expect(mocks.setupRecommendations).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ workspaceDir: target.workspaceDir }),
|
||||
);
|
||||
expect(mocks.setupPluginConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ workspaceDir: target.workspaceDir }),
|
||||
);
|
||||
expect(mocks.finalizeSetup).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ workspaceDir: target.workspaceDir }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
// Regression tests: provider auth failures re-prompt instead of killing the wizard.
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { WizardCancelledError, type WizardPrompter } from "./prompts.js";
|
||||
import { runSetupModelAuthStep } from "./setup.model-auth.js";
|
||||
@@ -10,6 +11,7 @@ const resolvePreferredProviderForAuthChoice = vi.hoisted(() => vi.fn());
|
||||
const promptDefaultModel = vi.hoisted(() => vi.fn());
|
||||
const applyPrimaryModel = vi.hoisted(() => vi.fn((config: unknown) => config));
|
||||
const promptAuthChoiceGrouped = vi.hoisted(() => vi.fn());
|
||||
const ensureAuthProfileStore = vi.hoisted(() => vi.fn(() => ({ profiles: {} })));
|
||||
|
||||
vi.mock("../commands/auth-choice.js", () => ({
|
||||
applyAuthChoice,
|
||||
@@ -29,7 +31,7 @@ vi.mock("../commands/auth-choice-prompt.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../agents/auth-profiles.runtime.js", () => ({
|
||||
ensureAuthProfileStore: vi.fn(() => ({ profiles: {} })),
|
||||
ensureAuthProfileStore,
|
||||
}));
|
||||
|
||||
function createPrompter(): WizardPrompter {
|
||||
@@ -50,13 +52,112 @@ function createRuntime(): RuntimeEnv {
|
||||
return { log: vi.fn(), error: vi.fn(), exit: vi.fn() } as unknown as RuntimeEnv;
|
||||
}
|
||||
|
||||
describe("runSetupModelAuthStep provider failures", () => {
|
||||
function createDefaultAgentConfig(): OpenClawConfig {
|
||||
return {
|
||||
agents: {
|
||||
defaults: { workspace: "/tmp/global-workspace" },
|
||||
entries: {
|
||||
ops: {
|
||||
default: true,
|
||||
agentDir: "/tmp/ops-agent",
|
||||
workspace: "/tmp/ops-workspace",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("runSetupModelAuthStep", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
promptDefaultModel.mockResolvedValue({});
|
||||
warnIfModelConfigLooksOff.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it("targets the configured default agent for auth and model setup", async () => {
|
||||
const config = createDefaultAgentConfig();
|
||||
promptAuthChoiceGrouped.mockResolvedValueOnce("anthropic-cli");
|
||||
applyAuthChoice.mockResolvedValueOnce({
|
||||
config,
|
||||
authProfiles: [],
|
||||
persistAuthProfiles: async () => {},
|
||||
});
|
||||
|
||||
await runSetupModelAuthStep({
|
||||
config,
|
||||
opts: {},
|
||||
prompter: createPrompter(),
|
||||
runtime: createRuntime(),
|
||||
});
|
||||
|
||||
expect(ensureAuthProfileStore).toHaveBeenCalledWith("/tmp/ops-agent", {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
expect(promptAuthChoiceGrouped).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ workspaceDir: "/tmp/ops-workspace" }),
|
||||
);
|
||||
expect(applyAuthChoice).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentId: "ops",
|
||||
agentDir: "/tmp/ops-agent",
|
||||
}),
|
||||
);
|
||||
expect(promptDefaultModel).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentId: "ops",
|
||||
agentDir: "/tmp/ops-agent",
|
||||
workspaceDir: "/tmp/ops-workspace",
|
||||
}),
|
||||
);
|
||||
expect(warnIfModelConfigLooksOff).toHaveBeenCalledWith(expect.anything(), expect.anything(), {
|
||||
agentId: "ops",
|
||||
agentDir: "/tmp/ops-agent",
|
||||
validateCatalog: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("validates an interactive skip against the configured default agent", async () => {
|
||||
const config = createDefaultAgentConfig();
|
||||
promptAuthChoiceGrouped.mockResolvedValueOnce("skip");
|
||||
|
||||
await runSetupModelAuthStep({
|
||||
config,
|
||||
opts: {},
|
||||
prompter: createPrompter(),
|
||||
runtime: createRuntime(),
|
||||
});
|
||||
|
||||
expect(warnIfModelConfigLooksOff).toHaveBeenCalledWith(expect.anything(), expect.anything(), {
|
||||
agentId: "ops",
|
||||
agentDir: "/tmp/ops-agent",
|
||||
validateCatalog: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("applies an interactive model selection to the agent override", async () => {
|
||||
const config = createDefaultAgentConfig();
|
||||
config.agents!.defaults!.model = "openai/global-model";
|
||||
config.agents!.entries!.ops!.model = {
|
||||
primary: "anthropic/old-model",
|
||||
fallbacks: ["openai/fallback-model"],
|
||||
};
|
||||
promptAuthChoiceGrouped.mockResolvedValueOnce("skip");
|
||||
promptDefaultModel.mockResolvedValueOnce({ model: "google/new-model" });
|
||||
|
||||
const result = await runSetupModelAuthStep({
|
||||
config,
|
||||
opts: {},
|
||||
prompter: createPrompter(),
|
||||
runtime: createRuntime(),
|
||||
});
|
||||
|
||||
expect(result.config.agents?.entries?.ops?.model).toEqual({
|
||||
primary: "google/new-model",
|
||||
fallbacks: ["openai/fallback-model"],
|
||||
});
|
||||
expect(result.config.agents?.defaults?.model).toBe("openai/global-model");
|
||||
});
|
||||
|
||||
it("re-prompts after a provider setup error instead of aborting", async () => {
|
||||
promptAuthChoiceGrouped.mockResolvedValueOnce("anthropic-cli").mockResolvedValueOnce("skip");
|
||||
applyAuthChoice.mockRejectedValueOnce(
|
||||
@@ -69,7 +170,6 @@ describe("runSetupModelAuthStep provider failures", () => {
|
||||
opts: {},
|
||||
prompter,
|
||||
runtime: createRuntime(),
|
||||
workspaceDir: "/tmp/workspace",
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -95,7 +195,6 @@ describe("runSetupModelAuthStep provider failures", () => {
|
||||
opts: { authChoice: "anthropic-cli" },
|
||||
prompter: createPrompter(),
|
||||
runtime: createRuntime(),
|
||||
workspaceDir: "/tmp/workspace",
|
||||
}),
|
||||
).rejects.toThrow("Claude CLI is not authenticated");
|
||||
});
|
||||
@@ -110,7 +209,6 @@ describe("runSetupModelAuthStep provider failures", () => {
|
||||
opts: {},
|
||||
prompter: createPrompter(),
|
||||
runtime: createRuntime(),
|
||||
workspaceDir: "/tmp/workspace",
|
||||
}),
|
||||
).rejects.toThrow(WizardCancelledError);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
// Model/auth provider selection step shared by the classic wizard and bootstrap onboarding.
|
||||
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
|
||||
import {
|
||||
applyOnboardingPrimaryModel,
|
||||
resolveOnboardingAgentTarget,
|
||||
} from "../commands/onboard-agent-target.js";
|
||||
import type { AuthChoice, OnboardOptions } from "../commands/onboard-types.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
@@ -127,9 +131,8 @@ export async function runSetupModelAuthStep(params: {
|
||||
opts: OnboardOptions;
|
||||
prompter: WizardPrompter;
|
||||
runtime: RuntimeEnv;
|
||||
workspaceDir: string;
|
||||
}): Promise<SetupModelAuthCandidate> {
|
||||
const { opts, prompter, runtime, workspaceDir } = params;
|
||||
const { opts, prompter, runtime } = params;
|
||||
let nextConfig = params.stagedCandidate?.config ?? params.config;
|
||||
let replacementBaseConfig = params.config;
|
||||
let authProfiles: PreparedAuthChoiceResult["authProfiles"] =
|
||||
@@ -150,18 +153,20 @@ export async function runSetupModelAuthStep(params: {
|
||||
const authChoicePromptModule = await import("../commands/auth-choice-prompt.js");
|
||||
promptAuthChoiceGrouped = authChoicePromptModule.promptAuthChoiceGrouped;
|
||||
keepCurrentAuthChoice = authChoicePromptModule.KEEP_CURRENT_AUTH_CHOICE;
|
||||
authStore = ensureAuthProfileStore(undefined, {
|
||||
const target = resolveOnboardingAgentTarget(nextConfig);
|
||||
authStore = ensureAuthProfileStore(target.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
}
|
||||
while (true) {
|
||||
if (authChoiceFromPrompt) {
|
||||
const target = resolveOnboardingAgentTarget(nextConfig);
|
||||
authChoice = await promptAuthChoiceGrouped!({
|
||||
prompter,
|
||||
store: authStore!,
|
||||
includeSkip: true,
|
||||
config: nextConfig,
|
||||
workspaceDir,
|
||||
workspaceDir: target.workspaceDir,
|
||||
allowKeepCurrentProvider: true,
|
||||
});
|
||||
}
|
||||
@@ -193,7 +198,8 @@ export async function runSetupModelAuthStep(params: {
|
||||
// Explicit skip should stay cold: do not bootstrap auth/profile machinery
|
||||
// or run model/auth checks when the caller already chose to skip setup.
|
||||
if (authChoiceFromPrompt) {
|
||||
const { applyPrimaryModel, promptDefaultModel } = await loadModelPickerModule();
|
||||
const { promptDefaultModel } = await loadModelPickerModule();
|
||||
const target = resolveOnboardingAgentTarget(nextConfig);
|
||||
const modelSelection = await promptDefaultModel({
|
||||
config: nextConfig,
|
||||
prompter,
|
||||
@@ -201,27 +207,35 @@ export async function runSetupModelAuthStep(params: {
|
||||
ignoreAllowlist: true,
|
||||
includeProviderPluginSetups: false,
|
||||
loadCatalog: false,
|
||||
workspaceDir,
|
||||
agentId: target.agentId,
|
||||
agentDir: target.agentDir,
|
||||
workspaceDir: target.workspaceDir,
|
||||
runtime,
|
||||
});
|
||||
if (modelSelection.config) {
|
||||
nextConfig = modelSelection.config;
|
||||
}
|
||||
if (modelSelection.model) {
|
||||
nextConfig = applyPrimaryModel(nextConfig, modelSelection.model);
|
||||
nextConfig = applyOnboardingPrimaryModel(nextConfig, target, modelSelection.model);
|
||||
}
|
||||
|
||||
const { warnIfModelConfigLooksOff } = await loadAuthChoiceModule();
|
||||
await warnIfModelConfigLooksOff(nextConfig, prompter, { validateCatalog: false });
|
||||
const validationTarget = resolveOnboardingAgentTarget(nextConfig);
|
||||
await warnIfModelConfigLooksOff(nextConfig, prompter, {
|
||||
agentId: validationTarget.agentId,
|
||||
agentDir: validationTarget.agentDir,
|
||||
validateCatalog: false,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const [
|
||||
{ prepareAuthChoice, resolvePreferredProviderForAuthChoice, warnIfModelConfigLooksOff },
|
||||
{ applyPrimaryModel, promptDefaultModel },
|
||||
{ promptDefaultModel },
|
||||
] = await Promise.all([loadAuthChoiceModule(), loadModelPickerModule()]);
|
||||
prompter.disableBackNavigation?.();
|
||||
const target = resolveOnboardingAgentTarget(nextConfig);
|
||||
let authResult: PreparedAuthChoiceResult;
|
||||
try {
|
||||
authResult = await prepareAuthChoice({
|
||||
@@ -229,6 +243,8 @@ export async function runSetupModelAuthStep(params: {
|
||||
config: nextConfig,
|
||||
prompter,
|
||||
runtime,
|
||||
agentId: target.agentId,
|
||||
agentDir: target.agentDir,
|
||||
setDefaultModel: true,
|
||||
preserveExistingDefaultModel: true,
|
||||
opts: {
|
||||
@@ -260,13 +276,19 @@ export async function runSetupModelAuthStep(params: {
|
||||
break;
|
||||
}
|
||||
if (authResult.agentModelOverride) {
|
||||
nextConfig = applyPrimaryModel(nextConfig, authResult.agentModelOverride);
|
||||
const overrideTarget = resolveOnboardingAgentTarget(nextConfig);
|
||||
nextConfig = applyOnboardingPrimaryModel(
|
||||
nextConfig,
|
||||
overrideTarget,
|
||||
authResult.agentModelOverride,
|
||||
);
|
||||
}
|
||||
|
||||
const updatedTarget = resolveOnboardingAgentTarget(nextConfig);
|
||||
const authChoiceModelSelectionPolicy = await resolveAuthChoiceModelSelectionPolicy({
|
||||
authChoice,
|
||||
config: nextConfig,
|
||||
workspaceDir,
|
||||
workspaceDir: updatedTarget.workspaceDir,
|
||||
resolvePreferredProviderForAuthChoice,
|
||||
});
|
||||
const shouldPromptModelSelection =
|
||||
@@ -280,18 +302,25 @@ export async function runSetupModelAuthStep(params: {
|
||||
includeProviderPluginSetups: true,
|
||||
preferredProvider: authChoiceModelSelectionPolicy?.preferredProvider,
|
||||
browseCatalogOnDemand: true,
|
||||
workspaceDir,
|
||||
agentId: updatedTarget.agentId,
|
||||
agentDir: updatedTarget.agentDir,
|
||||
workspaceDir: updatedTarget.workspaceDir,
|
||||
runtime,
|
||||
});
|
||||
if (modelSelection.config) {
|
||||
nextConfig = modelSelection.config;
|
||||
}
|
||||
if (modelSelection.model) {
|
||||
nextConfig = applyPrimaryModel(nextConfig, modelSelection.model);
|
||||
nextConfig = applyOnboardingPrimaryModel(nextConfig, updatedTarget, modelSelection.model);
|
||||
}
|
||||
}
|
||||
|
||||
await warnIfModelConfigLooksOff(nextConfig, prompter, { validateCatalog: false });
|
||||
const validationTarget = resolveOnboardingAgentTarget(nextConfig);
|
||||
await warnIfModelConfigLooksOff(nextConfig, prompter, {
|
||||
agentId: validationTarget.agentId,
|
||||
agentDir: validationTarget.agentDir,
|
||||
validateCatalog: false,
|
||||
});
|
||||
break;
|
||||
}
|
||||
return { config: nextConfig, authProfiles, persistAuthProfiles };
|
||||
|
||||
+11
-11
@@ -1,5 +1,6 @@
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import { resolveOnboardingAgentTarget } from "../commands/onboard-agent-target.js";
|
||||
import type { GatewayAuthChoice, OnboardMode, OnboardOptions } from "../commands/onboard-types.js";
|
||||
import { resolveGatewayPort } from "../config/config.js";
|
||||
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
|
||||
@@ -53,7 +54,6 @@ async function offerLiveModelVerification(params: {
|
||||
opts: OnboardOptions;
|
||||
prompter: WizardPrompter;
|
||||
runtime: RuntimeEnv;
|
||||
workspaceDir: string;
|
||||
writeConfig: (config: OpenClawConfig) => Promise<OpenClawConfig>;
|
||||
required?: boolean;
|
||||
}): Promise<{ config: OpenClawConfig; verified: boolean }> {
|
||||
@@ -130,7 +130,6 @@ async function offerLiveModelVerification(params: {
|
||||
opts: { ...params.opts, authChoice: undefined },
|
||||
prompter: params.prompter,
|
||||
runtime: params.runtime,
|
||||
workspaceDir: params.workspaceDir,
|
||||
});
|
||||
shouldPersistCandidate = true;
|
||||
}
|
||||
@@ -566,7 +565,7 @@ async function runSetupWizardOnce(
|
||||
|
||||
const { applyLocalSetupWorkspaceConfig, applySkipBootstrapConfig } =
|
||||
await loadOnboardConfigModule();
|
||||
const { workspaceDir, allowWorkspaceChange } = await resolveSetupWorkspaceSelection({
|
||||
const { allowWorkspaceChange } = await resolveSetupWorkspaceSelection({
|
||||
baseConfig,
|
||||
requestedWorkspaceDir,
|
||||
prompter,
|
||||
@@ -586,7 +585,6 @@ async function runSetupWizardOnce(
|
||||
opts,
|
||||
prompter,
|
||||
runtime,
|
||||
workspaceDir,
|
||||
});
|
||||
await modelAuth.persistAuthProfiles();
|
||||
nextConfig = modelAuth.config;
|
||||
@@ -626,7 +624,6 @@ async function runSetupWizardOnce(
|
||||
opts,
|
||||
prompter,
|
||||
runtime,
|
||||
workspaceDir,
|
||||
writeConfig: async (config) =>
|
||||
await writeSetupConfigFile(config, { allowConfigSizeDrop: false }),
|
||||
required: usedImportFlow && keepExistingModelConfig,
|
||||
@@ -662,11 +659,13 @@ async function runSetupWizardOnce(
|
||||
nextConfig = await writeSetupConfigFile(nextConfig, {
|
||||
allowConfigSizeDrop: false,
|
||||
});
|
||||
let onboardingTarget = resolveOnboardingAgentTarget(nextConfig);
|
||||
const { logConfigUpdated } = await loadConfigLoggingModule();
|
||||
logConfigUpdated(runtime);
|
||||
await onboardHelpers.ensureWorkspaceAndSessions(workspaceDir, runtime, {
|
||||
await onboardHelpers.ensureWorkspaceAndSessions(onboardingTarget.workspaceDir, runtime, {
|
||||
skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap),
|
||||
skipOptionalBootstrapFiles: nextConfig.agents?.defaults?.skipOptionalBootstrapFiles,
|
||||
agentId: onboardingTarget.agentId,
|
||||
});
|
||||
|
||||
if (!usedImportFlow) {
|
||||
@@ -688,7 +687,7 @@ async function runSetupWizardOnce(
|
||||
await prompter.note(t("wizard.setup.skipSkills"), t("wizard.setup.skillsTitle"));
|
||||
} else {
|
||||
const { setupSkills } = await import("../commands/onboard-skills.js");
|
||||
nextConfig = await setupSkills(nextConfig, workspaceDir, runtime, prompter, {
|
||||
nextConfig = await setupSkills(nextConfig, onboardingTarget.workspaceDir, runtime, prompter, {
|
||||
nodeManager: opts.nodeManager,
|
||||
});
|
||||
}
|
||||
@@ -700,14 +699,14 @@ async function runSetupWizardOnce(
|
||||
config: nextConfig,
|
||||
prompter,
|
||||
runtime,
|
||||
workspaceDir,
|
||||
workspaceDir: onboardingTarget.workspaceDir,
|
||||
});
|
||||
const { setupAppRecommendations } = await import("./setup.app-recommendations.js");
|
||||
const recommendationOutcome = await setupAppRecommendations({
|
||||
config: nextConfig,
|
||||
prompter,
|
||||
runtime,
|
||||
workspaceDir,
|
||||
workspaceDir: onboardingTarget.workspaceDir,
|
||||
modelRouteVerified: liveModelVerified,
|
||||
});
|
||||
nextConfig = recommendationOutcome.config;
|
||||
@@ -716,7 +715,7 @@ async function runSetupWizardOnce(
|
||||
nextConfig = await setupPluginConfig({
|
||||
config: nextConfig,
|
||||
prompter,
|
||||
workspaceDir,
|
||||
workspaceDir: onboardingTarget.workspaceDir,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -729,6 +728,7 @@ async function runSetupWizardOnce(
|
||||
nextConfig = await writeSetupConfigFile(nextConfig, {
|
||||
allowConfigSizeDrop: false,
|
||||
});
|
||||
onboardingTarget = resolveOnboardingAgentTarget(nextConfig);
|
||||
commitAppRecommendationResult?.();
|
||||
|
||||
const { finalizeSetupWizard } = await import("./setup.finalize.js");
|
||||
@@ -738,7 +738,7 @@ async function runSetupWizardOnce(
|
||||
baseConfig,
|
||||
hadExistingConfig: snapshot.exists,
|
||||
nextConfig,
|
||||
workspaceDir,
|
||||
workspaceDir: onboardingTarget.workspaceDir,
|
||||
settings,
|
||||
prompter,
|
||||
runtime,
|
||||
|
||||
Reference in New Issue
Block a user