mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-28 12:36:55 +02:00
224 lines
7.8 KiB
TypeScript
224 lines
7.8 KiB
TypeScript
import fs from "node:fs";
|
|
import fsPromises from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import {
|
|
cleanTsdownOutputRoots,
|
|
createTsdownOutputScanner,
|
|
pruneSourceCheckoutBundledPluginNodeModules,
|
|
pruneStaleRootChunkFiles,
|
|
resolveTsdownBuildInvocation,
|
|
runTsdownBuildInvocation,
|
|
} from "../../scripts/tsdown-build.mjs";
|
|
import { createScriptTestHarness } from "./test-helpers.js";
|
|
|
|
const { createTempDir } = createScriptTestHarness();
|
|
|
|
describe("resolveTsdownBuildInvocation", () => {
|
|
it("routes Windows tsdown builds through the pnpm runner instead of shell=true", () => {
|
|
const result = resolveTsdownBuildInvocation({
|
|
platform: "win32",
|
|
nodeExecPath: "C:\\Program Files\\nodejs\\node.exe",
|
|
npmExecPath: "C:/Users/test/AppData/Local/pnpm/10.32.1/bin/pnpm.cjs",
|
|
env: {},
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
command: "C:\\Program Files\\nodejs\\node.exe",
|
|
args: [
|
|
"C:/Users/test/AppData/Local/pnpm/10.32.1/bin/pnpm.cjs",
|
|
"exec",
|
|
"tsdown",
|
|
"--config-loader",
|
|
"unrun",
|
|
"--logLevel",
|
|
"warn",
|
|
"--no-clean",
|
|
],
|
|
options: {
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
shell: false,
|
|
windowsVerbatimArguments: undefined,
|
|
env: {},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("keeps source-checkout prune best-effort", () => {
|
|
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
const rmSync = vi.spyOn(fs, "rmSync");
|
|
|
|
rmSync.mockImplementation(() => {
|
|
throw new Error("locked");
|
|
});
|
|
|
|
expect(() =>
|
|
pruneSourceCheckoutBundledPluginNodeModules({
|
|
cwd: process.cwd(),
|
|
}),
|
|
).not.toThrow();
|
|
|
|
expect(warn).toHaveBeenCalledWith(
|
|
"tsdown: could not prune bundled plugin source node_modules: Error: locked",
|
|
);
|
|
|
|
warn.mockRestore();
|
|
rmSync.mockRestore();
|
|
});
|
|
|
|
it("prunes stale hashed root chunk files but keeps stable aliases and nested assets", async () => {
|
|
const rootDir = createTempDir("openclaw-tsdown-build-");
|
|
const distDir = path.join(rootDir, "dist");
|
|
const distRuntimeDir = path.join(rootDir, "dist-runtime");
|
|
await fsPromises.mkdir(path.join(distDir, "control-ui"), { recursive: true });
|
|
await fsPromises.mkdir(distRuntimeDir, { recursive: true });
|
|
await fsPromises.writeFile(path.join(distDir, "delegate-BPjCe4gC.js"), "old delegate\n");
|
|
await fsPromises.writeFile(path.join(distDir, "compact.runtime-2DiEmVcA.js"), "old runtime\n");
|
|
await fsPromises.writeFile(path.join(distDir, "compact.runtime.js"), "stable alias\n");
|
|
await fsPromises.writeFile(path.join(distDir, "entry.js"), "entry\n");
|
|
await fsPromises.writeFile(path.join(distDir, "control-ui", "index.html"), "asset\n");
|
|
await fsPromises.writeFile(
|
|
path.join(distRuntimeDir, "heartbeat-runner.runtime-fspOEj_1.js"),
|
|
"old runtime\n",
|
|
);
|
|
await fsPromises.writeFile(path.join(distRuntimeDir, "heartbeat-runner.runtime.js"), "alias\n");
|
|
|
|
pruneStaleRootChunkFiles({ cwd: rootDir });
|
|
|
|
await expect(
|
|
fsPromises.readFile(path.join(distDir, "compact.runtime.js"), "utf8"),
|
|
).resolves.toBe("stable alias\n");
|
|
await expect(fsPromises.readFile(path.join(distDir, "entry.js"), "utf8")).resolves.toBe(
|
|
"entry\n",
|
|
);
|
|
await expect(
|
|
fsPromises.readFile(path.join(distDir, "control-ui", "index.html"), "utf8"),
|
|
).resolves.toBe("asset\n");
|
|
await expect(
|
|
fsPromises.readFile(path.join(distRuntimeDir, "heartbeat-runner.runtime.js"), "utf8"),
|
|
).resolves.toBe("alias\n");
|
|
await expect(fsPromises.stat(path.join(distDir, "delegate-BPjCe4gC.js"))).rejects.toThrow();
|
|
await expect(
|
|
fsPromises.stat(path.join(distDir, "compact.runtime-2DiEmVcA.js")),
|
|
).rejects.toThrow();
|
|
await expect(
|
|
fsPromises.stat(path.join(distRuntimeDir, "heartbeat-runner.runtime-fspOEj_1.js")),
|
|
).rejects.toThrow();
|
|
});
|
|
|
|
it("cleans tsdown output roots before using tsdown --no-clean", async () => {
|
|
const rootDir = createTempDir("openclaw-tsdown-clean-");
|
|
const distFile = path.join(rootDir, "dist", "stale.js");
|
|
const distRuntimeFile = path.join(rootDir, "dist-runtime", "stale.js");
|
|
const unrelatedFile = path.join(rootDir, "tmp", "keep.js");
|
|
await fsPromises.mkdir(path.dirname(distFile), { recursive: true });
|
|
await fsPromises.mkdir(path.dirname(distRuntimeFile), { recursive: true });
|
|
await fsPromises.mkdir(path.dirname(unrelatedFile), { recursive: true });
|
|
await fsPromises.writeFile(distFile, "stale\n");
|
|
await fsPromises.writeFile(distRuntimeFile, "stale\n");
|
|
await fsPromises.writeFile(unrelatedFile, "keep\n");
|
|
|
|
cleanTsdownOutputRoots({ cwd: rootDir });
|
|
|
|
await expect(fsPromises.stat(path.join(rootDir, "dist"))).rejects.toThrow();
|
|
await expect(fsPromises.stat(path.join(rootDir, "dist-runtime"))).rejects.toThrow();
|
|
await expect(fsPromises.readFile(unrelatedFile, "utf8")).resolves.toBe("keep\n");
|
|
});
|
|
});
|
|
|
|
describe("createTsdownOutputScanner", () => {
|
|
it("tracks fatal build diagnostics while bounding captured output", () => {
|
|
const scanner = createTsdownOutputScanner({ maxCaptureBytes: 20 });
|
|
|
|
scanner.append("prefix that should be trimmed\n");
|
|
scanner.append("[INEFFECTIVE_DYNAMIC_IMPORT]\n");
|
|
scanner.append("[UNRESOLVED_IMPORT] src/index.ts\n");
|
|
|
|
const result = scanner.finish();
|
|
|
|
expect(result.hasIneffectiveDynamicImport).toBe(true);
|
|
expect(result.fatalUnresolvedImport).toContain("[UNRESOLVED_IMPORT] src/index.ts");
|
|
expect(result.captured.length).toBeLessThanOrEqual(20);
|
|
});
|
|
|
|
it("ignores unresolved imports from bundled plugin and dependency paths", () => {
|
|
const scanner = createTsdownOutputScanner();
|
|
|
|
scanner.append("[UNRESOLVED_IMPORT] extensions/telegram/src/index.ts\n");
|
|
scanner.append("[UNRESOLVED_IMPORT] node_modules/example/index.js\n");
|
|
|
|
expect(scanner.finish().fatalUnresolvedImport).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("runTsdownBuildInvocation", () => {
|
|
function createWriteSink() {
|
|
const chunks: string[] = [];
|
|
return {
|
|
sink: {
|
|
write(chunk: unknown) {
|
|
chunks.push(Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk));
|
|
return true;
|
|
},
|
|
},
|
|
chunks,
|
|
};
|
|
}
|
|
|
|
it("streams child output while preserving diagnostics for post-run checks", async () => {
|
|
const output = createWriteSink();
|
|
const result = await runTsdownBuildInvocation(
|
|
{
|
|
command: process.execPath,
|
|
args: [
|
|
"-e",
|
|
"process.stdout.write('stdout-ok\\n'); process.stderr.write('[INEFFECTIVE_DYNAMIC_IMPORT]\\n')",
|
|
],
|
|
options: {
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
shell: false,
|
|
env: process.env,
|
|
},
|
|
},
|
|
{
|
|
stdout: output.sink,
|
|
stderr: output.sink,
|
|
env: { ...process.env, OPENCLAW_TSDOWN_HEARTBEAT_MS: "0" },
|
|
},
|
|
);
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(result.hasIneffectiveDynamicImport).toBe(true);
|
|
expect(output.chunks.join("")).toContain("stdout-ok");
|
|
});
|
|
|
|
it("terminates the child when OPENCLAW_TSDOWN_TIMEOUT_MS elapses", async () => {
|
|
const output = createWriteSink();
|
|
const result = await runTsdownBuildInvocation(
|
|
{
|
|
command: process.execPath,
|
|
args: ["-e", "setTimeout(() => {}, 10000)"],
|
|
options: {
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
shell: false,
|
|
env: process.env,
|
|
},
|
|
},
|
|
{
|
|
stdout: output.sink,
|
|
stderr: output.sink,
|
|
env: {
|
|
...process.env,
|
|
OPENCLAW_TSDOWN_HEARTBEAT_MS: "0",
|
|
OPENCLAW_TSDOWN_TIMEOUT_MS: "50",
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(result.timedOut).toBe(true);
|
|
expect(result.status).toBeNull();
|
|
expect(result.signal).toBe("SIGTERM");
|
|
expect(output.chunks.join("")).toContain("timeout after 50ms");
|
|
});
|
|
});
|