Skip to content

Commit 3fbabc3

Browse files
thymikeeclaude
andcommitted
refactor(capture-kit): take the host explicitly for Swift cache and recording tooling
capture-kit no longer reads the command host, so its suites need no bound host and its local test-host copy is gone; the root recording seams supply the host. test-kit keeps depending on capture-kit again, and capture-kit does not depend on test-kit. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
1 parent f675340 commit 3fbabc3

25 files changed

Lines changed: 204 additions & 133 deletions

‎packages/capture-kit/package.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,6 @@
356356
}
357357
},
358358
"devDependencies": {
359-
"@agent-device/test-kit": "workspace:*",
360359
"@types/pngjs": "^6.0.5",
361360
"fast-check": "^4.9.0",
362361
"jpeg-js": "^0.4.4",
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { captureHostEnvironment } from '@agent-device/host-kit/process';
2+
import type { HostEnvironment } from '@agent-device/kernel/source-value';
3+
4+
/**
5+
* The host a capture-kit test hands to code that takes one explicitly: a snapshot of the process
6+
* as it is when called, so `vi.stubEnv` made earlier in the test reaches it. Test-kit depends on
7+
* capture-kit, so capture-kit keeps this local.
8+
*/
9+
export function createTestHost(): HostEnvironment {
10+
return captureHostEnvironment(process);
11+
}

‎packages/capture-kit/src/index.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export {
77
createAppLogStartResult,
88
decodeAppLogProcessMarker,
99
} from './app-log-runtime.ts';
10-
export { createAppLogLiveHandleFromFinish } from './app-log-live-handle.ts';
10+
export { createAppLogLiveHandle, createAppLogLiveHandleFromFinish } from './app-log-live-handle.ts';
1111
export { createHostAudioProbeCaptureOperations } from './audio-probe-runtime.ts';
1212
export { decodeDurableDescriptor } from './durable-descriptor-codec.ts';
1313
export { createScreenRecordingLiveHandle } from './screen-recording-live-handle.ts';

‎packages/capture-kit/src/recording/__tests__/overlay.test.ts‎

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
2+
import { createTestHost } from '../../host-environment.fixtures.ts';
23
import fs from 'node:fs';
34
import path from 'node:path';
45
import { mkdtempForTestSync } from '../../tmp-dir.fixtures.ts';
5-
import { bindTestCommandHost } from '@agent-device/test-kit';
6-
7-
bindTestCommandHost();
86

97
vi.mock('@agent-device/host-kit/command', async (importOriginal) => {
108
const fs = await import('node:fs');
@@ -66,12 +64,14 @@ test('overlay burns touches through a cached helper and same-directory temp outp
6664
fs.writeFileSync(telemetryPath, '{"events":[]}');
6765

6866
await overlayRecordingTouches({
67+
host: createTestHost(),
6968
videoPath,
7069
telemetryPath,
7170
deadline: startRecordingOverlayBudget(),
7271
});
7372
fs.writeFileSync(videoPath, 'second original');
7473
await overlayRecordingTouches({
74+
host: createTestHost(),
7575
videoPath,
7676
telemetryPath,
7777
deadline: startRecordingOverlayBudget(),
@@ -112,7 +112,12 @@ test('overlay preserves Swift helper compile hints', async () => {
112112
});
113113

114114
await expect(
115-
overlayRecordingTouches({ videoPath, telemetryPath, deadline: startRecordingOverlayBudget() }),
115+
overlayRecordingTouches({
116+
host: createTestHost(),
117+
videoPath,
118+
telemetryPath,
119+
deadline: startRecordingOverlayBudget(),
120+
}),
116121
).rejects.toMatchObject({
117122
code: 'COMMAND_FAILED',
118123
message: 'Failed to add touch overlays to the recording',
@@ -129,6 +134,7 @@ test('overlay defaults to the fast medium export preset', async () => {
129134
fs.writeFileSync(telemetryPath, '{"events":[]}');
130135

131136
await overlayRecordingTouches({
137+
host: createTestHost(),
132138
videoPath,
133139
telemetryPath,
134140
deadline: startRecordingOverlayBudget(),
@@ -146,6 +152,7 @@ test('overlay forwards the requested high export preset', async () => {
146152
fs.writeFileSync(telemetryPath, '{"events":[]}');
147153

148154
await overlayRecordingTouches({
155+
host: createTestHost(),
149156
videoPath,
150157
telemetryPath,
151158
deadline: startRecordingOverlayBudget(),
@@ -164,6 +171,7 @@ test('overlay caps the helper frame rate at the caller fps only when one was ask
164171
fs.writeFileSync(telemetryPath, '{"events":[]}');
165172

166173
await overlayRecordingTouches({
174+
host: createTestHost(),
167175
videoPath,
168176
telemetryPath,
169177
deadline: startRecordingOverlayBudget(),
@@ -172,6 +180,7 @@ test('overlay caps the helper frame rate at the caller fps only when one was ask
172180

173181
mockRunCmd.mockClear();
174182
await overlayRecordingTouches({
183+
host: createTestHost(),
175184
videoPath,
176185
telemetryPath,
177186
deadline: startRecordingOverlayBudget(),
@@ -188,6 +197,7 @@ test('overlay hands the helper what is left of its budget, inside the record req
188197
fs.writeFileSync(telemetryPath, '{"events":[]}');
189198

190199
await overlayRecordingTouches({
200+
host: createTestHost(),
191201
videoPath,
192202
telemetryPath,
193203
deadline: startRecordingOverlayBudget(),
@@ -215,7 +225,7 @@ test('overlay hands the helper only what the finalization left of the budget', a
215225
const deadline = startRecordingOverlayBudget();
216226
vi.setSystemTime(Date.now() + spentMs);
217227

218-
await overlayRecordingTouches({ videoPath, telemetryPath, deadline });
228+
await overlayRecordingTouches({ host: createTestHost(), videoPath, telemetryPath, deadline });
219229

220230
const helperCall = mockRunCmd.mock.calls.find(([cmd]) => cmd !== 'xcrun');
221231
expect(helperCall?.[2]?.timeoutMs).toBe(OVERLAY_BUDGET_MS - spentMs);
@@ -237,7 +247,12 @@ test('overlay leaves the video as recorded when compiling the helper spent its b
237247
});
238248

239249
await expect(
240-
overlayRecordingTouches({ videoPath, telemetryPath, deadline: startRecordingOverlayBudget() }),
250+
overlayRecordingTouches({
251+
host: createTestHost(),
252+
videoPath,
253+
telemetryPath,
254+
deadline: startRecordingOverlayBudget(),
255+
}),
241256
).rejects.toMatchObject({
242257
code: 'COMMAND_FAILED',
243258
message: 'Failed to add touch overlays to the recording',
@@ -254,6 +269,7 @@ test('overlay names its budget when the export ran out of it', async () => {
254269
fs.writeFileSync(telemetryPath, '{"events":[]}');
255270

256271
await overlayRecordingTouches({
272+
host: createTestHost(),
257273
videoPath,
258274
telemetryPath,
259275
deadline: startRecordingOverlayBudget(),
@@ -265,7 +281,12 @@ test('overlay names its budget when the export ran out of it', async () => {
265281
});
266282

267283
await expect(
268-
overlayRecordingTouches({ videoPath, telemetryPath, deadline: startRecordingOverlayBudget() }),
284+
overlayRecordingTouches({
285+
host: createTestHost(),
286+
videoPath,
287+
telemetryPath,
288+
deadline: startRecordingOverlayBudget(),
289+
}),
269290
).rejects.toMatchObject({
270291
code: 'COMMAND_FAILED',
271292
message: `Failed to add touch overlays to the recording: the export did not finish within its ${OVERLAY_BUDGET_MS}ms budget; a lower --fps renders faster`,
@@ -280,6 +301,7 @@ test('overlay keeps the plain failure when the export failed with budget to spar
280301
fs.writeFileSync(telemetryPath, '{"events":[]}');
281302

282303
await overlayRecordingTouches({
304+
host: createTestHost(),
283305
videoPath,
284306
telemetryPath,
285307
deadline: startRecordingOverlayBudget(),
@@ -289,7 +311,12 @@ test('overlay keeps the plain failure when the export failed with budget to spar
289311
});
290312

291313
await expect(
292-
overlayRecordingTouches({ videoPath, telemetryPath, deadline: startRecordingOverlayBudget() }),
314+
overlayRecordingTouches({
315+
host: createTestHost(),
316+
videoPath,
317+
telemetryPath,
318+
deadline: startRecordingOverlayBudget(),
319+
}),
293320
).rejects.toMatchObject({
294321
message: 'Failed to add touch overlays to the recording',
295322
});
@@ -307,12 +334,15 @@ test('overlay keeps the video as recorded when verifying the export spent its bu
307334
});
308335

309336
await expect(
310-
overlayRecordingTouches({ videoPath, telemetryPath, deadline }),
337+
overlayRecordingTouches({ host: createTestHost(), videoPath, telemetryPath, deadline }),
311338
).rejects.toMatchObject({
312339
code: 'COMMAND_FAILED',
313340
message: `Failed to add touch overlays to the recording: the export did not finish within its ${OVERLAY_BUDGET_MS}ms budget; a lower --fps renders faster`,
314341
});
315-
expect(vi.mocked(waitForPlayableVideo)).toHaveBeenCalledWith(expect.any(String), { deadline });
342+
expect(vi.mocked(waitForPlayableVideo)).toHaveBeenCalledWith(expect.any(String), {
343+
host: expect.objectContaining({ cwd: process.cwd() }),
344+
deadline,
345+
});
316346
expect(fs.readFileSync(videoPath, 'utf8')).toBe('original');
317347
expect(fs.readdirSync(tmpDir).filter((name) => name.includes('agent-device-'))).toEqual([]);
318348
});

‎packages/capture-kit/src/recording/contact-sheet-frames.test.ts‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { beforeEach, describe, expect, test, vi } from 'vitest';
2+
import { createTestHost } from '../host-environment.fixtures.ts';
23
import path from 'node:path';
34
import {
45
CONTACT_SHEET_EXTRACTION_REASON,
@@ -7,13 +8,10 @@ import {
78
} from './contact-sheet-report.ts';
89
import { AppError, isRequestCanceledError } from '@agent-device/kernel/errors';
910
import { mkdtempForTestSync } from '../tmp-dir.fixtures.ts';
10-
import { bindTestCommandHost } from '@agent-device/test-kit';
1111
import { solidPng } from '../png-pixels.fixtures.ts';
1212
import { writeDecodedFrames, type StubFrame } from './contact-sheet.fixtures.ts';
1313
import { extractRecordingFrames } from './contact-sheet-frames.ts';
1414

15-
bindTestCommandHost();
16-
1715
vi.mock(import('@agent-device/host-kit/command'), async (importOriginal) => ({
1816
...(await importOriginal()),
1917
runCmd: vi.fn(),
@@ -40,9 +38,9 @@ function reasonOf(action: () => Promise<unknown>): Promise<unknown> | unknown {
4038

4139
/** Decoding is Apple tooling, so every case here states the host it is pretending to be. */
4240
function extract(
43-
input: Omit<Parameters<typeof extractRecordingFrames>[0], 'hostPlatform'>,
41+
input: Omit<Parameters<typeof extractRecordingFrames>[0], 'hostPlatform' | 'host'>,
4442
): Promise<Awaited<ReturnType<typeof extractRecordingFrames>>> {
45-
return extractRecordingFrames({ ...input, hostPlatform: 'darwin' });
43+
return extractRecordingFrames({ host: createTestHost(), ...input, hostPlatform: 'darwin' });
4644
}
4745

4846
function withTimes(args: readonly string[], times: string): string[] {
@@ -66,6 +64,7 @@ describe('extractRecordingFrames', () => {
6664
expect(
6765
await reasonOf(() =>
6866
extractRecordingFrames({
67+
host: createTestHost(),
6968
videoPath: VIDEO,
7069
scratchDir,
7170
maxWidth: CELL_WIDTH,

‎packages/capture-kit/src/recording/contact-sheet-frames.ts‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import fs from 'node:fs';
2+
import type { HostEnvironment } from '@agent-device/kernel/source-value';
23
import path from 'node:path';
34
import {
45
CONTACT_SHEET_EXTRACTION_REASON,
@@ -67,6 +68,7 @@ export type ExtractedRecordingFrames = Readonly<{
6768
*/
6869
export async function extractRecordingFrames(
6970
input: Readonly<{
71+
host: HostEnvironment;
7072
videoPath: string;
7173
scratchDir: string;
7274
timesMs: readonly number[];
@@ -107,7 +109,7 @@ export async function extractRecordingFrames(
107109
],
108110
{
109111
timeoutMs: EXTRACTION_TIMEOUT_MS,
110-
env: buildSwiftToolEnv(),
112+
env: buildSwiftToolEnv(input.host.env),
111113
allowFailure: true,
112114
...(input.signal === undefined ? {} : { signal: input.signal }),
113115
},
@@ -260,11 +262,17 @@ function lastNonEmptyLine(text: string): string {
260262
* Compiles the decoder helper, or fails as an extraction failure: a cold Swift cache that cannot
261263
* build the helper is exactly as unable to produce a sheet as a helper that exits nonzero.
262264
*/
263-
async function compileDecoder(input: { videoPath: string; signal?: AbortSignal }): Promise<string> {
265+
async function compileDecoder(input: {
266+
host: HostEnvironment;
267+
videoPath: string;
268+
signal?: AbortSignal;
269+
}): Promise<string> {
270+
const { cwd } = input.host;
264271
try {
265272
return await compileSwiftSourceFile({
266-
sourcePath: resolveRecordingScriptPath(FRAMES_SCRIPT, findProjectRoot()),
267-
extraSourcePaths: [resolveRecordingScriptPath(SHARED_SUPPORT_SCRIPT, findProjectRoot())],
273+
host: input.host,
274+
sourcePath: resolveRecordingScriptPath(FRAMES_SCRIPT, findProjectRoot(), cwd),
275+
extraSourcePaths: [resolveRecordingScriptPath(SHARED_SUPPORT_SCRIPT, findProjectRoot(), cwd)],
268276
cacheName: 'recording-frames',
269277
timeoutMs: COMPILATION_TIMEOUT_MS,
270278
});

‎packages/capture-kit/src/recording/contact-sheet.test.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { beforeEach, describe, expect, test, vi } from 'vitest';
2+
import { createTestHost } from '../host-environment.fixtures.ts';
23
import fs from 'node:fs';
34
import path from 'node:path';
45
import {
@@ -12,14 +13,11 @@ import {
1213
import { AppError } from '@agent-device/kernel/errors';
1314
import { BLACK, RED, WHITE, paintPng, solidPng } from '../png-pixels.fixtures.ts';
1415
import { mkdtempForTestSync } from '../tmp-dir.fixtures.ts';
15-
import { bindTestCommandHost } from '@agent-device/test-kit';
1616
import { decodePng } from '../png.ts';
1717
import { mp4Atom, mp4MovieHeader } from './mp4.fixtures.ts';
1818
import { writeDecodedFrames, type StubFrame } from './contact-sheet.fixtures.ts';
1919
import { buildRecordingContactSheet, recordingContactSheetPath } from './contact-sheet.ts';
2020

21-
bindTestCommandHost();
22-
2321
vi.mock(import('@agent-device/host-kit/command'), async (importOriginal) => ({
2422
...(await importOriginal()),
2523
runCmd: vi.fn(),
@@ -101,6 +99,7 @@ type BuildInput = Partial<
10199

102100
function build(videoPath: string, input: BuildInput = {}) {
103101
return buildRecordingContactSheet({
102+
host: createTestHost(),
104103
...input,
105104
videoPath,
106105
maxPixels: input.maxPixels ?? 20_000_000,

‎packages/capture-kit/src/recording/contact-sheet.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import fs from 'node:fs';
2+
import type { HostEnvironment } from '@agent-device/kernel/source-value';
23
import os from 'node:os';
34
import path from 'node:path';
45
import {
@@ -50,6 +51,7 @@ export type { RecordingContactSheetResult } from './contact-sheet-report.ts';
5051
*/
5152
export async function buildRecordingContactSheet(
5253
input: Readonly<{
54+
host: HostEnvironment;
5355
videoPath: string;
5456
outputPath?: string;
5557
maxPixels: number;
@@ -83,6 +85,7 @@ export async function buildRecordingContactSheet(
8385
const scratchDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-contact-sheet-'));
8486
try {
8587
const extracted = await extractRecordingFrames({
88+
host: input.host,
8689
videoPath,
8790
scratchDir,
8891
timesMs: grid.timesMs,

0 commit comments

Comments
 (0)