Skip to content
Closed
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
281 changes: 280 additions & 1 deletion src/__tests__/remote-connection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import {
hasDeferredMetroConfig,
materializeRemoteConnectionForCommand,
PROXY_REMOTE_LEASE_TTL_MS,
} from '../cli/commands/connection-runtime.ts';
import { stopMetroCompanion } from '../client-metro-companion.ts';
import { AppError } from '../utils/errors.ts';
Expand Down Expand Up @@ -74,11 +75,27 @@ function createTestClient(
release?: AgentDeviceClient['leases']['release'];
prepare?: AgentDeviceClient['metro']['prepare'];
closeSession?: AgentDeviceClient['sessions']['close'];
listDevices?: AgentDeviceClient['devices']['list'];
} = {},
): AgentDeviceClient {
return {
command: createThrowingMethodGroup<AgentDeviceClient['command']>(),
devices: createThrowingMethodGroup<AgentDeviceClient['devices']>(),
devices: createThrowingMethodGroup<AgentDeviceClient['devices']>({
list:
options.listDevices ??
(async () => [
{
platform: 'android',
target: 'mobile',
kind: 'emulator',
id: 'emulator-5554',
name: 'Android Emulator',
booted: true,
identifiers: { serial: 'emulator-5554' },
android: { serial: 'emulator-5554' },
},
]),
}),
sessions: createThrowingMethodGroup<AgentDeviceClient['sessions']>({
close:
options.closeSession ??
Expand Down Expand Up @@ -182,6 +199,88 @@ test('connect auto-generates a local session and writes minimal remote state', a
fs.rmSync(tempRoot, { recursive: true, force: true });
});

test('connect proxy writes normal remote state with generated non-secret profile', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-connect-proxy-'));
const stateDir = path.join(tempRoot, '.state');

await captureStdout(async () => {
await connectCommand({
positionals: ['proxy'],
flags: {
json: true,
help: false,
version: false,
stateDir,
daemonBaseUrl: 'http://proxy.example.test/agent-device',
daemonAuthToken: 'proxy-secret',
platform: 'android',
},
client: createTestClient(),
});
});

const state = readActiveConnectionState({ stateDir });
assert.ok(state);
assert.match(state.session, /^adc-[a-z0-9]+$/);
assert.equal(state.tenant, 'proxy');
assert.match(state.runId, /^proxy-[a-f0-9]{16}$/);
assert.equal(state.leaseProvider, 'proxy');
assert.match(state.clientId ?? '', /^[a-f0-9]{16}$/);
assert.equal(state.leaseBackend, 'android-instance');
assert.equal(state.leaseId, undefined);
assert.equal(state.daemon?.baseUrl, 'http://proxy.example.test/agent-device');
assert.match(state.remoteConfigPath, /remote-connections\/generated\/proxy-[a-f0-9]{16}\.json$/);
const generated = JSON.parse(fs.readFileSync(state.remoteConfigPath, 'utf8')) as Record<
string,
unknown
>;
assert.equal(generated.daemonBaseUrl, 'http://proxy.example.test/agent-device');
assert.equal(generated.daemonAuthToken, undefined);
assert.equal(generated.leaseProvider, 'proxy');
assert.equal(generated.leaseTtlMs, undefined);
assert.equal(JSON.stringify(generated).includes('proxy-secret'), false);
fs.rmSync(tempRoot, { recursive: true, force: true });
});

test('connect proxy rejects remote-config and unknown provider combinations', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-connect-proxy-errors-'));
const stateDir = path.join(tempRoot, '.state');
const remoteConfigPath = path.join(tempRoot, 'remote.json');
fs.writeFileSync(remoteConfigPath, '{}');

await assert.rejects(
async () =>
await connectCommand({
positionals: ['proxy'],
flags: {
json: true,
help: false,
version: false,
stateDir,
remoteConfig: remoteConfigPath,
},
client: createTestClient(),
}),
/mutually exclusive/,
);

await assert.rejects(
async () =>
await connectCommand({
positionals: ['wat'],
flags: {
json: true,
help: false,
version: false,
stateDir,
},
client: createTestClient(),
}),
/Supported providers: proxy/,
);
fs.rmSync(tempRoot, { recursive: true, force: true });
});

test('connect reports deferred Metro runtime preparation when remote config has Metro settings', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-connect-metro-notice-'));
const stateDir = path.join(tempRoot, '.state');
Expand Down Expand Up @@ -399,6 +498,134 @@ test('deferred materialization allocates lease and prepares Metro for open', asy
fs.rmSync(tempRoot, { recursive: true, force: true });
});

test('proxy open resolves device key before allocating lease', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-connect-proxy-open-'));
const stateDir = path.join(tempRoot, '.state');
const remoteConfigPath = path.join(tempRoot, 'remote.json');
fs.writeFileSync(remoteConfigPath, JSON.stringify({ daemonBaseUrl: 'https://daemon.example' }));
writeRemoteConnectionState({
stateDir,
state: {
version: 1,
session: 'adc-proxy',
remoteConfigPath,
remoteConfigHash: hashRemoteConfigFile(remoteConfigPath),
daemon: { baseUrl: 'https://daemon.example' },
tenant: 'proxy',
runId: 'proxy-client-1',
leaseProvider: 'proxy',
clientId: 'client-1',
connectedAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
});
let allocateRequest: Parameters<AgentDeviceClient['leases']['allocate']>[0] | undefined;

const materialized = await materializeRemoteConnectionForCommand({
command: 'open',
flags: {
json: true,
help: false,
version: false,
stateDir,
remoteConfig: remoteConfigPath,
daemonBaseUrl: 'https://daemon.example',
tenant: 'proxy',
runId: 'proxy-client-1',
session: 'adc-proxy',
platform: 'ios',
},
client: createTestClient({
listDevices: async () => [
{
platform: 'ios',
target: 'mobile',
kind: 'simulator',
id: 'SIM-001',
name: 'iPhone 16',
booted: true,
identifiers: { udid: 'SIM-001' },
ios: { udid: 'SIM-001' },
},
],
allocate: async (request) => {
allocateRequest = request;
return {
leaseId: 'abc123abc123abc1',
tenantId: request.tenant,
runId: request.runId,
backend: request.leaseBackend ?? 'ios-instance',
leaseProvider: request.leaseProvider,
provider: request.leaseProvider,
clientId: request.clientId,
deviceKey: request.deviceKey,
};
},
}),
});

assert.equal(allocateRequest?.leaseProvider, 'proxy');
assert.equal(allocateRequest?.clientId, 'client-1');
assert.equal(allocateRequest?.deviceKey, 'ios:mobile:SIM-001');
assert.equal(allocateRequest?.ttlMs, PROXY_REMOTE_LEASE_TTL_MS);
assert.equal(allocateRequest?.leaseBackend, 'ios-instance');
assert.equal(materialized.flags.leaseId, 'abc123abc123abc1');
assert.equal(materialized.connection?.deviceKey, 'ios:mobile:SIM-001');
const state = readRemoteConnectionState({ stateDir, session: 'adc-proxy' });
assert.equal(state?.leaseId, 'abc123abc123abc1');
assert.equal(state?.deviceKey, 'ios:mobile:SIM-001');
assert.equal(state?.leaseProvider, 'proxy');
fs.rmSync(tempRoot, { recursive: true, force: true });
});

test('proxy commands without active device lease fail before allocation', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-connect-proxy-closed-'));
const stateDir = path.join(tempRoot, '.state');
const remoteConfigPath = path.join(tempRoot, 'remote.json');
fs.writeFileSync(remoteConfigPath, JSON.stringify({ daemonBaseUrl: 'https://daemon.example' }));
writeRemoteConnectionState({
stateDir,
state: {
version: 1,
session: 'adc-proxy',
remoteConfigPath,
remoteConfigHash: hashRemoteConfigFile(remoteConfigPath),
tenant: 'proxy',
runId: 'proxy-client-1',
leaseProvider: 'proxy',
clientId: 'client-1',
leaseBackend: 'ios-instance',
connectedAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
});

await assert.rejects(
async () =>
await materializeRemoteConnectionForCommand({
command: 'snapshot',
flags: {
json: true,
help: false,
version: false,
stateDir,
remoteConfig: remoteConfigPath,
tenant: 'proxy',
runId: 'proxy-client-1',
session: 'adc-proxy',
platform: 'ios',
},
client: createTestClient({
allocate: async () => {
throw new Error('snapshot should not allocate without proxy device lease');
},
}),
}),
/No active proxy device lease for this session; run open first/,
);
fs.rmSync(tempRoot, { recursive: true, force: true });
});

test('direct remote-config materialization creates state and prepares Metro for open', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-direct-remote-open-'));
const stateDir = path.join(tempRoot, '.state');
Expand Down Expand Up @@ -1331,6 +1558,58 @@ test('disconnect without a session uses active connection state', async () => {
fs.rmSync(tempRoot, { recursive: true, force: true });
});

test('disconnect releases proxy lease with provider client and device metadata', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-disconnect-proxy-'));
const stateDir = path.join(tempRoot, '.state');
const remoteConfigPath = path.join(tempRoot, 'remote.json');
fs.writeFileSync(remoteConfigPath, '{}');
writeRemoteConnectionState({
stateDir,
state: {
version: 1,
session: 'adc-proxy',
remoteConfigPath,
remoteConfigHash: hashRemoteConfigFile(remoteConfigPath),
tenant: 'proxy',
runId: 'proxy-client-1',
leaseId: 'abc123abc123abc1',
leaseBackend: 'ios-instance',
leaseProvider: 'proxy',
clientId: 'client-1',
deviceKey: 'ios:mobile:SIM-001',
connectedAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
});
let releaseRequest: Parameters<AgentDeviceClient['leases']['release']>[0] | undefined;

await captureStdout(async () => {
await disconnectCommand({
positionals: [],
flags: {
json: true,
help: false,
version: false,
stateDir,
shutdown: true,
},
client: createTestClient({
release: async (request) => {
releaseRequest = request;
return { released: true };
},
}),
});
});

assert.equal(releaseRequest?.leaseProvider, 'proxy');
assert.equal(releaseRequest?.clientId, 'client-1');
assert.equal(releaseRequest?.deviceKey, 'ios:mobile:SIM-001');
assert.equal(releaseRequest?.leaseId, 'abc123abc123abc1');
assert.equal(readRemoteConnectionState({ stateDir, session: 'adc-proxy' }), null);
fs.rmSync(tempRoot, { recursive: true, force: true });
});

test('connection status reports missing state without daemon calls', async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-connection-status-'));
let handled = false;
Expand Down
Loading