Files
openclaw/test/scripts/plugin-release-git-lifecycle.test.ts
Peter Steinberger 2a53c32dc2 test(channels,infra,sdk): remove low-value tests (batch d016) (#158764)
* test(signal): deslop s080 tests

* test(node-host): deslop s085 tests

* test(imessage): deslop s084 tests

* test(scripts): deslop s075 tests

* test(flows): deslop s089 tests

* test(slack): deslop s087 tests

* test(mattermost): deslop s091 tests

* test(infra): deslop s086 tests

* test(plugin-sdk): deslop s090 tests

* test(voice-call): deslop s092 tests

* style(test): format batch d016 replay

* test(mattermost): preserve retained reaction regression

* test: retain independent contracts in batch d016

* test(plugin-sdk): avoid shadowed table labels

* test(node-host): remove unused runtime test type
2026-09-26 10:51:39 +00:00

704 lines
22 KiB
TypeScript

import { expect, it } from "vitest";
import { runCiGitStep } from "./ci-git-owner.test-support.js";
// Plugin publication jobs run on Ubuntu. Native Job Object and POSIX owner
// behavior stays in the shared ci-platform-checkout suites.
const posixIt = it.skipIf(process.platform === "win32");
const sha = "a".repeat(40);
const workflowSha = "b".repeat(40);
const releaseTag = "v2026.8.1";
const alphaBranch = "tideclaw/alpha/2026-08-30-1200Z";
const packageDir = "extensions/fixture";
const packageJson = '{"name":"@openclaw/fixture","version":"2026.8.33"}\n';
type PluginMode =
| "clawhub-resolve"
| "clawhub-oidc"
| "clawhub-trust"
| "npm-resolve"
| "npm-trust"
| "npm-preflight-read"
| "npm-publish-read";
type RunOptions = Partial<Parameters<typeof runCiGitStep>[0]>;
const modes: Record<
PluginMode,
{ workflow: { file: string; job: string; step: string }; env: Record<string, string> }
> = {
"clawhub-resolve": {
workflow: {
file: ".github/workflows/plugin-clawhub-release.yml",
job: "preview_plugins_clawhub",
step: "Resolve checked-out ref",
},
env: { RELEASE_TAG: "", TARGET_REF: "" },
},
"clawhub-oidc": {
workflow: {
file: ".github/workflows/plugin-clawhub-release.yml",
job: "preview_plugins_clawhub",
step: "Validate OIDC source matches workflow ref",
},
env: {
DRY_RUN: "false",
RELEASE_PUBLISH_RUN_ATTEMPT: "",
RELEASE_PUBLISH_RUN_ID: "",
RELEASE_TAG: "",
TARGET_SHA: sha,
WORKFLOW_REF: "refs/heads/main",
WORKFLOW_SHA: sha,
},
},
"clawhub-trust": {
workflow: {
file: ".github/workflows/plugin-clawhub-release.yml",
job: "preview_plugins_clawhub",
step: "Validate ref is on a trusted publish branch",
},
env: { TRUSTED_PUBLISH_BRANCH: "main" },
},
"npm-resolve": {
workflow: {
file: ".github/workflows/plugin-npm-release.yml",
job: "preview_plugins_npm",
step: "Resolve checked-out ref",
},
env: {},
},
"npm-trust": {
workflow: {
file: ".github/workflows/plugin-npm-release.yml",
job: "preview_plugins_npm",
step: "Validate ref is on a trusted publish branch",
},
env: {
RELEASE_CANDIDATE_BRANCH: "",
NPM_DIST_TAG: "default",
PREFLIGHT_ONLY: "false",
// Ancestry cases isolate Git policy; tag admission is asserted with it enabled.
REQUIRE_NPM_PUBLISH_ENVIRONMENT: "false",
PREPARED_ARTIFACT: "",
PUBLISH_SCOPE: "selected",
RELEASE_PLUGINS: "",
RELEASE_PUBLISH_RUN_ATTEMPT: "",
RELEASE_PUBLISH_RUN_ID: "",
SOURCE_REF: sha,
TRUSTED_PUBLISHER_PREFLIGHT: "false",
WORKFLOW_REF: "refs/heads/main",
WORKFLOW_SHA: workflowSha,
},
},
"npm-preflight-read": {
workflow: {
file: ".github/workflows/plugin-npm-release.yml",
job: "verify_plugin_npm_preflight",
step: "Read exact npm preflight source package",
},
env: { EXTENSION_ID: "fixture", PACKAGE_DIR: packageDir, SOURCE_SHA: sha },
},
"npm-publish-read": {
workflow: {
file: ".github/workflows/plugin-npm-release.yml",
job: "publish_plugins_npm",
step: "Read exact npm publication source package",
},
env: { EXTENSION_ID: "fixture", PACKAGE_DIR: packageDir, TARGET_SHA: sha },
},
};
function pluginRun(mode: PluginMode, options: RunOptions = {}) {
const selected = modes[mode];
return runCiGitStep({
workflow: selected.workflow,
fetchResults: [],
...options,
env: { ...selected.env, ...options.env },
revisions: { HEAD: sha, ...options.revisions },
});
}
function gitCommands(report: Awaited<ReturnType<typeof pluginRun>>) {
return report.commands.filter(({ tool }) => tool === "git").map(({ args }) => args);
}
posixIt.each([
{
mode: "clawhub-resolve" as const,
commands: [
[
"fetch",
"--no-tags",
"origin",
"+refs/heads/main:refs/remotes/origin/main",
"+refs/heads/release/*:refs/remotes/origin/release/*",
],
["rev-parse", "HEAD"],
],
output: `sha=${sha}\n`,
},
{
mode: "npm-resolve" as const,
commands: [["rev-parse", "HEAD"]],
output: `sha=${sha}\n`,
},
{
mode: "clawhub-trust" as const,
commands: [["merge-base", "--is-ancestor", "HEAD", "origin/main"]],
output: "",
},
{
mode: "npm-trust" as const,
commands: [
[
"fetch",
"--no-tags",
"origin",
"+refs/heads/main:refs/remotes/origin/main",
"+refs/heads/release/*:refs/remotes/origin/release/*",
],
["merge-base", "--is-ancestor", "HEAD", "origin/main"],
],
output: "",
},
])(
"$mode drains every Git tree before success or output",
async ({ commands, mode, output }) => {
const report = await pluginRun(mode);
expect(report.code, report.output).toBe(0);
expect(gitCommands(report)).toEqual(commands);
expect(report.githubOutput).toBe(output);
expect(report.readyAttempts).toHaveLength(commands.length);
if (output) {
expect(report.boundaries.some(({ name }) => name === "output")).toBe(true);
}
},
55_000,
);
posixIt.each(["npm-preflight-read", "npm-publish-read"] as const)(
"%s preserves exact source package bytes before the next consumer",
async (mode) => {
const sourceRef = mode === "npm-preflight-read" ? "SOURCE_SHA" : "TARGET_SHA";
const report = await pluginRun(mode, {
commandResults: {
[`show ${sha}:${packageDir}/package.json`]: { code: 0, output: packageJson },
},
});
expect(report.code, report.output).toBe(0);
expect(gitCommands(report)).toEqual([
["fetch", "--no-tags", "--depth=1", "--filter=blob:none", "origin", sha],
["show", `${sha}:${packageDir}/package.json`],
]);
expect(report.pluginSourcePackage).toBe(packageJson);
expect(modes[mode].env[sourceRef]).toBe(sha);
},
55_000,
);
posixIt.each(["npm-preflight-read", "npm-publish-read"] as const)(
"%s rejects partial source package output after ordinary show failure",
async (mode) => {
const report = await pluginRun(mode, {
commandResults: {
[`show ${sha}:${packageDir}/package.json`]: { code: 23, output: "{partial" },
},
});
expect(report.code, report.output).toBe(23);
expect(gitCommands(report).at(-1)?.[0]).toBe("show");
expect(report.pluginSourcePackage).toBe("");
},
55_000,
);
posixIt.each(
(["npm-preflight-read", "npm-publish-read"] as const).flatMap((mode) =>
([23, 125, "hang"] as const).map((failure) => ({ failure, mode })),
),
)(
"$mode fetch failure $failure stops before source package readback",
async ({ failure, mode }) => {
const report = await pluginRun(mode, { fetchResults: [failure] });
expect(report.code, report.output).toBe(failure === "hang" ? 124 : failure);
expect(gitCommands(report).at(-1)?.[0]).toBe("fetch");
expect(report.pluginSourcePackage).toBe("");
},
55_000,
);
posixIt.each([1, 125, 143])(
"ClawHub resolves origin fallback after safely drained ordinary local probe failure %s",
async (code) => {
const report = await pluginRun("clawhub-resolve", {
env: { TARGET_REF: "release/fixture" },
commandResults: {
"rev-parse --verify --quiet release/fixture^{commit}": { code, output: "" },
},
revisions: { "origin/release/fixture^{commit}": sha },
});
expect(report.code, report.output).toBe(0);
expect(gitCommands(report)).toEqual([
[
"fetch",
"--no-tags",
"origin",
"+refs/heads/main:refs/remotes/origin/main",
"+refs/heads/release/*:refs/remotes/origin/release/*",
],
["rev-parse", "--verify", "--quiet", "release/fixture^{commit}"],
["rev-parse", "--verify", "--quiet", "origin/release/fixture^{commit}"],
["rev-parse", "origin/release/fixture^{commit}"],
["checkout", "--detach", sha],
["rev-parse", "HEAD"],
]);
expect(report.githubOutput).toBe(`sha=${sha}\n`);
},
55_000,
);
posixIt(
"ClawHub release tags retain their second bounded fetch",
async () => {
const report = await pluginRun("clawhub-resolve", {
env: { RELEASE_TAG: releaseTag },
});
expect(report.code, report.output).toBe(0);
expect(report.fetches.map(({ args }) => args)).toEqual([
[
"fetch",
"--no-tags",
"origin",
"+refs/heads/main:refs/remotes/origin/main",
"+refs/heads/release/*:refs/remotes/origin/release/*",
],
["fetch", "--no-tags", "origin", `+refs/tags/${releaseTag}:refs/tags/${releaseTag}`],
]);
},
55_000,
);
posixIt(
"ClawHub protected tooling validates the exact peeled release target",
async () => {
const report = await pluginRun("clawhub-oidc", {
env: {
RELEASE_PUBLISH_RUN_ATTEMPT: "2",
RELEASE_PUBLISH_RUN_ID: "123",
RELEASE_TAG: releaseTag,
TARGET_SHA: sha,
WORKFLOW_REF: "refs/tags/release-publish/abcdef123456-2",
WORKFLOW_SHA: workflowSha,
},
revisions: { [`${releaseTag}^{commit}`]: sha },
});
expect(report.code, report.output).toBe(0);
expect(gitCommands(report)).toEqual([["rev-parse", `${releaseTag}^{commit}`]]);
expect(report.output).toContain(
"Protected release tooling is publishing the exact immutable release tag target.",
);
},
55_000,
);
posixIt.each([1, 125])(
"ClawHub protected tag ordinary lookup failure %s retains OIDC rejection",
async (code) => {
const report = await pluginRun("clawhub-oidc", {
env: {
RELEASE_PUBLISH_RUN_ATTEMPT: "2",
RELEASE_PUBLISH_RUN_ID: "123",
RELEASE_TAG: releaseTag,
TARGET_SHA: sha,
WORKFLOW_REF: "refs/tags/release-publish/abcdef123456-2",
WORKFLOW_SHA: workflowSha,
},
commandResults: { [`rev-parse ${releaseTag}^{commit}`]: { code } },
});
expect(report.code, report.output).toBe(1);
expect(report.output).toContain(
"Plugin ClawHub OIDC publish target is not bound to protected tooling and the exact release tag.",
);
},
55_000,
);
posixIt.each(["clawhub-trust", "npm-trust"] as const)(
"%s accepts a release branch only after successful enumeration",
async (mode) => {
const report = await pluginRun(mode, {
commandResults: {
"merge-base --is-ancestor HEAD origin/main": { code: 1 },
"for-each-ref --format=%(refname) refs/remotes/origin/release": {
code: 0,
output: "refs/remotes/origin/release/2026.8.1\n",
},
"merge-base --is-ancestor HEAD refs/remotes/origin/release/2026.8.1": { code: 0 },
},
});
expect(report.code, report.output).toBe(0);
expect(gitCommands(report).at(-1)).toEqual([
"merge-base",
"--is-ancestor",
"HEAD",
"refs/remotes/origin/release/2026.8.1",
]);
},
55_000,
);
posixIt(
"npm-trust rejects a Tideclaw alpha publish after main and release misses",
async () => {
const report = await pluginRun("npm-trust", {
env: { WORKFLOW_REF: `refs/heads/${alphaBranch}` },
commandResults: {
"merge-base --is-ancestor HEAD origin/main": { code: 1 },
"for-each-ref --format=%(refname) refs/remotes/origin/release": { code: 0, output: "" },
},
});
expect(report.code, report.output).toBe(1);
expect(report.output).toContain(
"Plugin npm publishes must target a commit reachable from main or release/*.",
);
expect(report.fetches.some(({ args }) => args.join(" ").includes(alphaBranch))).toBe(false);
},
55_000,
);
posixIt(
"clawhub-trust accepts only the matching Tideclaw alpha branch after main and release misses",
async () => {
const report = await pluginRun("clawhub-trust", {
env: { TRUSTED_PUBLISH_BRANCH: alphaBranch },
commandResults: {
"merge-base --is-ancestor HEAD origin/main": { code: 1 },
"for-each-ref --format=%(refname) refs/remotes/origin/release": { code: 0, output: "" },
[`merge-base --is-ancestor HEAD refs/remotes/origin/${alphaBranch}`]: { code: 0 },
},
});
expect(report.code, report.output).toBe(0);
expect(report.fetches.at(-1)?.args).toEqual([
"fetch",
"--no-tags",
"origin",
`+refs/heads/${alphaBranch}:refs/remotes/origin/${alphaBranch}`,
]);
},
55_000,
);
posixIt.each(["refs/heads/extended-stable/2026.8.33", "refs/heads/main"])(
"npm extended-stable preflight retains exact-tip admission from %s",
async (workflowRef) => {
const branch = "extended-stable/2026.8.33";
const report = await pluginRun("npm-trust", {
env: {
PREFLIGHT_ONLY: "true",
NPM_DIST_TAG: "extended-stable",
PUBLISH_SCOPE: "all-publishable",
SOURCE_REF: sha,
WORKFLOW_REF: workflowRef,
},
revisions: {
[`${sha}^{commit}`]: sha,
[`refs/heads/${branch}`]: sha,
[`refs/remotes/origin/${branch}`]: sha,
},
});
expect(report.code, report.output).toBe(0);
expect(report.fetches.map(({ args }) => args)).toEqual([
["fetch", "--no-tags", "origin", `+refs/heads/${branch}:refs/remotes/origin/${branch}`],
]);
// Preflight adds its exact-source check before the extended-stable tip check.
expect(gitCommands(report).filter(([operation]) => operation === "rev-parse")).toHaveLength(6);
},
55_000,
);
const candidateAdmissionCases: Array<{
name: string;
env: Record<string, string>;
commands: RunOptions["commandResults"];
code: number;
message: string;
}> = [
{ name: "qualified protected tooling", env: {}, commands: {}, code: 0, message: "" },
{
name: "wrong candidate month",
env: { RELEASE_CANDIDATE_BRANCH: "extended-stable/2026.7.33" },
commands: {},
code: 1,
message: "release_candidate_branch must be extended-stable/2026.8.33",
},
{
name: "mutable main tooling",
env: { WORKFLOW_REF: "refs/heads/main" },
commands: {},
code: 1,
message: "require a protected release-publish/<sha12>-<n> tooling tag",
},
{
name: "tooling outside main",
env: {},
commands: { [`merge-base --is-ancestor ${workflowSha} origin/main`]: { code: 1 } },
code: 1,
message: "workflow revision is not reachable from current main",
},
{
name: "candidate outside monthly branch",
env: {},
commands: {
"merge-base --is-ancestor HEAD refs/remotes/origin/extended-stable/2026.8.33": { code: 1 },
},
code: 1,
message: "target must be reachable from extended-stable/2026.8.33",
},
{
name: "tooling ancestry Git failure",
env: {},
commands: { [`merge-base --is-ancestor ${workflowSha} origin/main`]: { code: 23 } },
code: 23,
message: "",
},
{
name: "preflight candidate override",
env: { PREFLIGHT_ONLY: "true", REQUIRE_NPM_PUBLISH_ENVIRONMENT: "false" },
commands: {},
code: 1,
message: "preflight must not include release_candidate_branch",
},
{
name: "non-extended candidate override",
env: { NPM_DIST_TAG: "default" },
commands: {},
code: 1,
message: "release_candidate_branch is only valid for extended-stable publication",
},
];
posixIt.each(candidateAdmissionCases)(
"npm canonical candidate admission: $name",
async ({ env, commands, code, message }) => {
const report = await pluginRun("npm-trust", {
env: {
NPM_DIST_TAG: "extended-stable",
PUBLISH_SCOPE: "all-publishable",
RELEASE_CANDIDATE_BRANCH: "extended-stable/2026.8.33",
REQUIRE_NPM_PUBLISH_ENVIRONMENT: "true",
WORKFLOW_REF: `refs/tags/release-publish/${workflowSha.slice(0, 12)}-123`,
...env,
},
revisions: { [`${sha}^{commit}`]: sha },
commandResults: commands,
});
expect(report.code, report.output).toBe(code);
if (message) {
expect(report.output).toContain(message);
}
if (code === 0) {
expect(gitCommands(report).slice(-2)).toEqual([
["merge-base", "--is-ancestor", workflowSha, "origin/main"],
["merge-base", "--is-ancestor", "HEAD", "refs/remotes/origin/extended-stable/2026.8.33"],
]);
}
},
55_000,
);
posixIt.each([
["moved canonical tip", "refs/heads/main", "c".repeat(40)],
["untrusted workflow branch", "refs/heads/topic", sha],
["same-name main tag", "refs/tags/main", sha],
])(
"npm extended-stable preflight rejects %s",
async (_name, workflowRef, branchSha) => {
const branch = "extended-stable/2026.8.33";
const report = await pluginRun("npm-trust", {
env: {
PREFLIGHT_ONLY: "true",
NPM_DIST_TAG: "extended-stable",
PUBLISH_SCOPE: "all-publishable",
SOURCE_REF: sha,
WORKFLOW_REF: workflowRef,
},
revisions: {
[`${sha}^{commit}`]: sha,
[`refs/remotes/origin/${branch}`]: branchSha,
},
});
expect(report.code, report.output).toBe(1);
expect(report.output).toContain("Extended-stable plugin");
},
55_000,
);
posixIt(
"npm preflight rejects before Tideclaw fallback after main and release misses",
async () => {
const report = await pluginRun("npm-trust", {
env: { PREFLIGHT_ONLY: "true", SOURCE_REF: sha, WORKFLOW_REF: `refs/heads/${alphaBranch}` },
revisions: { [`${sha}^{commit}`]: sha },
commandResults: {
"merge-base --is-ancestor HEAD origin/main": { code: 1 },
"for-each-ref --format=%(refname) refs/remotes/origin/release": { code: 0, output: "" },
},
});
expect(report.code, report.output).toBe(1);
expect(report.output).toContain(
"Plugin npm preflight target must be reachable from main or release/*.",
);
expect(report.fetches).toHaveLength(1);
expect(report.output).not.toContain("matching Tideclaw alpha branch");
},
55_000,
);
posixIt.each(["clawhub-trust", "npm-trust"] as const)(
"%s treats merge-base errors other than ordinary 1 as terminal",
async (mode) => {
const report = await pluginRun(mode, {
commandResults: { "merge-base --is-ancestor HEAD origin/main": { code: 23 } },
});
expect(report.code, report.output).toBe(23);
expect(gitCommands(report)).toHaveLength(mode === "npm-trust" ? 2 : 1);
expect(report.fetches).toHaveLength(mode === "npm-trust" ? 1 : 0);
},
55_000,
);
posixIt.each(["clawhub-trust", "npm-trust"] as const)(
"%s treats release-ref enumeration failure as terminal",
async (mode) => {
const report = await pluginRun(mode, {
commandResults: {
"merge-base --is-ancestor HEAD origin/main": { code: 1 },
"for-each-ref --format=%(refname) refs/remotes/origin/release": { code: 23 },
},
});
expect(report.code, report.output).toBe(23);
expect(gitCommands(report).at(-1)?.[0]).toBe("for-each-ref");
expect(report.fetches).toHaveLength(mode === "npm-trust" ? 1 : 0);
},
55_000,
);
const terminalCases: Array<{
commandResults?: RunOptions["commandResults"];
env?: Record<string, string>;
match: string;
mode: PluginMode;
operation: string;
revisions?: Record<string, string>;
}> = [
{ mode: "clawhub-resolve" as const, operation: "fetch", match: "^fetch " },
{
mode: "clawhub-resolve" as const,
operation: "rev-parse",
match: "^rev-parse --verify --quiet ",
env: { TARGET_REF: "release/fixture" },
revisions: { "origin/release/fixture^{commit}": sha },
},
{
mode: "clawhub-resolve" as const,
operation: "checkout",
match: "^checkout ",
env: { TARGET_REF: sha },
revisions: { [`${sha}^{commit}`]: sha },
},
{
mode: "clawhub-oidc" as const,
operation: "rev-parse",
match: "^rev-parse ",
env: {
RELEASE_PUBLISH_RUN_ATTEMPT: "1",
RELEASE_PUBLISH_RUN_ID: "1",
RELEASE_TAG: releaseTag,
TARGET_SHA: sha,
WORKFLOW_REF: "refs/tags/release-publish/abcdef123456-1",
WORKFLOW_SHA: workflowSha,
},
},
{
mode: "clawhub-trust" as const,
operation: "merge-base",
match: "^merge-base ",
},
{
mode: "clawhub-trust" as const,
operation: "for-each-ref",
match: "^for-each-ref ",
commandResults: { "merge-base --is-ancestor HEAD origin/main": { code: 1 } },
},
{ mode: "npm-resolve" as const, operation: "rev-parse", match: "^rev-parse " },
{ mode: "npm-trust" as const, operation: "fetch", match: "^fetch " },
{
mode: "npm-trust" as const,
operation: "merge-base",
match: "^merge-base ",
},
{
mode: "npm-trust" as const,
operation: "for-each-ref",
match: "^for-each-ref ",
commandResults: { "merge-base --is-ancestor HEAD origin/main": { code: 1 } },
},
{ mode: "npm-preflight-read" as const, operation: "fetch", match: "^fetch " },
{
mode: "npm-preflight-read" as const,
operation: "show",
match: "^show ",
commandResults: {
[`show ${sha}:${packageDir}/package.json`]: { code: 0, output: packageJson },
},
},
{ mode: "npm-publish-read" as const, operation: "fetch", match: "^fetch " },
{
mode: "npm-publish-read" as const,
operation: "show",
match: "^show ",
commandResults: {
[`show ${sha}:${packageDir}/package.json`]: { code: 0, output: packageJson },
},
},
];
posixIt.each(
terminalCases.flatMap((entry) =>
(["cleanup-failure", "cancel"] as const).map((failure) =>
Object.assign({}, entry, { failure }),
),
),
)(
"$mode $operation $failure fences every later Git/output/consumer boundary",
async ({ commandResults, env, failure, match, mode, operation, revisions }) => {
const report = await pluginRun(mode, {
commandResults,
env,
revisions,
gitFault: { match, code: failure },
});
expect(report.code, report.output).toBe(failure === "cancel" ? 143 : 125);
expect(gitCommands(report).at(-1)?.[0]).toBe(operation);
expect(
gitCommands(report).filter((args) => new RegExp(match).test(args.join(" "))),
).toHaveLength(1);
expect(report.githubOutput).toBe("");
expect(report.commands.some(({ tool }) => ["node", "pnpm"].includes(tool))).toBe(false);
},
55_000,
);
posixIt.each(
(["clawhub-resolve", "npm-resolve", "npm-preflight-read", "npm-publish-read"] as const).flatMap(
(mode) => (["owner", "python", "git"] as const).map((setupFailure) => ({ mode, setupFailure })),
),
)(
"$mode setup failure $setupFailure cannot publish or consume Git output",
async ({ mode, setupFailure }) => {
const report = await pluginRun(mode, { setupFailure });
expect(report.code, report.output).not.toBe(0);
expect(report.githubOutput).toBe("");
expect(report.commands.some(({ tool }) => ["node", "pnpm"].includes(tool))).toBe(false);
},
55_000,
);