mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
e6b4fa2810
* fix: isolate concurrent remote connections * refactor: harden remote connection state * fix(cli): scope every emitted connect command to its own session `scopeNextSteps` only reached `ConnectReadiness.nextSteps`, so two command-bearing outputs still shipped unscoped: - `providerArtifactNotes()` emitted `agent-device artifacts --json` as a prose note, which never passes through that helper. - `buildDeferredRuntimeNotice()` emitted `agent-device metro prepare --remote-config <path>` independently in connection.ts. On the shared-host concurrency path this branch fixes, following either one resolves against the host-global active connection, so the artifacts instruction can return another job's provider video and log URLs (#1659). Both producers now take the connection state and format through one exported `scopeCommand` helper, which is the single place a suggested command is bound to its originating session. The metro config path is shell-quoted alongside the session name. Coverage: the BrowserStack route asserts human and JSON shapes carry one `--session` per suggested command and that each emitted session resolves back through `readRemoteConnectionState` to the connection that printed it; `connection status` pins the scoped deferred-metro `nextStep`. --------- Co-authored-by: Claude <noreply@anthropic.com>
72 lines
2.3 KiB
TypeScript
72 lines
2.3 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { mkdtempForTestSync } from '../../src/__tests__/test-utils/tmp-dir.ts';
|
|
import { readRemoteConnectionState } from '../../src/remote/remote-connection-state.ts';
|
|
import { runSourceCliJsonSync } from './cli-json.ts';
|
|
|
|
test('separate CLI processes do not adopt and overwrite the ambient remote session', () => {
|
|
const root = mkdtempForTestSync('agent-device-connect-process-isolation-');
|
|
const stateDir = path.join(root, 'state');
|
|
const firstConfig = writeRemoteConfig(root, 'first', 'iPhone 15 Pro', '17');
|
|
const secondConfig = writeRemoteConfig(root, 'second', 'iPad Pro 11 2025', '26');
|
|
|
|
const first = connectInSeparateProcess(stateDir, firstConfig);
|
|
const second = connectInSeparateProcess(stateDir, secondConfig);
|
|
|
|
assert.equal(first.status, 0, first.stderr);
|
|
assert.equal(second.status, 0, second.stderr);
|
|
const firstSession = readConnectedSession(first.json);
|
|
const secondSession = readConnectedSession(second.json);
|
|
assert.notEqual(secondSession, firstSession);
|
|
assert.equal(
|
|
readRemoteConnectionState({ stateDir, session: firstSession })?.remoteConfigPath,
|
|
firstConfig,
|
|
);
|
|
assert.equal(
|
|
readRemoteConnectionState({ stateDir, session: secondSession })?.remoteConfigPath,
|
|
secondConfig,
|
|
);
|
|
});
|
|
|
|
function writeRemoteConfig(
|
|
root: string,
|
|
runId: string,
|
|
device: string,
|
|
providerOsVersion: string,
|
|
): string {
|
|
const configPath = path.join(root, `${runId}.remote.json`);
|
|
fs.writeFileSync(
|
|
configPath,
|
|
`${JSON.stringify({
|
|
daemonBaseUrl: 'https://daemon.example.test/agent-device',
|
|
tenant: 'browserstack',
|
|
runId,
|
|
sessionIsolation: 'tenant',
|
|
platform: 'ios',
|
|
device,
|
|
providerOsVersion,
|
|
})}\n`,
|
|
);
|
|
return configPath;
|
|
}
|
|
|
|
function connectInSeparateProcess(stateDir: string, remoteConfig: string) {
|
|
return runSourceCliJsonSync([
|
|
'connect',
|
|
'--remote-config',
|
|
remoteConfig,
|
|
'--state-dir',
|
|
stateDir,
|
|
'--force',
|
|
'--json',
|
|
]);
|
|
}
|
|
|
|
function readConnectedSession(output: unknown): string {
|
|
const session = (output as { data?: { session?: unknown } } | undefined)?.data?.session;
|
|
assert.ok(typeof session === 'string');
|
|
return session;
|
|
}
|