Files
openclaw/test/scripts/xcode-test-logs.test.ts
Peter Steinberger 50ca949040 ci: bound hourly iOS proof and accept coalesced jobs (#158413)
* ci: bound hourly iOS proof and accept coalesced jobs

* test: align native workflow fixtures with hourly tier
2026-09-25 16:24:06 -07:00

141 lines
5.5 KiB
TypeScript

import { spawnSync } from "node:child_process";
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { parse } from "yaml";
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
const helper = path.resolve("scripts/lib/swift-toolchain.sh");
describe.skipIf(process.platform === "win32")("Apple command log spool", () => {
it.each([0, 65])(
"retains complete file output and exit %s with a bounded console tail",
(code) => {
const root = tempDirs.make("openclaw-xcode-test-logs-");
const bin = path.join(root, "tools with spaces");
const log = path.join(root, "results with spaces", "test.log");
const trace = path.join(root, "trace.json");
const args = ["test", "-scheme", "Scheme With Spaces", "-only-testing:Tests/cleanup"];
mkdirSync(bin);
const executable = path.join(bin, "apple-command-fixture");
writeFileSync(
executable,
`#!/usr/bin/env node
const fs = require("node:fs");
const regularFiles = [1, 2].every((fd) => fs.fstatSync(fd).isFile());
fs.writeFileSync(process.env.FIXTURE_TRACE, JSON.stringify({ regularFiles, args: process.argv.slice(2) }));
if (!regularFiles) process.exit(99);
fs.writeSync(1, "STDOUT_START\\n" + "o".repeat(200000) + "\\nSTDOUT_END\\n");
fs.writeSync(2, "STDERR_START\\n" + "e".repeat(200000) + "\\nSTDERR_END\\n");
process.exit(${code});
`,
);
chmodSync(executable, 0o755);
const result = spawnSync(
"/bin/bash",
[
"-euo",
"pipefail",
"-c",
'source "$1"; shift; run_apple_command_logged "$@"',
"fixture",
helper,
log,
executable,
...args,
],
{
encoding: "utf8",
timeout: 10_000,
maxBuffer: 20_000,
env: {
...process.env,
FIXTURE_TRACE: trace,
},
},
);
expect(result.error).toBeUndefined();
expect(result.status, result.stderr).toBe(code);
expect(JSON.parse(readFileSync(trace, "utf8"))).toEqual({ regularFiles: true, args });
expect(readFileSync(log, "utf8")).toBe(
`STDOUT_START\n${"o".repeat(200000)}\nSTDOUT_END\nSTDERR_START\n${"e".repeat(200000)}\nSTDERR_END\n`,
);
expect(Buffer.byteLength(result.stdout + result.stderr)).toBeLessThan(9000);
expect(result.stdout).toContain("STDERR_END");
expect(result.stdout).toContain(`[apple-command] Exit ${code}; full log: ${log}`);
},
);
});
it("routes each iOS simulator test through workflow-owned log capture and retains failure evidence", () => {
const workflow = parse(readFileSync(".github/workflows/ci.yml", "utf8")) as {
jobs: Record<
string,
{ steps: { name: string; run?: string; if?: string; with?: { path?: string } }[] }
>;
};
const job = workflow.jobs["ios-build"];
if (!job) {
throw new Error("The workflow must include the iOS build job");
}
const steps = job.steps;
for (const [name, logPaths] of [
["Run focused iOS voice cleanup simulator tests", ["OpenClawVoiceCleanupTests.log"]],
[
"Run focused iOS lifecycle simulator tests",
["${result_bundle%.xcresult}.log", "OpenClawWatchDeliveryUITests.log"],
],
[
"Run focused Apple Watch operation simulator tests",
["OpenClawWatchBuild.log", "OpenClawWatchOperationTests.log"],
],
] as const) {
const run = steps.find((step) => step.name === name)?.run;
expect(run, name).toContain("source .ci-harness/scripts/lib/swift-toolchain.sh");
expect(run?.match(/\brun_apple_command_logged\b/gu), name).toHaveLength(logPaths.length);
expect(run?.match(/\brun_apple_command_logged [^\n]+ xcodebuild \\/gu), name).toHaveLength(
logPaths.length,
);
for (const logPath of logPaths) {
expect(run, name).toContain(logPath);
}
// The Watch product-path query must keep its JSON output contract.
expect(run?.match(/^\s*xcodebuild /gmu) ?? [], name).toHaveLength(
name.includes("Apple Watch") ? 1 : 0,
);
if (name.includes("Apple Watch")) {
expect(run).toContain("-showBuildSettings -json |");
}
}
const attachments = steps.find(
(step) => step.name === "Prove native managed document download and export",
);
expect(attachments?.run).toBe('/bin/bash scripts/test-ios-chat-attachments.sh "$BASELINE_SHA"');
expect(attachments?.if).toBe(
"matrix.phase == 'tests' && needs.preflight.outputs.validation_tier != 'main' && needs.preflight.outputs.compatibility_target != 'true'",
);
const smoke = steps.find((step) => step.name === "Run focused iOS voice cleanup simulator tests");
expect(smoke?.if).toContain("matrix.phase == 'smoke'");
for (const suite of [
"ManagedDocumentEnvelopeTests",
"IOSMediaArtifactLoaderTests",
"OpenClawTypographyTests",
]) {
expect(smoke?.run).toContain(`-only-testing:OpenClawTests/${suite}`);
}
const upload = steps.find((step) => step.name === "Upload iOS lifecycle simulator evidence");
expect(upload?.if).toContain("always()");
expect(upload?.if).toContain("steps.ios_attachment_tests.outcome");
expect(upload?.with?.path?.trim().split("\n")).toEqual([
"apps/ios/build/LifecycleTestResults/*.xcresult",
"apps/ios/build/LifecycleTestResults/*.log",
"apps/ios/build/LifecycleTestResults/Attachment-*",
"apps/ios/build/LifecycleTestResults/attachments-*",
]);
});