mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 17:53:39 +00:00
* fix(qa): bound Mantis lane commands * fix(qa): use inactivity watchdog for Mantis commands * fix(qa): use stage deadlines for Mantis commands * fix(qa): propagate Mantis command cancellation * fix(qa): stabilize Mantis abort harness * fix(mantis): preserve abort termination * fix(qa): clean temporary Mantis worktrees * test(qa): cover Mantis cleanup failures * fix(qa): preserve Mantis cleanup failures * test(qa): stabilize Mantis process readiness * test(qa): harden Mantis process tree fixtures * fix(qa): normalize Mantis scenario deadline * test(qa): use supported process output capture * test(qa): split Mantis process runtime coverage * test(qa): clear Mantis process wait timers * refactor(qa): split Mantis command runtime * fix(qa): keep Mantis command stage internal * fix(qa): protect Mantis worktree cleanup ownership * refactor(qa): split Mantis evidence artifact handling * fix(qa): remove quarantined Mantis worktrees * fix(qa): support legacy Git in Mantis cleanup * fix(qa): satisfy Mantis runtime checks * test(ci): shrink Mantis assertion baseline * fix(qa): abort Mantis runs on CLI signals * fix(qa): retain Mantis signal ownership during cleanup * test(qa): exercise repeated Mantis interrupts * test(qa-lab): pass signal to interrupt listener * fix(qa): preserve Mantis cleanup after interruption * fix(qa): settle Mantis cleanup before artifact reads * fix(qa): preserve Mantis evidence across failed reruns * fix(qa): publish Mantis comparison as one generation * fix(qa): bind Mantis publication to owned directories * fix(qa): keep Mantis cleanup Git-owned * fix(qa): bind Mantis cleanup to owned worktrees * fix(qa): narrow Mantis worktree recovery * fix(qa): keep Mantis recovery state atomic * fix(qa): preserve Mantis failure diagnostics * fix(qa): satisfy Mantis failure diagnostics gates * docs(qa): clarify Mantis output container boundary * fix(qa): remove stale Mantis ownership helper * fix(qa): remove stale legacy worktree fixture * fix(qa): preserve stable Mantis artifact paths * fix(qa): make Mantis publication lock waits cancelable * fix(qa): preserve fs-safe lock retry policy * fix(qa): keep Mantis artifacts at documented paths * fix(qa): keep failed Mantis reruns internally consistent * fix(qa): scope shutdown grace to Mantis runs * fix(qa): retain Mantis backups after restore failure * fix: restore process and lock safety contracts * fix: preserve signal exit type through launcher * fix(test): keep launcher fixtures outside production roots Move the extracted launcher fixture to test/scripts and preserve its original imports and lifetime. This fixes the production TypeScript and channel import boundaries without exclusions or compiler weakening. Core production and infra test types, 13 focused launcher cases, focused lint, line ratchets and independent P0-P2 review passed. * test: align release inventory with bundled publication deferral Retain the exact CI-tested main parent and its policy excluding logbook, memory-wiki, and onepassword from publication until release. Correct the stale exact-inventory expectations to 95 npm and 91 ClawHub packages. The full package-set assertions and publication policy remain unchanged. The previously failing focused inventory test, format/lint, assertion ratchet and independent P0-P2 correction review passed. Co-authored-by: xingzhou <275915537+zhangguiping-xydt@users.noreply.github.com> --------- Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com> Co-authored-by: zhangguiping-xydt <275915537+zhangguiping-xydt@users.noreply.github.com> Co-authored-by: xingzhou <275915537+zhangguiping-xydt@users.noreply.github.com>
473 lines
17 KiB
TypeScript
473 lines
17 KiB
TypeScript
// Shared filesystem and process fixtures for the node launcher boundary.
|
|
// Tests node process runner lifecycle and captured output.
|
|
import { execFileSync, spawnSync as realSpawnSync } from "node:child_process";
|
|
import { EventEmitter } from "node:events";
|
|
import fsSync from "node:fs";
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import {
|
|
bundledDistPluginFile,
|
|
bundledPluginFile,
|
|
bundledPluginRoot,
|
|
} from "openclaw/plugin-sdk/test-fixtures";
|
|
import { expect, it as baseIt } from "vitest";
|
|
import { copyBundledPluginMetadata } from "../../scripts/copy-bundled-plugin-metadata.mts";
|
|
import {
|
|
BUILD_STAMP_FILE,
|
|
RUNTIME_POSTBUILD_STAMP_FILE,
|
|
} from "../../scripts/lib/local-build-metadata-paths.mts";
|
|
import {
|
|
writeBuildStamp,
|
|
writeRuntimePostBuildStamp,
|
|
} from "../../scripts/lib/local-build-metadata.mts";
|
|
import {
|
|
UPDATE_COMPATIBILITY_INVENTORY_FILE,
|
|
writeUpdateCompatibilityChunks,
|
|
} from "../../scripts/lib/update-compat-chunks.mts";
|
|
import { runNodeMain } from "../../scripts/run-node.mts";
|
|
import { withTestDir } from "../../src/test-helpers/temp-dir.js";
|
|
import {
|
|
previousReleaseInventory,
|
|
writeUpdateCompatibilityBuildFixture,
|
|
} from "./update-compat-chunks.test-support.js";
|
|
export const it = baseIt.extend<{ tmp: string }>({
|
|
tmp: async ({ task: _task }, use) => {
|
|
await withTestDir({ prefix: "openclaw-run-node-" }, use);
|
|
},
|
|
});
|
|
|
|
export const ROOT_SRC = "src/index.ts";
|
|
export const ROOT_TSCONFIG = "tsconfig.json";
|
|
export const ROOT_PACKAGE = "package.json";
|
|
export const ROOT_TSDOWN = "tsdown.config.ts";
|
|
export const RUNTIME_POSTBUILD_IMPLEMENTATION_PATHS = [
|
|
"scripts/check-built-plugin-control-plane-modules.mts",
|
|
"scripts/copy-bundled-plugin-metadata.mts",
|
|
"scripts/copy-hook-metadata.ts",
|
|
"scripts/runtime-postbuild.mts",
|
|
"scripts/stage-bundled-plugin-runtime.mts",
|
|
"scripts/write-build-info.ts",
|
|
"scripts/write-official-channel-catalog.mts",
|
|
] as const;
|
|
const DEPLOYMENT_MANIFEST = "deployment.json";
|
|
export const GENERATED_PLUGIN_ASSET_BUNDLE = "extensions/demo/src/host/assets/view.bundle.js";
|
|
export const GENERATED_PLUGIN_ASSET_BUNDLE_HASH = "extensions/demo/src/host/assets/.bundle.hash";
|
|
export const DIST_ENTRY = "dist/entry.js";
|
|
export const BUILD_STAMP = `dist/${BUILD_STAMP_FILE}`;
|
|
export const RUNTIME_POSTBUILD_STAMP = `dist/${RUNTIME_POSTBUILD_STAMP_FILE}`;
|
|
export const DIST_PLUGIN_SDK_CORE = "dist/plugin-sdk/core.js";
|
|
export const DIST_CHANNEL_CATALOG = "dist/channel-catalog.json";
|
|
export const DIST_BUILD_INFO = "dist/build-info.json";
|
|
export const DIST_LEGACY_UPDATE_NODE_RUNNER_COMPAT = "dist/shared-Y6bNiw2w.js";
|
|
export const DIST_LEGACY_UPDATE_NODE_RUNNER_COMPAT_ALT = "dist/shared-DTaQo6Hi.js";
|
|
export const DIST_LEGACY_UPDATE_NODE_RUNNER_COMPAT_0229A108 = "dist/shared-1Uyqkfns.js";
|
|
export const DIST_LEGACY_UPDATE_NODE_RUNNER_COMPAT_2026_9_1 = "dist/shared-DFJEouXv.js";
|
|
export const DIST_LEGACY_CLI_EXIT_COMPAT = "dist/memory-state-CcqRgDZU.js";
|
|
export const DIST_LEGACY_CLI_EXIT_COMPAT_ALT = "dist/memory-state-DwGdReW4.js";
|
|
export const DIST_STABLE_ROOT_RUNTIME_SOURCE = "dist/model-catalog.runtime-AbCd1234.js";
|
|
export const DIST_STABLE_ROOT_RUNTIME_SOURCE_ALT = "dist/model-catalog.runtime-EfGh5678.js";
|
|
export const DIST_STABLE_ROOT_RUNTIME_ALIAS = "dist/model-catalog.runtime.js";
|
|
export const DIST_LEGACY_ROOT_RUNTIME_TARGET = "dist/abort.runtime.js";
|
|
export const DIST_LEGACY_ROOT_RUNTIME_COMPAT = "dist/abort.runtime-DX6vo4yJ.js";
|
|
export const QA_LAB_PLUGIN_SDK_ENTRY = "dist/plugin-sdk/qa-lab.js";
|
|
export const QA_RUNTIME_PLUGIN_SDK_ENTRY = "dist/plugin-sdk/qa-runtime.js";
|
|
export const EXTENSION_INDEX = bundledPluginFile("demo", "index.ts");
|
|
export const EXTENSION_SRC = bundledPluginFile("demo", "src/index.ts");
|
|
export const EXTENSION_EXTRA_SRC = bundledPluginFile("demo", "src/extra.ts");
|
|
export const EXTENSION_SKILL = bundledPluginFile("demo", "skills/SKILL.md");
|
|
export const EXTENSION_MANIFEST = bundledPluginFile("demo", "openclaw.plugin.json");
|
|
export const EXTENSION_PACKAGE = bundledPluginFile("demo", "package.json");
|
|
export const EXTENSION_README = bundledPluginFile("demo", "README.md");
|
|
export const DIST_EXTENSION_INDEX = bundledDistPluginFile("demo", "index.js");
|
|
export const DIST_EXTENSION_SRC = bundledDistPluginFile("demo", "src/index.js");
|
|
export const DIST_EXTENSION_SKILL = bundledDistPluginFile("demo", "skills/SKILL.md");
|
|
export const DIST_EXTENSION_RUNTIME_SRC = "dist-runtime/extensions/demo/src/index.js";
|
|
export const DIST_RUNTIME_EXTENSION_INDEX = "dist-runtime/extensions/demo/index.js";
|
|
export const DIST_RUNTIME_EXTENSION_MANIFEST = "dist-runtime/extensions/demo/openclaw.plugin.json";
|
|
export const DIST_RUNTIME_EXTENSION_PACKAGE = "dist-runtime/extensions/demo/package.json";
|
|
export const DIST_RUNTIME_EXTENSION_SKILL = "dist-runtime/extensions/demo/skills/SKILL.md";
|
|
export const DIST_OPENCLAW_ALIAS_PACKAGE = "dist/extensions/node_modules/openclaw/package.json";
|
|
export const DIST_OPENCLAW_ALIAS_PLUGIN_SDK_CORE =
|
|
"dist/extensions/node_modules/openclaw/plugin-sdk/core.js";
|
|
export const DIST_OPENCLAW_ALIAS_PLUGIN_SDK_STRING_COERCE =
|
|
"dist/extensions/node_modules/openclaw/plugin-sdk/string-coerce-runtime.js";
|
|
export const DIFFS_PACKAGE = "extensions/diffs/package.json";
|
|
export const DIFFS_VIEWER_RUNTIME_SOURCE = "extensions/diffs/assets/viewer-runtime.js";
|
|
export const DIST_DIFFS_VIEWER_RUNTIME = "dist/extensions/diffs/assets/viewer-runtime.js";
|
|
export const DIST_RUNTIME_DIFFS_VIEWER_RUNTIME =
|
|
"dist-runtime/extensions/diffs/assets/viewer-runtime.js";
|
|
export const BUNDLED_HOOK_METADATA = "src/hooks/bundled/demo/HOOK.md";
|
|
export const DIST_BUNDLED_HOOK_METADATA = "dist/bundled/demo/HOOK.md";
|
|
export const DIST_EXTENSION_MANIFEST = bundledDistPluginFile("demo", "openclaw.plugin.json");
|
|
export const DIST_EXTENSION_PACKAGE = bundledDistPluginFile("demo", "package.json");
|
|
|
|
const OLD_TIME = new Date("2026-03-13T10:00:00.000Z");
|
|
const BUILD_TIME = new Date("2026-03-13T12:00:00.000Z");
|
|
export const NEW_TIME = new Date("2026-03-13T12:00:01.000Z");
|
|
|
|
const BASE_PROJECT_FILES = {
|
|
[ROOT_TSCONFIG]: "{}\n",
|
|
[ROOT_PACKAGE]: '{"name":"openclaw-test"}\n',
|
|
[DIST_ENTRY]: "console.log('built');\n",
|
|
[BUILD_STAMP]: '{"head":"abc123","inputsClean":true}\n',
|
|
} as const;
|
|
|
|
export function createExitedProcess(code: number | null, signal: string | null = null) {
|
|
return {
|
|
on: (event: string, cb: (code: number | null, signal: string | null) => void) => {
|
|
if (event === "exit") {
|
|
queueMicrotask(() => cb(code, signal));
|
|
}
|
|
return undefined;
|
|
},
|
|
};
|
|
}
|
|
|
|
export function createPipedExitedProcess(params: {
|
|
code?: number | null;
|
|
signal?: string | null;
|
|
stderr?: string;
|
|
stdout?: string;
|
|
}) {
|
|
const stdout = new EventEmitter();
|
|
const stderr = new EventEmitter();
|
|
return {
|
|
stdout,
|
|
stderr,
|
|
on: (event: string, cb: (code: number | null, signal: string | null) => void) => {
|
|
if (event === "exit") {
|
|
queueMicrotask(() => {
|
|
if (params.stdout) {
|
|
stdout.emit("data", Buffer.from(params.stdout));
|
|
}
|
|
if (params.stderr) {
|
|
stderr.emit("data", Buffer.from(params.stderr));
|
|
}
|
|
cb(params.code ?? 0, params.signal ?? null);
|
|
});
|
|
}
|
|
return undefined;
|
|
},
|
|
};
|
|
}
|
|
|
|
export function createFakeProcess() {
|
|
return Object.assign(new EventEmitter(), {
|
|
pid: 4242,
|
|
execPath: process.execPath,
|
|
}) as unknown as NodeJS.Process;
|
|
}
|
|
|
|
// Launcher plumbing tests do not need the real runtime artifact copier.
|
|
export async function skipRuntimePostBuild(): Promise<void> {}
|
|
|
|
export async function syncBundledPluginMetadata(params?: {
|
|
cwd?: string;
|
|
env?: Record<string, string | undefined>;
|
|
}): Promise<void> {
|
|
copyBundledPluginMetadata({ cwd: params?.cwd, env: params?.env });
|
|
}
|
|
|
|
export function firstMockCall<T extends unknown[]>(mock: { mock: { calls: T[] } }): T | undefined {
|
|
return mock.mock.calls[0];
|
|
}
|
|
|
|
export async function writeRuntimePostBuildScaffold(tmp: string): Promise<void> {
|
|
await fs.mkdir(path.join(tmp, "extensions"), { recursive: true });
|
|
await writeProjectFiles(tmp, {
|
|
[DIST_PLUGIN_SDK_CORE]: "export const core = true;\n",
|
|
[DIST_CHANNEL_CATALOG]: '{"entries":[]}\n',
|
|
[DIST_BUILD_INFO]: '{"buildId":"test-build"}\n',
|
|
[DIST_LEGACY_UPDATE_NODE_RUNNER_COMPAT]: "export function resolveNodeRunner() {}\n",
|
|
[DIST_LEGACY_UPDATE_NODE_RUNNER_COMPAT_ALT]: "export function resolveNodeRunner() {}\n",
|
|
[DIST_LEGACY_UPDATE_NODE_RUNNER_COMPAT_0229A108]: "export function resolveNodeRunner() {}\n",
|
|
[DIST_LEGACY_CLI_EXIT_COMPAT]: "export function hasMemoryRuntime() { return false; }\n",
|
|
[DIST_LEGACY_CLI_EXIT_COMPAT_ALT]: "export function hasMemoryRuntime() { return false; }\n",
|
|
[DIST_OPENCLAW_ALIAS_PACKAGE]:
|
|
'{"name":"openclaw","type":"module","exports":{"./plugin-sdk/core":"./plugin-sdk/core.js"}}\n',
|
|
[DIST_OPENCLAW_ALIAS_PLUGIN_SDK_CORE]: "export * from '../../../../plugin-sdk/core.js';\n",
|
|
});
|
|
writeUpdateCompatibilityBuildFixture(tmp);
|
|
writeUpdateCompatibilityChunks({
|
|
distDir: path.join(tmp, "dist"),
|
|
sourceDir: tmp,
|
|
inventory: previousReleaseInventory,
|
|
});
|
|
await touchProjectFiles(
|
|
tmp,
|
|
[
|
|
DIST_CHANNEL_CATALOG,
|
|
DIST_BUILD_INFO,
|
|
DIST_PLUGIN_SDK_CORE,
|
|
DIST_LEGACY_UPDATE_NODE_RUNNER_COMPAT,
|
|
DIST_LEGACY_UPDATE_NODE_RUNNER_COMPAT_ALT,
|
|
DIST_LEGACY_UPDATE_NODE_RUNNER_COMPAT_0229A108,
|
|
`dist/${UPDATE_COMPATIBILITY_INVENTORY_FILE}`,
|
|
...previousReleaseInventory.releases.flatMap((release) =>
|
|
release.chunks.map((chunk) => `dist/${chunk.path}`),
|
|
),
|
|
DIST_LEGACY_CLI_EXIT_COMPAT,
|
|
DIST_LEGACY_CLI_EXIT_COMPAT_ALT,
|
|
DIST_OPENCLAW_ALIAS_PACKAGE,
|
|
DIST_OPENCLAW_ALIAS_PLUGIN_SDK_CORE,
|
|
],
|
|
BUILD_TIME,
|
|
);
|
|
}
|
|
|
|
export function expectedBuildSpawn() {
|
|
return [process.execPath, "--import", "tsx", "scripts/build-all.mts", "qaRuntime"];
|
|
}
|
|
|
|
export function statusCommandSpawn() {
|
|
return [process.execPath, "openclaw.mjs", "status"];
|
|
}
|
|
|
|
export function gatewayStatusCommandSpawn() {
|
|
return [
|
|
process.execPath,
|
|
"openclaw.mjs",
|
|
"gateway",
|
|
"status",
|
|
"--deep",
|
|
"--require-rpc",
|
|
"--json",
|
|
];
|
|
}
|
|
|
|
export function resolvePath(tmp: string, relativePath: string) {
|
|
return path.join(tmp, relativePath);
|
|
}
|
|
|
|
export function isTsxScriptArgs(args: string[], scriptPath: string): boolean {
|
|
return args[0] === "--import" && args[1] === "tsx" && args[2] === scriptPath;
|
|
}
|
|
|
|
export async function expectPathMissing(targetPath: string): Promise<void> {
|
|
let accessError: unknown;
|
|
try {
|
|
await fs.access(targetPath);
|
|
} catch (error) {
|
|
accessError = error;
|
|
}
|
|
expect((accessError as NodeJS.ErrnoException | undefined)?.code).toBe("ENOENT");
|
|
}
|
|
|
|
async function writeProjectFiles(tmp: string, files: Record<string, string>) {
|
|
await Promise.all(
|
|
Object.entries(files).map(async ([relativePath, contents]) => {
|
|
const absolutePath = resolvePath(tmp, relativePath);
|
|
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
|
await fs.writeFile(absolutePath, contents, "utf-8");
|
|
}),
|
|
);
|
|
}
|
|
|
|
export async function touchProjectFiles(tmp: string, relativePaths: string[], time: Date) {
|
|
await Promise.all(
|
|
relativePaths.map(async (relativePath) => {
|
|
const absolutePath = resolvePath(tmp, relativePath);
|
|
await fs.utimes(absolutePath, time, time);
|
|
}),
|
|
);
|
|
}
|
|
|
|
export async function setupTrackedProject(
|
|
tmp: string,
|
|
options: {
|
|
files?: Record<string, string>;
|
|
oldPaths?: string[];
|
|
buildPaths?: string[];
|
|
newPaths?: string[];
|
|
} = {},
|
|
) {
|
|
await writeRuntimePostBuildScaffold(tmp);
|
|
await writeProjectFiles(tmp, {
|
|
...BASE_PROJECT_FILES,
|
|
...options.files,
|
|
});
|
|
await touchProjectFiles(tmp, options.oldPaths ?? [], OLD_TIME);
|
|
await touchProjectFiles(tmp, options.buildPaths ?? [], BUILD_TIME);
|
|
await touchProjectFiles(tmp, options.newPaths ?? [], NEW_TIME);
|
|
}
|
|
|
|
export async function setupStampedProject(
|
|
tmp: string,
|
|
options: {
|
|
files?: Record<string, string>;
|
|
oldPaths?: string[];
|
|
newPaths?: string[];
|
|
rootSource?: boolean;
|
|
trackConfig?: boolean;
|
|
},
|
|
): Promise<void> {
|
|
const files = {
|
|
...(options.rootSource === false ? {} : { [ROOT_SRC]: "export const value = 1;\n" }),
|
|
...options.files,
|
|
};
|
|
const excludedPaths = new Set([...(options.oldPaths ?? []), ...(options.newPaths ?? [])]);
|
|
const buildPaths = [
|
|
...Object.keys(files).filter((filePath) => !excludedPaths.has(filePath)),
|
|
...(options.trackConfig ? [ROOT_TSCONFIG, ROOT_PACKAGE] : []),
|
|
DIST_ENTRY,
|
|
BUILD_STAMP,
|
|
];
|
|
await setupTrackedProject(tmp, {
|
|
files,
|
|
...(options.oldPaths ? { oldPaths: options.oldPaths } : {}),
|
|
buildPaths,
|
|
...(options.newPaths ? { newPaths: options.newPaths } : {}),
|
|
});
|
|
}
|
|
|
|
export async function writeImmutableDeploymentManifest(tmp: string): Promise<void> {
|
|
await writeProjectFiles(tmp, {
|
|
[DEPLOYMENT_MANIFEST]: `${JSON.stringify({ kind: "git", sourceHead: "a".repeat(40) })}\n`,
|
|
});
|
|
}
|
|
|
|
export function createSpawnRecorder(
|
|
options: {
|
|
gitHead?: string;
|
|
gitStatus?: string;
|
|
} = {},
|
|
) {
|
|
const spawnCalls: string[][] = [];
|
|
const spawn = (cmd: string, args: string[]) => {
|
|
spawnCalls.push([cmd, ...args]);
|
|
return createExitedProcess(0);
|
|
};
|
|
const spawnSync = (cmd: string, args: string[]) => {
|
|
if (cmd === "git" && args[0] === "rev-parse" && options.gitHead !== undefined) {
|
|
return { status: 0, stdout: options.gitHead };
|
|
}
|
|
if (cmd === "git" && args[0] === "status" && options.gitStatus !== undefined) {
|
|
return { status: 0, stdout: options.gitStatus };
|
|
}
|
|
return { status: 1, stdout: "" };
|
|
};
|
|
return { spawnCalls, spawn, spawnSync };
|
|
}
|
|
|
|
export function createCurrentGitSpawnRecorder(
|
|
options: { gitHead?: string; gitStatus?: string } = {},
|
|
) {
|
|
return createSpawnRecorder({ gitHead: "abc123\n", gitStatus: "", ...options });
|
|
}
|
|
|
|
export function createBuildRequirementDeps(
|
|
tmp: string,
|
|
options: {
|
|
gitStatus?: string;
|
|
env?: Record<string, string>;
|
|
} = {},
|
|
) {
|
|
const { spawnSync } = createCurrentGitSpawnRecorder({ gitStatus: options.gitStatus ?? "" });
|
|
return {
|
|
cwd: tmp,
|
|
env: {
|
|
...process.env,
|
|
...options.env,
|
|
},
|
|
fs: fsSync,
|
|
spawnSync,
|
|
distRoot: path.join(tmp, "dist"),
|
|
distEntry: path.join(tmp, DIST_ENTRY),
|
|
buildStampPath: path.join(tmp, BUILD_STAMP),
|
|
runtimePostBuildStampPath: path.join(tmp, RUNTIME_POSTBUILD_STAMP),
|
|
sourceRoots: [path.join(tmp, "src"), path.join(tmp, bundledPluginRoot("demo"))].map(
|
|
(sourceRoot) => ({
|
|
name: path.relative(tmp, sourceRoot).replaceAll("\\", "/"),
|
|
path: sourceRoot,
|
|
}),
|
|
),
|
|
configFiles: [ROOT_TSCONFIG, ROOT_PACKAGE, ROOT_TSDOWN].map((filePath) =>
|
|
path.join(tmp, filePath),
|
|
),
|
|
};
|
|
}
|
|
|
|
export async function trackProjectWithGit(tmp: string) {
|
|
const git = (...args: string[]) =>
|
|
execFileSync(
|
|
"git",
|
|
["-c", `core.hooksPath=${path.join(tmp, ".git", "disabled-hooks")}`, ...args],
|
|
{ cwd: tmp, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
|
|
).trim();
|
|
git("init", "--quiet", "--template=");
|
|
git("config", "core.quotePath", "true");
|
|
git("add", "--all");
|
|
git(
|
|
"-c",
|
|
"user.name=OpenClaw Test",
|
|
"-c",
|
|
"user.email=test@openclaw.invalid",
|
|
"-c",
|
|
"commit.gpgsign=false",
|
|
"commit",
|
|
"--quiet",
|
|
"-m",
|
|
"test: track runner fixture",
|
|
);
|
|
writeBuildStamp({ cwd: tmp, spawnSync: realSpawnSync });
|
|
writeRuntimePostBuildStamp({ cwd: tmp, spawnSync: realSpawnSync });
|
|
await touchProjectFiles(tmp, [BUILD_STAMP, RUNTIME_POSTBUILD_STAMP], BUILD_TIME);
|
|
return {
|
|
git,
|
|
deps: { ...createBuildRequirementDeps(tmp), env: {}, spawnSync: realSpawnSync },
|
|
};
|
|
}
|
|
|
|
type RunNodeTestOptions = NonNullable<Parameters<typeof runNodeMain>[0]> & {
|
|
stdout?: NodeJS.WriteStream;
|
|
};
|
|
type RunNodeResult = Awaited<ReturnType<typeof runNodeMain>>;
|
|
|
|
export async function runNodeCommand(
|
|
tmp: string,
|
|
options: RunNodeTestOptions,
|
|
): Promise<RunNodeResult> {
|
|
const { env, ...overrides } = options;
|
|
return await runNodeMain({
|
|
cwd: tmp,
|
|
args: ["status"],
|
|
...overrides,
|
|
env: { ...process.env, OPENCLAW_RUNNER_LOG: "0", ...env },
|
|
execPath: process.execPath,
|
|
platform: options.platform ?? process.platform,
|
|
} as RunNodeTestOptions);
|
|
}
|
|
|
|
type RunCommandParams = {
|
|
tmp: string;
|
|
args?: string[];
|
|
spawn: (cmd: string, args: string[]) => ReturnType<typeof createExitedProcess>;
|
|
spawnSync?: (cmd: string, args: string[]) => { status: number; stdout: string };
|
|
stderr?: NodeJS.WriteStream;
|
|
env?: Record<string, string>;
|
|
runRuntimePostBuild?: (params?: {
|
|
cwd?: string;
|
|
env?: Record<string, string | undefined>;
|
|
}) => void | Promise<void>;
|
|
};
|
|
|
|
export async function runStatusCommand({
|
|
tmp,
|
|
...options
|
|
}: RunCommandParams): Promise<RunNodeResult> {
|
|
return await runNodeCommand(tmp, options);
|
|
}
|
|
|
|
export async function runQaCommand(params: RunCommandParams): Promise<RunNodeResult> {
|
|
return await runStatusCommand({
|
|
...params,
|
|
args: ["qa", "suite", "--transport", "qa-channel", "--provider-mode", "mock-openai"],
|
|
});
|
|
}
|
|
|
|
export async function expectManifestId(tmp: string, relativePath: string, id: string) {
|
|
const manifest = JSON.parse(await fs.readFile(resolvePath(tmp, relativePath), "utf-8")) as {
|
|
id?: unknown;
|
|
};
|
|
expect(manifest.id).toBe(id);
|
|
}
|