Skip to content

Commit 91fffd5

Browse files
committed
test(remote-connection): extract shared fixtures from the connection suite
The connection suite repeated its setup inline: the fake AgentDeviceClient, the Metro prepare reply, the temp state directory, and the persisted connection state literal with its version, profile hash, and timestamps. Those move to a sibling fixtures module as named exports; every scenario keeps its title, its inputs, and its own assertions. jscpd (--min-tokens 80 --min-lines 8): 535 -> 211 duplicated lines, 27 -> 10 clones. Test count 50 -> 50, assert calls 213 -> 213.
1 parent 5eebba5 commit 91fffd5

2 files changed

Lines changed: 327 additions & 444 deletions

File tree

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import path from 'node:path';
2+
import type { AgentDeviceClient } from '../agent-device-client.ts';
3+
import {
4+
hashRemoteConfigFile,
5+
writeRemoteConnectionState,
6+
type RemoteConnectionState,
7+
} from '../remote/remote-connection-state.ts';
8+
import { mkdtempForTestSync } from './test-utils/tmp-dir.ts';
9+
10+
export const unexpectedCommandCall = async (): Promise<never> => {
11+
throw new Error('unexpected call');
12+
};
13+
14+
function createThrowingMethodGroup<T extends object>(methods: Partial<T> = {}): T {
15+
return new Proxy(methods, {
16+
get: (target, property) => target[property as keyof T] ?? unexpectedCommandCall,
17+
}) as T;
18+
}
19+
20+
export type MetroPrepareResult = Awaited<ReturnType<AgentDeviceClient['metro']['prepare']>>;
21+
22+
/** A reused React Native Metro whose Android bundle is served from the sandbox host. */
23+
export function metroPrepareResult(
24+
overrides: Partial<MetroPrepareResult> = {},
25+
): MetroPrepareResult {
26+
return {
27+
projectRoot: '/tmp/project',
28+
kind: 'react-native',
29+
dependenciesInstalled: false,
30+
packageManager: null,
31+
started: false,
32+
reused: true,
33+
pid: 0,
34+
logPath: '/tmp/project/.agent-device/metro.log',
35+
statusUrl: 'http://127.0.0.1:8081/status',
36+
runtimeFilePath: null,
37+
iosRuntime: { platform: 'ios' },
38+
androidRuntime: {
39+
platform: 'android',
40+
bundleUrl: 'https://sandbox.example.test/index.bundle?platform=android',
41+
},
42+
bridge: null,
43+
...overrides,
44+
};
45+
}
46+
47+
/**
48+
* A client whose device inventory is one booted Android emulator and whose lease, session-close,
49+
* and Metro groups answer with plain success. Every other method throws `unexpected call`.
50+
*/
51+
export function createTestClient(
52+
options: {
53+
allocate?: AgentDeviceClient['leases']['allocate'];
54+
heartbeat?: AgentDeviceClient['leases']['heartbeat'];
55+
release?: AgentDeviceClient['leases']['release'];
56+
prepare?: AgentDeviceClient['metro']['prepare'];
57+
closeSession?: AgentDeviceClient['sessions']['close'];
58+
listDevices?: AgentDeviceClient['devices']['list'];
59+
} = {},
60+
): AgentDeviceClient {
61+
return {
62+
command: createThrowingMethodGroup<AgentDeviceClient['command']>(),
63+
devices: createThrowingMethodGroup<AgentDeviceClient['devices']>({
64+
list:
65+
options.listDevices ??
66+
(async () => [
67+
{
68+
platform: 'android',
69+
target: 'mobile',
70+
kind: 'emulator',
71+
id: 'emulator-5554',
72+
name: 'Android Emulator',
73+
booted: true,
74+
identifiers: { serial: 'emulator-5554' },
75+
android: { serial: 'emulator-5554' },
76+
},
77+
]),
78+
}),
79+
sessions: createThrowingMethodGroup<AgentDeviceClient['sessions']>({
80+
close:
81+
options.closeSession ??
82+
(async () => ({
83+
session: 'adc-android',
84+
identifiers: { session: 'adc-android' },
85+
})),
86+
}),
87+
apps: createThrowingMethodGroup<AgentDeviceClient['apps']>(),
88+
materializations: createThrowingMethodGroup<AgentDeviceClient['materializations']>(),
89+
leases: createThrowingMethodGroup<AgentDeviceClient['leases']>({
90+
allocate:
91+
options.allocate ??
92+
(async (request) => ({
93+
leaseId: 'lease-1',
94+
tenantId: request.tenant,
95+
runId: request.runId,
96+
backend: request.leaseBackend ?? 'android-instance',
97+
})),
98+
heartbeat:
99+
options.heartbeat ??
100+
(async (request) => ({
101+
leaseId: request.leaseId,
102+
tenantId: request.tenant ?? 'acme',
103+
runId: request.runId ?? 'run-123',
104+
backend: request.leaseBackend ?? 'android-instance',
105+
})),
106+
release: options.release ?? (async () => ({ released: true })),
107+
}),
108+
metro: createThrowingMethodGroup<AgentDeviceClient['metro']>({
109+
prepare: options.prepare ?? (async () => metroPrepareResult()),
110+
}),
111+
capture: createThrowingMethodGroup<AgentDeviceClient['capture']>(),
112+
interactions: createThrowingMethodGroup<AgentDeviceClient['interactions']>(),
113+
replay: createThrowingMethodGroup<AgentDeviceClient['replay']>(),
114+
batch: createThrowingMethodGroup<AgentDeviceClient['batch']>(),
115+
observability: createThrowingMethodGroup<AgentDeviceClient['observability']>(),
116+
debug: createThrowingMethodGroup<AgentDeviceClient['debug']>(),
117+
recording: createThrowingMethodGroup<AgentDeviceClient['recording']>(),
118+
settings: createThrowingMethodGroup<AgentDeviceClient['settings']>(),
119+
};
120+
}
121+
122+
export type ConnectionWorkspace = {
123+
tempRoot: string;
124+
stateDir: string;
125+
remoteConfigPath: string;
126+
};
127+
128+
/** A scratch state directory and the path of its remote-config profile, which is not yet written. */
129+
export function connectionWorkspace(prefix: string): ConnectionWorkspace {
130+
const tempRoot = mkdtempForTestSync(prefix);
131+
return {
132+
tempRoot,
133+
stateDir: path.join(tempRoot, '.state'),
134+
remoteConfigPath: path.join(tempRoot, 'remote.json'),
135+
};
136+
}
137+
138+
export type StoredConnectionSeed = Omit<
139+
RemoteConnectionState,
140+
'version' | 'remoteConfigHash' | 'connectedAt' | 'updatedAt'
141+
>;
142+
143+
/**
144+
* Persists a connection recorded against the profile file as it exists now: the stored hash is
145+
* taken at seed time, so a later edit of `state.remoteConfigPath` reads as a changed profile.
146+
*/
147+
export function seedConnectionState(options: {
148+
stateDir: string;
149+
state: StoredConnectionSeed;
150+
}): RemoteConnectionState {
151+
const state: RemoteConnectionState = {
152+
version: 1,
153+
remoteConfigHash: hashRemoteConfigFile(options.state.remoteConfigPath),
154+
connectedAt: new Date().toISOString(),
155+
updatedAt: new Date().toISOString(),
156+
...options.state,
157+
};
158+
writeRemoteConnectionState({ stateDir: options.stateDir, state });
159+
return state;
160+
}

0 commit comments

Comments
 (0)