Skip to content

Commit 34ece01

Browse files
committed
refactor: finish companion tunnel naming
1 parent d8c058e commit 34ece01

15 files changed

Lines changed: 213 additions & 96 deletions

rslib.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export default defineConfig({
3131
selectors: 'src/selectors.ts',
3232
finders: 'src/finders.ts',
3333
bin: 'src/bin.ts',
34+
'companion-tunnel': 'src/companion-tunnel.ts',
3435
'metro-companion': 'src/metro-companion.ts',
3536
daemon: 'src/daemon.ts',
3637
'update-check-entry': 'src/utils/update-check-entry.ts',

src/__tests__/client-companion-tunnel-worker.test.ts

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -350,18 +350,18 @@ test('metro companion worker proxies websocket frames to the local upstream serv
350350

351351
const companion = spawn(
352352
process.execPath,
353-
['--experimental-strip-types', 'src/metro-companion.ts', '--agent-device-run-metro-companion'],
353+
['--experimental-strip-types', 'src/companion-tunnel.ts', '--agent-device-run-metro-companion'],
354354
{
355355
cwd: process.cwd(),
356356
env: {
357357
...process.env,
358-
AGENT_DEVICE_METRO_COMPANION_SERVER_BASE_URL: `http://127.0.0.1:${bridgePort}`,
359-
AGENT_DEVICE_METRO_COMPANION_BEARER_TOKEN: 'test-token',
360-
AGENT_DEVICE_METRO_COMPANION_LOCAL_BASE_URL: `http://127.0.0.1:${upstreamPort}`,
361-
AGENT_DEVICE_METRO_COMPANION_REGISTER_PATH: '/api/metro/companion/register',
362-
AGENT_DEVICE_METRO_COMPANION_SCOPE_TENANT_ID: 'tenant-1',
363-
AGENT_DEVICE_METRO_COMPANION_SCOPE_RUN_ID: 'run-1',
364-
AGENT_DEVICE_METRO_COMPANION_SCOPE_LEASE_ID: 'lease-1',
358+
AGENT_DEVICE_COMPANION_TUNNEL_SERVER_BASE_URL: `http://127.0.0.1:${bridgePort}`,
359+
AGENT_DEVICE_COMPANION_TUNNEL_BEARER_TOKEN: 'test-token',
360+
AGENT_DEVICE_COMPANION_TUNNEL_LOCAL_BASE_URL: `http://127.0.0.1:${upstreamPort}`,
361+
AGENT_DEVICE_COMPANION_TUNNEL_REGISTER_PATH: '/api/metro/companion/register',
362+
AGENT_DEVICE_COMPANION_TUNNEL_SCOPE_TENANT_ID: 'tenant-1',
363+
AGENT_DEVICE_COMPANION_TUNNEL_SCOPE_RUN_ID: 'run-1',
364+
AGENT_DEVICE_COMPANION_TUNNEL_SCOPE_LEASE_ID: 'lease-1',
365365
},
366366
stdio: ['ignore', 'pipe', 'pipe'],
367367
},
@@ -502,18 +502,18 @@ test('metro companion worker reconnects after the bridge closes immediately afte
502502

503503
const companion = spawn(
504504
process.execPath,
505-
['--experimental-strip-types', 'src/metro-companion.ts', '--agent-device-run-metro-companion'],
505+
['--experimental-strip-types', 'src/companion-tunnel.ts', '--agent-device-run-metro-companion'],
506506
{
507507
cwd: process.cwd(),
508508
env: {
509509
...process.env,
510-
AGENT_DEVICE_METRO_COMPANION_SERVER_BASE_URL: `http://127.0.0.1:${bridgePort}`,
511-
AGENT_DEVICE_METRO_COMPANION_BEARER_TOKEN: 'test-token',
512-
AGENT_DEVICE_METRO_COMPANION_LOCAL_BASE_URL: `http://127.0.0.1:${localPort}`,
513-
AGENT_DEVICE_METRO_COMPANION_REGISTER_PATH: '/api/metro/companion/register',
514-
AGENT_DEVICE_METRO_COMPANION_SCOPE_TENANT_ID: 'tenant-1',
515-
AGENT_DEVICE_METRO_COMPANION_SCOPE_RUN_ID: 'run-1',
516-
AGENT_DEVICE_METRO_COMPANION_SCOPE_LEASE_ID: 'lease-1',
510+
AGENT_DEVICE_COMPANION_TUNNEL_SERVER_BASE_URL: `http://127.0.0.1:${bridgePort}`,
511+
AGENT_DEVICE_COMPANION_TUNNEL_BEARER_TOKEN: 'test-token',
512+
AGENT_DEVICE_COMPANION_TUNNEL_LOCAL_BASE_URL: `http://127.0.0.1:${localPort}`,
513+
AGENT_DEVICE_COMPANION_TUNNEL_REGISTER_PATH: '/api/metro/companion/register',
514+
AGENT_DEVICE_COMPANION_TUNNEL_SCOPE_TENANT_ID: 'tenant-1',
515+
AGENT_DEVICE_COMPANION_TUNNEL_SCOPE_RUN_ID: 'run-1',
516+
AGENT_DEVICE_COMPANION_TUNNEL_SCOPE_LEASE_ID: 'lease-1',
517517
},
518518
stdio: ['ignore', 'pipe', 'pipe'],
519519
},
@@ -610,19 +610,19 @@ test('metro companion worker exits after its state file is removed', async () =>
610610

611611
const companion = spawn(
612612
process.execPath,
613-
['--experimental-strip-types', 'src/metro-companion.ts', '--agent-device-run-metro-companion'],
613+
['--experimental-strip-types', 'src/companion-tunnel.ts', '--agent-device-run-metro-companion'],
614614
{
615615
cwd: process.cwd(),
616616
env: {
617617
...process.env,
618-
AGENT_DEVICE_METRO_COMPANION_SERVER_BASE_URL: `http://127.0.0.1:${bridgePort}`,
619-
AGENT_DEVICE_METRO_COMPANION_BEARER_TOKEN: 'test-token',
620-
AGENT_DEVICE_METRO_COMPANION_LOCAL_BASE_URL: `http://127.0.0.1:${localPort}`,
621-
AGENT_DEVICE_METRO_COMPANION_REGISTER_PATH: '/api/metro/companion/register',
622-
AGENT_DEVICE_METRO_COMPANION_SCOPE_TENANT_ID: 'tenant-1',
623-
AGENT_DEVICE_METRO_COMPANION_SCOPE_RUN_ID: 'run-1',
624-
AGENT_DEVICE_METRO_COMPANION_SCOPE_LEASE_ID: 'lease-1',
625-
AGENT_DEVICE_METRO_COMPANION_STATE_PATH: statePath,
618+
AGENT_DEVICE_COMPANION_TUNNEL_SERVER_BASE_URL: `http://127.0.0.1:${bridgePort}`,
619+
AGENT_DEVICE_COMPANION_TUNNEL_BEARER_TOKEN: 'test-token',
620+
AGENT_DEVICE_COMPANION_TUNNEL_LOCAL_BASE_URL: `http://127.0.0.1:${localPort}`,
621+
AGENT_DEVICE_COMPANION_TUNNEL_REGISTER_PATH: '/api/metro/companion/register',
622+
AGENT_DEVICE_COMPANION_TUNNEL_SCOPE_TENANT_ID: 'tenant-1',
623+
AGENT_DEVICE_COMPANION_TUNNEL_SCOPE_RUN_ID: 'run-1',
624+
AGENT_DEVICE_COMPANION_TUNNEL_SCOPE_LEASE_ID: 'lease-1',
625+
AGENT_DEVICE_COMPANION_TUNNEL_STATE_PATH: statePath,
626626
},
627627
stdio: ['ignore', 'pipe', 'pipe'],
628628
},
@@ -649,7 +649,7 @@ test('metro companion worker exits after its state file is removed', async () =>
649649
assert.equal(exit.code, 0, `unexpected worker stderr: ${stderr}`);
650650
});
651651

652-
test('metro companion worker exits immediately when its state file is already missing', async () => {
652+
test('metro companion compatibility launcher reads legacy env and exits when state file is missing', async () => {
653653
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-metro-companion-worker-'));
654654
const statePath = path.join(tempRoot, 'missing-metro-companion.json');
655655
cleanupTasks.push(async () => {

src/__tests__/client-metro-companion.test.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ function assertCompanionSpawnTarget(): void {
4141
assert.ok(firstCall, 'expected runCmdDetached to be called');
4242
assert.equal(firstCall[0], process.execPath);
4343
assert.ok(
44-
firstCall[1].some((arg) => arg.includes('src/metro-companion.ts')),
44+
firstCall[1].some((arg) => arg.includes('src/companion-tunnel.ts')),
4545
`expected companion entry path in ${JSON.stringify(firstCall[1])}`,
4646
);
4747
assert.equal(firstCall[1].at(-1), '--agent-device-run-metro-companion');
@@ -251,6 +251,48 @@ test('metro and React DevTools companions use distinct profile state paths', asy
251251
}
252252
});
253253

254+
test('spawned companion uses neutral env names and clears stale legacy names', async () => {
255+
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-companion-env-'));
256+
try {
257+
vi.mocked(runCmdDetached).mockReturnValueOnce(999);
258+
vi.mocked(readProcessStartTime).mockReturnValue('start-999');
259+
vi.mocked(readProcessCommand).mockReturnValue(
260+
`${process.execPath} src/companion-tunnel.ts --agent-device-run-metro-companion`,
261+
);
262+
263+
await ensureMetroCompanion({
264+
projectRoot,
265+
serverBaseUrl: 'https://bridge.example.test',
266+
bearerToken: 'token',
267+
localBaseUrl: 'http://127.0.0.1:8081',
268+
bridgeScope: TEST_BRIDGE_SCOPE,
269+
launchUrl: 'myapp://open',
270+
env: {
271+
AGENT_DEVICE_METRO_COMPANION_SERVER_BASE_URL: 'https://legacy.example.test',
272+
AGENT_DEVICE_METRO_COMPANION_BEARER_TOKEN: 'legacy-token',
273+
AGENT_DEVICE_METRO_COMPANION_LOCAL_BASE_URL: 'http://127.0.0.1:9999',
274+
AGENT_DEVICE_METRO_COMPANION_LAUNCH_URL: 'legacy://open',
275+
},
276+
});
277+
278+
const env = vi.mocked(runCmdDetached).mock.calls[0]?.[2]?.env;
279+
assert.equal(env?.AGENT_DEVICE_COMPANION_TUNNEL_SERVER_BASE_URL, 'https://bridge.example.test');
280+
assert.equal(env?.AGENT_DEVICE_COMPANION_TUNNEL_BEARER_TOKEN, 'token');
281+
assert.equal(env?.AGENT_DEVICE_COMPANION_TUNNEL_LOCAL_BASE_URL, 'http://127.0.0.1:8081');
282+
assert.equal(env?.AGENT_DEVICE_COMPANION_TUNNEL_LAUNCH_URL, 'myapp://open');
283+
assert.equal(env?.AGENT_DEVICE_COMPANION_TUNNEL_REGISTER_PATH, '/api/metro/companion/register');
284+
assert.equal(env?.AGENT_DEVICE_COMPANION_TUNNEL_SCOPE_TENANT_ID, 'tenant-1');
285+
assert.equal(env?.AGENT_DEVICE_COMPANION_TUNNEL_SCOPE_RUN_ID, 'run-1');
286+
assert.equal(env?.AGENT_DEVICE_COMPANION_TUNNEL_SCOPE_LEASE_ID, 'lease-1');
287+
assert.equal(env?.AGENT_DEVICE_METRO_COMPANION_SERVER_BASE_URL, undefined);
288+
assert.equal(env?.AGENT_DEVICE_METRO_COMPANION_BEARER_TOKEN, undefined);
289+
assert.equal(env?.AGENT_DEVICE_METRO_COMPANION_LOCAL_BASE_URL, undefined);
290+
assert.equal(env?.AGENT_DEVICE_METRO_COMPANION_LAUNCH_URL, undefined);
291+
} finally {
292+
fs.rmSync(projectRoot, { recursive: true, force: true });
293+
}
294+
});
295+
254296
test('legacy state without bridge scope is stopped before respawn', async () => {
255297
const projectRoot = fs.mkdtempSync(
256298
path.join(os.tmpdir(), 'agent-device-metro-companion-legacy-'),

src/cli/commands/connection-runtime.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { resolveDaemonPaths } from '../../daemon/config.ts';
22
import { stopReactDevtoolsCompanion } from '../../client-react-devtools-companion.ts';
33
import { stopMetroTunnel } from '../../metro.ts';
44
import { resolveRemoteConfigProfile } from '../../remote-config.ts';
5-
import type { CompanionTunnelScope as MetroBridgeScope } from '../../client-companion-tunnel-contract.ts';
5+
import type { MetroBridgeScope } from '../../client-companion-tunnel-contract.ts';
66
import {
77
buildRemoteConnectionDaemonState,
88
hashRemoteConfigFile,

src/client-companion-tunnel-contract.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,31 @@ export const COMPANION_TUNNEL_RECONNECT_DELAY_MS = 1_000;
44
export const COMPANION_TUNNEL_LEASE_CHECK_INTERVAL_MS = 250;
55
export const WS_READY_STATE_OPEN = 1;
66

7-
export const ENV_COMPANION_TUNNEL_SERVER_BASE_URL = 'AGENT_DEVICE_METRO_COMPANION_SERVER_BASE_URL';
8-
export const ENV_COMPANION_TUNNEL_BEARER_TOKEN = 'AGENT_DEVICE_METRO_COMPANION_BEARER_TOKEN';
9-
export const ENV_COMPANION_TUNNEL_LOCAL_BASE_URL = 'AGENT_DEVICE_METRO_COMPANION_LOCAL_BASE_URL';
10-
export const ENV_COMPANION_TUNNEL_LAUNCH_URL = 'AGENT_DEVICE_METRO_COMPANION_LAUNCH_URL';
11-
export const ENV_COMPANION_TUNNEL_STATE_PATH = 'AGENT_DEVICE_METRO_COMPANION_STATE_PATH';
12-
export const ENV_COMPANION_TUNNEL_SCOPE_TENANT_ID = 'AGENT_DEVICE_METRO_COMPANION_SCOPE_TENANT_ID';
13-
export const ENV_COMPANION_TUNNEL_SCOPE_RUN_ID = 'AGENT_DEVICE_METRO_COMPANION_SCOPE_RUN_ID';
14-
export const ENV_COMPANION_TUNNEL_SCOPE_LEASE_ID = 'AGENT_DEVICE_METRO_COMPANION_SCOPE_LEASE_ID';
15-
export const ENV_COMPANION_TUNNEL_REGISTER_PATH = 'AGENT_DEVICE_METRO_COMPANION_REGISTER_PATH';
16-
export const ENV_COMPANION_TUNNEL_UNREGISTER_PATH = 'AGENT_DEVICE_METRO_COMPANION_UNREGISTER_PATH';
17-
export const ENV_COMPANION_TUNNEL_DEVICE_PORT = 'AGENT_DEVICE_METRO_COMPANION_DEVICE_PORT';
18-
export const ENV_COMPANION_TUNNEL_SESSION = 'AGENT_DEVICE_METRO_COMPANION_SESSION';
7+
export const ENV_COMPANION_TUNNEL_SERVER_BASE_URL = 'AGENT_DEVICE_COMPANION_TUNNEL_SERVER_BASE_URL';
8+
export const ENV_COMPANION_TUNNEL_BEARER_TOKEN = 'AGENT_DEVICE_COMPANION_TUNNEL_BEARER_TOKEN';
9+
export const ENV_COMPANION_TUNNEL_LOCAL_BASE_URL = 'AGENT_DEVICE_COMPANION_TUNNEL_LOCAL_BASE_URL';
10+
export const ENV_COMPANION_TUNNEL_LAUNCH_URL = 'AGENT_DEVICE_COMPANION_TUNNEL_LAUNCH_URL';
11+
export const ENV_COMPANION_TUNNEL_STATE_PATH = 'AGENT_DEVICE_COMPANION_TUNNEL_STATE_PATH';
12+
export const ENV_COMPANION_TUNNEL_SCOPE_TENANT_ID = 'AGENT_DEVICE_COMPANION_TUNNEL_SCOPE_TENANT_ID';
13+
export const ENV_COMPANION_TUNNEL_SCOPE_RUN_ID = 'AGENT_DEVICE_COMPANION_TUNNEL_SCOPE_RUN_ID';
14+
export const ENV_COMPANION_TUNNEL_SCOPE_LEASE_ID = 'AGENT_DEVICE_COMPANION_TUNNEL_SCOPE_LEASE_ID';
15+
export const ENV_COMPANION_TUNNEL_REGISTER_PATH = 'AGENT_DEVICE_COMPANION_TUNNEL_REGISTER_PATH';
16+
export const ENV_COMPANION_TUNNEL_UNREGISTER_PATH = 'AGENT_DEVICE_COMPANION_TUNNEL_UNREGISTER_PATH';
17+
export const ENV_COMPANION_TUNNEL_DEVICE_PORT = 'AGENT_DEVICE_COMPANION_TUNNEL_DEVICE_PORT';
18+
export const ENV_COMPANION_TUNNEL_SESSION = 'AGENT_DEVICE_COMPANION_TUNNEL_SESSION';
1919

2020
export type CompanionTunnelScope = {
2121
tenantId: string;
2222
runId: string;
2323
leaseId: string;
2424
};
2525

26+
export type MetroBridgeScope = CompanionTunnelScope;
27+
28+
export class MissingCompanionEnvError extends Error {
29+
override name = 'MissingCompanionEnvError';
30+
}
31+
2632
export type CompanionTunnelWorkerOptions = {
2733
serverBaseUrl: string;
2834
bearerToken: string;

src/client-companion-tunnel-env.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const CURRENT_ENV_PREFIX = 'AGENT_DEVICE_COMPANION_TUNNEL_';
2+
const LEGACY_ENV_PREFIX = 'AGENT_DEVICE_METRO_COMPANION_';
3+
4+
type CompanionTunnelEnvName = `${typeof CURRENT_ENV_PREFIX}${string}`;
5+
6+
function legacyEnvName(name: CompanionTunnelEnvName): string {
7+
return name.replace(CURRENT_ENV_PREFIX, LEGACY_ENV_PREFIX);
8+
}
9+
10+
export function setWorkerEnv(
11+
env: NodeJS.ProcessEnv,
12+
name: CompanionTunnelEnvName,
13+
value: string,
14+
): void {
15+
env[name] = value;
16+
delete env[legacyEnvName(name)];
17+
}
18+
19+
export function clearWorkerEnv(env: NodeJS.ProcessEnv, name: CompanionTunnelEnvName): void {
20+
delete env[name];
21+
delete env[legacyEnvName(name)];
22+
}
23+
24+
export function readWorkerEnv(
25+
env: NodeJS.ProcessEnv,
26+
name: CompanionTunnelEnvName,
27+
): string | undefined {
28+
const value = env[name]?.trim();
29+
if (value) return value;
30+
return env[legacyEnvName(name)]?.trim() || undefined;
31+
}

src/client-companion-tunnel-worker.ts

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ import {
1818
METRO_COMPANION_RUN_ARG,
1919
REACT_DEVTOOLS_COMPANION_RUN_ARG,
2020
WS_READY_STATE_OPEN,
21+
MissingCompanionEnvError,
2122
type CompanionTunnelWorkerOptions,
2223
} from './client-companion-tunnel-contract.ts';
24+
import { readWorkerEnv } from './client-companion-tunnel-env.ts';
2325
import type {
2426
MetroTunnelRequestMessage as MetroCompanionRequest,
2527
MetroTunnelResponseMessage,
@@ -446,21 +448,27 @@ function readWorkerOptions(
446448
if (commandArg !== METRO_COMPANION_RUN_ARG && commandArg !== REACT_DEVTOOLS_COMPANION_RUN_ARG) {
447449
return null;
448450
}
449-
const serverBaseUrl = env[ENV_COMPANION_TUNNEL_SERVER_BASE_URL]?.trim();
450-
const bearerToken = env[ENV_COMPANION_TUNNEL_BEARER_TOKEN]?.trim();
451-
const localBaseUrl = env[ENV_COMPANION_TUNNEL_LOCAL_BASE_URL]?.trim();
451+
const serverBaseUrl = readWorkerEnv(env, ENV_COMPANION_TUNNEL_SERVER_BASE_URL);
452+
const bearerToken = readWorkerEnv(env, ENV_COMPANION_TUNNEL_BEARER_TOKEN);
453+
const localBaseUrl = readWorkerEnv(env, ENV_COMPANION_TUNNEL_LOCAL_BASE_URL);
452454
if (!serverBaseUrl || !bearerToken || !localBaseUrl) {
453-
throw new Error('Companion tunnel worker is missing required environment configuration.');
455+
throw new MissingCompanionEnvError(
456+
'Companion tunnel worker is missing required environment configuration.',
457+
);
454458
}
455-
const tenantId = env[ENV_COMPANION_TUNNEL_SCOPE_TENANT_ID]?.trim();
456-
const runId = env[ENV_COMPANION_TUNNEL_SCOPE_RUN_ID]?.trim();
457-
const leaseId = env[ENV_COMPANION_TUNNEL_SCOPE_LEASE_ID]?.trim();
459+
const tenantId = readWorkerEnv(env, ENV_COMPANION_TUNNEL_SCOPE_TENANT_ID);
460+
const runId = readWorkerEnv(env, ENV_COMPANION_TUNNEL_SCOPE_RUN_ID);
461+
const leaseId = readWorkerEnv(env, ENV_COMPANION_TUNNEL_SCOPE_LEASE_ID);
458462
if (!tenantId || !runId || !leaseId) {
459-
throw new Error('Companion tunnel worker is missing required bridge scope configuration.');
463+
throw new MissingCompanionEnvError(
464+
'Companion tunnel worker is missing required bridge scope configuration.',
465+
);
460466
}
461-
const registerPath = env[ENV_COMPANION_TUNNEL_REGISTER_PATH]?.trim();
467+
const registerPath = readWorkerEnv(env, ENV_COMPANION_TUNNEL_REGISTER_PATH);
462468
if (!registerPath) {
463-
throw new Error('Companion tunnel worker is missing required register path configuration.');
469+
throw new MissingCompanionEnvError(
470+
'Companion tunnel worker is missing required register path configuration.',
471+
);
464472
}
465473
return {
466474
serverBaseUrl,
@@ -472,11 +480,11 @@ function readWorkerOptions(
472480
runId,
473481
leaseId,
474482
},
475-
launchUrl: env[ENV_COMPANION_TUNNEL_LAUNCH_URL]?.trim() || undefined,
476-
statePath: env[ENV_COMPANION_TUNNEL_STATE_PATH]?.trim() || undefined,
477-
unregisterPath: env[ENV_COMPANION_TUNNEL_UNREGISTER_PATH]?.trim() || undefined,
478-
devicePort: parseDevicePort(env[ENV_COMPANION_TUNNEL_DEVICE_PORT]),
479-
session: env[ENV_COMPANION_TUNNEL_SESSION]?.trim() || undefined,
483+
launchUrl: readWorkerEnv(env, ENV_COMPANION_TUNNEL_LAUNCH_URL),
484+
statePath: readWorkerEnv(env, ENV_COMPANION_TUNNEL_STATE_PATH),
485+
unregisterPath: readWorkerEnv(env, ENV_COMPANION_TUNNEL_UNREGISTER_PATH),
486+
devicePort: parseDevicePort(readWorkerEnv(env, ENV_COMPANION_TUNNEL_DEVICE_PORT)),
487+
session: readWorkerEnv(env, ENV_COMPANION_TUNNEL_SESSION),
480488
};
481489
}
482490

0 commit comments

Comments
 (0)