mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-30 11:21:07 +00:00
test: dedupe utility and config suites
This commit is contained in:
@@ -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([
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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([
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user