Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -161,13 +161,15 @@ jobs:
git fetch origin "$BASE_SHA" --depth=1 || echo 'Base fetch failed; bridge proof selection will fail open.'
node --experimental-strip-types scripts/apple-ci-impact.ts bridge

- name: Verify clean-installed Simulator snapshot bridge preparation and the fold-helper -Werror gate
- name: Verify the darwin-only native source proofs (snapshot bridge, fold-helper -Werror, recording scripts)
if: github.event_name == 'pull_request' && steps.bridge-impact.outputs.run != 'false'
run: |
pnpm build
pnpm exec vitest run \
packages/platform-apple/src/snapshot-source/native-runtime.test.ts \
packages/platform-apple/src/foldable/fold-helper-cache.test.ts
packages/platform-apple/src/foldable/fold-helper-cache.test.ts \
packages/platform-apple/src/foldable/simulator-hid-native.test.ts \
packages/capture-kit/src/recording/__tests__/recording-scripts.test.ts
pnpm check:package -- --verify-snapshot-bridge-preparation

- name: Upload macOS artifacts
Expand Down
123 changes: 58 additions & 65 deletions packages/capture-kit/src/recording/__tests__/recording-scripts.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { beforeAll, test } from 'vitest';
import { beforeAll, describe, test } from 'vitest';
import assert from 'node:assert/strict';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
Expand All @@ -15,78 +15,71 @@ const recordingScriptsDir = path.resolve(
);
const recordingTestSupportDir = path.resolve(__dirname, '../../../../../test/integration/support');
const SWIFT_TYPECHECK_TIMEOUT_MS = 60_000;
let swiftCompilerPath = 'swiftc';
let swiftSdkPath = '';

async function assertSwiftScriptTypechecks(
scriptPath: string,
extraSourcePaths: string[] = [],
): Promise<void> {
const result = await runCmd(
swiftCompilerPath,
['-sdk', swiftSdkPath, '-typecheck', scriptPath, ...extraSourcePaths],
{
allowFailure: true,
},
);
assert.equal(
result.exitCode,
0,
`${path.basename(scriptPath)} should typecheck\n${result.stderr}`,
);
}
type TypecheckOutcome = { exitCode: number; stderr: string; source: string };

beforeAll(async () => {
if (process.platform !== 'darwin') return;
const [compilerResult, sdkResult] = await Promise.all([
runCmd('xcrun', ['--find', 'swiftc']),
runCmd('xcrun', ['--show-sdk-path', '--sdk', 'macosx']),
]);
swiftCompilerPath = compilerResult.stdout.trim();
swiftSdkPath = sdkResult.stdout.trim();
});
// The three `swiftc -typecheck` invocations run in beforeAll, not in the test bodies: each is a
// real compiler launch, and the unit slow-test gate budgets `packages/**` test cases far below one
// compile (see the budget note in docs/agents/testing.md). The snapshot-bridge and fold-helper
// gates in platform-apple keep their native compiles out of test-case wall time the same way.
// One beforeAll covers all three so the SDK probe and the two shared-source compiles are paid once
// per file rather than once per case.
describe.skipIf(process.platform !== 'darwin')('recording Swift scripts typecheck', () => {
let outcomes: TypecheckOutcome[] = [];

test(
'recording inspect Swift script typechecks',
async (t) => {
if (process.platform !== 'darwin') {
t.skip('Swift recording scripts are only validated on macOS');
}
beforeAll(
async () => {
const [compiler, sdk] = await Promise.all([
runCmd('xcrun', ['--find', 'swiftc']),
runCmd('xcrun', ['--show-sdk-path', '--sdk', 'macosx']),
]);
const swiftCompilerPath = compiler.stdout.trim() || 'swiftc';
const swiftSdkPath = sdk.stdout.trim();
const sharedSupport = path.join(recordingScriptsDir, 'RecordingExportSupport.swift');
const targets: Array<{ source: string; extraSources: string[] }> = [
{ source: path.join(recordingTestSupportDir, 'recording-inspect.swift'), extraSources: [] },
{
source: path.join(recordingScriptsDir, 'recording-overlay.swift'),
extraSources: [sharedSupport],
},
{
source: path.join(recordingScriptsDir, 'recording-frames.swift'),
extraSources: [sharedSupport],
},
];

await assertSwiftScriptTypechecks(
path.join(recordingTestSupportDir, 'recording-inspect.swift'),
);
},
SWIFT_TYPECHECK_TIMEOUT_MS,
);
outcomes = await Promise.all(
targets.map(async ({ source, extraSources }): Promise<TypecheckOutcome> => {
const result = await runCmd(
swiftCompilerPath,
['-sdk', swiftSdkPath, '-typecheck', source, ...extraSources],
{ allowFailure: true, timeoutMs: SWIFT_TYPECHECK_TIMEOUT_MS },
);
return { source, exitCode: result.exitCode, stderr: result.stderr };
}),
);
},
SWIFT_TYPECHECK_TIMEOUT_MS * 3 + 30_000,
);

test(
'recording overlay Swift script typechecks',
async (t) => {
if (process.platform !== 'darwin') {
t.skip('Swift recording scripts are only validated on macOS');
}
test('recording inspect Swift script typechecks', () => {
assertTypechecked(outcomes, 'recording-inspect.swift');
});

await assertSwiftScriptTypechecks(path.join(recordingScriptsDir, 'recording-overlay.swift'), [
path.join(recordingScriptsDir, 'RecordingExportSupport.swift'),
]);
},
SWIFT_TYPECHECK_TIMEOUT_MS,
);
test('recording overlay Swift script typechecks', () => {
assertTypechecked(outcomes, 'recording-overlay.swift');
});

test(
'recording frames Swift script typechecks',
async (t) => {
if (process.platform !== 'darwin') {
t.skip('Swift recording scripts are only validated on macOS');
}
test('recording frames Swift script typechecks', () => {
assertTypechecked(outcomes, 'recording-frames.swift');
});
});

await assertSwiftScriptTypechecks(path.join(recordingScriptsDir, 'recording-frames.swift'), [
path.join(recordingScriptsDir, 'RecordingExportSupport.swift'),
]);
},
SWIFT_TYPECHECK_TIMEOUT_MS,
);
function assertTypechecked(outcomes: readonly TypecheckOutcome[], basename: string): void {
const outcome = outcomes.find((entry) => path.basename(entry.source) === basename);
assert.ok(outcome, `${basename} was never typechecked`);
assert.equal(outcome.exitCode, 0, `${basename} should typecheck\n${outcome.stderr}`);
}

test('recording overlays are explicitly unsupported on non-macOS hosts', () => {
assert.equal(
Expand Down
25 changes: 22 additions & 3 deletions scripts/__tests__/apple-ci-impact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ function cacheInputs(action: string): string[] {
);
}

type WorkflowStep = { run?: string; with?: { gate?: string } };

/**
* Index of the first workflow step matching a predicate, counted across every job in file order,
* or -1. Steps are located by the command they run or the gate they invoke rather than by their
* title, and through the same YAML parser the assertions below already use, so a step renamed for
* scope reasons — or a `run:` block reindented — is not mistaken for a sequencing regression.
*/
function workflowStepIndex(workflow: string, matches: (step: WorkflowStep) => boolean): number {
const doc = parse(workflow) as { jobs?: Record<string, { steps?: WorkflowStep[] }> };
return Object.values(doc.jobs ?? {})
.flatMap((job) => job.steps ?? [])
.findIndex(matches);
}

test('every runner build-cache input triggers the PR XCTest lane', () => {
const action = fs.readFileSync(
path.join(repoRoot, '.github/actions/setup-apple-runner-build/action.yml'),
Expand Down Expand Up @@ -58,10 +73,14 @@ test('the PR workflow applies the impact decision to the XCTest step', () => {
});

test('macOS clean-install proof follows live UI replay', () => {
// Ordered by what each step runs, not by its title: the clean-install proof must not fire
// before the replay that can raise local-network permission UI, and a step renamed for scope
// reasons is not a sequencing regression.
const workflow = fs.readFileSync(path.join(repoRoot, '.github/workflows/macos.yml'), 'utf8');
const replay = workflow.indexOf('- name: Run macOS integration test');
const proof = workflow.indexOf(
'- name: Verify clean-installed Simulator snapshot bridge preparation',
const replay = workflowStepIndex(workflow, (step) => step.with?.gate === 'replay-macos');
const proof = workflowStepIndex(
workflow,
(step) => step.run?.includes('--verify-snapshot-bridge-preparation') ?? false,
);
expect(replay).toBeGreaterThan(-1);
expect(proof).toBeGreaterThan(replay);
Expand Down
Loading