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, overrides = {}) { return validatePrepublishPluginRegistryArtifact({ artifactDir: paths.artifactDir, expectedCandidateVersion: VERSION, expectedManifestSha256: sha256(paths.manifestPath), expectedSourceSha: SOURCE_SHA, requiredPackages: [PACKAGE_NAME], ...overrides, }); } function firstPackage(paths: ReturnType) { const [entry] = paths.manifest.packages; if (!entry) { throw new Error("fixture manifest must contain one package"); } return entry; } function addCompanionPackage(paths: ReturnType) { 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", ); }); });