Skip to content

Commit 5aa8134

Browse files
committed
refactor: bind session resources to stable lifetimes
1 parent d396f3b commit 5aa8134

58 files changed

Lines changed: 1506 additions & 668 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎packages/capture-kit/src/capture-admission/__tests__/audio-probe-session-resource.test.ts‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { makeCaptureSessionBinding } from '../../durable-capture/session-binding.fixtures.ts';
12
import fs from 'node:fs/promises';
23
import path from 'node:path';
34
import { expect, test, vi } from 'vitest';
@@ -44,6 +45,10 @@ test('audio-probe disposes on a failed finish because terminating the helper is
4445
);
4546
const session: DurableCaptureSessionState = {};
4647
sessionStore.set(sessionName, session);
48+
const binding = makeCaptureSessionBinding(sessionStore, sessionName, {
49+
read: (session) => session.audioProbe,
50+
replace: (session, audioProbe) => ({ ...session, audioProbe }),
51+
});
4752
const statusPath = path.join(sessionStore.resolveSessionDir(sessionName), 'audio-probe.json');
4853
const terminate = vi.fn(async () => {});
4954
let resolveExit!: (result: HostCommandResult) => void;
@@ -85,9 +90,7 @@ test('audio-probe disposes on a failed finish because terminating the helper is
8590
});
8691
await adoptStartedAudioProbe({
8792
admissionLedger: createAudioProbeAdmissionLedger(),
88-
session,
89-
sessionName,
90-
sessionStore,
93+
binding,
9194
device,
9295
owner: localRuntimeOwner('apple'),
9396
fence,
@@ -101,9 +104,7 @@ test('audio-probe disposes on a failed finish because terminating the helper is
101104
await expect(
102105
finishLiveAudioProbe({
103106
intent: 'capture',
104-
session: sessionStore.get(sessionName) ?? session,
105-
sessionName,
106-
sessionStore,
107+
binding,
107108
}),
108109
).rejects.toThrow('helper exited before completing the capture');
109110

‎packages/capture-kit/src/capture-admission/__tests__/durable-capture-resource.fixtures.ts‎

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { makeCaptureSessionBinding } from '../../durable-capture/session-binding.fixtures.ts';
12
import { vi } from 'vitest';
23
import type { AppLogCompletion, AppLogLiveHandle } from '@agent-device/contracts/app-log-runtime';
34
import type { CleanupOutcome, FinishOutcome } from '@agent-device/contracts/durable-resource';
@@ -30,19 +31,10 @@ export const testCaptureStore = createDurableCaptureResourceStore({
3031
export function createTestCaptureResource(
3132
store: DurableCaptureResourceStore<'app-log'> = testCaptureStore,
3233
) {
33-
return createDurableCaptureResource<
34-
'app-log',
35-
AppLogLiveHandle,
36-
AppLogCompletion,
37-
TestCaptureSession
38-
>({
34+
return createDurableCaptureResource<'app-log', AppLogLiveHandle, AppLogCompletion>({
3935
resourceKind: 'app-log',
4036
displayName: 'test capture',
4137
store,
42-
sessionSlot: {
43-
read: (session) => session.appLog,
44-
replace: (session, appLog) => ({ ...session, appLog, appLogFailure: undefined }),
45-
},
4638
completionMetadata: (completion) => ({
4739
outputPath: completion.outputPath,
4840
completedAt: completion.completedAt,
@@ -72,6 +64,10 @@ export function makeDurableCaptureContext(
7264
const session: TestCaptureSession = {};
7365
sessionStore.set(sessionName, session);
7466
return {
67+
binding: makeCaptureSessionBinding(sessionStore, sessionName, {
68+
read: (session) => session.appLog,
69+
replace: (session, appLog) => ({ ...session, appLog, appLogFailure: undefined }),
70+
}),
7571
admissionLedger: createDurableCaptureAdmissionLedger({ displayName: 'test capture' }),
7672
session,
7773
sessionName,

‎packages/capture-kit/src/capture-admission/__tests__/durable-capture-resource.test.ts‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,7 @@ test('one coordinator exposes the typed manifest and all lifecycle entrypoints',
2525
await expect(
2626
testCaptureResource.finishLive({
2727
intent: 'capture',
28-
session: active,
29-
sessionName: context.sessionName,
30-
sessionStore: context.sessionStore,
28+
binding: context.binding,
3129
}),
3230
).resolves.toMatchObject({ outputPath: '/tmp/app.log', completedAt: 2 });
3331
expect(context.sessionStore.get(context.sessionName)?.appLog).toBeUndefined();

‎packages/capture-kit/src/capture-admission/__tests__/screen-recording-boundary-faults.test.ts‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { makeCaptureSessionBinding } from '../../durable-capture/session-binding.fixtures.ts';
12
import path from 'node:path';
23
import { expect, test, vi } from 'vitest';
34
import { createDurableResourceEnvelope } from '../../durable-resource-envelope.ts';
@@ -187,16 +188,11 @@ function createScreenRecordingTestResource(
187188
return createDurableCaptureResource<
188189
'screen-recording',
189190
ScreenRecordingLiveHandle,
190-
ScreenRecordingCompletion,
191-
DurableCaptureSessionState
191+
ScreenRecordingCompletion
192192
>({
193193
resourceKind: 'screen-recording',
194194
displayName: 'screen recording',
195195
store,
196-
sessionSlot: {
197-
read: (session) => session.screenRecording,
198-
replace: (session, screenRecording) => ({ ...session, screenRecording }),
199-
},
200196
completionMetadata: (completion) => ({
201197
backend: completion.backend,
202198
outputPath: completion.outPath,
@@ -217,9 +213,14 @@ function makeContext(resource: ReturnType<typeof createScreenRecordingTestResour
217213
const sessionName = 'recording';
218214
const session: DurableCaptureSessionState = {};
219215
sessionStore.set(sessionName, session);
216+
const binding = makeCaptureSessionBinding(sessionStore, sessionName, {
217+
read: (session) => session.screenRecording,
218+
replace: (session, screenRecording) => ({ ...session, screenRecording }),
219+
});
220220
return {
221221
admissionLedger: createDurableCaptureAdmissionLedger({ displayName: 'screen recording' }),
222222
session,
223+
binding,
223224
sessionName,
224225
sessionStore,
225226
device,

‎packages/capture-kit/src/capture-admission/__tests__/screen-recording-session-resource.test.ts‎

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { makeCaptureSessionBinding } from '../../durable-capture/session-binding.fixtures.ts';
12
import { expect, test, vi } from 'vitest';
23
import { PendingTransferGuard } from '@agent-device/contracts/async-lifecycle';
34
import { localRuntimeOwner } from '@agent-device/contracts/platform-runtime';
@@ -35,6 +36,10 @@ test('screen recording persists durable truth before adopting only handle and en
3536
const sessionName = 'recording';
3637
const session: TestRecordingSession = { name: sessionName, device };
3738
sessionStore.set(sessionName, session);
39+
const binding = makeCaptureSessionBinding(sessionStore, sessionName, {
40+
read: (session) => session.screenRecording,
41+
replace: (session, screenRecording) => ({ ...session, screenRecording }),
42+
});
3843
const owner = localRuntimeOwner('android');
3944
const fence = { token: 'recording-fence', generation: 1 } as const;
4045
const finish = vi.fn(async () => ({
@@ -79,9 +84,7 @@ test('screen recording persists durable truth before adopting only handle and en
7984

8085
await adoptStartedScreenRecording({
8186
admissionLedger: createScreenRecordingAdmissionLedger(),
82-
session,
83-
sessionName,
84-
sessionStore,
87+
binding,
8588
device: session.device,
8689
owner,
8790
fence,
@@ -101,9 +104,10 @@ test('screen recording persists durable truth before adopting only handle and en
101104

102105
const active = sessionStore.get(sessionName);
103106
if (!active) throw new Error('Expected screen-recording session');
104-
await expect(
105-
finishLiveScreenRecording({ intent: 'capture', session: active, sessionName, sessionStore }),
106-
).resolves.toMatchObject({ backend: 'android', outPath: '/tmp/recording.mp4' });
107+
await expect(finishLiveScreenRecording({ intent: 'capture', binding })).resolves.toMatchObject({
108+
backend: 'android',
109+
outPath: '/tmp/recording.mp4',
110+
});
107111
expect(finish).toHaveBeenCalledOnce();
108112
expect(sessionStore.get(sessionName)?.screenRecording).toBeUndefined();
109113
});
@@ -115,6 +119,10 @@ test('a failed recording finish keeps the record open and never disposes the rec
115119
const sessionName = 'recording';
116120
const session: TestRecordingSession = { name: sessionName, device };
117121
sessionStore.set(sessionName, session);
122+
const binding = makeCaptureSessionBinding(sessionStore, sessionName, {
123+
read: (session) => session.screenRecording,
124+
replace: (session, screenRecording) => ({ ...session, screenRecording }),
125+
});
118126
const owner = localRuntimeOwner('android');
119127
const fence = { token: 'recording-fence', generation: 1 } as const;
120128
const finishError = new Error('failed to retrieve playable Android recording');
@@ -150,9 +158,7 @@ test('a failed recording finish keeps the record open and never disposes the rec
150158
});
151159
await adoptStartedScreenRecording({
152160
admissionLedger: createScreenRecordingAdmissionLedger(),
153-
session,
154-
sessionName,
155-
sessionStore,
161+
binding,
156162
device: session.device,
157163
owner,
158164
fence,
@@ -163,9 +169,7 @@ test('a failed recording finish keeps the record open and never disposes the rec
163169

164170
const active = sessionStore.get(sessionName);
165171
if (!active) throw new Error('Expected screen-recording session');
166-
await expect(
167-
finishLiveScreenRecording({ intent: 'capture', session: active, sessionName, sessionStore }),
168-
).rejects.toBe(finishError);
172+
await expect(finishLiveScreenRecording({ intent: 'capture', binding })).rejects.toBe(finishError);
169173

170174
expect(forceCleanup).not.toHaveBeenCalled();
171175
expect(sessionStore.get(sessionName)?.screenRecording?.handle).toBe(handle);
@@ -182,6 +186,10 @@ test('a record stop that fails after collecting resumes through the fence withou
182186
const sessionName = 'recording';
183187
const session: TestRecordingSession = { name: sessionName, device };
184188
sessionStore.set(sessionName, session);
189+
const binding = makeCaptureSessionBinding(sessionStore, sessionName, {
190+
read: (session) => session.screenRecording,
191+
replace: (session, screenRecording) => ({ ...session, screenRecording }),
192+
});
185193
const owner = localRuntimeOwner('android');
186194
const fence = { token: 'recording-fence', generation: 1 } as const;
187195
const signals = vi.fn(async () => ({ observation: { recorder: 'confirmed' as const } }));
@@ -216,9 +224,7 @@ test('a record stop that fails after collecting resumes through the fence withou
216224
);
217225
await adoptStartedScreenRecording({
218226
admissionLedger: createScreenRecordingAdmissionLedger(),
219-
session,
220-
sessionName,
221-
sessionStore,
227+
binding,
222228
device: session.device,
223229
owner,
224230
fence,
@@ -239,9 +245,7 @@ test('a record stop that fails after collecting resumes through the fence withou
239245
if (!active) throw new Error('Expected screen-recording session');
240246
return finishLiveScreenRecording({
241247
intent: 'capture',
242-
session: active,
243-
sessionName,
244-
sessionStore,
248+
binding,
245249
});
246250
};
247251

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,16 @@
11
import path from 'node:path';
22
import { safeSessionName } from '@agent-device/host-kit/session-paths';
3-
import type { DurableCaptureSessionStore } from '../../durable-capture/index.ts';
43
import { mkdtempForTestSync } from '../../tmp-dir.fixtures.ts';
4+
import { makeCaptureFixtureStore } from '../../durable-capture/session-binding.fixtures.ts';
55

6-
export type CaptureAdmissionSessionStore<S> = DurableCaptureSessionStore<S> &
7-
Readonly<{
8-
get(name: string): S | undefined;
9-
sessionsDir: string;
10-
}>;
6+
export type CaptureAdmissionSessionStore<S> = ReturnType<
7+
typeof makeCaptureAdmissionSessionStore<S>
8+
>;
119

12-
/**
13-
* The whole of the daemon `SessionStore` these admission modules ever address — `set`,
14-
* `resolveSessionDir`, and the read-back a test asserts on — over a fresh temp directory, so a
15-
* test of this family needs no session record, store class, or daemon import.
16-
*/
17-
export function makeCaptureAdmissionSessionStore<S>(
18-
prefix: string,
19-
): CaptureAdmissionSessionStore<S> {
10+
export function makeCaptureAdmissionSessionStore<S>(prefix: string) {
2011
const sessionsDir = mkdtempForTestSync(prefix);
21-
const sessions = new Map<string, S>();
22-
return {
23-
set: (name, session) => void sessions.set(name, session),
24-
get: (name) => sessions.get(name),
25-
resolveSessionDir: (name) => path.join(sessionsDir, safeSessionName(name)),
12+
return Object.freeze({
13+
...makeCaptureFixtureStore<S>((name) => path.join(sessionsDir, safeSessionName(name))),
2614
sessionsDir,
27-
};
15+
});
2816
}

‎packages/capture-kit/src/capture-admission/audio-probe-session-resource.ts‎

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,20 @@ import type {
99
RuntimeOwnerRef,
1010
} from '@agent-device/contracts/platform-runtime';
1111
import type { DeviceInfo } from '@agent-device/kernel/device';
12-
import type { DurableCaptureSessionStore } from '../durable-capture/index.ts';
12+
import type { DurableCaptureSessionBinding } from '../durable-capture/index.ts';
1313
import { createDurableCaptureResource } from './durable-capture-resource.ts';
1414
import type { DurableCaptureFinishIntent } from './durable-capture-resource.ts';
1515
import type { AudioProbeAdmissionLedger } from './audio-probe-admission-ledger.ts';
1616
import { audioProbeResourceStore } from './audio-probe-resource-store.ts';
17-
import type { DurableCaptureSessionState } from './session-state-slice.ts';
1817

1918
export const audioProbeDurableResource = createDurableCaptureResource<
2019
'audio-probe',
2120
AudioProbeLiveHandle,
22-
AudioProbeCompletion,
23-
DurableCaptureSessionState
21+
AudioProbeCompletion
2422
>({
2523
resourceKind: 'audio-probe',
2624
displayName: 'audio probe',
2725
store: audioProbeResourceStore,
28-
sessionSlot: {
29-
read: (session) => session.audioProbe,
30-
replace: (session, audioProbe) => ({ ...session, audioProbe }),
31-
},
3226
completionMetadata: (completion) => ({
3327
backend: completion.backend ?? 'unknown',
3428
source: completion.source,
@@ -48,9 +42,7 @@ export const audioProbeDurableResource = createDurableCaptureResource<
4842

4943
export function adoptStartedAudioProbe(params: {
5044
admissionLedger: AudioProbeAdmissionLedger;
51-
session: DurableCaptureSessionState;
52-
sessionName: string;
53-
sessionStore: DurableCaptureSessionStore<DurableCaptureSessionState>;
45+
binding: DurableCaptureSessionBinding<'audio-probe', AudioProbeLiveHandle>;
5446
device: DeviceInfo;
5547
owner: RuntimeOwnerRef;
5648
fence: ResourceOwnershipFence;
@@ -62,9 +54,7 @@ export function adoptStartedAudioProbe(params: {
6254
}
6355

6456
export function finishLiveAudioProbe(params: {
65-
session: DurableCaptureSessionState;
66-
sessionName: string;
67-
sessionStore: DurableCaptureSessionStore<DurableCaptureSessionState>;
57+
binding: DurableCaptureSessionBinding<'audio-probe', AudioProbeLiveHandle>;
6858
intent: DurableCaptureFinishIntent;
6959
}): Promise<AudioProbeCompletion> {
7060
return audioProbeDurableResource.finishLive(params);

0 commit comments

Comments
 (0)