Files
Hy-Memory/memhub/scripts/build_runtime_distribution.test.cjs
T
2026-10-16 16:26:27 +08:00

590 lines
19 KiB
JavaScript

const assert = require("node:assert/strict");
const {
chmod,
mkdtemp,
mkdir,
readFile,
readlink,
realpath,
rm,
symlink,
utimes,
writeFile,
} = require("node:fs/promises");
const { spawnSync } = require("node:child_process");
const os = require("node:os");
const path = require("node:path");
const { test } = require("node:test");
const {
assertCoreFresh,
assertDistributionSelfContained,
buildRuntimeDistribution,
packageMacosNodeDependencies,
} = require("./build_runtime_distribution.cjs");
async function fixture(platform = "linux") {
const root = await mkdtemp(path.join(os.tmpdir(), "om-runtime-dist-"));
await mkdir(path.join(root, "packages/runtime-control/src"), {
recursive: true,
});
await writeFile(
path.join(root, "packages/runtime-control/package.json"),
JSON.stringify({ name: "@one-memory/runtime-control", version: "0.0.0" }),
);
await writeFile(
path.join(root, "packages/runtime-control/src/core-supervisor.ts"),
"// fixture source\n",
);
await mkdir(path.join(root, "apps/core/src/one_memory"), {
recursive: true,
});
await writeFile(
path.join(root, "apps/core/src/one_memory/context_memory.py"),
"# fixture core source\n",
);
await writeFile(
path.join(root, "apps/core/one-memory.spec"),
"# fixture spec\n",
);
await writeFile(path.join(root, "apps/core/pyproject.toml"), "[project]\n");
for (const file of [
"apps/cli/dist/one-memory.cjs",
"apps/core/dist/one-memory-runtime/one-memory",
"packages/runtime-control/dist/runtime.cjs",
"packages/host-bridge/dist/mcp.cjs",
"packages/host-bridge/dist/recall-cli.cjs",
"packages/host-bridge/dist/sync-hook/manifest.json",
"apps/web/dist/index.html",
"SBOM.spdx.json",
"THIRD_PARTY_NOTICES.md",
"LICENSE",
"packages/host-bridge/src/memmy-adapter/claude-code/LICENSE.memmy",
"packages/host-bridge/src/memmy-adapter/claude-code/PROVENANCE.md",
]) {
await mkdir(path.dirname(path.join(root, file)), { recursive: true });
await writeFile(
path.join(root, file),
file === "apps/cli/dist/one-memory.cjs"
? 'process.stdout.write(process.execPath + "\\n")'
: file === "SBOM.spdx.json"
? JSON.stringify({
SPDXID: "SPDXRef-Package-memmy-agent-derived",
name: "MemTensor memmy-agent",
})
: file === "THIRD_PARTY_NOTICES.md"
? "# Third-Party Notices\n\nMemTensor memmy-agent, MIT\n"
: file ===
"packages/host-bridge/src/memmy-adapter/claude-code/LICENSE.memmy"
? "MIT License\nCopyright (c) 2026-present MemTensor\n"
: file,
{ mode: 0o700 },
);
}
const node = path.join(root, platform === "win32" ? "node.exe" : "node");
await writeFile(node, "node", { mode: 0o700 });
return { root, node, output: path.join(root, "release") };
}
test("builds one manifest for CLI, Runtime, Core, Host Bridge and Web", async (t) => {
const value = await fixture();
t.after(() => rm(value.root, { recursive: true, force: true }));
const manifest = await buildRuntimeDistribution({
sourceRoot: value.root,
output: value.output,
platform: "linux",
arch: "x64",
version: "1.0.0",
buildNumber: 42,
gitCommit: "a".repeat(40),
nodeCommand: value.node,
});
assert.equal(manifest.build_number, 42);
assert.equal(manifest.git_commit, "a".repeat(40));
assert.equal(manifest.runtime_protocol, 1);
assert.deepEqual(manifest.supported_cli_protocol, { min: 1, max: 1 });
for (const file of [
"bin/node",
"bin/one-memory",
"bin/one-memory.cjs",
"core/one-memory",
"runtime/runtime.cjs",
"host-bridge/mcp.cjs",
"web/index.html",
"SBOM.spdx.json",
"THIRD_PARTY_NOTICES.md",
"licenses/LICENSE.one-memory",
"licenses/LICENSE.memmy",
"licenses/PROVENANCE.memmy.md",
]) {
assert.equal(typeof manifest.files[file], "string", file);
}
const sbom = JSON.parse(
await readFile(path.join(value.output, "SBOM.spdx.json"), "utf8"),
);
const notice = await readFile(
path.join(value.output, "THIRD_PARTY_NOTICES.md"),
"utf8",
);
assert.match(notice, /MemTensor|LICENSE\.memmy/);
assert.ok(
JSON.stringify(sbom).includes("MemTensor") ||
JSON.stringify(sbom).includes("memmy"),
);
assert.deepEqual(
JSON.parse(
await readFile(path.join(value.output, "manifest.json"), "utf8"),
),
manifest,
);
});
test("macOS distribution requires and hashes the native service observer", async (t) => {
const value = await fixture("darwin");
t.after(() => rm(value.root, { recursive: true, force: true }));
const options = {
sourceRoot: value.root,
output: value.output,
platform: "darwin",
arch: "arm64",
version: "1.0.0",
buildNumber: 43,
nodeCommand: value.node,
};
await assert.rejects(buildRuntimeDistribution(options), /service-observer/);
await writeFile(
path.join(value.root, "packages/runtime-control/dist/service-observer"),
"synthetic observer",
{ mode: 0o755 },
);
const manifest = await buildRuntimeDistribution(options);
assert.match(manifest.files["runtime/service-observer"], /^[a-f0-9]{64}$/);
});
test("macOS packages a relocatable Node dylib closure", async (t) => {
if (process.platform !== "darwin") return;
const root = await mkdtemp(path.join(os.tmpdir(), "om-node-runtime-"));
t.after(() => rm(root, { recursive: true, force: true }));
const packagedNode = path.join(root, "bin", "node");
await mkdir(path.dirname(packagedNode), { recursive: true });
await (await import("node:fs/promises")).copyFile(
process.execPath,
packagedNode,
);
const libraries = await packageMacosNodeDependencies(
process.execPath,
packagedNode,
root,
);
// Official Node builds may depend only on macOS system libraries, in which
// case there is no private dylib closure to copy.
assert.ok(Array.isArray(libraries));
const result = spawnSync(packagedNode, ["-e", "process.stdout.write('ok')"], {
encoding: "utf8",
});
assert.equal(result.status, 0, result.stderr);
assert.equal(result.stdout, "ok");
});
test("resource copying preserves internal relative symlink targets", async (t) => {
const value = await fixture();
t.after(() => rm(value.root, { recursive: true, force: true }));
await symlink(
"index.html",
path.join(value.root, "apps/web/dist/linked.html"),
);
await mkdir(path.join(value.root, "apps/web/dist/assets"));
await writeFile(
path.join(value.root, "apps/web/dist/assets/app.js"),
"asset",
);
await symlink("assets", path.join(value.root, "apps/web/dist/linked-assets"));
const manifest = await buildRuntimeDistribution({
sourceRoot: value.root,
output: value.output,
platform: "linux",
arch: "x64",
version: "1.0.0",
buildNumber: 44,
nodeCommand: value.node,
});
assert.equal(
await readlink(path.join(value.output, "web/linked.html")),
"index.html",
);
assert.equal(
await readlink(path.join(value.output, "web/linked-assets")),
"assets",
);
assert.match(manifest.files["web/linked.html"], /^[a-f0-9]{64}$/);
assert.match(manifest.files["web/linked-assets"], /^[a-f0-9]{64}$/);
assert.notEqual(
manifest.files["web/linked.html"],
manifest.files["web/index.html"],
);
});
test("does not replace an existing distribution when a required input is missing", async (t) => {
const value = await fixture();
t.after(() => rm(value.root, { recursive: true, force: true }));
await mkdir(value.output);
await writeFile(path.join(value.output, "keep"), "old release");
await rm(path.join(value.root, "apps/web/dist/index.html"));
await assert.rejects(
buildRuntimeDistribution({
sourceRoot: value.root,
output: value.output,
platform: "linux",
arch: "x64",
version: "1.0.0",
buildNumber: 45,
nodeCommand: value.node,
}),
);
assert.equal(
await readFile(path.join(value.output, "keep"), "utf8"),
"old release",
);
});
test("the packaged launcher stamps build identity and turns on diagnostics", async (t) => {
if (process.platform === "win32") return;
const value = await fixture();
t.after(() => rm(value.root, { recursive: true, force: true }));
await buildRuntimeDistribution({
sourceRoot: value.root,
output: value.output,
platform: "linux",
arch: "x64",
version: "1.0.0",
buildNumber: 85,
packageId: "One-Memory-H85-6929cbd-candidate",
nodeCommand: value.node,
});
const launcher = await readFile(
path.join(value.output, "bin", "one-memory"),
"utf8",
);
// Without the identity stamp the diagnostic chain has no build/package/round
// ids, and without Diagnostics=1 Core writes no kernel half at all.
assert.match(
launcher,
/ONE_MEMORY_BUILD_ID="\$\{ONE_MEMORY_BUILD_ID:-One-Memory-H85-6929cbd-candidate\}"/,
);
assert.match(
launcher,
/ONE_MEMORY_PACKAGE_ID="\$\{ONE_MEMORY_PACKAGE_ID:-One-Memory-H85-6929cbd-candidate\}"/,
);
assert.match(launcher, /ONE_MEMORY_ROUND="\$\{ONE_MEMORY_ROUND:-85\}"/);
assert.match(
launcher,
/ONE_MEMORY_BUILD_NUMBER="\$\{ONE_MEMORY_BUILD_NUMBER:-85\}"/,
);
assert.match(launcher, /ONE_MEMORY_DIAGNOSTICS=1/);
// The Runtime derives its Core port from whether it is the everyday install,
// so the launcher has to name its own root.
assert.match(launcher, /ONE_MEMORY_INSTALL_ROOT=/);
// The dist manifest now carries the formal package identity so a host install
// can bind to the same artifact (C5 identity binding); it must echo the
// builder-supplied packageId rather than being inferred.
const manifest = JSON.parse(
await readFile(path.join(value.output, "manifest.json"), "utf8"),
);
assert.equal(manifest.package_id, "One-Memory-H85-6929cbd-candidate");
});
test("self-containment rejects a dist polluted with smoke/readiness/owner files", async (t) => {
const value = await fixture();
t.after(() => rm(value.root, { recursive: true, force: true }));
await buildRuntimeDistribution({
sourceRoot: value.root,
output: value.output,
platform: "linux",
arch: "x64",
version: "1.0.0",
buildNumber: 85,
nodeCommand: value.node,
});
// A clean build is self-contained.
await assert.doesNotReject(assertDistributionSelfContained(value.output));
// Writing runtime state into the dist is exactly what the installer refuses
// with `distribution_manifest_incomplete` (H85 lifecycle matrix).
await mkdir(path.join(value.output, "smoke-data"), { recursive: true });
await writeFile(
path.join(value.output, "smoke-data", "one-memory.sqlite3"),
"",
);
await writeFile(path.join(value.output, "readiness.json"), "{}");
await writeFile(path.join(value.output, ".one-memory-hourly-owned"), "x\n");
await assert.rejects(
assertDistributionSelfContained(value.output),
/distribution_manifest_incomplete/,
);
});
test("the public CLI launcher resolves its distribution through a PATH symlink", async (t) => {
if (process.platform === "win32") return;
const value = await fixture();
t.after(() => rm(value.root, { recursive: true, force: true }));
await buildRuntimeDistribution({
sourceRoot: value.root,
output: value.output,
platform: "linux",
arch: "x64",
version: "1.0.0",
buildNumber: 46,
nodeCommand: value.node,
});
// The fixture's fake bundled node becomes executable for this path test by
// replacing it with the current real Node binary after manifest generation.
await rm(path.join(value.output, "bin", "node"));
await (await import("node:fs/promises")).copyFile(
process.execPath,
path.join(value.output, "bin", "node"),
);
if (process.platform === "darwin") {
await packageMacosNodeDependencies(
process.execPath,
path.join(value.output, "bin", "node"),
value.output,
);
}
await chmod(path.join(value.output, "bin", "node"), 0o700);
const publicBin = path.join(value.root, "public-bin");
await mkdir(publicBin);
const command = path.join(publicBin, "one-memory");
await symlink(path.join(value.output, "bin", "one-memory"), command);
const result = spawnSync(command, [], { encoding: "utf8" });
assert.equal(result.status, 0, result.stderr);
assert.equal(
await realpathSafe(result.stdout.trim()),
await realpathSafe(path.join(value.output, "bin", "node")),
);
});
test("the installed CLI launcher names the managed install root, not its version directory", async (t) => {
if (process.platform === "win32") return;
const value = await fixture();
t.after(() => rm(value.root, { recursive: true, force: true }));
const installRoot = path.join(value.root, "managed");
const selected = path.join(installRoot, "versions", "1.0.0-test");
if (process.platform === "darwin") {
await writeFile(
path.join(value.root, "packages/runtime-control/dist/service-observer"),
"synthetic observer",
{ mode: 0o755 },
);
}
await buildRuntimeDistribution({
sourceRoot: value.root,
output: selected,
platform: process.platform === "darwin" ? "darwin" : "linux",
arch: process.arch,
version: "1.0.0",
buildNumber: 47,
nodeCommand: value.node,
});
await rm(path.join(selected, "bin", "node"));
await (await import("node:fs/promises")).copyFile(
process.execPath,
path.join(selected, "bin", "node"),
);
if (process.platform === "darwin") {
await packageMacosNodeDependencies(
process.execPath,
path.join(selected, "bin", "node"),
selected,
);
}
await writeFile(
path.join(selected, "bin", "one-memory.cjs"),
"process.stdout.write(process.env.ONE_MEMORY_INSTALL_ROOT || '')",
);
const publicBin = path.join(value.root, "managed-bin");
await mkdir(publicBin);
const command = path.join(publicBin, "one-memory");
await symlink(path.join(selected, "bin", "one-memory"), command);
const result = spawnSync(command, [], { encoding: "utf8" });
assert.equal(result.status, 0, result.stderr);
assert.equal(
await realpathSafe(result.stdout),
await realpathSafe(installRoot),
);
});
async function realpathSafe(target) {
return realpath(target);
}
test("rejects a missing or unsafe build number before writing", async (t) => {
const value = await fixture();
t.after(() => rm(value.root, { recursive: true, force: true }));
await assert.rejects(
buildRuntimeDistribution({
sourceRoot: value.root,
output: value.output,
platform: "linux",
arch: "x64",
version: "1.0.0",
nodeCommand: value.node,
}),
/build_number must be a non-negative safe integer/,
);
await assert.rejects(
buildRuntimeDistribution({
sourceRoot: value.root,
output: value.output,
platform: "linux",
arch: "x64",
version: "1.0.0",
buildNumber: 1.5,
nodeCommand: value.node,
}),
/build_number must be a non-negative safe integer/,
);
});
test("rejects a non-SemVer Runtime identity before staging files", async (t) => {
const value = await fixture();
t.after(() => rm(value.root, { recursive: true, force: true }));
await assert.rejects(
buildRuntimeDistribution({
sourceRoot: value.root,
output: value.output,
platform: "linux",
arch: "x64",
version: "release-one",
buildNumber: 1,
nodeCommand: path.join(value.root, "node"),
}),
/valid SemVer/,
);
});
test("refuses to package a stale runtime-control dist", async (t) => {
const value = await fixture();
t.after(() => rm(value.root, { recursive: true, force: true }));
const old = new Date(Date.now() - 60_000);
await utimes(
path.join(value.root, "packages/runtime-control/src/core-supervisor.ts"),
new Date(),
new Date(),
);
await utimes(
path.join(value.root, "packages/runtime-control/dist/runtime.cjs"),
old,
old,
);
await assert.rejects(
buildRuntimeDistribution({
sourceRoot: value.root,
output: value.output,
platform: "linux",
arch: "x64",
version: "1.0.0",
buildNumber: 50,
nodeCommand: value.node,
}),
/runtime-control dist is stale/,
);
});
test("refuses to package a stale CLI bundle that inlines runtime-control", async (t) => {
const value = await fixture();
t.after(() => rm(value.root, { recursive: true, force: true }));
// The CLI bundle inlines runtime-control's service-plan. A runtime-control
// source change with a stale CLI bundle is exactly how the H87 package
// shipped an install path that still ignored the per-root Core port.
await mkdir(path.join(value.root, "apps/cli/src"), { recursive: true });
await writeFile(
path.join(value.root, "apps/cli/src/cli.ts"),
"// fixture cli source\n",
{ mode: 0o700 },
);
const old = new Date(Date.now() - 60_000);
await utimes(
path.join(value.root, "packages/runtime-control/src/core-supervisor.ts"),
new Date(),
new Date(),
);
// Keep runtime-control itself fresh so only the CLI staleness check can trip.
await utimes(
path.join(value.root, "packages/runtime-control/dist/runtime.cjs"),
new Date(Date.now() + 60_000),
new Date(Date.now() + 60_000),
);
await utimes(path.join(value.root, "apps/cli/dist/one-memory.cjs"), old, old);
await assert.rejects(
buildRuntimeDistribution({
sourceRoot: value.root,
output: value.output,
platform: "linux",
arch: "x64",
version: "1.0.0",
buildNumber: 51,
nodeCommand: value.node,
}),
/CLI bundle is stale/,
);
});
test("refuses to package a stale Core distribution", async (t) => {
const value = await fixture();
t.after(() => rm(value.root, { recursive: true, force: true }));
await mkdir(path.join(value.root, "apps/core/src/one_memory"), {
recursive: true,
});
await writeFile(
path.join(value.root, "apps/core/src/one_memory/context_memory.py"),
"# newer source\n",
);
await writeFile(
path.join(value.root, "apps/core/one-memory.spec"),
"# spec\n",
);
await writeFile(
path.join(value.root, "apps/core/pyproject.toml"),
"[project]\n",
);
const old = new Date(Date.now() - 60_000);
await utimes(
path.join(value.root, "apps/core/src/one_memory/context_memory.py"),
new Date(),
new Date(),
);
await utimes(
path.join(value.root, "apps/core/dist/one-memory-runtime/one-memory"),
old,
old,
);
await assert.rejects(
assertCoreFresh(value.root, "linux"),
/core distribution is stale/,
);
await assert.rejects(
buildRuntimeDistribution({
sourceRoot: value.root,
output: value.output,
platform: "linux",
arch: "x64",
version: "1.0.0",
buildNumber: 51,
nodeCommand: value.node,
}),
/core distribution is stale/,
);
});