Skip to content

Commit c955abf

Browse files
committed
test(apple): split stale-bundle cleanup coverage out of the pinned runner-session suite
runner-session.test.ts is over the test-size tripwire and its pin may only shrink. Move the three stale-bundle cleanup tests (boot availability, best-effort stall, concurrent start) into a sibling file named for the domain question, carrying the same seam scaffolding.
1 parent e11f93b commit c955abf

2 files changed

Lines changed: 228 additions & 65 deletions

File tree

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import assert from 'node:assert/strict';
2+
import { beforeEach, test, vi } from 'vitest';
3+
import { IOS_SIMULATOR } from './device-fixtures.ts';
4+
import { appleRunnerTestHost } from '../test-host.ts';
5+
import {
6+
makeBackgroundRunner,
7+
makeClassifyOwnerLivenessViaMocks,
8+
runnerResponse,
9+
} from './runner-session-fixtures.ts';
10+
import { mkdtempForTestSync } from './tmp-dir.ts';
11+
12+
const {
13+
mockAcquireXcodebuildSimulatorSetRedirect,
14+
mockCleanupTempFile,
15+
mockEnsureXctestrunArtifact,
16+
mockGetFreePort,
17+
mockIsProcessAlive,
18+
mockIsProcessGroupAlive,
19+
mockPrepareXctestrunWithEnv,
20+
mockReadProcessCommand,
21+
mockReadProcessStartTime,
22+
mockResolveExpectedRunnerCacheMetadata,
23+
mockResolveRunnerDerivedPath,
24+
mockRunAppleToolCommand,
25+
mockRunCmdBackground,
26+
mockRunXcrun,
27+
mockSendRunnerCommandOnce,
28+
mockSignalPidsBestEffort,
29+
mockSignalProcessGroupBestEffort,
30+
mockWaitForRunner,
31+
mockRedirectRelease,
32+
} = vi.hoisted(() => ({
33+
mockAcquireXcodebuildSimulatorSetRedirect: vi.fn(),
34+
mockCleanupTempFile: vi.fn(),
35+
mockEnsureXctestrunArtifact: vi.fn(),
36+
mockGetFreePort: vi.fn(),
37+
mockIsProcessAlive: vi.fn(),
38+
mockIsProcessGroupAlive: vi.fn(),
39+
mockPrepareXctestrunWithEnv: vi.fn(),
40+
// Non-empty default: RUNNER_OWNER_START_TIME below is computed at module
41+
// load (before beforeEach), and readProcessStartTime's real implementation
42+
// shells out to `ps` with a 1s timeout that can miss under CPU contention,
43+
// flipping a live owner to 'owner-process-dead'. Deterministic value, no
44+
// shell-out; identity is still enforced by pid in beforeEach below.
45+
mockReadProcessCommand: vi.fn((_pid: number) => null as string | null),
46+
mockReadProcessStartTime: vi.fn((_pid: number) => 'fixed-test-owner-start-time' as string | null),
47+
mockResolveExpectedRunnerCacheMetadata: vi.fn(),
48+
mockResolveRunnerDerivedPath: vi.fn(),
49+
mockRunAppleToolCommand: vi.fn(),
50+
mockRunCmdBackground: vi.fn(),
51+
mockRunXcrun: vi.fn(),
52+
mockSendRunnerCommandOnce: vi.fn(),
53+
// The runner child pid below is fabricated (4242), so the signal writes are
54+
// mocked next to the liveness reads: a real signal to a made-up pid can hit a
55+
// sibling vitest fork (#1824), and the shared setup refuses it outright.
56+
mockSignalPidsBestEffort: vi.fn(),
57+
mockSignalProcessGroupBestEffort: vi.fn(),
58+
mockWaitForRunner: vi.fn(),
59+
mockRedirectRelease: vi.fn(),
60+
}));
61+
62+
vi.mock('../runner-io.ts', async () => {
63+
const actual = await vi.importActual<typeof import('../runner-io.ts')>('../runner-io.ts');
64+
return {
65+
...actual,
66+
cleanupTempFile: mockCleanupTempFile,
67+
getFreePort: mockGetFreePort,
68+
};
69+
});
70+
71+
vi.mock('../runner-transport.ts', async () => {
72+
const actual = await vi.importActual<typeof import('../runner-transport.ts')>(
73+
'../runner-transport.ts',
74+
);
75+
return {
76+
...actual,
77+
sendRunnerCommandOnce: mockSendRunnerCommandOnce,
78+
};
79+
});
80+
81+
vi.mock('../runner-xctestrun.ts', async () => {
82+
const actual = await vi.importActual<typeof import('../runner-xctestrun.ts')>(
83+
'../runner-xctestrun.ts',
84+
);
85+
return {
86+
...actual,
87+
acquireXcodebuildSimulatorSetRedirect: mockAcquireXcodebuildSimulatorSetRedirect,
88+
ensureXctestrunArtifact: mockEnsureXctestrunArtifact,
89+
prepareXctestrunWithEnv: mockPrepareXctestrunWithEnv,
90+
resolveExpectedRunnerCacheMetadata: mockResolveExpectedRunnerCacheMetadata,
91+
resolveRunnerDerivedPath: mockResolveRunnerDerivedPath,
92+
};
93+
});
94+
95+
vi.mock('../runner-startup-transport.ts', async () => {
96+
const actual = await vi.importActual<typeof import('../runner-startup-transport.ts')>(
97+
'../runner-startup-transport.ts',
98+
);
99+
return { ...actual, waitForRunner: mockWaitForRunner };
100+
});
101+
102+
import { abortAllIosRunnerSessions, ensureRunnerSession } from '../runner-session.ts';
103+
import { IOS_RUNNER_CONTAINER_BUNDLE_IDS } from '../runner-xctestrun.ts';
104+
import { AppError } from '@agent-device/kernel/errors';
105+
106+
const TEST_OWNER_START_TIME = 'fixed-test-owner-start-time';
107+
let leaseOwnerStateDirOverride: string | undefined;
108+
109+
// Split from runner-session.test.ts: that file is over the test-size tripwire
110+
// and may only shrink, so the stale-bundle cleanup family answers its own
111+
// domain question here with the same seam scaffolding.
112+
beforeEach(async () => {
113+
appleRunnerTestHost.update({
114+
runCmdBackground: mockRunCmdBackground,
115+
isProcessAlive: mockIsProcessAlive,
116+
isProcessGroupAlive: mockIsProcessGroupAlive,
117+
readProcessCommand: mockReadProcessCommand,
118+
readProcessStartTime: mockReadProcessStartTime,
119+
signalPidsBestEffort: mockSignalPidsBestEffort,
120+
signalProcessGroupBestEffort: mockSignalProcessGroupBestEffort,
121+
runAppleToolCommand: mockRunAppleToolCommand,
122+
runXcrun: mockRunXcrun,
123+
leaseOwnerStateDir: () => leaseOwnerStateDirOverride,
124+
classifyOwnerLiveness: makeClassifyOwnerLivenessViaMocks({
125+
isProcessAlive: (pid) => Boolean(mockIsProcessAlive(pid)),
126+
readProcessStartTime: (pid) => (mockReadProcessStartTime(pid) as string | null) ?? null,
127+
}),
128+
});
129+
await abortAllIosRunnerSessions();
130+
vi.resetAllMocks();
131+
leaseOwnerStateDirOverride = undefined;
132+
process.env.AGENT_DEVICE_IOS_RUNNER_LEASE_DIR = mkdtempForTestSync(
133+
'agent-device-runner-lease-test-',
134+
);
135+
mockRunXcrun.mockResolvedValue({ exitCode: 0, stdout: '', stderr: '' });
136+
mockEnsureXctestrunArtifact.mockResolvedValue({
137+
xctestrunPath: '/tmp/base-runner.xctestrun',
138+
derived: '/tmp/derived',
139+
cache: 'miss',
140+
artifact: 'rebuilt',
141+
buildMs: 12,
142+
xctestrunPathSource: 'build',
143+
});
144+
mockGetFreePort.mockResolvedValue(8123);
145+
mockPrepareXctestrunWithEnv.mockResolvedValue({
146+
xctestrunPath: '/tmp/session-runner.xctestrun',
147+
jsonPath: '/tmp/session-runner.json',
148+
});
149+
mockResolveExpectedRunnerCacheMetadata.mockReturnValue({ schemaVersion: 1 });
150+
mockResolveRunnerDerivedPath.mockReturnValue('/tmp/derived');
151+
mockAcquireXcodebuildSimulatorSetRedirect.mockResolvedValue({ release: mockRedirectRelease });
152+
mockRunCmdBackground.mockReturnValue(makeBackgroundRunner(4242));
153+
mockRunAppleToolCommand.mockResolvedValue({ exitCode: 0, stdout: '', stderr: '' });
154+
mockIsProcessAlive.mockReturnValue(true);
155+
mockIsProcessGroupAlive.mockReturnValue(false);
156+
mockReadProcessCommand.mockReturnValue(null);
157+
// Our pid reads back its fixed start time; any other pid reads as
158+
// not-found, same as a real `ps` miss.
159+
mockReadProcessStartTime.mockImplementation((pid: number) =>
160+
pid === process.pid ? TEST_OWNER_START_TIME : null,
161+
);
162+
mockWaitForRunner.mockResolvedValue(runnerResponse({ uptimeMs: 1 }));
163+
});
164+
165+
test('runner session keeps boot and stale bundle cleanup available when needed', async () => {
166+
const device = { ...IOS_SIMULATOR, id: 'runner-session-clean-sim', booted: false };
167+
168+
await ensureRunnerSession(device, {
169+
cleanStaleBundles: true,
170+
});
171+
172+
assert.equal(
173+
mockRunXcrun.mock.calls.some((call) => call[0]?.includes('bootstatus')),
174+
true,
175+
);
176+
assert.equal(
177+
mockRunXcrun.mock.calls.some((call) => call[0]?.includes('uninstall')),
178+
true,
179+
);
180+
const uninstallCalls = mockRunXcrun.mock.calls.filter((call) => call[0]?.includes('uninstall'));
181+
assert.equal(
182+
uninstallCalls.every((call) => call[1]?.timeoutMs === 10_000),
183+
true,
184+
);
185+
});
186+
187+
test('runner session stale bundle cleanup is best-effort when simctl stalls', async () => {
188+
const device = { ...IOS_SIMULATOR, id: 'runner-session-clean-timeout-sim' };
189+
190+
mockRunXcrun
191+
.mockRejectedValueOnce(new AppError('COMMAND_FAILED', 'simctl uninstall timed out'))
192+
.mockResolvedValue({ exitCode: 0, stdout: '', stderr: '' });
193+
194+
const session = await ensureRunnerSession(device, {
195+
cleanStaleBundles: true,
196+
});
197+
198+
assert.equal(session.deviceId, device.id);
199+
assert.equal(mockRunCmdBackground.mock.calls.length, 1);
200+
});
201+
202+
test('stale bundle uninstalls start concurrently', async () => {
203+
const device = { ...IOS_SIMULATOR, id: 'runner-session-clean-concurrent-sim' };
204+
const uninstallGates: Array<
205+
(result: { exitCode: number; stdout: string; stderr: string }) => void
206+
> = [];
207+
mockRunXcrun.mockImplementation(async (args: string[]) => {
208+
if (args.includes('uninstall')) {
209+
return await new Promise((resolve) => {
210+
uninstallGates.push(resolve);
211+
});
212+
}
213+
return { exitCode: 0, stdout: '', stderr: '' };
214+
});
215+
216+
const sessionPromise = ensureRunnerSession(device, { cleanStaleBundles: true });
217+
// Both container bundle uninstalls must be in flight before either resolves;
218+
// against the sequential pre-fix loop this wait times out with one gate held.
219+
await vi.waitFor(() =>
220+
assert.equal(uninstallGates.length, IOS_RUNNER_CONTAINER_BUNDLE_IDS.length),
221+
);
222+
for (const resolve of uninstallGates) {
223+
resolve({ exitCode: 0, stdout: '', stderr: '' });
224+
}
225+
const session = await sessionPromise;
226+
227+
assert.equal(session.deviceId, device.id);
228+
});

packages/platform-apple/src/runner/__tests__/runner-session.test.ts

Lines changed: 0 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,6 @@ import {
130130
stopIosRunnerSession,
131131
validateRunnerDevice,
132132
} from '../runner-session.ts';
133-
import { IOS_RUNNER_CONTAINER_BUNDLE_IDS } from '../runner-xctestrun.ts';
134133
import {
135134
cleanupRunnerLeasesForOwner,
136135
prepareRunnerLeaseForStartup,
@@ -1471,70 +1470,6 @@ test('runner session restarts dead runner without graceful shutdown', async () =
14711470
assert.equal(mockRedirectRelease.mock.calls.length, 1);
14721471
});
14731472

1474-
test('runner session keeps boot and stale bundle cleanup available when needed', async () => {
1475-
const device = { ...IOS_SIMULATOR, id: 'runner-session-clean-sim', booted: false };
1476-
1477-
await ensureRunnerSession(device, {
1478-
cleanStaleBundles: true,
1479-
});
1480-
1481-
assert.equal(
1482-
mockRunXcrun.mock.calls.some((call) => call[0]?.includes('bootstatus')),
1483-
true,
1484-
);
1485-
assert.equal(
1486-
mockRunXcrun.mock.calls.some((call) => call[0]?.includes('uninstall')),
1487-
true,
1488-
);
1489-
const uninstallCalls = mockRunXcrun.mock.calls.filter((call) => call[0]?.includes('uninstall'));
1490-
assert.equal(
1491-
uninstallCalls.every((call) => call[1]?.timeoutMs === 10_000),
1492-
true,
1493-
);
1494-
});
1495-
1496-
test('runner session stale bundle cleanup is best-effort when simctl stalls', async () => {
1497-
const device = { ...IOS_SIMULATOR, id: 'runner-session-clean-timeout-sim' };
1498-
1499-
mockRunXcrun
1500-
.mockRejectedValueOnce(new AppError('COMMAND_FAILED', 'simctl uninstall timed out'))
1501-
.mockResolvedValue({ exitCode: 0, stdout: '', stderr: '' });
1502-
1503-
const session = await ensureRunnerSession(device, {
1504-
cleanStaleBundles: true,
1505-
});
1506-
1507-
assert.equal(session.deviceId, device.id);
1508-
assert.equal(mockRunCmdBackground.mock.calls.length, 1);
1509-
});
1510-
1511-
test('stale bundle uninstalls start concurrently', async () => {
1512-
const device = { ...IOS_SIMULATOR, id: 'runner-session-clean-concurrent-sim' };
1513-
const uninstallGates: Array<
1514-
(result: { exitCode: number; stdout: string; stderr: string }) => void
1515-
> = [];
1516-
mockRunXcrun.mockImplementation(async (args: string[]) => {
1517-
if (args.includes('uninstall')) {
1518-
return await new Promise((resolve) => {
1519-
uninstallGates.push(resolve);
1520-
});
1521-
}
1522-
return { exitCode: 0, stdout: '', stderr: '' };
1523-
});
1524-
1525-
const sessionPromise = ensureRunnerSession(device, { cleanStaleBundles: true });
1526-
// Both container bundle uninstalls must be in flight before either resolves.
1527-
await vi.waitFor(() =>
1528-
assert.equal(uninstallGates.length, IOS_RUNNER_CONTAINER_BUNDLE_IDS.length),
1529-
);
1530-
for (const resolve of uninstallGates) {
1531-
resolve({ exitCode: 0, stdout: '', stderr: '' });
1532-
}
1533-
const session = await sessionPromise;
1534-
1535-
assert.equal(session.deviceId, device.id);
1536-
});
1537-
15381473
test('runner session stop kills only owned stale xcodebuild runner processes without in-memory session', async () => {
15391474
const deviceId = '11C70358-8331-4872-A0CA-F15B6859B6FC';
15401475
writeRunnerLease(makeRunnerLease({ deviceId, ownerToken: runnerOwnerToken() }));

0 commit comments

Comments
 (0)