|
1 | 1 | import { test } from 'vitest'; |
2 | 2 | import assert from 'node:assert/strict'; |
| 3 | +import fs from 'node:fs'; |
| 4 | +import os from 'node:os'; |
| 5 | +import path from 'node:path'; |
3 | 6 | import { createAgentDeviceClient, type AgentDeviceClientConfig } from '../client.ts'; |
4 | 7 | import type { DaemonRequest, DaemonResponse } from '../daemon/types.ts'; |
5 | 8 | import { AppError } from '../utils/errors.ts'; |
@@ -347,6 +350,169 @@ test('client throws AppError for daemon failures', async () => { |
347 | 350 | ); |
348 | 351 | }); |
349 | 352 |
|
| 353 | +test('client.command forwards typed back options through shared request metadata', async () => { |
| 354 | + const setup = createTransport(async () => ({ |
| 355 | + ok: true, |
| 356 | + data: { |
| 357 | + action: 'back', |
| 358 | + mode: 'system', |
| 359 | + }, |
| 360 | + })); |
| 361 | + const client = createAgentDeviceClient(setup.config, { transport: setup.transport }); |
| 362 | + |
| 363 | + const result = await client.command('back', { |
| 364 | + mode: 'system', |
| 365 | + platform: 'android', |
| 366 | + }); |
| 367 | + |
| 368 | + assert.equal(setup.calls.length, 1); |
| 369 | + assert.equal(setup.calls[0]?.session, 'qa'); |
| 370 | + assert.equal(setup.calls[0]?.command, 'back'); |
| 371 | + assert.deepEqual(setup.calls[0]?.positionals, []); |
| 372 | + assert.equal(setup.calls[0]?.flags?.backMode, 'system'); |
| 373 | + assert.equal(setup.calls[0]?.flags?.platform, 'android'); |
| 374 | + assert.equal(setup.calls[0]?.flags?.daemonBaseUrl, 'http://daemon.example.test'); |
| 375 | + assert.equal(setup.calls[0]?.meta?.tenantId, 'acme'); |
| 376 | + assert.equal(setup.calls[0]?.meta?.runId, 'run-123'); |
| 377 | + assert.equal(setup.calls[0]?.meta?.leaseId, 'lease-123'); |
| 378 | + assert.deepEqual(result, { |
| 379 | + action: 'back', |
| 380 | + mode: 'system', |
| 381 | + }); |
| 382 | +}); |
| 383 | + |
| 384 | +test('client.command prepares wait selector options without raw daemon requests', async () => { |
| 385 | + const setup = createTransport(async () => ({ |
| 386 | + ok: true, |
| 387 | + data: { |
| 388 | + selector: 'role=button[name="Continue"]', |
| 389 | + waitedMs: 120, |
| 390 | + }, |
| 391 | + })); |
| 392 | + const client = createAgentDeviceClient(setup.config, { transport: setup.transport }); |
| 393 | + |
| 394 | + const result = await client.command('wait', { |
| 395 | + selector: 'role=button[name="Continue"]', |
| 396 | + timeoutMs: 1_500, |
| 397 | + depth: 3, |
| 398 | + raw: true, |
| 399 | + }); |
| 400 | + |
| 401 | + assert.equal(setup.calls.length, 1); |
| 402 | + assert.equal(setup.calls[0]?.command, 'wait'); |
| 403 | + assert.deepEqual(setup.calls[0]?.positionals, ['role=button[name="Continue"]', '1500']); |
| 404 | + assert.equal(setup.calls[0]?.flags?.snapshotDepth, 3); |
| 405 | + assert.equal(setup.calls[0]?.flags?.snapshotRaw, true); |
| 406 | + assert.deepEqual(result, { |
| 407 | + selector: 'role=button[name="Continue"]', |
| 408 | + waitedMs: 120, |
| 409 | + }); |
| 410 | +}); |
| 411 | + |
| 412 | +test('client.command maps appSwitcher alias to daemon app-switcher command', async () => { |
| 413 | + const setup = createTransport(async () => ({ |
| 414 | + ok: true, |
| 415 | + data: { |
| 416 | + action: 'app-switcher', |
| 417 | + }, |
| 418 | + })); |
| 419 | + const client = createAgentDeviceClient(setup.config, { transport: setup.transport }); |
| 420 | + |
| 421 | + await client.command('appSwitcher', {}); |
| 422 | + |
| 423 | + assert.equal(setup.calls.length, 1); |
| 424 | + assert.equal(setup.calls[0]?.command, 'app-switcher'); |
| 425 | + assert.deepEqual(setup.calls[0]?.positionals, []); |
| 426 | +}); |
| 427 | + |
| 428 | +test('remote-config defaults are scoped to the command escape hatch', async () => { |
| 429 | + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-client-remote-scope-')); |
| 430 | + try { |
| 431 | + const remoteConfig = path.join(tempRoot, 'remote.json'); |
| 432 | + fs.writeFileSync( |
| 433 | + remoteConfig, |
| 434 | + JSON.stringify({ |
| 435 | + session: 'remote-session', |
| 436 | + platform: 'android', |
| 437 | + daemonBaseUrl: 'http://127.0.0.1:9124/agent-device', |
| 438 | + }), |
| 439 | + ); |
| 440 | + const setup = createTransport(async () => ({ |
| 441 | + ok: true, |
| 442 | + data: { |
| 443 | + devices: [], |
| 444 | + }, |
| 445 | + })); |
| 446 | + const client = createAgentDeviceClient( |
| 447 | + { |
| 448 | + session: 'local-session', |
| 449 | + remoteConfig, |
| 450 | + cwd: tempRoot, |
| 451 | + }, |
| 452 | + { transport: setup.transport }, |
| 453 | + ); |
| 454 | + |
| 455 | + await client.devices.list(); |
| 456 | + |
| 457 | + assert.equal(setup.calls.length, 1); |
| 458 | + assert.equal(setup.calls[0]?.session, 'local-session'); |
| 459 | + assert.equal(setup.calls[0]?.command, 'devices'); |
| 460 | + assert.equal(setup.calls[0]?.flags?.platform, undefined); |
| 461 | + assert.equal(setup.calls[0]?.flags?.daemonBaseUrl, undefined); |
| 462 | + } finally { |
| 463 | + fs.rmSync(tempRoot, { recursive: true, force: true }); |
| 464 | + } |
| 465 | +}); |
| 466 | + |
| 467 | +test('client.command applies remote-config defaults before daemon transport', async () => { |
| 468 | + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-client-remote-config-')); |
| 469 | + try { |
| 470 | + const remoteConfig = path.join(tempRoot, 'remote.json'); |
| 471 | + fs.writeFileSync( |
| 472 | + remoteConfig, |
| 473 | + JSON.stringify({ |
| 474 | + session: 'remote-session', |
| 475 | + platform: 'android', |
| 476 | + daemonBaseUrl: 'http://127.0.0.1:9124/agent-device', |
| 477 | + tenant: 'remote-tenant', |
| 478 | + sessionIsolation: 'tenant', |
| 479 | + runId: 'remote-run', |
| 480 | + leaseId: 'remote-lease', |
| 481 | + }), |
| 482 | + ); |
| 483 | + const setup = createTransport(async () => ({ |
| 484 | + ok: true, |
| 485 | + data: { |
| 486 | + action: 'home', |
| 487 | + }, |
| 488 | + })); |
| 489 | + const client = createAgentDeviceClient( |
| 490 | + { |
| 491 | + remoteConfig, |
| 492 | + cwd: tempRoot, |
| 493 | + }, |
| 494 | + { transport: setup.transport }, |
| 495 | + ); |
| 496 | + |
| 497 | + await client.command('home', {}); |
| 498 | + |
| 499 | + assert.equal(setup.calls.length, 1); |
| 500 | + assert.equal(setup.calls[0]?.session, 'remote-session'); |
| 501 | + assert.equal(setup.calls[0]?.command, 'home'); |
| 502 | + assert.equal(setup.calls[0]?.flags?.platform, 'android'); |
| 503 | + assert.equal(setup.calls[0]?.flags?.daemonBaseUrl, 'http://127.0.0.1:9124/agent-device'); |
| 504 | + assert.equal(setup.calls[0]?.flags?.tenant, 'remote-tenant'); |
| 505 | + assert.equal(setup.calls[0]?.flags?.sessionIsolation, 'tenant'); |
| 506 | + assert.equal(setup.calls[0]?.flags?.runId, 'remote-run'); |
| 507 | + assert.equal(setup.calls[0]?.flags?.leaseId, 'remote-lease'); |
| 508 | + assert.equal(setup.calls[0]?.meta?.tenantId, 'remote-tenant'); |
| 509 | + assert.equal(setup.calls[0]?.meta?.runId, 'remote-run'); |
| 510 | + assert.equal(setup.calls[0]?.meta?.leaseId, 'remote-lease'); |
| 511 | + } finally { |
| 512 | + fs.rmSync(tempRoot, { recursive: true, force: true }); |
| 513 | + } |
| 514 | +}); |
| 515 | + |
350 | 516 | test('client capture.snapshot preserves visibility metadata from daemon responses', async () => { |
351 | 517 | const setup = createTransport(async () => ({ |
352 | 518 | ok: true, |
|
0 commit comments