test: dedupe utility and config suites

This commit is contained in:
Peter Steinberger
2026-03-28 00:26:10 +00:00
parent d8f97358d7
commit fef688fb7a
24 changed files with 1178 additions and 1312 deletions

View File

@@ -316,69 +316,78 @@ describe("argv helpers", () => {
expect(getPositiveIntFlagValue(argv, "--timeout")).toBe(expected);
});
it("builds parse argv from raw args", () => {
const cases = [
{
rawArgs: ["node", "openclaw", "status"],
expected: ["node", "openclaw", "status"],
},
{
rawArgs: ["node-22", "openclaw", "status"],
expected: ["node-22", "openclaw", "status"],
},
{
rawArgs: ["node-22.2.0.exe", "openclaw", "status"],
expected: ["node-22.2.0.exe", "openclaw", "status"],
},
{
rawArgs: ["node-22.2", "openclaw", "status"],
expected: ["node-22.2", "openclaw", "status"],
},
{
rawArgs: ["node-22.2.exe", "openclaw", "status"],
expected: ["node-22.2.exe", "openclaw", "status"],
},
{
rawArgs: ["/usr/bin/node-22.2.0", "openclaw", "status"],
expected: ["/usr/bin/node-22.2.0", "openclaw", "status"],
},
{
rawArgs: ["node24", "openclaw", "status"],
expected: ["node24", "openclaw", "status"],
},
{
rawArgs: ["/usr/bin/node24", "openclaw", "status"],
expected: ["/usr/bin/node24", "openclaw", "status"],
},
{
rawArgs: ["node24.exe", "openclaw", "status"],
expected: ["node24.exe", "openclaw", "status"],
},
{
rawArgs: ["nodejs", "openclaw", "status"],
expected: ["nodejs", "openclaw", "status"],
},
{
rawArgs: ["node-dev", "openclaw", "status"],
expected: ["node", "openclaw", "node-dev", "openclaw", "status"],
},
{
rawArgs: ["openclaw", "status"],
expected: ["node", "openclaw", "status"],
},
{
rawArgs: ["bun", "src/entry.ts", "status"],
expected: ["bun", "src/entry.ts", "status"],
},
] as const;
for (const testCase of cases) {
const parsed = buildParseArgv({
programName: "openclaw",
rawArgs: [...testCase.rawArgs],
});
expect(parsed).toEqual([...testCase.expected]);
}
it.each([
{
name: "keeps plain node argv",
rawArgs: ["node", "openclaw", "status"],
expected: ["node", "openclaw", "status"],
},
{
name: "keeps version-suffixed node binary",
rawArgs: ["node-22", "openclaw", "status"],
expected: ["node-22", "openclaw", "status"],
},
{
name: "keeps windows versioned node exe",
rawArgs: ["node-22.2.0.exe", "openclaw", "status"],
expected: ["node-22.2.0.exe", "openclaw", "status"],
},
{
name: "keeps dotted node binary",
rawArgs: ["node-22.2", "openclaw", "status"],
expected: ["node-22.2", "openclaw", "status"],
},
{
name: "keeps dotted node exe",
rawArgs: ["node-22.2.exe", "openclaw", "status"],
expected: ["node-22.2.exe", "openclaw", "status"],
},
{
name: "keeps absolute versioned node path",
rawArgs: ["/usr/bin/node-22.2.0", "openclaw", "status"],
expected: ["/usr/bin/node-22.2.0", "openclaw", "status"],
},
{
name: "keeps node24 shorthand",
rawArgs: ["node24", "openclaw", "status"],
expected: ["node24", "openclaw", "status"],
},
{
name: "keeps absolute node24 shorthand",
rawArgs: ["/usr/bin/node24", "openclaw", "status"],
expected: ["/usr/bin/node24", "openclaw", "status"],
},
{
name: "keeps windows node24 exe",
rawArgs: ["node24.exe", "openclaw", "status"],
expected: ["node24.exe", "openclaw", "status"],
},
{
name: "keeps nodejs binary",
rawArgs: ["nodejs", "openclaw", "status"],
expected: ["nodejs", "openclaw", "status"],
},
{
name: "prefixes fallback when first arg is not a node launcher",
rawArgs: ["node-dev", "openclaw", "status"],
expected: ["node", "openclaw", "node-dev", "openclaw", "status"],
},
{
name: "prefixes fallback when raw args start at program name",
rawArgs: ["openclaw", "status"],
expected: ["node", "openclaw", "status"],
},
{
name: "keeps bun execution argv",
rawArgs: ["bun", "src/entry.ts", "status"],
expected: ["bun", "src/entry.ts", "status"],
},
] as const)("builds parse argv from raw args: $name", ({ rawArgs, expected }) => {
const parsed = buildParseArgv({
programName: "openclaw",
rawArgs: [...rawArgs],
});
expect(parsed).toEqual([...expected]);
});
it("builds parse argv from fallback args", () => {
@@ -389,29 +398,20 @@ describe("argv helpers", () => {
expect(fallbackArgv).toEqual(["node", "openclaw", "status"]);
});
it("decides when to migrate state", () => {
const nonMutatingArgv = [
["node", "openclaw", "status"],
["node", "openclaw", "health"],
["node", "openclaw", "sessions"],
["node", "openclaw", "config", "get", "update"],
["node", "openclaw", "config", "unset", "update"],
["node", "openclaw", "models", "list"],
["node", "openclaw", "models", "status"],
["node", "openclaw", "update", "status", "--json"],
["node", "openclaw", "agent", "--message", "hi"],
] as const;
const mutatingArgv = [
["node", "openclaw", "agents", "list"],
["node", "openclaw", "message", "send"],
] as const;
for (const argv of nonMutatingArgv) {
expect(shouldMigrateState([...argv])).toBe(false);
}
for (const argv of mutatingArgv) {
expect(shouldMigrateState([...argv])).toBe(true);
}
it.each([
{ argv: ["node", "openclaw", "status"], expected: false },
{ argv: ["node", "openclaw", "health"], expected: false },
{ argv: ["node", "openclaw", "sessions"], expected: false },
{ argv: ["node", "openclaw", "config", "get", "update"], expected: false },
{ argv: ["node", "openclaw", "config", "unset", "update"], expected: false },
{ argv: ["node", "openclaw", "models", "list"], expected: false },
{ argv: ["node", "openclaw", "models", "status"], expected: false },
{ argv: ["node", "openclaw", "update", "status", "--json"], expected: false },
{ argv: ["node", "openclaw", "agent", "--message", "hi"], expected: false },
{ argv: ["node", "openclaw", "agents", "list"], expected: true },
{ argv: ["node", "openclaw", "message", "send"], expected: true },
] as const)("decides when to migrate state: $argv", ({ argv, expected }) => {
expect(shouldMigrateState([...argv])).toBe(expected);
});
it.each([

View File

@@ -19,14 +19,11 @@ describe("waitForever", () => {
});
describe("shouldSkipRespawnForArgv", () => {
it("skips respawn for help/version calls", () => {
const cases = [
["node", "openclaw", "--help"],
["node", "openclaw", "-V"],
] as const;
for (const argv of cases) {
expect(shouldSkipRespawnForArgv([...argv]), argv.join(" ")).toBe(true);
}
it.each([
{ argv: ["node", "openclaw", "--help"] },
{ argv: ["node", "openclaw", "-V"] },
] as const)("skips respawn for argv %j", ({ argv }) => {
expect(shouldSkipRespawnForArgv([...argv]), argv.join(" ")).toBe(true);
});
it("keeps respawn path for normal commands", () => {
@@ -66,46 +63,37 @@ describe("dns cli", () => {
});
describe("parseByteSize", () => {
it("parses byte-size units and shorthand values", () => {
const cases = [
["parses 10kb", "10kb", 10 * 1024],
["parses 1mb", "1mb", 1024 * 1024],
["parses 2gb", "2gb", 2 * 1024 * 1024 * 1024],
["parses shorthand 5k", "5k", 5 * 1024],
["parses shorthand 1m", "1m", 1024 * 1024],
] as const;
for (const [name, input, expected] of cases) {
expect(parseByteSize(input), name).toBe(expected);
}
it.each([
["parses 10kb", "10kb", 10 * 1024],
["parses 1mb", "1mb", 1024 * 1024],
["parses 2gb", "2gb", 2 * 1024 * 1024 * 1024],
["parses shorthand 5k", "5k", 5 * 1024],
["parses shorthand 1m", "1m", 1024 * 1024],
] as const)("%s", (_name, input, expected) => {
expect(parseByteSize(input)).toBe(expected);
});
it("uses default unit when omitted", () => {
expect(parseByteSize("123")).toBe(123);
});
it("rejects invalid values", () => {
const cases = ["", "nope", "-5kb"] as const;
for (const input of cases) {
expect(() => parseByteSize(input), input || "<empty>").toThrow();
}
it.each(["", "nope", "-5kb"] as const)("rejects invalid value %j", (input) => {
expect(() => parseByteSize(input)).toThrow();
});
});
describe("parseDurationMs", () => {
it("parses duration strings", () => {
const cases = [
["parses bare ms", "10000", 10_000],
["parses seconds suffix", "10s", 10_000],
["parses minutes suffix", "1m", 60_000],
["parses hours suffix", "2h", 7_200_000],
["parses days suffix", "2d", 172_800_000],
["supports decimals", "0.5s", 500],
["parses composite hours+minutes", "1h30m", 5_400_000],
["parses composite with milliseconds", "2m500ms", 120_500],
] as const;
for (const [name, input, expected] of cases) {
expect(parseDurationMs(input), name).toBe(expected);
}
it.each([
["parses bare ms", "10000", 10_000],
["parses seconds suffix", "10s", 10_000],
["parses minutes suffix", "1m", 60_000],
["parses hours suffix", "2h", 7_200_000],
["parses days suffix", "2d", 172_800_000],
["supports decimals", "0.5s", 500],
["parses composite hours+minutes", "1h30m", 5_400_000],
["parses composite with milliseconds", "2m500ms", 120_500],
] as const)("%s", (_name, input, expected) => {
expect(parseDurationMs(input)).toBe(expected);
});
it("rejects invalid composite strings", () => {

View File

@@ -159,6 +159,12 @@ const { defaultRuntime } = await import("../runtime.js");
const { updateCommand, updateStatusCommand, updateWizardCommand } = await import("./update-cli.js");
const { resolveGitInstallDir } = await import("./update-cli/shared.js");
type UpdateCliScenario = {
name: string;
run: () => Promise<void>;
assert: () => void;
};
describe("update-cli", () => {
const fixtureRoot = "/tmp/openclaw-update-tests";
let fixtureCount = 0;
@@ -235,6 +241,12 @@ describe("update-cli", () => {
...overrides,
}) as UpdateRunResult;
const runUpdateCliScenario = async (testCase: UpdateCliScenario) => {
vi.clearAllMocks();
await testCase.run();
testCase.assert();
};
const runRestartFallbackScenario = async (params: { daemonInstall: "ok" | "fail" }) => {
vi.mocked(runGatewayUpdate).mockResolvedValue(makeOkUpdateResult());
if (params.daemonInstall === "fail") {
@@ -396,79 +408,67 @@ describe("update-cli", () => {
setStdoutTty(false);
});
it("updateCommand dry-run previews without mutating and bypasses downgrade confirmation", async () => {
const cases = [
{
name: "preview mode",
run: async () => {
vi.mocked(defaultRuntime.log).mockClear();
serviceLoaded.mockResolvedValue(true);
await updateCommand({ dryRun: true, channel: "beta" });
},
assert: () => {
expect(writeConfigFile).not.toHaveBeenCalled();
expect(runGatewayUpdate).not.toHaveBeenCalled();
expect(runDaemonInstall).not.toHaveBeenCalled();
expect(runRestartScript).not.toHaveBeenCalled();
expect(runDaemonRestart).not.toHaveBeenCalled();
const logs = vi.mocked(defaultRuntime.log).mock.calls.map((call) => String(call[0]));
expect(logs.join("\n")).toContain("Update dry-run");
expect(logs.join("\n")).toContain("No changes were applied.");
},
it.each([
{
name: "preview mode",
run: async () => {
vi.mocked(defaultRuntime.log).mockClear();
serviceLoaded.mockResolvedValue(true);
await updateCommand({ dryRun: true, channel: "beta" });
},
{
name: "downgrade bypass",
run: async () => {
await setupNonInteractiveDowngrade();
vi.mocked(defaultRuntime.exit).mockClear();
await updateCommand({ dryRun: true });
},
assert: () => {
expect(vi.mocked(defaultRuntime.exit).mock.calls.some((call) => call[0] === 1)).toBe(
false,
);
expect(runGatewayUpdate).not.toHaveBeenCalled();
},
},
] as const;
assert: () => {
expect(writeConfigFile).not.toHaveBeenCalled();
expect(runGatewayUpdate).not.toHaveBeenCalled();
expect(runDaemonInstall).not.toHaveBeenCalled();
expect(runRestartScript).not.toHaveBeenCalled();
expect(runDaemonRestart).not.toHaveBeenCalled();
for (const testCase of cases) {
vi.clearAllMocks();
await testCase.run();
testCase.assert();
}
});
it("updateStatusCommand renders table and json output", async () => {
const cases = [
{
name: "table output",
options: { json: false },
assert: () => {
const logs = vi.mocked(defaultRuntime.log).mock.calls.map((call) => call[0]);
expect(logs.join("\n")).toContain("OpenClaw update status");
},
const logs = vi.mocked(defaultRuntime.log).mock.calls.map((call) => String(call[0]));
expect(logs.join("\n")).toContain("Update dry-run");
expect(logs.join("\n")).toContain("No changes were applied.");
},
{
name: "json output",
options: { json: true },
assert: () => {
const last = vi.mocked(defaultRuntime.writeJson).mock.calls.at(-1)?.[0];
expect(last).toBeDefined();
const parsed = last as Record<string, unknown>;
const channel = parsed.channel as { value?: unknown };
expect(channel.value).toBe("stable");
},
},
{
name: "downgrade bypass",
run: async () => {
await setupNonInteractiveDowngrade();
vi.mocked(defaultRuntime.exit).mockClear();
await updateCommand({ dryRun: true });
},
] as const;
assert: () => {
expect(vi.mocked(defaultRuntime.exit).mock.calls.some((call) => call[0] === 1)).toBe(false);
expect(runGatewayUpdate).not.toHaveBeenCalled();
},
},
] as const)("updateCommand dry-run behavior: $name", runUpdateCliScenario);
for (const testCase of cases) {
vi.mocked(defaultRuntime.log).mockClear();
await updateStatusCommand(testCase.options);
testCase.assert();
}
});
it.each([
{
name: "table output",
run: async () => {
vi.mocked(defaultRuntime.log).mockClear();
await updateStatusCommand({ json: false });
},
assert: () => {
const logs = vi.mocked(defaultRuntime.log).mock.calls.map((call) => call[0]);
expect(logs.join("\n")).toContain("OpenClaw update status");
},
},
{
name: "json output",
run: async () => {
vi.mocked(defaultRuntime.log).mockClear();
await updateStatusCommand({ json: true });
},
assert: () => {
const last = vi.mocked(defaultRuntime.writeJson).mock.calls.at(-1)?.[0];
expect(last).toBeDefined();
const parsed = last as Record<string, unknown>;
const channel = parsed.channel as { value?: unknown };
expect(channel.value).toBe("stable");
},
},
] as const)("updateStatusCommand rendering: $name", runUpdateCliScenario);
it("parses update status --json as the subcommand option", async () => {
const program = new Command();
@@ -607,55 +607,56 @@ describe("update-cli", () => {
);
});
it("resolves package install specs from tags and env overrides", async () => {
for (const scenario of [
{
name: "explicit dist-tag",
run: async () => {
mockPackageInstallStatus(createCaseDir("openclaw-update"));
await updateCommand({ tag: "next" });
},
expectedSpec: "openclaw@next",
it.each([
{
name: "explicit dist-tag",
run: async () => {
mockPackageInstallStatus(createCaseDir("openclaw-update"));
await updateCommand({ tag: "next" });
},
{
name: "main shorthand",
run: async () => {
mockPackageInstallStatus(createCaseDir("openclaw-update"));
await updateCommand({ yes: true, tag: "main" });
},
expectedSpec: "github:openclaw/openclaw#main",
expectedSpec: "openclaw@next",
},
{
name: "main shorthand",
run: async () => {
mockPackageInstallStatus(createCaseDir("openclaw-update"));
await updateCommand({ yes: true, tag: "main" });
},
{
name: "explicit git package spec",
run: async () => {
mockPackageInstallStatus(createCaseDir("openclaw-update"));
await updateCommand({ yes: true, tag: "github:openclaw/openclaw#main" });
},
expectedSpec: "github:openclaw/openclaw#main",
expectedSpec: "github:openclaw/openclaw#main",
},
{
name: "explicit git package spec",
run: async () => {
mockPackageInstallStatus(createCaseDir("openclaw-update"));
await updateCommand({ yes: true, tag: "github:openclaw/openclaw#main" });
},
{
name: "OPENCLAW_UPDATE_PACKAGE_SPEC override",
run: async () => {
mockPackageInstallStatus(createCaseDir("openclaw-update"));
await withEnvAsync(
{ OPENCLAW_UPDATE_PACKAGE_SPEC: "http://10.211.55.2:8138/openclaw-next.tgz" },
async () => {
await updateCommand({ yes: true, tag: "latest" });
},
);
},
expectedSpec: "http://10.211.55.2:8138/openclaw-next.tgz",
expectedSpec: "github:openclaw/openclaw#main",
},
{
name: "OPENCLAW_UPDATE_PACKAGE_SPEC override",
run: async () => {
mockPackageInstallStatus(createCaseDir("openclaw-update"));
await withEnvAsync(
{ OPENCLAW_UPDATE_PACKAGE_SPEC: "http://10.211.55.2:8138/openclaw-next.tgz" },
async () => {
await updateCommand({ yes: true, tag: "latest" });
},
);
},
]) {
expectedSpec: "http://10.211.55.2:8138/openclaw-next.tgz",
},
] as const)(
"resolves package install specs from tags and env overrides: $name",
async ({ run, expectedSpec }) => {
vi.clearAllMocks();
readPackageName.mockResolvedValue("openclaw");
readPackageVersion.mockResolvedValue("1.0.0");
resolveGlobalManager.mockResolvedValue("npm");
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(process.cwd());
await scenario.run();
expectPackageInstallSpec(scenario.expectedSpec);
}
});
await run();
expectPackageInstallSpec(expectedSpec);
},
);
it("fails package updates when the installed correction version does not match the requested target", async () => {
const tempDir = createCaseDir("openclaw-update");
@@ -762,45 +763,37 @@ describe("update-cli", () => {
expect(updateOptions?.env?.NODE_LLAMA_CPP_SKIP_DOWNLOAD).toBe("1");
});
it("updateCommand reports success and failure outcomes", async () => {
const cases = [
{
name: "outputs JSON when --json is set",
run: async () => {
vi.mocked(runGatewayUpdate).mockResolvedValue(makeOkUpdateResult());
vi.mocked(defaultRuntime.writeJson).mockClear();
await updateCommand({ json: true });
},
assert: () => {
const jsonOutput = vi.mocked(defaultRuntime.writeJson).mock.calls.at(-1)?.[0];
expect(jsonOutput).toBeDefined();
},
it.each([
{
name: "outputs JSON when --json is set",
run: async () => {
vi.mocked(runGatewayUpdate).mockResolvedValue(makeOkUpdateResult());
vi.mocked(defaultRuntime.writeJson).mockClear();
await updateCommand({ json: true });
},
{
name: "exits with error on failure",
run: async () => {
vi.mocked(runGatewayUpdate).mockResolvedValue({
status: "error",
mode: "git",
reason: "rebase-failed",
steps: [],
durationMs: 100,
} satisfies UpdateRunResult);
vi.mocked(defaultRuntime.exit).mockClear();
await updateCommand({});
},
assert: () => {
expect(defaultRuntime.exit).toHaveBeenCalledWith(1);
},
assert: () => {
const jsonOutput = vi.mocked(defaultRuntime.writeJson).mock.calls.at(-1)?.[0];
expect(jsonOutput).toBeDefined();
},
] as const;
for (const testCase of cases) {
vi.clearAllMocks();
await testCase.run();
testCase.assert();
}
});
},
{
name: "exits with error on failure",
run: async () => {
vi.mocked(runGatewayUpdate).mockResolvedValue({
status: "error",
mode: "git",
reason: "rebase-failed",
steps: [],
durationMs: 100,
} satisfies UpdateRunResult);
vi.mocked(defaultRuntime.exit).mockClear();
await updateCommand({});
},
assert: () => {
expect(defaultRuntime.exit).toHaveBeenCalledWith(1);
},
},
] as const)("updateCommand reports outcomes: $name", runUpdateCliScenario);
it("persists the requested channel only after a successful package update", async () => {
const tempDir = createCaseDir("openclaw-update");
@@ -888,96 +881,88 @@ describe("update-cli", () => {
expect(lastWrite?.update?.channel).toBe("beta");
});
it("updateCommand handles service env refresh and restart behavior", async () => {
const cases = [
{
name: "refreshes service env when already installed",
run: async () => {
vi.mocked(runGatewayUpdate).mockResolvedValue({
status: "ok",
mode: "git",
steps: [],
durationMs: 100,
} satisfies UpdateRunResult);
vi.mocked(runDaemonInstall).mockResolvedValue(undefined);
serviceLoaded.mockResolvedValue(true);
it.each([
{
name: "refreshes service env when already installed",
run: async () => {
vi.mocked(runGatewayUpdate).mockResolvedValue({
status: "ok",
mode: "git",
steps: [],
durationMs: 100,
} satisfies UpdateRunResult);
vi.mocked(runDaemonInstall).mockResolvedValue(undefined);
serviceLoaded.mockResolvedValue(true);
await updateCommand({});
},
assert: () => {
expect(runDaemonInstall).toHaveBeenCalledWith({
force: true,
json: undefined,
});
expect(runRestartScript).toHaveBeenCalled();
expect(runDaemonRestart).not.toHaveBeenCalled();
},
await updateCommand({});
},
{
name: "falls back to daemon restart when service env refresh cannot complete",
run: async () => {
vi.mocked(runDaemonRestart).mockResolvedValue(true);
await runRestartFallbackScenario({ daemonInstall: "fail" });
},
assert: () => {
expect(runDaemonInstall).toHaveBeenCalledWith({
force: true,
json: undefined,
});
expect(runDaemonRestart).toHaveBeenCalled();
},
assert: () => {
expect(runDaemonInstall).toHaveBeenCalledWith({
force: true,
json: undefined,
});
expect(runRestartScript).toHaveBeenCalled();
expect(runDaemonRestart).not.toHaveBeenCalled();
},
{
name: "keeps going when daemon install succeeds but restart fallback still handles relaunch",
run: async () => {
vi.mocked(runDaemonRestart).mockResolvedValue(true);
await runRestartFallbackScenario({ daemonInstall: "ok" });
},
assert: () => {
expect(runDaemonInstall).toHaveBeenCalledWith({
force: true,
json: undefined,
});
expect(runDaemonRestart).toHaveBeenCalled();
},
},
{
name: "falls back to daemon restart when service env refresh cannot complete",
run: async () => {
vi.mocked(runDaemonRestart).mockResolvedValue(true);
await runRestartFallbackScenario({ daemonInstall: "fail" });
},
{
name: "skips service env refresh when --no-restart is set",
run: async () => {
vi.mocked(runGatewayUpdate).mockResolvedValue(makeOkUpdateResult());
serviceLoaded.mockResolvedValue(true);
assert: () => {
expect(runDaemonInstall).toHaveBeenCalledWith({
force: true,
json: undefined,
});
expect(runDaemonRestart).toHaveBeenCalled();
},
},
{
name: "keeps going when daemon install succeeds but restart fallback still handles relaunch",
run: async () => {
vi.mocked(runDaemonRestart).mockResolvedValue(true);
await runRestartFallbackScenario({ daemonInstall: "ok" });
},
assert: () => {
expect(runDaemonInstall).toHaveBeenCalledWith({
force: true,
json: undefined,
});
expect(runDaemonRestart).toHaveBeenCalled();
},
},
{
name: "skips service env refresh when --no-restart is set",
run: async () => {
vi.mocked(runGatewayUpdate).mockResolvedValue(makeOkUpdateResult());
serviceLoaded.mockResolvedValue(true);
await updateCommand({ restart: false });
},
assert: () => {
expect(runDaemonInstall).not.toHaveBeenCalled();
expect(runRestartScript).not.toHaveBeenCalled();
expect(runDaemonRestart).not.toHaveBeenCalled();
},
await updateCommand({ restart: false });
},
{
name: "skips success message when restart does not run",
run: async () => {
vi.mocked(runGatewayUpdate).mockResolvedValue(makeOkUpdateResult());
vi.mocked(runDaemonRestart).mockResolvedValue(false);
vi.mocked(defaultRuntime.log).mockClear();
await updateCommand({ restart: true });
},
assert: () => {
const logLines = vi.mocked(defaultRuntime.log).mock.calls.map((call) => String(call[0]));
expect(logLines.some((line) => line.includes("Daemon restarted successfully."))).toBe(
false,
);
},
assert: () => {
expect(runDaemonInstall).not.toHaveBeenCalled();
expect(runRestartScript).not.toHaveBeenCalled();
expect(runDaemonRestart).not.toHaveBeenCalled();
},
] as const;
for (const testCase of cases) {
vi.clearAllMocks();
await testCase.run();
testCase.assert();
}
});
},
{
name: "skips success message when restart does not run",
run: async () => {
vi.mocked(runGatewayUpdate).mockResolvedValue(makeOkUpdateResult());
vi.mocked(runDaemonRestart).mockResolvedValue(false);
vi.mocked(defaultRuntime.log).mockClear();
await updateCommand({ restart: true });
},
assert: () => {
const logLines = vi.mocked(defaultRuntime.log).mock.calls.map((call) => String(call[0]));
expect(logLines.some((line) => line.includes("Daemon restarted successfully."))).toBe(
false,
);
},
},
] as const)("updateCommand service refresh behavior: $name", runUpdateCliScenario);
it.each([
{
@@ -1109,47 +1094,46 @@ describe("update-cli", () => {
}
});
it("validates update command invocation errors", async () => {
const cases = [
{
name: "update command invalid timeout",
run: async () => await updateCommand({ timeout: "invalid" }),
requireTty: false,
expectedError: "timeout",
},
{
name: "update status command invalid timeout",
run: async () => await updateStatusCommand({ timeout: "invalid" }),
requireTty: false,
expectedError: "timeout",
},
{
name: "update wizard invalid timeout",
run: async () => await updateWizardCommand({ timeout: "invalid" }),
requireTty: true,
expectedError: "timeout",
},
{
name: "update wizard requires a TTY",
run: async () => await updateWizardCommand({}),
requireTty: false,
expectedError: "Update wizard requires a TTY",
},
] as const;
for (const testCase of cases) {
setTty(testCase.requireTty);
it.each([
{
name: "update command invalid timeout",
run: async () => await updateCommand({ timeout: "invalid" }),
requireTty: false,
expectedError: "timeout",
},
{
name: "update status command invalid timeout",
run: async () => await updateStatusCommand({ timeout: "invalid" }),
requireTty: false,
expectedError: "timeout",
},
{
name: "update wizard invalid timeout",
run: async () => await updateWizardCommand({ timeout: "invalid" }),
requireTty: true,
expectedError: "timeout",
},
{
name: "update wizard requires a TTY",
run: async () => await updateWizardCommand({}),
requireTty: false,
expectedError: "Update wizard requires a TTY",
},
] as const)(
"validates update command invocation errors: $name",
async ({ run, requireTty, expectedError, name }) => {
setTty(requireTty);
vi.mocked(defaultRuntime.error).mockClear();
vi.mocked(defaultRuntime.exit).mockClear();
await testCase.run();
await run();
expect(defaultRuntime.error, testCase.name).toHaveBeenCalledWith(
expect.stringContaining(testCase.expectedError),
expect(defaultRuntime.error, name).toHaveBeenCalledWith(
expect.stringContaining(expectedError),
);
expect(defaultRuntime.exit, testCase.name).toHaveBeenCalledWith(1);
}
});
expect(defaultRuntime.exit, name).toHaveBeenCalledWith(1);
},
);
it.each([
{