fix(test): rebuild startup memory artifacts

This commit is contained in:
Vincent Koc 2026-05-28 13:50:08 +02:00
parent 653e8d1ea5
commit 4ce3c3e36c
No known key found for this signature in database
5 changed files with 270 additions and 3 deletions

View file

@ -7,11 +7,13 @@ import { fileURLToPath, pathToFileURL } from "node:url";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const entryCandidates = ["dist/entry.js", "dist/entry.mjs"];
const startupMetadataPath = "dist/cli-startup-metadata.json";
export function hasCliStartupBuild(params = {}) {
const rootDir = params.rootDir ?? repoRoot;
const exists = params.existsSync ?? existsSync;
return entryCandidates.some((relativePath) => exists(path.join(rootDir, relativePath)));
const hasEntry = entryCandidates.some((relativePath) => exists(path.join(rootDir, relativePath)));
return hasEntry && exists(path.join(rootDir, startupMetadataPath));
}
export function ensureCliStartupBuild(params = {}) {
@ -24,7 +26,9 @@ export function ensureCliStartupBuild(params = {}) {
const spawn = params.spawnSync ?? spawnSync;
const buildScript = path.join(rootDir, "scripts", "build-all.mjs");
console.error("[cli-startup-build] dist/entry missing; running cliStartup build profile");
console.error(
"[cli-startup-build] dist startup entry or metadata missing; running cliStartup build profile",
);
const result = spawn(nodeExecPath, [buildScript, "cliStartup"], {
cwd: rootDir,
env: params.env ?? process.env,

View file

@ -0,0 +1,102 @@
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { existsSync, readdirSync } from "node:fs";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import {
collectBundledPluginBuildEntries,
NON_PACKAGED_BUNDLED_PLUGIN_DIRS,
} from "./lib/bundled-plugin-build-entries.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
function collectExpectedExtensionMemoryEntryIds(rootDir, env) {
try {
const entries = collectBundledPluginBuildEntries({ cwd: rootDir, env });
return entries
.filter(
(entry) =>
entry.hasManifest &&
!NON_PACKAGED_BUNDLED_PLUGIN_DIRS.has(entry.id) &&
entry.sourceEntries.length > 0,
)
.map((entry) => entry.id)
.toSorted((left, right) => left.localeCompare(right));
} catch {
return null;
}
}
export function hasBuiltExtensionMemoryEntries(params = {}) {
const rootDir = params.rootDir ?? repoRoot;
const exists = params.existsSync ?? existsSync;
const readDir = params.readdirSync ?? readdirSync;
const extensionsDir = path.join(rootDir, "dist", "extensions");
if (!exists(extensionsDir)) {
return false;
}
let extensionIds;
try {
extensionIds = readDir(extensionsDir);
} catch {
return false;
}
const requiredExtensionIds =
params.requiredExtensionIds?.length > 0
? params.requiredExtensionIds
: collectExpectedExtensionMemoryEntryIds(rootDir, params.env ?? process.env);
if (!requiredExtensionIds || requiredExtensionIds.length === 0) {
return extensionIds.some((extensionId) =>
exists(path.join(extensionsDir, extensionId, "index.js")),
);
}
return requiredExtensionIds.every((extensionId) =>
exists(path.join(extensionsDir, extensionId, "index.js")),
);
}
export function ensureExtensionMemoryBuild(params = {}) {
const rootDir = params.rootDir ?? repoRoot;
if (
hasBuiltExtensionMemoryEntries({
rootDir,
existsSync: params.existsSync,
readdirSync: params.readdirSync,
requiredExtensionIds: params.requiredExtensionIds,
env: params.env,
})
) {
return { built: false };
}
const nodeExecPath = params.nodeExecPath ?? process.execPath;
const spawn = params.spawnSync ?? spawnSync;
const buildScript = path.join(rootDir, "scripts", "build-all.mjs");
console.error(
"[extension-memory-build] dist/extensions missing; running cliStartup build profile",
);
const result = spawn(nodeExecPath, [buildScript, "cliStartup"], {
cwd: rootDir,
env: params.env ?? process.env,
stdio: params.stdio ?? "inherit",
});
if (result.error) {
throw result.error;
}
const status = result.status ?? (result.signal ? 1 : 0);
if (status !== 0) {
throw new Error(`cliStartup build profile failed with exit code ${status}`);
}
return { built: true };
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
try {
ensureExtensionMemoryBuild();
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
}

View file

@ -4,6 +4,7 @@ import { spawn } from "node:child_process";
import { existsSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { ensureExtensionMemoryBuild } from "./ensure-extension-memory-build.mjs";
import { formatErrorMessage } from "./lib/error-format.mjs";
const DEFAULT_CONCURRENCY = 6;
@ -241,6 +242,10 @@ function findExtensionEntries(repoRoot) {
async function main() {
const options = parseArgs(process.argv.slice(2));
const repoRoot = process.cwd();
ensureExtensionMemoryBuild({
rootDir: repoRoot,
requiredExtensionIds: options.extensions,
});
const allEntries = findExtensionEntries(repoRoot);
const selectedEntries =
options.extensions.length === 0

View file

@ -28,14 +28,24 @@ describe("ensure-cli-startup-build", () => {
const root = makeTempRoot();
mkdirSync(path.join(root, "dist"), { recursive: true });
writeFileSync(path.join(root, "dist", "entry.js"), "export {};\n", "utf8");
writeFileSync(
path.join(root, "dist", "cli-startup-metadata.json"),
'{"rootHelpText":"help\\n"}\n',
"utf8",
);
expect(hasCliStartupBuild({ rootDir: root })).toBe(true);
});
it("skips the build profile when dist entry output already exists", () => {
it("skips the build profile when dist entry output and startup metadata already exist", () => {
const root = makeTempRoot();
mkdirSync(path.join(root, "dist"), { recursive: true });
writeFileSync(path.join(root, "dist", "entry.mjs"), "export {};\n", "utf8");
writeFileSync(
path.join(root, "dist", "cli-startup-metadata.json"),
'{"rootHelpText":"help\\n"}\n',
"utf8",
);
const result = ensureCliStartupBuild({
rootDir: root,
@ -47,6 +57,35 @@ describe("ensure-cli-startup-build", () => {
expect(result).toEqual({ built: false });
});
it("runs the cliStartup build profile when startup metadata is missing", () => {
const root = makeTempRoot();
mkdirSync(path.join(root, "dist"), { recursive: true });
writeFileSync(path.join(root, "dist", "entry.js"), "export {};\n", "utf8");
const calls: unknown[] = [];
const result = ensureCliStartupBuild({
rootDir: root,
nodeExecPath: "/node",
spawnSync: (command, args, options) => {
calls.push({ command, args, options });
return { status: 0 };
},
stdio: "pipe",
});
expect(result).toEqual({ built: true });
expect(calls).toEqual([
{
command: "/node",
args: [path.join(root, "scripts", "build-all.mjs"), "cliStartup"],
options: expect.objectContaining({
cwd: root,
stdio: "pipe",
}),
},
]);
});
it("runs the cliStartup build profile when dist entry output is missing", () => {
const root = makeTempRoot();
const calls: unknown[] = [];

View file

@ -0,0 +1,117 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
ensureExtensionMemoryBuild,
hasBuiltExtensionMemoryEntries,
} from "../../scripts/ensure-extension-memory-build.mjs";
const tempRoots: string[] = [];
function makeTempRoot(): string {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-extension-memory-build-"));
tempRoots.push(root);
mkdirSync(path.join(root, "scripts"), { recursive: true });
writeFileSync(path.join(root, "scripts", "build-all.mjs"), "", "utf8");
return root;
}
afterEach(() => {
for (const root of tempRoots.splice(0)) {
rmSync(root, { recursive: true, force: true });
}
});
describe("ensure-extension-memory-build", () => {
it("detects existing built extension entrypoints", () => {
const root = makeTempRoot();
mkdirSync(path.join(root, "dist", "extensions", "telegram"), { recursive: true });
writeFileSync(
path.join(root, "dist", "extensions", "telegram", "index.js"),
"export {};\n",
"utf8",
);
expect(
hasBuiltExtensionMemoryEntries({ rootDir: root, requiredExtensionIds: ["telegram"] }),
).toBe(true);
});
it("rejects partial built extension entrypoint sets", () => {
const root = makeTempRoot();
mkdirSync(path.join(root, "dist", "extensions", "discord"), { recursive: true });
writeFileSync(
path.join(root, "dist", "extensions", "discord", "index.js"),
"export {};\n",
"utf8",
);
expect(
hasBuiltExtensionMemoryEntries({
rootDir: root,
requiredExtensionIds: ["discord", "telegram"],
}),
).toBe(false);
});
it("skips the build profile when extension entrypoints already exist", () => {
const root = makeTempRoot();
mkdirSync(path.join(root, "dist", "extensions", "discord"), { recursive: true });
writeFileSync(
path.join(root, "dist", "extensions", "discord", "index.js"),
"export {};\n",
"utf8",
);
const result = ensureExtensionMemoryBuild({
rootDir: root,
requiredExtensionIds: ["discord"],
spawnSync: () => {
throw new Error("unexpected build");
},
});
expect(result).toEqual({ built: false });
});
it("runs the cliStartup build profile when extension entrypoints are missing", () => {
const root = makeTempRoot();
const calls: unknown[] = [];
const result = ensureExtensionMemoryBuild({
rootDir: root,
requiredExtensionIds: ["discord"],
nodeExecPath: "/node",
spawnSync: (command, args, options) => {
calls.push({ command, args, options });
return { status: 0 };
},
stdio: "pipe",
});
expect(result).toEqual({ built: true });
expect(calls).toEqual([
{
command: "/node",
args: [path.join(root, "scripts", "build-all.mjs"), "cliStartup"],
options: expect.objectContaining({
cwd: root,
stdio: "pipe",
}),
},
]);
});
it("fails when the cliStartup build profile fails", () => {
const root = makeTempRoot();
expect(() =>
ensureExtensionMemoryBuild({
rootDir: root,
spawnSync: () => ({ status: 1 }),
stdio: "pipe",
}),
).toThrow("cliStartup build profile failed with exit code 1");
});
});