import { afterEach, beforeEach, vi } from "vitest"; import type { UpdateRunResult } from "../../infra/update-runner.js"; import type { captureTargetDatabaseSchemaContext } from "./schema-preflight.js"; import type { executeMutableUpdate } from "./update-command-execution.js"; import type { PreManagedServiceStop } from "./update-command-service.js"; const mocks = vi.hoisted(() => ({ captureManagedContext: vi.fn(), captureManagedPreflight: vi.fn< typeof import("./update-command-managed-context.js").captureOwnedManagedUpdatePreflightContext >(), captureSchemaContext: vi.fn(), checkTargetSchemas: vi.fn(), formatSchemaRefusalLines: vi.fn(), hasSchemaRefusal: vi.fn(), maybeRestartService: vi.fn(), maybeStopService: vi.fn(), prepareMutableUpdate: vi.fn<(env?: NodeJS.ProcessEnv) => Promise>(), pluginPreflight: vi.fn(), pluginTargets: vi.fn(), pluginRecords: vi.fn(), npmMetadata: vi.fn(), readGitRecovery: vi.fn(), runGitUpdate: vi.fn(), runPackageUpdate: vi.fn(), runtimeError: vi.fn(), revalidateSchemaContext: vi.fn(), validateCanary: vi.fn(), nativeSupport: vi.fn< typeof import("./update-command-service-command.js").isUpdatedInstallGatewayExecutorSupported >(), serviceStopped: false, shouldBlockServiceUpdate: vi.fn(), verifyPackageRecovery: vi.fn(), })); vi.mock("./update-command-service-command.js", async (importOriginal) => ({ ...(await importOriginal()), isUpdatedInstallGatewayExecutorSupported: mocks.nativeSupport, })); afterEach(() => vi.restoreAllMocks()); vi.mock("../../infra/update-global.js", async (importOriginal) => ({ ...(await importOriginal()), verifyPackageUpdateRecovery: mocks.verifyPackageRecovery, })); vi.mock("../../infra/update-candidate-canary.js", () => ({ validateUpdateCandidateCanary: mocks.validateCanary, })); vi.mock("./update-command-plugin-preflight.js", () => ({ preflightConfiguredNpmPluginTargets: mocks.pluginPreflight, })); vi.mock("../../commands/doctor/shared/missing-configured-plugin-install.targets.js", () => ({ collectConfiguredNpmPluginTargets: mocks.pluginTargets, })); vi.mock("../../plugins/installed-plugin-index-records.js", async (importOriginal) => ({ ...(await importOriginal()), loadInstalledPluginIndexInstallRecords: mocks.pluginRecords, })); vi.mock("../../infra/install-source-utils.js", async (importOriginal) => ({ ...(await importOriginal()), resolveNpmSpecMetadata: mocks.npmMetadata, })); vi.mock("../../infra/update-runner-git-recovery.js", () => ({ readCurrentGitUpdateRecovery: mocks.readGitRecovery, })); vi.mock("../../runtime.js", () => ({ defaultRuntime: { error: mocks.runtimeError }, })); vi.mock("./schema-preflight.js", () => ({ captureTargetDatabaseSchemaContext: mocks.captureSchemaContext, checkTargetDatabaseSchemasForContexts: mocks.checkTargetSchemas, formatSchemaRefusalLines: mocks.formatSchemaRefusalLines, hasSchemaRefusal: mocks.hasSchemaRefusal, })); vi.mock("./update-command-git.js", async (importOriginal) => ({ ...(await importOriginal()), updateGitInstall: mocks.runGitUpdate, })); vi.mock("./update-command-handoff.js", () => ({ formatUpdateAncestryBlockMessage: (message: string) => message, handoffUpdateFromGateway: vi.fn(), })); vi.mock("./update-command-managed-context.js", async (importOriginal) => ({ ...(await importOriginal()), captureOwnedManagedUpdateContext: mocks.captureManagedContext, captureOwnedManagedUpdatePreflightContext: mocks.captureManagedPreflight, revalidateUpdateDatabaseContext: mocks.revalidateSchemaContext, })); vi.mock("./update-command-package.js", async (importOriginal) => ({ ...(await importOriginal()), runPackageInstallUpdate: mocks.runPackageUpdate, })); vi.mock("./update-command-service.js", async () => { const actual = await vi.importActual( "./update-command-service-maintenance.js", ); const { resolveUpdatedGatewayRestartPort } = await import("./update-command-service-plan.js"); return { maybeRestartServiceAfterFailedMutableUpdate: mocks.maybeRestartService, maybeStopManagedServiceBeforeMutableUpdate: mocks.maybeStopService, shouldBlockMutableUpdateFromGatewayServiceEnv: mocks.shouldBlockServiceUpdate, UpdateCommandAbort: actual.UpdateCommandAbort, resolveUpdatedGatewayRestartPort, }; }); const successfulUpdate: UpdateRunResult = { status: "ok", mode: "npm", root: "/opt/openclaw", before: { version: "1.0.0" }, after: { version: "1.0.1" }, steps: [], durationMs: 1, }; function executionParams( updateInstallKind: "git" | "package", ): Parameters[0] { return { root: "/opt/openclaw", installKind: updateInstallKind, updateInstallKind, switchToGit: false, timeoutMs: 30_000, updateStepTimeoutMs: 30_000, startedAt: 1, progress: {}, stop: vi.fn(), channel: "stable", tag: "1.0.1", opts: { json: true }, shouldRestart: true, packageInstallSpec: "openclaw@1.0.1", packageTargetVersion: "1.0.1", managedServiceRootRedirect: null, invocationCwd: "/work", recoveryState: { triageTarget: { env: {} } }, prepareMutableUpdate: mocks.prepareMutableUpdate, packageTargetSchemaVersions: { state: 15, agent: 19 }, }; } function schemaContext( profile: string, ): Awaited> { const env = { OPENCLAW_PROFILE: profile }; return { env, readEnv: { ...env }, config: {}, configSnapshot: { path: `/fixture/${profile}/openclaw.json`, exists: true, raw: "{}", parsed: {}, resolved: {}, sourceConfig: {}, config: {}, runtimeConfig: {}, valid: true, issues: [], warnings: [], legacyIssues: [], }, }; } function inspectOrStopService(phase: "inspect" | "prepare" = "prepare"): PreManagedServiceStop { const running = !mocks.serviceStopped; if (phase === "prepare") { mocks.serviceStopped = true; } return { stopped: phase === "prepare", inspected: true, runtimeInspected: true, running, serviceEnv: { OPENCLAW_PROFILE: "default" }, serviceUpdateVerdict: { kind: "owned", root: "/opt/openclaw", fingerprint: "service-fingerprint", refreshDefinition: false, }, }; } beforeEach(() => { vi.clearAllMocks(); mocks.serviceStopped = false; mocks.validateCanary.mockResolvedValue({ status: "ok", phase: "readiness", steps: [], durationMs: 1, logTail: [], }); mocks.captureManagedContext.mockResolvedValue(undefined); mocks.captureManagedPreflight.mockResolvedValue(schemaContext("default")); mocks.captureSchemaContext.mockResolvedValue(schemaContext("invoker")); mocks.revalidateSchemaContext.mockImplementation(async (context) => context); mocks.checkTargetSchemas.mockResolvedValue({ incompatible: [], indeterminate: [] }); mocks.formatSchemaRefusalLines.mockReturnValue(["schema refused"]); mocks.hasSchemaRefusal.mockImplementation( (schemas) => schemas.incompatible.length > 0 || schemas.indeterminate.length > 0, ); mocks.maybeRestartService.mockResolvedValue(undefined); mocks.maybeStopService.mockImplementation(async ({ phase }) => inspectOrStopService(phase)); mocks.prepareMutableUpdate.mockResolvedValue(undefined); mocks.pluginPreflight.mockResolvedValue([]); mocks.readGitRecovery.mockResolvedValue({ serviceRestartSafe: true }); mocks.runGitUpdate.mockResolvedValue({ ...successfulUpdate, mode: "git" }); mocks.runPackageUpdate.mockResolvedValue(successfulUpdate); mocks.shouldBlockServiceUpdate.mockReturnValue(false); mocks.verifyPackageRecovery.mockResolvedValue({ serviceRestartSafe: true }); }); export { executionParams, inspectOrStopService, mocks, schemaContext, successfulUpdate };