mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-20 21:51:28 +00:00
refactor: dedupe cli config cron and install flows
This commit is contained in:
@@ -80,13 +80,8 @@ export function openBoundaryFileSync(params: OpenBoundaryFileSyncParams): Bounda
|
||||
if (resolved instanceof Promise) {
|
||||
return toBoundaryValidationError(new Error("Unexpected async boundary resolution"));
|
||||
}
|
||||
if ("ok" in resolved) {
|
||||
return resolved;
|
||||
}
|
||||
return openBoundaryFileResolved({
|
||||
absolutePath: resolved.absolutePath,
|
||||
resolvedPath: resolved.resolvedPath,
|
||||
rootRealPath: resolved.rootRealPath,
|
||||
return finalizeBoundaryFileOpen({
|
||||
resolved,
|
||||
maxBytes: params.maxBytes,
|
||||
rejectHardlinks: params.rejectHardlinks,
|
||||
allowedType: params.allowedType,
|
||||
@@ -123,6 +118,27 @@ function openBoundaryFileResolved(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function finalizeBoundaryFileOpen(params: {
|
||||
resolved: ResolvedBoundaryFilePath | BoundaryFileOpenResult;
|
||||
maxBytes?: number;
|
||||
rejectHardlinks?: boolean;
|
||||
allowedType?: SafeOpenSyncAllowedType;
|
||||
ioFs: BoundaryReadFs;
|
||||
}): BoundaryFileOpenResult {
|
||||
if ("ok" in params.resolved) {
|
||||
return params.resolved;
|
||||
}
|
||||
return openBoundaryFileResolved({
|
||||
absolutePath: params.resolved.absolutePath,
|
||||
resolvedPath: params.resolved.resolvedPath,
|
||||
rootRealPath: params.resolved.rootRealPath,
|
||||
maxBytes: params.maxBytes,
|
||||
rejectHardlinks: params.rejectHardlinks,
|
||||
allowedType: params.allowedType,
|
||||
ioFs: params.ioFs,
|
||||
});
|
||||
}
|
||||
|
||||
export async function openBoundaryFile(
|
||||
params: OpenBoundaryFileParams,
|
||||
): Promise<BoundaryFileOpenResult> {
|
||||
@@ -140,13 +156,8 @@ export async function openBoundaryFile(
|
||||
}),
|
||||
});
|
||||
const resolved = maybeResolved instanceof Promise ? await maybeResolved : maybeResolved;
|
||||
if ("ok" in resolved) {
|
||||
return resolved;
|
||||
}
|
||||
return openBoundaryFileResolved({
|
||||
absolutePath: resolved.absolutePath,
|
||||
resolvedPath: resolved.resolvedPath,
|
||||
rootRealPath: resolved.rootRealPath,
|
||||
return finalizeBoundaryFileOpen({
|
||||
resolved,
|
||||
maxBytes: params.maxBytes,
|
||||
rejectHardlinks: params.rejectHardlinks,
|
||||
allowedType: params.allowedType,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
buildChannelAccountSnapshot,
|
||||
formatChannelAllowFrom,
|
||||
resolveChannelAccountConfigured,
|
||||
resolveChannelAccountEnabled,
|
||||
} from "../channels/account-summary.js";
|
||||
import { listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import type { ChannelAccountSnapshot, ChannelPlugin } from "../channels/plugins/types.js";
|
||||
@@ -38,32 +40,6 @@ const formatAccountLabel = (params: { accountId: string; name?: string }) => {
|
||||
const accountLine = (label: string, details: string[]) =>
|
||||
` - ${label}${details.length ? ` (${details.join(", ")})` : ""}`;
|
||||
|
||||
const resolveAccountEnabled = (
|
||||
plugin: ChannelPlugin,
|
||||
account: unknown,
|
||||
cfg: OpenClawConfig,
|
||||
): boolean => {
|
||||
if (plugin.config.isEnabled) {
|
||||
return plugin.config.isEnabled(account, cfg);
|
||||
}
|
||||
if (!account || typeof account !== "object") {
|
||||
return true;
|
||||
}
|
||||
const enabled = (account as { enabled?: boolean }).enabled;
|
||||
return enabled !== false;
|
||||
};
|
||||
|
||||
const resolveAccountConfigured = async (
|
||||
plugin: ChannelPlugin,
|
||||
account: unknown,
|
||||
cfg: OpenClawConfig,
|
||||
): Promise<boolean> => {
|
||||
if (plugin.config.isConfigured) {
|
||||
return await plugin.config.isConfigured(account, cfg);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const buildAccountDetails = (params: {
|
||||
entry: ChannelAccountEntry;
|
||||
plugin: ChannelPlugin;
|
||||
@@ -133,8 +109,12 @@ export async function buildChannelSummary(
|
||||
|
||||
for (const accountId of resolvedAccountIds) {
|
||||
const account = plugin.config.resolveAccount(effective, accountId);
|
||||
const enabled = resolveAccountEnabled(plugin, account, effective);
|
||||
const configured = await resolveAccountConfigured(plugin, account, effective);
|
||||
const enabled = resolveChannelAccountEnabled({ plugin, account, cfg: effective });
|
||||
const configured = await resolveChannelAccountConfigured({
|
||||
plugin,
|
||||
account,
|
||||
cfg: effective,
|
||||
});
|
||||
const snapshot = buildChannelAccountSnapshot({
|
||||
plugin,
|
||||
account,
|
||||
|
||||
@@ -14,6 +14,43 @@ export function extractErrorCode(err: unknown): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function readErrorName(err: unknown): string {
|
||||
if (!err || typeof err !== "object") {
|
||||
return "";
|
||||
}
|
||||
const name = (err as { name?: unknown }).name;
|
||||
return typeof name === "string" ? name : "";
|
||||
}
|
||||
|
||||
export function collectErrorGraphCandidates(
|
||||
err: unknown,
|
||||
resolveNested?: (current: Record<string, unknown>) => Iterable<unknown>,
|
||||
): unknown[] {
|
||||
const queue: unknown[] = [err];
|
||||
const seen = new Set<unknown>();
|
||||
const candidates: unknown[] = [];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift();
|
||||
if (current == null || seen.has(current)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(current);
|
||||
candidates.push(current);
|
||||
|
||||
if (!current || typeof current !== "object" || !resolveNested) {
|
||||
continue;
|
||||
}
|
||||
for (const nested of resolveNested(current as Record<string, unknown>)) {
|
||||
if (nested != null && !seen.has(nested)) {
|
||||
queue.push(nested);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard for NodeJS.ErrnoException (any error with a `code` property).
|
||||
*/
|
||||
|
||||
@@ -18,6 +18,49 @@ describe("resolveAllowAlwaysPatterns", () => {
|
||||
return exe;
|
||||
}
|
||||
|
||||
function expectAllowAlwaysBypassBlocked(params: {
|
||||
dir: string;
|
||||
firstCommand: string;
|
||||
secondCommand: string;
|
||||
env: Record<string, string | undefined>;
|
||||
persistedPattern: string;
|
||||
}) {
|
||||
const safeBins = resolveSafeBins(undefined);
|
||||
const first = evaluateShellAllowlist({
|
||||
command: params.firstCommand,
|
||||
allowlist: [],
|
||||
safeBins,
|
||||
cwd: params.dir,
|
||||
env: params.env,
|
||||
platform: process.platform,
|
||||
});
|
||||
const persisted = resolveAllowAlwaysPatterns({
|
||||
segments: first.segments,
|
||||
cwd: params.dir,
|
||||
env: params.env,
|
||||
platform: process.platform,
|
||||
});
|
||||
expect(persisted).toEqual([params.persistedPattern]);
|
||||
|
||||
const second = evaluateShellAllowlist({
|
||||
command: params.secondCommand,
|
||||
allowlist: [{ pattern: params.persistedPattern }],
|
||||
safeBins,
|
||||
cwd: params.dir,
|
||||
env: params.env,
|
||||
platform: process.platform,
|
||||
});
|
||||
expect(second.allowlistSatisfied).toBe(false);
|
||||
expect(
|
||||
requiresExecApproval({
|
||||
ask: "on-miss",
|
||||
security: "allowlist",
|
||||
analysisOk: second.analysisOk,
|
||||
allowlistSatisfied: second.allowlistSatisfied,
|
||||
}),
|
||||
).toBe(true);
|
||||
}
|
||||
|
||||
it("returns direct executable paths for non-shell segments", () => {
|
||||
const exe = path.join("/tmp", "openclaw-tool");
|
||||
const patterns = resolveAllowAlwaysPatterns({
|
||||
@@ -233,42 +276,14 @@ describe("resolveAllowAlwaysPatterns", () => {
|
||||
const busybox = makeExecutable(dir, "busybox");
|
||||
const echo = makeExecutable(dir, "echo");
|
||||
makeExecutable(dir, "id");
|
||||
const safeBins = resolveSafeBins(undefined);
|
||||
const env = { PATH: `${dir}${path.delimiter}${process.env.PATH ?? ""}` };
|
||||
|
||||
const first = evaluateShellAllowlist({
|
||||
command: `${busybox} sh -c 'echo warmup-ok'`,
|
||||
allowlist: [],
|
||||
safeBins,
|
||||
cwd: dir,
|
||||
expectAllowAlwaysBypassBlocked({
|
||||
dir,
|
||||
firstCommand: `${busybox} sh -c 'echo warmup-ok'`,
|
||||
secondCommand: `${busybox} sh -c 'id > marker'`,
|
||||
env,
|
||||
platform: process.platform,
|
||||
persistedPattern: echo,
|
||||
});
|
||||
const persisted = resolveAllowAlwaysPatterns({
|
||||
segments: first.segments,
|
||||
cwd: dir,
|
||||
env,
|
||||
platform: process.platform,
|
||||
});
|
||||
expect(persisted).toEqual([echo]);
|
||||
|
||||
const second = evaluateShellAllowlist({
|
||||
command: `${busybox} sh -c 'id > marker'`,
|
||||
allowlist: [{ pattern: echo }],
|
||||
safeBins,
|
||||
cwd: dir,
|
||||
env,
|
||||
platform: process.platform,
|
||||
});
|
||||
expect(second.allowlistSatisfied).toBe(false);
|
||||
expect(
|
||||
requiresExecApproval({
|
||||
ask: "on-miss",
|
||||
security: "allowlist",
|
||||
analysisOk: second.analysisOk,
|
||||
allowlistSatisfied: second.allowlistSatisfied,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("prevents allow-always bypass for dispatch-wrapper + shell-wrapper chains", () => {
|
||||
@@ -278,41 +293,13 @@ describe("resolveAllowAlwaysPatterns", () => {
|
||||
const dir = makeTempDir();
|
||||
const echo = makeExecutable(dir, "echo");
|
||||
makeExecutable(dir, "id");
|
||||
const safeBins = resolveSafeBins(undefined);
|
||||
const env = makePathEnv(dir);
|
||||
|
||||
const first = evaluateShellAllowlist({
|
||||
command: "/usr/bin/nice /bin/zsh -lc 'echo warmup-ok'",
|
||||
allowlist: [],
|
||||
safeBins,
|
||||
cwd: dir,
|
||||
expectAllowAlwaysBypassBlocked({
|
||||
dir,
|
||||
firstCommand: "/usr/bin/nice /bin/zsh -lc 'echo warmup-ok'",
|
||||
secondCommand: "/usr/bin/nice /bin/zsh -lc 'id > marker'",
|
||||
env,
|
||||
platform: process.platform,
|
||||
persistedPattern: echo,
|
||||
});
|
||||
const persisted = resolveAllowAlwaysPatterns({
|
||||
segments: first.segments,
|
||||
cwd: dir,
|
||||
env,
|
||||
platform: process.platform,
|
||||
});
|
||||
expect(persisted).toEqual([echo]);
|
||||
|
||||
const second = evaluateShellAllowlist({
|
||||
command: "/usr/bin/nice /bin/zsh -lc 'id > marker'",
|
||||
allowlist: [{ pattern: echo }],
|
||||
safeBins,
|
||||
cwd: dir,
|
||||
env,
|
||||
platform: process.platform,
|
||||
});
|
||||
expect(second.allowlistSatisfied).toBe(false);
|
||||
expect(
|
||||
requiresExecApproval({
|
||||
ask: "on-miss",
|
||||
security: "allowlist",
|
||||
analysisOk: second.analysisOk,
|
||||
allowlistSatisfied: second.allowlistSatisfied,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -616,16 +616,26 @@ export function buildSafeShellCommand(params: { command: string; platform?: stri
|
||||
return { ok: true, rendered: argv.map((token) => shellEscapeSingleArg(token)).join(" ") };
|
||||
},
|
||||
});
|
||||
if (!rebuilt.ok) {
|
||||
return { ok: false, reason: rebuilt.reason };
|
||||
}
|
||||
return { ok: true, command: rebuilt.command };
|
||||
return finalizeRebuiltShellCommand(rebuilt);
|
||||
}
|
||||
|
||||
function renderQuotedArgv(argv: string[]): string {
|
||||
return argv.map((token) => shellEscapeSingleArg(token)).join(" ");
|
||||
}
|
||||
|
||||
function finalizeRebuiltShellCommand(
|
||||
rebuilt: ReturnType<typeof rebuildShellCommandFromSource>,
|
||||
expectedSegmentCount?: number,
|
||||
): { ok: boolean; command?: string; reason?: string } {
|
||||
if (!rebuilt.ok) {
|
||||
return { ok: false, reason: rebuilt.reason };
|
||||
}
|
||||
if (typeof expectedSegmentCount === "number" && rebuilt.segmentCount !== expectedSegmentCount) {
|
||||
return { ok: false, reason: "segment count mismatch" };
|
||||
}
|
||||
return { ok: true, command: rebuilt.command };
|
||||
}
|
||||
|
||||
export function resolvePlannedSegmentArgv(segment: ExecCommandSegment): string[] | null {
|
||||
if (segment.resolution?.policyBlocked === true) {
|
||||
return null;
|
||||
@@ -688,13 +698,7 @@ export function buildSafeBinsShellCommand(params: {
|
||||
return { ok: true, rendered };
|
||||
},
|
||||
});
|
||||
if (!rebuilt.ok) {
|
||||
return { ok: false, reason: rebuilt.reason };
|
||||
}
|
||||
if (rebuilt.segmentCount !== params.segments.length) {
|
||||
return { ok: false, reason: "segment count mismatch" };
|
||||
}
|
||||
return { ok: true, command: rebuilt.command };
|
||||
return finalizeRebuiltShellCommand(rebuilt, params.segments.length);
|
||||
}
|
||||
|
||||
export function buildEnforcedShellCommand(params: {
|
||||
@@ -717,13 +721,7 @@ export function buildEnforcedShellCommand(params: {
|
||||
return { ok: true, rendered: renderQuotedArgv(argv) };
|
||||
},
|
||||
});
|
||||
if (!rebuilt.ok) {
|
||||
return { ok: false, reason: rebuilt.reason };
|
||||
}
|
||||
if (rebuilt.segmentCount !== params.segments.length) {
|
||||
return { ok: false, reason: "segment count mismatch" };
|
||||
}
|
||||
return { ok: true, command: rebuilt.command };
|
||||
return finalizeRebuiltShellCommand(rebuilt, params.segments.length);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -625,6 +625,36 @@ describe("exec approvals shell allowlist (chained commands)", () => {
|
||||
});
|
||||
|
||||
describe("exec approvals allowlist evaluation", () => {
|
||||
function evaluateAutoAllowSkills(params: {
|
||||
analysis: {
|
||||
ok: boolean;
|
||||
segments: Array<{
|
||||
raw: string;
|
||||
argv: string[];
|
||||
resolution: {
|
||||
rawExecutable: string;
|
||||
executableName: string;
|
||||
resolvedPath?: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
resolvedPath: string;
|
||||
}) {
|
||||
return evaluateExecAllowlist({
|
||||
analysis: params.analysis,
|
||||
allowlist: [],
|
||||
safeBins: new Set(),
|
||||
skillBins: [{ name: "skill-bin", resolvedPath: params.resolvedPath }],
|
||||
autoAllowSkills: true,
|
||||
cwd: "/tmp",
|
||||
});
|
||||
}
|
||||
|
||||
function expectAutoAllowSkillsMiss(result: ReturnType<typeof evaluateExecAllowlist>): void {
|
||||
expect(result.allowlistSatisfied).toBe(false);
|
||||
expect(result.segmentSatisfiedBy).toEqual([null]);
|
||||
}
|
||||
|
||||
it("satisfies allowlist on exact match", () => {
|
||||
const analysis = {
|
||||
ok: true,
|
||||
@@ -696,13 +726,9 @@ describe("exec approvals allowlist evaluation", () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = evaluateExecAllowlist({
|
||||
const result = evaluateAutoAllowSkills({
|
||||
analysis,
|
||||
allowlist: [],
|
||||
safeBins: new Set(),
|
||||
skillBins: [{ name: "skill-bin", resolvedPath: "/opt/skills/skill-bin" }],
|
||||
autoAllowSkills: true,
|
||||
cwd: "/tmp",
|
||||
resolvedPath: "/opt/skills/skill-bin",
|
||||
});
|
||||
expect(result.allowlistSatisfied).toBe(true);
|
||||
});
|
||||
@@ -722,16 +748,11 @@ describe("exec approvals allowlist evaluation", () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = evaluateExecAllowlist({
|
||||
const result = evaluateAutoAllowSkills({
|
||||
analysis,
|
||||
allowlist: [],
|
||||
safeBins: new Set(),
|
||||
skillBins: [{ name: "skill-bin", resolvedPath: "/tmp/skill-bin" }],
|
||||
autoAllowSkills: true,
|
||||
cwd: "/tmp",
|
||||
resolvedPath: "/tmp/skill-bin",
|
||||
});
|
||||
expect(result.allowlistSatisfied).toBe(false);
|
||||
expect(result.segmentSatisfiedBy).toEqual([null]);
|
||||
expectAutoAllowSkillsMiss(result);
|
||||
});
|
||||
|
||||
it("does not satisfy auto-allow skills when command resolution is missing", () => {
|
||||
@@ -748,16 +769,11 @@ describe("exec approvals allowlist evaluation", () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = evaluateExecAllowlist({
|
||||
const result = evaluateAutoAllowSkills({
|
||||
analysis,
|
||||
allowlist: [],
|
||||
safeBins: new Set(),
|
||||
skillBins: [{ name: "skill-bin", resolvedPath: "/opt/skills/skill-bin" }],
|
||||
autoAllowSkills: true,
|
||||
cwd: "/tmp",
|
||||
resolvedPath: "/opt/skills/skill-bin",
|
||||
});
|
||||
expect(result.allowlistSatisfied).toBe(false);
|
||||
expect(result.segmentSatisfiedBy).toEqual([null]);
|
||||
expectAutoAllowSkillsMiss(result);
|
||||
});
|
||||
|
||||
it("returns empty segment details for chain misses", () => {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import path from "node:path";
|
||||
import {
|
||||
POSIX_INLINE_COMMAND_FLAGS,
|
||||
POWERSHELL_INLINE_COMMAND_FLAGS,
|
||||
resolveInlineCommandMatch,
|
||||
} from "./shell-inline-command.js";
|
||||
|
||||
export const MAX_DISPATCH_WRAPPER_DEPTH = 4;
|
||||
|
||||
@@ -51,9 +56,6 @@ const SHELL_WRAPPER_CANONICAL = new Set<string>([
|
||||
...POWERSHELL_WRAPPER_NAMES,
|
||||
]);
|
||||
|
||||
const POSIX_INLINE_COMMAND_FLAGS = new Set(["-lc", "-c", "--command"]);
|
||||
const POWERSHELL_INLINE_COMMAND_FLAGS = new Set(["-c", "-command", "--command"]);
|
||||
|
||||
const ENV_OPTIONS_WITH_VALUE = new Set([
|
||||
"-u",
|
||||
"--unset",
|
||||
@@ -586,30 +588,7 @@ function extractInlineCommandByFlags(
|
||||
flags: ReadonlySet<string>,
|
||||
options: { allowCombinedC?: boolean } = {},
|
||||
): string | null {
|
||||
for (let i = 1; i < argv.length; i += 1) {
|
||||
const token = argv[i]?.trim();
|
||||
if (!token) {
|
||||
continue;
|
||||
}
|
||||
const lower = token.toLowerCase();
|
||||
if (lower === "--") {
|
||||
break;
|
||||
}
|
||||
if (flags.has(lower)) {
|
||||
const cmd = argv[i + 1]?.trim();
|
||||
return cmd ? cmd : null;
|
||||
}
|
||||
if (options.allowCombinedC && /^-[^-]*c[^-]*$/i.test(token)) {
|
||||
const commandIndex = lower.indexOf("c");
|
||||
const inline = token.slice(commandIndex + 1).trim();
|
||||
if (inline) {
|
||||
return inline;
|
||||
}
|
||||
const cmd = argv[i + 1]?.trim();
|
||||
return cmd ? cmd : null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return resolveInlineCommandMatch(argv, flags, options).command;
|
||||
}
|
||||
|
||||
function extractShellWrapperPayload(argv: string[], spec: ShellWrapperSpec): string | null {
|
||||
|
||||
38
src/infra/install-from-npm-spec.ts
Normal file
38
src/infra/install-from-npm-spec.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { NpmIntegrityDriftPayload } from "./npm-integrity.js";
|
||||
import {
|
||||
finalizeNpmSpecArchiveInstall,
|
||||
installFromNpmSpecArchiveWithInstaller,
|
||||
type NpmSpecArchiveFinalInstallResult,
|
||||
} from "./npm-pack-install.js";
|
||||
import { validateRegistryNpmSpec } from "./npm-registry-spec.js";
|
||||
|
||||
export async function installFromValidatedNpmSpecArchive<
|
||||
TResult extends { ok: boolean },
|
||||
TArchiveInstallParams extends { archivePath: string },
|
||||
>(params: {
|
||||
spec: string;
|
||||
timeoutMs: number;
|
||||
tempDirPrefix: string;
|
||||
expectedIntegrity?: string;
|
||||
onIntegrityDrift?: (payload: NpmIntegrityDriftPayload) => boolean | Promise<boolean>;
|
||||
warn?: (message: string) => void;
|
||||
installFromArchive: (params: TArchiveInstallParams) => Promise<TResult>;
|
||||
archiveInstallParams: Omit<TArchiveInstallParams, "archivePath">;
|
||||
}): Promise<NpmSpecArchiveFinalInstallResult<TResult>> {
|
||||
const spec = params.spec.trim();
|
||||
const specError = validateRegistryNpmSpec(spec);
|
||||
if (specError) {
|
||||
return { ok: false, error: specError };
|
||||
}
|
||||
const flowResult = await installFromNpmSpecArchiveWithInstaller({
|
||||
tempDirPrefix: params.tempDirPrefix,
|
||||
spec,
|
||||
timeoutMs: params.timeoutMs,
|
||||
expectedIntegrity: params.expectedIntegrity,
|
||||
onIntegrityDrift: params.onIntegrityDrift,
|
||||
warn: params.warn,
|
||||
installFromArchive: params.installFromArchive,
|
||||
archiveInstallParams: params.archiveInstallParams,
|
||||
});
|
||||
return finalizeNpmSpecArchiveInstall(flowResult);
|
||||
}
|
||||
@@ -147,3 +147,20 @@ export async function installPackageDir(params: {
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export async function installPackageDirWithManifestDeps(params: {
|
||||
sourceDir: string;
|
||||
targetDir: string;
|
||||
mode: "install" | "update";
|
||||
timeoutMs: number;
|
||||
logger?: { info?: (message: string) => void };
|
||||
copyErrorPrefix: string;
|
||||
depsLogMessage: string;
|
||||
manifestDependencies?: Record<string, unknown>;
|
||||
afterCopy?: () => void | Promise<void>;
|
||||
}): Promise<{ ok: true } | { ok: false; error: string }> {
|
||||
return installPackageDir({
|
||||
...params,
|
||||
hasDeps: Object.keys(params.manifestDependencies ?? {}).length > 0,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -56,6 +56,31 @@ async function runPack(spec: string, cwd: string, timeoutMs = 1000) {
|
||||
});
|
||||
}
|
||||
|
||||
async function expectPackFallsBackToDetectedArchive(params: { stdout: string }) {
|
||||
const cwd = await createTempDir("openclaw-install-source-utils-");
|
||||
const archivePath = path.join(cwd, "openclaw-plugin-1.2.3.tgz");
|
||||
await fs.writeFile(archivePath, "", "utf-8");
|
||||
runCommandWithTimeoutMock.mockResolvedValue({
|
||||
stdout: params.stdout,
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
});
|
||||
|
||||
const result = await packNpmSpecToArchive({
|
||||
spec: "openclaw-plugin@1.2.3",
|
||||
timeoutMs: 5000,
|
||||
cwd,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
archivePath,
|
||||
metadata: {},
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
runCommandWithTimeoutMock.mockClear();
|
||||
});
|
||||
@@ -195,53 +220,11 @@ describe("packNpmSpecToArchive", () => {
|
||||
});
|
||||
|
||||
it("falls back to archive detected in cwd when npm pack stdout is empty", async () => {
|
||||
const cwd = await createTempDir("openclaw-install-source-utils-");
|
||||
const archivePath = path.join(cwd, "openclaw-plugin-1.2.3.tgz");
|
||||
await fs.writeFile(archivePath, "", "utf-8");
|
||||
runCommandWithTimeoutMock.mockResolvedValue({
|
||||
stdout: " \n\n",
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
});
|
||||
|
||||
const result = await packNpmSpecToArchive({
|
||||
spec: "openclaw-plugin@1.2.3",
|
||||
timeoutMs: 5000,
|
||||
cwd,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
archivePath,
|
||||
metadata: {},
|
||||
});
|
||||
await expectPackFallsBackToDetectedArchive({ stdout: " \n\n" });
|
||||
});
|
||||
|
||||
it("falls back to archive detected in cwd when stdout does not contain a tgz", async () => {
|
||||
const cwd = await createTempDir("openclaw-install-source-utils-");
|
||||
const archivePath = path.join(cwd, "openclaw-plugin-1.2.3.tgz");
|
||||
await fs.writeFile(archivePath, "", "utf-8");
|
||||
runCommandWithTimeoutMock.mockResolvedValue({
|
||||
stdout: "npm pack completed successfully\n",
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
});
|
||||
|
||||
const result = await packNpmSpecToArchive({
|
||||
spec: "openclaw-plugin@1.2.3",
|
||||
timeoutMs: 5000,
|
||||
cwd,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
archivePath,
|
||||
metadata: {},
|
||||
});
|
||||
await expectPackFallsBackToDetectedArchive({ stdout: "npm pack completed successfully\n" });
|
||||
});
|
||||
|
||||
it("returns friendly error for 404 (package not on npm)", async () => {
|
||||
|
||||
@@ -14,6 +14,26 @@ export type NpmSpecResolution = {
|
||||
resolvedAt?: string;
|
||||
};
|
||||
|
||||
export type NpmResolutionFields = {
|
||||
resolvedName?: string;
|
||||
resolvedVersion?: string;
|
||||
resolvedSpec?: string;
|
||||
integrity?: string;
|
||||
shasum?: string;
|
||||
resolvedAt?: string;
|
||||
};
|
||||
|
||||
export function buildNpmResolutionFields(resolution?: NpmSpecResolution): NpmResolutionFields {
|
||||
return {
|
||||
resolvedName: resolution?.name,
|
||||
resolvedVersion: resolution?.version,
|
||||
resolvedSpec: resolution?.resolvedSpec,
|
||||
integrity: resolution?.integrity,
|
||||
shasum: resolution?.shasum,
|
||||
resolvedAt: resolution?.resolvedAt,
|
||||
};
|
||||
}
|
||||
|
||||
export type NpmIntegrityDrift = {
|
||||
expectedIntegrity: string;
|
||||
actualIntegrity: string;
|
||||
|
||||
@@ -155,20 +155,24 @@ describe("sendPoll channel normalization", () => {
|
||||
});
|
||||
});
|
||||
|
||||
const setMattermostGatewayRegistry = () => {
|
||||
setRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
pluginId: "mattermost",
|
||||
source: "test",
|
||||
plugin: {
|
||||
...createMattermostLikePlugin({ onSendText: () => {} }),
|
||||
outbound: { deliveryMode: "gateway" },
|
||||
},
|
||||
},
|
||||
]),
|
||||
);
|
||||
};
|
||||
|
||||
describe("gateway url override hardening", () => {
|
||||
it("drops gateway url overrides in backend mode (SSRF hardening)", async () => {
|
||||
setRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
pluginId: "mattermost",
|
||||
source: "test",
|
||||
plugin: {
|
||||
...createMattermostLikePlugin({ onSendText: () => {} }),
|
||||
outbound: { deliveryMode: "gateway" },
|
||||
},
|
||||
},
|
||||
]),
|
||||
);
|
||||
setMattermostGatewayRegistry();
|
||||
|
||||
callGatewayMock.mockResolvedValueOnce({ messageId: "m1" });
|
||||
await sendMessage({
|
||||
@@ -196,18 +200,7 @@ describe("gateway url override hardening", () => {
|
||||
});
|
||||
|
||||
it("forwards explicit agentId in gateway send params", async () => {
|
||||
setRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
pluginId: "mattermost",
|
||||
source: "test",
|
||||
plugin: {
|
||||
...createMattermostLikePlugin({ onSendText: () => {} }),
|
||||
outbound: { deliveryMode: "gateway" },
|
||||
},
|
||||
},
|
||||
]),
|
||||
);
|
||||
setMattermostGatewayRegistry();
|
||||
|
||||
callGatewayMock.mockResolvedValueOnce({ messageId: "m-agent" });
|
||||
await sendMessage({
|
||||
|
||||
@@ -301,43 +301,44 @@ describe("resolveSessionDeliveryTarget", () => {
|
||||
expect(resolved.to).toBe("63448508");
|
||||
});
|
||||
|
||||
it("allows heartbeat delivery to Slack DMs and avoids inherited threadId by default", () => {
|
||||
const cfg: OpenClawConfig = {};
|
||||
const resolved = resolveHeartbeatDeliveryTarget({
|
||||
cfg,
|
||||
entry: {
|
||||
sessionId: "sess-heartbeat-outbound",
|
||||
updatedAt: 1,
|
||||
lastChannel: "slack",
|
||||
lastTo: "user:U123",
|
||||
lastThreadId: "1739142736.000100",
|
||||
},
|
||||
const resolveHeartbeatTarget = (
|
||||
entry: Parameters<typeof resolveHeartbeatDeliveryTarget>[0]["entry"],
|
||||
directPolicy?: "allow" | "block",
|
||||
) =>
|
||||
resolveHeartbeatDeliveryTarget({
|
||||
cfg: {},
|
||||
entry,
|
||||
heartbeat: {
|
||||
target: "last",
|
||||
...(directPolicy ? { directPolicy } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
it("allows heartbeat delivery to Slack DMs and avoids inherited threadId by default", () => {
|
||||
const resolved = resolveHeartbeatTarget({
|
||||
sessionId: "sess-heartbeat-outbound",
|
||||
updatedAt: 1,
|
||||
lastChannel: "slack",
|
||||
lastTo: "user:U123",
|
||||
lastThreadId: "1739142736.000100",
|
||||
});
|
||||
|
||||
expect(resolved.channel).toBe("slack");
|
||||
expect(resolved.to).toBe("user:U123");
|
||||
expect(resolved.threadId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("blocks heartbeat delivery to Slack DMs when directPolicy is block", () => {
|
||||
const cfg: OpenClawConfig = {};
|
||||
const resolved = resolveHeartbeatDeliveryTarget({
|
||||
cfg,
|
||||
entry: {
|
||||
const resolved = resolveHeartbeatTarget(
|
||||
{
|
||||
sessionId: "sess-heartbeat-outbound",
|
||||
updatedAt: 1,
|
||||
lastChannel: "slack",
|
||||
lastTo: "user:U123",
|
||||
lastThreadId: "1739142736.000100",
|
||||
},
|
||||
heartbeat: {
|
||||
target: "last",
|
||||
directPolicy: "block",
|
||||
},
|
||||
});
|
||||
"block",
|
||||
);
|
||||
|
||||
expect(resolved.channel).toBe("none");
|
||||
expect(resolved.reason).toBe("dm-blocked");
|
||||
@@ -460,19 +461,12 @@ describe("resolveSessionDeliveryTarget", () => {
|
||||
});
|
||||
|
||||
it("uses session chatType hint when target parser cannot classify and allows direct by default", () => {
|
||||
const cfg: OpenClawConfig = {};
|
||||
const resolved = resolveHeartbeatDeliveryTarget({
|
||||
cfg,
|
||||
entry: {
|
||||
sessionId: "sess-heartbeat-imessage-direct",
|
||||
updatedAt: 1,
|
||||
lastChannel: "imessage",
|
||||
lastTo: "chat-guid-unknown-shape",
|
||||
chatType: "direct",
|
||||
},
|
||||
heartbeat: {
|
||||
target: "last",
|
||||
},
|
||||
const resolved = resolveHeartbeatTarget({
|
||||
sessionId: "sess-heartbeat-imessage-direct",
|
||||
updatedAt: 1,
|
||||
lastChannel: "imessage",
|
||||
lastTo: "chat-guid-unknown-shape",
|
||||
chatType: "direct",
|
||||
});
|
||||
|
||||
expect(resolved.channel).toBe("imessage");
|
||||
@@ -480,21 +474,16 @@ describe("resolveSessionDeliveryTarget", () => {
|
||||
});
|
||||
|
||||
it("blocks session chatType direct hints when directPolicy is block", () => {
|
||||
const cfg: OpenClawConfig = {};
|
||||
const resolved = resolveHeartbeatDeliveryTarget({
|
||||
cfg,
|
||||
entry: {
|
||||
const resolved = resolveHeartbeatTarget(
|
||||
{
|
||||
sessionId: "sess-heartbeat-imessage-direct",
|
||||
updatedAt: 1,
|
||||
lastChannel: "imessage",
|
||||
lastTo: "chat-guid-unknown-shape",
|
||||
chatType: "direct",
|
||||
},
|
||||
heartbeat: {
|
||||
target: "last",
|
||||
directPolicy: "block",
|
||||
},
|
||||
});
|
||||
"block",
|
||||
);
|
||||
|
||||
expect(resolved.channel).toBe("none");
|
||||
expect(resolved.reason).toBe("dm-blocked");
|
||||
|
||||
18
src/infra/package-tag.ts
Normal file
18
src/infra/package-tag.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export function normalizePackageTagInput(
|
||||
value: string | undefined | null,
|
||||
packageNames: readonly string[],
|
||||
): string | null {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const packageName of packageNames) {
|
||||
const prefix = `${packageName}@`;
|
||||
if (trimmed.startsWith(prefix)) {
|
||||
return trimmed.slice(prefix.length);
|
||||
}
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
35
src/infra/shell-inline-command.ts
Normal file
35
src/infra/shell-inline-command.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
export const POSIX_INLINE_COMMAND_FLAGS = new Set(["-lc", "-c", "--command"]);
|
||||
export const POWERSHELL_INLINE_COMMAND_FLAGS = new Set(["-c", "-command", "--command"]);
|
||||
|
||||
export function resolveInlineCommandMatch(
|
||||
argv: string[],
|
||||
flags: ReadonlySet<string>,
|
||||
options: { allowCombinedC?: boolean } = {},
|
||||
): { command: string | null; valueTokenIndex: number | null } {
|
||||
for (let i = 1; i < argv.length; i += 1) {
|
||||
const token = argv[i]?.trim();
|
||||
if (!token) {
|
||||
continue;
|
||||
}
|
||||
const lower = token.toLowerCase();
|
||||
if (lower === "--") {
|
||||
break;
|
||||
}
|
||||
if (flags.has(lower)) {
|
||||
const valueTokenIndex = i + 1 < argv.length ? i + 1 : null;
|
||||
const command = argv[i + 1]?.trim();
|
||||
return { command: command ? command : null, valueTokenIndex };
|
||||
}
|
||||
if (options.allowCombinedC && /^-[^-]*c[^-]*$/i.test(token)) {
|
||||
const commandIndex = lower.indexOf("c");
|
||||
const inline = token.slice(commandIndex + 1).trim();
|
||||
if (inline) {
|
||||
return { command: inline, valueTokenIndex: i };
|
||||
}
|
||||
const valueTokenIndex = i + 1 < argv.length ? i + 1 : null;
|
||||
const command = argv[i + 1]?.trim();
|
||||
return { command: command ? command : null, valueTokenIndex };
|
||||
}
|
||||
}
|
||||
return { command: null, valueTokenIndex: null };
|
||||
}
|
||||
@@ -5,6 +5,11 @@ import {
|
||||
unwrapDispatchWrappersForResolution,
|
||||
unwrapKnownShellMultiplexerInvocation,
|
||||
} from "./exec-wrapper-resolution.js";
|
||||
import {
|
||||
POSIX_INLINE_COMMAND_FLAGS,
|
||||
POWERSHELL_INLINE_COMMAND_FLAGS,
|
||||
resolveInlineCommandMatch,
|
||||
} from "./shell-inline-command.js";
|
||||
|
||||
export type SystemRunCommandValidation =
|
||||
| {
|
||||
@@ -63,41 +68,12 @@ const POSIX_OR_POWERSHELL_INLINE_WRAPPER_NAMES = new Set([
|
||||
"zsh",
|
||||
]);
|
||||
|
||||
const POSIX_INLINE_COMMAND_FLAGS = new Set(["-lc", "-c", "--command"]);
|
||||
const POWERSHELL_INLINE_COMMAND_FLAGS = new Set(["-c", "-command", "--command"]);
|
||||
|
||||
function unwrapShellWrapperArgv(argv: string[]): string[] {
|
||||
const dispatchUnwrapped = unwrapDispatchWrappersForResolution(argv);
|
||||
const shellMultiplexer = unwrapKnownShellMultiplexerInvocation(dispatchUnwrapped);
|
||||
return shellMultiplexer.kind === "unwrapped" ? shellMultiplexer.argv : dispatchUnwrapped;
|
||||
}
|
||||
|
||||
function resolveInlineCommandTokenIndex(
|
||||
argv: string[],
|
||||
flags: ReadonlySet<string>,
|
||||
options: { allowCombinedC?: boolean } = {},
|
||||
): number | null {
|
||||
for (let i = 1; i < argv.length; i += 1) {
|
||||
const token = argv[i]?.trim();
|
||||
if (!token) {
|
||||
continue;
|
||||
}
|
||||
const lower = token.toLowerCase();
|
||||
if (lower === "--") {
|
||||
break;
|
||||
}
|
||||
if (flags.has(lower)) {
|
||||
return i + 1 < argv.length ? i + 1 : null;
|
||||
}
|
||||
if (options.allowCombinedC && /^-[^-]*c[^-]*$/i.test(token)) {
|
||||
const commandIndex = lower.indexOf("c");
|
||||
const inline = token.slice(commandIndex + 1).trim();
|
||||
return inline ? i : i + 1 < argv.length ? i + 1 : null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function hasTrailingPositionalArgvAfterInlineCommand(argv: string[]): boolean {
|
||||
const wrapperArgv = unwrapShellWrapperArgv(argv);
|
||||
const token0 = wrapperArgv[0]?.trim();
|
||||
@@ -112,10 +88,10 @@ function hasTrailingPositionalArgvAfterInlineCommand(argv: string[]): boolean {
|
||||
|
||||
const inlineCommandIndex =
|
||||
wrapper === "powershell" || wrapper === "pwsh"
|
||||
? resolveInlineCommandTokenIndex(wrapperArgv, POWERSHELL_INLINE_COMMAND_FLAGS)
|
||||
: resolveInlineCommandTokenIndex(wrapperArgv, POSIX_INLINE_COMMAND_FLAGS, {
|
||||
? resolveInlineCommandMatch(wrapperArgv, POWERSHELL_INLINE_COMMAND_FLAGS).valueTokenIndex
|
||||
: resolveInlineCommandMatch(wrapperArgv, POSIX_INLINE_COMMAND_FLAGS, {
|
||||
allowCombinedC: true,
|
||||
});
|
||||
}).valueTokenIndex;
|
||||
if (inlineCommandIndex === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import process from "node:process";
|
||||
import { extractErrorCode, formatUncaughtError } from "./errors.js";
|
||||
import {
|
||||
collectErrorGraphCandidates,
|
||||
extractErrorCode,
|
||||
formatUncaughtError,
|
||||
readErrorName,
|
||||
} from "./errors.js";
|
||||
|
||||
type UnhandledRejectionHandler = (reason: unknown) => boolean;
|
||||
|
||||
@@ -62,14 +67,6 @@ function getErrorCause(err: unknown): unknown {
|
||||
return (err as { cause?: unknown }).cause;
|
||||
}
|
||||
|
||||
function getErrorName(err: unknown): string {
|
||||
if (!err || typeof err !== "object") {
|
||||
return "";
|
||||
}
|
||||
const name = (err as { name?: unknown }).name;
|
||||
return typeof name === "string" ? name : "";
|
||||
}
|
||||
|
||||
function extractErrorCodeOrErrno(err: unknown): string | undefined {
|
||||
const code = extractErrorCode(err);
|
||||
if (code) {
|
||||
@@ -96,44 +93,6 @@ function extractErrorCodeWithCause(err: unknown): string | undefined {
|
||||
return extractErrorCode(getErrorCause(err));
|
||||
}
|
||||
|
||||
function collectErrorCandidates(err: unknown): unknown[] {
|
||||
const queue: unknown[] = [err];
|
||||
const seen = new Set<unknown>();
|
||||
const candidates: unknown[] = [];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift();
|
||||
if (current == null || seen.has(current)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(current);
|
||||
candidates.push(current);
|
||||
|
||||
if (!current || typeof current !== "object") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const maybeNested: Array<unknown> = [
|
||||
(current as { cause?: unknown }).cause,
|
||||
(current as { reason?: unknown }).reason,
|
||||
(current as { original?: unknown }).original,
|
||||
(current as { error?: unknown }).error,
|
||||
(current as { data?: unknown }).data,
|
||||
];
|
||||
const errors = (current as { errors?: unknown }).errors;
|
||||
if (Array.isArray(errors)) {
|
||||
maybeNested.push(...errors);
|
||||
}
|
||||
for (const nested of maybeNested) {
|
||||
if (nested != null && !seen.has(nested)) {
|
||||
queue.push(nested);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an error is an AbortError.
|
||||
* These are typically intentional cancellations (e.g., during shutdown) and shouldn't crash.
|
||||
@@ -172,13 +131,25 @@ export function isTransientNetworkError(err: unknown): boolean {
|
||||
if (!err) {
|
||||
return false;
|
||||
}
|
||||
for (const candidate of collectErrorCandidates(err)) {
|
||||
for (const candidate of collectErrorGraphCandidates(err, (current) => {
|
||||
const nested: Array<unknown> = [
|
||||
current.cause,
|
||||
current.reason,
|
||||
current.original,
|
||||
current.error,
|
||||
current.data,
|
||||
];
|
||||
if (Array.isArray(current.errors)) {
|
||||
nested.push(...current.errors);
|
||||
}
|
||||
return nested;
|
||||
})) {
|
||||
const code = extractErrorCodeOrErrno(candidate);
|
||||
if (code && TRANSIENT_NETWORK_CODES.has(code)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const name = getErrorName(candidate);
|
||||
const name = readErrorName(candidate);
|
||||
if (name && TRANSIENT_NETWORK_ERROR_NAMES.has(name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "./control-ui-assets.js";
|
||||
import { detectPackageManager as detectPackageManagerImpl } from "./detect-package-manager.js";
|
||||
import { readPackageName, readPackageVersion } from "./package-json.js";
|
||||
import { normalizePackageTagInput } from "./package-tag.js";
|
||||
import { trimLogTail } from "./restart-sentinel.js";
|
||||
import {
|
||||
channelToNpmTag,
|
||||
@@ -312,17 +313,7 @@ function managerInstallArgs(manager: "pnpm" | "bun" | "npm") {
|
||||
}
|
||||
|
||||
function normalizeTag(tag?: string) {
|
||||
const trimmed = tag?.trim();
|
||||
if (!trimmed) {
|
||||
return "latest";
|
||||
}
|
||||
if (trimmed.startsWith("openclaw@")) {
|
||||
return trimmed.slice("openclaw@".length);
|
||||
}
|
||||
if (trimmed.startsWith(`${DEFAULT_PACKAGE_NAME}@`)) {
|
||||
return trimmed.slice(`${DEFAULT_PACKAGE_NAME}@`.length);
|
||||
}
|
||||
return trimmed;
|
||||
return normalizePackageTagInput(tag, ["openclaw", DEFAULT_PACKAGE_NAME]) ?? "latest";
|
||||
}
|
||||
|
||||
export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<UpdateRunResult> {
|
||||
|
||||
@@ -147,6 +147,32 @@ describe("update-startup", () => {
|
||||
});
|
||||
}
|
||||
|
||||
function createBetaAutoUpdateConfig(params?: { checkOnStart?: boolean }) {
|
||||
return {
|
||||
update: {
|
||||
...(params?.checkOnStart === false ? { checkOnStart: false } : {}),
|
||||
channel: "beta" as const,
|
||||
auto: {
|
||||
enabled: true,
|
||||
betaCheckIntervalHours: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function runAutoUpdateCheckWithDefaults(params: {
|
||||
cfg: { update?: Record<string, unknown> };
|
||||
runAutoUpdate?: ReturnType<typeof createAutoUpdateSuccessMock>;
|
||||
}) {
|
||||
await runGatewayUpdateCheck({
|
||||
cfg: params.cfg,
|
||||
log: { info: vi.fn() },
|
||||
isNixMode: false,
|
||||
allowInTests: true,
|
||||
...(params.runAutoUpdate ? { runAutoUpdate: params.runAutoUpdate } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "stable channel",
|
||||
@@ -310,19 +336,8 @@ describe("update-startup", () => {
|
||||
mockPackageUpdateStatus("beta", "2.0.0-beta.1");
|
||||
const runAutoUpdate = createAutoUpdateSuccessMock();
|
||||
|
||||
await runGatewayUpdateCheck({
|
||||
cfg: {
|
||||
update: {
|
||||
channel: "beta",
|
||||
auto: {
|
||||
enabled: true,
|
||||
betaCheckIntervalHours: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
log: { info: vi.fn() },
|
||||
isNixMode: false,
|
||||
allowInTests: true,
|
||||
await runAutoUpdateCheckWithDefaults({
|
||||
cfg: createBetaAutoUpdateConfig(),
|
||||
runAutoUpdate,
|
||||
});
|
||||
|
||||
@@ -338,20 +353,8 @@ describe("update-startup", () => {
|
||||
mockPackageUpdateStatus("beta", "2.0.0-beta.1");
|
||||
const runAutoUpdate = createAutoUpdateSuccessMock();
|
||||
|
||||
await runGatewayUpdateCheck({
|
||||
cfg: {
|
||||
update: {
|
||||
checkOnStart: false,
|
||||
channel: "beta",
|
||||
auto: {
|
||||
enabled: true,
|
||||
betaCheckIntervalHours: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
log: { info: vi.fn() },
|
||||
isNixMode: false,
|
||||
allowInTests: true,
|
||||
await runAutoUpdateCheckWithDefaults({
|
||||
cfg: createBetaAutoUpdateConfig({ checkOnStart: false }),
|
||||
runAutoUpdate,
|
||||
});
|
||||
|
||||
@@ -381,19 +384,8 @@ describe("update-startup", () => {
|
||||
const originalArgv = process.argv.slice();
|
||||
process.argv = [process.execPath, "/opt/openclaw/dist/entry.js"];
|
||||
try {
|
||||
await runGatewayUpdateCheck({
|
||||
cfg: {
|
||||
update: {
|
||||
channel: "beta",
|
||||
auto: {
|
||||
enabled: true,
|
||||
betaCheckIntervalHours: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
log: { info: vi.fn() },
|
||||
isNixMode: false,
|
||||
allowInTests: true,
|
||||
await runAutoUpdateCheckWithDefaults({
|
||||
cfg: createBetaAutoUpdateConfig(),
|
||||
});
|
||||
} finally {
|
||||
process.argv = originalArgv;
|
||||
|
||||
Reference in New Issue
Block a user