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
19 changes: 19 additions & 0 deletions src/main/input/command-executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,25 @@ describe('DesktopCommandExecutor', () => {
expect(overlay.hideCount).toBe(6);
});

it('executes non-movement no-response commands without coalescing', async () => {
const { adapter, executor, overlay } = createExecutor();

await executor.execute(command('mouse.click', { button: 'left' }, { responseMode: 'none' }));
await executor.execute(command('mouse.scroll', { dx: 0, dy: -3 }, { responseMode: 'none' }));
await executor.execute(command('keyboard.key', { key: 'Enter' }, { responseMode: 'none' }));
await executor.execute(command('window.control', { action: 'switchNext' }, { responseMode: 'none' }));

expect(adapter.calls).toEqual([
{ method: 'clickMouse', args: ['left'] },
{ method: 'scrollMouse', args: [{ dx: 0, dy: -3 }] },
{ method: 'pressKey', args: ['Enter'] },
{ method: 'controlWindow', args: ['switchNext'] }
]);
expect(overlay.events).toEqual(['click']);
expect(overlay.activeCount).toBe(2);
expect(overlay.hideCount).toBe(2);
});

it('passes empty committed text to the adapter', async () => {
const { adapter, executor } = createExecutor();

Expand Down
5 changes: 3 additions & 2 deletions src/main/input/pointer-profile.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MAX_POINTER_DELTA, type PointerMovementProfile } from '../../shared/protocol';
import { MAX_POINTER_DELTA, NO_ACK_CONTROL_COMMAND_TYPES, type PointerMovementProfile } from '../../shared/protocol';

type Point = { x: number; y: number };
type Bounds = { x: number; y: number; width: number; height: number };
Expand Down Expand Up @@ -33,7 +33,8 @@ export function createPointerMovementProfile(input: {
large: toLogicalDelta(TARGET_NATIVE_DELTAS.large, scaleFactor, maxDelta)
},
capabilities: {
noAckMouseMove: true
noAckMouseMove: true,
noAckCommands: [...NO_ACK_CONTROL_COMMAND_TYPES]
}
};
}
Expand Down
29 changes: 28 additions & 1 deletion src/main/pairing/auth.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import type { MouseMoveCommand, PingCommand } from '../../shared/protocol';
import type { MouseClickCommand, MouseMoveCommand, PingCommand } from '../../shared/protocol';
import { PROTOCOL_VERSION } from '../../shared/protocol';
import { createCommandAuthProof, CommandAuthValidator, COMMAND_TIMESTAMP_TOLERANCE_MS } from './auth';
import { MemoryPairingStore } from './pairing-store';
Expand Down Expand Up @@ -80,8 +80,11 @@ describe('CommandAuthValidator', () => {
it('includes no-response mode in command auth proofs', async () => {
const ackMove = createMouseMoveCommand();
const noAckMove = createMouseMoveCommand({ responseMode: 'none' });
const ackClick = createMouseClickCommand();
const noAckClick = createMouseClickCommand({ responseMode: 'none' });

expect(createCommandAuthProof(noAckMove, token)).not.toBe(createCommandAuthProof(ackMove, token));
expect(createCommandAuthProof(noAckClick, token)).not.toBe(createCommandAuthProof(ackClick, token));
});

it('rejects response mode tampering after signing', async () => {
Expand All @@ -93,6 +96,12 @@ describe('CommandAuthValidator', () => {
ok: false,
reason: 'invalid_auth'
});

const clickCommand = createMouseClickCommand();
await expect(validator.validate({ ...clickCommand, responseMode: 'none' })).resolves.toEqual({
ok: false,
reason: 'invalid_auth'
});
});

it('rejects expired timestamps', async () => {
Expand Down Expand Up @@ -152,3 +161,21 @@ function createMouseMoveCommand(overrides: Partial<MouseMoveCommand> = {}): Mous
auth: overrides.auth ?? createCommandAuthProof(merged, token)
};
}

function createMouseClickCommand(overrides: Partial<MouseClickCommand> = {}): MouseClickCommand {
const command = {
version: PROTOCOL_VERSION,
id: 'click-1',
deviceId: 'android-1',
timestamp: now,
type: 'mouse.click',
payload: { button: 'left' },
auth: ''
} satisfies MouseClickCommand;

const merged = { ...command, ...overrides } as MouseClickCommand;
return {
...merged,
auth: overrides.auth ?? createCommandAuthProof(merged, token)
};
}
97 changes: 96 additions & 1 deletion src/main/transport/remote-session-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import { describe, expect, it, vi } from 'vitest';
import {
PROTOCOL_VERSION,
validateProtocolResponse,
type CommandRequest,
type KeyboardKeyCommand,
type MouseClickCommand,
type MouseMoveCommand,
type PairingCompleteResponse,
type DisconnectingCommand,
type PingCommand
type PingCommand,
type WindowControlCommand
} from '../../shared/protocol';
import { createCommandAuthProof, CommandAuthValidator } from '../pairing/auth';
import { PairingApprovalManager } from '../pairing/pairing-approval-manager';
Expand Down Expand Up @@ -84,6 +88,46 @@ describe('RemoteSessionManager', () => {
expect(onError).toHaveBeenCalledWith('Move failed.');
});

it('executes authenticated no-response control commands without sending an ack', async () => {
const handled: CommandRequest[] = [];
const manager = createManager({
onCommand: (command) => {
handled.push(command);
return { ok: true };
}
});
const connection = createConnection();
manager.addConnection(connection);
const commands = [
createMouseClickCommand({ responseMode: 'none' }),
createKeyboardKeyCommand({ responseMode: 'none' }),
createWindowControlCommand({ responseMode: 'none' })
];

for (const command of commands) {
await manager.handleMessage(connection.id, JSON.stringify(command));
}

expect(sentResponses(connection)).toEqual([]);
expect(handled).toEqual(commands);
});

it('suppresses adapter failure responses for no-response control commands', async () => {
const onError = vi.fn();
const manager = createManager({
onError,
onCommand: () => ({ ok: false, code: 'adapter_failure', message: 'Click failed.' })
});
const connection = createConnection();
manager.addConnection(connection);
const command = createMouseClickCommand({ responseMode: 'none' });

await manager.handleMessage(connection.id, JSON.stringify(command));

expect(sentResponses(connection)).toEqual([]);
expect(onError).toHaveBeenCalledWith('Click failed.');
});

it('acks authenticated heartbeat pings without executing desktop commands', async () => {
const onCommand = vi.fn();
const manager = createManager({ onCommand });
Expand Down Expand Up @@ -316,6 +360,57 @@ function createMouseMoveCommand(overrides: Partial<MouseMoveCommand> = {}): Mous
};
}

function createMouseClickCommand(overrides: Partial<MouseClickCommand> = {}): MouseClickCommand {
const command = {
version: PROTOCOL_VERSION,
id: 'click-1',
deviceId: 'android-1',
timestamp: now,
type: 'mouse.click',
payload: { button: 'left' },
auth: ''
} satisfies MouseClickCommand;
const merged = { ...command, ...overrides } as MouseClickCommand;
return {
...merged,
auth: overrides.auth ?? createCommandAuthProof(merged, token)
};
}

function createKeyboardKeyCommand(overrides: Partial<KeyboardKeyCommand> = {}): KeyboardKeyCommand {
const command = {
version: PROTOCOL_VERSION,
id: 'key-1',
deviceId: 'android-1',
timestamp: now,
type: 'keyboard.key',
payload: { key: 'Enter' },
auth: ''
} satisfies KeyboardKeyCommand;
const merged = { ...command, ...overrides } as KeyboardKeyCommand;
return {
...merged,
auth: overrides.auth ?? createCommandAuthProof(merged, token)
};
}

function createWindowControlCommand(overrides: Partial<WindowControlCommand> = {}): WindowControlCommand {
const command = {
version: PROTOCOL_VERSION,
id: 'window-1',
deviceId: 'android-1',
timestamp: now,
type: 'window.control',
payload: { action: 'switchNext' },
auth: ''
} satisfies WindowControlCommand;
const merged = { ...command, ...overrides } as WindowControlCommand;
return {
...merged,
auth: overrides.auth ?? createCommandAuthProof(merged, token)
};
}

function createDisconnectingCommand(overrides: Partial<DisconnectingCommand> = {}): DisconnectingCommand {
const command = {
version: PROTOCOL_VERSION,
Expand Down
7 changes: 6 additions & 1 deletion src/main/transport/remote-session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
createErrorResponse,
createPairingCompleteResponse,
createPointerProfileResponse,
NO_ACK_CONTROL_COMMAND_TYPES,
parseProtocolRequest,
type CommandResponseMode,
type CommandRequest,
Expand Down Expand Up @@ -303,5 +304,9 @@ function commandResponseMode(command: CommandRequest): CommandResponseMode {
}

function shouldSuppressAck(command: CommandRequest): boolean {
return command.type === 'mouse.move' && commandResponseMode(command) === 'none';
return isNoAckControlCommand(command) && commandResponseMode(command) === 'none';
}

function isNoAckControlCommand(command: CommandRequest): boolean {
return (NO_ACK_CONTROL_COMMAND_TYPES as readonly CommandRequest['type'][]).includes(command.type);
}
95 changes: 69 additions & 26 deletions src/shared/protocol.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
createPairingCompleteResponse,
createPointerProfileResponse,
MAX_POINTER_DELTA,
NO_ACK_CONTROL_COMMAND_TYPES,
parseProtocolRequest,
PROTOCOL_VERSION,
validateProtocolRequest,
Expand Down Expand Up @@ -45,15 +46,33 @@ describe('protocol request validation', () => {
}
});

it('accepts no-response mode only for mouse movement', () => {
expect(
validateProtocolRequest({
...baseCommand,
type: 'mouse.move',
payload: { dx: 12, dy: -6 },
responseMode: 'none'
})
).toMatchObject({ ok: true });
it('accepts no-response mode only for user control commands', () => {
const payloads = {
'mouse.move': { dx: 12, dy: -6 },
'mouse.click': { button: 'left' },
'mouse.doubleClick': { button: 'middle' },
'mouse.rightClick': {},
'mouse.scroll': { dx: 0, dy: -3 },
'mouse.dragStart': { button: 'left' },
'mouse.dragEnd': { button: 'left' },
'keyboard.key': { key: 'Enter' },
'keyboard.shortcut': { keys: ['Ctrl', 'C'] },
'keyboard.typeText': { text: 'Hello' },
'media.control': { action: 'playPause' },
'window.control': { action: 'switchNext' }
};

for (const type of NO_ACK_CONTROL_COMMAND_TYPES) {
expect(
validateProtocolRequest({
...baseCommand,
type,
payload: payloads[type],
responseMode: 'none'
})
).toMatchObject({ ok: true });
}

expect(
validateProtocolRequest({
...baseCommand,
Expand All @@ -62,22 +81,16 @@ describe('protocol request validation', () => {
responseMode: 'ack'
})
).toMatchObject({ ok: true });
expect(
validateProtocolRequest({
...baseCommand,
type: 'mouse.scroll',
payload: { dx: 0, dy: -3 },
responseMode: 'none'
})
).toMatchObject({ ok: false, error: 'invalid_payload' });
expect(
validateProtocolRequest({
...baseCommand,
type: 'keyboard.key',
payload: { key: 'Enter' },
responseMode: 'none'
})
).toMatchObject({ ok: false, error: 'invalid_payload' });
for (const command of [
{ type: 'connection.ping', payload: {} },
{ type: 'connection.disconnecting', payload: {} },
{ type: 'pointer.profile', payload: {} }
]) {
expect(validateProtocolRequest({ ...baseCommand, ...command, responseMode: 'none' })).toMatchObject({
ok: false,
error: 'invalid_payload'
});
}
expect(
validateProtocolRequest({
...baseCommand,
Expand Down Expand Up @@ -307,14 +320,44 @@ describe('protocol response validation', () => {
large: 252
},
capabilities: {
noAckMouseMove: true
noAckMouseMove: true,
noAckCommands: [...NO_ACK_CONTROL_COMMAND_TYPES]
}
});

expect(response.payload.capabilities.noAckMouseMove).toBe(true);
expect(response.payload.capabilities.noAckCommands).toEqual([...NO_ACK_CONTROL_COMMAND_TYPES]);
expect(validateProtocolResponse(response)).toMatchObject({ ok: true });
});

it('accepts backward-compatible pointer profile capabilities', () => {
const baseProfile = {
version: PROTOCOL_VERSION,
id: 'profile-1',
type: 'pointer.profile',
ok: true,
payload: {
displayId: '0:0:1280:720:1.5',
scaleFactor: 1.5,
bounds: { x: 0, y: 0, width: 1280, height: 720 },
maxDelta: MAX_POINTER_DELTA,
recommendedDeltas: { small: 50, medium: 130, large: 252 }
},
error: null
};

expect(validateProtocolResponse(baseProfile)).toMatchObject({ ok: true });
expect(
validateProtocolResponse({
...baseProfile,
payload: {
...baseProfile.payload,
capabilities: { noAckMouseMove: true }
}
})
).toMatchObject({ ok: true });
});

it('rejects malformed pointer profile responses', () => {
expect(
validateProtocolResponse({
Expand Down
Loading