mirror of
https://github.com/openclaw/openclaw.git
synced 2026-09-28 14:12:28 +08:00
* 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
386 lines
13 KiB
TypeScript
386 lines
13 KiB
TypeScript
import { expect, it } from "vitest";
|
|
import { runCiGitStep } from "./ci-git-owner.test-support.js";
|
|
|
|
// These admission jobs run on Ubuntu. The shared ci-platform-checkout suite
|
|
// owns native Windows Job Object proof for the same pinned owner.
|
|
const posixIt = it.skipIf(process.platform === "win32");
|
|
const sha = "a".repeat(40);
|
|
const otherSha = "f".repeat(40);
|
|
const releaseTag = "v2026.8.1";
|
|
|
|
type ReleaseMode = "linux" | "macos" | "placeholder";
|
|
type RunOptions = Partial<Parameters<typeof runCiGitStep>[0]>;
|
|
|
|
const releases: Record<
|
|
ReleaseMode,
|
|
{
|
|
workflow: { file: string; job: string; step: string };
|
|
env: Record<string, string>;
|
|
revisions?: Record<string, string>;
|
|
}
|
|
> = {
|
|
linux: {
|
|
workflow: {
|
|
file: ".github/workflows/linux-app-release.yml",
|
|
job: "validate_release",
|
|
step: "Ensure tag commit is reachable from its release branch",
|
|
},
|
|
env: { RELEASE_TAG: releaseTag, WORKFLOW_SHA: otherSha },
|
|
revisions: { [`refs/tags/${releaseTag}^{commit}`]: sha },
|
|
},
|
|
macos: {
|
|
workflow: {
|
|
file: ".github/workflows/macos-release.yml",
|
|
job: "validate_macos_release_request",
|
|
step: "Validate release tag and package metadata",
|
|
},
|
|
env: { PUBLIC_RELEASE_BRANCH: "main", RELEASE_TAG: releaseTag },
|
|
},
|
|
placeholder: {
|
|
workflow: {
|
|
file: ".github/workflows/npm-placeholder-bootstrap.yml",
|
|
job: "plan",
|
|
step: "Validate trusted workflow and target",
|
|
},
|
|
env: {
|
|
EVENT_SHA: sha,
|
|
SOURCE_REF: sha,
|
|
WORKFLOW_REF: "refs/heads/main",
|
|
WORKFLOW_SHA: sha,
|
|
},
|
|
},
|
|
};
|
|
|
|
function releaseRun(mode: ReleaseMode, options: RunOptions = {}) {
|
|
const release = releases[mode];
|
|
return runCiGitStep({
|
|
workflow: release.workflow,
|
|
fetchResults: [],
|
|
...options,
|
|
env: { ...release.env, ...options.env },
|
|
revisions: { ...release.revisions, ...options.revisions },
|
|
});
|
|
}
|
|
|
|
function gitCommands(report: Awaited<ReturnType<typeof releaseRun>>) {
|
|
return report.commands.filter(({ tool }) => tool === "git").map(({ args }) => args);
|
|
}
|
|
|
|
posixIt.each([releaseTag, `${releaseTag}-2`])(
|
|
"Linux admits a stable tag from its matching release branch: %s",
|
|
async (tag) => {
|
|
const report = await releaseRun("linux", {
|
|
env: { RELEASE_TAG: tag },
|
|
revisions: { [`refs/tags/${tag}^{commit}`]: sha },
|
|
commandResults: {
|
|
[`merge-base --is-ancestor ${sha} origin/main`]: { code: 1 },
|
|
[`merge-base --is-ancestor ${sha} refs/remotes/origin/release/2026.8.1`]: { code: 0 },
|
|
},
|
|
});
|
|
expect(report.code, report.output).toBe(0);
|
|
expect(report.githubOutput).toBe(`tag_sha=${sha}\n`);
|
|
expect(gitCommands(report)).toContainEqual([
|
|
"fetch",
|
|
"--no-tags",
|
|
"origin",
|
|
"+refs/heads/release/2026.8.1:refs/remotes/origin/release/2026.8.1",
|
|
]);
|
|
},
|
|
55_000,
|
|
);
|
|
|
|
posixIt.each([
|
|
{
|
|
mode: "linux" as const,
|
|
commands: [
|
|
["fetch", "--no-tags", "origin", "+refs/heads/main:refs/remotes/origin/main"],
|
|
["merge-base", "--is-ancestor", otherSha, "origin/main"],
|
|
["rev-parse", `refs/tags/${releaseTag}^{commit}`],
|
|
["merge-base", "--is-ancestor", sha, "origin/main"],
|
|
],
|
|
output: `tag_sha=${sha}\n`,
|
|
},
|
|
{
|
|
mode: "macos" as const,
|
|
commands: [
|
|
["rev-parse", "HEAD"],
|
|
["fetch", "--no-tags", "origin", "+refs/heads/main:refs/remotes/origin/main"],
|
|
],
|
|
output: "",
|
|
},
|
|
{
|
|
mode: "placeholder" as const,
|
|
commands: [
|
|
["rev-parse", "HEAD"],
|
|
["fetch", "--no-tags", "origin", "+refs/heads/main:refs/remotes/origin/main"],
|
|
["merge-base", "--is-ancestor", sha, "origin/main"],
|
|
["merge-base", "--is-ancestor", sha, "origin/main"],
|
|
],
|
|
output: `sha=${sha}\n`,
|
|
},
|
|
])(
|
|
"$mode admission drains every Git tree before output or consumer",
|
|
async ({ mode, commands, output }) => {
|
|
const report = await releaseRun(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 (mode === "macos") {
|
|
expect(report.commands.filter(({ tool }) => tool === "pnpm").map(({ args }) => args)).toEqual(
|
|
[["release:openclaw:npm:check"]],
|
|
);
|
|
expect(report.boundaries.some(({ name }) => name === "consumer:pnpm")).toBe(true);
|
|
} else {
|
|
expect(report.boundaries.some(({ name }) => name === "output")).toBe(true);
|
|
}
|
|
},
|
|
55_000,
|
|
);
|
|
|
|
posixIt.each(
|
|
(["linux", "macos", "placeholder"] as const).flatMap((mode) =>
|
|
([23, 125, "hang"] as const).map((failure) => ({ failure, mode })),
|
|
),
|
|
)(
|
|
"$mode fetch failure $failure stops before output or consumer",
|
|
async ({ failure, mode }) => {
|
|
const report = await releaseRun(mode, { fetchResults: [failure] });
|
|
expect(report.code, report.output).toBe(failure === "hang" ? 124 : failure);
|
|
expect(gitCommands(report).at(-1)?.[0]).toBe("fetch");
|
|
expect(report.githubOutput).toBe("");
|
|
expect(report.commands.some(({ tool }) => tool === "pnpm")).toBe(false);
|
|
},
|
|
55_000,
|
|
);
|
|
|
|
posixIt.each(
|
|
(["linux", "macos"] as const).flatMap((mode) =>
|
|
([23, 125] as const).map((code) => ({ code, mode })),
|
|
),
|
|
)(
|
|
"$mode ordinary rev-parse status $code remains terminal",
|
|
async ({ code, mode }) => {
|
|
const report = await releaseRun(mode, {
|
|
gitFault: { match: "^rev-parse ", code },
|
|
});
|
|
expect(report.code, report.output).toBe(code);
|
|
expect(gitCommands(report).at(-1)?.[0]).toBe("rev-parse");
|
|
expect(report.githubOutput).toBe("");
|
|
expect(report.commands.some(({ tool }) => tool === "pnpm")).toBe(false);
|
|
},
|
|
55_000,
|
|
);
|
|
|
|
posixIt(
|
|
"macOS rejects an invalid public branch before any Git command",
|
|
async () => {
|
|
const report = await releaseRun("macos", {
|
|
env: { PUBLIC_RELEASE_BRANCH: "feature/not-a-release" },
|
|
});
|
|
expect(report.code, report.output).toBe(1);
|
|
expect(report.output).toContain(
|
|
"public_release_branch must be main or release/YYYY.M.PATCH, got feature/not-a-release.",
|
|
);
|
|
expect(gitCommands(report)).toEqual([]);
|
|
expect(report.commands.some(({ tool }) => tool === "pnpm")).toBe(false);
|
|
},
|
|
55_000,
|
|
);
|
|
|
|
const terminalOperations = [
|
|
{ mode: "linux" as const, match: "^fetch ", operation: "fetch" },
|
|
{ mode: "linux" as const, match: "^rev-parse ", operation: "rev-parse" },
|
|
{ mode: "linux" as const, match: "^merge-base ", operation: "merge-base" },
|
|
{ mode: "macos" as const, match: "^rev-parse ", operation: "rev-parse" },
|
|
{ mode: "macos" as const, match: "^fetch ", operation: "fetch" },
|
|
{ mode: "placeholder" as const, match: "^rev-parse ", operation: "rev-parse" },
|
|
{ mode: "placeholder" as const, match: "^fetch ", operation: "fetch" },
|
|
{
|
|
mode: "placeholder" as const,
|
|
match: "^merge-base ",
|
|
occurrence: 1,
|
|
operation: "merge-base",
|
|
},
|
|
{
|
|
mode: "placeholder" as const,
|
|
match: "^merge-base ",
|
|
occurrence: 2,
|
|
operation: "merge-base",
|
|
},
|
|
];
|
|
|
|
posixIt.each(
|
|
terminalOperations.flatMap((entry) =>
|
|
(["cleanup-failure", "cancel"] as const).map((failure) =>
|
|
Object.assign({}, entry, { failure }),
|
|
),
|
|
),
|
|
)(
|
|
"$mode $operation $failure is terminal before every later boundary",
|
|
async ({ failure, match, mode, occurrence, operation }) => {
|
|
const report = await releaseRun(mode, {
|
|
gitFault: { match, occurrence, code: failure },
|
|
});
|
|
expect(report.code, report.output).toBe(failure === "cancel" ? 143 : 125);
|
|
expect(gitCommands(report).at(-1)?.[0]).toBe(operation);
|
|
expect(report.githubOutput).toBe("");
|
|
expect(report.commands.some(({ tool }) => tool === "pnpm")).toBe(false);
|
|
expect(report.output).not.toMatch(/not reachable|requires ref to equal/u);
|
|
},
|
|
55_000,
|
|
);
|
|
|
|
posixIt.each([23, 125])(
|
|
"Linux ordinary merge-base status %s is terminal without trying another branch",
|
|
async (code) => {
|
|
const report = await releaseRun("linux", {
|
|
gitFault: { match: "^merge-base ", code },
|
|
});
|
|
expect(report.code, report.output).toBe(code);
|
|
expect(gitCommands(report).at(-1)?.[0]).toBe("merge-base");
|
|
expect(report.githubOutput).toBe("");
|
|
},
|
|
55_000,
|
|
);
|
|
|
|
posixIt(
|
|
"Linux rejects a tag outside main and its matching release branch",
|
|
async () => {
|
|
const report = await releaseRun("linux", {
|
|
commandResults: {
|
|
[`merge-base --is-ancestor ${sha} origin/main`]: { code: 1 },
|
|
[`merge-base --is-ancestor ${sha} refs/remotes/origin/release/2026.8.1`]: { code: 1 },
|
|
},
|
|
});
|
|
expect(report.code, report.output).toBe(1);
|
|
expect(report.output).toContain(
|
|
`Tag ${releaseTag} (${sha}) is not reachable from main or release/2026.8.1.`,
|
|
);
|
|
expect(report.githubOutput).toBe("");
|
|
},
|
|
55_000,
|
|
);
|
|
|
|
posixIt(
|
|
"Linux rejects tooling outside main before inspecting the candidate",
|
|
async () => {
|
|
const report = await releaseRun("linux", {
|
|
commandResults: { [`merge-base --is-ancestor ${otherSha} origin/main`]: { code: 1 } },
|
|
});
|
|
expect(report.code, report.output).toBe(1);
|
|
expect(report.output).toContain("Linux release tooling must be reachable from current main.");
|
|
expect(gitCommands(report).some(([operation]) => operation === "rev-parse")).toBe(false);
|
|
expect(report.githubOutput).toBe("");
|
|
},
|
|
55_000,
|
|
);
|
|
|
|
posixIt.each([128, "cleanup-failure", "cancel"] as const)(
|
|
"Linux matching release branch fetch failure %s cannot admit a stale ref",
|
|
async (failure) => {
|
|
const report = await releaseRun("linux", {
|
|
commandResults: { [`merge-base --is-ancestor ${sha} origin/main`]: { code: 1 } },
|
|
gitFault: { match: "^fetch ", occurrence: 2, code: failure },
|
|
});
|
|
expect(report.code, report.output).toBe(
|
|
failure === "cancel" ? 143 : failure === "cleanup-failure" ? 125 : failure,
|
|
);
|
|
expect(gitCommands(report).at(-1)?.[0]).toBe("fetch");
|
|
expect(report.githubOutput).toBe("");
|
|
},
|
|
55_000,
|
|
);
|
|
|
|
posixIt.each([
|
|
{ occurrence: 1, message: "workflow revision is not reachable" },
|
|
{ occurrence: 2, message: "target must be reachable" },
|
|
])(
|
|
"placeholder ordinary merge-base failure $occurrence keeps its custom rejection",
|
|
async ({ message, occurrence }) => {
|
|
const report = await releaseRun("placeholder", {
|
|
gitFault: { match: "^merge-base ", occurrence, code: 23 },
|
|
});
|
|
expect(report.code, report.output).toBe(1);
|
|
expect(report.output).toContain(message);
|
|
expect(gitCommands(report).filter(([operation]) => operation === "merge-base")).toHaveLength(
|
|
occurrence,
|
|
);
|
|
expect(report.githubOutput).toBe("");
|
|
},
|
|
55_000,
|
|
);
|
|
|
|
posixIt.each([23, 125])(
|
|
"placeholder rev-parse status %s retains exact-SHA rejection",
|
|
async (code) => {
|
|
const report = await releaseRun("placeholder", {
|
|
gitFault: { match: "^rev-parse ", code },
|
|
});
|
|
expect(report.code, report.output).toBe(1);
|
|
expect(report.output).toContain(
|
|
"NPM placeholder publication requires ref to equal the exact main workflow SHA.",
|
|
);
|
|
expect(gitCommands(report)).toHaveLength(1);
|
|
expect(report.githubOutput).toBe("");
|
|
},
|
|
55_000,
|
|
);
|
|
|
|
const placeholderIdentityMismatches: Array<{
|
|
env: Record<string, string>;
|
|
message: string;
|
|
}> = [
|
|
{
|
|
env: { WORKFLOW_REF: "refs/heads/release/2026.8.1" },
|
|
message: "must run from the trusted main workflow",
|
|
},
|
|
{
|
|
env: { EVENT_SHA: otherSha },
|
|
message: "requires ref to equal the exact main workflow SHA",
|
|
},
|
|
];
|
|
|
|
posixIt.each(placeholderIdentityMismatches)(
|
|
"placeholder rejects non-Git identity mismatch before checkout inspection",
|
|
async ({ env, message }) => {
|
|
const report = await releaseRun("placeholder", { env });
|
|
expect(report.code, report.output).toBe(1);
|
|
expect(report.output).toContain(message);
|
|
expect(gitCommands(report)).toEqual([]);
|
|
expect(report.githubOutput).toBe("");
|
|
},
|
|
55_000,
|
|
);
|
|
|
|
posixIt(
|
|
"placeholder rejects a checked-out SHA mismatch before fetch",
|
|
async () => {
|
|
const report = await releaseRun("placeholder", {
|
|
commandResults: { "rev-parse HEAD": { code: 0, output: `${otherSha}\n` } },
|
|
});
|
|
expect(report.code, report.output).toBe(1);
|
|
expect(report.output).toContain(
|
|
"NPM placeholder publication requires ref to equal the exact main workflow SHA.",
|
|
);
|
|
expect(gitCommands(report)).toEqual([["rev-parse", "HEAD"]]);
|
|
expect(report.githubOutput).toBe("");
|
|
},
|
|
55_000,
|
|
);
|
|
|
|
posixIt.each(
|
|
(["linux", "macos", "placeholder"] as const).flatMap((mode) =>
|
|
(["owner", "python", "git"] as const).map((setupFailure) => ({ mode, setupFailure })),
|
|
),
|
|
)(
|
|
"$mode setup failure $setupFailure cannot publish or consume admission",
|
|
async ({ mode, setupFailure }) => {
|
|
const report = await releaseRun(mode, { setupFailure });
|
|
expect(report.code, report.output).not.toBe(0);
|
|
expect(report.githubOutput).toBe("");
|
|
expect(report.commands.some(({ tool }) => tool === "pnpm")).toBe(false);
|
|
},
|
|
55_000,
|
|
);
|