test(refactor): dedupe cli and ios script scenarios

This commit is contained in:
Peter Steinberger
2026-03-02 11:16:33 +00:00
parent 1b98879295
commit 0c2d85529a
2 changed files with 121 additions and 82 deletions

View File

@@ -72,49 +72,87 @@ afterEach(() => {
}); });
describe("registerPreActionHooks", () => { describe("registerPreActionHooks", () => {
function buildProgram() { type CommandKey =
| "status"
| "doctor"
| "completion"
| "secrets"
| "update-status"
| "config-set"
| "agents"
| "configure"
| "onboard"
| "message-send";
function buildProgram(keys: readonly CommandKey[]) {
const enabled = new Set<CommandKey>(keys);
const has = (key: CommandKey) => enabled.has(key);
const program = new Command().name("openclaw"); const program = new Command().name("openclaw");
program.command("status").action(() => {}); if (has("status")) {
program.command("doctor").action(() => {}); program.command("status").action(() => {});
program.command("completion").action(() => {}); }
program.command("secrets").action(() => {}); if (has("doctor")) {
program program.command("doctor").action(() => {});
.command("update") }
.command("status") if (has("completion")) {
.option("--json") program.command("completion").action(() => {});
.action(() => {}); }
const config = program.command("config"); if (has("secrets")) {
config program.command("secrets").action(() => {});
.command("set") }
.argument("<path>") if (has("update-status")) {
.argument("<value>") program
.option("--json") .command("update")
.action(() => {}); .command("status")
program.command("agents").action(() => {}); .option("--json")
program.command("configure").action(() => {}); .action(() => {});
program.command("onboard").action(() => {}); }
program if (has("config-set")) {
.command("message") const config = program.command("config");
.command("send") config
.option("--json") .command("set")
.action(() => {}); .argument("<path>")
.argument("<value>")
.option("--json")
.action(() => {});
}
if (has("agents")) {
program.command("agents").action(() => {});
}
if (has("configure")) {
program.command("configure").action(() => {});
}
if (has("onboard")) {
program.command("onboard").action(() => {});
}
if (has("message-send")) {
program
.command("message")
.command("send")
.option("--json")
.action(() => {});
}
registerPreActionHooks(program, "9.9.9-test"); registerPreActionHooks(program, "9.9.9-test");
return program; return program;
} }
async function runCommand( async function runCommand(
params: { parseArgv: string[]; processArgv?: string[] }, params: { parseArgv: string[]; processArgv?: string[] },
program = buildProgram(), program: Command,
) { ) {
process.argv = params.processArgv ?? [...params.parseArgv]; process.argv = params.processArgv ?? [...params.parseArgv];
await program.parseAsync(params.parseArgv, { from: "user" }); await program.parseAsync(params.parseArgv, { from: "user" });
} }
it("emits banner, resolves config, and enables verbose from --debug", async () => { it("emits banner, resolves config, and enables verbose from --debug", async () => {
await runCommand({ const program = buildProgram(["status"]);
parseArgv: ["status"], await runCommand(
processArgv: ["node", "openclaw", "status", "--debug"], {
}); parseArgv: ["status"],
processArgv: ["node", "openclaw", "status", "--debug"],
},
program,
);
expect(emitCliBannerMock).toHaveBeenCalledWith("9.9.9-test"); expect(emitCliBannerMock).toHaveBeenCalledWith("9.9.9-test");
expect(setVerboseMock).toHaveBeenCalledWith(true); expect(setVerboseMock).toHaveBeenCalledWith(true);
@@ -127,10 +165,14 @@ describe("registerPreActionHooks", () => {
}); });
it("loads plugin registry for plugin-required commands", async () => { it("loads plugin registry for plugin-required commands", async () => {
await runCommand({ const program = buildProgram(["message-send"]);
parseArgv: ["message", "send"], await runCommand(
processArgv: ["node", "openclaw", "message", "send"], {
}); parseArgv: ["message", "send"],
processArgv: ["node", "openclaw", "message", "send"],
},
program,
);
expect(setVerboseMock).toHaveBeenCalledWith(false); expect(setVerboseMock).toHaveBeenCalledWith(false);
expect(process.env.NODE_NO_WARNINGS).toBe("1"); expect(process.env.NODE_NO_WARNINGS).toBe("1");
@@ -143,7 +185,7 @@ describe("registerPreActionHooks", () => {
it("loads plugin registry for configure/onboard/agents commands", async () => { it("loads plugin registry for configure/onboard/agents commands", async () => {
const commands = ["configure", "onboard", "agents"] as const; const commands = ["configure", "onboard", "agents"] as const;
const program = buildProgram(); const program = buildProgram(["configure", "onboard", "agents"]);
for (const command of commands) { for (const command of commands) {
vi.clearAllMocks(); vi.clearAllMocks();
await runCommand( await runCommand(
@@ -158,7 +200,7 @@ describe("registerPreActionHooks", () => {
}); });
it("skips config guard for doctor, completion, and secrets commands", async () => { it("skips config guard for doctor, completion, and secrets commands", async () => {
const program = buildProgram(); const program = buildProgram(["doctor", "completion", "secrets"]);
await runCommand( await runCommand(
{ {
parseArgv: ["doctor"], parseArgv: ["doctor"],
@@ -185,10 +227,14 @@ describe("registerPreActionHooks", () => {
}); });
it("skips preaction work when argv indicates help/version", async () => { it("skips preaction work when argv indicates help/version", async () => {
await runCommand({ const program = buildProgram(["status"]);
parseArgv: ["status"], await runCommand(
processArgv: ["node", "openclaw", "--version"], {
}); parseArgv: ["status"],
processArgv: ["node", "openclaw", "--version"],
},
program,
);
expect(emitCliBannerMock).not.toHaveBeenCalled(); expect(emitCliBannerMock).not.toHaveBeenCalled();
expect(setVerboseMock).not.toHaveBeenCalled(); expect(setVerboseMock).not.toHaveBeenCalled();
@@ -197,17 +243,21 @@ describe("registerPreActionHooks", () => {
it("hides banner when OPENCLAW_HIDE_BANNER is truthy", async () => { it("hides banner when OPENCLAW_HIDE_BANNER is truthy", async () => {
process.env.OPENCLAW_HIDE_BANNER = "1"; process.env.OPENCLAW_HIDE_BANNER = "1";
await runCommand({ const program = buildProgram(["status"]);
parseArgv: ["status"], await runCommand(
processArgv: ["node", "openclaw", "status"], {
}); parseArgv: ["status"],
processArgv: ["node", "openclaw", "status"],
},
program,
);
expect(emitCliBannerMock).not.toHaveBeenCalled(); expect(emitCliBannerMock).not.toHaveBeenCalled();
expect(ensureConfigReadyMock).toHaveBeenCalledTimes(1); expect(ensureConfigReadyMock).toHaveBeenCalledTimes(1);
}); });
it("suppresses doctor stdout for any --json output command", async () => { it("suppresses doctor stdout for any --json output command", async () => {
const program = buildProgram(); const program = buildProgram(["message-send", "update-status"]);
await runCommand( await runCommand(
{ {
parseArgv: ["message", "send", "--json"], parseArgv: ["message", "send", "--json"],
@@ -240,10 +290,14 @@ describe("registerPreActionHooks", () => {
}); });
it("does not treat config set --json (strict-parse alias) as json output mode", async () => { it("does not treat config set --json (strict-parse alias) as json output mode", async () => {
await runCommand({ const program = buildProgram(["config-set"]);
parseArgv: ["config", "set", "gateway.auth.mode", "{bad", "--json"], await runCommand(
processArgv: ["node", "openclaw", "config", "set", "gateway.auth.mode", "{bad", "--json"], {
}); parseArgv: ["config", "set", "gateway.auth.mode", "{bad", "--json"],
processArgv: ["node", "openclaw", "config", "set", "gateway.auth.mode", "{bad", "--json"],
},
program,
);
expect(ensureConfigReadyMock).toHaveBeenCalledWith({ expect(ensureConfigReadyMock).toHaveBeenCalledWith({
runtime: runtimeMock, runtime: runtimeMock,

View File

@@ -6,6 +6,9 @@ import path from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { afterAll, beforeAll, describe, expect, it } from "vitest";
const SCRIPT = path.join(process.cwd(), "scripts", "ios-team-id.sh"); const SCRIPT = path.join(process.cwd(), "scripts", "ios-team-id.sh");
const BASH_BIN = process.platform === "win32" ? "bash" : "/bin/bash";
const BASE_PATH = process.env.PATH ?? "/usr/bin:/bin";
const BASE_LANG = process.env.LANG ?? "C";
let fixtureRoot = ""; let fixtureRoot = "";
let sharedBinDir = ""; let sharedBinDir = "";
let caseId = 0; let caseId = 0;
@@ -25,13 +28,13 @@ function runScript(
} { } {
const binDir = path.join(homeDir, "bin"); const binDir = path.join(homeDir, "bin");
const env = { const env = {
...process.env,
HOME: homeDir, HOME: homeDir,
PATH: `${binDir}:${sharedBinDir}:${process.env.PATH ?? ""}`, PATH: `${binDir}:${sharedBinDir}:${BASE_PATH}`,
LANG: BASE_LANG,
...extraEnv, ...extraEnv,
}; };
try { try {
const stdout = execFileSync("bash", [SCRIPT], { const stdout = execFileSync(BASH_BIN, [SCRIPT], {
env, env,
encoding: "utf8", encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"], stdio: ["ignore", "pipe", "pipe"],
@@ -109,19 +112,20 @@ exit 1`,
return { homeDir, binDir }; return { homeDir, binDir };
} }
it("falls back to Xcode-managed provisioning profiles when preference teams are empty", async () => { it("resolves fallback and preferred team IDs from provisioning profiles", async () => {
const { homeDir } = await createHomeDir(); const { homeDir } = await createHomeDir();
await mkdir(path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles"), { const profilesDir = path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles");
recursive: true, await mkdir(profilesDir, { recursive: true });
}); await writeFile(path.join(profilesDir, "one.mobileprovision"), "stub1");
await writeFile(
path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles", "one.mobileprovision"),
"stub",
);
const result = runScript(homeDir); const fallbackResult = runScript(homeDir);
expect(result.ok).toBe(true); expect(fallbackResult.ok).toBe(true);
expect(result.stdout).toBe("AAAAA11111"); expect(fallbackResult.stdout).toBe("AAAAA11111");
await writeFile(path.join(profilesDir, "two.mobileprovision"), "stub2");
const preferredResult = runScript(homeDir, { IOS_PREFERRED_TEAM_ID: "BBBBB22222" });
expect(preferredResult.ok).toBe(true);
expect(preferredResult.stdout).toBe("BBBBB22222");
}); });
it("prints actionable guidance when Xcode account exists but no Team ID is resolvable", async () => { it("prints actionable guidance when Xcode account exists but no Team ID is resolvable", async () => {
@@ -148,25 +152,6 @@ exit 1`,
expect(result.stderr).toContain("IOS_DEVELOPMENT_TEAM"); expect(result.stderr).toContain("IOS_DEVELOPMENT_TEAM");
}); });
it("honors IOS_PREFERRED_TEAM_ID when multiple profile teams are available", async () => {
const { homeDir } = await createHomeDir();
await mkdir(path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles"), {
recursive: true,
});
await writeFile(
path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles", "one.mobileprovision"),
"stub1",
);
await writeFile(
path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles", "two.mobileprovision"),
"stub2",
);
const result = runScript(homeDir, { IOS_PREFERRED_TEAM_ID: "BBBBB22222" });
expect(result.ok).toBe(true);
expect(result.stdout).toBe("BBBBB22222");
});
it("matches preferred team IDs even when parser output uses CRLF line endings", async () => { it("matches preferred team IDs even when parser output uses CRLF line endings", async () => {
const { homeDir, binDir } = await createHomeDir(); const { homeDir, binDir } = await createHomeDir();
await writeExecutable( await writeExecutable(