Files
openclaw/test/scripts/prepublish-plugin-registry-artifact.test.ts
Peter Steinberger 9a2f91a79a fix(release): keep prerelease companions exact in cross-OS checks (#120556)
* fix(release): install exact cross-os companion artifacts

* fix(release): derive companion package type from resolver

* fix(release): produce provider-owned companion registries

* fix(release): preserve companions for npm candidates

* refactor(release): simplify companion registry flow

* test(release): align package acceptance contracts

* fix(release): guard optional registry JSON parsing

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-08-08 21:45:16 +08:00

358 lines
12 KiB
TypeScript

import { execFileSync, spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { resolveCrossOsCompanionPackages } from "../../scripts/lib/cross-os-release-checks/companions.ts";
import {
PREPUBLISH_PLUGIN_REGISTRY_MANIFEST,
createPrepublishPluginRegistryArtifact,
validatePrepublishPluginRegistryArtifact,
} from "../../scripts/prepublish-plugin-registry-artifact.mjs";
const SOURCE_SHA = "a".repeat(40);
const VERSION = "2026.8.1-beta.1";
const PACKAGE_NAME = "@openclaw/discord";
const TARBALL = "openclaw-discord-2026.8.1-beta.1.tgz";
const SCRIPT = path.resolve("scripts/prepublish-plugin-registry-artifact.mjs");
const tempDirs: string[] = [];
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { force: true, recursive: true });
}
});
function sha256(file: string): string {
return createHash("sha256").update(readFileSync(file)).digest("hex");
}
function fixture() {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-prepublish-plugin-registry-"));
tempDirs.push(root);
const packageRoot = path.join(root, "package");
const artifactDir = path.join(root, "artifact");
mkdirSync(packageRoot);
mkdirSync(artifactDir);
writeFileSync(
path.join(packageRoot, "package.json"),
`${JSON.stringify({ name: PACKAGE_NAME, version: VERSION })}\n`,
);
const tarballPath = path.join(artifactDir, TARBALL);
execFileSync("tar", ["-czf", tarballPath, "-C", root, "package"]);
const manifestPath = path.join(artifactDir, PREPUBLISH_PLUGIN_REGISTRY_MANIFEST);
const manifest = {
schema: "openclaw.prepublish-plugin-registry/v1",
schemaVersion: 1,
sourceSha: SOURCE_SHA,
candidateVersion: VERSION,
packages: [
{
name: PACKAGE_NAME,
version: VERSION,
tarball: TARBALL,
sha256: sha256(tarballPath),
},
],
};
const writeManifest = () => {
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
};
writeManifest();
return { artifactDir, manifest, manifestPath, tarballPath, writeManifest };
}
function validate(paths: ReturnType<typeof fixture>, overrides = {}) {
return validatePrepublishPluginRegistryArtifact({
artifactDir: paths.artifactDir,
expectedCandidateVersion: VERSION,
expectedManifestSha256: sha256(paths.manifestPath),
expectedSourceSha: SOURCE_SHA,
requiredPackages: [PACKAGE_NAME],
...overrides,
});
}
function firstPackage(paths: ReturnType<typeof fixture>) {
const [entry] = paths.manifest.packages;
if (!entry) {
throw new Error("fixture manifest must contain one package");
}
return entry;
}
function addCompanionPackage(paths: ReturnType<typeof fixture>) {
const name = "@openclaw/feishu";
const tarball = "openclaw-feishu-2026.8.1-beta.1.tgz";
const archiveRoot = path.join(path.dirname(paths.artifactDir), "feishu-package");
const packageRoot = path.join(archiveRoot, "package");
mkdirSync(packageRoot, { recursive: true });
writeFileSync(
path.join(packageRoot, "package.json"),
`${JSON.stringify({ name, version: VERSION })}\n`,
);
const tarballPath = path.join(paths.artifactDir, tarball);
execFileSync("tar", ["-czf", tarballPath, "-C", archiveRoot, "package"]);
paths.manifest.packages.push({
name,
version: VERSION,
tarball,
sha256: sha256(tarballPath),
});
paths.writeManifest();
}
function cliFixture() {
const repoRoot = mkdtempSync(path.join(tmpdir(), "openclaw-prepublish-plugin-cli-"));
tempDirs.push(repoRoot);
const packageDir = path.join(repoRoot, "extensions", "discord");
const scriptsDir = path.join(repoRoot, "scripts", "lib");
mkdirSync(packageDir, { recursive: true });
mkdirSync(scriptsDir, { recursive: true });
writeFileSync(
path.join(repoRoot, "package.json"),
`${JSON.stringify({ name: "openclaw", version: VERSION })}\n`,
);
writeFileSync(
path.join(packageDir, "package.json"),
`${JSON.stringify({
name: PACKAGE_NAME,
version: VERSION,
openclaw: { release: { publishToNpm: true } },
})}\n`,
);
writeFileSync(
path.join(scriptsDir, "plugin-npm-runtime-build.mjs"),
'console.log("runtime build stdout");\n',
);
writeFileSync(
path.join(scriptsDir, "plugin-npm-package-manifest.mjs"),
`import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
const repoRoot = process.cwd();
const packageDir = process.argv[process.argv.indexOf("--run") + 1];
const outputDir = process.argv[process.argv.indexOf("--pack-destination") + 1];
const staging = path.join(repoRoot, ".pack-fixture");
fs.mkdirSync(path.join(staging, "package"), { recursive: true });
fs.copyFileSync(path.join(repoRoot, packageDir, "package.json"), path.join(staging, "package", "package.json"));
execFileSync("tar", ["-czf", path.join(outputDir, "${TARBALL}"), "-C", staging, "package"]);
console.log("package manifest stdout");
`,
);
execFileSync("git", ["init"], { cwd: repoRoot });
execFileSync("git", ["config", "user.email", "release-test@example.invalid"], {
cwd: repoRoot,
});
execFileSync("git", ["config", "user.name", "Release Test"], { cwd: repoRoot });
execFileSync("git", ["config", "commit.gpgsign", "false"], { cwd: repoRoot });
execFileSync("git", ["add", "."], { cwd: repoRoot });
execFileSync("git", ["commit", "-m", "test: seed release source"], { cwd: repoRoot });
const sourceSha = execFileSync("git", ["rev-parse", "HEAD"], {
cwd: repoRoot,
encoding: "utf8",
}).trim();
return { repoRoot, sourceSha };
}
describe("prepublish plugin registry artifact", () => {
it("validates the immutable manifest, package set, hashes, and packed identity", () => {
const paths = fixture();
const result = validate(paths);
expect(result.manifest.packages.map((entry) => entry.name)).toEqual([PACKAGE_NAME]);
});
it("requires the complete immutable identity tuple", () => {
const paths = fixture();
const common = {
artifactDir: paths.artifactDir,
expectedCandidateVersion: VERSION,
expectedManifestSha256: sha256(paths.manifestPath),
expectedSourceSha: SOURCE_SHA,
requiredPackages: [PACKAGE_NAME],
};
for (const field of [
"expectedCandidateVersion",
"expectedManifestSha256",
"expectedSourceSha",
] as const) {
expect(() =>
validatePrepublishPluginRegistryArtifact({ ...common, [field]: undefined }),
).toThrow(field);
}
});
it("accepts immutable companion packages beyond the selected Docker plan", () => {
const paths = fixture();
addCompanionPackage(paths);
expect(validate(paths).manifest.packages.map((entry) => entry.name)).toEqual([
"@openclaw/discord",
"@openclaw/feishu",
]);
});
it("extracts only required cross-OS companions from the validated registry", () => {
const paths = fixture();
addCompanionPackage(paths);
expect(
resolveCrossOsCompanionPackages({
artifactDir: paths.artifactDir,
candidateVersion: VERSION,
manifestSha256: sha256(paths.manifestPath),
requiredPackages: ["@openclaw/feishu"],
sourceSha: SOURCE_SHA,
}),
).toEqual([
{
name: "@openclaw/feishu",
tarballPath: path.join(paths.artifactDir, "openclaw-feishu-2026.8.1-beta.1.tgz"),
},
]);
});
it("rejects mismatched cross-OS companion registry identities", () => {
const paths = fixture();
const common = {
artifactDir: paths.artifactDir,
candidateVersion: VERSION,
manifestSha256: sha256(paths.manifestPath),
requiredPackages: [PACKAGE_NAME],
sourceSha: SOURCE_SHA,
};
expect(() => resolveCrossOsCompanionPackages({ ...common, sourceSha: "b".repeat(40) })).toThrow(
"source SHA differs",
);
expect(() =>
resolveCrossOsCompanionPackages({ ...common, candidateVersion: "2026.8.1-beta.2" }),
).toThrow("version differs");
expect(() =>
resolveCrossOsCompanionPackages({ ...common, manifestSha256: "c".repeat(64) }),
).toThrow("manifest SHA-256 differs");
writeFileSync(paths.tarballPath, "tampered");
expect(() => resolveCrossOsCompanionPackages(common)).toThrow("tarball SHA-256 mismatch");
});
it("refuses to create an artifact from tracked changes under the same HEAD", () => {
const repoRoot = mkdtempSync(path.join(tmpdir(), "openclaw-prepublish-plugin-source-"));
tempDirs.push(repoRoot);
writeFileSync(
path.join(repoRoot, "package.json"),
`${JSON.stringify({ name: "openclaw", version: VERSION })}\n`,
);
execFileSync("git", ["init"], { cwd: repoRoot });
execFileSync("git", ["config", "user.email", "release-test@example.invalid"], {
cwd: repoRoot,
});
execFileSync("git", ["config", "user.name", "Release Test"], { cwd: repoRoot });
execFileSync("git", ["config", "commit.gpgsign", "false"], { cwd: repoRoot });
execFileSync("git", ["add", "package.json"], { cwd: repoRoot });
execFileSync("git", ["commit", "-m", "test: seed release source"], { cwd: repoRoot });
const sourceSha = execFileSync("git", ["rev-parse", "HEAD"], {
cwd: repoRoot,
encoding: "utf8",
}).trim();
writeFileSync(
path.join(repoRoot, "package.json"),
`${JSON.stringify({ name: "openclaw", version: `${VERSION}-dirty` })}\n`,
);
expect(() =>
createPrepublishPluginRegistryArtifact({
repoRoot,
outputDir: path.join(repoRoot, "artifact"),
sourceSha,
candidateVersion: VERSION,
requiredPackages: [],
}),
).toThrow("tracked changes");
});
it("keeps noisy package commands off the CLI JSON stdout contract", () => {
const { repoRoot, sourceSha } = cliFixture();
const artifactDir = path.join(repoRoot, "artifact");
const result = spawnSync(
process.execPath,
[
SCRIPT,
"create",
"--repo-root",
repoRoot,
"--artifact-dir",
artifactDir,
"--source-sha",
sourceSha,
"--candidate-version",
VERSION,
"--required-packages-json",
JSON.stringify([PACKAGE_NAME]),
],
{ cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
);
expect(result.status).toBe(0);
expect(JSON.parse(result.stdout)).toMatchObject({
manifestSha256: expect.stringMatching(/^[0-9a-f]{64}$/u),
packages: [PACKAGE_NAME],
});
expect(result.stderr).toContain("runtime build stdout");
expect(result.stderr).toContain("package manifest stdout");
});
it("rejects traversal and duplicate package entries", () => {
const traversal = fixture();
firstPackage(traversal).tarball = "../escape.tgz";
traversal.writeManifest();
expect(() => validate(traversal)).toThrow("invalid package entry");
const duplicate = fixture();
duplicate.manifest.packages.push({ ...firstPackage(duplicate) });
duplicate.writeManifest();
expect(() =>
validate(duplicate, { requiredPackages: [PACKAGE_NAME, "@openclaw/feishu"] }),
).toThrow("duplicate package");
});
it("rejects missing and extra artifact files", () => {
const missing = fixture();
unlinkSync(missing.tarballPath);
expect(() => validate(missing)).toThrow("missing, extra, or non-file");
const extra = fixture();
writeFileSync(path.join(extra.artifactDir, "extra.txt"), "unexpected");
expect(() => validate(extra)).toThrow("missing, extra, or non-file");
});
it("rejects hash, identity, version, source SHA, and required-set mismatches", () => {
const hash = fixture();
writeFileSync(hash.tarballPath, "tampered");
expect(() => validate(hash)).toThrow("tarball SHA-256 mismatch");
const identity = fixture();
firstPackage(identity).name = "@openclaw/feishu";
identity.writeManifest();
expect(() => validate(identity, { requiredPackages: ["@openclaw/feishu"] })).toThrow(
"tarball identity mismatch",
);
const version = fixture();
expect(() => validate(version, { expectedCandidateVersion: "2026.8.1-beta.2" })).toThrow(
"version differs",
);
const source = fixture();
expect(() => validate(source, { expectedSourceSha: "b".repeat(40) })).toThrow(
"source SHA differs",
);
const required = fixture();
expect(() => validate(required, { requiredPackages: ["@openclaw/feishu"] })).toThrow(
"missing Docker-plan package",
);
});
});