openclaw / src /cli /update-cli /update-command-activation.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
eb3f11e verified
Raw
History Blame Contribute Delete
6.19 kB
import fs from "node:fs";
import path from "node:path";
import { afterEach, expect, it, vi } from "vitest";
import { createDeferred } from "../../../test/helpers/promise.js";
import { useAutoCleanupTempDirTracker } from "../../../test/helpers/temp-dir.js";
import * as temporaryRoot from "../../infra/tmp-openclaw-dir.js";
import { createManagedHandoffLeaseStore } from "../../infra/update-managed-service-handoff-lease.js";
import {
createUpdateRun,
getUpdateRun,
recordUpdateRunPhase,
} from "../../infra/update-run-ledger.js";
import { defaultRuntime } from "../../runtime.js";
import { closeOpenClawStateDatabaseForTest } from "../../state/openclaw-state-db.js";
import {
withUpdateCommandExecutor,
withUpdateCommandExecutorChild,
} from "./update-command-executor.js";
import { withUpdateCommandTerminalResult } from "./update-command-terminal.js";
const dirs = useAutoCleanupTempDirTracker(afterEach);
afterEach(() => {
vi.useRealTimers();
closeOpenClawStateDatabaseForTest();
vi.restoreAllMocks();
});
it("records an activation timeout when executor settlement remains pending", async () => {
const root = fs.realpathSync(dirs.make("update-activation-"));
const temporary = path.join(root, "private-tmp");
fs.mkdirSync(temporary, { mode: 0o700 });
vi.spyOn(temporaryRoot, "resolvePreferredOpenClawTmpDir").mockReturnValue(temporary);
const env = { ...process.env, OPENCLAW_STATE_DIR: path.join(root, "state") };
const run = { runId: createUpdateRun({ trigger: "cli" }, { env }).runId, env };
const output = vi.spyOn(defaultRuntime, "writeJson").mockImplementation(() => {});
vi.spyOn(defaultRuntime, "log").mockImplementation(() => {});
const entered = createDeferred();
const release = createDeferred();
let childWork: Promise<unknown> | undefined;
let outcome: unknown;
let assertCurrent: (() => void) | undefined;
const budget = 10 * 60_000;
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "Date"] });
const running = withUpdateCommandTerminalResult(
async (registerRun) => {
registerRun(run);
await withUpdateCommandExecutor(run.runId, async (executor) => {
const fence = await executor.enter(root, { activationTimeoutMs: budget });
assertCurrent = fence.assertCurrent;
recordUpdateRunPhase(run.runId, "activating", undefined, { env });
childWork = withUpdateCommandExecutorChild(fence, root, async () => {
entered.resolve();
await release.promise;
});
void childWork.catch(() => {});
await entered.promise;
// Returning does not settle the admitted child's outstanding work.
});
},
{ json: true },
).catch((error: unknown) => {
outcome = error;
});
try {
await entered.promise;
await vi.advanceTimersByTimeAsync(budget - 1);
expect(getUpdateRun(run.runId, { env })?.status).toBe("running");
expect(assertCurrent).toBeDefined();
await vi.advanceTimersByTimeAsync(budget + 1);
await running;
expect(outcome).toMatchObject({ result: { reason: "update-activation-timeout" } });
expect(assertCurrent).toThrow("activation");
expect(getUpdateRun(run.runId, { env })).toMatchObject({
phase: "finished",
status: "failed",
reason: "update-activation-timeout",
});
expect(output).toHaveBeenCalledWith(
expect.objectContaining({
reason: "update-activation-timeout",
status: "error",
}),
);
expect(createManagedHandoffLeaseStore().read(root).kind).toBe("current");
await expect(
withUpdateCommandExecutor(run.runId, (other) => other.enter(root)),
).rejects.toThrow("Another update executor");
} finally {
release.resolve();
await childWork?.catch(() => {});
await running;
}
});
it.each([false, true])(
"checks the activation deadline after synchronous work (expired: %s)",
async (expired) => {
const root = fs.realpathSync(dirs.make("update-activation-clock-"));
const temporary = path.join(root, "private-tmp");
fs.mkdirSync(temporary, { mode: 0o700 });
vi.spyOn(temporaryRoot, "resolvePreferredOpenClawTmpDir").mockReturnValue(temporary);
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "Date"] });
const work = withUpdateCommandExecutor("clock-probe", async (executor) => {
await executor.enter(root, { activationTimeoutMs: 60_000 });
// Move the clock without dispatching timers, as a blocking native probe can.
vi.setSystemTime(Date.now() + (expired ? 60_001 : 59_999));
return "completed";
});
if (expired) {
await expect(work).rejects.toMatchObject({ reason: "update-activation-timeout" });
} else {
await expect(work).resolves.toBe("completed");
}
expect(createManagedHandoffLeaseStore().read(root)).toEqual({ kind: "absent" });
expect(vi.getTimerCount()).toBe(0);
},
);
it.each([undefined, 48 * 60 * 60_000])(
"keeps activation alive for measured state and caller allowance %s",
async (callerTimeoutMs) => {
const { resolveUpdateFinalizationTimeoutMs } =
await import("../../infra/update-finalization-budget.js");
const root = fs.realpathSync(dirs.make("update-activation-size-"));
const database = path.join(root, "agent.sqlite");
const descriptor = fs.openSync(database, "w");
fs.ftruncateSync(descriptor, 2 * 1024 ** 3);
fs.closeSync(descriptor);
const budget = await resolveUpdateFinalizationTimeoutMs(callerTimeoutMs, {
databases: [{ path: database }],
env: { ...process.env, OPENCLAW_STATE_DIR: root },
});
const legacyBudget = Math.max(30 * 60_000, (callerTimeoutMs ?? 0) * 6);
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "Date"] });
await expect(
withUpdateCommandExecutor("measured-activation", async (executor) => {
const fence = await executor.enter(root, { activationTimeoutMs: budget });
vi.setSystemTime(Date.now() + (callerTimeoutMs ?? legacyBudget + 1));
fence.assertCurrent();
return "completed";
}),
).resolves.toBe("completed");
expect(createManagedHandoffLeaseStore().read(root)).toEqual({ kind: "absent" });
},
);