Skip to content

Commit 102b139

Browse files
committed
fix: resolve recording teardown session keys
1 parent 1f5f6f6 commit 102b139

6 files changed

Lines changed: 86 additions & 13 deletions

File tree

src/daemon/handlers/__tests__/session-close-shutdown.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,33 @@ test('close finalizes an active iOS simulator recording before deleting the sess
543543
);
544544
});
545545

546+
test('close resolves a recording resource through the effective session key', async () => {
547+
const sessionStore = makeSessionStore();
548+
const effectiveSessionName = 'cwd:0123456789abcdef:default';
549+
const session = makeIosSimulatorRecordingSession(sessionStore, effectiveSessionName);
550+
session.name = 'default';
551+
const finish = recordingFinishMock(session);
552+
sessionStore.set(effectiveSessionName, session);
553+
554+
const response = await handleSessionCommands({
555+
req: {
556+
token: 't',
557+
session: 'default',
558+
command: 'close',
559+
positionals: [],
560+
flags: {},
561+
},
562+
sessionName: effectiveSessionName,
563+
logPath: path.join(os.tmpdir(), 'daemon.log'),
564+
sessionStore,
565+
invoke: noopInvoke,
566+
});
567+
568+
expect(response?.ok).toBe(true);
569+
expect(finish).toHaveBeenCalledOnce();
570+
expect(sessionStore.get(effectiveSessionName)).toBeUndefined();
571+
});
572+
546573
test('close surfaces a recording finalization failure through the cleanup-failure channel', async () => {
547574
const sessionStore = makeSessionStore();
548575
const sessionName = 'ios-recording-close-failure-session';

src/daemon/handlers/session-close.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ async function runSessionCloseTeardown(params: {
107107
}
108108
};
109109
const retainAppleRunner = shouldRetainAppleRunnerAfterClose(req, session);
110-
await stopBestEffortSessionResources(session, sessionStore, attemptCleanup);
110+
await stopBestEffortSessionResources(session, sessionName, sessionStore, attemptCleanup);
111111
const platformCloseError = repairArmed
112112
? undefined
113113
: await dispatchTargetedPlatformClose({ req, session, logPath });
@@ -126,21 +126,22 @@ type CleanupRunner = (step: string, run: () => Promise<void>) => Promise<void>;
126126

127127
async function stopBestEffortSessionResources(
128128
session: SessionState,
129+
sessionName: string,
129130
sessionStore: SessionStore,
130131
attemptCleanup: CleanupRunner,
131132
): Promise<void> {
132133
// Recording overlay finalization needs the Apple runner.
133-
const currentSession = sessionStore.get(session.name) ?? session;
134+
const currentSession = sessionStore.get(sessionName) ?? session;
134135
if (currentSession.screenRecording) {
135136
await attemptCleanup('recording', () =>
136137
finishSessionScreenRecording({
137138
session: currentSession,
138-
sessionName: session.name,
139+
sessionName,
139140
sessionStore,
140141
}),
141142
);
142143
}
143-
await attemptCleanup('app_log', () => stopSessionAppLog({ session, sessionStore }));
144+
await attemptCleanup('app_log', () => stopSessionAppLog({ session, sessionName, sessionStore }));
144145
await attemptCleanup('audio_probe', async () => {
145146
await stopSessionAudioProbe(session, 'session-close');
146147
});

src/daemon/server/daemon-runtime-recording-teardown.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,38 @@ test('daemon shutdown awaits durable recording finalization inside its extended
127127
expect(stderrChunks.join('')).not.toMatch(/timed out/);
128128
expect(sessionStore.get(session.name)).toBeUndefined();
129129
});
130+
131+
test('daemon shutdown resolves durable recording resources through the effective session key', async () => {
132+
const root = mkdtempForTestSync('agent-device-shutdown-effective-session-');
133+
const sessionStore = new SessionStore(path.join(root, 'sessions'));
134+
const effectiveSessionName = 'cwd:0123456789abcdef:default';
135+
const session = makeRecordingSession({
136+
name: effectiveSessionName,
137+
sessionStore,
138+
finish: async () => ({
139+
status: 'completed' as const,
140+
result: {
141+
backend: 'simctl recordVideo',
142+
outPath: path.join(root, 'recording.mp4'),
143+
startedAt: 1,
144+
completedAt: 2,
145+
scope: 'app' as const,
146+
showTouches: false,
147+
recordOnlySession: false,
148+
},
149+
}),
150+
});
151+
session.name = 'default';
152+
sessionStore.set(effectiveSessionName, session);
153+
const stderrChunks: string[] = [];
154+
155+
await teardownDaemonSessionForShutdown({
156+
session,
157+
sessionStore,
158+
stateDir: root,
159+
stderr: { write: (chunk) => stderrChunks.push(chunk) },
160+
});
161+
162+
expect(stderrChunks.join('')).not.toMatch(/resource record is missing/);
163+
expect(sessionStore.get(effectiveSessionName)).toBeUndefined();
164+
});

src/daemon/server/daemon-runtime.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,16 @@ export async function teardownDaemonSessionForShutdown(params: {
107107
afterSuccessfulTeardown?: (session: SessionState) => Promise<void>;
108108
}): Promise<void> {
109109
const { session, sessionStore, stateDir, stderr, beforeDelete, afterSuccessfulTeardown } = params;
110+
const sessionName = sessionStore.resolveStoredSessionName(session);
110111
const timeoutMs = resolveDaemonSessionTeardownTimeoutMs(session);
111112
// The ownership-fenced app-log side effect must settle while this process
112113
// still owns the daemon lock. It is intentionally outside the generic
113114
// teardown race so lock release and runtime shutdown cannot overtake it.
114-
const appLogTeardownSucceeded = await stopSessionAppLog({ session, sessionStore }).then(
115+
const appLogTeardownSucceeded = await stopSessionAppLog({
116+
session,
117+
sessionName,
118+
sessionStore,
119+
}).then(
115120
() => true,
116121
(error) => {
117122
stderr.write(
@@ -122,11 +127,11 @@ export async function teardownDaemonSessionForShutdown(params: {
122127
return false;
123128
},
124129
);
125-
const sessionAfterAppLog = sessionStore.get(session.name) ?? session;
130+
const sessionAfterAppLog = sessionStore.get(sessionName) ?? session;
126131
const teardown = teardownSessionResources({
127132
appLog: 'already-settled',
128133
session: sessionAfterAppLog,
129-
sessionName: session.name,
134+
sessionName,
130135
sessionStore,
131136
stateDir,
132137
}).then(
@@ -154,7 +159,7 @@ export async function teardownDaemonSessionForShutdown(params: {
154159
sessionStore.finalizeRepairTeardown(session);
155160
await beforeDelete?.(session);
156161
if (teardownSucceeded) await afterSuccessfulTeardown?.(session);
157-
sessionStore.delete(session.name);
162+
sessionStore.delete(sessionName);
158163
}
159164

160165
export type DaemonRuntimeOptions = {

src/daemon/session-store.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,11 @@ export class SessionStore {
309309
return expandSessionPath(filePath, cwd);
310310
}
311311

312-
private resolveStoredSessionName(session: SessionState): string {
312+
/**
313+
* Resolve the map key for a live session object. SessionState.name is the
314+
* public session name, while the map key may include cwd/tenant isolation.
315+
*/
316+
resolveStoredSessionName(session: SessionState): string {
313317
for (const [name, value] of this.sessions) {
314318
if (value === session) return name;
315319
}

src/daemon/session-teardown.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,16 @@ export async function stopAppleRunnerForClose(session: SessionState): Promise<vo
4343

4444
export async function stopSessionAppLog(params: {
4545
session: SessionState;
46+
sessionName: string;
4647
sessionStore: SessionStore;
4748
}): Promise<void> {
48-
const { session, sessionStore } = params;
49+
const { session, sessionName, sessionStore } = params;
4950
if (!session.appLog) return;
5051
await forceCleanupSessionAppLog({
5152
session,
52-
sessionName: session.name,
53+
sessionName,
5354
sessionStore,
54-
resourcePath: appLogResourceStore.resolvePath(sessionStore.resolveSessionDir(session.name)),
55+
resourcePath: appLogResourceStore.resolvePath(sessionStore.resolveSessionDir(sessionName)),
5556
});
5657
}
5758

@@ -170,7 +171,7 @@ export async function teardownSessionResources(
170171
? [
171172
{
172173
step: 'app_log',
173-
run: () => stopSessionAppLog({ session, sessionStore }),
174+
run: () => stopSessionAppLog({ session, sessionName, sessionStore }),
174175
},
175176
]
176177
: [];

0 commit comments

Comments
 (0)