mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 10:55:31 -06:00
refactor: mechanical dedup batch (protocol types, update-cli bridge, talk fallback) (#114432)
* refactor(onboarding): remove search setup barrel * refactor(plugins): reuse detected package manifest * test(update): replace global helper bridges * refactor(talk): remove dead legacy response fallback * refactor(protocol): derive root types from schema The maintainer approved broadening the additive schema-backed type surface without a protocol version bump. * fix(plugins): drop stale package path import * test(protocol): type dynamic registry lookups * docs(talk): explain canonical response boundary * refactor(talk): enforce canonical response input * fix(protocol): keep root type exports registry-free * refactor(update): expose helpers through test facades * fix(protocol): keep result types on leaf schema modules
This commit is contained in:
committed by
GitHub
parent
0f879595d4
commit
c6b2ec28c8
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
readGatewayServiceState,
|
||||
resolveGatewayService,
|
||||
type GatewayService,
|
||||
} from "../../daemon/service.js";
|
||||
import { recoverInstalledLaunchAgent } from "../daemon-cli/launchd-recovery.js";
|
||||
|
||||
export type PostUpdateLaunchAgentRecoveryResult =
|
||||
| { attempted: false; recovered: false }
|
||||
| { attempted: true; recovered: true; message: string }
|
||||
| { attempted: true; recovered: false; detail: string };
|
||||
|
||||
type PostUpdateLaunchAgentRecoveryDeps = {
|
||||
platform?: NodeJS.Platform;
|
||||
readState?: typeof readGatewayServiceState;
|
||||
recover?: typeof recoverInstalledLaunchAgent;
|
||||
};
|
||||
|
||||
export async function recoverInstalledLaunchAgentAfterUpdate(params: {
|
||||
service?: GatewayService;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
deps?: PostUpdateLaunchAgentRecoveryDeps;
|
||||
}): Promise<PostUpdateLaunchAgentRecoveryResult> {
|
||||
const platform = params.deps?.platform ?? process.platform;
|
||||
if (platform !== "darwin") {
|
||||
return { attempted: false, recovered: false };
|
||||
}
|
||||
|
||||
const service = params.service ?? resolveGatewayService();
|
||||
const readState = params.deps?.readState ?? readGatewayServiceState;
|
||||
const recover = params.deps?.recover ?? recoverInstalledLaunchAgent;
|
||||
const state = await readState(service, { env: params.env }).catch(() => null);
|
||||
if (state?.loaded) {
|
||||
return { attempted: false, recovered: false };
|
||||
}
|
||||
if (state && !state.installed && !state.runtime?.missingSupervision) {
|
||||
return { attempted: false, recovered: false };
|
||||
}
|
||||
|
||||
const recovered = await recover({ result: "restarted", env: state?.env ?? params.env }).catch(
|
||||
() => null,
|
||||
);
|
||||
if (!recovered) {
|
||||
return {
|
||||
attempted: true,
|
||||
recovered: false,
|
||||
detail:
|
||||
"LaunchAgent was installed but not loaded; automatic bootstrap/kickstart recovery failed.",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
attempted: true,
|
||||
recovered: true,
|
||||
message: recovered.message,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import path from "node:path";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import type { PluginInstallRecord } from "../../config/types.plugins.js";
|
||||
import { pathExists } from "../../infra/fs-safe.js";
|
||||
import type { UpdateRunResult } from "../../infra/update-runner.js";
|
||||
import { normalizePluginsConfig, resolveEffectiveEnableState } from "../../plugins/config-state.js";
|
||||
import {
|
||||
resolveTrustedSourceLinkedOfficialClawHubSpec,
|
||||
resolveTrustedSourceLinkedOfficialNpmSpec,
|
||||
} from "../../plugins/official-external-install-records.js";
|
||||
import { resolveUserPath } from "../../utils.js";
|
||||
import {
|
||||
hasNativePackageInstallPayload,
|
||||
resolveBundleInstallRecordPayload,
|
||||
validateBundleInstallRecordPayload,
|
||||
} from "./plugin-payload-validation.js";
|
||||
|
||||
export type PostCorePluginUpdateResult = NonNullable<
|
||||
NonNullable<UpdateRunResult["postUpdate"]>["plugins"]
|
||||
>;
|
||||
|
||||
export type MissingPluginInstallPayload = {
|
||||
pluginId: string;
|
||||
installPath?: string;
|
||||
reason: "missing-install-path" | "missing-package-dir" | "missing-package-json";
|
||||
};
|
||||
|
||||
export function resolvePostSyncPluginUpdateSkipIds(params: {
|
||||
switchedToClawHub: readonly string[];
|
||||
switchedToNpm: readonly string[];
|
||||
repairedMissingPayloadIds: ReadonlySet<string>;
|
||||
}): Set<string> {
|
||||
return new Set([
|
||||
...params.switchedToClawHub,
|
||||
...params.switchedToNpm,
|
||||
...params.repairedMissingPayloadIds,
|
||||
]);
|
||||
}
|
||||
|
||||
function isTrackedPackageInstallRecord(record: PluginInstallRecord): boolean {
|
||||
return (
|
||||
record.source === "npm" ||
|
||||
record.source === "clawhub" ||
|
||||
record.source === "git" ||
|
||||
record.source === "marketplace"
|
||||
);
|
||||
}
|
||||
|
||||
export async function collectMissingPluginInstallPayloads(params: {
|
||||
records: Record<string, PluginInstallRecord>;
|
||||
config?: OpenClawConfig;
|
||||
skipDisabledPlugins?: boolean;
|
||||
syncOfficialPluginInstalls?: boolean;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<MissingPluginInstallPayload[]> {
|
||||
const env = params.env ?? process.env;
|
||||
const normalizedPluginConfig =
|
||||
params.skipDisabledPlugins && params.config
|
||||
? normalizePluginsConfig(params.config.plugins)
|
||||
: undefined;
|
||||
const missing: MissingPluginInstallPayload[] = [];
|
||||
for (const [pluginId, record] of Object.entries(params.records).toSorted(([left], [right]) =>
|
||||
left.localeCompare(right),
|
||||
)) {
|
||||
if (!isTrackedPackageInstallRecord(record)) {
|
||||
continue;
|
||||
}
|
||||
const officialNpmSpec = params.syncOfficialPluginInstalls
|
||||
? resolveTrustedSourceLinkedOfficialNpmSpec({ pluginId, record })
|
||||
: undefined;
|
||||
const officialClawHubSpec = params.syncOfficialPluginInstalls
|
||||
? resolveTrustedSourceLinkedOfficialClawHubSpec({ pluginId, record })
|
||||
: undefined;
|
||||
if (normalizedPluginConfig && params.config) {
|
||||
const enableState = resolveEffectiveEnableState({
|
||||
id: pluginId,
|
||||
origin: "global",
|
||||
config: normalizedPluginConfig,
|
||||
rootConfig: params.config,
|
||||
});
|
||||
if (!enableState.enabled && !officialNpmSpec && !officialClawHubSpec) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const rawInstallPath = normalizeOptionalString(record.installPath);
|
||||
if (!rawInstallPath) {
|
||||
missing.push({ pluginId, reason: "missing-install-path" });
|
||||
continue;
|
||||
}
|
||||
const installPath = resolveUserPath(rawInstallPath, env);
|
||||
if (!(await pathExists(installPath))) {
|
||||
missing.push({ pluginId, installPath, reason: "missing-package-dir" });
|
||||
continue;
|
||||
}
|
||||
const bundlePayload = resolveBundleInstallRecordPayload({ record, installPath });
|
||||
if (bundlePayload.isBundlePayload) {
|
||||
if (await hasNativePackageInstallPayload(installPath)) {
|
||||
continue;
|
||||
}
|
||||
const bundleFailure = validateBundleInstallRecordPayload({
|
||||
pluginId,
|
||||
installPath,
|
||||
record,
|
||||
bundleFormat: bundlePayload.bundleFormat,
|
||||
});
|
||||
if (bundleFailure) {
|
||||
missing.push({ pluginId, installPath, reason: "missing-package-json" });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const packageJsonPath = path.join(installPath, "package.json");
|
||||
if (!(await pathExists(packageJsonPath))) {
|
||||
missing.push({ pluginId, installPath, reason: "missing-package-json" });
|
||||
}
|
||||
}
|
||||
return missing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the post-core-update result we return when the active config cannot
|
||||
* even be parsed. Mandatory post-core convergence requires a parseable
|
||||
* config to know which plugins are configured; if one isn't available, we
|
||||
* refuse to restart the gateway and surface this as a hard error so the
|
||||
* existing `status === "error"` => `exit 1` pre-restart gate fires.
|
||||
*/
|
||||
export function buildInvalidConfigPostCoreUpdateResult(): {
|
||||
message: string;
|
||||
guidance: string[];
|
||||
result: PostCorePluginUpdateResult;
|
||||
} {
|
||||
const guidance = [
|
||||
"Run `openclaw doctor` to inspect the config validation errors.",
|
||||
"Once the config parses, rerun `openclaw update repair`.",
|
||||
];
|
||||
const message =
|
||||
"Plugin post-update convergence skipped because the config is invalid; refusing to restart the gateway with an unverified plugin set.";
|
||||
return {
|
||||
message,
|
||||
guidance,
|
||||
result: {
|
||||
status: "error",
|
||||
reason: "invalid-config",
|
||||
changed: false,
|
||||
sync: {
|
||||
changed: false,
|
||||
switchedToBundled: [],
|
||||
switchedToNpm: [],
|
||||
warnings: [],
|
||||
errors: [],
|
||||
},
|
||||
npm: {
|
||||
changed: false,
|
||||
outcomes: [],
|
||||
},
|
||||
integrityDrifts: [],
|
||||
warnings: [{ reason: "invalid-config", message, guidance }],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,52 +1,11 @@
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import type { PluginInstallRecord } from "../../config/types.plugins.js";
|
||||
import type { PostCorePluginUpdateResult } from "./update-command-plugins.js";
|
||||
import "./update-command-plugins.js";
|
||||
import {
|
||||
buildInvalidConfigPostCoreUpdateResult,
|
||||
collectMissingPluginInstallPayloads,
|
||||
resolvePostSyncPluginUpdateSkipIds,
|
||||
} from "./update-command-plugins-internals.js";
|
||||
|
||||
type MissingPluginInstallPayload = {
|
||||
pluginId: string;
|
||||
installPath?: string;
|
||||
reason: "missing-install-path" | "missing-package-dir" | "missing-package-json";
|
||||
export const testing = {
|
||||
buildInvalidConfigPostCoreUpdateResult,
|
||||
collectMissingPluginInstallPayloads,
|
||||
resolvePostSyncPluginUpdateSkipIds,
|
||||
};
|
||||
|
||||
type UpdateCommandPluginsTestApi = {
|
||||
buildInvalidConfigPostCoreUpdateResult(): {
|
||||
message: string;
|
||||
guidance: string[];
|
||||
result: PostCorePluginUpdateResult;
|
||||
};
|
||||
collectMissingPluginInstallPayloads(params: {
|
||||
records: Record<string, PluginInstallRecord>;
|
||||
config?: OpenClawConfig;
|
||||
skipDisabledPlugins?: boolean;
|
||||
syncOfficialPluginInstalls?: boolean;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<MissingPluginInstallPayload[]>;
|
||||
resolvePostSyncPluginUpdateSkipIds(params: {
|
||||
switchedToClawHub: readonly string[];
|
||||
switchedToNpm: readonly string[];
|
||||
repairedMissingPayloadIds: ReadonlySet<string>;
|
||||
}): Set<string>;
|
||||
};
|
||||
|
||||
function getTestApi(): UpdateCommandPluginsTestApi {
|
||||
return (globalThis as Record<PropertyKey, unknown>)[
|
||||
Symbol.for("openclaw.updateCommandPluginsTestApi")
|
||||
] as UpdateCommandPluginsTestApi;
|
||||
}
|
||||
|
||||
export function buildInvalidConfigPostCoreUpdateResult() {
|
||||
return getTestApi().buildInvalidConfigPostCoreUpdateResult();
|
||||
}
|
||||
|
||||
export async function collectMissingPluginInstallPayloads(
|
||||
params: Parameters<UpdateCommandPluginsTestApi["collectMissingPluginInstallPayloads"]>[0],
|
||||
): Promise<MissingPluginInstallPayload[]> {
|
||||
return await getTestApi().collectMissingPluginInstallPayloads(params);
|
||||
}
|
||||
|
||||
export function resolvePostSyncPluginUpdateSkipIds(
|
||||
params: Parameters<UpdateCommandPluginsTestApi["resolvePostSyncPluginUpdateSkipIds"]>[0],
|
||||
): Set<string> {
|
||||
return getTestApi().resolvePostSyncPluginUpdateSkipIds(params);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// Plugin synchronization and convergence after the core update.
|
||||
import path from "node:path";
|
||||
import { confirm, isCancel, text } from "@clack/prompts";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { stripAnsi } from "../../../packages/terminal-core/src/ansi.js";
|
||||
import { stylePromptMessage } from "../../../packages/terminal-core/src/prompt-style.js";
|
||||
import { sanitizeTerminalText } from "../../../packages/terminal-core/src/safe-text.js";
|
||||
@@ -10,20 +8,13 @@ import { readConfigFileSnapshot } from "../../config/config.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import type { PluginInstallRecord } from "../../config/types.plugins.js";
|
||||
import type { ClawHubRiskAcknowledgementRequest } from "../../infra/clawhub-install-trust.js";
|
||||
import { pathExists } from "../../infra/fs-safe.js";
|
||||
import type { UpdateChannel } from "../../infra/update-channels.js";
|
||||
import type { UpdateRunResult } from "../../infra/update-runner.js";
|
||||
import { normalizePluginsConfig, resolveEffectiveEnableState } from "../../plugins/config-state.js";
|
||||
import { commitPluginInstallRecordsWithConfig } from "../../plugins/install-record-commit.js";
|
||||
import {
|
||||
loadInstalledPluginIndexInstallRecords,
|
||||
withoutPluginInstallRecords,
|
||||
withPluginInstallRecords,
|
||||
} from "../../plugins/installed-plugin-index-records.js";
|
||||
import {
|
||||
resolveTrustedSourceLinkedOfficialClawHubSpec,
|
||||
resolveTrustedSourceLinkedOfficialNpmSpec,
|
||||
} from "../../plugins/official-external-install-records.js";
|
||||
import { refreshPluginRegistryAfterConfigMutation } from "../../plugins/registry-refresh.js";
|
||||
import {
|
||||
isClawHubTrustSkippedOutcome,
|
||||
@@ -33,46 +24,27 @@ import {
|
||||
type PluginUpdateOutcome,
|
||||
} from "../../plugins/update.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { resolveUserPath } from "../../utils.js";
|
||||
import { listPersistedBundledPluginLocationBridges } from "../plugins-location-bridges.js";
|
||||
import {
|
||||
hasNativePackageInstallPayload,
|
||||
resolveBundleInstallRecordPayload,
|
||||
validateBundleInstallRecordPayload,
|
||||
} from "./plugin-payload-validation.js";
|
||||
import {
|
||||
convergenceWarningsToOutcomes,
|
||||
runPostCorePluginConvergence,
|
||||
} from "./post-core-plugin-convergence.js";
|
||||
import { readPackageVersion, type UpdateCommandOptions } from "./shared.js";
|
||||
import {
|
||||
buildInvalidConfigPostCoreUpdateResult,
|
||||
collectMissingPluginInstallPayloads,
|
||||
resolvePostSyncPluginUpdateSkipIds,
|
||||
type MissingPluginInstallPayload,
|
||||
type PostCorePluginUpdateResult,
|
||||
} from "./update-command-plugins-internals.js";
|
||||
|
||||
export type { PostCorePluginUpdateResult } from "./update-command-plugins-internals.js";
|
||||
|
||||
const POST_UPDATE_PLUGIN_REPAIR_GUIDANCE =
|
||||
"Run openclaw update repair to retry post-update plugin repair.";
|
||||
|
||||
export type PostCorePluginUpdateResult = NonNullable<
|
||||
NonNullable<UpdateRunResult["postUpdate"]>["plugins"]
|
||||
>;
|
||||
|
||||
type MissingPluginInstallPayload = {
|
||||
pluginId: string;
|
||||
installPath?: string;
|
||||
reason: "missing-install-path" | "missing-package-dir" | "missing-package-json";
|
||||
};
|
||||
|
||||
type PostUpdatePluginWarning = NonNullable<PostCorePluginUpdateResult["warnings"]>[number];
|
||||
|
||||
function resolvePostSyncPluginUpdateSkipIds(params: {
|
||||
switchedToClawHub: readonly string[];
|
||||
switchedToNpm: readonly string[];
|
||||
repairedMissingPayloadIds: ReadonlySet<string>;
|
||||
}): Set<string> {
|
||||
return new Set([
|
||||
...params.switchedToClawHub,
|
||||
...params.switchedToNpm,
|
||||
...params.repairedMissingPayloadIds,
|
||||
]);
|
||||
}
|
||||
|
||||
function isClawHubTrustNotice(message: string): boolean {
|
||||
const trimmed = stripAnsi(message).trimStart();
|
||||
return (
|
||||
@@ -133,85 +105,6 @@ function resolveUpdateClawHubRiskAcknowledgementOptions(
|
||||
};
|
||||
}
|
||||
|
||||
function isTrackedPackageInstallRecord(record: PluginInstallRecord): boolean {
|
||||
return (
|
||||
record.source === "npm" ||
|
||||
record.source === "clawhub" ||
|
||||
record.source === "git" ||
|
||||
record.source === "marketplace"
|
||||
);
|
||||
}
|
||||
|
||||
async function collectMissingPluginInstallPayloads(params: {
|
||||
records: Record<string, PluginInstallRecord>;
|
||||
config?: OpenClawConfig;
|
||||
skipDisabledPlugins?: boolean;
|
||||
syncOfficialPluginInstalls?: boolean;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<MissingPluginInstallPayload[]> {
|
||||
const env = params.env ?? process.env;
|
||||
const normalizedPluginConfig =
|
||||
params.skipDisabledPlugins && params.config
|
||||
? normalizePluginsConfig(params.config.plugins)
|
||||
: undefined;
|
||||
const missing: MissingPluginInstallPayload[] = [];
|
||||
for (const [pluginId, record] of Object.entries(params.records).toSorted(([left], [right]) =>
|
||||
left.localeCompare(right),
|
||||
)) {
|
||||
if (!isTrackedPackageInstallRecord(record)) {
|
||||
continue;
|
||||
}
|
||||
const officialNpmSpec = params.syncOfficialPluginInstalls
|
||||
? resolveTrustedSourceLinkedOfficialNpmSpec({ pluginId, record })
|
||||
: undefined;
|
||||
const officialClawHubSpec = params.syncOfficialPluginInstalls
|
||||
? resolveTrustedSourceLinkedOfficialClawHubSpec({ pluginId, record })
|
||||
: undefined;
|
||||
if (normalizedPluginConfig && params.config) {
|
||||
const enableState = resolveEffectiveEnableState({
|
||||
id: pluginId,
|
||||
origin: "global",
|
||||
config: normalizedPluginConfig,
|
||||
rootConfig: params.config,
|
||||
});
|
||||
if (!enableState.enabled && !officialNpmSpec && !officialClawHubSpec) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const rawInstallPath = normalizeOptionalString(record.installPath);
|
||||
if (!rawInstallPath) {
|
||||
missing.push({ pluginId, reason: "missing-install-path" });
|
||||
continue;
|
||||
}
|
||||
const installPath = resolveUserPath(rawInstallPath, env);
|
||||
if (!(await pathExists(installPath))) {
|
||||
missing.push({ pluginId, installPath, reason: "missing-package-dir" });
|
||||
continue;
|
||||
}
|
||||
const bundlePayload = resolveBundleInstallRecordPayload({ record, installPath });
|
||||
if (bundlePayload.isBundlePayload) {
|
||||
if (await hasNativePackageInstallPayload(installPath)) {
|
||||
continue;
|
||||
}
|
||||
const bundleFailure = validateBundleInstallRecordPayload({
|
||||
pluginId,
|
||||
installPath,
|
||||
record,
|
||||
bundleFormat: bundlePayload.bundleFormat,
|
||||
});
|
||||
if (bundleFailure) {
|
||||
missing.push({ pluginId, installPath, reason: "missing-package-json" });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const packageJsonPath = path.join(installPath, "package.json");
|
||||
if (!(await pathExists(packageJsonPath))) {
|
||||
missing.push({ pluginId, installPath, reason: "missing-package-json" });
|
||||
}
|
||||
}
|
||||
return missing;
|
||||
}
|
||||
|
||||
function formatMissingPluginPayloadReason(entry: MissingPluginInstallPayload): string {
|
||||
if (entry.reason === "missing-install-path") {
|
||||
return "installPath is missing";
|
||||
@@ -295,58 +188,6 @@ function isActionableSkippedPostUpdateOutcome(outcome: PluginUpdateOutcome): boo
|
||||
return isDisabledAfterFailureOutcome(outcome) || isClawHubTrustSkippedOutcome(outcome);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the post-core-update result we return when the active config cannot
|
||||
* even be parsed. Mandatory post-core convergence requires a parseable
|
||||
* config to know which plugins are configured; if one isn't available, we
|
||||
* refuse to restart the gateway and surface this as a hard error so the
|
||||
* existing `status === "error"` ⇒ `exit 1` pre-restart gate fires.
|
||||
*
|
||||
*/
|
||||
function buildInvalidConfigPostCoreUpdateResult(): {
|
||||
message: string;
|
||||
guidance: string[];
|
||||
result: PostCorePluginUpdateResult;
|
||||
} {
|
||||
const guidance = [
|
||||
"Run `openclaw doctor` to inspect the config validation errors.",
|
||||
"Once the config parses, rerun `openclaw update repair`.",
|
||||
];
|
||||
const message =
|
||||
"Plugin post-update convergence skipped because the config is invalid; refusing to restart the gateway with an unverified plugin set.";
|
||||
return {
|
||||
message,
|
||||
guidance,
|
||||
result: {
|
||||
status: "error",
|
||||
reason: "invalid-config",
|
||||
changed: false,
|
||||
sync: {
|
||||
changed: false,
|
||||
switchedToBundled: [],
|
||||
switchedToNpm: [],
|
||||
warnings: [],
|
||||
errors: [],
|
||||
},
|
||||
npm: {
|
||||
changed: false,
|
||||
outcomes: [],
|
||||
},
|
||||
integrityDrifts: [],
|
||||
warnings: [{ reason: "invalid-config", message, guidance }],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (process.env.VITEST || process.env.NODE_ENV === "test") {
|
||||
(globalThis as Record<PropertyKey, unknown>)[Symbol.for("openclaw.updateCommandPluginsTestApi")] =
|
||||
{
|
||||
buildInvalidConfigPostCoreUpdateResult,
|
||||
collectMissingPluginInstallPayloads,
|
||||
resolvePostSyncPluginUpdateSkipIds,
|
||||
};
|
||||
}
|
||||
|
||||
export async function updatePluginsAfterCoreUpdate(params: {
|
||||
root: string;
|
||||
channel: UpdateChannel;
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import type { GatewayService } from "../../daemon/service.js";
|
||||
import type { UpdateRunResult } from "../../infra/update-runner.js";
|
||||
import { replaceCliName, resolveCliName } from "../cli-name.js";
|
||||
import { formatCliCommand } from "../command-format.js";
|
||||
import {
|
||||
waitForGatewayHealthyRestart,
|
||||
type GatewayRestartSnapshot,
|
||||
} from "../daemon-cli/restart-health.js";
|
||||
import {
|
||||
recoverInstalledLaunchAgentAfterUpdate,
|
||||
type PostUpdateLaunchAgentRecoveryResult,
|
||||
} from "./update-command-launch-agent-recovery.js";
|
||||
|
||||
const CLI_NAME = resolveCliName();
|
||||
|
||||
export function isPackageManagerUpdateMode(
|
||||
mode: UpdateRunResult["mode"],
|
||||
): mode is "npm" | "pnpm" | "bun" {
|
||||
return mode === "npm" || mode === "pnpm" || mode === "bun";
|
||||
}
|
||||
|
||||
export function shouldUseLegacyProcessRestartAfterUpdate(params: {
|
||||
updateMode: UpdateRunResult["mode"];
|
||||
}): boolean {
|
||||
return !isPackageManagerUpdateMode(params.updateMode);
|
||||
}
|
||||
|
||||
type PostUpdateGatewayHealthRecoveryDeps = {
|
||||
recoverLaunchAgent?: typeof recoverInstalledLaunchAgentAfterUpdate;
|
||||
waitForHealthy?: typeof waitForGatewayHealthyRestart;
|
||||
};
|
||||
|
||||
export async function recoverLaunchAgentAndRecheckGatewayHealth(params: {
|
||||
health: GatewayRestartSnapshot;
|
||||
service: GatewayService;
|
||||
port: number;
|
||||
expectedVersion?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
deps?: PostUpdateGatewayHealthRecoveryDeps;
|
||||
}): Promise<{
|
||||
health: GatewayRestartSnapshot;
|
||||
launchAgentRecovery: PostUpdateLaunchAgentRecoveryResult | null;
|
||||
}> {
|
||||
if (params.health.healthy) {
|
||||
return { health: params.health, launchAgentRecovery: null };
|
||||
}
|
||||
|
||||
const recoverLaunchAgent =
|
||||
params.deps?.recoverLaunchAgent ?? recoverInstalledLaunchAgentAfterUpdate;
|
||||
const launchAgentRecovery = await recoverLaunchAgent({
|
||||
service: params.service,
|
||||
env: params.env,
|
||||
});
|
||||
if (!launchAgentRecovery.recovered) {
|
||||
return { health: params.health, launchAgentRecovery };
|
||||
}
|
||||
|
||||
const waitForHealthy = params.deps?.waitForHealthy ?? waitForGatewayHealthyRestart;
|
||||
const health = await waitForHealthy({
|
||||
service: params.service,
|
||||
port: params.port,
|
||||
expectedVersion: params.expectedVersion,
|
||||
env: params.env,
|
||||
supervisorKeepsAlive: true,
|
||||
});
|
||||
return { health, launchAgentRecovery };
|
||||
}
|
||||
|
||||
export async function hasLoadedLaunchdKeepAliveSupervisor(params: {
|
||||
service: GatewayService;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<boolean> {
|
||||
if (process.platform !== "darwin") {
|
||||
return false;
|
||||
}
|
||||
// OpenClaw's loaded LaunchAgent has canonical KeepAlive policy. Read this once before
|
||||
// polling so an unloaded agent can still reach the existing recovery path promptly.
|
||||
return await params.service.isLoaded({ env: params.env }).catch(() => false);
|
||||
}
|
||||
|
||||
function formatPostUpdateGatewayRecoveryLine(platform: NodeJS.Platform): string {
|
||||
const restartCommand = replaceCliName(formatCliCommand("openclaw gateway restart"), CLI_NAME);
|
||||
const installCommand = replaceCliName(
|
||||
formatCliCommand("openclaw gateway install --force"),
|
||||
CLI_NAME,
|
||||
);
|
||||
const statusCommand = replaceCliName(
|
||||
formatCliCommand("openclaw gateway status --deep"),
|
||||
CLI_NAME,
|
||||
);
|
||||
if (platform === "darwin") {
|
||||
return `Recovery: run \`${restartCommand}\`; if the LaunchAgent is installed but not loaded, run \`${installCommand}\` from the logged-in macOS user session, then rerun \`${statusCommand}\`.`;
|
||||
}
|
||||
if (platform === "linux") {
|
||||
return `Recovery: run \`${restartCommand}\`; if the systemd user service is missing, stale, or not active, run \`${installCommand}\` from the same user account, then rerun \`${statusCommand}\`.`;
|
||||
}
|
||||
if (platform === "win32") {
|
||||
return `Recovery: run \`${restartCommand}\`; if the gateway Scheduled Task or Windows login item is missing, stale, or not running, run \`${installCommand}\` from the same user account, then rerun \`${statusCommand}\`.`;
|
||||
}
|
||||
return `Recovery: run \`${restartCommand}\`; if the local service manager reports the gateway service is missing, stale, or not running, run \`${installCommand}\` from the same user account, then rerun \`${statusCommand}\`.`;
|
||||
}
|
||||
|
||||
export function formatPostUpdateGatewayRecoveryInstructions(
|
||||
result: UpdateRunResult,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): string[] {
|
||||
const lines = [formatPostUpdateGatewayRecoveryLine(platform)];
|
||||
const beforeVersion = normalizeOptionalString(result.before?.version);
|
||||
if (isPackageManagerUpdateMode(result.mode) && beforeVersion) {
|
||||
lines.push(
|
||||
`Rollback: reinstall OpenClaw ${beforeVersion} with the same package manager, then rerun \`${replaceCliName(formatCliCommand("openclaw gateway install --force"), CLI_NAME)}\`.`,
|
||||
);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
@@ -1,98 +1,15 @@
|
||||
import type { GatewayServiceState } from "../../daemon/service-types.js";
|
||||
import type { GatewayService } from "../../daemon/service.js";
|
||||
import type { UpdateRunResult } from "../../infra/update-runner.js";
|
||||
import type { GatewayRestartSnapshot } from "../daemon-cli/restart-health.js";
|
||||
import "./update-command-service.js";
|
||||
import { recoverInstalledLaunchAgentAfterUpdate } from "./update-command-launch-agent-recovery.js";
|
||||
import {
|
||||
formatPostUpdateGatewayRecoveryInstructions,
|
||||
hasLoadedLaunchdKeepAliveSupervisor,
|
||||
recoverLaunchAgentAndRecheckGatewayHealth,
|
||||
shouldUseLegacyProcessRestartAfterUpdate,
|
||||
} from "./update-command-service-recovery.js";
|
||||
|
||||
type PostUpdateLaunchAgentRecoveryResult =
|
||||
| { attempted: false; recovered: false }
|
||||
| { attempted: true; recovered: true; message: string }
|
||||
| { attempted: true; recovered: false; detail: string };
|
||||
|
||||
type UpdateCommandServiceTestApi = {
|
||||
formatPostUpdateGatewayRecoveryInstructions(
|
||||
result: UpdateRunResult,
|
||||
platform?: NodeJS.Platform,
|
||||
): string[];
|
||||
recoverInstalledLaunchAgentAfterUpdate(params: {
|
||||
service?: GatewayService;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
deps?: {
|
||||
platform?: NodeJS.Platform;
|
||||
readState?: (
|
||||
service: GatewayService,
|
||||
args: { env?: NodeJS.ProcessEnv },
|
||||
) => Promise<GatewayServiceState>;
|
||||
recover?: (params: {
|
||||
result: "restarted";
|
||||
env?: Record<string, string | undefined>;
|
||||
}) => Promise<{ result: "restarted"; loaded: true; message: string } | null>;
|
||||
};
|
||||
}): Promise<PostUpdateLaunchAgentRecoveryResult>;
|
||||
recoverLaunchAgentAndRecheckGatewayHealth(params: {
|
||||
health: GatewayRestartSnapshot;
|
||||
service: GatewayService;
|
||||
port: number;
|
||||
expectedVersion?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
deps?: {
|
||||
recoverLaunchAgent?: (params: {
|
||||
service?: GatewayService;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}) => Promise<PostUpdateLaunchAgentRecoveryResult>;
|
||||
waitForHealthy?: (params: {
|
||||
service: GatewayService;
|
||||
port: number;
|
||||
expectedVersion?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}) => Promise<GatewayRestartSnapshot>;
|
||||
};
|
||||
}): Promise<{
|
||||
health: GatewayRestartSnapshot;
|
||||
launchAgentRecovery: PostUpdateLaunchAgentRecoveryResult | null;
|
||||
}>;
|
||||
hasLoadedLaunchdKeepAliveSupervisor(params: {
|
||||
service: GatewayService;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<boolean>;
|
||||
shouldUseLegacyProcessRestartAfterUpdate(params: {
|
||||
updateMode: UpdateRunResult["mode"];
|
||||
}): boolean;
|
||||
export const testing = {
|
||||
formatPostUpdateGatewayRecoveryInstructions,
|
||||
recoverInstalledLaunchAgentAfterUpdate,
|
||||
recoverLaunchAgentAndRecheckGatewayHealth,
|
||||
hasLoadedLaunchdKeepAliveSupervisor,
|
||||
shouldUseLegacyProcessRestartAfterUpdate,
|
||||
};
|
||||
|
||||
function getTestApi(): UpdateCommandServiceTestApi {
|
||||
return (globalThis as Record<PropertyKey, unknown>)[
|
||||
Symbol.for("openclaw.updateCommandServiceTestApi")
|
||||
] as UpdateCommandServiceTestApi;
|
||||
}
|
||||
|
||||
export function formatPostUpdateGatewayRecoveryInstructions(
|
||||
result: UpdateRunResult,
|
||||
platform?: NodeJS.Platform,
|
||||
): string[] {
|
||||
return getTestApi().formatPostUpdateGatewayRecoveryInstructions(result, platform);
|
||||
}
|
||||
|
||||
export async function recoverInstalledLaunchAgentAfterUpdate(
|
||||
params: Parameters<UpdateCommandServiceTestApi["recoverInstalledLaunchAgentAfterUpdate"]>[0],
|
||||
): Promise<PostUpdateLaunchAgentRecoveryResult> {
|
||||
return await getTestApi().recoverInstalledLaunchAgentAfterUpdate(params);
|
||||
}
|
||||
|
||||
export async function recoverLaunchAgentAndRecheckGatewayHealth(
|
||||
params: Parameters<UpdateCommandServiceTestApi["recoverLaunchAgentAndRecheckGatewayHealth"]>[0],
|
||||
) {
|
||||
return await getTestApi().recoverLaunchAgentAndRecheckGatewayHealth(params);
|
||||
}
|
||||
|
||||
export async function hasLoadedLaunchdKeepAliveSupervisor(
|
||||
params: Parameters<UpdateCommandServiceTestApi["hasLoadedLaunchdKeepAliveSupervisor"]>[0],
|
||||
): Promise<boolean> {
|
||||
return await getTestApi().hasLoadedLaunchdKeepAliveSupervisor(params);
|
||||
}
|
||||
|
||||
export function shouldUseLegacyProcessRestartAfterUpdate(
|
||||
params: Parameters<UpdateCommandServiceTestApi["shouldUseLegacyProcessRestartAfterUpdate"]>[0],
|
||||
): boolean {
|
||||
return getTestApi().shouldUseLegacyProcessRestartAfterUpdate(params);
|
||||
}
|
||||
|
||||
@@ -28,11 +28,7 @@ import {
|
||||
} from "../../daemon/schtasks.js";
|
||||
import { summarizeGatewayServiceLayout } from "../../daemon/service-layout.js";
|
||||
import type { GatewayServiceCommandConfig } from "../../daemon/service-types.js";
|
||||
import {
|
||||
readGatewayServiceState,
|
||||
resolveGatewayService,
|
||||
type GatewayService,
|
||||
} from "../../daemon/service.js";
|
||||
import { readGatewayServiceState, resolveGatewayService } from "../../daemon/service.js";
|
||||
import { parseStrictPositiveInteger } from "../../infra/parse-finite-number.js";
|
||||
import { getSelfAndAncestorPidsSync } from "../../infra/restart-stale-pids.js";
|
||||
import { nodeVersionSatisfiesEngine } from "../../infra/runtime-guard.js";
|
||||
@@ -45,12 +41,10 @@ import { replaceCliName, resolveCliName } from "../cli-name.js";
|
||||
import { formatCliCommand } from "../command-format.js";
|
||||
import { installCompletion } from "../completion-runtime.js";
|
||||
import { runDaemonInstall, runDaemonRestart } from "../daemon-cli.js";
|
||||
import { recoverInstalledLaunchAgent } from "../daemon-cli/launchd-recovery.js";
|
||||
import {
|
||||
renderRestartDiagnostics,
|
||||
terminateStaleGatewayPids,
|
||||
waitForGatewayHealthyRestart,
|
||||
type GatewayRestartSnapshot,
|
||||
} from "../daemon-cli/restart-health.js";
|
||||
import {
|
||||
registerSignalExitBarrier,
|
||||
@@ -60,6 +54,15 @@ import {
|
||||
import { runRestartScript } from "./restart-helper.js";
|
||||
import { resolveNodeRunner, type UpdateCommandOptions } from "./shared.js";
|
||||
import { createUpdateConfigSnapshot } from "./update-command-config.js";
|
||||
import {
|
||||
formatPostUpdateGatewayRecoveryInstructions,
|
||||
hasLoadedLaunchdKeepAliveSupervisor,
|
||||
isPackageManagerUpdateMode,
|
||||
recoverLaunchAgentAndRecheckGatewayHealth,
|
||||
shouldUseLegacyProcessRestartAfterUpdate,
|
||||
} from "./update-command-service-recovery.js";
|
||||
|
||||
export { isPackageManagerUpdateMode } from "./update-command-service-recovery.js";
|
||||
|
||||
const CLI_NAME = resolveCliName();
|
||||
const SERVICE_REFRESH_TIMEOUT_MS = 60_000;
|
||||
@@ -80,12 +83,6 @@ const JSON_MODE_SERVICE_STDOUT = new Writable({
|
||||
},
|
||||
});
|
||||
|
||||
export function isPackageManagerUpdateMode(
|
||||
mode: UpdateRunResult["mode"],
|
||||
): mode is "npm" | "pnpm" | "bun" {
|
||||
return mode === "npm" || mode === "pnpm" || mode === "bun";
|
||||
}
|
||||
|
||||
export function shouldPrepareUpdatedInstallRestart(params: {
|
||||
updateMode: UpdateRunResult["mode"];
|
||||
serviceInstalled: boolean;
|
||||
@@ -109,163 +106,6 @@ export function shouldPrepareUpdatedInstallRestart(params: {
|
||||
return params.serviceLoaded;
|
||||
}
|
||||
|
||||
function shouldUseLegacyProcessRestartAfterUpdate(params: {
|
||||
updateMode: UpdateRunResult["mode"];
|
||||
}): boolean {
|
||||
return !isPackageManagerUpdateMode(params.updateMode);
|
||||
}
|
||||
|
||||
type PostUpdateLaunchAgentRecoveryResult =
|
||||
| { attempted: false; recovered: false }
|
||||
| { attempted: true; recovered: true; message: string }
|
||||
| { attempted: true; recovered: false; detail: string };
|
||||
|
||||
type PostUpdateLaunchAgentRecoveryDeps = {
|
||||
platform?: NodeJS.Platform;
|
||||
readState?: typeof readGatewayServiceState;
|
||||
recover?: typeof recoverInstalledLaunchAgent;
|
||||
};
|
||||
|
||||
async function recoverInstalledLaunchAgentAfterUpdate(params: {
|
||||
service?: GatewayService;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
deps?: PostUpdateLaunchAgentRecoveryDeps;
|
||||
}): Promise<PostUpdateLaunchAgentRecoveryResult> {
|
||||
const platform = params.deps?.platform ?? process.platform;
|
||||
if (platform !== "darwin") {
|
||||
return { attempted: false, recovered: false };
|
||||
}
|
||||
|
||||
const service = params.service ?? resolveGatewayService();
|
||||
const readState = params.deps?.readState ?? readGatewayServiceState;
|
||||
const recover = params.deps?.recover ?? recoverInstalledLaunchAgent;
|
||||
const state = await readState(service, { env: params.env }).catch(() => null);
|
||||
if (state?.loaded) {
|
||||
return { attempted: false, recovered: false };
|
||||
}
|
||||
if (state && !state.installed && !state.runtime?.missingSupervision) {
|
||||
return { attempted: false, recovered: false };
|
||||
}
|
||||
|
||||
const recovered = await recover({ result: "restarted", env: state?.env ?? params.env }).catch(
|
||||
() => null,
|
||||
);
|
||||
if (!recovered) {
|
||||
return {
|
||||
attempted: true,
|
||||
recovered: false,
|
||||
detail:
|
||||
"LaunchAgent was installed but not loaded; automatic bootstrap/kickstart recovery failed.",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
attempted: true,
|
||||
recovered: true,
|
||||
message: recovered.message,
|
||||
};
|
||||
}
|
||||
|
||||
type PostUpdateGatewayHealthRecoveryDeps = {
|
||||
recoverLaunchAgent?: typeof recoverInstalledLaunchAgentAfterUpdate;
|
||||
waitForHealthy?: typeof waitForGatewayHealthyRestart;
|
||||
};
|
||||
|
||||
async function recoverLaunchAgentAndRecheckGatewayHealth(params: {
|
||||
health: GatewayRestartSnapshot;
|
||||
service: GatewayService;
|
||||
port: number;
|
||||
expectedVersion?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
deps?: PostUpdateGatewayHealthRecoveryDeps;
|
||||
}): Promise<{
|
||||
health: GatewayRestartSnapshot;
|
||||
launchAgentRecovery: PostUpdateLaunchAgentRecoveryResult | null;
|
||||
}> {
|
||||
if (params.health.healthy) {
|
||||
return { health: params.health, launchAgentRecovery: null };
|
||||
}
|
||||
|
||||
const recoverLaunchAgent =
|
||||
params.deps?.recoverLaunchAgent ?? recoverInstalledLaunchAgentAfterUpdate;
|
||||
const launchAgentRecovery = await recoverLaunchAgent({
|
||||
service: params.service,
|
||||
env: params.env,
|
||||
});
|
||||
if (!launchAgentRecovery.recovered) {
|
||||
return { health: params.health, launchAgentRecovery };
|
||||
}
|
||||
|
||||
const waitForHealthy = params.deps?.waitForHealthy ?? waitForGatewayHealthyRestart;
|
||||
const health = await waitForHealthy({
|
||||
service: params.service,
|
||||
port: params.port,
|
||||
expectedVersion: params.expectedVersion,
|
||||
env: params.env,
|
||||
supervisorKeepsAlive: true,
|
||||
});
|
||||
return { health, launchAgentRecovery };
|
||||
}
|
||||
|
||||
async function hasLoadedLaunchdKeepAliveSupervisor(params: {
|
||||
service: GatewayService;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<boolean> {
|
||||
if (process.platform !== "darwin") {
|
||||
return false;
|
||||
}
|
||||
// OpenClaw's loaded LaunchAgent has canonical KeepAlive policy. Read this once before
|
||||
// polling so an unloaded agent can still reach the existing recovery path promptly.
|
||||
return await params.service.isLoaded({ env: params.env }).catch(() => false);
|
||||
}
|
||||
|
||||
function formatPostUpdateGatewayRecoveryLine(platform: NodeJS.Platform): string {
|
||||
const restartCommand = replaceCliName(formatCliCommand("openclaw gateway restart"), CLI_NAME);
|
||||
const installCommand = replaceCliName(
|
||||
formatCliCommand("openclaw gateway install --force"),
|
||||
CLI_NAME,
|
||||
);
|
||||
const statusCommand = replaceCliName(
|
||||
formatCliCommand("openclaw gateway status --deep"),
|
||||
CLI_NAME,
|
||||
);
|
||||
if (platform === "darwin") {
|
||||
return `Recovery: run \`${restartCommand}\`; if the LaunchAgent is installed but not loaded, run \`${installCommand}\` from the logged-in macOS user session, then rerun \`${statusCommand}\`.`;
|
||||
}
|
||||
if (platform === "linux") {
|
||||
return `Recovery: run \`${restartCommand}\`; if the systemd user service is missing, stale, or not active, run \`${installCommand}\` from the same user account, then rerun \`${statusCommand}\`.`;
|
||||
}
|
||||
if (platform === "win32") {
|
||||
return `Recovery: run \`${restartCommand}\`; if the gateway Scheduled Task or Windows login item is missing, stale, or not running, run \`${installCommand}\` from the same user account, then rerun \`${statusCommand}\`.`;
|
||||
}
|
||||
return `Recovery: run \`${restartCommand}\`; if the local service manager reports the gateway service is missing, stale, or not running, run \`${installCommand}\` from the same user account, then rerun \`${statusCommand}\`.`;
|
||||
}
|
||||
|
||||
function formatPostUpdateGatewayRecoveryInstructions(
|
||||
result: UpdateRunResult,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): string[] {
|
||||
const lines = [formatPostUpdateGatewayRecoveryLine(platform)];
|
||||
const beforeVersion = normalizeOptionalString(result.before?.version);
|
||||
if (isPackageManagerUpdateMode(result.mode) && beforeVersion) {
|
||||
lines.push(
|
||||
`Rollback: reinstall OpenClaw ${beforeVersion} with the same package manager, then rerun \`${replaceCliName(formatCliCommand("openclaw gateway install --force"), CLI_NAME)}\`.`,
|
||||
);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
if (process.env.VITEST || process.env.NODE_ENV === "test") {
|
||||
(globalThis as Record<PropertyKey, unknown>)[Symbol.for("openclaw.updateCommandServiceTestApi")] =
|
||||
{
|
||||
formatPostUpdateGatewayRecoveryInstructions,
|
||||
recoverInstalledLaunchAgentAfterUpdate,
|
||||
recoverLaunchAgentAndRecheckGatewayHealth,
|
||||
hasLoadedLaunchdKeepAliveSupervisor,
|
||||
shouldUseLegacyProcessRestartAfterUpdate,
|
||||
};
|
||||
}
|
||||
|
||||
export type PreManagedServiceStop = {
|
||||
stopped: boolean;
|
||||
inspected: boolean;
|
||||
|
||||
@@ -10,11 +10,7 @@ import {
|
||||
updatePluginsAfterCoreUpdate,
|
||||
type PostCorePluginUpdateResult,
|
||||
} from "./update-command-plugins.js";
|
||||
import {
|
||||
buildInvalidConfigPostCoreUpdateResult,
|
||||
collectMissingPluginInstallPayloads,
|
||||
resolvePostSyncPluginUpdateSkipIds,
|
||||
} from "./update-command-plugins.test-support.js";
|
||||
import { testing as updateCommandPluginsTesting } from "./update-command-plugins.test-support.js";
|
||||
import { resolvePostCoreUpdateChildStdio } from "./update-command-post-core.js";
|
||||
import { applyPostPluginConfigValidation } from "./update-command-post-plugin-validation.js";
|
||||
import {
|
||||
@@ -23,13 +19,7 @@ import {
|
||||
resolveUpdatedGatewayRestartPort,
|
||||
shouldPrepareUpdatedInstallRestart,
|
||||
} from "./update-command-service.js";
|
||||
import {
|
||||
formatPostUpdateGatewayRecoveryInstructions,
|
||||
hasLoadedLaunchdKeepAliveSupervisor,
|
||||
recoverInstalledLaunchAgentAfterUpdate,
|
||||
recoverLaunchAgentAndRecheckGatewayHealth,
|
||||
shouldUseLegacyProcessRestartAfterUpdate,
|
||||
} from "./update-command-service.test-support.js";
|
||||
import { testing as updateCommandServiceTesting } from "./update-command-service.test-support.js";
|
||||
|
||||
describe("resolveGatewayInstallEntrypoint", () => {
|
||||
it("prefers dist/index.js over dist/entry.js when both exist", async () => {
|
||||
@@ -294,7 +284,7 @@ describe("collectMissingPluginInstallPayloads", () => {
|
||||
await fs.mkdir(noPackageJsonDir, { recursive: true });
|
||||
|
||||
await expect(
|
||||
collectMissingPluginInstallPayloads({
|
||||
updateCommandPluginsTesting.collectMissingPluginInstallPayloads({
|
||||
env: { HOME: tmpDir } as NodeJS.ProcessEnv,
|
||||
records: {
|
||||
present: {
|
||||
@@ -355,7 +345,7 @@ describe("collectMissingPluginInstallPayloads", () => {
|
||||
"utf8",
|
||||
);
|
||||
await expect(
|
||||
collectMissingPluginInstallPayloads({
|
||||
updateCommandPluginsTesting.collectMissingPluginInstallPayloads({
|
||||
env: { HOME: tmpDir } as NodeJS.ProcessEnv,
|
||||
records: {
|
||||
"cursor-bundle": {
|
||||
@@ -382,7 +372,7 @@ describe("collectMissingPluginInstallPayloads", () => {
|
||||
"utf8",
|
||||
);
|
||||
await expect(
|
||||
collectMissingPluginInstallPayloads({
|
||||
updateCommandPluginsTesting.collectMissingPluginInstallPayloads({
|
||||
env: { HOME: tmpDir } as NodeJS.ProcessEnv,
|
||||
records: {
|
||||
"cursor-bundle": {
|
||||
@@ -419,7 +409,7 @@ describe("collectMissingPluginInstallPayloads", () => {
|
||||
"utf8",
|
||||
);
|
||||
await expect(
|
||||
collectMissingPluginInstallPayloads({
|
||||
updateCommandPluginsTesting.collectMissingPluginInstallPayloads({
|
||||
env: { HOME: tmpDir } as NodeJS.ProcessEnv,
|
||||
records: {
|
||||
"dual-format-bundle": {
|
||||
@@ -442,7 +432,7 @@ describe("collectMissingPluginInstallPayloads", () => {
|
||||
await fs.mkdir(path.join(bundleDir, ".codex-plugin"), { recursive: true });
|
||||
await fs.writeFile(path.join(bundleDir, ".codex-plugin", "plugin.json"), "[]", "utf8");
|
||||
await expect(
|
||||
collectMissingPluginInstallPayloads({
|
||||
updateCommandPluginsTesting.collectMissingPluginInstallPayloads({
|
||||
env: { HOME: tmpDir } as NodeJS.ProcessEnv,
|
||||
records: {
|
||||
"bad-bundle": {
|
||||
@@ -469,7 +459,7 @@ describe("collectMissingPluginInstallPayloads", () => {
|
||||
const missingDir = path.join(tmpDir, "state", "npm", "node_modules", "@openclaw", "missing");
|
||||
try {
|
||||
await expect(
|
||||
collectMissingPluginInstallPayloads({
|
||||
updateCommandPluginsTesting.collectMissingPluginInstallPayloads({
|
||||
env: { HOME: tmpDir } as NodeJS.ProcessEnv,
|
||||
skipDisabledPlugins: true,
|
||||
config: {
|
||||
@@ -500,7 +490,7 @@ describe("collectMissingPluginInstallPayloads", () => {
|
||||
const missingDir = path.join(tmpDir, "state", "npm", "node_modules", "@openclaw", "codex");
|
||||
try {
|
||||
await expect(
|
||||
collectMissingPluginInstallPayloads({
|
||||
updateCommandPluginsTesting.collectMissingPluginInstallPayloads({
|
||||
env: { HOME: tmpDir } as NodeJS.ProcessEnv,
|
||||
skipDisabledPlugins: true,
|
||||
syncOfficialPluginInstalls: true,
|
||||
@@ -540,7 +530,7 @@ describe("collectMissingPluginInstallPayloads", () => {
|
||||
const missingDir = path.join(tmpDir, "state", "clawhub", "diagnostics-otel");
|
||||
try {
|
||||
await expect(
|
||||
collectMissingPluginInstallPayloads({
|
||||
updateCommandPluginsTesting.collectMissingPluginInstallPayloads({
|
||||
env: { HOME: tmpDir } as NodeJS.ProcessEnv,
|
||||
skipDisabledPlugins: true,
|
||||
syncOfficialPluginInstalls: true,
|
||||
@@ -577,7 +567,7 @@ describe("collectMissingPluginInstallPayloads", () => {
|
||||
describe("resolvePostSyncPluginUpdateSkipIds", () => {
|
||||
it("skips plugins already switched through ClawHub or npm and repaired payloads", () => {
|
||||
expect(
|
||||
resolvePostSyncPluginUpdateSkipIds({
|
||||
updateCommandPluginsTesting.resolvePostSyncPluginUpdateSkipIds({
|
||||
switchedToClawHub: ["whatsapp"],
|
||||
switchedToNpm: ["voice-call"],
|
||||
repairedMissingPayloadIds: new Set(["telegram"]),
|
||||
@@ -588,14 +578,26 @@ describe("resolvePostSyncPluginUpdateSkipIds", () => {
|
||||
|
||||
describe("shouldUseLegacyProcessRestartAfterUpdate", () => {
|
||||
it("never restarts package updates through the pre-update process", () => {
|
||||
expect(shouldUseLegacyProcessRestartAfterUpdate({ updateMode: "npm" })).toBe(false);
|
||||
expect(shouldUseLegacyProcessRestartAfterUpdate({ updateMode: "pnpm" })).toBe(false);
|
||||
expect(shouldUseLegacyProcessRestartAfterUpdate({ updateMode: "bun" })).toBe(false);
|
||||
expect(
|
||||
updateCommandServiceTesting.shouldUseLegacyProcessRestartAfterUpdate({ updateMode: "npm" }),
|
||||
).toBe(false);
|
||||
expect(
|
||||
updateCommandServiceTesting.shouldUseLegacyProcessRestartAfterUpdate({ updateMode: "pnpm" }),
|
||||
).toBe(false);
|
||||
expect(
|
||||
updateCommandServiceTesting.shouldUseLegacyProcessRestartAfterUpdate({ updateMode: "bun" }),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps the in-process restart path for non-package updates", () => {
|
||||
expect(shouldUseLegacyProcessRestartAfterUpdate({ updateMode: "git" })).toBe(true);
|
||||
expect(shouldUseLegacyProcessRestartAfterUpdate({ updateMode: "unknown" })).toBe(true);
|
||||
expect(
|
||||
updateCommandServiceTesting.shouldUseLegacyProcessRestartAfterUpdate({ updateMode: "git" }),
|
||||
).toBe(true);
|
||||
expect(
|
||||
updateCommandServiceTesting.shouldUseLegacyProcessRestartAfterUpdate({
|
||||
updateMode: "unknown",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -608,7 +610,10 @@ describe("formatPostUpdateGatewayRecoveryInstructions", () => {
|
||||
};
|
||||
|
||||
it("uses systemd wording on Linux instead of macOS LaunchAgent instructions", () => {
|
||||
const [line] = formatPostUpdateGatewayRecoveryInstructions(result, "linux");
|
||||
const [line] = updateCommandServiceTesting.formatPostUpdateGatewayRecoveryInstructions(
|
||||
result,
|
||||
"linux",
|
||||
);
|
||||
|
||||
expect(line).toContain("the systemd user service");
|
||||
expect(line).toContain("openclaw gateway restart");
|
||||
@@ -620,14 +625,20 @@ describe("formatPostUpdateGatewayRecoveryInstructions", () => {
|
||||
});
|
||||
|
||||
it("keeps LaunchAgent recovery wording on macOS", () => {
|
||||
const [line] = formatPostUpdateGatewayRecoveryInstructions(result, "darwin");
|
||||
const [line] = updateCommandServiceTesting.formatPostUpdateGatewayRecoveryInstructions(
|
||||
result,
|
||||
"darwin",
|
||||
);
|
||||
|
||||
expect(line).toContain("the LaunchAgent is installed but not loaded");
|
||||
expect(line).toContain("logged-in macOS user session");
|
||||
});
|
||||
|
||||
it("uses Windows service-manager wording on Windows", () => {
|
||||
const [line] = formatPostUpdateGatewayRecoveryInstructions(result, "win32");
|
||||
const [line] = updateCommandServiceTesting.formatPostUpdateGatewayRecoveryInstructions(
|
||||
result,
|
||||
"win32",
|
||||
);
|
||||
|
||||
expect(line).toContain("the gateway Scheduled Task or Windows login item");
|
||||
expect(line).not.toContain("LaunchAgent");
|
||||
@@ -635,7 +646,10 @@ describe("formatPostUpdateGatewayRecoveryInstructions", () => {
|
||||
});
|
||||
|
||||
it("uses generic service-manager wording for unsupported Node platforms", () => {
|
||||
const [line] = formatPostUpdateGatewayRecoveryInstructions(result, "freebsd");
|
||||
const [line] = updateCommandServiceTesting.formatPostUpdateGatewayRecoveryInstructions(
|
||||
result,
|
||||
"freebsd",
|
||||
);
|
||||
|
||||
expect(line).toContain("local service manager");
|
||||
expect(line).not.toContain("systemd");
|
||||
@@ -664,7 +678,7 @@ describe("recoverInstalledLaunchAgentAfterUpdate", () => {
|
||||
}));
|
||||
|
||||
await expect(
|
||||
recoverInstalledLaunchAgentAfterUpdate({
|
||||
updateCommandServiceTesting.recoverInstalledLaunchAgentAfterUpdate({
|
||||
service,
|
||||
env: serviceEnv,
|
||||
deps: {
|
||||
@@ -688,7 +702,7 @@ describe("recoverInstalledLaunchAgentAfterUpdate", () => {
|
||||
const recover = vi.fn();
|
||||
|
||||
await expect(
|
||||
recoverInstalledLaunchAgentAfterUpdate({
|
||||
updateCommandServiceTesting.recoverInstalledLaunchAgentAfterUpdate({
|
||||
service: {} as never,
|
||||
deps: {
|
||||
platform: "linux",
|
||||
@@ -714,7 +728,7 @@ describe("recoverInstalledLaunchAgentAfterUpdate", () => {
|
||||
const recover = vi.fn();
|
||||
|
||||
await expect(
|
||||
recoverInstalledLaunchAgentAfterUpdate({
|
||||
updateCommandServiceTesting.recoverInstalledLaunchAgentAfterUpdate({
|
||||
service: {} as never,
|
||||
deps: {
|
||||
platform: "darwin",
|
||||
@@ -739,7 +753,7 @@ describe("recoverInstalledLaunchAgentAfterUpdate", () => {
|
||||
const recover = vi.fn(async () => null);
|
||||
|
||||
await expect(
|
||||
recoverInstalledLaunchAgentAfterUpdate({
|
||||
updateCommandServiceTesting.recoverInstalledLaunchAgentAfterUpdate({
|
||||
service: {} as never,
|
||||
deps: {
|
||||
platform: "darwin",
|
||||
@@ -782,7 +796,7 @@ describe("recoverLaunchAgentAndRecheckGatewayHealth", () => {
|
||||
const waitForHealthy = vi.fn(async () => healthy);
|
||||
|
||||
await expect(
|
||||
recoverLaunchAgentAndRecheckGatewayHealth({
|
||||
updateCommandServiceTesting.recoverLaunchAgentAndRecheckGatewayHealth({
|
||||
health: unhealthy,
|
||||
service,
|
||||
port: 18790,
|
||||
@@ -830,7 +844,7 @@ describe("recoverLaunchAgentAndRecheckGatewayHealth", () => {
|
||||
}));
|
||||
const waitForHealthy = vi.fn(async () => stillUnhealthy);
|
||||
|
||||
const result = await recoverLaunchAgentAndRecheckGatewayHealth({
|
||||
const result = await updateCommandServiceTesting.recoverLaunchAgentAndRecheckGatewayHealth({
|
||||
health: unhealthy,
|
||||
service,
|
||||
port: 18790,
|
||||
@@ -851,10 +865,15 @@ describe("hasLoadedLaunchdKeepAliveSupervisor", () => {
|
||||
const service = { isLoaded } as unknown as GatewayService;
|
||||
|
||||
await expect(
|
||||
hasLoadedLaunchdKeepAliveSupervisor({ service, env: { OPENCLAW_PROFILE: "work" } }),
|
||||
updateCommandServiceTesting.hasLoadedLaunchdKeepAliveSupervisor({
|
||||
service,
|
||||
env: { OPENCLAW_PROFILE: "work" },
|
||||
}),
|
||||
).resolves.toBe(false);
|
||||
isLoaded.mockResolvedValue(true);
|
||||
await expect(hasLoadedLaunchdKeepAliveSupervisor({ service })).resolves.toBe(true);
|
||||
await expect(
|
||||
updateCommandServiceTesting.hasLoadedLaunchdKeepAliveSupervisor({ service }),
|
||||
).resolves.toBe(true);
|
||||
|
||||
platformSpy.mockRestore();
|
||||
});
|
||||
@@ -864,7 +883,7 @@ describe("hasLoadedLaunchdKeepAliveSupervisor", () => {
|
||||
const isLoaded = vi.fn().mockResolvedValue(true);
|
||||
|
||||
await expect(
|
||||
hasLoadedLaunchdKeepAliveSupervisor({
|
||||
updateCommandServiceTesting.hasLoadedLaunchdKeepAliveSupervisor({
|
||||
service: { isLoaded } as unknown as GatewayService,
|
||||
}),
|
||||
).resolves.toBe(false);
|
||||
@@ -932,14 +951,14 @@ describe("updatePluginsAfterCoreUpdate (invalid config end-to-end)", () => {
|
||||
|
||||
describe("buildInvalidConfigPostCoreUpdateResult", () => {
|
||||
it("returns status:error so the existing pre-restart gate exits 1 instead of restarting on invalid config", () => {
|
||||
const built = buildInvalidConfigPostCoreUpdateResult();
|
||||
const built = updateCommandPluginsTesting.buildInvalidConfigPostCoreUpdateResult();
|
||||
expect(built.result.status).toBe("error");
|
||||
expect(built.result.reason).toBe("invalid-config");
|
||||
expect(built.result.changed).toBe(false);
|
||||
});
|
||||
|
||||
it("surfaces actionable repair guidance in both the structural warnings and the message string", () => {
|
||||
const built = buildInvalidConfigPostCoreUpdateResult();
|
||||
const built = updateCommandPluginsTesting.buildInvalidConfigPostCoreUpdateResult();
|
||||
expect(built.guidance).toStrictEqual([
|
||||
"Run `openclaw doctor` to inspect the config validation errors.",
|
||||
"Once the config parses, rerun `openclaw update repair`.",
|
||||
|
||||
Reference in New Issue
Block a user