Skip to content

Commit d3d0c57

Browse files
committed
fix: centralize scoped shutdown hints and touch probe liveness
1 parent 1be1932 commit d3d0c57

5 files changed

Lines changed: 108 additions & 37 deletions

File tree

‎src/daemon/interaction/internal/__tests__/interaction-touch-response.test.ts‎

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,14 @@ test.each([false, true])('selector-touch observation after retirement=%s', async
161161
}
162162
});
163163

164-
test.each([false, true])(
165-
'point-touch publication after a held frame probe, retired=%s',
166-
async (retired) => {
164+
test.each([
165+
[false, false],
166+
[false, true],
167+
[true, false],
168+
[true, true],
169+
])(
170+
'point-touch publication after a held frame probe, retired=%s, fails=%s',
171+
async (retired, fails) => {
167172
const sessionStore = makeSessionStore();
168173
const address = 'cwd:held-touch-response:default';
169174
const session = makeSession('default');
@@ -198,29 +203,38 @@ test.each([false, true])(
198203
},
199204
},
200205
};
201-
const running = buildTargetedTouchResponsePayloads({
202-
params: {
203-
req: {
204-
token: 't',
205-
command: 'press',
206-
positionals: ['10', '20'],
207-
session: 'default',
208-
flags: {},
209-
},
210-
sessionName: address,
211-
sessionRef: ref,
212-
sessionStore,
213-
contextFromFlags,
214-
captureSnapshotForSession: async () => {
215-
startProbe();
216-
await released;
217-
return sessionStore.requireCurrent(ref).snapshot!;
218-
},
206+
const logPath = path.join(mkdtempForTestSync('held-point-'), 'request.log');
207+
const running = withDiagnosticsScope(
208+
{ command: 'press', session: address, logPath, debug: true },
209+
async () => {
210+
const payloads = await buildTargetedTouchResponsePayloads({
211+
params: {
212+
req: {
213+
token: 't',
214+
command: 'press',
215+
positionals: ['10', '20'],
216+
session: 'default',
217+
flags: {},
218+
},
219+
sessionName: address,
220+
sessionRef: ref,
221+
sessionStore,
222+
contextFromFlags,
223+
captureSnapshotForSession: async () => {
224+
startProbe();
225+
await released;
226+
if (fails) throw new Error('frame capture failed');
227+
return sessionStore.requireCurrent(ref).snapshot!;
228+
},
229+
},
230+
result,
231+
staleRefsWarning: undefined,
232+
extra: {},
233+
});
234+
flushDiagnosticsToSessionFile({ force: true });
235+
return payloads;
219236
},
220-
result,
221-
staleRefsWarning: undefined,
222-
extra: {},
223-
});
237+
);
224238
await probing;
225239
let successor;
226240
if (retired) {
@@ -229,6 +243,8 @@ test.each([false, true])(
229243
}
230244
releaseProbe();
231245
const payloads = await running;
246+
const diagnostics = fs.existsSync(logPath) ? fs.readFileSync(logPath, 'utf8') : '';
247+
expect(diagnostics.includes('touch_reference_frame_resolve_failed')).toBe(!retired && fails);
232248
expect(payloads.responseData.x).toBe(10);
233249
if (retired) {
234250
expect(payloads.responseData.settle).toBeUndefined();

‎src/daemon/interaction/internal/interaction-touch-reference-frame.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ async function resolveDirectTouchReferenceFrame(params: {
1818
}): Promise<GestureReferenceFrame | undefined> {
1919
const { ref, flags, sessionStore, contextFromFlags, captureSnapshotForSession, observation } =
2020
params;
21-
const session = sessionStore.requireCurrent(ref);
21+
const session = sessionStore.resolveCurrent(ref);
22+
if (!session) return undefined;
2223
const recording = session.screenRecording?.handle;
2324
if (!recording) {
2425
return undefined;
@@ -68,13 +69,13 @@ export async function resolveDirectTouchReferenceFrameSafely(params: {
6869
try {
6970
return await resolveDirectTouchReferenceFrame(params);
7071
} catch (error) {
72+
const current = params.sessionStore.resolveCurrent(params.ref);
73+
if (!current) return undefined;
7174
emitDiagnostic({
7275
level: 'warn',
7376
phase: 'touch_reference_frame_resolve_failed',
7477
data: {
75-
platform:
76-
params.sessionStore.resolveCurrent(params.ref)?.device.platform ??
77-
params.ref.session.device.platform,
78+
platform: current.device.platform,
7879
error: error instanceof Error ? error.message : String(error),
7980
},
8081
});

‎src/daemon/interaction/internal/interaction-touch-response.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,9 +293,8 @@ export async function buildTargetedTouchResponsePayloads(params: {
293293
extra: Record<string, unknown>;
294294
}): Promise<InteractionResponsePayloads> {
295295
const { params: handlerParams, result, publicData, extra } = params;
296-
const liveBeforeProbe = handlerParams.sessionStore.resolveCurrent(handlerParams.sessionRef!);
297296
const probedFrame =
298-
liveBeforeProbe && result.kind === 'point'
297+
result.kind === 'point'
299298
? await resolveDirectTouchReferenceFrameSafely({
300299
ref: handlerParams.sessionRef!,
301300
flags: handlerParams.req.flags,

‎src/daemon/server/daemon-runtime-lifecycle-shutdown.test.ts‎

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ vi.mock('../../provider-device-runtimes.ts', () => ({
4646
createDaemonProviderRuntimeComposition: async () => ({ runtimes: [], platformModules: [] }),
4747
}));
4848

49-
import { startDaemonRuntime } from './daemon-runtime.ts';
49+
import { startDaemonRuntime, teardownDaemonSessionForShutdown } from './daemon-runtime.ts';
50+
import { makeIosSession } from '../../__tests__/test-utils/session-factories.ts';
51+
import { makeSessionStore } from '../../__tests__/test-utils/store-factory.ts';
5052

5153
afterEach(() => {
5254
lifecycleEvents.length = 0;
@@ -108,3 +110,48 @@ test('a SIGTERM shutdown gives the handoff a diagnostics scope to write its reas
108110
fs.rmSync(stateDir, { recursive: true, force: true });
109111
}
110112
});
113+
114+
test.each([false, true])(
115+
'shutdown forwards only the addressed lifetime’s runtime hints, retired=%s',
116+
async (retired) => {
117+
const sessionStore = makeSessionStore('shutdown-scoped-hints-');
118+
const address = 'cwd:shutdown-hints:default';
119+
const ref = sessionStore.publish(address, makeIosSession('default'));
120+
const publicRef = sessionStore.publish('default', makeIosSession('default'));
121+
sessionStore.setRuntimeHints(address, { metroHost: 'scoped.example', metroPort: 8082 });
122+
sessionStore.setRuntimeHints('default', { metroHost: 'public.example', metroPort: 8083 });
123+
let successor;
124+
if (retired) {
125+
sessionStore.retire(ref);
126+
successor = sessionStore.publish(address, makeIosSession('default'));
127+
sessionStore.setRuntimeHints(address, { metroHost: 'successor.example', metroPort: 8084 });
128+
}
129+
const finalizeApplicationLifecycle = vi.fn(async () => {});
130+
131+
await teardownDaemonSessionForShutdown({
132+
ref,
133+
sessionStore,
134+
stderr: { write: () => {} },
135+
finalizeApplicationLifecycle,
136+
});
137+
138+
expect(finalizeApplicationLifecycle).toHaveBeenCalledWith(
139+
ref.session,
140+
retired ? {} : { metroHost: 'scoped.example', metroPort: '8082' },
141+
);
142+
expect(sessionStore.resolveCurrent(publicRef)).toBe(publicRef.session);
143+
expect(sessionStore.getRuntimeHints('default')).toEqual({
144+
metroHost: 'public.example',
145+
metroPort: 8083,
146+
});
147+
if (successor) {
148+
expect(sessionStore.resolveCurrent(successor)).toBe(successor.session);
149+
expect(sessionStore.getRuntimeHints(address)).toEqual({
150+
metroHost: 'successor.example',
151+
metroPort: 8084,
152+
});
153+
} else {
154+
expect(sessionStore.lookup(address)).toBeUndefined();
155+
}
156+
},
157+
);

‎src/daemon/server/daemon-runtime.ts‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { runtimeHintValues } from '../session-runtime.ts';
3636
import { closeDaemonServers } from './server-shutdown.ts';
3737
import type { DaemonInvokeFn } from '../daemon-request.ts';
3838
import type { SessionRef, SessionState } from '../session-state.ts';
39+
import type { RuntimeHintValues } from '@agent-device/contracts/application-lifecycle-runtime';
3940
import { createDaemonIdleReap } from './daemon-idle-reap.ts';
4041
import { createSessionIdleExpiry } from './daemon-session-idle-expiry.ts';
4142
import { resolveSessionIdleExpiryMs } from '../session-idle-expiry.ts';
@@ -137,7 +138,10 @@ export async function teardownDaemonSessionForShutdown(params: {
137138
sessionStore: SessionStore;
138139
stateDir?: string;
139140
stderr: WritableOutput;
140-
finalizeApplicationLifecycle?: (session: SessionState) => Promise<void>;
141+
finalizeApplicationLifecycle?: (
142+
session: SessionState,
143+
runtimeHints: RuntimeHintValues,
144+
) => Promise<void>;
141145
beforeDelete?: (session: SessionState) => Promise<void>;
142146
afterSuccessfulTeardown?: (session: SessionState) => Promise<void>;
143147
}): Promise<void> {
@@ -150,7 +154,11 @@ export async function teardownDaemonSessionForShutdown(params: {
150154
beforeDelete,
151155
afterSuccessfulTeardown,
152156
} = params;
153-
const session = sessionStore.resolveCurrent(ref) ?? ref.session;
157+
const current = sessionStore.resolveCurrent(ref);
158+
const session = current ?? ref.session;
159+
const runtimeHints = runtimeHintValues(
160+
current ? sessionStore.getRuntimeHints(ref.address) : undefined,
161+
);
154162
const timeoutMs = resolveDaemonSessionTeardownTimeoutMs(session);
155163
// The ownership-fenced app-log side effect must settle while this process
156164
// still owns the daemon lock. It is intentionally outside the generic
@@ -182,7 +190,8 @@ export async function teardownDaemonSessionForShutdown(params: {
182190
session,
183191
stderr,
184192
resource: 'lifecycle',
185-
teardown: async () => await finalizeApplicationLifecycle(sessionAfterAppLog),
193+
teardown: async () =>
194+
await finalizeApplicationLifecycle(sessionAfterAppLog, runtimeHints),
186195
})
187196
: true;
188197
return genericTeardownSucceeded && lifecycleTeardownSucceeded;
@@ -458,13 +467,12 @@ export async function startDaemonRuntime(
458467

459468
const teardownDaemonSession = async (ref: SessionRef): Promise<void> => {
460469
const session = sessionStore.resolveCurrent(ref) ?? ref.session;
461-
const runtimeHints = runtimeHintValues(sessionStore.getRuntimeHints(ref.address));
462470
try {
463471
await teardownDaemonSessionForShutdown({
464472
ref,
465473
sessionStore,
466474
stderr,
467-
finalizeApplicationLifecycle: async (sessionToFinalize) =>
475+
finalizeApplicationLifecycle: async (sessionToFinalize, runtimeHints) =>
468476
await finalizeDaemonSessionApplicationLifecycle({
469477
gateway: deviceRuntimeGateway,
470478
scope: createDaemonRecoveryPlatformScope(),

0 commit comments

Comments
 (0)