openclaw/test/scripts/release-version.test.ts
Peter Steinberger 3cb68020b6
refactor(scripts): deslop shared tooling
## What Problem This Solves

Repository scripts retain private copies of shared tooling helpers and an unused translation subprocess runner, increasing maintenance work across sibling scripts.

## User Impact

Internal tooling cleanup: existing flags, diagnostics, generated bytes, JSON formats, exit codes, and cleanup policies stay the same. The build wrapper additionally detects forbidden dynamic-import warnings when compiler output splits the warning marker across chunks.

## Why This Change Was Made

- Remove the retired i18n process runner. Its final production caller moved to the shared formatter in #95534; retain real CLI/privacy/provider tests and run their subprocesses with the existing dependency.
- Share Docker resource, signal-trap, platform, workspace-staging, and plugin-selection helpers while retaining each caller's environment precedence and lifecycle decisions.
- Reuse existing E2E fixture JSON/assertion and package-path helpers; retain distinct write modes, recovery ordering, and mounted/frozen harness contracts.
- Share macOS guest desktop-user/home resolution through its existing owner while preserving per-caller timeout policy.
- Route release/mobile flags through the existing version flag specifications, preserving split-only values, duplicate/mode rules, help timing, and error text. Reuse existing retry sleep and comparator owners and remove an unreachable iOS output branch.
- Reuse existing guard entrypoint, failure-trailer, diagnostic-line, and metadata-normalization owners.

Measured reduction: **732 net production lines**, with test changes counted separately. PR tooling and its protected import closure, CI planners/shards, baselines, and generated artifacts are unchanged.

## Fixes Found Along the Way

The tsdown scanner checked each raw output chunk for its warning marker. It now checks the existing combined-line buffer. The regression exercises every split inside the marker and fails on the original source for the intended missing-warning assertion.

## Evidence

- Blacksmith Testbox `tbx_01m3tp4sabwd0807kj9jkqqmbr`: frozen dependency install and candidate source-byte verification; all nine changed test files and 59 selected sibling files passed, including real CLI, package fixture, recovery, Docker harness, and Parallels transport contracts.
- Generated channel metadata is byte-identical; differential metadata normalization cases passed.
- SDK surface check passed. Independent isolated Codex review completed with no P0–P2 findings.
- Initial changed checks caught an invalid direct source import in a proposed snapshot-distance consolidation; that independent change was withdrawn. No boundary exception was added. The final changed checks pass, including script/test lint and Docker shell/scheduler checks; madge reports 0 cycles and the runtime import check reports 0 cycles.
- Per-file single-worker wall time for changed suites: translation 9.92s; Docker helper 38.88s; live Docker auth 1.71s; mobile ref 2.36s; mobile release 6.23s; release preparation 2.42s; version 2.04s; installer 15.01s; tsdown 2.70s. The existing Docker helper suite executes shell/process cleanup and container-command boundary fixtures; new assertions reuse those fixtures. New parser/scanner cases use no sleeps or polling.

Hosted CI will be verified against this PR's exact pushed head. No live deployment or release was performed.
2026-10-01 05:14:07 +00:00

318 lines
9.2 KiB
TypeScript

// Release version tests cover one-command core and native version alignment.
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
applyReleaseVersionPlan,
parseReleaseVersionArgs,
planReleaseVersion,
} from "../../scripts/release-version.ts";
import { cleanupTempDirs, makeTempDir } from "../helpers/temp-dir.js";
const SCRIPT = path.resolve("scripts/release-version.ts");
const tempDirs = new Set<string>();
afterEach(() => {
cleanupTempDirs(tempDirs);
});
function writeFixture(params?: {
androidVersion?: string;
androidVersionCode?: number;
packageVersion?: string;
}): string {
const root = makeTempDir(tempDirs, "openclaw-release-version-");
fs.mkdirSync(path.join(root, "apps", "macos", "Sources", "OpenClaw", "Resources"), {
recursive: true,
});
fs.mkdirSync(path.join(root, "apps", "android", "Config"), { recursive: true });
fs.mkdirSync(path.join(root, "apps", "android", "fastlane", "metadata", "android", "en-US"), {
recursive: true,
});
fs.writeFileSync(
path.join(root, "package.json"),
`${JSON.stringify(
{
name: "openclaw",
version: params?.packageVersion ?? "2026.6.11",
private: true,
},
null,
2,
)}\n`,
);
fs.writeFileSync(
path.join(root, "apps", "macos", "Sources", "OpenClaw", "Resources", "Info.plist"),
[
"<plist>",
"<dict>",
" <key>CFBundleShortVersionString</key>",
" <string>2026.6.11</string>",
" <key>CFBundleVersion</key>",
" <string>2026061100</string>",
"</dict>",
"</plist>",
"",
].join("\n"),
);
fs.writeFileSync(
path.join(root, "apps", "android", "version.json"),
`${JSON.stringify(
{
version: params?.androidVersion ?? "2026.7.1",
versionCode: params?.androidVersionCode ?? 2026070102,
},
null,
2,
)}\n`,
);
fs.writeFileSync(path.join(root, "apps", "android", "Config", "Version.properties"), "stale\n");
fs.writeFileSync(
path.join(root, "apps", "android", "CHANGELOG.md"),
[
"# Android Changelog",
"",
"## 2026.7.2",
"",
"- New release notes.",
"",
"## 2026.7.1",
"",
"- Previous release notes.",
"",
].join("\n"),
);
fs.writeFileSync(
path.join(
root,
"apps",
"android",
"fastlane",
"metadata",
"android",
"en-US",
"release_notes.txt",
),
"- Previous release notes.\n",
);
return root;
}
function readJson(filePath: string): Record<string, unknown> {
return JSON.parse(fs.readFileSync(filePath, "utf8")) as Record<string, unknown>;
}
it("rejects alpha release preparation before reading or changing packages", () => {
expect(() => planReleaseVersion({ version: "2026.9.24-alpha.1" })).toThrow(
"Alpha releases are retired;",
);
});
describe("release version argument parsing", () => {
it("defaults to check mode and keeps Android opt-in", () => {
expect(parseReleaseVersionArgs(["--version", "2026.7.2-beta.1"])).toMatchObject({
android: false,
mode: "check",
version: "2026.7.2-beta.1",
});
});
it("keeps last-value ordering and rejects incomplete options after help", () => {
expect(
parseReleaseVersionArgs([
"--write",
"--version",
"2026.7.1",
"--",
"--check",
"--version",
"2026.7.2",
]),
).toMatchObject({ mode: "check", version: "2026.7.2" });
expect(() => parseReleaseVersionArgs(["--help", "--root", "-h"])).toThrow(
"Missing value for --root.",
);
expect(() => parseReleaseVersionArgs(["--version=2026.7.2"])).toThrow(
"Unknown argument: --version=2026.7.2",
);
});
});
describe("release version planning", () => {
it("aligns root and macOS metadata to a prerelease without moving Android", () => {
const root = writeFixture();
const plan = planReleaseVersion({
rootDir: root,
version: "2026.7.2-beta.1",
});
expect(plan.changes.map((change) => path.relative(root, change.path))).toEqual([
"package.json",
"apps/macos/Sources/OpenClaw/Resources/Info.plist",
]);
applyReleaseVersionPlan(plan);
expect(readJson(path.join(root, "package.json"))).toMatchObject({
name: "openclaw",
private: true,
version: "2026.7.2-beta.1",
});
expect(
fs.readFileSync(
path.join(root, "apps", "macos", "Sources", "OpenClaw", "Resources", "Info.plist"),
"utf8",
),
).toContain("<string>2026070200</string>");
expect(readJson(path.join(root, "apps", "android", "version.json"))).toMatchObject({
version: "2026.7.1",
versionCode: 2026070102,
});
});
it("keeps repository versions at the base version for correction tags", () => {
const root = writeFixture();
const plan = planReleaseVersion({
rootDir: root,
version: "2026.7.2-3",
});
applyReleaseVersionPlan(plan);
expect(plan.version).toBe("2026.7.2-3");
expect(readJson(path.join(root, "package.json"))).toMatchObject({
version: "2026.7.2",
});
expect(
fs.readFileSync(
path.join(root, "apps", "macos", "Sources", "OpenClaw", "Resources", "Info.plist"),
"utf8",
),
).toContain("<string>2026.7.2</string>");
});
it("keeps an existing Android build increment on the same release train", () => {
const root = writeFixture();
const plan = planReleaseVersion({
android: true,
rootDir: root,
version: "2026.7.1-beta.4",
});
applyReleaseVersionPlan(plan);
expect(readJson(path.join(root, "apps", "android", "version.json"))).toEqual({
version: "2026.7.1",
versionCode: 2026070102,
});
expect(
fs.readFileSync(path.join(root, "apps", "android", "Config", "Version.properties"), "utf8"),
).toContain("OPENCLAW_ANDROID_VERSION_CODE=2026070102");
expect(
fs.readFileSync(
path.join(
root,
"apps",
"android",
"fastlane",
"metadata",
"android",
"en-US",
"release_notes.txt",
),
"utf8",
),
).toBe("- Previous release notes.\n");
});
it("starts a new Android train at its canonical build code", () => {
const root = writeFixture();
const plan = planReleaseVersion({
android: true,
rootDir: root,
version: "2026.7.2-3",
});
applyReleaseVersionPlan(plan);
expect(readJson(path.join(root, "apps", "android", "version.json"))).toEqual({
version: "2026.7.2",
versionCode: 2026070201,
});
expect(
fs.readFileSync(
path.join(
root,
"apps",
"android",
"fastlane",
"metadata",
"android",
"en-US",
"release_notes.txt",
),
"utf8",
),
).toBe("- New release notes.\n");
});
it("validates every selected file before writing any changes", () => {
const root = writeFixture();
const packagePath = path.join(root, "package.json");
const before = fs.readFileSync(packagePath, "utf8");
fs.writeFileSync(
path.join(root, "apps", "macos", "Sources", "OpenClaw", "Resources", "Info.plist"),
"<plist><dict></dict></plist>\n",
);
expect(() =>
planReleaseVersion({
rootDir: root,
version: "2026.7.2-beta.1",
}),
).toThrow("must contain exactly one string value for CFBundleShortVersionString");
expect(fs.readFileSync(packagePath, "utf8")).toBe(before);
});
});
describe("release version CLI", () => {
it("reports drift in check mode, writes it once, then passes", () => {
const root = writeFixture();
const check = spawnSync(
process.execPath,
["--import", "tsx", SCRIPT, "--root", root, "--version", "2026.7.2-beta.1"],
{ encoding: "utf8" },
);
expect(check.status).toBe(1);
expect(check.stderr).toContain("Release version 2026.7.2-beta.1 requires updates:");
expect(check.stderr).toContain("- package.json");
const write = spawnSync(
process.execPath,
["--import", "tsx", SCRIPT, "--root", root, "--version", "2026.7.2-beta.1", "--write"],
{ encoding: "utf8" },
);
expect(write.status).toBe(0);
expect(write.stdout).toContain("Updated release version 2026.7.2-beta.1:");
const recheck = spawnSync(
process.execPath,
["--import", "tsx", SCRIPT, "--root", root, "--version", "2026.7.2-beta.1"],
{ encoding: "utf8" },
);
expect(recheck.status).toBe(0);
expect(recheck.stdout).toBe("Release version 2026.7.2-beta.1 is already aligned.\n");
});
it("rejects invalid versions without changing the fixture", () => {
const root = writeFixture();
const packagePath = path.join(root, "package.json");
const before = fs.readFileSync(packagePath, "utf8");
const result = spawnSync(
process.execPath,
["--import", "tsx", SCRIPT, "--root", root, "--version", "7.2.0", "--write"],
{ encoding: "utf8" },
);
expect(result.status).toBe(1);
expect(result.stderr).toContain("Invalid release version '7.2.0'");
expect(fs.readFileSync(packagePath, "utf8")).toBe(before);
});
});