mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-27 14:34:19 +00:00
perf(gateway): cache stable plugin index fingerprints
This commit is contained in:
@@ -10,7 +10,6 @@ import {
|
||||
execServerUrlFromClient,
|
||||
openSocket,
|
||||
rpc,
|
||||
shellQuote,
|
||||
waitForHttpBodyDeltas,
|
||||
} from "./sandbox-exec-server.test-helpers.js";
|
||||
|
||||
@@ -19,6 +18,12 @@ afterEach(async () => {
|
||||
await closeCodexSandboxExecServersForTests();
|
||||
});
|
||||
|
||||
function testExecEnv(): NodeJS.ProcessEnv {
|
||||
return {
|
||||
PATH: process.env.PATH,
|
||||
};
|
||||
}
|
||||
|
||||
describe("OpenClaw Codex sandbox exec-server HTTP", () => {
|
||||
it("routes HTTP requests through the sandbox backend", async () => {
|
||||
const runShellCommand = vi.fn(async () => ({
|
||||
@@ -84,13 +89,13 @@ describe("OpenClaw Codex sandbox exec-server HTTP", () => {
|
||||
});
|
||||
const buildExecSpec = vi.fn(async () => ({
|
||||
argv: [
|
||||
"/bin/sh",
|
||||
"-lc",
|
||||
process.execPath,
|
||||
"-e",
|
||||
[headerLine, bodyLine, doneLine]
|
||||
.map((line) => `printf '%s\\n' ${shellQuote(line)}`)
|
||||
.join("; "),
|
||||
.map((line) => `process.stdout.write(${JSON.stringify(`${line}\n`)});`)
|
||||
.join(""),
|
||||
],
|
||||
env: process.env,
|
||||
env: testExecEnv(),
|
||||
stdinMode: "pipe-closed" as const,
|
||||
}));
|
||||
const runShellCommand = vi.fn(async () => ({
|
||||
@@ -167,7 +172,7 @@ describe("OpenClaw Codex sandbox exec-server HTTP", () => {
|
||||
"setInterval(() => {}, 1000);",
|
||||
].join(""),
|
||||
],
|
||||
env: process.env,
|
||||
env: testExecEnv(),
|
||||
finalizeToken: "stream-token",
|
||||
stdinMode: "pipe-closed",
|
||||
}),
|
||||
|
||||
@@ -40,8 +40,8 @@ export function createSandboxContext(overrides: {
|
||||
buildExecSpec:
|
||||
overrides.buildExecSpec ??
|
||||
(async () => ({
|
||||
argv: ["/bin/sh", "-lc", "true"],
|
||||
env: process.env,
|
||||
argv: [process.execPath, "-e", ""],
|
||||
env: { PATH: process.env.PATH },
|
||||
stdinMode: "pipe-closed",
|
||||
})),
|
||||
finalizeExec: overrides.finalizeExec,
|
||||
|
||||
@@ -20,6 +20,26 @@ afterEach(async () => {
|
||||
await closeCodexSandboxExecServersForTests();
|
||||
});
|
||||
|
||||
function testExecEnv(): NodeJS.ProcessEnv {
|
||||
return {
|
||||
PATH: process.env.PATH,
|
||||
};
|
||||
}
|
||||
|
||||
function echoFirstInputLineScript(prefix: string): string {
|
||||
return [
|
||||
"let data = '';",
|
||||
"process.stdin.setEncoding('utf8');",
|
||||
"process.stdin.on('data', (chunk) => {",
|
||||
"data += chunk;",
|
||||
"if (data.includes('\\n')) {",
|
||||
`process.stdout.write(${JSON.stringify(prefix)} + data);`,
|
||||
"process.exit(0);",
|
||||
"}",
|
||||
"});",
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
describe("OpenClaw Codex sandbox exec-server", () => {
|
||||
it("reports unavailable app-server remote environment support without exposing an environment", async () => {
|
||||
const sandbox = createSandboxContext({});
|
||||
@@ -95,8 +115,8 @@ describe("OpenClaw Codex sandbox exec-server", () => {
|
||||
|
||||
it("registers a sandbox-backed Codex environment and routes process execution through it", async () => {
|
||||
const buildExecSpec = vi.fn(async () => ({
|
||||
argv: ["/bin/sh", "-lc", "printf 'sandbox-process-ok\\n'"],
|
||||
env: process.env,
|
||||
argv: [process.execPath, "-e", "process.stdout.write('sandbox-process-ok\\n')"],
|
||||
env: testExecEnv(),
|
||||
stdinMode: "pipe-closed" as const,
|
||||
}));
|
||||
const sandbox = createSandboxContext({ buildExecSpec });
|
||||
@@ -172,8 +192,8 @@ describe("OpenClaw Codex sandbox exec-server", () => {
|
||||
|
||||
it("rejects unsupported arg0 overrides instead of dropping them", async () => {
|
||||
const buildExecSpec = vi.fn(async () => ({
|
||||
argv: ["/bin/sh", "-lc", "true"],
|
||||
env: process.env,
|
||||
argv: [process.execPath, "-e", ""],
|
||||
env: testExecEnv(),
|
||||
stdinMode: "pipe-closed" as const,
|
||||
}));
|
||||
const sandbox = createSandboxContext({ buildExecSpec });
|
||||
@@ -204,8 +224,8 @@ describe("OpenClaw Codex sandbox exec-server", () => {
|
||||
it("accepts stdin writes for pipe-backed processes", async () => {
|
||||
const sandbox = createSandboxContext({
|
||||
buildExecSpec: async () => ({
|
||||
argv: ["/bin/sh", "-lc", 'read line; printf "echo:%s\\n" "$line"'],
|
||||
env: process.env,
|
||||
argv: [process.execPath, "-e", echoFirstInputLineScript("echo:")],
|
||||
env: testExecEnv(),
|
||||
stdinMode: "pipe-open",
|
||||
}),
|
||||
});
|
||||
@@ -242,8 +262,8 @@ describe("OpenClaw Codex sandbox exec-server", () => {
|
||||
|
||||
it("keeps tty process starts pipe-backed for sandbox backends", async () => {
|
||||
const buildExecSpec = vi.fn(async () => ({
|
||||
argv: ["/bin/sh", "-lc", 'read line; printf "tty:%s\\n" "$line"'],
|
||||
env: process.env,
|
||||
argv: [process.execPath, "-e", echoFirstInputLineScript("tty:")],
|
||||
env: testExecEnv(),
|
||||
stdinMode: "pipe-open" as const,
|
||||
}));
|
||||
const sandbox = createSandboxContext({ buildExecSpec });
|
||||
@@ -289,7 +309,7 @@ describe("OpenClaw Codex sandbox exec-server", () => {
|
||||
vi.stubEnv("OPENCLAW_TEST_DATABASE_PASSWORD", "host-password");
|
||||
vi.stubEnv("OPENCLAW_TEST_PRIVATE_KEY", "host-private-key");
|
||||
const buildExecSpec = vi.fn(async () => ({
|
||||
argv: ["/bin/sh", "-lc", "true"],
|
||||
argv: [process.execPath, "-e", ""],
|
||||
env: {},
|
||||
stdinMode: "pipe-closed" as const,
|
||||
}));
|
||||
@@ -336,7 +356,7 @@ describe("OpenClaw Codex sandbox exec-server", () => {
|
||||
"-e",
|
||||
"process.stdout.write('aaaa'); process.stderr.write('bbbb');",
|
||||
],
|
||||
env: process.env,
|
||||
env: testExecEnv(),
|
||||
stdinMode: "pipe-closed",
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
||||
@@ -239,8 +240,9 @@ const installRecordsCache = new Map<string, InstallRecordsCacheEntry>();
|
||||
|
||||
function readFileSignature(filePath: string): string {
|
||||
try {
|
||||
const stat = fs.statSync(filePath);
|
||||
return `${stat.mtimeMs}:${stat.size}`;
|
||||
const stat = fs.statSync(filePath, { bigint: true });
|
||||
const hash = crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("base64url");
|
||||
return `${stat.mtimeNs}:${stat.ctimeNs}:${stat.size}:${hash}`;
|
||||
} catch {
|
||||
return "missing";
|
||||
}
|
||||
|
||||
@@ -6,7 +6,10 @@ import {
|
||||
writePersistedInstalledPluginIndex,
|
||||
} from "./installed-plugin-index-store.js";
|
||||
import type { InstalledPluginIndex } from "./installed-plugin-index.js";
|
||||
import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js";
|
||||
import {
|
||||
loadPluginManifestRegistryForInstalledIndex,
|
||||
resolveInstalledManifestRegistryIndexFingerprint,
|
||||
} from "./manifest-registry-installed.js";
|
||||
import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
@@ -70,7 +73,106 @@ function createIndex(rootDir: string): InstalledPluginIndex {
|
||||
};
|
||||
}
|
||||
|
||||
function fileSignature(filePath: string) {
|
||||
const stat = fs.statSync(filePath);
|
||||
return {
|
||||
size: stat.size,
|
||||
mtimeMs: stat.mtimeMs,
|
||||
ctimeMs: stat.ctimeMs,
|
||||
};
|
||||
}
|
||||
|
||||
function createIndexWithFileSignatures(rootDir: string): InstalledPluginIndex {
|
||||
const index = createIndex(rootDir);
|
||||
return {
|
||||
...index,
|
||||
plugins: index.plugins.map((record) => ({
|
||||
...record,
|
||||
manifestFile: fileSignature(record.manifestPath),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function deepFreeze<T>(value: T, seen = new WeakSet<object>()): T {
|
||||
if (!value || typeof value !== "object") {
|
||||
return value;
|
||||
}
|
||||
const object = value as object;
|
||||
if (seen.has(object)) {
|
||||
return value;
|
||||
}
|
||||
seen.add(object);
|
||||
for (const child of Object.values(value)) {
|
||||
deepFreeze(child, seen);
|
||||
}
|
||||
return Object.freeze(value);
|
||||
}
|
||||
|
||||
describe("loadPluginManifestRegistryForInstalledIndex", () => {
|
||||
it("reuses frozen installed-index fingerprints when file signatures are persisted", () => {
|
||||
const rootDir = makeTempDir();
|
||||
writePlugin(rootDir, "installed", "installed-");
|
||||
const index = deepFreeze(createIndexWithFileSignatures(rootDir));
|
||||
const first = resolveInstalledManifestRegistryIndexFingerprint(index);
|
||||
const manifestPath = path.join(rootDir, "openclaw.plugin.json");
|
||||
const nextMtime = new Date(Date.now() + 5000);
|
||||
fs.utimesSync(manifestPath, nextMtime, nextMtime);
|
||||
const second = resolveInstalledManifestRegistryIndexFingerprint(index);
|
||||
|
||||
expect(second).toBe(first);
|
||||
});
|
||||
|
||||
it("recomputes installed-index fingerprints for mutable index objects", () => {
|
||||
const rootDir = makeTempDir();
|
||||
writePlugin(rootDir, "installed", "installed-");
|
||||
const index = createIndexWithFileSignatures(rootDir);
|
||||
const first = resolveInstalledManifestRegistryIndexFingerprint(index);
|
||||
const record = index.plugins[0];
|
||||
if (!record) {
|
||||
throw new Error("expected index record");
|
||||
}
|
||||
record.manifestHash = "changed";
|
||||
const second = resolveInstalledManifestRegistryIndexFingerprint(index);
|
||||
|
||||
expect(second).not.toBe(first);
|
||||
});
|
||||
|
||||
it("does not cache shallow-frozen installed-index fingerprints with mutable nested records", () => {
|
||||
const rootDir = makeTempDir();
|
||||
writePlugin(rootDir, "installed", "installed-");
|
||||
const index = createIndexWithFileSignatures(rootDir);
|
||||
const record = index.plugins[0];
|
||||
if (!record) {
|
||||
throw new Error("expected index record");
|
||||
}
|
||||
Object.freeze(index.installRecords);
|
||||
Object.freeze(index.diagnostics);
|
||||
Object.freeze(record);
|
||||
Object.freeze(index.plugins);
|
||||
Object.freeze(index);
|
||||
const first = resolveInstalledManifestRegistryIndexFingerprint(index);
|
||||
|
||||
const agentHarnesses = record.startup.agentHarnesses as string[];
|
||||
agentHarnesses.push("changed");
|
||||
const second = resolveInstalledManifestRegistryIndexFingerprint(index);
|
||||
|
||||
expect(second).not.toBe(first);
|
||||
});
|
||||
|
||||
it("does not cache frozen installed-index fingerprints that depend on live file state", () => {
|
||||
const rootDir = makeTempDir();
|
||||
writePlugin(rootDir, "installed", "installed-");
|
||||
const index = deepFreeze(createIndex(rootDir));
|
||||
const first = resolveInstalledManifestRegistryIndexFingerprint(index);
|
||||
|
||||
const manifestPath = path.join(rootDir, "openclaw.plugin.json");
|
||||
const nextMtime = new Date(Date.now() + 5000);
|
||||
fs.utimesSync(manifestPath, nextMtime, nextMtime);
|
||||
const second = resolveInstalledManifestRegistryIndexFingerprint(index);
|
||||
|
||||
expect(second).not.toBe(first);
|
||||
});
|
||||
|
||||
it("reconstructs installed-index manifest registries when manifest files change", () => {
|
||||
const rootDir = makeTempDir();
|
||||
const manifestPath = path.join(rootDir, "openclaw.plugin.json");
|
||||
|
||||
@@ -7,6 +7,7 @@ import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import { normalizeOptionalTrimmedStringList } from "../shared/string-normalization.js";
|
||||
import type { PluginCandidate } from "./discovery.js";
|
||||
import { hashJson } from "./installed-plugin-index-hash.js";
|
||||
import type { InstalledPluginFileSignature } from "./installed-plugin-index-hash.js";
|
||||
import type { InstalledPluginIndex, InstalledPluginIndexRecord } from "./installed-plugin-index.js";
|
||||
import { extractPluginInstallRecordsFromInstalledPluginIndex } from "./installed-plugin-index.js";
|
||||
import { loadPluginManifestRegistry, type PluginManifestRegistry } from "./manifest-registry.js";
|
||||
@@ -25,6 +26,37 @@ import {
|
||||
type PluginDependencySpecMap,
|
||||
} from "./status-dependencies.js";
|
||||
|
||||
const installedManifestRegistryIndexFingerprintCache = new WeakMap<InstalledPluginIndex, string>();
|
||||
|
||||
function isDeepFrozenJsonLike(value: unknown, seen = new WeakSet<object>()): boolean {
|
||||
if (!value || typeof value !== "object") {
|
||||
return true;
|
||||
}
|
||||
const object = value as object;
|
||||
if (seen.has(object)) {
|
||||
return true;
|
||||
}
|
||||
if (!Object.isFrozen(object)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(object);
|
||||
return Object.values(value).every((entry) => isDeepFrozenJsonLike(entry, seen));
|
||||
}
|
||||
|
||||
function hasPersistedFileSignatures(index: InstalledPluginIndex): boolean {
|
||||
return index.plugins.every(
|
||||
(record) =>
|
||||
record.manifestFile !== undefined &&
|
||||
(record.packageJson === undefined || record.packageJson.fileSignature !== undefined),
|
||||
);
|
||||
}
|
||||
|
||||
function isInstalledManifestRegistryIndexFingerprintCacheable(
|
||||
index: InstalledPluginIndex,
|
||||
): boolean {
|
||||
return hasPersistedFileSignatures(index) && isDeepFrozenJsonLike(index);
|
||||
}
|
||||
|
||||
function isRelativePathInsideOrEqual(relativePath: string): boolean {
|
||||
return (
|
||||
relativePath === "" ||
|
||||
@@ -61,12 +93,19 @@ function safeFileSignature(filePath: string | undefined): string | undefined {
|
||||
}
|
||||
try {
|
||||
const stat = fs.statSync(filePath);
|
||||
return `${filePath}:${stat.size}:${stat.mtimeMs}`;
|
||||
return formatFileSignature(filePath, stat);
|
||||
} catch {
|
||||
return `${filePath}:missing`;
|
||||
}
|
||||
}
|
||||
|
||||
function formatFileSignature(
|
||||
filePath: string,
|
||||
signature: Pick<InstalledPluginFileSignature, "size" | "mtimeMs">,
|
||||
): string {
|
||||
return `${filePath}:${signature.size}:${signature.mtimeMs}`;
|
||||
}
|
||||
|
||||
function buildInstalledManifestRegistryIndexKey(index: InstalledPluginIndex) {
|
||||
const realpathCache = new Map<string, string>();
|
||||
return {
|
||||
@@ -78,9 +117,12 @@ function buildInstalledManifestRegistryIndexKey(index: InstalledPluginIndex) {
|
||||
installRecords: index.installRecords,
|
||||
diagnostics: index.diagnostics,
|
||||
plugins: index.plugins.map((record) => {
|
||||
const packageJsonFile =
|
||||
record.packageJson?.fileSignature ??
|
||||
safeFileSignature(resolvePackageJsonPath(record, realpathCache));
|
||||
const packageJsonPath = resolvePackageJsonPath(record, realpathCache);
|
||||
const packageJsonFile = record.packageJson?.fileSignature
|
||||
? packageJsonPath
|
||||
? formatFileSignature(packageJsonPath, record.packageJson.fileSignature)
|
||||
: undefined
|
||||
: safeFileSignature(packageJsonPath);
|
||||
return {
|
||||
pluginId: record.pluginId,
|
||||
packageName: record.packageName,
|
||||
@@ -91,7 +133,9 @@ function buildInstalledManifestRegistryIndexKey(index: InstalledPluginIndex) {
|
||||
packageChannel: record.packageChannel,
|
||||
manifestPath: record.manifestPath,
|
||||
manifestHash: record.manifestHash,
|
||||
manifestFile: safeFileSignature(record.manifestPath),
|
||||
manifestFile: record.manifestFile
|
||||
? formatFileSignature(record.manifestPath, record.manifestFile)
|
||||
: safeFileSignature(record.manifestPath),
|
||||
format: record.format,
|
||||
bundleFormat: record.bundleFormat,
|
||||
source: record.source,
|
||||
@@ -116,7 +160,15 @@ function buildInstalledManifestRegistryIndexKey(index: InstalledPluginIndex) {
|
||||
export function resolveInstalledManifestRegistryIndexFingerprint(
|
||||
index: InstalledPluginIndex,
|
||||
): string {
|
||||
return hashJson(buildInstalledManifestRegistryIndexKey(index));
|
||||
const cached = installedManifestRegistryIndexFingerprintCache.get(index);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const fingerprint = hashJson(buildInstalledManifestRegistryIndexKey(index));
|
||||
if (isInstalledManifestRegistryIndexFingerprintCacheable(index)) {
|
||||
installedManifestRegistryIndexFingerprintCache.set(index, fingerprint);
|
||||
}
|
||||
return fingerprint;
|
||||
}
|
||||
|
||||
function resolveInstalledPluginRootDir(record: InstalledPluginIndexRecord): string {
|
||||
|
||||
@@ -81,6 +81,53 @@ function writeNodeShim(binDir: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
function writeBashExecutable(filePath: string, lines: string[]): void {
|
||||
fs.writeFileSync(filePath, ["#!/bin/bash", "set -euo pipefail", ...lines, ""].join("\n"));
|
||||
fs.chmodSync(filePath, 0o755);
|
||||
}
|
||||
|
||||
function writeFakeTimeout(filePath: string, supportsKillAfter: boolean): void {
|
||||
writeBashExecutable(filePath, [
|
||||
'if [ "${1:-}" = "--kill-after=1s" ]; then',
|
||||
` exit ${supportsKillAfter ? 0 : 1}`,
|
||||
"fi",
|
||||
'printf "%s\\n" "$*" >"$OPENCLAW_TEST_TIMEOUT_ARGS"',
|
||||
'while [ "$#" -gt 0 ]; do',
|
||||
' case "$1" in',
|
||||
" --)",
|
||||
" shift",
|
||||
" break",
|
||||
" ;;",
|
||||
" -k|--kill-after)",
|
||||
" shift 2",
|
||||
" ;;",
|
||||
" --kill-after=*|-*)",
|
||||
" shift",
|
||||
" ;;",
|
||||
" *)",
|
||||
" shift",
|
||||
" break",
|
||||
" ;;",
|
||||
" esac",
|
||||
"done",
|
||||
'exec "$@"',
|
||||
]);
|
||||
}
|
||||
|
||||
function writeFakeNpm(filePath: string): void {
|
||||
writeBashExecutable(filePath, ['printf "%s\\n" "$*" >"$OPENCLAW_TEST_NPM_ARGS"']);
|
||||
}
|
||||
|
||||
function expectNpmInstallObserved(argsPath: string, expectedArgs: string, prefix: string): void {
|
||||
if (fs.existsSync(argsPath)) {
|
||||
expect(fs.readFileSync(argsPath, "utf8").trim()).toBe(expectedArgs);
|
||||
return;
|
||||
}
|
||||
expect(
|
||||
fs.existsSync(path.join(prefix, "lib/node_modules/openclaw-e2e-fixture/package.json")),
|
||||
).toBe(true);
|
||||
}
|
||||
|
||||
describe("scripts/lib/openclaw-e2e-instance.sh", () => {
|
||||
it("sources decoded test-state scripts", () => {
|
||||
const result = runHelper(base64('export OPENCLAW_E2E_INSTANCE_TEST="ok"\n'));
|
||||
@@ -112,29 +159,10 @@ describe("scripts/lib/openclaw-e2e-instance.sh", () => {
|
||||
const npmArgsPath = path.join(tempDir, "npm-args.txt");
|
||||
const logPath = path.join(tempDir, "install.log");
|
||||
const packagePath = path.join(tempDir, "openclaw.tgz");
|
||||
const prefixPath = path.join(tempDir, "prefix");
|
||||
writePackageFixture(packagePath);
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, "timeout"),
|
||||
[
|
||||
"#!/bin/sh",
|
||||
"set -eu",
|
||||
'if [ "${1:-}" = "--kill-after=1s" ]; then',
|
||||
" exit 0",
|
||||
"fi",
|
||||
'printf "%s\\n" "$*" >"$OPENCLAW_TEST_TIMEOUT_ARGS"',
|
||||
'while [ "$#" -gt 0 ] && [ "$1" != "npm" ]; do shift; done',
|
||||
'[ "$#" -gt 0 ] || exit 127',
|
||||
"shift",
|
||||
'exec "$OPENCLAW_TEST_NPM_BIN" "$@"',
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, "npm"),
|
||||
["#!/bin/sh", "set -eu", 'printf "%s\\n" "$*" >"$OPENCLAW_TEST_NPM_ARGS"', ""].join("\n"),
|
||||
);
|
||||
fs.chmodSync(path.join(tempDir, "timeout"), 0o755);
|
||||
fs.chmodSync(path.join(tempDir, "npm"), 0o755);
|
||||
writeFakeTimeout(path.join(tempDir, "timeout"), true);
|
||||
writeFakeNpm(path.join(tempDir, "npm"));
|
||||
|
||||
const result = spawnSync(
|
||||
"/bin/bash",
|
||||
@@ -143,7 +171,7 @@ describe("scripts/lib/openclaw-e2e-instance.sh", () => {
|
||||
[
|
||||
"set -euo pipefail",
|
||||
`source ${shellQuote(helperPath)}`,
|
||||
`openclaw_e2e_install_package ${shellQuote(logPath)} ${shellQuote("fixture package")}`,
|
||||
`openclaw_e2e_install_package ${shellQuote(logPath)} ${shellQuote("fixture package")} ${shellQuote(prefixPath)}`,
|
||||
].join("; "),
|
||||
],
|
||||
{
|
||||
@@ -162,10 +190,12 @@ describe("scripts/lib/openclaw-e2e-instance.sh", () => {
|
||||
expectShellSuccess(result);
|
||||
expect(result.stdout).toContain("Installing fixture package...");
|
||||
expect(fs.readFileSync(timeoutArgsPath, "utf8").trim()).toBe(
|
||||
`--kill-after=30s 42s npm install -g ${packagePath} --no-fund --no-audit`,
|
||||
`--kill-after=30s 42s npm install -g --prefix ${prefixPath} ${packagePath} --no-fund --no-audit`,
|
||||
);
|
||||
expect(fs.readFileSync(npmArgsPath, "utf8").trim()).toBe(
|
||||
`install -g ${packagePath} --no-fund --no-audit`,
|
||||
expectNpmInstallObserved(
|
||||
npmArgsPath,
|
||||
`install -g --prefix ${prefixPath} ${packagePath} --no-fund --no-audit`,
|
||||
prefixPath,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { force: true, recursive: true });
|
||||
@@ -179,29 +209,10 @@ describe("scripts/lib/openclaw-e2e-instance.sh", () => {
|
||||
const npmArgsPath = path.join(tempDir, "npm-args.txt");
|
||||
const logPath = path.join(tempDir, "install.log");
|
||||
const packagePath = path.join(tempDir, "openclaw.tgz");
|
||||
const prefixPath = path.join(tempDir, "prefix");
|
||||
writePackageFixture(packagePath);
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, "timeout"),
|
||||
[
|
||||
"#!/bin/sh",
|
||||
"set -eu",
|
||||
'if [ "${1:-}" = "--kill-after=1s" ]; then',
|
||||
" exit 1",
|
||||
"fi",
|
||||
'printf "%s\\n" "$*" >"$OPENCLAW_TEST_TIMEOUT_ARGS"',
|
||||
'while [ "$#" -gt 0 ] && [ "$1" != "npm" ]; do shift; done',
|
||||
'[ "$#" -gt 0 ] || exit 127',
|
||||
"shift",
|
||||
'exec "$OPENCLAW_TEST_NPM_BIN" "$@"',
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, "npm"),
|
||||
["#!/bin/sh", "set -eu", 'printf "%s\\n" "$*" >"$OPENCLAW_TEST_NPM_ARGS"', ""].join("\n"),
|
||||
);
|
||||
fs.chmodSync(path.join(tempDir, "timeout"), 0o755);
|
||||
fs.chmodSync(path.join(tempDir, "npm"), 0o755);
|
||||
writeFakeTimeout(path.join(tempDir, "timeout"), false);
|
||||
writeFakeNpm(path.join(tempDir, "npm"));
|
||||
|
||||
const result = spawnSync(
|
||||
"/bin/bash",
|
||||
@@ -210,7 +221,7 @@ describe("scripts/lib/openclaw-e2e-instance.sh", () => {
|
||||
[
|
||||
"set -euo pipefail",
|
||||
`source ${shellQuote(helperPath)}`,
|
||||
`openclaw_e2e_install_package ${shellQuote(logPath)} ${shellQuote("fixture package")}`,
|
||||
`openclaw_e2e_install_package ${shellQuote(logPath)} ${shellQuote("fixture package")} ${shellQuote(prefixPath)}`,
|
||||
].join("; "),
|
||||
],
|
||||
{
|
||||
@@ -228,10 +239,12 @@ describe("scripts/lib/openclaw-e2e-instance.sh", () => {
|
||||
|
||||
expectShellSuccess(result);
|
||||
expect(fs.readFileSync(timeoutArgsPath, "utf8").trim()).toBe(
|
||||
`42s npm install -g ${packagePath} --no-fund --no-audit`,
|
||||
`42s npm install -g --prefix ${prefixPath} ${packagePath} --no-fund --no-audit`,
|
||||
);
|
||||
expect(fs.readFileSync(npmArgsPath, "utf8").trim()).toBe(
|
||||
`install -g ${packagePath} --no-fund --no-audit`,
|
||||
expectNpmInstallObserved(
|
||||
npmArgsPath,
|
||||
`install -g --prefix ${prefixPath} ${packagePath} --no-fund --no-audit`,
|
||||
prefixPath,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { force: true, recursive: true });
|
||||
@@ -245,29 +258,10 @@ describe("scripts/lib/openclaw-e2e-instance.sh", () => {
|
||||
const npmArgsPath = path.join(tempDir, "npm-args.txt");
|
||||
const logPath = path.join(tempDir, "install.log");
|
||||
const packagePath = path.join(tempDir, "openclaw.tgz");
|
||||
const prefixPath = path.join(tempDir, "prefix");
|
||||
writePackageFixture(packagePath);
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, "gtimeout"),
|
||||
[
|
||||
"#!/bin/bash",
|
||||
"set -euo pipefail",
|
||||
'if [ "${1:-}" = "--kill-after=1s" ]; then',
|
||||
" exit 0",
|
||||
"fi",
|
||||
'printf "%s\\n" "$*" >"$OPENCLAW_TEST_TIMEOUT_ARGS"',
|
||||
'while [ "$#" -gt 0 ] && [ "$1" != "npm" ]; do shift; done',
|
||||
'[ "$#" -gt 0 ] || exit 127',
|
||||
"shift",
|
||||
'exec "$OPENCLAW_TEST_NPM_BIN" "$@"',
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, "npm"),
|
||||
["#!/bin/sh", "set -eu", 'printf "%s\\n" "$*" >"$OPENCLAW_TEST_NPM_ARGS"', ""].join("\n"),
|
||||
);
|
||||
fs.chmodSync(path.join(tempDir, "gtimeout"), 0o755);
|
||||
fs.chmodSync(path.join(tempDir, "npm"), 0o755);
|
||||
writeFakeTimeout(path.join(tempDir, "gtimeout"), true);
|
||||
writeFakeNpm(path.join(tempDir, "npm"));
|
||||
|
||||
const result = spawnSync(
|
||||
"/bin/bash",
|
||||
@@ -276,7 +270,7 @@ describe("scripts/lib/openclaw-e2e-instance.sh", () => {
|
||||
[
|
||||
"set -euo pipefail",
|
||||
`source ${shellQuote(helperPath)}`,
|
||||
`openclaw_e2e_install_package ${shellQuote(logPath)} ${shellQuote("fixture package")}`,
|
||||
`openclaw_e2e_install_package ${shellQuote(logPath)} ${shellQuote("fixture package")} ${shellQuote(prefixPath)}`,
|
||||
].join("; "),
|
||||
],
|
||||
{
|
||||
@@ -294,10 +288,12 @@ describe("scripts/lib/openclaw-e2e-instance.sh", () => {
|
||||
|
||||
expectShellSuccess(result);
|
||||
expect(fs.readFileSync(timeoutArgsPath, "utf8").trim()).toBe(
|
||||
`--kill-after=30s 42s npm install -g ${packagePath} --no-fund --no-audit`,
|
||||
`--kill-after=30s 42s npm install -g --prefix ${prefixPath} ${packagePath} --no-fund --no-audit`,
|
||||
);
|
||||
expect(fs.readFileSync(npmArgsPath, "utf8").trim()).toBe(
|
||||
`install -g ${packagePath} --no-fund --no-audit`,
|
||||
expectNpmInstallObserved(
|
||||
npmArgsPath,
|
||||
`install -g --prefix ${prefixPath} ${packagePath} --no-fund --no-audit`,
|
||||
prefixPath,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { force: true, recursive: true });
|
||||
@@ -310,13 +306,10 @@ describe("scripts/lib/openclaw-e2e-instance.sh", () => {
|
||||
const npmArgsPath = path.join(tempDir, "npm-args.txt");
|
||||
const logPath = path.join(tempDir, "install.log");
|
||||
const packagePath = path.join(tempDir, "openclaw.tgz");
|
||||
const prefixPath = path.join(tempDir, "prefix");
|
||||
writePackageFixture(packagePath);
|
||||
writeNodeShim(tempDir);
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, "npm"),
|
||||
["#!/bin/sh", "set -eu", 'printf "%s\\n" "$*" >"$OPENCLAW_TEST_NPM_ARGS"', ""].join("\n"),
|
||||
);
|
||||
fs.chmodSync(path.join(tempDir, "npm"), 0o755);
|
||||
writeFakeNpm(path.join(tempDir, "npm"));
|
||||
|
||||
const result = spawnSync(
|
||||
"/bin/bash",
|
||||
@@ -325,7 +318,7 @@ describe("scripts/lib/openclaw-e2e-instance.sh", () => {
|
||||
[
|
||||
"set -euo pipefail",
|
||||
`source ${shellQuote(helperPath)}`,
|
||||
`openclaw_e2e_install_package ${shellQuote(logPath)} ${shellQuote("fixture package")}`,
|
||||
`openclaw_e2e_install_package ${shellQuote(logPath)} ${shellQuote("fixture package")} ${shellQuote(prefixPath)}`,
|
||||
].join("; "),
|
||||
],
|
||||
{
|
||||
@@ -341,8 +334,10 @@ describe("scripts/lib/openclaw-e2e-instance.sh", () => {
|
||||
|
||||
expectShellSuccess(result);
|
||||
expect(fs.readFileSync(logPath, "utf8")).toContain("using Node watchdog");
|
||||
expect(fs.readFileSync(npmArgsPath, "utf8").trim()).toBe(
|
||||
`install -g ${packagePath} --no-fund --no-audit`,
|
||||
expectNpmInstallObserved(
|
||||
npmArgsPath,
|
||||
`install -g --prefix ${prefixPath} ${packagePath} --no-fund --no-audit`,
|
||||
prefixPath,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { force: true, recursive: true });
|
||||
|
||||
@@ -35,14 +35,13 @@ async function createApp(
|
||||
queue: ExecApprovalRequest[] = [createExecApproval()],
|
||||
) {
|
||||
const { OpenClawApp } = await import("./app.ts");
|
||||
const app = new OpenClawApp();
|
||||
Object.defineProperty(app, "client", {
|
||||
value: { request },
|
||||
writable: true,
|
||||
const app = Object.create(OpenClawApp.prototype) as InstanceType<typeof OpenClawApp>;
|
||||
Object.defineProperties(app, {
|
||||
client: { value: { request }, writable: true },
|
||||
execApprovalBusy: { value: false, writable: true },
|
||||
execApprovalError: { value: null, writable: true },
|
||||
execApprovalQueue: { value: queue, writable: true },
|
||||
});
|
||||
app.execApprovalQueue = queue;
|
||||
app.execApprovalBusy = false;
|
||||
app.execApprovalError = null;
|
||||
return app;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user