Skip to content

Commit bfb8f1f

Browse files
committed
fix: carry remote-config run id for install-from-source
1 parent d4ee32f commit bfb8f1f

2 files changed

Lines changed: 111 additions & 3 deletions

File tree

src/__tests__/cli-config.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,103 @@ test('remote config defaults override generic config and env for remote workflow
265265
fs.rmSync(root, { recursive: true, force: true });
266266
});
267267

268+
test('install-from-source forwards remote-config run id with explicit lease binding', async () => {
269+
const { root, home, project } = makeTempWorkspace();
270+
const remoteConfig = path.join(project, 'agent-device.remote.json');
271+
fs.writeFileSync(
272+
remoteConfig,
273+
JSON.stringify({
274+
daemonBaseUrl: 'http://remote-mac.example.test:9124/agent-device',
275+
tenant: 'micha-pierzcha-a',
276+
sessionIsolation: 'tenant',
277+
runId: 'demo-run-001',
278+
platform: 'android',
279+
}),
280+
'utf8',
281+
);
282+
283+
let stdout = '';
284+
let stderr = '';
285+
let code: number | null = null;
286+
const calls: Array<Omit<DaemonRequest, 'token'>> = [];
287+
288+
const originalExit = process.exit;
289+
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
290+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
291+
const originalCwd = process.cwd();
292+
const previousEnv = new Map<string, string | undefined>();
293+
294+
process.chdir(project);
295+
previousEnv.set('HOME', process.env.HOME);
296+
process.env.HOME = home;
297+
298+
(process as any).exit = ((nextCode?: number) => {
299+
throw new ExitSignal(nextCode ?? 0);
300+
}) as typeof process.exit;
301+
(process.stdout as any).write = ((chunk: unknown) => {
302+
stdout += String(chunk);
303+
return true;
304+
}) as typeof process.stdout.write;
305+
(process.stderr as any).write = ((chunk: unknown) => {
306+
stderr += String(chunk);
307+
return true;
308+
}) as typeof process.stderr.write;
309+
310+
const sendToDaemon = async (req: Omit<DaemonRequest, 'token'>): Promise<DaemonResponse> => {
311+
calls.push(req);
312+
return {
313+
ok: true,
314+
data: {
315+
launchTarget: 'com.example.demo',
316+
packageName: 'com.example.demo',
317+
},
318+
};
319+
};
320+
321+
try {
322+
await runCli(
323+
[
324+
'install-from-source',
325+
'https://example.com/app.apk',
326+
'--remote-config',
327+
remoteConfig,
328+
'--lease-id',
329+
'lease-demo-001',
330+
'--json',
331+
],
332+
{ sendToDaemon },
333+
);
334+
} catch (error) {
335+
if (error instanceof ExitSignal) code = error.code;
336+
else throw error;
337+
} finally {
338+
process.exit = originalExit;
339+
process.stdout.write = originalStdoutWrite;
340+
process.stderr.write = originalStderrWrite;
341+
process.chdir(originalCwd);
342+
for (const [key, value] of previousEnv.entries()) {
343+
if (value === undefined) delete process.env[key];
344+
else process.env[key] = value;
345+
}
346+
}
347+
348+
assert.equal(code, null);
349+
assert.equal(stderr, '');
350+
const payload = JSON.parse(stdout);
351+
assert.equal(payload.success, true);
352+
assert.equal(payload.data.launchTarget, 'com.example.demo');
353+
assert.equal(calls.length, 1);
354+
assert.equal(calls[0]?.meta?.tenantId, 'micha-pierzcha-a');
355+
assert.equal(calls[0]?.meta?.runId, 'demo-run-001');
356+
assert.equal(calls[0]?.meta?.leaseId, 'lease-demo-001');
357+
assert.equal(calls[0]?.flags?.tenant, 'micha-pierzcha-a');
358+
assert.equal(calls[0]?.flags?.runId, 'demo-run-001');
359+
assert.equal(calls[0]?.flags?.leaseId, 'lease-demo-001');
360+
assert.deepEqual(calls[0]?.positionals, []);
361+
362+
fs.rmSync(root, { recursive: true, force: true });
363+
});
364+
268365
test('missing explicit remote config path returns parse error before daemon dispatch', async () => {
269366
const { root, home, project } = makeTempWorkspace();
270367

src/utils/remote-config.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
11
import type { CliFlags } from './command-schema.ts';
2-
import { REMOTE_OPEN_PROFILE_KEYS, type RemoteConfigProfile } from '../remote-config-schema.ts';
2+
import {
3+
REMOTE_CONFIG_FIELD_SPECS,
4+
REMOTE_OPEN_PROFILE_KEYS,
5+
type RemoteConfigProfile,
6+
} from '../remote-config-schema.ts';
37
import { resolveRemoteConfigProfile } from '../remote-config-core.ts';
48

5-
// This subset is intentional: only the flags used by remote-config-backed CLI defaults belong here.
9+
// Remote config can supply defaults for any supported CLI flag that exists in the profile schema.
10+
// Command validation later strips unsupported defaults for the active command.
11+
const REMOTE_CONFIG_DEFAULT_FLAG_KEYS = REMOTE_CONFIG_FIELD_SPECS.map(
12+
(spec) => spec.key,
13+
) as readonly (keyof RemoteConfigProfile)[];
14+
15+
// This subset is still intentional: open preserves extra remote-config defaults after command
16+
// parsing so it can prepare remote Metro without exposing every Metro flag as an open CLI option.
617
export const REMOTE_OPEN_FLAG_KEYS = [
718
'remoteConfig',
819
...REMOTE_OPEN_PROFILE_KEYS,
920
] as const satisfies readonly (keyof CliFlags)[];
1021

1122
function profileToCliFlags(profile: RemoteConfigProfile): Partial<CliFlags> {
1223
const flags: Partial<CliFlags> = {};
13-
for (const key of REMOTE_OPEN_PROFILE_KEYS) {
24+
for (const key of REMOTE_CONFIG_DEFAULT_FLAG_KEYS) {
1425
const value = profile[key];
1526
if (value !== undefined) {
1627
(flags as Record<string, unknown>)[key] = value;

0 commit comments

Comments
 (0)