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: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,11 @@ jobs:
${{ runner.os }}-cocoapods-

- name: Install cocoapods
if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true'
# Always run the Gemfile-pinned pod install, even on a Pods cache hit:
# skipping it lets the React Native CLI reinstall pods mid-build with
# the runner image's unpinned system CocoaPods, which can be older
# than the one that generated Podfile.lock.
if: env.turbo_cache_hit != 1
run: |
cd example
bundle install
Expand Down
12 changes: 12 additions & 0 deletions scripts/run-device-contract-job.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
rmSync,
writeFileSync,
} from 'node:fs';
import { homedir } from 'node:os';
import { dirname, resolve } from 'node:path';

const configuration = process.argv[2];
Expand Down Expand Up @@ -99,6 +100,17 @@ let finalStatus = 1;
for (let index = 1; index <= 2; index += 1) {
const attempt = `attempt-${index}`;
clearAttemptEvidence(attempt);
if (index > 1 && configuration.startsWith('ios')) {
// The restored runner artifact can be broken in ways that fail hard (see
// issue #76): rebuild from scratch on retries instead of restoring it.
const appleRunnerArtifact = resolve(
homedir(),
'.agent-device',
'apple-runner'
);
console.log(`Purging ${appleRunnerArtifact} so ${attempt} rebuilds it`);
rmSync(appleRunnerArtifact, { force: true, recursive: true });
}
const startedAt = Date.now();
const resetResult = configuration.startsWith('ios')
? spawnSync(
Expand Down
88 changes: 86 additions & 2 deletions src/__tests__/issue39-device-contract.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { describe, expect, it, jest } from '@jest/globals';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { execFileSync, spawnSync } from 'node:child_process';
import {
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { resolve } from 'node:path';
import { reconcileReorder } from '../semantic';
Expand Down Expand Up @@ -668,6 +674,84 @@ describe('issue 39 portable device contract', () => {
expect(isolatedRunner).toContain('infrastructureFailure ? 75 : 1');
});

it('purges the apple-runner cache before iOS retry attempts only', () => {
const directory = mkdtempSync(resolve(tmpdir(), 'issue39-retry-'));
try {
const home = resolve(directory, 'home');
const appleRunner = resolve(home, '.agent-device', 'apple-runner');
const workspace = resolve(directory, 'workspace');
mkdirSync(resolve(workspace, 'scripts'), { recursive: true });
writeFileSync(
resolve(workspace, 'scripts/reset-device-contract-ios-simulator.mjs'),
'process.exit(0);\n'
);
writeFileSync(
resolve(workspace, 'scripts/run-device-contract-isolated.mjs'),
[
"import { appendFileSync, existsSync } from 'node:fs';",
"import { homedir } from 'node:os';",
"import { resolve } from 'node:path';",
'appendFileSync(',
" 'attempts.log',",
' `${process.env.ISSUE39_MATRIX_ATTEMPT} runner-cached=${existsSync(',
" resolve(homedir(), '.agent-device', 'apple-runner')",
' )}\\n`',
');',
'process.exit(Number(process.env.ISSUE39_STUB_EXIT));',
'',
].join('\n')
);
const runJob = (configuration: string, stubExit: number) => {
mkdirSync(appleRunner, { recursive: true });
rmSync(resolve(workspace, 'attempts.log'), { force: true });
const job = spawnSync(
process.execPath,
[resolve(root, 'scripts/run-device-contract-job.mjs'), configuration],
{
cwd: workspace,
encoding: 'utf8',
env: {
...process.env,
HOME: home,
ISSUE39_STUB_EXIT: String(stubExit),
},
}
);
return {
attempts: readFileSync(resolve(workspace, 'attempts.log'), 'utf8'),
output: `${job.stdout}${job.stderr}`,
status: job.status,
};
};

// Infrastructure failure (exit 75) on iOS: the retry attempt must start
// with no restored runner artifact, and the purge must be visible in the
// attempt log.
const iosRetry = runJob('ios27.native', 75);
expect(iosRetry.status).toBe(75);
expect(iosRetry.attempts).toBe(
'attempt-1 runner-cached=true\nattempt-2 runner-cached=false\n'
);
expect(iosRetry.output).toContain('apple-runner');

// Contract failure on iOS: no retry, so no purge.
const iosContractFailure = runJob('ios27.fallback', 1);
expect(iosContractFailure.status).toBe(1);
expect(iosContractFailure.attempts).toBe(
'attempt-1 runner-cached=true\n'
);

// Android retries never touch the Apple runner artifact.
const androidRetry = runJob('android.fallback', 75);
expect(androidRetry.status).toBe(75);
expect(androidRetry.attempts).toBe(
'attempt-1 runner-cached=true\nattempt-2 runner-cached=true\n'
);
} finally {
rmSync(directory, { force: true, recursive: true });
}
});

it('keeps the physical iOS 27 VoiceOver record tied to the named public contract', () => {
const protocol = read('artifacts/issue-39/PHYSICAL-IOS27-VOICEOVER.md');
const record = read(
Expand Down
Loading