diff --git a/packages/stim-cli/src/__tests__/device-remote.test.ts b/packages/stim-cli/src/__tests__/device-remote.test.ts index 5c47d8874..f718c13ea 100644 --- a/packages/stim-cli/src/__tests__/device-remote.test.ts +++ b/packages/stim-cli/src/__tests__/device-remote.test.ts @@ -128,7 +128,6 @@ describe('explicit backend selection', () => { test('proxy requires both daemon variables with a precise remedy', async () => { const missingToken = await resolveRemoteContext({ root, - label: 'wt', backend: 'proxy', easBin: '/bin/eas', env: { AGENT_DEVICE_DAEMON_BASE_URL: env.AGENT_DEVICE_DAEMON_BASE_URL }, @@ -142,7 +141,6 @@ describe('explicit backend selection', () => { const missingUrl = await resolveRemoteContext({ root, - label: 'wt', backend: 'proxy', easBin: '/bin/eas', env: { AGENT_DEVICE_DAEMON_AUTH_TOKEN: env.AGENT_DEVICE_DAEMON_AUTH_TOKEN }, @@ -158,7 +156,6 @@ describe('explicit backend selection', () => { test('proxy uses only the supplied daemon and never creates an EAS session', async () => { const resolved = await resolveRemoteContext({ root, - label: 'wt', backend: 'proxy', easBin: '/bin/eas', env, @@ -180,7 +177,6 @@ describe('explicit backend selection', () => { test('eas requires the EAS CLI even when proxy variables are present', async () => { const resolved = await resolveRemoteContext({ root, - label: 'wt', backend: 'eas', easBin: null, env, @@ -196,7 +192,6 @@ describe('explicit backend selection', () => { test('eas ignores proxy variables and creates an EAS session', async () => { const resolved = await resolveRemoteContext({ root, - label: 'wt', backend: 'eas', easBin: '/bin/eas', env, @@ -258,7 +253,6 @@ process.stdout.write(${JSON.stringify(CREATED)}); const resolved = await resolveRemoteContext({ root, - label: 'wt', backend: 'eas', easBin, env: process.env, @@ -287,6 +281,39 @@ process.stdout.write(${JSON.stringify(CREATED)}); ); }); +describe('each workspace names its own remote session', () => { + async function bootNames(workspaceRoot: string) { + resetExecutor(); + const resolved = await resolveRemoteContext({ + root: workspaceRoot, + backend: 'eas', + easBin: '/bin/eas', + env: {}, + lookupAgentDevice: () => '/bin/agent-device', + }); + if (!('ctx' in resolved)) throw new Error(resolved.failed); + const exec = mockExec({ outputs: { sim: CREATED } }); + await remoteIosDeps(resolved.ctx).ensureBooted({}); + const sim = exec.calls.find((call) => call.file === '/bin/eas' && call.args[0] === 'sim'); + return { + eas: sim?.args[sim.args.indexOf('--name') + 1], + agentDevice: JSON.parse(readFileSync(remoteProfilePath(workspaceRoot), 'utf-8')).session, + }; + } + + test('two worktrees whose app directories share a name do not share a session', async () => { + const first = join(root, 'a', 'app'); + const second = join(root, 'b', 'app'); + mkdirSync(first, { recursive: true }); + mkdirSync(second, { recursive: true }); + const one = await bootNames(first); + const two = await bootNames(second); + expect(one.agentDevice).not.toBe(two.agentDevice); + expect(one.eas).not.toBe(two.eas); + expect(one.eas).toMatch(/^stim-app-[0-9a-f]{16}$/); + }); +}); + describe('the expensive step happens after the Metro gate', () => { test('ensureOwnedDevice creates no session and runs no command', async () => { const exec = mockExec(); diff --git a/packages/stim-cli/src/commands/android.ts b/packages/stim-cli/src/commands/android.ts index 0fc29d8b5..6a584fc18 100644 --- a/packages/stim-cli/src/commands/android.ts +++ b/packages/stim-cli/src/commands/android.ts @@ -832,7 +832,6 @@ export async function runAndroid(options: RunAndroidOptions = {} as RunAndroidOp if (remoteBackend) { const resolved = await resolveRemoteDeviceContext({ root, - label, platform: PLATFORM, backend: remoteBackend, easBin: resolveEasBin(root)?.file ?? null, diff --git a/packages/stim-cli/src/commands/ios.ts b/packages/stim-cli/src/commands/ios.ts index d3ef4aded..036ff848b 100644 --- a/packages/stim-cli/src/commands/ios.ts +++ b/packages/stim-cli/src/commands/ios.ts @@ -473,13 +473,11 @@ async function runIos( const registerProject = () => d.upsertProject(root, { bundleId: d.detectBundleId(root) ?? undefined, isExpo }); if (remoteBackend !== 'eas') registerProject(); const proj = d.getProject(root); - const label = d.projectShortcut(root, proj); let remoteDevice: ReturnType | null = null; if (remoteBackend) { const resolved = await d.resolveRemoteContext({ root, - label, backend: remoteBackend, easBin: d.resolveEasCliBin(root)?.file ?? null, }); diff --git a/packages/stim-cli/src/commands/ios/dependencies.ts b/packages/stim-cli/src/commands/ios/dependencies.ts index f28ce273b..0db45dde3 100644 --- a/packages/stim-cli/src/commands/ios/dependencies.ts +++ b/packages/stim-cli/src/commands/ios/dependencies.ts @@ -52,7 +52,7 @@ import { } from '../../engine/xcode.ts'; import { pidExists, resolveProjectMetro } from '../../metro.ts'; import { createNdjsonWriter } from '../../ndjson.ts'; -import { detectBundleId, detectIsExpo, findProjectRoot, projectShortcut } from '../../workspace/project.ts'; +import { detectBundleId, detectIsExpo, findProjectRoot } from '../../workspace/project.ts'; import { resolveCacheProviderConfig, resolveSettings } from '../../workspace/settings.ts'; import { writeWorkspaceLaunch } from '../../supervisor/state.ts'; import { readWorkspaceState, writeWorkspaceState } from '../../workspace/workspace-state.ts'; @@ -79,7 +79,6 @@ export interface IosDeps { devClientScheme: typeof devClientScheme; getProject: typeof getProject; upsertProject: typeof upsertProject; - projectShortcut: typeof projectShortcut; checkDeviceCapacity: typeof checkDeviceCapacity; ensureOwnedDevice: typeof ensureOwnedDevice; listIosRuntimes: typeof listIosRuntimes; @@ -157,7 +156,6 @@ export const DEFAULT_DEPS: IosDeps = { devClientScheme, getProject, upsertProject, - projectShortcut, checkDeviceCapacity, ensureOwnedDevice, listIosRuntimes, diff --git a/packages/stim-cli/src/engine/device-remote.ts b/packages/stim-cli/src/engine/device-remote.ts index 227cf5477..9181c34b5 100644 --- a/packages/stim-cli/src/engine/device-remote.ts +++ b/packages/stim-cli/src/engine/device-remote.ts @@ -4,8 +4,9 @@ import { getExecutor } from '../exec.ts'; import { readJsonObject } from '../json-file.ts'; import { pidExists } from '../metro.ts'; import { gateMetroOrigin, REMOTE_METRO_WRONG } from './metro-gate.ts'; -import { workspaceDir, workspaceLogsDir, workspaceStateFile } from '../workspace/paths.ts'; +import { workspaceDir, workspaceId, workspaceLogsDir, workspaceStateFile } from '../workspace/paths.ts'; import { clearRemoteSession, readMetroTunnel, readRemoteSession } from '../supervisor/state.ts'; +import { ownedDeviceLabel } from '../workspace/project.ts'; import { acceptAlertArgs, closeArgs, @@ -424,7 +425,6 @@ function remoteDeviceDeps(ctx: RemoteContext) { export async function resolveRemoteContext({ root, - label, backend, platform = 'ios', easBin, @@ -433,7 +433,6 @@ export async function resolveRemoteContext({ maxDurationMinutes = null, }: { root: string; - label: string; backend: RemoteDeviceBackend; platform?: 'ios' | 'android'; easBin: string | null; @@ -492,7 +491,8 @@ export async function resolveRemoteContext({ return { ctx: { root, - label, + // agent-device stores remote connection state per session name under ~/.agent-device/remote-connections/. + label: `${ownedDeviceLabel(root)}-${workspaceId(root)}`, backend, platform, easBin: easBin ?? '',