Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
94 changes: 18 additions & 76 deletions packages/contracts/src/alert-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect, test, vi } from 'vitest';
import { alertRuntimeOperationFacts, bindAlertLeg } from './alert-runtime.ts';
import { localInteractorSource, providerInteractorSource } from './interactor-operation-binding.ts';
import { localInteractorSource } from './interactor-operation-binding.ts';
import type { AlertInteractorOptions, Interactor } from './interactor-types.ts';

const device = {
Expand All @@ -12,42 +12,12 @@ const device = {
booted: true,
} as const;

// The composition the interactor catalog performs, spelled out so each assertion below
// still exercises one facet executor reached through one interactor source.
const bindLocalAlertReadInteractor = (params: {
device: typeof device;
signal: AbortSignal;
resolveInteractor: any;
}) => bindAlertLeg('readAlert', params.signal, localInteractorSource(params));
const bindLocalAlertWaitInteractor = (params: {
device: typeof device;
signal: AbortSignal;
resolveInteractor: any;
}) => bindAlertLeg('awaitAlert', params.signal, localInteractorSource(params));
const bindLocalAlertAcceptInteractor = (params: {
device: typeof device;
signal: AbortSignal;
resolveInteractor: any;
}) => bindAlertLeg('acceptAlert', params.signal, localInteractorSource(params));
const bindLocalAlertDismissInteractor = (params: {
device: typeof device;
signal: AbortSignal;
resolveInteractor: any;
}) => bindAlertLeg('dismissAlert', params.signal, localInteractorSource(params));
const bindProviderAlertAcceptInteractor = (params: {
device: typeof device;
signal: AbortSignal;
resolveInteractor: any;
}) =>
bindAlertLeg(
'acceptAlert',
params.signal,
providerInteractorSource({ ...params, operation: 'alert accept' }),
);

test('builds the exact alert operation fact catalog', () => {
const read = { available: true } as const;
const wait = { available: false, reason: 'owner-capability-missing' } as const;
const wait = {
available: false,
reason: 'owner-capability-missing',
} as const;
const accept = { available: true } as const;
const dismiss = { available: true } as const;

Expand All @@ -70,20 +40,24 @@ test('each local leg forwards the window and the session target to its own owner
};
const resolveInteractor = vi.fn(async () => legs as unknown as Interactor);
const signal = new AbortController().signal;
const params = { device, signal, resolveInteractor };
const source = localInteractorSource({ device, resolveInteractor });
const input = {
timeoutMs: 37,
appBundleId: 'com.example.app',
surface: 'app' as const,
execution: { logPath: '/tmp/daemon.log', requestId: 'alert-1' },
};

await bindLocalAlertReadInteractor(params).readAlert(input);
await bindLocalAlertWaitInteractor(params).awaitAlert(input);
await bindLocalAlertAcceptInteractor(params).acceptAlert(input);
await bindLocalAlertDismissInteractor(params).dismissAlert(input);
await bindAlertLeg('readAlert', signal, source).readAlert(input);
await bindAlertLeg('awaitAlert', signal, source).awaitAlert(input);
await bindAlertLeg('acceptAlert', signal, source).acceptAlert(input);
await bindAlertLeg('dismissAlert', signal, source).dismissAlert(input);

const expectedOptions = { timeoutMs: 37, appBundleId: 'com.example.app', surface: 'app' };
const expectedOptions = {
timeoutMs: 37,
appBundleId: 'com.example.app',
surface: 'app',
};
expect(legs.readAlert).toHaveBeenCalledWith(expectedOptions);
expect(legs.awaitAlert).toHaveBeenCalledWith(expectedOptions);
expect(legs.acceptAlert).toHaveBeenCalledWith(expectedOptions);
Expand All @@ -99,44 +73,12 @@ test('each local leg forwards the window and the session target to its own owner
// A frontmost-app session carries no bundle at all, and the option object must not invent one.
test('an absent target field never reaches the owner as an explicit undefined', async () => {
const readAlert = vi.fn(async (_options?: AlertInteractorOptions) => ({}));
const operations = bindLocalAlertReadInteractor({
device,
signal: new AbortController().signal,
resolveInteractor: async () => ({ readAlert }) as unknown as Interactor,
});
const resolveInteractor = async () => ({ readAlert }) as unknown as Interactor;
const source = localInteractorSource({ device, resolveInteractor });
const operations = bindAlertLeg('readAlert', new AbortController().signal, source);

await operations.readAlert({ surface: 'frontmost-app' });

expect(readAlert).toHaveBeenCalledWith({ surface: 'frontmost-app' });
expect(Object.keys(readAlert.mock.calls[0]?.[0] ?? {})).toEqual(['surface']);
});

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

await expect(operations.acceptAlert({})).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 readAlert = vi.fn(async () => ({}));
const resolveInteractor = vi.fn(async () => ({ readAlert }) as unknown as Interactor);

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

await expect(operations.readAlert({})).rejects.toThrow();
expect(resolveInteractor).not.toHaveBeenCalled();
expect(readAlert).not.toHaveBeenCalled();
});
51 changes: 5 additions & 46 deletions packages/contracts/src/app-event-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect, test, vi } from 'vitest';
import { appEventRuntimeOperationFacts, bindAppEvent } from './app-event-runtime.ts';
import type { Interactor } from './interactor-types.ts';
import { localInteractorSource, providerInteractorSource } from './interactor-operation-binding.ts';
import { localInteractorSource } from './interactor-operation-binding.ts';

const device = {
platform: 'android',
Expand All @@ -11,22 +11,11 @@ const device = {
booted: true,
} as const;

const bindAppEventLocal = (
params: Parameters<typeof localInteractorSource>[0] & { signal: AbortSignal },
) => bindAppEvent(params.signal, localInteractorSource(params));
const bindAppEventProvider = (
params: Parameters<typeof providerInteractorSource>[0] extends infer P
? Omit<P, 'operation'> & { signal: AbortSignal }
: never,
) =>
bindAppEvent(
params.signal,
providerInteractorSource({ ...params, operation: 'trigger-app-event' }),
);

test('builds the exact app-event operation fact catalog', () => {
const triggerAppEvent = { available: true } as const;
expect(appEventRuntimeOperationFacts({ triggerAppEvent })).toEqual({ triggerAppEvent });
expect(appEventRuntimeOperationFacts({ triggerAppEvent })).toEqual({
triggerAppEvent,
});
});

// The URL is resolved daemon-side from the event name, payload, and per-platform template; what
Expand All @@ -36,7 +25,7 @@ test('a local binding opens the resolved event URL against the session app', asy
const resolveInteractor = vi.fn(async () => ({ open }) as unknown as Interactor);
const signal = new AbortController().signal;

const operations = bindAppEventLocal({ device, signal, resolveInteractor });
const operations = bindAppEvent(signal, localInteractorSource({ device, resolveInteractor }));
await operations.triggerAppEvent({
eventUrl: 'myapp://agent-device/event?name=checkout',
options: { appBundleId: 'com.example.app' },
Expand All @@ -53,33 +42,3 @@ test('a local binding opens the resolved event URL against the session app', asy
appBundleId: 'com.example.app',
});
});

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

await expect(operations.triggerAppEvent({ eventUrl: 'myapp://x' })).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 open = vi.fn(async () => undefined);
const resolveInteractor = vi.fn(async () => ({ open }) as unknown as Interactor);

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

await expect(operations.triggerAppEvent({ eventUrl: 'myapp://x' })).rejects.toThrow();
expect(resolveInteractor).not.toHaveBeenCalled();
expect(open).not.toHaveBeenCalled();
});
51 changes: 5 additions & 46 deletions packages/contracts/src/app-switcher-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect, test, vi } from 'vitest';
import { appSwitcherRuntimeOperationFacts, bindAppSwitcher } from './app-switcher-runtime.ts';
import type { Interactor } from './interactor-types.ts';
import { localInteractorSource, providerInteractorSource } from './interactor-operation-binding.ts';
import { localInteractorSource } from './interactor-operation-binding.ts';

const device = {
platform: 'android',
Expand All @@ -11,30 +11,19 @@ const device = {
booted: true,
} as const;

const bindAppSwitcherLocal = (
params: Parameters<typeof localInteractorSource>[0] & { signal: AbortSignal },
) => bindAppSwitcher(params.signal, localInteractorSource(params));
const bindAppSwitcherProvider = (
params: Parameters<typeof providerInteractorSource>[0] extends infer P
? Omit<P, 'operation'> & { signal: AbortSignal }
: never,
) =>
bindAppSwitcher(
params.signal,
providerInteractorSource({ ...params, operation: 'app-switcher' }),
);

test('builds the exact app-switcher operation fact catalog', () => {
const appSwitcher = { available: true } as const;
expect(appSwitcherRuntimeOperationFacts({ appSwitcher })).toEqual({ appSwitcher });
expect(appSwitcherRuntimeOperationFacts({ appSwitcher })).toEqual({
appSwitcher,
});
});

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

const operations = bindAppSwitcherLocal({ device, signal, resolveInteractor });
const operations = bindAppSwitcher(signal, localInteractorSource({ device, resolveInteractor }));
await operations.appSwitcher({
options: { appBundleId: 'com.example.app' },
execution: { logPath: '/tmp/daemon.log', requestId: 'switcher-1' },
Expand All @@ -48,33 +37,3 @@ test('a local binding drives the interactor with the request runner context', as
});
expect(appSwitcher).toHaveBeenCalledOnce();
});

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

await expect(operations.appSwitcher({})).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 appSwitcher = vi.fn(async () => undefined);
const resolveInteractor = vi.fn(async () => ({ appSwitcher }) as unknown as Interactor);

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

await expect(operations.appSwitcher({})).rejects.toThrow();
expect(resolveInteractor).not.toHaveBeenCalled();
expect(appSwitcher).not.toHaveBeenCalled();
});
59 changes: 2 additions & 57 deletions packages/contracts/src/back-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect, test, vi } from 'vitest';
import { bindBack, backRuntimeOperationFacts } from './back-runtime.ts';
import type { Interactor } from './interactor-types.ts';
import { localInteractorSource, providerInteractorSource } from './interactor-operation-binding.ts';
import { localInteractorSource } from './interactor-operation-binding.ts';

const device = {
platform: 'android',
Expand All @@ -11,15 +11,6 @@ const device = {
booted: true,
} as const;

const bindBackLocal = (
params: Parameters<typeof localInteractorSource>[0] & { signal: AbortSignal },
) => bindBack(params.signal, localInteractorSource(params));
const bindBackProvider = (
params: Parameters<typeof providerInteractorSource>[0] extends infer P
? Omit<P, 'operation'> & { signal: AbortSignal }
: never,
) => bindBack(params.signal, providerInteractorSource({ ...params, operation: 'back' }));

test('builds the exact back operation fact catalog', () => {
const back = { available: true } as const;
expect(backRuntimeOperationFacts({ back })).toEqual({ back });
Expand All @@ -30,7 +21,7 @@ test('a local binding drives the interactor with the requested mode', async () =
const resolveInteractor = vi.fn(async () => ({ back }) as unknown as Interactor);
const signal = new AbortController().signal;

const operations = bindBackLocal({ device, signal, resolveInteractor });
const operations = bindBack(signal, localInteractorSource({ device, resolveInteractor }));
await operations.back({
mode: 'system',
options: { appBundleId: 'com.example.app' },
Expand All @@ -45,49 +36,3 @@ test('a local binding drives the interactor with the requested mode', async () =
});
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 = bindBackProvider({ 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 = bindBackProvider({
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 = bindBackLocal({
device,
signal: controller.signal,
resolveInteractor,
});

await expect(operations.back({})).rejects.toThrow();
expect(resolveInteractor).not.toHaveBeenCalled();
expect(back).not.toHaveBeenCalled();
});
Loading
Loading