Files
openclaw/test/scripts/openclaw-npm-extended-stable-release.test.ts
Peter Steinberger fe261b0f59 chore(tooling): typecheck root test/** with a dedicated tsgo lane (#104475)
* chore(types): add declaration files for scripts/lib and scripts/e2e modules

* chore(types): add declaration files for top-level script modules (a-m)

* chore(types): add declaration files for top-level script modules (n-z)

* test: use a non-secret-shaped gateway token fixture

* test: type ci workflow guard helpers for the root test lane

* chore(tooling): typecheck root test/** with a dedicated tsgo lane

- test/tsconfig/tsconfig.test.root.json: root-test program (strict unused checks,
  fixtures excluded; two Docker E2E clients that import built dist/** stay out,
  same rationale as the scripts/e2e exclusion in tsconfig.scripts.json)
- tsgo:test:root wired into tsgo:test, check:test-types, scripts/check.mjs, and
  the ci.yml test-types shard, mirroring the tsgo:scripts lane (#104348)
- changed-lane routing: test/**/*.ts (excluding fixtures) and the lane tsconfig
  now trigger 'typecheck test root' in check:changed; previously test/ paths ran
  lint only, so harness type errors surfaced first in CI (#104287 envDir case)
- burn down all 1071 latent type errors in the program: precise param/local
  types across test/scripts, test/vitest, test/e2e, and transitive scripts/e2e
  program members; 205 sibling .d.mts declaration files for imported .mjs
  modules (committed separately); zero any, zero ts-expect-error
- resolve the pre-existing testing star-export ambiguity in
  scripts/e2e/parallels/common.ts with an explicit re-export

Closes #104388

* chore(types): correct declaration fidelity per structured review

- re-derive 51 .d.mts files from implementation data flow instead of
  initializers: fix a wrong never return (runTestProjectsDelegation returns
  the child), add encoding-sensitive exec/spawn overloads (plain-gh), restore
  the full release profile union, make parsed paths string | null, add missing
  parseArgs fields via help/non-help unions, add a missing sibling declaration
  (budget-number-args), drop 15 unused lint directives
- precise install-record/tuple typing removes the type-aware oxlint
  regressions the first declarations caused in scripts/e2e implementations
- route .mts declaration edits under test/ to the testRoot lane and reference
  the test-root project from tsconfig.projects.json so tsgo:all covers it
  (closes both review findings against the lane wiring)

* chore(scripts): keep telegram runner dist typing structural for the boundary guard

* chore(types): declare runtime pack and gateway readiness exports added on main

* test: pin the importTargetPlan form of the plugin-contract plan import

The guard expectation still referenced the raw await import( form that
7ae5996bb3 (#103975) replaced with the importTargetPlan fallback helper;
the assertion fails on current main.
2026-07-11 06:15:41 -07:00

494 lines
16 KiB
TypeScript

import { spawnSync } from "node:child_process";
import { describe, expect, it, vi } from "vitest";
import {
capturePriorExtendedStableSelector,
extendedStableSelectorRepairCommand,
parseExtendedStableGuardBypass,
parsePriorExtendedStableSelector,
validateFullReleaseValidationManifest,
validateNpmPublishBoundary,
validateExtendedStableNpmReleaseRequest,
validateExtendedStableRunIdentity,
verifyExtendedStableRegistryReadback,
} from "../../scripts/openclaw-npm-extended-stable-release.mjs";
const sha = "a".repeat(40);
const branch = "extended-stable/2026.6.33";
describe("npm extended-stable publication boundary", () => {
it("parses only explicit boolean extended-stable guard values", () => {
expect(parseExtendedStableGuardBypass()).toBe(false);
expect(parseExtendedStableGuardBypass("")).toBe(false);
expect(parseExtendedStableGuardBypass("false")).toBe(false);
expect(parseExtendedStableGuardBypass("true")).toBe(true);
expect(() => parseExtendedStableGuardBypass("1")).toThrow(/must be "true" or "false"/u);
});
it.each([
["2026.6.11-alpha.1", "alpha"],
["2026.6.11-beta.1", "beta"],
["2026.6.11", "alpha"],
["2026.6.11", "beta"],
["2026.6.11", "latest"],
["2026.6.11-1", "alpha"],
["2026.6.11-1", "beta"],
["2026.6.11-1", "latest"],
["2026.6.33", "extended-stable"],
["2026.6.34", "extended-stable"],
])("accepts %s on %s", (version, distTag) => {
expect(() => validateNpmPublishBoundary(version, distTag)).not.toThrow();
});
it.each([
["2026.6.11", "extended-stable"],
["2026.6.11-alpha.1", "beta"],
["2026.6.11-alpha.1", "extended-stable"],
["2026.6.11-beta.1", "latest"],
["2026.6.11-beta.1", "extended-stable"],
["2026.6.33", "alpha"],
["2026.6.33", "beta"],
["2026.6.33", "latest"],
["2026.6.33-1", "alpha"],
["2026.6.33-1", "beta"],
["2026.6.33-1", "latest"],
["2026.6.33-1", "extended-stable"],
["2026.6.33", "stable"],
["2026.6.33", "nightly"],
])("rejects %s on %s", (version, distTag) => {
expect(() => validateNpmPublishBoundary(version, distTag)).toThrow();
});
it("prints exactly channel then publish tag from the dependency-free CLI", () => {
const result = spawnSync(
process.execPath,
["scripts/openclaw-npm-extended-stable-release.mjs", "publish-plan"],
{
encoding: "utf8",
env: {
...process.env,
PACKAGE_VERSION: "2026.6.33",
REQUESTED_PUBLISH_TAG: "extended-stable",
},
},
);
expect(result.status).toBe(0);
expect(result.stdout).toBe("stable\nextended-stable\n");
expect(result.stderr).toBe("");
});
it("allows a pre-.33 final extended-stable version only with the explicit bypass", () => {
expect(() =>
validateNpmPublishBoundary("2026.6.11", "extended-stable", {
bypassExtendedStableGuard: true,
}),
).not.toThrow();
expect(() => validateNpmPublishBoundary("2026.6.11", "extended-stable")).toThrow(
/patch 33 or above/u,
);
});
it.each(["alpha", "beta", "latest"])(
"rejects extended-stable guard bypass with the %s dist-tag",
(distTag) => {
expect(() =>
validateNpmPublishBoundary("2026.6.11", distTag, {
bypassExtendedStableGuard: true,
}),
).toThrow(/only be used with the extended-stable npm dist-tag/u);
},
);
it("preserves the unknown dist-tag rejection when bypass is requested", () => {
expect(() =>
validateNpmPublishBoundary("2026.6.11", "nightly", {
bypassExtendedStableGuard: true,
}),
).toThrow('Unsupported npm dist-tag "nightly"');
});
it.each([
["malformed bypass", "extended-stable", "sometimes", /must be "true" or "false"/u],
[
"non-extended-stable bypass",
"beta",
"true",
/only be used with the extended-stable npm dist-tag/u,
],
])("rejects %s in the dependency-free CLI", (_label, distTag, bypass, error) => {
const result = spawnSync(
process.execPath,
["scripts/openclaw-npm-extended-stable-release.mjs", "publish-plan"],
{
encoding: "utf8",
env: {
...process.env,
BYPASS_EXTENDED_STABLE_GUARD: bypass,
PACKAGE_VERSION: "2026.6.11",
REQUESTED_PUBLISH_TAG: distTag,
},
},
);
expect(result.status).not.toBe(0);
expect(result.stderr).toMatch(error);
});
});
describe("extended-stable npm release request", () => {
const valid = {
npmDistTag: "extended-stable",
releaseTag: "v2026.6.33",
npmWorkflowRef: "refs/heads/extended-stable/2026.6.33",
checkoutSha: sha,
tagSha: sha,
extendedStableBranchSha: sha,
packageVersion: "2026.6.33",
mainPackageVersion: "2026.7.2",
};
it("accepts .33, later patches, and any later protected-main calendar month", () => {
expect(validateExtendedStableNpmReleaseRequest(valid)).toEqual({
extendedStable: true,
releaseVersion: "2026.6.33",
extendedStableBranch: "extended-stable/2026.6.33",
});
expect(
validateExtendedStableNpmReleaseRequest({
...valid,
releaseTag: "v2026.6.34",
packageVersion: "2026.6.34",
}),
).toMatchObject({ extendedStable: true, releaseVersion: "2026.6.34" });
expect(
validateExtendedStableNpmReleaseRequest({
...valid,
releaseTag: "v2026.12.33",
npmWorkflowRef: "refs/heads/extended-stable/2026.12.33",
packageVersion: "2026.12.33",
mainPackageVersion: "2027.1.1",
}),
).toMatchObject({
extendedStable: true,
extendedStableBranch: "extended-stable/2026.12.33",
});
expect(() =>
validateExtendedStableNpmReleaseRequest({ ...valid, mainPackageVersion: "2026.8.1" }),
).not.toThrow();
expect(() =>
validateExtendedStableNpmReleaseRequest({ ...valid, mainPackageVersion: "2027.1.1" }),
).not.toThrow();
expect(() =>
validateExtendedStableNpmReleaseRequest({ ...valid, mainPackageVersion: "2028.12.32" }),
).not.toThrow();
});
it.each([
["patch below 33", { releaseTag: "v2026.6.32", packageVersion: "2026.6.32" }],
["beta prerelease", { releaseTag: "v2026.6.33-beta.1", packageVersion: "2026.6.33-beta.1" }],
["alpha prerelease", { releaseTag: "v2026.6.33-alpha.1", packageVersion: "2026.6.33-alpha.1" }],
["correction suffix", { releaseTag: "v2026.6.33-1", packageVersion: "2026.6.33-1" }],
["wrong branch", { npmWorkflowRef: "refs/heads/extended-stable/2026.6.34" }],
["checkout mismatch", { checkoutSha: "b".repeat(40) }],
["tag mismatch", { tagSha: "b".repeat(40) }],
["branch tip mismatch", { extendedStableBranchSha: "b".repeat(40) }],
["package mismatch", { packageVersion: "2026.6.34" }],
["main same month", { mainPackageVersion: "2026.6.1" }],
["main earlier month", { mainPackageVersion: "2026.5.32" }],
["main earlier year", { mainPackageVersion: "2025.12.32" }],
["main patch at monthly boundary", { mainPackageVersion: "2026.7.33" }],
])("rejects %s", (_label, changes) => {
expect(() => validateExtendedStableNpmReleaseRequest({ ...valid, ...changes })).toThrow();
});
it("preserves SHA-only regular preflight requests", () => {
expect(
validateExtendedStableNpmReleaseRequest({
...valid,
npmDistTag: "beta",
releaseTag: sha,
}),
).toEqual({ extendedStable: false });
});
it("accepts a SHA-only extended-stable preflight from an arbitrary workflow ref", () => {
expect(
validateExtendedStableNpmReleaseRequest({
...valid,
preflightOnly: true,
releaseTag: sha,
npmWorkflowRef: "refs/heads/main",
extendedStableBranchSha: "",
}),
).toEqual({
extendedStable: true,
releaseVersion: "2026.6.33",
extendedStableBranch: "extended-stable/2026.6.33",
});
expect(() =>
validateExtendedStableNpmReleaseRequest({
...valid,
preflightOnly: true,
releaseTag: "b".repeat(40),
npmWorkflowRef: "refs/heads/dev/preflight-candidate",
extendedStableBranchSha: "",
}),
).toThrow(/must match the checked-out commit/u);
expect(() =>
validateExtendedStableNpmReleaseRequest({
...valid,
preflightOnly: true,
releaseTag: sha,
checkoutSha: "",
}),
).toThrow(/requires the full checked-out commit SHA/u);
expect(() => validateExtendedStableNpmReleaseRequest({ ...valid, releaseTag: sha })).toThrow(
/exact final vYYYY\.M\.P release tag/u,
);
});
it("bypasses patch and protected-main policy while preserving canonical branch identity", () => {
const bypassed = {
...valid,
bypassExtendedStableGuard: true,
releaseTag: "v2026.6.11",
packageVersion: "2026.6.11",
mainPackageVersion: "",
};
expect(validateExtendedStableNpmReleaseRequest(bypassed)).toEqual({
extendedStable: true,
releaseVersion: "2026.6.11",
extendedStableBranch: "extended-stable/2026.6.33",
bypassExtendedStableGuard: true,
});
expect(() =>
validateExtendedStableNpmReleaseRequest({ ...bypassed, packageVersion: "2026.6.12" }),
).toThrow(/package version mismatch/u);
expect(() =>
validateExtendedStableNpmReleaseRequest({
...bypassed,
npmWorkflowRef: "refs/heads/dev/extended-stable-publish-test",
}),
).toThrow(/workflow ref mismatch/u);
expect(() =>
validateExtendedStableNpmReleaseRequest({
...bypassed,
extendedStableBranchSha: "b".repeat(40),
}),
).toThrow(/branch tip SHAs must match/u);
});
it("rejects bypass on a regular npm release request", () => {
expect(() =>
validateExtendedStableNpmReleaseRequest({
...valid,
bypassExtendedStableGuard: true,
npmDistTag: "beta",
releaseTag: sha,
}),
).toThrow(/only be used with the extended-stable npm dist-tag/u);
});
});
describe("extended-stable npm run identity", () => {
const validPreflight = {
workflowName: "OpenClaw NPM Release",
event: "workflow_dispatch",
conclusion: "success",
headBranch: branch,
headSha: sha,
};
it("accepts exact extended-stable preflight and validation runs", () => {
expect(
validateExtendedStableRunIdentity({
run: validPreflight,
kind: "preflight",
npmDistTag: "extended-stable",
expectedBranch: branch,
expectedSha: sha,
}),
).toBe(validPreflight);
expect(() =>
validateExtendedStableRunIdentity({
run: {
...validPreflight,
workflowName: "Full Release Validation",
status: "completed",
},
kind: "validation",
npmDistTag: "extended-stable",
expectedBranch: branch,
expectedSha: sha,
}),
).not.toThrow();
});
it("accepts only a completed successful Plugin NPM Release run on the exact branch and SHA", () => {
const pluginRun = {
workflowName: "Plugin NPM Release",
displayTitle: `Plugin NPM Release [extended-stable] ${sha}`,
event: "workflow_dispatch",
status: "completed",
conclusion: "success",
headBranch: branch,
headSha: sha,
};
expect(
validateExtendedStableRunIdentity({
run: pluginRun,
kind: "plugin",
npmDistTag: "extended-stable",
expectedBranch: branch,
expectedSha: sha,
}),
).toBe(pluginRun);
for (const changes of [
{ workflowName: "OpenClaw NPM Release" },
{ displayTitle: `Plugin NPM Release [default] ${sha}` },
{ displayTitle: `Plugin NPM Release [extended-stable] ${"b".repeat(40)}` },
{ status: "in_progress" },
{ conclusion: "failure" },
{ headBranch: "main" },
{ headSha: "b".repeat(40) },
]) {
expect(() =>
validateExtendedStableRunIdentity({
run: { ...pluginRun, ...changes },
kind: "plugin",
npmDistTag: "extended-stable",
expectedBranch: branch,
expectedSha: sha,
}),
).toThrow();
}
});
it.each([
["wrong branch", { headBranch: "main" }],
["missing branch", { headBranch: undefined }],
["wrong SHA", { headSha: "b".repeat(40) }],
["missing SHA", { headSha: undefined }],
])("rejects %s", (_label, changes) => {
expect(() =>
validateExtendedStableRunIdentity({
run: { ...validPreflight, ...changes },
kind: "preflight",
npmDistTag: "extended-stable",
expectedBranch: branch,
expectedSha: sha,
}),
).toThrow(/headBranch=.*headSha=/u);
});
});
describe("Full Validation manifest identity", () => {
const valid = {
workflowName: "Full Release Validation",
workflowRef: branch,
targetSha: sha,
runId: "123",
runAttempt: "2",
};
it("accepts the exact branch and target SHA", () => {
expect(
validateFullReleaseValidationManifest({
manifest: valid,
npmDistTag: "extended-stable",
expectedWorkflowRef: branch,
expectedSha: sha,
expectedRunId: "123",
expectedRunAttempt: "2",
}),
).toBe(valid);
});
it.each([
["wrong workflow ref", { workflowRef: "main" }],
["missing workflow ref", { workflowRef: undefined }],
["wrong target SHA", { targetSha: "b".repeat(40) }],
["missing target SHA", { targetSha: undefined }],
["wrong run ID", { runId: "124" }],
["wrong run attempt", { runAttempt: "1" }],
])("rejects %s", (_label, changes) => {
expect(() =>
validateFullReleaseValidationManifest({
manifest: { ...valid, ...changes },
npmDistTag: "extended-stable",
expectedWorkflowRef: branch,
expectedSha: sha,
expectedRunId: "123",
expectedRunAttempt: "2",
}),
).toThrow();
});
});
describe("extended-stable selector capture", () => {
it("distinguishes bootstrap absence from an existing selector", () => {
expect(parsePriorExtendedStableSelector('{"latest":"2026.7.1"}')).toBe("absent");
expect(parsePriorExtendedStableSelector('{"extended-stable":"2026.6.33"}')).toBe("2026.6.33");
});
it.each(["not json", "null", "[]", '"2026.6.33"'])("rejects invalid result %s", (value) => {
expect(() => parsePriorExtendedStableSelector(value)).toThrow();
});
it("rejects command failure rather than treating it as bootstrap", () => {
expect(() =>
capturePriorExtendedStableSelector({ query: () => ({ status: 1, stdout: "" }) }),
).toThrow(/query failed/u);
});
});
describe("extended-stable registry readback", () => {
it("accepts eventual convergence and sleeps 10 seconds between attempts", async () => {
let attempt = 0;
const sleep = vi.fn(async (_delay: number) => {});
const result = await verifyExtendedStableRegistryReadback({
expectedVersion: "2026.6.33",
query: async (target: string) => {
if (target === "openclaw@2026.6.33") {
attempt += 1;
}
return { status: 0, stdout: attempt >= 2 ? "2026.6.33\n" : "2026.6.32\n" };
},
sleep,
});
expect(result).toEqual({
exactVersion: "2026.6.33",
extendedStableSelector: "2026.6.33",
attemptsUsed: 2,
});
expect(sleep).toHaveBeenCalledOnce();
expect(sleep).toHaveBeenCalledWith(10_000);
});
it("exhausts exactly 12 dual-query attempts on mismatch or failure", async () => {
const query = vi.fn(async () => ({ status: 1, stdout: "" }));
const sleep = vi.fn(async (_delay: number) => {});
await expect(
verifyExtendedStableRegistryReadback({ expectedVersion: "2026.6.33", query, sleep }),
).rejects.toThrow(/after 12 attempts/u);
expect(query).toHaveBeenCalledTimes(24);
expect(sleep).toHaveBeenCalledTimes(11);
expect(sleep.mock.calls.every(([delay]) => delay === 10_000)).toBe(true);
});
});
describe("extended-stable selector repair", () => {
it("points the selector at the expected published version", () => {
expect(extendedStableSelectorRepairCommand("v2026.6.33")).toBe(
"npm dist-tag add openclaw@2026.6.33 extended-stable",
);
});
it.each([undefined, "absent", "2026.6.33-beta.1", "2026.6.33-1"])(
"rejects an invalid expected version: %s",
(expectedVersion) => {
expect(() => extendedStableSelectorRepairCommand(expectedVersion)).toThrow(
"Extended-stable selector repair requires an exact final YYYY.M.P version.",
);
},
);
});