test: share cli and channel setup fixtures

This commit is contained in:
Peter Steinberger
2026-03-26 18:13:22 +00:00
parent 02cf12371f
commit 1f740ff099
9 changed files with 353 additions and 513 deletions

View File

@@ -1,20 +1,10 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { registerBrowserManageCommands } from "./browser-cli-manage.js";
import { createBrowserProgram } from "./browser-cli-test-helpers.js";
import type { CliRuntimeCapture } from "./test-runtime-capture.js";
const runtimeState = vi.hoisted(() => ({ capture: null as CliRuntimeCapture | null }));
function getRuntimeCapture(): CliRuntimeCapture {
if (!runtimeState.capture) {
throw new Error("runtime capture not initialized");
}
return runtimeState.capture;
}
function getRuntime() {
return getRuntimeCapture().defaultRuntime;
}
import {
createBrowserProgram,
getBrowserCliRuntime,
getBrowserCliRuntimeCapture,
} from "./browser-cli-test-helpers.js";
const mocks = vi.hoisted(() => {
return {
@@ -32,19 +22,15 @@ vi.mock("./browser-cli-shared.js", () => ({
callBrowserRequest: mocks.callBrowserRequest,
}));
vi.mock("./cli-utils.js", () => ({
runCommandWithRuntime: async (
_runtime: unknown,
action: () => Promise<void>,
onError: (err: unknown) => void,
) => await action().catch(onError),
vi.mock("./cli-utils.js", async () => ({
...(await (await import("./browser-cli-test-helpers.js")).createBrowserCliUtilsMockModule()),
}));
vi.mock("../runtime.js", async () => {
const { createCliRuntimeCapture } = await import("./test-runtime-capture.js");
runtimeState.capture ??= createCliRuntimeCapture();
return { defaultRuntime: runtimeState.capture.defaultRuntime };
});
vi.mock(
"../runtime.js",
async () =>
await (await import("./browser-cli-test-helpers.js")).createBrowserCliRuntimeMockModule(),
);
function createProgram() {
const { program, browser, parentOpts } = createBrowserProgram();
@@ -55,7 +41,7 @@ function createProgram() {
describe("browser manage output", () => {
beforeEach(() => {
mocks.callBrowserRequest.mockClear();
getRuntimeCapture().resetRuntimeCapture();
getBrowserCliRuntimeCapture().resetRuntimeCapture();
});
it("shows chrome-mcp transport for existing-session status without fake CDP fields", async () => {
@@ -88,7 +74,7 @@ describe("browser manage output", () => {
from: "user",
});
const output = getRuntime().log.mock.calls.at(-1)?.[0] as string;
const output = getBrowserCliRuntime().log.mock.calls.at(-1)?.[0] as string;
expect(output).toContain("transport: chrome-mcp");
expect(output).not.toContain("cdpPort:");
expect(output).not.toContain("cdpUrl:");
@@ -124,7 +110,7 @@ describe("browser manage output", () => {
from: "user",
});
const output = getRuntime().log.mock.calls.at(-1)?.[0] as string;
const output = getBrowserCliRuntime().log.mock.calls.at(-1)?.[0] as string;
expect(output).toContain(
"userDataDir: /Users/test/Library/Application Support/BraveSoftware/Brave-Browser",
);
@@ -155,7 +141,7 @@ describe("browser manage output", () => {
const program = createProgram();
await program.parseAsync(["browser", "profiles"], { from: "user" });
const output = getRuntime().log.mock.calls.at(-1)?.[0] as string;
const output = getBrowserCliRuntime().log.mock.calls.at(-1)?.[0] as string;
expect(output).toContain("chrome-live: running (2 tabs) [existing-session]");
expect(output).toContain("transport: chrome-mcp");
expect(output).not.toContain("port: 0");
@@ -183,7 +169,7 @@ describe("browser manage output", () => {
{ from: "user" },
);
const output = getRuntime().log.mock.calls.at(-1)?.[0] as string;
const output = getBrowserCliRuntime().log.mock.calls.at(-1)?.[0] as string;
expect(output).toContain('Created profile "chrome-live"');
expect(output).toContain("transport: chrome-mcp");
expect(output).not.toContain("port: 0");
@@ -220,7 +206,7 @@ describe("browser manage output", () => {
from: "user",
});
const output = getRuntime().log.mock.calls.at(-1)?.[0] as string;
const output = getBrowserCliRuntime().log.mock.calls.at(-1)?.[0] as string;
expect(output).toContain("cdpUrl: https://example.com/chrome?token=supers…7890");
expect(output).not.toContain("alice");
expect(output).not.toContain("supersecretpasswordvalue1234");

View File

@@ -1,16 +1,6 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { registerBrowserManageCommands } from "./browser-cli-manage.js";
import { createBrowserProgram } from "./browser-cli-test-helpers.js";
import type { CliRuntimeCapture } from "./test-runtime-capture.js";
const runtimeState = vi.hoisted(() => ({ capture: null as CliRuntimeCapture | null }));
function getRuntimeCapture(): CliRuntimeCapture {
if (!runtimeState.capture) {
throw new Error("runtime capture not initialized");
}
return runtimeState.capture;
}
import { createBrowserProgram, getBrowserCliRuntimeCapture } from "./browser-cli-test-helpers.js";
const mocks = vi.hoisted(() => {
return {
@@ -36,19 +26,15 @@ vi.mock("./browser-cli-shared.js", () => ({
callBrowserRequest: mocks.callBrowserRequest,
}));
vi.mock("./cli-utils.js", () => ({
runCommandWithRuntime: async (
_runtime: unknown,
action: () => Promise<void>,
onError: (err: unknown) => void,
) => await action().catch(onError),
vi.mock("./cli-utils.js", async () => ({
...(await (await import("./browser-cli-test-helpers.js")).createBrowserCliUtilsMockModule()),
}));
vi.mock("../runtime.js", async () => {
const { createCliRuntimeCapture } = await import("./test-runtime-capture.js");
runtimeState.capture ??= createCliRuntimeCapture();
return { defaultRuntime: runtimeState.capture.defaultRuntime };
});
vi.mock(
"../runtime.js",
async () =>
await (await import("./browser-cli-test-helpers.js")).createBrowserCliRuntimeMockModule(),
);
describe("browser manage start timeout option", () => {
function createProgram() {
@@ -60,7 +46,7 @@ describe("browser manage start timeout option", () => {
beforeEach(() => {
mocks.callBrowserRequest.mockClear();
getRuntimeCapture().resetRuntimeCapture();
getBrowserCliRuntimeCapture().resetRuntimeCapture();
});
it("uses parent --timeout for browser start instead of hardcoded 15s", async () => {

View File

@@ -1,20 +1,10 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { registerBrowserStateCommands } from "./browser-cli-state.js";
import { createBrowserProgram as createBrowserProgramShared } from "./browser-cli-test-helpers.js";
import type { CliRuntimeCapture } from "./test-runtime-capture.js";
const runtimeState = vi.hoisted(() => ({ capture: null as CliRuntimeCapture | null }));
function getRuntimeCapture(): CliRuntimeCapture {
if (!runtimeState.capture) {
throw new Error("runtime capture not initialized");
}
return runtimeState.capture;
}
function getRuntime() {
return getRuntimeCapture().defaultRuntime;
}
import {
createBrowserProgram as createBrowserProgramShared,
getBrowserCliRuntime,
getBrowserCliRuntimeCapture,
} from "./browser-cli-test-helpers.js";
const mocks = vi.hoisted(() => ({
callBrowserRequest: vi.fn(async (..._args: unknown[]) => ({ ok: true })),
@@ -29,11 +19,11 @@ vi.mock("./browser-cli-resize.js", () => ({
runBrowserResizeWithOutput: mocks.runBrowserResizeWithOutput,
}));
vi.mock("../runtime.js", async () => {
const { createCliRuntimeCapture } = await import("./test-runtime-capture.js");
runtimeState.capture ??= createCliRuntimeCapture();
return { defaultRuntime: runtimeState.capture.defaultRuntime };
});
vi.mock(
"../runtime.js",
async () =>
await (await import("./browser-cli-test-helpers.js")).createBrowserCliRuntimeMockModule(),
);
describe("browser state option collisions", () => {
const createStateProgram = ({ withGatewayUrl = false } = {}) => {
@@ -64,8 +54,8 @@ describe("browser state option collisions", () => {
beforeEach(() => {
mocks.callBrowserRequest.mockClear();
mocks.runBrowserResizeWithOutput.mockClear();
getRuntimeCapture().resetRuntimeCapture();
getRuntime().exit.mockImplementation(() => {});
getBrowserCliRuntimeCapture().resetRuntimeCapture();
getBrowserCliRuntime().exit.mockImplementation(() => {});
});
it("forwards parent-captured --target-id on `browser cookies set`", async () => {
@@ -145,37 +135,39 @@ describe("browser state option collisions", () => {
await runBrowserCommand(["set", "offline", "maybe"]);
expect(mocks.callBrowserRequest).not.toHaveBeenCalled();
expect(getRuntime().error).toHaveBeenCalledWith(expect.stringContaining("Expected on|off"));
expect(getRuntime().exit).toHaveBeenCalledWith(1);
expect(getBrowserCliRuntime().error).toHaveBeenCalledWith(
expect.stringContaining("Expected on|off"),
);
expect(getBrowserCliRuntime().exit).toHaveBeenCalledWith(1);
});
it("errors when set media receives an invalid value", async () => {
await runBrowserCommand(["set", "media", "sepia"]);
expect(mocks.callBrowserRequest).not.toHaveBeenCalled();
expect(getRuntime().error).toHaveBeenCalledWith(
expect(getBrowserCliRuntime().error).toHaveBeenCalledWith(
expect.stringContaining("Expected dark|light|none"),
);
expect(getRuntime().exit).toHaveBeenCalledWith(1);
expect(getBrowserCliRuntime().exit).toHaveBeenCalledWith(1);
});
it("errors when headers JSON is missing", async () => {
await runBrowserCommand(["set", "headers"]);
expect(mocks.callBrowserRequest).not.toHaveBeenCalled();
expect(getRuntime().error).toHaveBeenCalledWith(
expect(getBrowserCliRuntime().error).toHaveBeenCalledWith(
expect.stringContaining("Missing headers JSON"),
);
expect(getRuntime().exit).toHaveBeenCalledWith(1);
expect(getBrowserCliRuntime().exit).toHaveBeenCalledWith(1);
});
it("errors when headers JSON is not an object", async () => {
await runBrowserCommand(["set", "headers", "--json", "[]"]);
expect(mocks.callBrowserRequest).not.toHaveBeenCalled();
expect(getRuntime().error).toHaveBeenCalledWith(
expect(getBrowserCliRuntime().error).toHaveBeenCalledWith(
expect.stringContaining("Headers JSON must be a JSON object"),
);
expect(getRuntime().exit).toHaveBeenCalledWith(1);
expect(getBrowserCliRuntime().exit).toHaveBeenCalledWith(1);
});
});

View File

@@ -1,5 +1,7 @@
import { Command } from "commander";
import type { BrowserParentOpts } from "./browser-cli-shared.js";
import { createCliRuntimeCapture } from "./test-runtime-capture.js";
import type { CliRuntimeCapture } from "./test-runtime-capture.js";
export function createBrowserProgram(params?: { withGatewayUrl?: boolean }): {
program: Command;
@@ -17,3 +19,37 @@ export function createBrowserProgram(params?: { withGatewayUrl?: boolean }): {
const parentOpts = (cmd: Command) => cmd.parent?.opts?.() as BrowserParentOpts;
return { program, browser, parentOpts };
}
const browserCliRuntimeState = { capture: null as CliRuntimeCapture | null };
export function getBrowserCliRuntimeCapture(): CliRuntimeCapture {
if (!browserCliRuntimeState.capture) {
throw new Error("runtime capture not initialized");
}
return browserCliRuntimeState.capture;
}
export function getBrowserCliRuntime() {
return getBrowserCliRuntimeCapture().defaultRuntime;
}
export async function mockBrowserCliDefaultRuntime() {
browserCliRuntimeState.capture ??= createCliRuntimeCapture();
return { defaultRuntime: browserCliRuntimeState.capture.defaultRuntime };
}
export async function runCommandWithRuntimeMock(
_runtime: unknown,
action: () => Promise<void>,
onError: (err: unknown) => void,
) {
return await action().catch(onError);
}
export async function createBrowserCliUtilsMockModule() {
return { runCommandWithRuntime: runCommandWithRuntimeMock };
}
export async function createBrowserCliRuntimeMockModule() {
return await mockBrowserCliDefaultRuntime();
}

View File

@@ -45,6 +45,78 @@ vi.mock("@clack/prompts", () => ({
const { registerSecretsCli } = await import("./secrets-cli.js");
function createManualSecretsPlan() {
return {
version: 1,
protocolVersion: 1,
generatedAt: "2026-02-26T00:00:00.000Z",
generatedBy: "manual",
targets: [],
};
}
function createConfigureInteractiveResult(options?: {
targets?: unknown[];
changed?: boolean;
resolvabilityComplete?: boolean;
}) {
return {
plan: {
version: 1,
protocolVersion: 1,
generatedAt: "2026-02-26T00:00:00.000Z",
generatedBy: "openclaw secrets configure",
targets: options?.targets ?? [],
},
preflight: {
mode: "dry-run" as const,
changed: options?.changed ?? false,
changedFiles: options?.changed ? ["/tmp/openclaw.json"] : [],
checks: {
resolvability: true,
resolvabilityComplete: options?.resolvabilityComplete ?? true,
},
refsChecked: 0,
skippedExecRefs: 0,
warningCount: 0,
warnings: [],
},
};
}
function createSecretsApplyResult(options?: {
mode?: "dry-run" | "write";
changed?: boolean;
resolvabilityComplete?: boolean;
}) {
return {
mode: options?.mode ?? "dry-run",
changed: options?.changed ?? false,
changedFiles: options?.changed ? ["/tmp/openclaw.json"] : [],
checks: {
resolvability: true,
resolvabilityComplete: options?.resolvabilityComplete ?? true,
},
refsChecked: 0,
skippedExecRefs: 0,
warningCount: 0,
warnings: [],
};
}
async function withPlanFile(run: (planPath: string) => Promise<void>) {
const planPath = path.join(
os.tmpdir(),
`openclaw-secrets-cli-test-${Date.now()}-${Math.random().toString(16).slice(2)}.json`,
);
await fs.writeFile(planPath, `${JSON.stringify(createManualSecretsPlan())}\n`, "utf8");
try {
await run(planPath);
} finally {
await fs.rm(planPath, { force: true });
}
}
describe("secrets CLI", () => {
const createProgram = () => {
const program = new Command();
@@ -142,12 +214,9 @@ describe("secrets CLI", () => {
});
it("runs secrets configure then apply when confirmed", async () => {
runSecretsConfigureInteractive.mockResolvedValue({
plan: {
version: 1,
protocolVersion: 1,
generatedAt: "2026-02-26T00:00:00.000Z",
generatedBy: "openclaw secrets configure",
runSecretsConfigureInteractive.mockResolvedValue(
createConfigureInteractiveResult({
changed: true,
targets: [
{
type: "skills.entries.apiKey",
@@ -160,35 +229,10 @@ describe("secrets CLI", () => {
},
},
],
},
preflight: {
mode: "dry-run",
changed: true,
changedFiles: ["/tmp/openclaw.json"],
checks: {
resolvability: true,
resolvabilityComplete: true,
},
refsChecked: 1,
skippedExecRefs: 0,
warningCount: 0,
warnings: [],
},
});
}),
);
confirm.mockResolvedValue(true);
runSecretsApply.mockResolvedValue({
mode: "write",
changed: true,
changedFiles: ["/tmp/openclaw.json"],
checks: {
resolvability: true,
resolvabilityComplete: true,
},
refsChecked: 1,
skippedExecRefs: 0,
warningCount: 0,
warnings: [],
});
runSecretsApply.mockResolvedValue(createSecretsApplyResult({ mode: "write", changed: true }));
await createProgram().parseAsync(["secrets", "configure"], { from: "user" });
expect(runSecretsConfigureInteractive).toHaveBeenCalled();
@@ -209,28 +253,7 @@ describe("secrets CLI", () => {
});
it("forwards --agent to secrets configure", async () => {
runSecretsConfigureInteractive.mockResolvedValue({
plan: {
version: 1,
protocolVersion: 1,
generatedAt: "2026-02-26T00:00:00.000Z",
generatedBy: "openclaw secrets configure",
targets: [],
},
preflight: {
mode: "dry-run",
changed: false,
changedFiles: [],
checks: {
resolvability: true,
resolvabilityComplete: true,
},
refsChecked: 0,
skippedExecRefs: 0,
warningCount: 0,
warnings: [],
},
});
runSecretsConfigureInteractive.mockResolvedValue(createConfigureInteractiveResult());
confirm.mockResolvedValue(false);
await createProgram().parseAsync(["secrets", "configure", "--agent", "ops"], { from: "user" });
@@ -243,154 +266,57 @@ describe("secrets CLI", () => {
});
it("forwards --allow-exec to secrets apply dry-run", async () => {
const planPath = path.join(
os.tmpdir(),
`openclaw-secrets-cli-test-${Date.now()}-${Math.random().toString(16).slice(2)}.json`,
);
await fs.writeFile(
planPath,
`${JSON.stringify({
version: 1,
protocolVersion: 1,
generatedAt: new Date().toISOString(),
generatedBy: "manual",
targets: [],
})}\n`,
"utf8",
);
runSecretsApply.mockResolvedValue({
mode: "dry-run",
changed: false,
changedFiles: [],
checks: {
resolvability: true,
resolvabilityComplete: true,
},
refsChecked: 0,
skippedExecRefs: 0,
warningCount: 0,
warnings: [],
});
await withPlanFile(async (planPath) => {
runSecretsApply.mockResolvedValue(createSecretsApplyResult());
await createProgram().parseAsync(
["secrets", "apply", "--from", planPath, "--dry-run", "--allow-exec"],
{
from: "user",
},
);
expect(runSecretsApply).toHaveBeenCalledWith(
expect.objectContaining({
write: false,
allowExec: true,
}),
);
await fs.rm(planPath, { force: true });
await createProgram().parseAsync(
["secrets", "apply", "--from", planPath, "--dry-run", "--allow-exec"],
{
from: "user",
},
);
expect(runSecretsApply).toHaveBeenCalledWith(
expect.objectContaining({
write: false,
allowExec: true,
}),
);
});
});
it("forwards --allow-exec to secrets apply write mode", async () => {
const planPath = path.join(
os.tmpdir(),
`openclaw-secrets-cli-test-${Date.now()}-${Math.random().toString(16).slice(2)}.json`,
);
await fs.writeFile(
planPath,
`${JSON.stringify({
version: 1,
protocolVersion: 1,
generatedAt: new Date().toISOString(),
generatedBy: "manual",
targets: [],
})}\n`,
"utf8",
);
runSecretsApply.mockResolvedValue({
mode: "write",
changed: false,
changedFiles: [],
checks: {
resolvability: true,
resolvabilityComplete: true,
},
refsChecked: 0,
skippedExecRefs: 0,
warningCount: 0,
warnings: [],
});
await withPlanFile(async (planPath) => {
runSecretsApply.mockResolvedValue(createSecretsApplyResult({ mode: "write" }));
await createProgram().parseAsync(["secrets", "apply", "--from", planPath, "--allow-exec"], {
from: "user",
await createProgram().parseAsync(["secrets", "apply", "--from", planPath, "--allow-exec"], {
from: "user",
});
expect(runSecretsApply).toHaveBeenCalledWith(
expect.objectContaining({
write: true,
allowExec: true,
}),
);
});
expect(runSecretsApply).toHaveBeenCalledWith(
expect.objectContaining({
write: true,
allowExec: true,
}),
);
await fs.rm(planPath, { force: true });
});
it("does not print skipped-exec note when apply dry-run skippedExecRefs is zero", async () => {
const planPath = path.join(
os.tmpdir(),
`openclaw-secrets-cli-test-${Date.now()}-${Math.random().toString(16).slice(2)}.json`,
);
await fs.writeFile(
planPath,
`${JSON.stringify({
version: 1,
protocolVersion: 1,
generatedAt: new Date().toISOString(),
generatedBy: "manual",
targets: [],
})}\n`,
"utf8",
);
runSecretsApply.mockResolvedValue({
mode: "dry-run",
changed: false,
changedFiles: [],
checks: {
resolvability: true,
resolvabilityComplete: false,
},
refsChecked: 0,
skippedExecRefs: 0,
warningCount: 0,
warnings: [],
});
await withPlanFile(async (planPath) => {
runSecretsApply.mockResolvedValue(createSecretsApplyResult({ resolvabilityComplete: false }));
await createProgram().parseAsync(["secrets", "apply", "--from", planPath, "--dry-run"], {
from: "user",
await createProgram().parseAsync(["secrets", "apply", "--from", planPath, "--dry-run"], {
from: "user",
});
expect(runtimeLogs.some((line) => line.includes("Secrets apply dry-run note: skipped"))).toBe(
false,
);
});
expect(runtimeLogs.some((line) => line.includes("Secrets apply dry-run note: skipped"))).toBe(
false,
);
await fs.rm(planPath, { force: true });
});
it("does not print skipped-exec note when configure preflight skippedExecRefs is zero", async () => {
runSecretsConfigureInteractive.mockResolvedValue({
plan: {
version: 1,
protocolVersion: 1,
generatedAt: "2026-02-26T00:00:00.000Z",
generatedBy: "openclaw secrets configure",
targets: [],
},
preflight: {
mode: "dry-run",
changed: false,
changedFiles: [],
checks: {
resolvability: true,
resolvabilityComplete: false,
},
refsChecked: 0,
skippedExecRefs: 0,
warningCount: 0,
warnings: [],
},
});
runSecretsConfigureInteractive.mockResolvedValue(
createConfigureInteractiveResult({ resolvabilityComplete: false }),
);
confirm.mockResolvedValue(false);
await createProgram().parseAsync(["secrets", "configure"], { from: "user" });
@@ -398,41 +324,8 @@ describe("secrets CLI", () => {
});
it("forwards --allow-exec to configure preflight and apply", async () => {
runSecretsConfigureInteractive.mockResolvedValue({
plan: {
version: 1,
protocolVersion: 1,
generatedAt: "2026-02-26T00:00:00.000Z",
generatedBy: "openclaw secrets configure",
targets: [],
},
preflight: {
mode: "dry-run",
changed: false,
changedFiles: [],
checks: {
resolvability: true,
resolvabilityComplete: true,
},
refsChecked: 0,
skippedExecRefs: 0,
warningCount: 0,
warnings: [],
},
});
runSecretsApply.mockResolvedValue({
mode: "write",
changed: false,
changedFiles: [],
checks: {
resolvability: true,
resolvabilityComplete: true,
},
refsChecked: 0,
skippedExecRefs: 0,
warningCount: 0,
warnings: [],
});
runSecretsConfigureInteractive.mockResolvedValue(createConfigureInteractiveResult());
runSecretsApply.mockResolvedValue(createSecretsApplyResult({ mode: "write" }));
await createProgram().parseAsync(["secrets", "configure", "--apply", "--yes", "--allow-exec"], {
from: "user",

View File

@@ -45,6 +45,22 @@ function createProgram() {
return program;
}
function primeDeepAuditConfig(sourceConfig = { gateway: { mode: "local" } }) {
loadConfig.mockReturnValue(sourceConfig);
resolveCommandSecretRefsViaGateway.mockResolvedValue({
resolvedConfig: sourceConfig,
diagnostics: [],
targetStatesByPath: {},
hadUnresolvedTargets: false,
});
runSecurityAudit.mockResolvedValue({
ts: 0,
summary: { critical: 0, warn: 0, info: 0 },
findings: [],
});
return sourceConfig;
}
describe("security CLI", () => {
beforeEach(() => {
resetRuntimeCapture();
@@ -131,27 +147,31 @@ describe("security CLI", () => {
]);
});
it("forwards --token to deep probe auth without altering command-level resolver mode", async () => {
const sourceConfig = { gateway: { mode: "local" } };
loadConfig.mockReturnValue(sourceConfig);
resolveCommandSecretRefsViaGateway.mockResolvedValue({
resolvedConfig: sourceConfig,
diagnostics: [],
targetStatesByPath: {},
hadUnresolvedTargets: false,
});
runSecurityAudit.mockResolvedValue({
ts: 0,
summary: { critical: 0, warn: 0, info: 0 },
findings: [],
});
await createProgram().parseAsync(
["security", "audit", "--deep", "--token", "explicit-token", "--json"],
{
from: "user",
it.each([
{
title: "forwards --token to deep probe auth without altering command-level resolver mode",
argv: ["--token", "explicit-token"],
deepProbeAuth: { token: "explicit-token" },
},
{
title: "forwards --password to deep probe auth without altering command-level resolver mode",
argv: ["--password", "explicit-password"],
deepProbeAuth: { password: "explicit-password" },
},
{
title: "forwards both --token and --password to deep probe auth",
argv: ["--token", "explicit-token", "--password", "explicit-password"],
deepProbeAuth: {
token: "explicit-token",
password: "explicit-password",
},
);
},
])("$title", async ({ argv, deepProbeAuth }) => {
primeDeepAuditConfig();
await createProgram().parseAsync(["security", "audit", "--deep", ...argv, "--json"], {
from: "user",
});
expect(resolveCommandSecretRefsViaGateway).toHaveBeenCalledWith(
expect.objectContaining({
@@ -161,84 +181,7 @@ describe("security CLI", () => {
expect(runSecurityAudit).toHaveBeenCalledWith(
expect.objectContaining({
deep: true,
deepProbeAuth: { token: "explicit-token" },
}),
);
});
it("forwards --password to deep probe auth without altering command-level resolver mode", async () => {
const sourceConfig = { gateway: { mode: "local" } };
loadConfig.mockReturnValue(sourceConfig);
resolveCommandSecretRefsViaGateway.mockResolvedValue({
resolvedConfig: sourceConfig,
diagnostics: [],
targetStatesByPath: {},
hadUnresolvedTargets: false,
});
runSecurityAudit.mockResolvedValue({
ts: 0,
summary: { critical: 0, warn: 0, info: 0 },
findings: [],
});
await createProgram().parseAsync(
["security", "audit", "--deep", "--password", "explicit-password", "--json"],
{
from: "user",
},
);
expect(resolveCommandSecretRefsViaGateway).toHaveBeenCalledWith(
expect.objectContaining({
mode: "read_only_status",
}),
);
expect(runSecurityAudit).toHaveBeenCalledWith(
expect.objectContaining({
deep: true,
deepProbeAuth: { password: "explicit-password" },
}),
);
});
it("forwards both --token and --password to deep probe auth", async () => {
const sourceConfig = { gateway: { mode: "local" } };
loadConfig.mockReturnValue(sourceConfig);
resolveCommandSecretRefsViaGateway.mockResolvedValue({
resolvedConfig: sourceConfig,
diagnostics: [],
targetStatesByPath: {},
hadUnresolvedTargets: false,
});
runSecurityAudit.mockResolvedValue({
ts: 0,
summary: { critical: 0, warn: 0, info: 0 },
findings: [],
});
await createProgram().parseAsync(
[
"security",
"audit",
"--deep",
"--token",
"explicit-token",
"--password",
"explicit-password",
"--json",
],
{
from: "user",
},
);
expect(runSecurityAudit).toHaveBeenCalledWith(
expect.objectContaining({
deep: true,
deepProbeAuth: {
token: "explicit-token",
password: "explicit-password",
},
deepProbeAuth,
}),
);
});

View File

@@ -9,6 +9,10 @@ import {
} from "./channel-setup/plugin-install.js";
import { setDefaultChannelPluginRegistryForTests } from "./channel-test-helpers.js";
import { configMocks, offsetMocks } from "./channels.mock-harness.js";
import {
createMSTeamsCatalogEntry,
createMSTeamsSetupPlugin,
} from "./channels.plugin-install.test-helpers.js";
import { baseConfigSnapshot, createTestRuntime } from "./test-runtime-config-helpers.js";
const catalogMocks = vi.hoisted(() => ({
@@ -37,55 +41,14 @@ vi.mock("../plugins/manifest-registry.js", async (importOriginal) => {
vi.mock("./channel-setup/plugin-install.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./channel-setup/plugin-install.js")>();
return {
...actual,
ensureChannelSetupPluginInstalled: vi.fn(async ({ cfg }) => ({ cfg, installed: true })),
loadChannelSetupPluginRegistrySnapshotForChannel: vi.fn(() => createTestRegistry()),
};
const { createMockChannelSetupPluginInstallModule } =
await import("./channels.plugin-install.test-helpers.js");
return createMockChannelSetupPluginInstallModule(actual);
});
const runtime = createTestRuntime();
let channelsAddCommand: typeof import("./channels.js").channelsAddCommand;
function createMSTeamsCatalogEntry(): ChannelPluginCatalogEntry {
return {
id: "msteams",
pluginId: "@openclaw/msteams-plugin",
meta: {
id: "msteams",
label: "Microsoft Teams",
selectionLabel: "Microsoft Teams",
docsPath: "/channels/msteams",
blurb: "teams channel",
},
install: {
npmSpec: "@openclaw/msteams",
},
};
}
function createMSTeamsSetupPlugin(): ChannelPlugin {
return {
...createChannelTestPluginBase({
id: "msteams",
label: "Microsoft Teams",
docsPath: "/channels/msteams",
}),
setup: {
applyAccountConfig: vi.fn(({ cfg, input }) => ({
...cfg,
channels: {
...cfg.channels,
msteams: {
enabled: true,
tenantId: input.token,
},
},
})),
},
} as ChannelPlugin;
}
function registerMSTeamsSetupPlugin(pluginId = "@openclaw/msteams-plugin"): void {
vi.mocked(loadChannelSetupPluginRegistrySnapshotForChannel).mockReturnValue(
createTestRegistry([{ pluginId, plugin: createMSTeamsSetupPlugin(), source: "test" }]),
@@ -124,6 +87,17 @@ function createSignalPlugin(
} as ChannelPlugin;
}
async function runSignalAddCommand(afterAccountConfigWritten: SignalAfterAccountConfigWritten) {
const plugin = createSignalPlugin(afterAccountConfigWritten);
setActivePluginRegistry(createTestRegistry([{ pluginId: "signal", plugin, source: "test" }]));
configMocks.readConfigFileSnapshot.mockResolvedValue({ ...baseConfigSnapshot });
await channelsAddCommand(
{ channel: "signal", account: "ops", signalNumber: "+15550001" },
runtime,
{ hasFlags: true },
);
}
describe("channelsAddCommand", () => {
beforeAll(async () => {
({ channelsAddCommand } = await import("./channels.js"));
@@ -352,15 +326,7 @@ describe("channelsAddCommand", () => {
it("runs post-setup hooks after writing config", async () => {
const afterAccountConfigWritten = vi.fn().mockResolvedValue(undefined);
const plugin = createSignalPlugin(afterAccountConfigWritten);
setActivePluginRegistry(createTestRegistry([{ pluginId: "signal", plugin, source: "test" }]));
configMocks.readConfigFileSnapshot.mockResolvedValue({ ...baseConfigSnapshot });
await channelsAddCommand(
{ channel: "signal", account: "ops", signalNumber: "+15550001" },
runtime,
{ hasFlags: true },
);
await runSignalAddCommand(afterAccountConfigWritten);
expect(configMocks.writeConfigFile).toHaveBeenCalledTimes(1);
expect(afterAccountConfigWritten).toHaveBeenCalledTimes(1);
@@ -391,15 +357,7 @@ describe("channelsAddCommand", () => {
it("keeps the saved config when a post-setup hook fails", async () => {
const afterAccountConfigWritten = vi.fn().mockRejectedValue(new Error("hook failed"));
const plugin = createSignalPlugin(afterAccountConfigWritten);
setActivePluginRegistry(createTestRegistry([{ pluginId: "signal", plugin, source: "test" }]));
configMocks.readConfigFileSnapshot.mockResolvedValue({ ...baseConfigSnapshot });
await channelsAddCommand(
{ channel: "signal", account: "ops", signalNumber: "+15550001" },
runtime,
{ hasFlags: true },
);
await runSignalAddCommand(afterAccountConfigWritten);
expect(configMocks.writeConfigFile).toHaveBeenCalledTimes(1);
expect(runtime.exit).not.toHaveBeenCalled();

View File

@@ -0,0 +1,79 @@
import { vi } from "vitest";
import type { ChannelPluginCatalogEntry } from "../channels/plugins/catalog.js";
import type { ChannelPlugin } from "../channels/plugins/types.js";
import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js";
export function createMockChannelSetupPluginInstallModule(
actual: typeof import("./channel-setup/plugin-install.js"),
) {
return {
...actual,
ensureChannelSetupPluginInstalled: vi.fn(async ({ cfg }) => ({ cfg, installed: true })),
loadChannelSetupPluginRegistrySnapshotForChannel: vi.fn(() => createTestRegistry()),
};
}
export function createMSTeamsCatalogEntry(): ChannelPluginCatalogEntry {
return {
id: "msteams",
pluginId: "@openclaw/msteams-plugin",
meta: {
id: "msteams",
label: "Microsoft Teams",
selectionLabel: "Microsoft Teams",
docsPath: "/channels/msteams",
blurb: "teams channel",
},
install: {
npmSpec: "@openclaw/msteams",
},
};
}
export function createMSTeamsSetupPlugin(): ChannelPlugin {
return {
...createChannelTestPluginBase({
id: "msteams",
label: "Microsoft Teams",
docsPath: "/channels/msteams",
}),
setup: {
applyAccountConfig: vi.fn(({ cfg, input }) => ({
...cfg,
channels: {
...cfg.channels,
msteams: {
enabled: true,
tenantId: input.token,
},
},
})),
},
} as ChannelPlugin;
}
export function createMSTeamsDeletePlugin(): ChannelPlugin {
return {
...createChannelTestPluginBase({
id: "msteams",
label: "Microsoft Teams",
docsPath: "/channels/msteams",
}),
config: {
...createChannelTestPluginBase({
id: "msteams",
label: "Microsoft Teams",
docsPath: "/channels/msteams",
}).config,
deleteAccount: vi.fn(({ cfg }: { cfg: Record<string, unknown> }) => {
const channels = (cfg.channels as Record<string, unknown> | undefined) ?? {};
const nextChannels = { ...channels };
delete nextChannels.msteams;
return {
...cfg,
channels: nextChannels,
};
}),
},
};
}

View File

@@ -1,12 +1,16 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { ChannelPluginCatalogEntry } from "../channels/plugins/catalog.js";
import { setActivePluginRegistry } from "../plugins/runtime.js";
import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js";
import { createTestRegistry } from "../test-utils/channel-plugins.js";
import {
ensureChannelSetupPluginInstalled,
loadChannelSetupPluginRegistrySnapshotForChannel,
} from "./channel-setup/plugin-install.js";
import { configMocks } from "./channels.mock-harness.js";
import {
createMSTeamsCatalogEntry,
createMSTeamsDeletePlugin,
} from "./channels.plugin-install.test-helpers.js";
import { baseConfigSnapshot, createTestRuntime } from "./test-runtime-config-helpers.js";
const catalogMocks = vi.hoisted(() => ({
@@ -23,11 +27,9 @@ vi.mock("../channels/plugins/catalog.js", async (importOriginal) => {
vi.mock("./channel-setup/plugin-install.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./channel-setup/plugin-install.js")>();
return {
...actual,
ensureChannelSetupPluginInstalled: vi.fn(async ({ cfg }) => ({ cfg, installed: true })),
loadChannelSetupPluginRegistrySnapshotForChannel: vi.fn(() => createTestRegistry()),
};
const { createMockChannelSetupPluginInstallModule } =
await import("./channels.plugin-install.test-helpers.js");
return createMockChannelSetupPluginInstallModule(actual);
});
const runtime = createTestRuntime();
@@ -70,44 +72,9 @@ describe("channelsRemoveCommand", () => {
},
},
});
const catalogEntry: ChannelPluginCatalogEntry = {
id: "msteams",
pluginId: "@openclaw/msteams-plugin",
meta: {
id: "msteams",
label: "Microsoft Teams",
selectionLabel: "Microsoft Teams",
docsPath: "/channels/msteams",
blurb: "teams channel",
},
install: {
npmSpec: "@openclaw/msteams",
},
};
const catalogEntry: ChannelPluginCatalogEntry = createMSTeamsCatalogEntry();
catalogMocks.listChannelPluginCatalogEntries.mockReturnValue([catalogEntry]);
const scopedPlugin = {
...createChannelTestPluginBase({
id: "msteams",
label: "Microsoft Teams",
docsPath: "/channels/msteams",
}),
config: {
...createChannelTestPluginBase({
id: "msteams",
label: "Microsoft Teams",
docsPath: "/channels/msteams",
}).config,
deleteAccount: vi.fn(({ cfg }: { cfg: Record<string, unknown> }) => {
const channels = (cfg.channels as Record<string, unknown> | undefined) ?? {};
const nextChannels = { ...channels };
delete nextChannels.msteams;
return {
...cfg,
channels: nextChannels,
};
}),
},
};
const scopedPlugin = createMSTeamsDeletePlugin();
vi.mocked(loadChannelSetupPluginRegistrySnapshotForChannel)
.mockReturnValueOnce(createTestRegistry())
.mockReturnValueOnce(