Files
openclaw/test/scripts/ios-periphery-comment-workflow.test.ts
Sash Zats 233b48daaa refactor: prune unused iOS code (#91996)
Prune unused iOS surfaces and regenerate the Xcode project. Add a scoped Periphery PR gate with hardened artifact handling and stale-status cleanup.

Co-authored-by: Sash Zats <sash@zats.io>
2026-06-15 02:07:15 -07:00

882 lines
22 KiB
TypeScript

import { Buffer } from "node:buffer";
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { compileFunction } from "node:vm";
import { describe, expect, it } from "vitest";
import { parse } from "yaml";
import { markdownToIR } from "../../packages/markdown-core/src/ir.js";
const WORKFLOW_PATH = ".github/workflows/ios-periphery-comment.yml";
const PRODUCER_WORKFLOW_PATH = ".github/workflows/ios-periphery.yml";
const ARTIFACT_NAME = "ios-periphery-dead-code-12345-2";
type WorkflowStep = {
id?: string;
name?: string;
with?: {
name?: string;
script?: string;
};
};
type Workflow = {
jobs?: {
comment?: {
steps?: WorkflowStep[];
};
};
};
type ProducerWorkflow = {
on?: {
pull_request?: {
paths?: string[];
types?: string[];
};
};
jobs?: {
scope?: {
steps?: WorkflowStep[];
};
scan?: {
steps?: WorkflowStep[];
};
};
};
type Artifact = {
expired: boolean;
id: number;
name: string;
size_in_bytes?: number;
};
type ExistingComment = {
body?: string;
id: number;
user?: {
login?: string;
type?: string;
};
};
type WorkflowRun = {
head_sha: string;
id: number;
pull_requests?: Array<{ number: number }>;
run_attempt: number;
run_number: number;
workflow_id: number;
};
function commenterScript(): string {
const workflow = parse(readFileSync(WORKFLOW_PATH, "utf8")) as Workflow;
const step = workflow.jobs?.comment?.steps?.find(
(candidate) => candidate.name === "Upsert Periphery PR comment",
);
const script = step?.with?.script;
if (!script) {
throw new Error("missing iOS Periphery commenter script");
}
return script;
}
function scopeScript(): string {
const workflow = parse(readFileSync(PRODUCER_WORKFLOW_PATH, "utf8")) as ProducerWorkflow;
const step = workflow.jobs?.scope?.steps?.find((candidate) => candidate.id === "scope");
const script = step?.with?.script;
if (!script) {
throw new Error("missing iOS Periphery scope script");
}
return script;
}
async function runScope(options: {
draft?: boolean;
eventName?: string;
files?: Array<string | { filename: string; previous_filename?: string }>;
}): Promise<string | undefined> {
const outputs = new Map<string, string>();
const core = {
setOutput(name: string, value: string) {
outputs.set(name, value);
},
};
const context = {
eventName: options.eventName ?? "pull_request",
payload: {
pull_request: {
draft: options.draft ?? false,
number: 123,
},
},
repo: {
owner: "openclaw",
repo: "openclaw",
},
};
const github = {
rest: {
pulls: {
listFiles() {},
},
},
async paginate() {
return (options.files ?? []).map((file) =>
typeof file === "string" ? { filename: file } : file,
);
},
};
const execute = compileFunction(`return (async () => {\n${scopeScript()}\n})();`, [
"context",
"core",
"github",
]) as (context: typeof context, core: typeof core, github: typeof github) => Promise<void>;
await execute(context, core, github);
return outputs.get("should-scan");
}
async function runCommenter(
artifact: Artifact,
archiveData: Buffer,
options: {
existingComments?: ExistingComment[];
liveHeadSha?: string;
liveHeadShaAfter?: string;
runHeadSha?: string;
runAttempt?: number;
scanJobConclusion?: string;
scopeJobConclusion?: string;
workflowRuns?: WorkflowRun[];
} = {},
) {
const script = commenterScript();
const core = {
infos: [] as string[],
warnings: [] as string[],
info(message: string) {
this.infos.push(message);
},
warning(message: string) {
this.warnings.push(message);
},
};
let downloadCount = 0;
let artifactListCount = 0;
let jobListCount = 0;
let pullGetCount = 0;
const createdBodies: string[] = [];
const updatedBodies: string[] = [];
const github = {
rest: {
actions: {
listJobsForWorkflowRun() {},
listWorkflowRunArtifacts() {},
listWorkflowRuns() {},
async downloadArtifact() {
downloadCount += 1;
return { data: archiveData };
},
},
issues: {
listComments() {},
async createComment(params: { body: string }) {
createdBodies.push(params.body);
},
async updateComment(params: { body: string }) {
updatedBodies.push(params.body);
},
},
pulls: {
async get() {
pullGetCount += 1;
return {
data: {
base: { repo: { full_name: "openclaw/openclaw" } },
head: {
sha:
pullGetCount > 1
? (options.liveHeadShaAfter ?? options.liveHeadSha ?? "head-sha")
: (options.liveHeadSha ?? "head-sha"),
},
number: 123,
state: "open",
},
};
},
},
},
async paginate(_request: unknown, params: Record<string, unknown>) {
if (params.run_id === 12345 && params.filter === "latest") {
jobListCount += 1;
return [
{
conclusion: options.scopeJobConclusion ?? "success",
name: "Detect iOS scan scope",
},
{
conclusion: options.scanJobConclusion ?? "success",
name: "Scan iOS dead code",
},
];
}
if (params.run_id === 12345) {
artifactListCount += 1;
return [{ ...artifact, name: artifact.name || ARTIFACT_NAME }];
}
if (params.workflow_id === 999) {
return (
options.workflowRuns ?? [
{
head_sha: options.runHeadSha ?? "head-sha",
id: 12345,
run_attempt: options.runAttempt ?? 2,
run_number: 8,
workflow_id: 999,
},
]
);
}
if (params.issue_number === 123) {
return options.existingComments ?? [];
}
throw new Error(`unexpected paginate call: ${JSON.stringify(params)}`);
},
};
const context = {
payload: {
workflow_run: {
event: "pull_request",
head_sha: options.runHeadSha ?? "head-sha",
id: 12345,
name: "iOS Periphery Dead Code",
pull_requests: [{ number: 123 }],
repository: { full_name: "openclaw/openclaw" },
run_attempt: options.runAttempt ?? 2,
run_number: 8,
workflow_id: 999,
},
},
repo: {
owner: "openclaw",
repo: "openclaw",
},
};
const execute = compileFunction(`return (async () => {\n${script}\n})();`, [
"require",
"context",
"core",
"github",
]) as (
require: NodeJS.Require,
context: typeof context,
core: typeof core,
github: typeof github,
) => Promise<void>;
await execute(createRequire(import.meta.url), context, core, github);
return {
artifactListCount,
core,
createdBodies,
downloadCount,
jobListCount,
pullGetCount,
updatedBodies,
};
}
function expectUnavailableComment(bodies: string[]): void {
expect(bodies).toHaveLength(1);
expect(bodies[0]).toContain("Periphery did not complete or its report could not be safely read.");
}
function crc32(input: Buffer): number {
let crc = 0xffffffff;
for (const byte of input) {
crc ^= byte;
for (let bit = 0; bit < 8; bit += 1) {
crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
}
}
return (crc ^ 0xffffffff) >>> 0;
}
function u16(value: number): Buffer {
const buffer = Buffer.alloc(2);
buffer.writeUInt16LE(value);
return buffer;
}
function u32(value: number): Buffer {
const buffer = Buffer.alloc(4);
buffer.writeUInt32LE(value);
return buffer;
}
function makeZip(files: Record<string, string>): Buffer {
const localParts: Buffer[] = [];
const centralParts: Buffer[] = [];
let offset = 0;
for (const [name, contents] of Object.entries(files)) {
const nameBuffer = Buffer.from(name, "utf8");
const contentsBuffer = Buffer.from(contents, "utf8");
const checksum = crc32(contentsBuffer);
const localHeader = Buffer.concat([
u32(0x04034b50),
u16(20),
u16(0),
u16(0),
u16(0),
u16(0),
u32(checksum),
u32(contentsBuffer.length),
u32(contentsBuffer.length),
u16(nameBuffer.length),
u16(0),
nameBuffer,
]);
localParts.push(localHeader, contentsBuffer);
centralParts.push(
Buffer.concat([
u32(0x02014b50),
u16(20),
u16(20),
u16(0),
u16(0),
u16(0),
u16(0),
u32(checksum),
u32(contentsBuffer.length),
u32(contentsBuffer.length),
u16(nameBuffer.length),
u16(0),
u16(0),
u16(0),
u16(0),
u32((0o100644 << 16) >>> 0),
u32(offset),
nameBuffer,
]),
);
offset += localHeader.length + contentsBuffer.length;
}
const localData = Buffer.concat(localParts);
const centralDirectory = Buffer.concat(centralParts);
const endOfCentralDirectory = Buffer.concat([
u32(0x06054b50),
u16(0),
u16(0),
u16(Object.keys(files).length),
u16(Object.keys(files).length),
u32(centralDirectory.length),
u32(localData.length),
u16(0),
]);
return Buffer.concat([localData, centralDirectory, endOfCentralDirectory]);
}
function markFirstCentralDirectoryEntryEncrypted(archive: Buffer): Buffer {
const result = Buffer.from(archive);
const offset = result.indexOf(Buffer.from([0x50, 0x4b, 0x01, 0x02]));
if (offset < 0) {
throw new Error("missing ZIP central directory entry");
}
result.writeUInt16LE(1, offset + 8);
return result;
}
describe("iOS Periphery comment workflow", () => {
it("parses the workflow YAML and embedded github-script JavaScript", () => {
expect(() => commenterScript()).not.toThrow();
expect(() =>
compileFunction(`return (async () => {\n${commenterScript()}\n})();`, [
"require",
"context",
"core",
"github",
]),
).not.toThrow();
});
it("scopes the report artifact to the workflow attempt", () => {
const workflow = parse(readFileSync(PRODUCER_WORKFLOW_PATH, "utf8")) as ProducerWorkflow;
const upload = workflow.jobs?.scan?.steps?.find(
(step) => step.name === "Upload Periphery report",
);
expect(upload?.with?.name).toBe(
"ios-periphery-dead-code-${{ github.run_id }}-${{ github.run_attempt }}",
);
});
it("runs scope detection for PR transitions that can clear stale findings", () => {
const workflow = parse(readFileSync(PRODUCER_WORKFLOW_PATH, "utf8")) as ProducerWorkflow;
expect(workflow.on?.pull_request?.types).toContain("converted_to_draft");
expect(workflow.on?.pull_request?.paths).toBeUndefined();
expect(() =>
compileFunction(`return (async () => {\n${scopeScript()}\n})();`, [
"context",
"core",
"github",
]),
).not.toThrow();
});
it.each([
{
expected: "false",
files: ["apps/ios/Sources/Test.swift"],
name: "draft pull request",
options: { draft: true },
},
{
expected: "true",
files: ["apps/ios/Sources/Test.swift"],
name: "iOS change",
options: {},
},
{
expected: "false",
files: ["docs/index.md"],
name: "out-of-scope change",
options: {},
},
{
expected: "true",
files: [
{
filename: "docs/Moved.swift",
previous_filename: "apps/ios/Sources/Moved.swift",
},
],
name: "iOS file renamed out of scope",
options: {},
},
{
expected: "true",
files: [],
name: "manual dispatch",
options: { eventName: "workflow_dispatch" },
},
])("sets scope output for $name", async ({ expected, files, options }) => {
await expect(runScope({ ...options, files })).resolves.toBe(expected);
});
it("accepts a valid small Periphery artifact", async () => {
const archive = makeZip({
"periphery.json": "[]\n",
"periphery.status": "0\n",
});
const result = await runCommenter(
{
expired: false,
id: 77,
name: ARTIFACT_NAME,
size_in_bytes: archive.length,
},
archive,
);
expect(result.downloadCount).toBe(1);
expect(result.core.warnings).toEqual([]);
});
it("rejects oversized artifact metadata before download", async () => {
const result = await runCommenter(
{
expired: false,
id: 77,
name: ARTIFACT_NAME,
size_in_bytes: 1024 * 1024 + 1,
},
Buffer.alloc(0),
);
expect(result.downloadCount).toBe(0);
expectUnavailableComment(result.createdBodies);
expect(result.core.warnings).toEqual([
`Skipping ${ARTIFACT_NAME}; compressed artifact size 1048577 exceeds the 1048576 byte limit.`,
]);
});
it("rejects unexpected artifact paths", async () => {
const archive = makeZip({
"../periphery.json": "[]\n",
"periphery.status": "0\n",
});
const result = await runCommenter(
{
expired: false,
id: 77,
name: ARTIFACT_NAME,
size_in_bytes: archive.length,
},
archive,
);
expectUnavailableComment(result.createdBodies);
expect(result.core.warnings).toEqual([
`Skipping ${ARTIFACT_NAME}; unexpected artifact entry ../periphery.json.`,
]);
});
it("rejects encrypted artifact entries", async () => {
const archive = markFirstCentralDirectoryEntryEncrypted(
makeZip({
"periphery.json": "[]\n",
"periphery.status": "0\n",
}),
);
const result = await runCommenter(
{
expired: false,
id: 77,
name: ARTIFACT_NAME,
size_in_bytes: archive.length,
},
archive,
);
expectUnavailableComment(result.createdBodies);
expect(result.core.warnings).toEqual([
`Skipping ${ARTIFACT_NAME}; periphery.json is encrypted.`,
]);
});
it("does not read artifacts from a stale workflow run", async () => {
const result = await runCommenter(
{
expired: false,
id: 77,
name: ARTIFACT_NAME,
size_in_bytes: 1,
},
Buffer.alloc(0),
{
liveHeadSha: "new-head",
runHeadSha: "old-head",
},
);
expect(result.artifactListCount).toBe(0);
expect(result.downloadCount).toBe(0);
});
it("replaces stale findings when the producer intentionally skips a scan", async () => {
const result = await runCommenter(
{
expired: false,
id: 77,
name: "ios-periphery-dead-code-12345-1",
size_in_bytes: 1,
},
Buffer.alloc(0),
{
existingComments: [
{
body: "<!-- openclaw-ios-periphery-dead-code -->\nprevious findings",
id: 99,
user: { login: "github-actions[bot]", type: "Bot" },
},
],
scanJobConclusion: "skipped",
},
);
expect(result.jobListCount).toBe(1);
expect(result.artifactListCount).toBe(0);
expect(result.createdBodies).toEqual([]);
expect(result.updatedBodies).toHaveLength(1);
expect(result.updatedBodies[0]).toContain(
"Periphery scan skipped because the pull request is a draft or no longer touches iOS scan scope.",
);
});
it("does not reuse an artifact from an earlier workflow attempt", async () => {
const result = await runCommenter(
{
expired: false,
id: 77,
name: "ios-periphery-dead-code-12345-1",
size_in_bytes: 1,
},
Buffer.alloc(0),
{
existingComments: [
{
body: "<!-- openclaw-ios-periphery-dead-code -->\nold findings",
id: 99,
user: { login: "github-actions[bot]", type: "Bot" },
},
],
},
);
expect(result.downloadCount).toBe(0);
expect(result.core.warnings).toEqual([`No ${ARTIFACT_NAME} artifact found.`]);
expect(result.updatedBodies).toHaveLength(1);
expect(result.updatedBodies[0]).toContain(
"Periphery did not complete or its report could not be safely read.",
);
});
it("revalidates the PR head before creating a comment", async () => {
const archive = makeZip({
"periphery.json": JSON.stringify([
{
kind: "function",
location: "Sources/Test.swift:12",
name: "unused",
},
]),
"periphery.status": "1\n",
});
const result = await runCommenter(
{
expired: false,
id: 77,
name: ARTIFACT_NAME,
size_in_bytes: archive.length,
},
archive,
{
liveHeadShaAfter: "new-head",
},
);
expect(result.downloadCount).toBe(1);
expect(result.pullGetCount).toBe(2);
expect(result.createdBodies).toEqual([]);
expect(result.updatedBodies).toEqual([]);
});
it("does not publish findings from a superseded workflow attempt", async () => {
const archive = makeZip({
"periphery.json": JSON.stringify([
{
kind: "function",
location: "Sources/Test.swift:12",
name: "unused",
},
]),
"periphery.status": "1\n",
});
const result = await runCommenter(
{
expired: false,
id: 77,
name: "ios-periphery-dead-code-12345-1",
size_in_bytes: archive.length,
},
archive,
{
runAttempt: 1,
workflowRuns: [
{
head_sha: "head-sha",
id: 12345,
run_attempt: 2,
run_number: 8,
workflow_id: 999,
},
],
},
);
expect(result.downloadCount).toBe(1);
expect(result.pullGetCount).toBe(2);
expect(result.createdBodies).toEqual([]);
expect(result.updatedBodies).toEqual([]);
});
it("does not publish findings from a newer run for the same pull request", async () => {
const archive = makeZip({
"periphery.json": JSON.stringify([
{
kind: "function",
location: "Sources/Test.swift:12",
name: "unused",
},
]),
"periphery.status": "1\n",
});
const result = await runCommenter(
{
expired: false,
id: 77,
name: ARTIFACT_NAME,
size_in_bytes: archive.length,
},
archive,
{
workflowRuns: [
{
head_sha: "head-sha",
id: 54321,
pull_requests: [{ number: 123 }],
run_attempt: 1,
run_number: 9,
workflow_id: 999,
},
],
},
);
expect(result.createdBodies).toEqual([]);
expect(result.updatedBodies).toEqual([]);
});
it("does not treat a run for another pull request as superseding", async () => {
const archive = makeZip({
"periphery.json": JSON.stringify([
{
kind: "function",
location: "Sources/Test.swift:12",
name: "unused",
},
]),
"periphery.status": "1\n",
});
const result = await runCommenter(
{
expired: false,
id: 77,
name: ARTIFACT_NAME,
size_in_bytes: archive.length,
},
archive,
{
workflowRuns: [
{
head_sha: "head-sha",
id: 54321,
pull_requests: [{ number: 456 }],
run_attempt: 1,
run_number: 9,
workflow_id: 999,
},
],
},
);
expect(result.createdBodies).toHaveLength(1);
expect(result.updatedBodies).toEqual([]);
});
it("escapes finding text before creating a PR comment", async () => {
const longName = `![click](https://example.invalid)\r\n@octocat|next${"a".repeat(260)}`;
const archive = makeZip({
"periphery.json": JSON.stringify([
{
kind: "<script>*bold*</script>",
location: "Sources/Test.swift:12",
name: longName,
},
]),
"periphery.status": "1\n",
});
const result = await runCommenter(
{
expired: false,
id: 77,
name: ARTIFACT_NAME,
size_in_bytes: archive.length,
},
archive,
);
expect(result.createdBodies).toHaveLength(1);
const body = result.createdBodies[0] ?? "";
const parsed = markdownToIR(body, { linkify: true, tableMode: "bullets" });
expect(body).not.toContain("\r");
expect(parsed.text).toContain("<script>*bold*</script>");
expect(parsed.text).toContain("![click](https://example.invalid) @octocat|next");
expect(parsed.links).toEqual([]);
});
it("treats non-object finding entries as an unreadable report", async () => {
const archive = makeZip({
"periphery.json": "[null]\n",
"periphery.status": "1\n",
});
const result = await runCommenter(
{
expired: false,
id: 77,
name: ARTIFACT_NAME,
size_in_bytes: archive.length,
},
archive,
);
expectUnavailableComment(result.createdBodies);
});
it("bounds the rendered comment after escaping", async () => {
const repeated = "{".repeat(500);
const archive = makeZip({
"periphery.json": JSON.stringify(
Array.from({ length: 50 }, (_, index) => ({
kind: repeated,
location: `${repeated}${index}:${index}`,
name: repeated,
})),
),
"periphery.status": "1\n",
});
const result = await runCommenter(
{
expired: false,
id: 77,
name: ARTIFACT_NAME,
size_in_bytes: archive.length,
},
archive,
);
expect(result.createdBodies).toHaveLength(1);
expect(result.createdBodies[0]?.length).toBeLessThanOrEqual(60_000);
});
it("does not overwrite a marker comment owned by another bot", async () => {
const archive = makeZip({
"periphery.json": JSON.stringify([
{
kind: "function",
location: "Sources/Test.swift:12",
name: "unused",
},
]),
"periphery.status": "1\n",
});
const result = await runCommenter(
{
expired: false,
id: 77,
name: ARTIFACT_NAME,
size_in_bytes: archive.length,
},
archive,
{
existingComments: [
{
body: "<!-- openclaw-ios-periphery-dead-code -->",
id: 99,
user: { login: "another-app[bot]", type: "Bot" },
},
],
},
);
expect(result.updatedBodies).toEqual([]);
expect(result.createdBodies).toHaveLength(1);
});
});