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" );
});
});
Messung V0.5 in Prozent C=99 H=98 G=98
¤ Dauer der Verarbeitung: 0.9 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland