Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions packages/contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@
"types": "./src/back-mode.ts",
"default": "./src/back-mode.ts"
},
"./back-runtime": {
"types": "./src/back-runtime.ts",
"default": "./src/back-runtime.ts"
},
"./capture": {
"types": "./src/facades/capture.ts",
"default": "./src/facades/capture.ts"
Expand Down Expand Up @@ -144,6 +148,10 @@
"types": "./src/gesture-plan-types.ts",
"default": "./src/gesture-plan-types.ts"
},
"./home-runtime": {
"types": "./src/home-runtime.ts",
"default": "./src/home-runtime.ts"
},
"./interaction": {
"types": "./src/facades/interaction.ts",
"default": "./src/facades/interaction.ts"
Expand All @@ -156,10 +164,18 @@
"types": "./src/interaction-guarantees.ts",
"default": "./src/interaction-guarantees.ts"
},
"./interactor-operation-catalog": {
"types": "./src/interactor-operation-catalog.ts",
"default": "./src/interactor-operation-catalog.ts"
},
"./interactor-types": {
"types": "./src/interactor-types.ts",
"default": "./src/interactor-types.ts"
},
"./keyboard-runtime": {
"types": "./src/keyboard-runtime.ts",
"default": "./src/keyboard-runtime.ts"
},
"./logs-runtime-plan": {
"types": "./src/logs-runtime-plan.ts",
"default": "./src/logs-runtime-plan.ts"
Expand All @@ -180,6 +196,10 @@
"types": "./src/facades/observability.ts",
"default": "./src/facades/observability.ts"
},
"./orientation-runtime": {
"types": "./src/orientation-runtime.ts",
"default": "./src/orientation-runtime.ts"
},
"./platform": {
"types": "./src/facades/platform.ts",
"default": "./src/facades/platform.ts"
Expand Down Expand Up @@ -272,6 +292,10 @@
"types": "./src/tv-remote.ts",
"default": "./src/tv-remote.ts"
},
"./tv-remote-runtime": {
"types": "./src/tv-remote-runtime.ts",
"default": "./src/tv-remote-runtime.ts"
},
"./type-text-runtime": {
"types": "./src/type-text-runtime.ts",
"default": "./src/type-text-runtime.ts"
Expand Down
87 changes: 87 additions & 0 deletions packages/contracts/src/back-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { expect, test, vi } from 'vitest';
import {
bindLocalBackInteractor,
bindProviderBackInteractor,
backRuntimeOperationFacts,
} from './back-runtime.ts';
import type { Interactor } from './interactor-types.ts';

const device = {
platform: 'android',
id: 'emulator-5554',
name: 'Pixel',
kind: 'emulator',
booted: true,
} as const;

test('builds the exact back operation fact catalog', () => {
const back = { available: true } as const;
expect(backRuntimeOperationFacts({ back })).toEqual({ back });
});

test('a local binding drives the interactor with the requested mode', async () => {
const back = vi.fn(async () => undefined);
const resolveInteractor = vi.fn(async () => ({ back }) as unknown as Interactor);
const signal = new AbortController().signal;

const operations = bindLocalBackInteractor({ device, signal, resolveInteractor });
await operations.back({
mode: 'system',
options: { appBundleId: 'com.example.app' },
execution: { logPath: '/tmp/daemon.log', requestId: 'back-1' },
});

expect(resolveInteractor).toHaveBeenCalledWith(device, {
logPath: '/tmp/daemon.log',
requestId: 'back-1',
appBundleId: 'com.example.app',
signal,
});
expect(back).toHaveBeenCalledWith('system');
});

test('a provider binding drives its own resolved interactor', async () => {
const back = vi.fn(async () => undefined);
const resolveInteractor = vi.fn(() => ({ back }) as unknown as Interactor);
const signal = new AbortController().signal;

const operations = bindProviderBackInteractor({ device, signal, resolveInteractor });
await operations.back({ execution: { requestId: 'back-2' } });

expect(resolveInteractor).toHaveBeenCalledWith({
requestId: 'back-2',
appBundleId: undefined,
signal,
});
expect(back).toHaveBeenCalledWith(undefined);
});

test('a provider binding fails closed when its exact owner exposes no interactor', async () => {
const operations = bindProviderBackInteractor({
device,
signal: new AbortController().signal,
resolveInteractor: () => undefined,
});

await expect(operations.back({})).rejects.toMatchObject({
code: 'UNSUPPORTED_OPERATION',
details: { reason: 'provider-runtime-interactor-missing', deviceId: device.id },
});
});

test('an already-cancelled request never resolves an interactor', async () => {
const controller = new AbortController();
controller.abort();
const back = vi.fn(async () => undefined);
const resolveInteractor = vi.fn(async () => ({ back }) as unknown as Interactor);

const operations = bindLocalBackInteractor({
device,
signal: controller.signal,
resolveInteractor,
});

await expect(operations.back({})).rejects.toThrow();
expect(resolveInteractor).not.toHaveBeenCalled();
expect(back).not.toHaveBeenCalled();
});
86 changes: 86 additions & 0 deletions packages/contracts/src/back-runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import type { DeviceInfo } from '@agent-device/kernel/device';
import {
localInteractorSource,
providerInteractorSource,
type LocalInteractorOperationResolver,
type ProviderInteractorOperationResolver,
} from './interactor-operation-binding.ts';
import type { BackMode, Interactor, RunnerContext } from './interactor-types.ts';
import type { RuntimeOperationFact } from './platform-runtime.ts';
import type { SnapshotRuntimeExecution } from './snapshot-runtime.ts';

/**
* Neutral intent for one back navigation. `mode` is already validated by the caller (the
* `--mode` flag); the operation names no command, request, session, or CLI flag.
*/
export type BackInput = Readonly<{
mode?: BackMode;
options?: Readonly<{ appBundleId?: string }>;
/** Same runner metadata a capture needs; reuses that type rather than restating it. */
execution?: SnapshotRuntimeExecution;
}>;

/**
* Back returns nothing. The legacy leaf discarded whatever the interactor answered and reported
* only the mode it requested, so a result type here would be a surface the command never had.
*/
export type BackRuntimeOperations = Readonly<{
back(input: BackInput): Promise<void>;
}>;

export type BackRuntimeOperationFacts = Readonly<{
back: RuntimeOperationFact;
}>;

export function backRuntimeOperationFacts(
input: Readonly<{ back: RuntimeOperationFact }>,
): BackRuntimeOperationFacts {
return Object.freeze({ back: input.back });
}

/**
* Captures one selected owner's interactor authority for the lifetime of a request binding. The
* owner is already chosen by the time a binder is called, so each entry point supplies its own
* resolution and this holds only what both share: the runner context and the navigation itself.
*/
function bindBack(
signal: AbortSignal,
resolveInteractor: (runner: RunnerContext) => Promise<Interactor>,
): BackRuntimeOperations {
return Object.freeze({
back: async (input: BackInput) => {
signal.throwIfAborted();
const interactor = await resolveInteractor({
...input.execution,
appBundleId: input.options?.appBundleId,
signal,
});
await interactor.back(input.mode);
},
});
}

export type LocalBackInteractorResolver = LocalInteractorOperationResolver;

export function bindLocalBackInteractor(
params: Readonly<{
device: DeviceInfo;
signal: AbortSignal;
resolveInteractor: LocalBackInteractorResolver;
}>,
): BackRuntimeOperations {
return bindBack(params.signal, localInteractorSource(params));
}

export type ProviderBackInteractorResolver = ProviderInteractorOperationResolver;

/** Provider bindings fail closed when their exact owner no longer exposes its interactor. */
export function bindProviderBackInteractor(
params: Readonly<{
device: DeviceInfo;
signal: AbortSignal;
resolveInteractor: ProviderBackInteractorResolver;
}>,
): BackRuntimeOperations {
return bindBack(params.signal, providerInteractorSource({ ...params, operation: 'back' }));
}
3 changes: 3 additions & 0 deletions packages/contracts/src/facades/interaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ export type {
FillUnconfirmedVerification,
FillVerificationTarget,
Interactor,
KeyboardDismissResult,
KeyboardEnterResult,
KeyboardStatusResult,
RunnerCallOptions,
RunnerContext,
ScreenshotOptions,
Expand Down
79 changes: 79 additions & 0 deletions packages/contracts/src/facades/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,14 @@ export {
focusRuntimeUse,
typeTextRuntimeUse,
viewportRuntimeUse,
backRuntimeUse,
homeRuntimeUse,
orientationRuntimeUse,
tvRemoteRuntimeUse,
keyboardRuntimePlanUses,
keyboardStatusUse,
keyboardDismissUse,
keyboardEnterUse,
} from '../platform-runtime-operations.ts';
export type {
ScreenshotRuntimePlan,
Expand Down Expand Up @@ -315,6 +323,77 @@ export type {
TypeTextRuntimeOperationFacts,
TypeTextRuntimeOperations,
} from '../type-text-runtime.ts';
export {
bindLocalBackInteractor,
bindProviderBackInteractor,
backRuntimeOperationFacts,
} from '../back-runtime.ts';
export type {
BackInput,
BackRuntimeOperationFacts,
BackRuntimeOperations,
LocalBackInteractorResolver,
ProviderBackInteractorResolver,
} from '../back-runtime.ts';
export {
bindLocalHomeInteractor,
bindProviderHomeInteractor,
homeRuntimeOperationFacts,
} from '../home-runtime.ts';
export type {
HomeInput,
HomeRuntimeOperationFacts,
HomeRuntimeOperations,
LocalHomeInteractorResolver,
ProviderHomeInteractorResolver,
} from '../home-runtime.ts';
export {
bindLocalOrientationInteractor,
bindProviderOrientationInteractor,
orientationRuntimeOperationFacts,
} from '../orientation-runtime.ts';
export type {
LocalOrientationInteractorResolver,
OrientationRuntimeOperationFacts,
OrientationRuntimeOperations,
ProviderOrientationInteractorResolver,
SetOrientationInput,
SetOrientationResult,
} from '../orientation-runtime.ts';
export {
bindLocalTvRemoteInteractor,
bindProviderTvRemoteInteractor,
tvRemoteRuntimeOperationFacts,
} from '../tv-remote-runtime.ts';
export type {
LocalTvRemoteInteractorResolver,
ProviderTvRemoteInteractorResolver,
TvRemoteInput,
TvRemoteRuntimeOperationFacts,
TvRemoteRuntimeOperations,
} from '../tv-remote-runtime.ts';
export {
bindLocalKeyboardStatusInteractor,
bindProviderKeyboardStatusInteractor,
bindLocalKeyboardDismissInteractor,
bindProviderKeyboardDismissInteractor,
bindLocalKeyboardEnterInteractor,
bindProviderKeyboardEnterInteractor,
keyboardRuntimeOperationFacts,
} from '../keyboard-runtime.ts';
export type {
KeyboardActionInput,
KeyboardDismissResult,
KeyboardDismissRuntimeOperations,
KeyboardEnterResult,
KeyboardEnterRuntimeOperations,
KeyboardRuntimeOperationFacts,
KeyboardRuntimeOperations,
KeyboardStatusResult,
KeyboardStatusRuntimeOperations,
LocalKeyboardInteractorResolver,
ProviderKeyboardInteractorResolver,
} from '../keyboard-runtime.ts';
export { viewportRuntimeOperationFacts } from '../viewport-runtime.ts';
export type {
SetViewportInput,
Expand Down
Loading
Loading