Skip to content

Commit 2a8cb15

Browse files
committed
refactor(daemon): carry the admitted session in the appstate target decision
1 parent ec6894d commit 2a8cb15

4 files changed

Lines changed: 80 additions & 34 deletions

File tree

‎src/daemon/handlers/__tests__/session-appstate-input.test.ts‎

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import type { DeviceInfo } from '@agent-device/kernel/device';
1212
import type { SessionState } from '../../session-state.ts';
1313
import type { DaemonRequest } from '../../daemon-request.ts';
1414
import { handleSessionCommands, mockInspectDeviceRuntimeFacts } from './session-command-harness.ts';
15-
import { handleSessionStateCommands } from '../session-state.ts';
1615
import { mkdtempForTestSync } from '../../../__tests__/test-utils/tmp-dir.ts';
1716
import { AppError } from '@agent-device/kernel/errors';
1817
import type {
@@ -453,25 +452,3 @@ test('appstate on iOS keeps the session answer, with no state, when the runner c
453452
expect(response.data).not.toHaveProperty('state');
454453
}
455454
});
456-
457-
test('appstate returns missing-session error for explicit session flag', async () => {
458-
const response = await handleSessionStateCommands({
459-
platformServices: daemonPlatformServicesFixture(),
460-
req: {
461-
token: 't',
462-
session: 'named',
463-
command: 'appstate',
464-
positionals: [],
465-
flags: { platform: 'ios', session: 'named' },
466-
},
467-
sessionName: 'named',
468-
sessionStore: makeSessionStore(),
469-
});
470-
471-
expect(response).toBeTruthy();
472-
expect(response?.ok).toBe(false);
473-
if (response && !response.ok) {
474-
expect(response.error.code).toBe('SESSION_NOT_FOUND');
475-
expect(response.error.message).toMatch(/Run open with --session named first/i);
476-
}
477-
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { daemonPlatformServicesFixture } from '../../__tests__/platform-services-fixture.ts';
2+
import { test, expect } from 'vitest';
3+
import { makeSessionStore } from './session-test-harness.ts';
4+
import { handleAppStateCommand } from '../session-appstate.ts';
5+
import type { DaemonRequest } from '../../daemon-request.ts';
6+
7+
function appStateRequest(session: string, flags: DaemonRequest['flags']): DaemonRequest {
8+
return { token: 't', session, command: 'appstate', positionals: [], flags };
9+
}
10+
11+
test('appstate returns missing-session error for explicit session flag', async () => {
12+
const response = await handleAppStateCommand({
13+
platformServices: daemonPlatformServicesFixture(),
14+
req: appStateRequest('named', { platform: 'ios', session: 'named' }),
15+
sessionName: 'named',
16+
sessionStore: makeSessionStore(),
17+
});
18+
19+
expect(response).toBeTruthy();
20+
expect(response.ok).toBe(false);
21+
if (!response.ok) {
22+
expect(response.error.code).toBe('SESSION_NOT_FOUND');
23+
expect(response.error.message).toMatch(/Run open with --session named first/i);
24+
}
25+
});
26+
27+
test('appstate refuses --platform ios without a session at the message level', async () => {
28+
const response = await handleAppStateCommand({
29+
platformServices: daemonPlatformServicesFixture(),
30+
req: appStateRequest('default', { platform: 'ios', device: 'iPhone 17' }),
31+
sessionName: 'default',
32+
sessionStore: makeSessionStore(),
33+
});
34+
35+
expect(response.ok).toBe(false);
36+
if (!response.ok) {
37+
expect(response.error.code).toBe('SESSION_NOT_FOUND');
38+
expect(response.error.message).toBe(
39+
'iOS appstate requires an active session on the target device. Run open first (for example: open --session sim --platform ios --device "<name>" <app>).',
40+
);
41+
}
42+
});
43+
44+
test('appstate refuses --platform macos without a session at the message level', async () => {
45+
const response = await handleAppStateCommand({
46+
platformServices: daemonPlatformServicesFixture(),
47+
req: appStateRequest('default', { platform: 'macos', device: 'Mac' }),
48+
sessionName: 'default',
49+
sessionStore: makeSessionStore(),
50+
});
51+
52+
expect(response.ok).toBe(false);
53+
if (!response.ok) {
54+
expect(response.error.code).toBe('SESSION_NOT_FOUND');
55+
expect(response.error.message).toBe(
56+
'macOS appstate requires an active session on the target device. Run open first (for example: open --session macos --platform macos "System Settings").',
57+
);
58+
}
59+
});

‎src/daemon/handlers/session-appstate-apple-session.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import type { DaemonResponse } from '../daemon-request.ts';
1818
* The session facts this answer is built from, as a structural view: the handler reads exactly
1919
* the fields it uses and passes no more of the session shape than this.
2020
*/
21-
type AppStateSessionRecord = Readonly<{
21+
export type AppStateSessionRecord = Readonly<{
2222
device: DeviceInfo;
2323
surface?: SessionSurface;
2424
appBundleId?: string;

‎src/daemon/handlers/session-appstate.ts‎

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { DaemonResponse } from '../daemon-request.ts';
1111
import {
1212
appleSessionAppStateResponse,
1313
appleSessionRequiredRefusal,
14+
type AppStateSessionRecord,
1415
} from './session-appstate-apple-session.ts';
1516
import { queryDeviceRuntimeAppState } from './session-appstate-device-runtime.ts';
1617

@@ -32,12 +33,19 @@ function appStateSessionNotFoundResponse(
3233

3334
/**
3435
* Has this appstate request an answerable target, and which authority answers it: the Apple
35-
* session it points at, or the device runtime the selector resolves to. Every way the request can
36-
* be refused without a device answers here.
36+
* session it points at, or the device runtime the selector resolves to. The apple-session branch
37+
* carries the session itself, so answering it can never re-read the store for a session that has
38+
* gone missing between admission and response. Every way the request can be refused without a
39+
* device answers here.
3740
*/
38-
function admitAppStateTarget(
39-
params: RuntimeCommandHandlerParams,
40-
): { refusal: DaemonResponse } | { answeredBy: 'apple-session' | 'device-runtime' } {
41+
type AppStateSessionStoreEntry = ReturnType<RuntimeCommandHandlerParams['sessionStore']['get']>;
42+
43+
type AppStateTargetDecision =
44+
| { refusal: DaemonResponse }
45+
| { answeredBy: 'apple-session'; session: AppStateSessionRecord }
46+
| { answeredBy: 'device-runtime'; session: AppStateSessionStoreEntry };
47+
48+
function admitAppStateTarget(params: RuntimeCommandHandlerParams): AppStateTargetDecision {
4149
const { req, sessionName, sessionStore } = params;
4250
const session = sessionStore.get(sessionName);
4351
const flags = req.flags ?? {};
@@ -59,7 +67,10 @@ function admitAppStateTarget(
5967
if (normalizedPlatform === 'macos' && !useSessionStateForApple) {
6068
return { refusal: appleSessionRequiredRefusal('macos') };
6169
}
62-
return { answeredBy: useSessionStateForApple && session ? 'apple-session' : 'device-runtime' };
70+
if (useSessionStateForApple && session) {
71+
return { answeredBy: 'apple-session' as const, session };
72+
}
73+
return { answeredBy: 'device-runtime' as const, session };
6374
}
6475

6576
/**
@@ -72,12 +83,11 @@ export async function handleAppStateCommand(
7283
const target = admitAppStateTarget(params);
7384
if ('refusal' in target) return target.refusal;
7485

75-
const session = params.sessionStore.get(params.sessionName);
76-
if (target.answeredBy === 'apple-session' && session) {
77-
return await appleSessionAppStateResponse(params, session);
86+
if (target.answeredBy === 'apple-session') {
87+
return await appleSessionAppStateResponse(params, target.session);
7888
}
7989
const device = await resolveCommandDevice({
80-
session,
90+
session: target.session,
8191
flags: params.req.flags ?? {},
8292
platformServices: params.platformServices,
8393
});

0 commit comments

Comments
 (0)