mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 12:44:59 +00:00
refactor: dedupe cli config cron and install flows
This commit is contained in:
@@ -26,8 +26,8 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
||||
return withTempHomeBase(fn, { prefix: "openclaw-agent-acp-" });
|
||||
}
|
||||
|
||||
function mockConfig(home: string, storePath: string) {
|
||||
loadConfigSpy.mockReturnValue({
|
||||
function createAcpEnabledConfig(home: string, storePath: string): OpenClawConfig {
|
||||
return {
|
||||
acp: {
|
||||
enabled: true,
|
||||
backend: "acpx",
|
||||
@@ -42,7 +42,11 @@ function mockConfig(home: string, storePath: string) {
|
||||
},
|
||||
},
|
||||
session: { store: storePath, mainKey: "main" },
|
||||
} satisfies OpenClawConfig);
|
||||
};
|
||||
}
|
||||
|
||||
function mockConfig(home: string, storePath: string) {
|
||||
loadConfigSpy.mockReturnValue(createAcpEnabledConfig(home, storePath));
|
||||
}
|
||||
|
||||
function mockConfigWithAcpOverrides(
|
||||
@@ -50,23 +54,12 @@ function mockConfigWithAcpOverrides(
|
||||
storePath: string,
|
||||
acpOverrides: Partial<NonNullable<OpenClawConfig["acp"]>>,
|
||||
) {
|
||||
loadConfigSpy.mockReturnValue({
|
||||
acp: {
|
||||
enabled: true,
|
||||
backend: "acpx",
|
||||
allowedAgents: ["codex"],
|
||||
dispatch: { enabled: true },
|
||||
...acpOverrides,
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai/gpt-5.3-codex" },
|
||||
models: { "openai/gpt-5.3-codex": {} },
|
||||
workspace: path.join(home, "openclaw"),
|
||||
},
|
||||
},
|
||||
session: { store: storePath, mainKey: "main" },
|
||||
} satisfies OpenClawConfig);
|
||||
const cfg = createAcpEnabledConfig(home, storePath);
|
||||
cfg.acp = {
|
||||
...cfg.acp,
|
||||
...acpOverrides,
|
||||
};
|
||||
loadConfigSpy.mockReturnValue(cfg);
|
||||
}
|
||||
|
||||
function writeAcpSessionStore(storePath: string) {
|
||||
|
||||
@@ -304,6 +304,24 @@ export function createAuthChoiceDefaultModelApplier(
|
||||
};
|
||||
}
|
||||
|
||||
export function createAuthChoiceDefaultModelApplierForMutableState(
|
||||
params: ApplyAuthChoiceParams,
|
||||
getConfig: () => ApplyAuthChoiceParams["config"],
|
||||
setConfig: (config: ApplyAuthChoiceParams["config"]) => void,
|
||||
getAgentModelOverride: () => string | undefined,
|
||||
setAgentModelOverride: (model: string | undefined) => void,
|
||||
): ReturnType<typeof createAuthChoiceDefaultModelApplier> {
|
||||
return createAuthChoiceDefaultModelApplier(
|
||||
params,
|
||||
createAuthChoiceModelStateBridge({
|
||||
getConfig,
|
||||
setConfig,
|
||||
getAgentModelOverride,
|
||||
setAgentModelOverride,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeTokenProviderInput(
|
||||
tokenProvider: string | null | undefined,
|
||||
): string | undefined {
|
||||
|
||||
@@ -4,8 +4,7 @@ import { normalizeApiKeyInput, validateApiKeyInput } from "./auth-choice.api-key
|
||||
import {
|
||||
normalizeSecretInputModeInput,
|
||||
createAuthChoiceAgentModelNoter,
|
||||
createAuthChoiceDefaultModelApplier,
|
||||
createAuthChoiceModelStateBridge,
|
||||
createAuthChoiceDefaultModelApplierForMutableState,
|
||||
ensureApiKeyFromOptionEnvOrPrompt,
|
||||
normalizeTokenProviderInput,
|
||||
} from "./auth-choice.apply-helpers.js";
|
||||
@@ -317,14 +316,12 @@ export async function applyAuthChoiceApiProviders(
|
||||
let nextConfig = params.config;
|
||||
let agentModelOverride: string | undefined;
|
||||
const noteAgentModel = createAuthChoiceAgentModelNoter(params);
|
||||
const applyProviderDefaultModel = createAuthChoiceDefaultModelApplier(
|
||||
const applyProviderDefaultModel = createAuthChoiceDefaultModelApplierForMutableState(
|
||||
params,
|
||||
createAuthChoiceModelStateBridge({
|
||||
getConfig: () => nextConfig,
|
||||
setConfig: (config) => (nextConfig = config),
|
||||
getAgentModelOverride: () => agentModelOverride,
|
||||
setAgentModelOverride: (model) => (agentModelOverride = model),
|
||||
}),
|
||||
() => nextConfig,
|
||||
(config) => (nextConfig = config),
|
||||
() => agentModelOverride,
|
||||
(model) => (agentModelOverride = model),
|
||||
);
|
||||
|
||||
let authChoice = params.authChoice;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { normalizeApiKeyInput, validateApiKeyInput } from "./auth-choice.api-key.js";
|
||||
import {
|
||||
createAuthChoiceDefaultModelApplier,
|
||||
createAuthChoiceModelStateBridge,
|
||||
createAuthChoiceDefaultModelApplierForMutableState,
|
||||
ensureApiKeyFromOptionEnvOrPrompt,
|
||||
normalizeSecretInputModeInput,
|
||||
} from "./auth-choice.apply-helpers.js";
|
||||
@@ -23,14 +22,12 @@ export async function applyAuthChoiceMiniMax(
|
||||
): Promise<ApplyAuthChoiceResult | null> {
|
||||
let nextConfig = params.config;
|
||||
let agentModelOverride: string | undefined;
|
||||
const applyProviderDefaultModel = createAuthChoiceDefaultModelApplier(
|
||||
const applyProviderDefaultModel = createAuthChoiceDefaultModelApplierForMutableState(
|
||||
params,
|
||||
createAuthChoiceModelStateBridge({
|
||||
getConfig: () => nextConfig,
|
||||
setConfig: (config) => (nextConfig = config),
|
||||
getAgentModelOverride: () => agentModelOverride,
|
||||
setAgentModelOverride: (model) => (agentModelOverride = model),
|
||||
}),
|
||||
() => nextConfig,
|
||||
(config) => (nextConfig = config),
|
||||
() => agentModelOverride,
|
||||
(model) => (agentModelOverride = model),
|
||||
);
|
||||
const requestedSecretInputMode = normalizeSecretInputModeInput(params.opts?.secretInputMode);
|
||||
const ensureMinimaxApiKey = async (opts: {
|
||||
|
||||
@@ -22,6 +22,8 @@ vi.mock("../terminal/note.js", () => ({
|
||||
note,
|
||||
}));
|
||||
|
||||
const { maybeRepairSandboxImages } = await import("./doctor-sandbox.js");
|
||||
|
||||
describe("maybeRepairSandboxImages", () => {
|
||||
const mockRuntime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
@@ -37,22 +39,32 @@ describe("maybeRepairSandboxImages", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("warns when sandbox mode is enabled but Docker is not available", async () => {
|
||||
// Simulate Docker not available (command fails)
|
||||
runExec.mockRejectedValue(new Error("Docker not installed"));
|
||||
|
||||
const config: OpenClawConfig = {
|
||||
function createSandboxConfig(mode: "off" | "all" | "non-main"): OpenClawConfig {
|
||||
return {
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "non-main",
|
||||
mode,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const { maybeRepairSandboxImages } = await import("./doctor-sandbox.js");
|
||||
await maybeRepairSandboxImages(config, mockRuntime, mockPrompter);
|
||||
async function runSandboxRepair(params: {
|
||||
mode: "off" | "all" | "non-main";
|
||||
dockerAvailable: boolean;
|
||||
}) {
|
||||
if (params.dockerAvailable) {
|
||||
runExec.mockResolvedValue({ stdout: "24.0.0", stderr: "" });
|
||||
} else {
|
||||
runExec.mockRejectedValue(new Error("Docker not installed"));
|
||||
}
|
||||
await maybeRepairSandboxImages(createSandboxConfig(params.mode), mockRuntime, mockPrompter);
|
||||
}
|
||||
|
||||
it("warns when sandbox mode is enabled but Docker is not available", async () => {
|
||||
await runSandboxRepair({ mode: "non-main", dockerAvailable: false });
|
||||
|
||||
// The warning should clearly indicate sandbox is enabled but won't work
|
||||
expect(note).toHaveBeenCalled();
|
||||
@@ -66,20 +78,7 @@ describe("maybeRepairSandboxImages", () => {
|
||||
});
|
||||
|
||||
it("warns when sandbox mode is 'all' but Docker is not available", async () => {
|
||||
runExec.mockRejectedValue(new Error("Docker not installed"));
|
||||
|
||||
const config: OpenClawConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { maybeRepairSandboxImages } = await import("./doctor-sandbox.js");
|
||||
await maybeRepairSandboxImages(config, mockRuntime, mockPrompter);
|
||||
await runSandboxRepair({ mode: "all", dockerAvailable: false });
|
||||
|
||||
expect(note).toHaveBeenCalled();
|
||||
const noteCall = note.mock.calls[0];
|
||||
@@ -90,41 +89,14 @@ describe("maybeRepairSandboxImages", () => {
|
||||
});
|
||||
|
||||
it("does not warn when sandbox mode is off", async () => {
|
||||
runExec.mockRejectedValue(new Error("Docker not installed"));
|
||||
|
||||
const config: OpenClawConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "off",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { maybeRepairSandboxImages } = await import("./doctor-sandbox.js");
|
||||
await maybeRepairSandboxImages(config, mockRuntime, mockPrompter);
|
||||
await runSandboxRepair({ mode: "off", dockerAvailable: false });
|
||||
|
||||
// No warning needed when sandbox is off
|
||||
expect(note).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not warn when Docker is available", async () => {
|
||||
// Simulate Docker available
|
||||
runExec.mockResolvedValue({ stdout: "24.0.0", stderr: "" });
|
||||
|
||||
const config: OpenClawConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "non-main",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { maybeRepairSandboxImages } = await import("./doctor-sandbox.js");
|
||||
await maybeRepairSandboxImages(config, mockRuntime, mockPrompter);
|
||||
await runSandboxRepair({ mode: "non-main", dockerAvailable: true });
|
||||
|
||||
// May have other notes about images, but not the Docker unavailable warning
|
||||
const dockerUnavailableWarning = note.mock.calls.find(
|
||||
|
||||
@@ -95,6 +95,73 @@ function patchTelegramAdapter(overrides: Parameters<typeof patchChannelOnboardin
|
||||
});
|
||||
}
|
||||
|
||||
function createUnexpectedConfigureCall(message: string) {
|
||||
return vi.fn(async () => {
|
||||
throw new Error(message);
|
||||
});
|
||||
}
|
||||
|
||||
async function runConfiguredTelegramSetup(params: {
|
||||
strictUnexpected?: boolean;
|
||||
configureWhenConfigured: NonNullable<
|
||||
Parameters<typeof patchTelegramAdapter>[0]["configureWhenConfigured"]
|
||||
>;
|
||||
configureErrorMessage: string;
|
||||
}) {
|
||||
const select = createQuickstartTelegramSelect({ strictUnexpected: params.strictUnexpected });
|
||||
const selection = vi.fn();
|
||||
const onAccountId = vi.fn();
|
||||
const configure = createUnexpectedConfigureCall(params.configureErrorMessage);
|
||||
const restore = patchTelegramAdapter({
|
||||
configureInteractive: undefined,
|
||||
configureWhenConfigured: params.configureWhenConfigured,
|
||||
configure,
|
||||
});
|
||||
const { prompter } = createUnexpectedQuickstartPrompter(
|
||||
select as unknown as WizardPrompter["select"],
|
||||
);
|
||||
|
||||
try {
|
||||
const cfg = await runSetupChannels(createTelegramCfg("old-token"), prompter, {
|
||||
quickstartDefaults: true,
|
||||
onSelection: selection,
|
||||
onAccountId,
|
||||
});
|
||||
return { cfg, selection, onAccountId, configure };
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
}
|
||||
|
||||
async function runQuickstartTelegramSetupWithInteractive(params: {
|
||||
configureInteractive: NonNullable<
|
||||
Parameters<typeof patchTelegramAdapter>[0]["configureInteractive"]
|
||||
>;
|
||||
configure?: NonNullable<Parameters<typeof patchTelegramAdapter>[0]["configure"]>;
|
||||
}) {
|
||||
const select = createQuickstartTelegramSelect();
|
||||
const selection = vi.fn();
|
||||
const onAccountId = vi.fn();
|
||||
const restore = patchTelegramAdapter({
|
||||
configureInteractive: params.configureInteractive,
|
||||
...(params.configure ? { configure: params.configure } : {}),
|
||||
});
|
||||
const { prompter } = createUnexpectedQuickstartPrompter(
|
||||
select as unknown as WizardPrompter["select"],
|
||||
);
|
||||
|
||||
try {
|
||||
const cfg = await runSetupChannels({} as OpenClawConfig, prompter, {
|
||||
quickstartDefaults: true,
|
||||
onSelection: selection,
|
||||
onAccountId,
|
||||
});
|
||||
return { cfg, selection, onAccountId };
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
}
|
||||
|
||||
vi.mock("node:fs/promises", () => ({
|
||||
default: {
|
||||
access: vi.fn(async () => {
|
||||
@@ -269,39 +336,20 @@ describe("setupChannels", () => {
|
||||
});
|
||||
|
||||
it("uses configureInteractive skip without mutating selection/account state", async () => {
|
||||
const select = createQuickstartTelegramSelect();
|
||||
const selection = vi.fn();
|
||||
const onAccountId = vi.fn();
|
||||
const configureInteractive = vi.fn(async () => "skip" as const);
|
||||
const restore = patchTelegramAdapter({
|
||||
const { cfg, selection, onAccountId } = await runQuickstartTelegramSetupWithInteractive({
|
||||
configureInteractive,
|
||||
});
|
||||
const { prompter } = createUnexpectedQuickstartPrompter(
|
||||
select as unknown as WizardPrompter["select"],
|
||||
|
||||
expect(configureInteractive).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ configured: false, label: expect.any(String) }),
|
||||
);
|
||||
|
||||
try {
|
||||
const cfg = await runSetupChannels({} as OpenClawConfig, prompter, {
|
||||
quickstartDefaults: true,
|
||||
onSelection: selection,
|
||||
onAccountId,
|
||||
});
|
||||
|
||||
expect(configureInteractive).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ configured: false, label: expect.any(String) }),
|
||||
);
|
||||
expect(selection).toHaveBeenCalledWith([]);
|
||||
expect(onAccountId).not.toHaveBeenCalled();
|
||||
expect(cfg.channels?.telegram?.botToken).toBeUndefined();
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
expect(selection).toHaveBeenCalledWith([]);
|
||||
expect(onAccountId).not.toHaveBeenCalled();
|
||||
expect(cfg.channels?.telegram?.botToken).toBeUndefined();
|
||||
});
|
||||
|
||||
it("applies configureInteractive result cfg/account updates", async () => {
|
||||
const select = createQuickstartTelegramSelect();
|
||||
const selection = vi.fn();
|
||||
const onAccountId = vi.fn();
|
||||
const configureInteractive = vi.fn(async ({ cfg }: { cfg: OpenClawConfig }) => ({
|
||||
cfg: {
|
||||
...cfg,
|
||||
@@ -312,38 +360,22 @@ describe("setupChannels", () => {
|
||||
} as OpenClawConfig,
|
||||
accountId: "acct-1",
|
||||
}));
|
||||
const configure = vi.fn(async () => {
|
||||
throw new Error("configure should not be called when configureInteractive is present");
|
||||
});
|
||||
const restore = patchTelegramAdapter({
|
||||
const configure = createUnexpectedConfigureCall(
|
||||
"configure should not be called when configureInteractive is present",
|
||||
);
|
||||
const { cfg, selection, onAccountId } = await runQuickstartTelegramSetupWithInteractive({
|
||||
configureInteractive,
|
||||
configure,
|
||||
});
|
||||
const { prompter } = createUnexpectedQuickstartPrompter(
|
||||
select as unknown as WizardPrompter["select"],
|
||||
);
|
||||
|
||||
try {
|
||||
const cfg = await runSetupChannels({} as OpenClawConfig, prompter, {
|
||||
quickstartDefaults: true,
|
||||
onSelection: selection,
|
||||
onAccountId,
|
||||
});
|
||||
|
||||
expect(configureInteractive).toHaveBeenCalledTimes(1);
|
||||
expect(configure).not.toHaveBeenCalled();
|
||||
expect(selection).toHaveBeenCalledWith(["telegram"]);
|
||||
expect(onAccountId).toHaveBeenCalledWith("telegram", "acct-1");
|
||||
expect(cfg.channels?.telegram?.botToken).toBe("new-token");
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
expect(configureInteractive).toHaveBeenCalledTimes(1);
|
||||
expect(configure).not.toHaveBeenCalled();
|
||||
expect(selection).toHaveBeenCalledWith(["telegram"]);
|
||||
expect(onAccountId).toHaveBeenCalledWith("telegram", "acct-1");
|
||||
expect(cfg.channels?.telegram?.botToken).toBe("new-token");
|
||||
});
|
||||
|
||||
it("uses configureWhenConfigured when channel is already configured", async () => {
|
||||
const select = createQuickstartTelegramSelect();
|
||||
const selection = vi.fn();
|
||||
const onAccountId = vi.fn();
|
||||
const configureWhenConfigured = vi.fn(async ({ cfg }: { cfg: OpenClawConfig }) => ({
|
||||
cfg: {
|
||||
...cfg,
|
||||
@@ -354,74 +386,37 @@ describe("setupChannels", () => {
|
||||
} as OpenClawConfig,
|
||||
accountId: "acct-2",
|
||||
}));
|
||||
const configure = vi.fn(async () => {
|
||||
throw new Error(
|
||||
"configure should not be called when configureWhenConfigured handles updates",
|
||||
);
|
||||
});
|
||||
const restore = patchTelegramAdapter({
|
||||
configureInteractive: undefined,
|
||||
const { cfg, selection, onAccountId, configure } = await runConfiguredTelegramSetup({
|
||||
configureWhenConfigured,
|
||||
configure,
|
||||
configureErrorMessage:
|
||||
"configure should not be called when configureWhenConfigured handles updates",
|
||||
});
|
||||
const { prompter } = createUnexpectedQuickstartPrompter(
|
||||
select as unknown as WizardPrompter["select"],
|
||||
|
||||
expect(configureWhenConfigured).toHaveBeenCalledTimes(1);
|
||||
expect(configureWhenConfigured).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ configured: true, label: expect.any(String) }),
|
||||
);
|
||||
|
||||
try {
|
||||
const cfg = await runSetupChannels(createTelegramCfg("old-token"), prompter, {
|
||||
quickstartDefaults: true,
|
||||
onSelection: selection,
|
||||
onAccountId,
|
||||
});
|
||||
|
||||
expect(configureWhenConfigured).toHaveBeenCalledTimes(1);
|
||||
expect(configureWhenConfigured).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ configured: true, label: expect.any(String) }),
|
||||
);
|
||||
expect(configure).not.toHaveBeenCalled();
|
||||
expect(selection).toHaveBeenCalledWith(["telegram"]);
|
||||
expect(onAccountId).toHaveBeenCalledWith("telegram", "acct-2");
|
||||
expect(cfg.channels?.telegram?.botToken).toBe("updated-token");
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
expect(configure).not.toHaveBeenCalled();
|
||||
expect(selection).toHaveBeenCalledWith(["telegram"]);
|
||||
expect(onAccountId).toHaveBeenCalledWith("telegram", "acct-2");
|
||||
expect(cfg.channels?.telegram?.botToken).toBe("updated-token");
|
||||
});
|
||||
|
||||
it("respects configureWhenConfigured skip without mutating selection or account state", async () => {
|
||||
const select = createQuickstartTelegramSelect({ strictUnexpected: true });
|
||||
const selection = vi.fn();
|
||||
const onAccountId = vi.fn();
|
||||
const configureWhenConfigured = vi.fn(async () => "skip" as const);
|
||||
const configure = vi.fn(async () => {
|
||||
throw new Error("configure should not run when configureWhenConfigured handles skip");
|
||||
});
|
||||
const restore = patchTelegramAdapter({
|
||||
configureInteractive: undefined,
|
||||
const { cfg, selection, onAccountId, configure } = await runConfiguredTelegramSetup({
|
||||
strictUnexpected: true,
|
||||
configureWhenConfigured,
|
||||
configure,
|
||||
configureErrorMessage: "configure should not run when configureWhenConfigured handles skip",
|
||||
});
|
||||
const { prompter } = createUnexpectedQuickstartPrompter(
|
||||
select as unknown as WizardPrompter["select"],
|
||||
|
||||
expect(configureWhenConfigured).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ configured: true, label: expect.any(String) }),
|
||||
);
|
||||
|
||||
try {
|
||||
const cfg = await runSetupChannels(createTelegramCfg("old-token"), prompter, {
|
||||
quickstartDefaults: true,
|
||||
onSelection: selection,
|
||||
onAccountId,
|
||||
});
|
||||
|
||||
expect(configureWhenConfigured).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ configured: true, label: expect.any(String) }),
|
||||
);
|
||||
expect(configure).not.toHaveBeenCalled();
|
||||
expect(selection).toHaveBeenCalledWith([]);
|
||||
expect(onAccountId).not.toHaveBeenCalled();
|
||||
expect(cfg.channels?.telegram?.botToken).toBe("old-token");
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
expect(configure).not.toHaveBeenCalled();
|
||||
expect(selection).toHaveBeenCalledWith([]);
|
||||
expect(onAccountId).not.toHaveBeenCalled();
|
||||
expect(cfg.channels?.telegram?.botToken).toBe("old-token");
|
||||
});
|
||||
|
||||
it("prefers configureInteractive over configureWhenConfigured when both hooks exist", async () => {
|
||||
|
||||
@@ -42,6 +42,21 @@ function createSelectPrompter(
|
||||
describe("promptRemoteGatewayConfig", () => {
|
||||
const envSnapshot = captureEnv(["OPENCLAW_ALLOW_INSECURE_PRIVATE_WS"]);
|
||||
|
||||
async function runRemotePrompt(params: {
|
||||
text: WizardPrompter["text"];
|
||||
selectResponses: Partial<Record<string, string>>;
|
||||
confirm: boolean;
|
||||
}) {
|
||||
const cfg = {} as OpenClawConfig;
|
||||
const prompter = createPrompter({
|
||||
confirm: vi.fn(async () => params.confirm),
|
||||
select: createSelectPrompter(params.selectResponses),
|
||||
text: params.text,
|
||||
});
|
||||
const next = await promptRemoteGatewayConfig(cfg, prompter);
|
||||
return { next, prompter };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
envSnapshot.restore();
|
||||
@@ -61,12 +76,6 @@ describe("promptRemoteGatewayConfig", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const select = createSelectPrompter({
|
||||
"Select gateway": "0",
|
||||
"Connection method": "direct",
|
||||
"Gateway auth": "token",
|
||||
});
|
||||
|
||||
const text: WizardPrompter["text"] = vi.fn(async (params) => {
|
||||
if (params.message === "Gateway WebSocket URL") {
|
||||
expect(params.initialValue).toBe("wss://gateway.tailnet.ts.net:18789");
|
||||
@@ -79,15 +88,16 @@ describe("promptRemoteGatewayConfig", () => {
|
||||
return "";
|
||||
}) as WizardPrompter["text"];
|
||||
|
||||
const cfg = {} as OpenClawConfig;
|
||||
const prompter = createPrompter({
|
||||
confirm: vi.fn(async () => true),
|
||||
select,
|
||||
const { next, prompter } = await runRemotePrompt({
|
||||
text,
|
||||
confirm: true,
|
||||
selectResponses: {
|
||||
"Select gateway": "0",
|
||||
"Connection method": "direct",
|
||||
"Gateway auth": "token",
|
||||
},
|
||||
});
|
||||
|
||||
const next = await promptRemoteGatewayConfig(cfg, prompter);
|
||||
|
||||
expect(next.gateway?.mode).toBe("remote");
|
||||
expect(next.gateway?.remote?.url).toBe("wss://gateway.tailnet.ts.net:18789");
|
||||
expect(next.gateway?.remote?.token).toBe("token-123");
|
||||
@@ -111,17 +121,12 @@ describe("promptRemoteGatewayConfig", () => {
|
||||
return "";
|
||||
}) as WizardPrompter["text"];
|
||||
|
||||
const select = createSelectPrompter({ "Gateway auth": "off" });
|
||||
|
||||
const cfg = {} as OpenClawConfig;
|
||||
const prompter = createPrompter({
|
||||
confirm: vi.fn(async () => false),
|
||||
select,
|
||||
const { next } = await runRemotePrompt({
|
||||
text,
|
||||
confirm: false,
|
||||
selectResponses: { "Gateway auth": "off" },
|
||||
});
|
||||
|
||||
const next = await promptRemoteGatewayConfig(cfg, prompter);
|
||||
|
||||
expect(next.gateway?.mode).toBe("remote");
|
||||
expect(next.gateway?.remote?.url).toBe("wss://remote.example.com:18789");
|
||||
expect(next.gateway?.remote?.token).toBeUndefined();
|
||||
@@ -138,17 +143,12 @@ describe("promptRemoteGatewayConfig", () => {
|
||||
return "";
|
||||
}) as WizardPrompter["text"];
|
||||
|
||||
const select = createSelectPrompter({ "Gateway auth": "off" });
|
||||
|
||||
const cfg = {} as OpenClawConfig;
|
||||
const prompter = createPrompter({
|
||||
confirm: vi.fn(async () => false),
|
||||
select,
|
||||
const { next } = await runRemotePrompt({
|
||||
text,
|
||||
confirm: false,
|
||||
selectResponses: { "Gateway auth": "off" },
|
||||
});
|
||||
|
||||
const next = await promptRemoteGatewayConfig(cfg, prompter);
|
||||
|
||||
expect(next.gateway?.remote?.url).toBe("ws://10.0.0.8:18789");
|
||||
});
|
||||
});
|
||||
|
||||
15
src/commands/status-all/channel-issues.ts
Normal file
15
src/commands/status-all/channel-issues.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export function groupChannelIssuesByChannel<T extends { channel: string }>(
|
||||
issues: readonly T[],
|
||||
): Map<string, T[]> {
|
||||
const byChannel = new Map<string, T[]>();
|
||||
for (const issue of issues) {
|
||||
const key = issue.channel;
|
||||
const list = byChannel.get(key);
|
||||
if (list) {
|
||||
list.push(issue);
|
||||
} else {
|
||||
byChannel.set(key, [issue]);
|
||||
}
|
||||
}
|
||||
return byChannel;
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import fs from "node:fs";
|
||||
import {
|
||||
buildChannelAccountSnapshot,
|
||||
formatChannelAllowFrom,
|
||||
resolveChannelAccountConfigured,
|
||||
resolveChannelAccountEnabled,
|
||||
} from "../../channels/account-summary.js";
|
||||
import { resolveChannelDefaultAccountId } from "../../channels/plugins/helpers.js";
|
||||
import { listChannelPlugins } from "../../channels/plugins/index.js";
|
||||
@@ -85,30 +87,6 @@ const formatAccountLabel = (params: { accountId: string; name?: string }) => {
|
||||
return base;
|
||||
};
|
||||
|
||||
const resolveAccountEnabled = (
|
||||
plugin: ChannelPlugin,
|
||||
account: unknown,
|
||||
cfg: OpenClawConfig,
|
||||
): boolean => {
|
||||
if (plugin.config.isEnabled) {
|
||||
return plugin.config.isEnabled(account, cfg);
|
||||
}
|
||||
const enabled = asRecord(account).enabled;
|
||||
return enabled !== false;
|
||||
};
|
||||
|
||||
const resolveAccountConfigured = async (
|
||||
plugin: ChannelPlugin,
|
||||
account: unknown,
|
||||
cfg: OpenClawConfig,
|
||||
): Promise<boolean> => {
|
||||
if (plugin.config.isConfigured) {
|
||||
return await plugin.config.isConfigured(account, cfg);
|
||||
}
|
||||
const configured = asRecord(account).configured;
|
||||
return configured !== false;
|
||||
};
|
||||
|
||||
const buildAccountNotes = (params: {
|
||||
plugin: ChannelPlugin;
|
||||
cfg: OpenClawConfig;
|
||||
@@ -343,8 +321,13 @@ export async function buildChannelsTable(
|
||||
const accounts: ChannelAccountRow[] = [];
|
||||
for (const accountId of resolvedAccountIds) {
|
||||
const account = plugin.config.resolveAccount(cfg, accountId);
|
||||
const enabled = resolveAccountEnabled(plugin, account, cfg);
|
||||
const configured = await resolveAccountConfigured(plugin, account, cfg);
|
||||
const enabled = resolveChannelAccountEnabled({ plugin, account, cfg });
|
||||
const configured = await resolveChannelAccountConfigured({
|
||||
plugin,
|
||||
account,
|
||||
cfg,
|
||||
readAccountConfiguredField: true,
|
||||
});
|
||||
const snapshot = buildChannelAccountSnapshot({
|
||||
plugin,
|
||||
cfg,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ProgressReporter } from "../../cli/progress.js";
|
||||
import { renderTable } from "../../terminal/table.js";
|
||||
import { isRich, theme } from "../../terminal/theme.js";
|
||||
import { groupChannelIssuesByChannel } from "./channel-issues.js";
|
||||
import { appendStatusAllDiagnosis } from "./diagnosis.js";
|
||||
import { formatTimeAgo } from "./format.js";
|
||||
|
||||
@@ -81,19 +82,7 @@ export async function buildStatusAllReportLines(params: {
|
||||
: theme.accentDim("SETUP"),
|
||||
Detail: row.detail,
|
||||
}));
|
||||
const channelIssuesByChannel = (() => {
|
||||
const map = new Map<string, ChannelIssueLike[]>();
|
||||
for (const issue of params.channelIssues) {
|
||||
const key = issue.channel;
|
||||
const list = map.get(key);
|
||||
if (list) {
|
||||
list.push(issue);
|
||||
} else {
|
||||
map.set(key, [issue]);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
})();
|
||||
const channelIssuesByChannel = groupChannelIssuesByChannel(params.channelIssues);
|
||||
const channelRowsWithIssues = channelRows.map((row) => {
|
||||
const issues = channelIssuesByChannel.get(row.channelId) ?? [];
|
||||
if (issues.length === 0) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import { theme } from "../terminal/theme.js";
|
||||
import { formatHealthChannelLines, type HealthSummary } from "./health.js";
|
||||
import { resolveControlUiLinks } from "./onboard-helpers.js";
|
||||
import { statusAllCommand } from "./status-all.js";
|
||||
import { groupChannelIssuesByChannel } from "./status-all/channel-issues.js";
|
||||
import { formatGatewayAuthUsed } from "./status-all/format.js";
|
||||
import { getDaemonStatusSummary, getNodeDaemonStatusSummary } from "./status.daemon.js";
|
||||
import {
|
||||
@@ -500,19 +501,7 @@ export async function statusCommand(
|
||||
|
||||
runtime.log("");
|
||||
runtime.log(theme.heading("Channels"));
|
||||
const channelIssuesByChannel = (() => {
|
||||
const map = new Map<string, typeof channelIssues>();
|
||||
for (const issue of channelIssues) {
|
||||
const key = issue.channel;
|
||||
const list = map.get(key);
|
||||
if (list) {
|
||||
list.push(issue);
|
||||
} else {
|
||||
map.set(key, [issue]);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
})();
|
||||
const channelIssuesByChannel = groupChannelIssuesByChannel(channelIssues);
|
||||
runtime.log(
|
||||
renderTable({
|
||||
width: tableWidth,
|
||||
|
||||
Reference in New Issue
Block a user