From 42a798a6d7e898bd7cdae8e71bebe07189e71865 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 12 Oct 2026 17:43:40 +0200 Subject: [PATCH 01/14] refactor(daemon): cut daemon test edges into root data fixtures and fakes Daemon tests and fixtures no longer import the root test-utils and plugin fixture modules: - device-inventory-gateways: daemon policy tests get a support fake at the ComposedDeviceInventoryGateways port composed from contracts projection and provider-request scoping only; root composition tests keep the real helper. - replay-script-source: daemon tests get a support fake that builds client replay source bundles through the package replay-backend registry and a deterministic suite walk, with no root command-layer imports. - android-snapshot-capture, android-ui-hierarchy-fixtures (plus the archived raw capture JSON) move under src/daemon/__tests__/support/ and are imported from the root test trees that share them (root -> daemon test support is an allowed direction); plugin.fixtures moves to @agent-device/test-kit as a data-only fixture over package vocabulary. --- packages/test-kit/package.json | 4 + .../test-kit/src}/plugin.fixtures.ts | 2 +- .../android-ui-hierarchy-scope.test.ts | 2 +- src/__tests__/android-ui-hierarchy.test.ts | 2 +- src/__tests__/cloud-connect-testmu.test.ts | 2 +- .../lease-provider-credentials.test.ts | 2 +- .../snapshot-alert-android-occlusion.test.ts | 2 +- .../provider-device-runtimes.test.ts | 6 +- src/cli/commands/__tests__/plugins.test.ts | 2 +- src/cli/connection/provider-policy.test.ts | 2 +- src/commands/schema/cli-config.test.ts | 2 +- .../snapshot-chrome-android-statusbar.test.ts | 2 +- src/core/interactors/android.test.ts | 2 +- .../android-system-dialog-occlusion.test.ts | 2 +- .../android-system-dialog-ref-frame.test.ts | 2 +- src/daemon/__tests__/back-runtime.test.ts | 2 +- src/daemon/__tests__/daemon-policy.test.ts | 2 +- src/daemon/__tests__/focus-runtime.test.ts | 2 +- .../__tests__/human-control-router-fixture.ts | 2 +- .../__tests__/orientation-runtime.test.ts | 2 +- .../__tests__/replay-device-selection.test.ts | 2 +- ...eplay-divergence-android-occlusion.test.ts | 2 +- ...on-replay-divergence-chrome-filter.test.ts | 2 +- .../session-replay-divergence-overlay.test.ts | 2 +- ...on-replay-maestro-error-projection.test.ts | 2 +- .../session-replay-maestro-failure.test.ts | 2 +- ...n-replay-maestro-remote-evalscript.test.ts | 2 +- ...ion-replay-maestro-session-address.test.ts | 2 +- ...on-replay-runtime-maestro-dispatch.test.ts | 2 +- ...ay-runtime-maestro-shell-artifacts.test.ts | 2 +- ...session-replay-runtime-maestro.fixtures.ts | 2 +- .../replay-runtime/application.test.ts | 2 +- .../session-replay-runtime-failure.test.ts | 2 +- .../session-replay-runtime.fixtures.ts | 2 +- .../session-replay-script-source.test.ts | 2 +- .../session-test-command-trust.test.ts | 2 +- .../session-test-source-discovery.test.ts | 2 +- .../session-test-suite-command-video.test.ts | 2 +- .../replay-suite/session-test-suite.test.ts | 2 +- .../__tests__/request-handler-catalog.test.ts | 2 +- .../request-router-apps-admission.test.ts | 2 +- .../request-router-artifacts-web.test.ts | 2 +- .../__tests__/request-router-cost.test.ts | 2 +- ...request-router-custom-action-flags.test.ts | 2 +- .../request-router-developer-dir.test.ts | 2 +- .../request-router-error-redaction.test.ts | 2 +- .../__tests__/request-router-events.test.ts | 2 +- .../request-router-idle-expired.test.ts | 2 +- .../request-router-open-claim.test.ts | 2 +- .../__tests__/request-router-open.test.ts | 4 +- .../request-router-record-flags.test.ts | 2 +- .../request-router-repair-expired.test.ts | 2 +- .../request-router-replay-env.test.ts | 4 +- .../request-router-response-level.test.ts | 2 +- .../request-router-session-address.test.ts | 2 +- .../request-router-typed-error.test.ts | 2 +- .../request-runtime-binding-router.test.ts | 2 +- .../request-save-script-transports.test.ts | 2 +- .../selector-capture-binding.test.ts | 2 +- ...shot-custom-actions-platform-guard.test.ts | 2 +- .../support}/android-ime-capture.raw.json | 0 .../android-qs-shade-capture.raw.json | 0 .../support}/android-snapshot-capture.ts | 0 .../support}/android-ui-hierarchy-fixtures.ts | 0 .../support/device-inventory-gateways.ts | 154 ++++++++++++++++++ .../__tests__/support/replay-script-source.ts | 152 +++++++++++++++++ .../__tests__/system-button-runtime.test.ts | 2 +- .../__tests__/test-device-runtime-gateway.ts | 2 +- .../__tests__/tv-remote-runtime.test.ts | 2 +- src/daemon/__tests__/viewport-runtime.test.ts | 2 +- .../session-appstate-device-runtime.test.ts | 2 +- .../__tests__/session-command-harness.ts | 2 +- .../__tests__/session-command-replay.test.ts | 2 +- .../__tests__/session-doctor-device.test.ts | 2 +- .../__tests__/session-doctor-warmup.test.ts | 2 +- .../__tests__/session-relaunch-close.test.ts | 2 +- .../__tests__/session-capabilities.test.ts | 2 +- .../session-devices-batch-runtime.test.ts | 2 +- .../session-open-url-prewarm.test.ts | 2 +- src/plugins/load.test.ts | 2 +- src/plugins/manifest.test.ts | 2 +- src/plugins/store.test.ts | 2 +- src/provider-credential-fingerprint.test.ts | 2 +- 83 files changed, 392 insertions(+), 78 deletions(-) rename {src/plugins => packages/test-kit/src}/plugin.fixtures.ts (98%) rename src/{__tests__/test-utils => daemon/__tests__/support}/android-ime-capture.raw.json (100%) rename src/{__tests__/test-utils => daemon/__tests__/support}/android-qs-shade-capture.raw.json (100%) rename src/{__tests__/test-utils => daemon/__tests__/support}/android-snapshot-capture.ts (100%) rename src/{__tests__/test-utils => daemon/__tests__/support}/android-ui-hierarchy-fixtures.ts (100%) create mode 100644 src/daemon/__tests__/support/device-inventory-gateways.ts create mode 100644 src/daemon/__tests__/support/replay-script-source.ts diff --git a/packages/test-kit/package.json b/packages/test-kit/package.json index b508b8c19a..ba9fc020f5 100644 --- a/packages/test-kit/package.json +++ b/packages/test-kit/package.json @@ -57,6 +57,10 @@ "types": "./src/loopback.ts", "default": "./src/loopback.ts" }, + "./plugin.fixtures": { + "types": "./src/plugin.fixtures.ts", + "default": "./src/plugin.fixtures.ts" + }, "./property-arbitraries": { "types": "./src/property-arbitraries.ts", "default": "./src/property-arbitraries.ts" diff --git a/src/plugins/plugin.fixtures.ts b/packages/test-kit/src/plugin.fixtures.ts similarity index 98% rename from src/plugins/plugin.fixtures.ts rename to packages/test-kit/src/plugin.fixtures.ts index 88d1082f10..bce8780eac 100644 --- a/src/plugins/plugin.fixtures.ts +++ b/packages/test-kit/src/plugin.fixtures.ts @@ -2,7 +2,7 @@ import crypto from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; import type { ProviderProfileField } from '@agent-device/contracts/provider-profile-fields'; -import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; +import { mkdtempForTestSync } from './tmp-dir.ts'; export function pluginHome() { const home = mkdtempForTestSync('provider-plugins-'); diff --git a/src/__tests__/android-ui-hierarchy-scope.test.ts b/src/__tests__/android-ui-hierarchy-scope.test.ts index a74f7e0adf..e954dfd01c 100644 --- a/src/__tests__/android-ui-hierarchy-scope.test.ts +++ b/src/__tests__/android-ui-hierarchy-scope.test.ts @@ -3,7 +3,7 @@ import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import { buildSnapshotState } from '@agent-device/capture-kit/snapshot-state'; -import { parseUiHierarchy } from './test-utils/android-ui-hierarchy-fixtures.ts'; +import { parseUiHierarchy } from '../daemon/__tests__/support/android-ui-hierarchy-fixtures.ts'; import { AndroidSnapshotPresentationFailure, isAndroidSnapshotPresentationFailure, diff --git a/src/__tests__/android-ui-hierarchy.test.ts b/src/__tests__/android-ui-hierarchy.test.ts index 0fc7ef29ed..be2170a61e 100644 --- a/src/__tests__/android-ui-hierarchy.test.ts +++ b/src/__tests__/android-ui-hierarchy.test.ts @@ -9,7 +9,7 @@ import { androidUiNodes, parseUiHierarchyTree, } from '@agent-device/platform-android/mechanics'; -import { parseUiHierarchy } from './test-utils/android-ui-hierarchy-fixtures.ts'; +import { parseUiHierarchy } from '../daemon/__tests__/support/android-ui-hierarchy-fixtures.ts'; function publishUiHierarchy(xml: string) { const captured = parseUiHierarchy(xml, 800, {}); diff --git a/src/__tests__/cloud-connect-testmu.test.ts b/src/__tests__/cloud-connect-testmu.test.ts index d72aafac68..068ad8ae4a 100644 --- a/src/__tests__/cloud-connect-testmu.test.ts +++ b/src/__tests__/cloud-connect-testmu.test.ts @@ -12,7 +12,7 @@ import { verifyTestMuConnection } from '@agent-device/testmu/connection-verifica import testMuPlugin from '@agent-device/testmu'; import { createPluginHost } from '../plugins/host.ts'; import { resolveConnectProviderProfile } from '../cli/connection/connect-provider-adapters.ts'; -import { selectPlugin, pluginHome } from '../plugins/plugin.fixtures.ts'; +import { selectPlugin, pluginHome } from '@agent-device/test-kit/plugin.fixtures'; import { installedPlugins } from '../plugins/store.ts'; import manifest from '@agent-device/testmu/package.json' with { type: 'json' }; import type { PluginConnection } from '../plugins/connection.ts'; diff --git a/src/__tests__/daemon-composition/handlers/lease-provider-credentials.test.ts b/src/__tests__/daemon-composition/handlers/lease-provider-credentials.test.ts index 9c7a8e2b3d..c7ef858510 100644 --- a/src/__tests__/daemon-composition/handlers/lease-provider-credentials.test.ts +++ b/src/__tests__/daemon-composition/handlers/lease-provider-credentials.test.ts @@ -11,7 +11,7 @@ import { providerCredentialFingerprint, readDaemonProviderCredentials, } from '../../../provider-credential-fingerprint.ts'; -import { pluginHome, selectPluginManifest } from '../../../plugins/plugin.fixtures.ts'; +import { pluginHome, selectPluginManifest } from '@agent-device/test-kit/plugin.fixtures'; /** * Lease refusal compares the client's credential fingerprint against the digest the daemon diff --git a/src/__tests__/daemon-composition/handlers/snapshot-alert-android-occlusion.test.ts b/src/__tests__/daemon-composition/handlers/snapshot-alert-android-occlusion.test.ts index e9464c2087..8aa00780a0 100644 --- a/src/__tests__/daemon-composition/handlers/snapshot-alert-android-occlusion.test.ts +++ b/src/__tests__/daemon-composition/handlers/snapshot-alert-android-occlusion.test.ts @@ -13,7 +13,7 @@ vi.mock('@agent-device/platform-android/mechanics', async (importOriginal) => { import { pressAndroid, snapshotAndroid } from '@agent-device/platform-android/mechanics'; import { ANDROID_EMULATOR } from '@agent-device/test-kit/device-fixtures'; import { createAndroidInteractor } from '../../../core/interactors/android.ts'; -import { makeAndroidSnapshotCapture } from '../../test-utils/android-snapshot-capture.ts'; +import { makeAndroidSnapshotCapture } from '../../../daemon/__tests__/support/android-snapshot-capture.ts'; // R59 moved the alert legs onto the Android interactor, which is where the occlusion reading // they depend on lives: the legs capture through the same presentation pass `snapshot` publishes, diff --git a/src/__tests__/provider-device-runtimes.test.ts b/src/__tests__/provider-device-runtimes.test.ts index 9971c34546..75a87e39cf 100644 --- a/src/__tests__/provider-device-runtimes.test.ts +++ b/src/__tests__/provider-device-runtimes.test.ts @@ -4,7 +4,11 @@ import { createDefaultProviderRuntimeComposition, createDaemonProviderRuntimeComposition, } from '../provider-device-runtimes.ts'; -import { pluginHome, selectPlugin, registrationSource } from '../plugins/plugin.fixtures.ts'; +import { + pluginHome, + selectPlugin, + registrationSource, +} from '@agent-device/test-kit/plugin.fixtures'; test('daemon composition registers installed provider runtimes and their lazy platform modules', async () => { const { home, env } = pluginHome(); diff --git a/src/cli/commands/__tests__/plugins.test.ts b/src/cli/commands/__tests__/plugins.test.ts index 897f063c04..fa9fba17da 100644 --- a/src/cli/commands/__tests__/plugins.test.ts +++ b/src/cli/commands/__tests__/plugins.test.ts @@ -3,7 +3,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { afterEach, test, vi } from 'vitest'; import { runCli } from '../../../cli.ts'; -import { pluginHome, selectPlugin } from '../../../plugins/plugin.fixtures.ts'; +import { pluginHome, selectPlugin } from '@agent-device/test-kit/plugin.fixtures'; import { parseRawArgs, usageForCommand } from '../../parser/args.ts'; import { beginStdoutCapture } from '../../../__tests__/test-utils/stream-capture.ts'; diff --git a/src/cli/connection/provider-policy.test.ts b/src/cli/connection/provider-policy.test.ts index b6ced9d546..42b1c5b3ca 100644 --- a/src/cli/connection/provider-policy.test.ts +++ b/src/cli/connection/provider-policy.test.ts @@ -3,7 +3,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { test } from 'vitest'; import { connectionProviderCapabilities, isConnectProviderName } from './provider-policy.ts'; -import { pluginHome, selectPlugin } from '../../plugins/plugin.fixtures.ts'; +import { pluginHome, selectPlugin } from '@agent-device/test-kit/plugin.fixtures'; import { installedPlugins } from '../../plugins/store.ts'; test('provider policy projects provider identity into semantic capabilities', () => { diff --git a/src/commands/schema/cli-config.test.ts b/src/commands/schema/cli-config.test.ts index c5b0e245f7..06f7cd0492 100644 --- a/src/commands/schema/cli-config.test.ts +++ b/src/commands/schema/cli-config.test.ts @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import { test } from 'vitest'; -import { pluginHome } from '../../plugins/plugin.fixtures.ts'; +import { pluginHome } from '@agent-device/test-kit/plugin.fixtures'; import { resolveConfigBackedFlagDefaults } from './cli-config.ts'; test('AGENT_DEVICE_HOME relocates user defaults; project and explicit config cannot select plugins', () => { diff --git a/src/core/__tests__/snapshot-chrome-android-statusbar.test.ts b/src/core/__tests__/snapshot-chrome-android-statusbar.test.ts index 594a50c060..453c421ca0 100644 --- a/src/core/__tests__/snapshot-chrome-android-statusbar.test.ts +++ b/src/core/__tests__/snapshot-chrome-android-statusbar.test.ts @@ -15,7 +15,7 @@ import { ANDROID_QS_SHADE_CAPTURE_RAW_NODES, walkInteractiveOnlyAndroidFixture, walkNonRawAndroidFixture, -} from '../../__tests__/test-utils/android-ui-hierarchy-fixtures.ts'; +} from '../../daemon/__tests__/support/android-ui-hierarchy-fixtures.ts'; import { isAndroidSystemChromeWindowResourceId } from '@agent-device/contracts/android-system-chrome'; /** diff --git a/src/core/interactors/android.test.ts b/src/core/interactors/android.test.ts index fa58f526a8..b201174560 100644 --- a/src/core/interactors/android.test.ts +++ b/src/core/interactors/android.test.ts @@ -10,7 +10,7 @@ import { scrollAndroid, snapshotAndroid, } from '@agent-device/platform-android/mechanics'; -import { makeAndroidSnapshotCapture } from '../../__tests__/test-utils/android-snapshot-capture.ts'; +import { makeAndroidSnapshotCapture } from '../../daemon/__tests__/support/android-snapshot-capture.ts'; vi.mock('@agent-device/platform-android/mechanics', async (importOriginal) => { const actual = await importOriginal(); diff --git a/src/daemon/__tests__/android-system-dialog-occlusion.test.ts b/src/daemon/__tests__/android-system-dialog-occlusion.test.ts index 35970cbd9e..71b75e9ea2 100644 --- a/src/daemon/__tests__/android-system-dialog-occlusion.test.ts +++ b/src/daemon/__tests__/android-system-dialog-occlusion.test.ts @@ -14,7 +14,7 @@ import { recoverAndroidBlockingSystemDialog as recoverOwnedAndroidBlockingSystem import { androidObservation } from '../../platform-runtime.ts'; import { makeAndroidSession } from './support/session-factories.ts'; import { makeTestScreenRecordingResource } from './support/screen-recording-live-handle.ts'; -import { makeAndroidSnapshotCapture } from '../../__tests__/test-utils/android-snapshot-capture.ts'; +import { makeAndroidSnapshotCapture } from './support/android-snapshot-capture.ts'; const recoverAndroidBlockingSystemDialog = ( params: Omit[0], 'observation'>, diff --git a/src/daemon/__tests__/android-system-dialog-ref-frame.test.ts b/src/daemon/__tests__/android-system-dialog-ref-frame.test.ts index e87668bee2..d5f632894d 100644 --- a/src/daemon/__tests__/android-system-dialog-ref-frame.test.ts +++ b/src/daemon/__tests__/android-system-dialog-ref-frame.test.ts @@ -12,7 +12,7 @@ import { recoverAndroidBlockingSystemDialog as recoverOwnedAndroidBlockingSystem import { androidObservation } from '../../platform-runtime.ts'; import { makeAndroidSession } from './support/session-factories.ts'; import { makeTestScreenRecordingResource } from './support/screen-recording-live-handle.ts'; -import { makeAndroidSnapshotCapture } from '../../__tests__/test-utils/android-snapshot-capture.ts'; +import { makeAndroidSnapshotCapture } from './support/android-snapshot-capture.ts'; import { refFrameState } from '../ref-frame.ts'; const recoverAndroidBlockingSystemDialog = ( diff --git a/src/daemon/__tests__/back-runtime.test.ts b/src/daemon/__tests__/back-runtime.test.ts index 63697502b9..327a339669 100644 --- a/src/daemon/__tests__/back-runtime.test.ts +++ b/src/daemon/__tests__/back-runtime.test.ts @@ -16,7 +16,7 @@ import { import { deviceShape } from '@agent-device/kernel/device'; import { makeSession } from './support/session-factories.ts'; import { makeSessionStore } from './support/store-factory.ts'; -import { createTestDeviceInventoryGateways } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; import { LeaseRegistry } from '../lease-registry.ts'; import { activateCompleteRefFrame, refFrameState } from '../ref-frame.ts'; import type { BindDeviceRuntime, InspectDeviceRuntimeFacts } from '../request-runtime-binding.ts'; diff --git a/src/daemon/__tests__/daemon-policy.test.ts b/src/daemon/__tests__/daemon-policy.test.ts index 63b8ea9262..b4bbec166a 100644 --- a/src/daemon/__tests__/daemon-policy.test.ts +++ b/src/daemon/__tests__/daemon-policy.test.ts @@ -8,7 +8,7 @@ vi.mock('../device/device-ready.ts', () => ({ ensureDeviceReady: vi.fn(async () import type { DeviceInfo } from '@agent-device/kernel/device'; import { ANDROID_EMULATOR, IOS_SIMULATOR } from '@agent-device/test-kit/device-fixtures'; -import { createTestDeviceInventoryGateways } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; import { makeIosSession } from './support/session-factories.ts'; import { makeSessionStore } from './support/store-factory.ts'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; diff --git a/src/daemon/__tests__/focus-runtime.test.ts b/src/daemon/__tests__/focus-runtime.test.ts index e6e93b94e9..d0ac6fd933 100644 --- a/src/daemon/__tests__/focus-runtime.test.ts +++ b/src/daemon/__tests__/focus-runtime.test.ts @@ -16,7 +16,7 @@ import { import { deviceShape } from '@agent-device/kernel/device'; import { makeSession } from './support/session-factories.ts'; import { makeSessionStore } from './support/store-factory.ts'; -import { createTestDeviceInventoryGateways } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; import { LeaseRegistry } from '../lease-registry.ts'; import { activateCompleteRefFrame, refFrameState } from '../ref-frame.ts'; import type { BindDeviceRuntime, InspectDeviceRuntimeFacts } from '../request-runtime-binding.ts'; diff --git a/src/daemon/__tests__/human-control-router-fixture.ts b/src/daemon/__tests__/human-control-router-fixture.ts index 6d986b41e4..b001cdc289 100644 --- a/src/daemon/__tests__/human-control-router-fixture.ts +++ b/src/daemon/__tests__/human-control-router-fixture.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; import { daemonPlatformServicesFixture, daemonClaimRecoveryGatewayFixture, diff --git a/src/daemon/__tests__/orientation-runtime.test.ts b/src/daemon/__tests__/orientation-runtime.test.ts index bc97a0abcd..006b715332 100644 --- a/src/daemon/__tests__/orientation-runtime.test.ts +++ b/src/daemon/__tests__/orientation-runtime.test.ts @@ -39,7 +39,7 @@ import { import { deviceShape } from '@agent-device/kernel/device'; import { makeSession } from './support/session-factories.ts'; import { makeSessionStore } from './support/store-factory.ts'; -import { createTestDeviceInventoryGateways } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; import { LeaseRegistry } from '../lease-registry.ts'; import { activateCompleteRefFrame, refFrameState } from '../ref-frame.ts'; import type { BindDeviceRuntime, InspectDeviceRuntimeFacts } from '../request-runtime-binding.ts'; diff --git a/src/daemon/__tests__/replay-device-selection.test.ts b/src/daemon/__tests__/replay-device-selection.test.ts index 4811e3995d..439892cad7 100644 --- a/src/daemon/__tests__/replay-device-selection.test.ts +++ b/src/daemon/__tests__/replay-device-selection.test.ts @@ -12,7 +12,7 @@ import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; import { maestroScriptSourceBundleFor, replayScriptSourceBundleFor, -} from '../../__tests__/test-utils/replay-script-source.ts'; +} from './support/replay-script-source.ts'; test('replay leaves deep-link opens to normal device resolution', async () => { const root = mkdtempForTestSync('agent-device-replay-device-selection-'); diff --git a/src/daemon/__tests__/replay-divergence/session-replay-divergence-android-occlusion.test.ts b/src/daemon/__tests__/replay-divergence/session-replay-divergence-android-occlusion.test.ts index 3696abd78e..f688b30af3 100644 --- a/src/daemon/__tests__/replay-divergence/session-replay-divergence-android-occlusion.test.ts +++ b/src/daemon/__tests__/replay-divergence/session-replay-divergence-android-occlusion.test.ts @@ -5,7 +5,7 @@ import { makeAndroidSession } from '../support/session-factories.ts'; import { ANDROID_QS_SHADE_CAPTURE_RAW_NODES, walkNonRawAndroidFixture, -} from '../../../__tests__/test-utils/android-ui-hierarchy-fixtures.ts'; +} from '../support/android-ui-hierarchy-fixtures.ts'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; import { SessionStore } from '../../session-store.ts'; import { replayCoordinatorForTest, replaySessionForTest } from './replay-session-fixture.ts'; diff --git a/src/daemon/__tests__/replay-divergence/session-replay-divergence-chrome-filter.test.ts b/src/daemon/__tests__/replay-divergence/session-replay-divergence-chrome-filter.test.ts index 86e3c076f3..c749cb288d 100644 --- a/src/daemon/__tests__/replay-divergence/session-replay-divergence-chrome-filter.test.ts +++ b/src/daemon/__tests__/replay-divergence/session-replay-divergence-chrome-filter.test.ts @@ -5,7 +5,7 @@ import { makeAndroidSession, makeIosSession } from '../support/session-factories import { ANDROID_IME_CAPTURE_RAW_NODES, walkNonRawAndroidFixture, -} from '../../../__tests__/test-utils/android-ui-hierarchy-fixtures.ts'; +} from '../support/android-ui-hierarchy-fixtures.ts'; import { SessionStore } from '../../session-store.ts'; import { buildReplayFailureDivergence } from '@agent-device/replay-port/session-replay-divergence'; import { replayDivergenceForTest } from './replay-session-fixture.ts'; diff --git a/src/daemon/__tests__/replay-divergence/session-replay-divergence-overlay.test.ts b/src/daemon/__tests__/replay-divergence/session-replay-divergence-overlay.test.ts index 46bc5d297d..6d76ba5d6d 100644 --- a/src/daemon/__tests__/replay-divergence/session-replay-divergence-overlay.test.ts +++ b/src/daemon/__tests__/replay-divergence/session-replay-divergence-overlay.test.ts @@ -5,7 +5,7 @@ import { makeAndroidSession } from '../support/session-factories.ts'; import { ANDROID_IME_CAPTURE_RAW_NODES, walkNonRawAndroidFixture, -} from '../../../__tests__/test-utils/android-ui-hierarchy-fixtures.ts'; +} from '../support/android-ui-hierarchy-fixtures.ts'; import { SessionStore } from '../../session-store.ts'; import { buildReplayFailureDivergence } from '@agent-device/replay-port/session-replay-divergence'; import { replayDivergenceForTest } from './replay-session-fixture.ts'; diff --git a/src/daemon/__tests__/replay-maestro/session-replay-maestro-error-projection.test.ts b/src/daemon/__tests__/replay-maestro/session-replay-maestro-error-projection.test.ts index d8cd6f3708..8f92e4eb90 100644 --- a/src/daemon/__tests__/replay-maestro/session-replay-maestro-error-projection.test.ts +++ b/src/daemon/__tests__/replay-maestro/session-replay-maestro-error-projection.test.ts @@ -6,7 +6,7 @@ import { runTypedMaestroReplay } from '@agent-device/replay-port/session-replay- import { SessionStore } from '../../session-store.ts'; import { replayCommandForTest } from '../replay-runtime/replay-command-fixture.ts'; import { makeIosSession } from '../support/session-factories.ts'; -import { maestroScriptSourceBundleFor } from '../../../__tests__/test-utils/replay-script-source.ts'; +import { maestroScriptSourceBundleFor } from '../support/replay-script-source.ts'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; import type { DaemonRequest } from '../../daemon-request.ts'; diff --git a/src/daemon/__tests__/replay-maestro/session-replay-maestro-failure.test.ts b/src/daemon/__tests__/replay-maestro/session-replay-maestro-failure.test.ts index 7d067f2836..90e42da114 100644 --- a/src/daemon/__tests__/replay-maestro/session-replay-maestro-failure.test.ts +++ b/src/daemon/__tests__/replay-maestro/session-replay-maestro-failure.test.ts @@ -1,4 +1,4 @@ -import { noMaestroIncludeSources } from '../../../__tests__/test-utils/replay-script-source.ts'; +import { noMaestroIncludeSources } from '../support/replay-script-source.ts'; import { beforeEach, expect, test, vi } from 'vitest'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; diff --git a/src/daemon/__tests__/replay-maestro/session-replay-maestro-remote-evalscript.test.ts b/src/daemon/__tests__/replay-maestro/session-replay-maestro-remote-evalscript.test.ts index aeaeab97af..ea0ee6ceb0 100644 --- a/src/daemon/__tests__/replay-maestro/session-replay-maestro-remote-evalscript.test.ts +++ b/src/daemon/__tests__/replay-maestro/session-replay-maestro-remote-evalscript.test.ts @@ -7,7 +7,7 @@ import { LeaseRegistry } from '../../lease-registry.ts'; import { platformResourceCleanupFixture } from '../support/platform-resource-cleanup-fixture.ts'; import { SessionStore } from '../../session-store.ts'; import { makeIosSession } from '../support/session-factories.ts'; -import { maestroScriptSourceBundleFor } from '../../../__tests__/test-utils/replay-script-source.ts'; +import { maestroScriptSourceBundleFor } from '../support/replay-script-source.ts'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; import type { DaemonRequest } from '../../daemon-request.ts'; import { diff --git a/src/daemon/__tests__/replay-maestro/session-replay-maestro-session-address.test.ts b/src/daemon/__tests__/replay-maestro/session-replay-maestro-session-address.test.ts index 0f7e309025..49421eab9d 100644 --- a/src/daemon/__tests__/replay-maestro/session-replay-maestro-session-address.test.ts +++ b/src/daemon/__tests__/replay-maestro/session-replay-maestro-session-address.test.ts @@ -6,7 +6,7 @@ import { runTypedMaestroReplay } from '@agent-device/replay-port/session-replay- import { SessionStore } from '../../session-store.ts'; import { replayCommandForTest } from '../replay-runtime/replay-command-fixture.ts'; import { makeIosSession } from '../support/session-factories.ts'; -import { maestroScriptSourceBundleFor } from '../../../__tests__/test-utils/replay-script-source.ts'; +import { maestroScriptSourceBundleFor } from '../support/replay-script-source.ts'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; import type { DaemonRequest } from '../../daemon-request.ts'; diff --git a/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro-dispatch.test.ts b/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro-dispatch.test.ts index c5022b6de6..5398e2a545 100644 --- a/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro-dispatch.test.ts +++ b/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro-dispatch.test.ts @@ -6,7 +6,7 @@ import { test } from 'vitest'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; import type { DaemonRequest, DaemonResponse } from '../../daemon-request.ts'; import { SessionStore } from '../../session-store.ts'; -import { replayScriptSourceBundleFor } from '../../../__tests__/test-utils/replay-script-source.ts'; +import { replayScriptSourceBundleFor } from '../support/replay-script-source.ts'; import { makeIosSession } from '../support/session-factories.ts'; const { runReplayFixture, runReplayForTest, assertNoUnresolvedInterpolation } = diff --git a/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro-shell-artifacts.test.ts b/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro-shell-artifacts.test.ts index 0f189ffbb4..62657b35a3 100644 --- a/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro-shell-artifacts.test.ts +++ b/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro-shell-artifacts.test.ts @@ -5,7 +5,7 @@ import path from 'node:path'; import { test } from 'vitest'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; import { SessionStore } from '../../session-store.ts'; -import { replayScriptSourceBundleFor } from '../../../__tests__/test-utils/replay-script-source.ts'; +import { replayScriptSourceBundleFor } from '../support/replay-script-source.ts'; const { runReplayFixture, runReplayForTest } = maestroReplayFixture; diff --git a/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro.fixtures.ts b/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro.fixtures.ts index 4fc367d40a..d3d6cf244a 100644 --- a/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro.fixtures.ts +++ b/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro.fixtures.ts @@ -51,7 +51,7 @@ import type { ReplayScriptSourceBundle } from '@agent-device/contracts/replay'; import { maestroScriptSourceBundleFor, replayScriptSourceBundleFor, -} from '../../../__tests__/test-utils/replay-script-source.ts'; +} from '../support/replay-script-source.ts'; import { captureSnapshotThroughLegacyDispatchFixture } from '../legacy-snapshot-capture-fixture.ts'; import { captureSnapshotWithInteractor } from '../../snapshot-interactor-capture.ts'; import { seedReplayFixtureSession } from '../replay-runtime/session-replay-runtime.fixtures.ts'; diff --git a/src/daemon/__tests__/replay-runtime/application.test.ts b/src/daemon/__tests__/replay-runtime/application.test.ts index 0f3c95cafb..a87f605558 100644 --- a/src/daemon/__tests__/replay-runtime/application.test.ts +++ b/src/daemon/__tests__/replay-runtime/application.test.ts @@ -10,7 +10,7 @@ import { } from '@agent-device/host-kit/request'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; import { makeIosSession } from '../support/session-factories.ts'; -import { withClientReplayScriptSources } from '../../../__tests__/test-utils/replay-script-source.ts'; +import { withClientReplayScriptSources } from '../support/replay-script-source.ts'; import type { DaemonRequest, DaemonResponse } from '../../daemon-request.ts'; import { SessionStore } from '../../session-store.ts'; import { diff --git a/src/daemon/__tests__/replay-runtime/session-replay-runtime-failure.test.ts b/src/daemon/__tests__/replay-runtime/session-replay-runtime-failure.test.ts index 2f3e154f81..a5547e6ec5 100644 --- a/src/daemon/__tests__/replay-runtime/session-replay-runtime-failure.test.ts +++ b/src/daemon/__tests__/replay-runtime/session-replay-runtime-failure.test.ts @@ -16,7 +16,7 @@ import { SessionStore } from '../../session-store.ts'; import type { DaemonResponse } from '../../daemon-request.ts'; import { makeIosSession } from '../support/session-factories.ts'; import { formatReplayDivergenceReport } from '@agent-device/ad-replay/divergence'; -import { maestroScriptSourceBundleFor } from '../../../__tests__/test-utils/replay-script-source.ts'; +import { maestroScriptSourceBundleFor } from '../support/replay-script-source.ts'; import { captureSnapshotThroughLegacyDispatchFixture, legacyDispatchCapture, diff --git a/src/daemon/__tests__/replay-runtime/session-replay-runtime.fixtures.ts b/src/daemon/__tests__/replay-runtime/session-replay-runtime.fixtures.ts index 65ef66b68b..ba5361b210 100644 --- a/src/daemon/__tests__/replay-runtime/session-replay-runtime.fixtures.ts +++ b/src/daemon/__tests__/replay-runtime/session-replay-runtime.fixtures.ts @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { makeAndroidSession, makeIosSession } from '../support/session-factories.ts'; -import { replayScriptSourceBundleFor } from '../../../__tests__/test-utils/replay-script-source.ts'; +import { replayScriptSourceBundleFor } from '../support/replay-script-source.ts'; import type { SessionStore } from '../../session-store.ts'; import type { DaemonRequest } from '../../daemon-request.ts'; diff --git a/src/daemon/__tests__/replay-runtime/session-replay-script-source.test.ts b/src/daemon/__tests__/replay-runtime/session-replay-script-source.test.ts index 186f7d6661..0baef9cfa6 100644 --- a/src/daemon/__tests__/replay-runtime/session-replay-script-source.test.ts +++ b/src/daemon/__tests__/replay-runtime/session-replay-script-source.test.ts @@ -25,7 +25,7 @@ import { makeIosSession } from '../support/session-factories.ts'; import { maestroScriptSourceBundleFor, replayScriptSourceBundleFor, -} from '../../../__tests__/test-utils/replay-script-source.ts'; +} from '../support/replay-script-source.ts'; import { REPLAY_SCRIPT_SOURCE_REQUIRED_MESSAGE } from '@agent-device/replay-port/script-source'; import { captureSnapshotThroughLegacyDispatchFixture, diff --git a/src/daemon/__tests__/replay-suite/session-test-command-trust.test.ts b/src/daemon/__tests__/replay-suite/session-test-command-trust.test.ts index 88f31ab89c..0ce50f4a80 100644 --- a/src/daemon/__tests__/replay-suite/session-test-command-trust.test.ts +++ b/src/daemon/__tests__/replay-suite/session-test-command-trust.test.ts @@ -12,7 +12,7 @@ import { } from '../../handlers/session-replay-command.ts'; import { runReplayTestCommand } from '@agent-device/replay-port/test-command'; import type { ReplayCommand, ReplayTestCommand } from '@agent-device/replay-port/command-types'; -import { replayScriptSourceBundleFor } from '../../../__tests__/test-utils/replay-script-source.ts'; +import { replayScriptSourceBundleFor } from '../support/replay-script-source.ts'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; import type { DaemonInvokeFn, DaemonRequest } from '../../daemon-request.ts'; diff --git a/src/daemon/__tests__/replay-suite/session-test-source-discovery.test.ts b/src/daemon/__tests__/replay-suite/session-test-source-discovery.test.ts index 7d847dec4b..3b2afb5505 100644 --- a/src/daemon/__tests__/replay-suite/session-test-source-discovery.test.ts +++ b/src/daemon/__tests__/replay-suite/session-test-source-discovery.test.ts @@ -7,7 +7,7 @@ import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; import { maestroScriptSourceBundleFor, replayScriptSourceBundleFor, -} from '../../../__tests__/test-utils/replay-script-source.ts'; +} from '../support/replay-script-source.ts'; // #1478 P3b: the daemon adapter's inspection capability. #1802 moved expansion and file reading // to the caller, so what is left here is exactly format routing plus per-engine manifest diff --git a/src/daemon/__tests__/replay-suite/session-test-suite-command-video.test.ts b/src/daemon/__tests__/replay-suite/session-test-suite-command-video.test.ts index abe292545b..3fcacc2f64 100644 --- a/src/daemon/__tests__/replay-suite/session-test-suite-command-video.test.ts +++ b/src/daemon/__tests__/replay-suite/session-test-suite-command-video.test.ts @@ -13,7 +13,7 @@ import { splitReplayCommandRequest, } from '@agent-device/replay-port/replay-dispatch-envelope'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; -import { replayScriptSourceBundleFor } from '../../../__tests__/test-utils/replay-script-source.ts'; +import { replayScriptSourceBundleFor } from '../support/replay-script-source.ts'; import { unavailableBindDevice, unavailableBindExactDevice, diff --git a/src/daemon/__tests__/replay-suite/session-test-suite.test.ts b/src/daemon/__tests__/replay-suite/session-test-suite.test.ts index 05518921b3..f68728fb5e 100644 --- a/src/daemon/__tests__/replay-suite/session-test-suite.test.ts +++ b/src/daemon/__tests__/replay-suite/session-test-suite.test.ts @@ -32,7 +32,7 @@ import { registerRequestAbort, } from '@agent-device/host-kit/request'; -import { withTestDeviceInventoryProvider as withDeviceInventoryProvider } from '../../../__tests__/test-utils/device-inventory-gateways.ts'; +import { withTestDeviceInventoryProvider as withDeviceInventoryProvider } from '../support/device-inventory-gateways.ts'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { makeAndroidSession, makeMacOsSession } from '../support/session-factories.ts'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; diff --git a/src/daemon/__tests__/request-handler-catalog.test.ts b/src/daemon/__tests__/request-handler-catalog.test.ts index 9fce2657c6..53e0c28cac 100644 --- a/src/daemon/__tests__/request-handler-catalog.test.ts +++ b/src/daemon/__tests__/request-handler-catalog.test.ts @@ -3,7 +3,7 @@ import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import { test } from 'vitest'; -import { withTestDeviceInventoryProvider as withTargetDeviceResolutionScope } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { withTestDeviceInventoryProvider as withTargetDeviceResolutionScope } from './support/device-inventory-gateways.ts'; import { INTERNAL_COMMANDS, PUBLIC_COMMANDS } from '@agent-device/command-registry/catalog'; import { commandDescriptors } from '@agent-device/command-registry/registry'; import { isRequestCanceledError, type AppError } from '@agent-device/kernel/errors'; diff --git a/src/daemon/__tests__/request-router-apps-admission.test.ts b/src/daemon/__tests__/request-router-apps-admission.test.ts index 0b87c7c99b..e2b53fccb4 100644 --- a/src/daemon/__tests__/request-router-apps-admission.test.ts +++ b/src/daemon/__tests__/request-router-apps-admission.test.ts @@ -3,7 +3,7 @@ import { expect, test, vi } from 'vitest'; import path from 'node:path'; import type { DeviceRuntimeGateway } from '@agent-device/contracts/platform-runtime'; import type { PlatformRuntimeOperations } from '@agent-device/contracts/platform-runtime-operations'; -import { createTestDeviceInventoryGateways } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; import { makeSessionStore } from './support/store-factory.ts'; import { LeaseRegistry } from '../lease-registry.ts'; import type { DaemonRequest } from '../daemon-request.ts'; diff --git a/src/daemon/__tests__/request-router-artifacts-web.test.ts b/src/daemon/__tests__/request-router-artifacts-web.test.ts index 4132fd662a..3ffb59668e 100644 --- a/src/daemon/__tests__/request-router-artifacts-web.test.ts +++ b/src/daemon/__tests__/request-router-artifacts-web.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; import { test, expect } from 'vitest'; import fs from 'node:fs'; diff --git a/src/daemon/__tests__/request-router-cost.test.ts b/src/daemon/__tests__/request-router-cost.test.ts index 0037f5c7d1..d4dace0607 100644 --- a/src/daemon/__tests__/request-router-cost.test.ts +++ b/src/daemon/__tests__/request-router-cost.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; import { test, expect, vi, beforeEach } from 'vitest'; import path from 'node:path'; diff --git a/src/daemon/__tests__/request-router-custom-action-flags.test.ts b/src/daemon/__tests__/request-router-custom-action-flags.test.ts index 14771d8dd1..eee07bcd4a 100644 --- a/src/daemon/__tests__/request-router-custom-action-flags.test.ts +++ b/src/daemon/__tests__/request-router-custom-action-flags.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; /** * `--actions` reads custom actions through the private-AX snapshot path, which * a raw capture does not use. Answering the pair with a capture that diff --git a/src/daemon/__tests__/request-router-developer-dir.test.ts b/src/daemon/__tests__/request-router-developer-dir.test.ts index 73513b5f36..c6447ad5a9 100644 --- a/src/daemon/__tests__/request-router-developer-dir.test.ts +++ b/src/daemon/__tests__/request-router-developer-dir.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; import path from 'node:path'; import { expect, test } from 'vitest'; import { runCmd } from '@agent-device/host-kit/command'; diff --git a/src/daemon/__tests__/request-router-error-redaction.test.ts b/src/daemon/__tests__/request-router-error-redaction.test.ts index db1e92b841..f49ed9d033 100644 --- a/src/daemon/__tests__/request-router-error-redaction.test.ts +++ b/src/daemon/__tests__/request-router-error-redaction.test.ts @@ -18,7 +18,7 @@ vi.mock('../device/device-ready.ts', () => ({ ensureDeviceReady: vi.fn(async () import { AppError } from '@agent-device/kernel/errors'; import type { DeviceRuntimeGateway, RuntimeFacts } from '@agent-device/contracts/platform-runtime'; import type { PlatformRuntimeOperations } from '@agent-device/contracts/platform-runtime-operations'; -import { createTestDeviceInventoryGateways } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; import { makeIosAppSession } from './support/session-factories.ts'; import { makeSessionStore } from './support/store-factory.ts'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; diff --git a/src/daemon/__tests__/request-router-events.test.ts b/src/daemon/__tests__/request-router-events.test.ts index 2faa781ff0..84f816a1d7 100644 --- a/src/daemon/__tests__/request-router-events.test.ts +++ b/src/daemon/__tests__/request-router-events.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; import { test, expect } from 'vitest'; import fs from 'node:fs'; diff --git a/src/daemon/__tests__/request-router-idle-expired.test.ts b/src/daemon/__tests__/request-router-idle-expired.test.ts index 373c97e566..dfb67e1ae2 100644 --- a/src/daemon/__tests__/request-router-idle-expired.test.ts +++ b/src/daemon/__tests__/request-router-idle-expired.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; /** * #2833: when a command finds no session because this daemon already expired it for idleness, the * answer stays `SESSION_NOT_FOUND` but carries the typed reason, the window it missed, and the device diff --git a/src/daemon/__tests__/request-router-open-claim.test.ts b/src/daemon/__tests__/request-router-open-claim.test.ts index 84147da293..c6d011e490 100644 --- a/src/daemon/__tests__/request-router-open-claim.test.ts +++ b/src/daemon/__tests__/request-router-open-claim.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; import { test, expect, vi, beforeEach } from 'vitest'; import fs from 'node:fs'; diff --git a/src/daemon/__tests__/request-router-open.test.ts b/src/daemon/__tests__/request-router-open.test.ts index 79b0430fa6..3bbce80a3b 100644 --- a/src/daemon/__tests__/request-router-open.test.ts +++ b/src/daemon/__tests__/request-router-open.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; import { daemonPlatformServicesFixture, daemonClaimRecoveryGatewayFixture, @@ -10,7 +10,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { getResolveTargetDeviceMock } from './request-router-dispatch-mocks.ts'; -import { replayScriptSourceBundleFor } from '../../__tests__/test-utils/replay-script-source.ts'; +import { replayScriptSourceBundleFor } from './support/replay-script-source.ts'; import { fakeCreateCommandSurface } from './support/command-surface-fake.ts'; vi.mock('../device/device-ready.ts', () => ({ ensureDeviceReady: vi.fn(async () => {}) })); diff --git a/src/daemon/__tests__/request-router-record-flags.test.ts b/src/daemon/__tests__/request-router-record-flags.test.ts index c4b2ccea8c..388fbf3850 100644 --- a/src/daemon/__tests__/request-router-record-flags.test.ts +++ b/src/daemon/__tests__/request-router-record-flags.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; /** * #1271 stage 2 (ADR 0012 amendment): `--record` and `--no-record` express * opposite recording intents for the same action, so both together is diff --git a/src/daemon/__tests__/request-router-repair-expired.test.ts b/src/daemon/__tests__/request-router-repair-expired.test.ts index f83d373fb8..c96207d998 100644 --- a/src/daemon/__tests__/request-router-repair-expired.test.ts +++ b/src/daemon/__tests__/request-router-repair-expired.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; /** * ADR 0012 decision 6, R7 (C5a): when a repair session was reaped before it was * finalized, the request router rewrites the resulting `SESSION_NOT_FOUND` into diff --git a/src/daemon/__tests__/request-router-replay-env.test.ts b/src/daemon/__tests__/request-router-replay-env.test.ts index c658c386fb..277891664c 100644 --- a/src/daemon/__tests__/request-router-replay-env.test.ts +++ b/src/daemon/__tests__/request-router-replay-env.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; import fs from 'node:fs'; import path from 'node:path'; import { expect, test } from 'vitest'; @@ -6,7 +6,7 @@ import { makeSessionStore } from './support/store-factory.ts'; import { LeaseRegistry } from '../lease-registry.ts'; import { createRequestHandler } from './test-device-runtime-gateway.ts'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; -import { replayScriptSourceBundleFor } from '../../__tests__/test-utils/replay-script-source.ts'; +import { replayScriptSourceBundleFor } from './support/replay-script-source.ts'; import { fakeCreateCommandSurface } from './support/command-surface-fake.ts'; function createHarness() { diff --git a/src/daemon/__tests__/request-router-response-level.test.ts b/src/daemon/__tests__/request-router-response-level.test.ts index 40ca7dc34b..95e5c82ad5 100644 --- a/src/daemon/__tests__/request-router-response-level.test.ts +++ b/src/daemon/__tests__/request-router-response-level.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; import { test, expect, vi, beforeEach } from 'vitest'; import path from 'node:path'; diff --git a/src/daemon/__tests__/request-router-session-address.test.ts b/src/daemon/__tests__/request-router-session-address.test.ts index f2b35b5a92..b39b710f60 100644 --- a/src/daemon/__tests__/request-router-session-address.test.ts +++ b/src/daemon/__tests__/request-router-session-address.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; import { legacyDispatchCapture } from './legacy-snapshot-capture-fixture.ts'; import { test, expect, vi, beforeEach } from 'vitest'; diff --git a/src/daemon/__tests__/request-router-typed-error.test.ts b/src/daemon/__tests__/request-router-typed-error.test.ts index 774d8862ea..02dd7655a3 100644 --- a/src/daemon/__tests__/request-router-typed-error.test.ts +++ b/src/daemon/__tests__/request-router-typed-error.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; import { legacyDispatchCapture } from './legacy-snapshot-capture-fixture.ts'; import { test, expect, vi, beforeEach } from 'vitest'; import fs from 'node:fs'; diff --git a/src/daemon/__tests__/request-runtime-binding-router.test.ts b/src/daemon/__tests__/request-runtime-binding-router.test.ts index 342009c6b3..911492bf8d 100644 --- a/src/daemon/__tests__/request-runtime-binding-router.test.ts +++ b/src/daemon/__tests__/request-runtime-binding-router.test.ts @@ -9,7 +9,7 @@ import { import type { PlatformRuntimeOperations } from '@agent-device/contracts/platform-runtime-operations'; import { createAppLogStartResult, createDurableResourceEnvelope } from '@agent-device/capture-kit'; import { createTestAppLogLiveHandle } from '@agent-device/test-kit/app-log-live-handle'; -import { createTestDeviceInventoryGateways } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; import { unavailableDeploymentSnapshotAndShutdownOperationFacts } from '@agent-device/test-kit/runtime-operation-facts'; import { makeSessionStore } from './support/store-factory.ts'; import { LeaseRegistry } from '../lease-registry.ts'; diff --git a/src/daemon/__tests__/request-save-script-transports.test.ts b/src/daemon/__tests__/request-save-script-transports.test.ts index 7e4ae3b329..80a04e3414 100644 --- a/src/daemon/__tests__/request-save-script-transports.test.ts +++ b/src/daemon/__tests__/request-save-script-transports.test.ts @@ -1,6 +1,6 @@ import { storeSessionForTest } from './support/store-factory.ts'; import { isSessionRecording } from '../session-script-publication-capability.ts'; -import { createTestDeviceInventoryGateways } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; /** * #1478 (P4-pre): raw-wire counterfactuals for the `flags.saveScript` seam. * diff --git a/src/daemon/__tests__/selector-capture-binding.test.ts b/src/daemon/__tests__/selector-capture-binding.test.ts index eea4a2fca3..2bf5361a32 100644 --- a/src/daemon/__tests__/selector-capture-binding.test.ts +++ b/src/daemon/__tests__/selector-capture-binding.test.ts @@ -3,7 +3,7 @@ import { expect, test } from 'vitest'; import { ANDROID_EMULATOR, IOS_SIMULATOR } from '@agent-device/test-kit/device-fixtures'; import { makeAndroidSession, makeIosSession } from './support/session-factories.ts'; import { makeSessionStore } from './support/store-factory.ts'; -import { withTestDeviceInventory } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { withTestDeviceInventory } from './support/device-inventory-gateways.ts'; import { resolveBoundSelectorCapture } from '../selector-capture-binding.ts'; import { createBoundSelectorRuntime } from '../selector-runtime-backend.ts'; import { selectorCaptureFixture } from './selector-capture-fixture.ts'; diff --git a/src/daemon/__tests__/snapshot-custom-actions-platform-guard.test.ts b/src/daemon/__tests__/snapshot-custom-actions-platform-guard.test.ts index ebf2fec7ff..f8d494c242 100644 --- a/src/daemon/__tests__/snapshot-custom-actions-platform-guard.test.ts +++ b/src/daemon/__tests__/snapshot-custom-actions-platform-guard.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; /** * Custom actions are read through the private-AX snapshot backend, which only * the iOS simulator has. Every other target would accept `--actions`, take an diff --git a/src/__tests__/test-utils/android-ime-capture.raw.json b/src/daemon/__tests__/support/android-ime-capture.raw.json similarity index 100% rename from src/__tests__/test-utils/android-ime-capture.raw.json rename to src/daemon/__tests__/support/android-ime-capture.raw.json diff --git a/src/__tests__/test-utils/android-qs-shade-capture.raw.json b/src/daemon/__tests__/support/android-qs-shade-capture.raw.json similarity index 100% rename from src/__tests__/test-utils/android-qs-shade-capture.raw.json rename to src/daemon/__tests__/support/android-qs-shade-capture.raw.json diff --git a/src/__tests__/test-utils/android-snapshot-capture.ts b/src/daemon/__tests__/support/android-snapshot-capture.ts similarity index 100% rename from src/__tests__/test-utils/android-snapshot-capture.ts rename to src/daemon/__tests__/support/android-snapshot-capture.ts diff --git a/src/__tests__/test-utils/android-ui-hierarchy-fixtures.ts b/src/daemon/__tests__/support/android-ui-hierarchy-fixtures.ts similarity index 100% rename from src/__tests__/test-utils/android-ui-hierarchy-fixtures.ts rename to src/daemon/__tests__/support/android-ui-hierarchy-fixtures.ts diff --git a/src/daemon/__tests__/support/device-inventory-gateways.ts b/src/daemon/__tests__/support/device-inventory-gateways.ts new file mode 100644 index 0000000000..c8aabcf676 --- /dev/null +++ b/src/daemon/__tests__/support/device-inventory-gateways.ts @@ -0,0 +1,154 @@ +// Daemon-side fake for the composed device-inventory gateway port. The root +// `src/__tests__/test-utils/device-inventory-gateways.ts` builds through the real +// `createComposedDeviceInventoryGateways` composition (platform-runtime-device-inventory); +// daemon policy tests only need the port's behavior, so this fixture composes the same +// package-owned rules (contracts projection + provider request scoping) without reaching +// into root composition. +import { + filterDeviceInventoryProjection, + LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS, + projectProviderDeviceInventoryRequest, + type DeviceInventoryProvider, + type DeviceInventoryRequest, + type ProviderDeviceInventorySource, +} from '@agent-device/contracts/device'; +import type { + ComposedDeviceInventoryGateways, + DeviceInventoryGateway, + ProviderAwareDeviceInventoryGateway, +} from '@agent-device/contracts/platform-module'; +import type { PlatformRequestScope } from '@agent-device/contracts/platform-runtime-host'; +import { + isApplePlatform, + type DeviceInfo, + type Platform, + type PlatformSelector, +} from '@agent-device/kernel/device'; +import { withDeviceInventoryContext } from '@agent-device/device-selection/device-inventory-context'; + +export type TestDeviceInventoryOptions = Readonly<{ + provider?: ProviderDeviceInventorySource; + local?: (request: Readonly) => Promise; +}>; + +const testRequestScope: PlatformRequestScope = Object.freeze({ + signal: new AbortController().signal, + diagnostics: Object.freeze({ emit: () => {} }), + progress: Object.freeze({ report: () => {} }), +}); + +type LocalDiscover = (request: Readonly) => Promise; + +async function discoverLocalFamily( + localDiscover: LocalDiscover, + platform: PlatformSelector, + request: Readonly, + scope: PlatformRequestScope, +): Promise { + scope.signal.throwIfAborted(); + const family: Platform = isApplePlatform(platform) ? 'apple' : platform; + const devices = (await localDiscover(request)).filter((device) => device.platform === family); + return filterDeviceInventoryProjection(devices, request); +} + +async function discoverLocal( + localDiscover: LocalDiscover, + request: Readonly, + scope: PlatformRequestScope, +): Promise { + scope.signal.throwIfAborted(); + if (request.platform) { + return await discoverLocalFamily(localDiscover, request.platform, request, scope); + } + const perFamily = await Promise.all( + LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS.map(async (selector) => { + try { + return await discoverLocalFamily( + localDiscover, + selector, + { ...request, platform: selector }, + scope, + ); + } catch { + return []; + } + }), + ); + scope.signal.throwIfAborted(); + return perFamily.flat(); +} + +export function createTestDeviceInventoryGateways( + options: TestDeviceInventoryOptions = {}, +): ComposedDeviceInventoryGateways { + const localDiscover = options.local ?? (async () => [] as readonly DeviceInfo[]); + const localOnly: DeviceInventoryGateway = Object.freeze({ + discover: (request, scope) => discoverLocal(localDiscover, request, scope), + }); + const provider = options.provider; + const discoverWithSource: ProviderAwareDeviceInventoryGateway['discoverWithSource'] = async ( + request, + scope, + ) => { + if (provider) { + scope.signal.throwIfAborted(); + const result = await provider.discover( + projectProviderDeviceInventoryRequest(request), + scope.signal, + ); + scope.signal.throwIfAborted(); + if (result.kind === 'inventory') { + return { + devices: filterDeviceInventoryProjection( + result.devices.map((device) => ({ ...device })), + request, + ), + source: 'provider', + }; + } + } + return { devices: await localOnly.discover(request, scope), source: 'local' }; + }; + const providerFirst: ProviderAwareDeviceInventoryGateway = Object.freeze({ + discover: async (request, scope) => (await discoverWithSource(request, scope)).devices, + discoverWithSource, + }); + return Object.freeze({ providerFirst, localOnly }); +} + +/** Test-only bridge for fixtures that still implement the public nullable provider port. */ +export function createTestDeviceInventoryGatewaysFromProvider( + provider: DeviceInventoryProvider, +): ComposedDeviceInventoryGateways { + return createTestDeviceInventoryGateways({ + provider: { + discover: async (request, signal) => { + signal.throwIfAborted(); + const devices = await provider(request, signal); + return devices === null || devices === undefined + ? { kind: 'declined' } + : { kind: 'inventory', devices }; + }, + }, + }); +} + +export async function withTestDeviceInventory( + options: TestDeviceInventoryOptions, + task: () => Promise, +): Promise { + return await withDeviceInventoryContext( + { ...createTestDeviceInventoryGateways(options), requestScope: testRequestScope }, + task, + ); +} + +export async function withTestDeviceInventoryProvider( + provider: DeviceInventoryProvider, + task: () => Promise, +): Promise { + return await withDeviceInventoryContext( + { ...createTestDeviceInventoryGatewaysFromProvider(provider), requestScope: testRequestScope }, + task, + ); +} diff --git a/src/daemon/__tests__/support/replay-script-source.ts b/src/daemon/__tests__/support/replay-script-source.ts new file mode 100644 index 0000000000..57c5ed2853 --- /dev/null +++ b/src/daemon/__tests__/support/replay-script-source.ts @@ -0,0 +1,152 @@ +// Daemon-side stand-in for the client's replay source collection (#1802). The root +// `src/__tests__/test-utils/replay-script-source.ts` runs the real client-side collector +// (`src/commands/replay/*`); daemon policy tests only need the SHAPE of the bundles a real +// request carries, so this builds them from the package-owned format resolution and +// Maestro include engine without reaching into the root command layer. Suite expansion +// stands in with a deterministic walk; the daemon never expands inputs itself (#1802). +import fs from 'node:fs'; +import path from 'node:path'; +import type { ReplayScriptSourceBundle } from '@agent-device/contracts/replay'; +import type { MaestroSourceReader } from '@agent-device/maestro'; +import { resolveUserPath } from '@agent-device/host-kit/file'; +import { resolveReplayFormat } from '@agent-device/ad-script'; +import type { DaemonRequest } from '../../daemon-request.ts'; + +/** A native `.ad` script's one-entry source bundle for test requests. */ +export function replayScriptSourceBundleFor(filePath: string): ReplayScriptSourceBundle { + const entry = resolveUserPath(filePath, { cwd: process.cwd() }); + return { entry, files: { [entry]: fs.readFileSync(entry, 'utf8') } }; +} + +/** A Maestro flow's bundle — async because the engine that walks its includes loads on demand. */ +export async function maestroScriptSourceBundleFor( + filePath: string, +): Promise { + return await collectReplayScriptSourceBundle({ + inputPath: filePath, + cwd: process.cwd(), + replayBackend: 'maestro', + }); +} + +/** + * `readSource` for a flow that declares no `runFlow` includes: the engine must + * never reach for one, so a call here means the fixture drifted rather than a + * missing file. + */ +export const noMaestroIncludeSources: MaestroSourceReader = (resolvedPath) => { + throw new Error(`unexpected Maestro include read: ${resolvedPath}`); +}; + +/** + * Attaches the script sources a real client would have collected for a `replay`/`test` request + * (#1802): the daemon opens no caller path, so every request that reaches one carries them. + * Test harnesses that stand in for "a request as it arrives at the daemon" call this so their + * cases keep expressing what they are about; a request that already carries its own sources (or + * deliberately carries none, to pin the daemon's refusal) is returned untouched. + * + * Collection that cannot succeed — a case driving `replay` at a path that deliberately does not + * exist — leaves the request alone rather than throwing: the real client raises that failure at + * the CLI boundary (pinned in `cli-client-commands.test.ts`), and a daemon-level harness must + * still deliver the request so the daemon's own response is what the case observes. + */ +export async function withClientReplayScriptSources(req: DaemonRequest): Promise { + if (req.command !== 'replay' && req.command !== 'test') return req; + const inputs = req.positionals ?? []; + const entryPath = inputs[0]; + if (entryPath === undefined) return req; + const cwd = req.meta?.cwd ?? process.cwd(); + const replayBackend = req.flags?.replayBackend; + try { + if (req.command === 'replay') { + if (req.flags?.replayScriptSource) return req; + return withFlags(req, { + replayScriptSource: await collectReplayScriptSourceBundle({ + inputPath: entryPath, + cwd, + replayBackend, + }), + }); + } + if (req.flags?.replayScriptSources) return req; + return withFlags(req, { + replayScriptSources: await Promise.all( + discoverReplaySourcePaths({ inputs, cwd, replayBackend }).map( + async (inputPath) => + await collectReplayScriptSourceBundle({ inputPath, cwd, replayBackend }), + ), + ), + }); + } catch { + return req; + } +} + +function withFlags(req: DaemonRequest, flags: Partial): DaemonRequest { + return { ...req, flags: { ...(req.flags ?? {}), ...flags } }; +} + +async function collectReplayScriptSourceBundle(params: { + inputPath: string; + cwd: string; + replayBackend?: string; +}): Promise { + const entry = resolveUserPath(params.inputPath, { cwd: params.cwd }); + const entrySource = fs.readFileSync(entry, 'utf8'); + const backendId = resolveReplayFormat(entry, params.replayBackend); + if (backendId === 'ad') return { entry, files: { [entry]: entrySource } }; + // Same lazy-load shape as the client collector: the Maestro engine (and its include walker) + // sits behind the package backend registry, never behind root command code. + const backend = await ( + await import('@agent-device/replay-port/replay-backend-registry') + ).getReplayBackend(backendId); + return { + entry, + files: backend.collectSourceFiles({ + entryPath: entry, + entrySource, + readSource: (resolvedPath: string) => { + try { + return fs.readFileSync(resolvedPath, 'utf8'); + } catch { + return undefined; + } + }, + }), + }; +} + +function discoverReplaySourcePaths(params: { + inputs: string[]; + cwd: string; + replayBackend?: string; +}): string[] { + const extensions = + params.replayBackend === 'maestro' ? new Set(['.yaml', '.yml', '.ad']) : new Set(['.ad']); + const paths: string[] = []; + for (const input of params.inputs) { + const resolved = resolveUserPath(input, { cwd: params.cwd }); + if (!fs.existsSync(resolved)) { + throw new Error(`test input not found: ${input}`); + } + if (fs.statSync(resolved).isDirectory()) { + paths.push(...listReplayFiles(resolved, extensions)); + } else if (extensions.has(path.extname(resolved).toLowerCase())) { + paths.push(path.normalize(resolved)); + } else { + throw new Error(`test does not support this file type: ${input}`); + } + } + return [...new Set(paths)]; +} + +function listReplayFiles(directory: string, extensions: ReadonlySet): string[] { + const found: string[] = []; + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const filePath = path.join(directory, entry.name); + if (entry.isDirectory()) found.push(...listReplayFiles(filePath, extensions)); + else if (entry.isFile() && extensions.has(path.extname(entry.name).toLowerCase())) + found.push(filePath); + } + return found; +} diff --git a/src/daemon/__tests__/system-button-runtime.test.ts b/src/daemon/__tests__/system-button-runtime.test.ts index e5b138c32e..d7a2434890 100644 --- a/src/daemon/__tests__/system-button-runtime.test.ts +++ b/src/daemon/__tests__/system-button-runtime.test.ts @@ -19,7 +19,7 @@ import type { SystemButton } from '@agent-device/contracts/system-button-runtime import { deviceShape, type DeviceInfo } from '@agent-device/kernel/device'; import { makeSession } from './support/session-factories.ts'; import { makeSessionStore } from './support/store-factory.ts'; -import { createTestDeviceInventoryGateways } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; import { LeaseRegistry } from '../lease-registry.ts'; import { activateCompleteRefFrame, refFrameState } from '../ref-frame.ts'; import type { BindDeviceRuntime, InspectDeviceRuntimeFacts } from '../request-runtime-binding.ts'; diff --git a/src/daemon/__tests__/test-device-runtime-gateway.ts b/src/daemon/__tests__/test-device-runtime-gateway.ts index 900f64e8da..0bf9382930 100644 --- a/src/daemon/__tests__/test-device-runtime-gateway.ts +++ b/src/daemon/__tests__/test-device-runtime-gateway.ts @@ -22,7 +22,7 @@ import type { BindDeviceRuntime, BindExactDeviceRuntime } from '../request-runti import type { DeviceInfo } from '@agent-device/kernel/device'; import { applicationLifecycleRuntimeFixture } from './application-lifecycle-runtime-fixture.ts'; import { createUnavailableRuntimeFactsForTest } from '@agent-device/test-kit/runtime-operation-facts'; -import { withClientReplayScriptSources } from '../../__tests__/test-utils/replay-script-source.ts'; +import { withClientReplayScriptSources } from './support/replay-script-source.ts'; import type { DaemonInvokeFn } from '../daemon-request.ts'; import { clearAndroidObservationFixture } from './android-observation-fixture.ts'; import { daemonPlatformServicesFixture } from './platform-services-fixture.ts'; diff --git a/src/daemon/__tests__/tv-remote-runtime.test.ts b/src/daemon/__tests__/tv-remote-runtime.test.ts index 1289fcb584..2cd7f431bb 100644 --- a/src/daemon/__tests__/tv-remote-runtime.test.ts +++ b/src/daemon/__tests__/tv-remote-runtime.test.ts @@ -16,7 +16,7 @@ import { tvRemoteRuntimeOperationFacts } from '@agent-device/contracts/tv-remote import { deviceShape } from '@agent-device/kernel/device'; import { makeSession } from './support/session-factories.ts'; import { makeSessionStore } from './support/store-factory.ts'; -import { createTestDeviceInventoryGateways } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; import { LeaseRegistry } from '../lease-registry.ts'; import { activateCompleteRefFrame, refFrameState } from '../ref-frame.ts'; import type { BindDeviceRuntime, InspectDeviceRuntimeFacts } from '../request-runtime-binding.ts'; diff --git a/src/daemon/__tests__/viewport-runtime.test.ts b/src/daemon/__tests__/viewport-runtime.test.ts index ce0b68cd0a..bebecea33b 100644 --- a/src/daemon/__tests__/viewport-runtime.test.ts +++ b/src/daemon/__tests__/viewport-runtime.test.ts @@ -16,7 +16,7 @@ import { viewportRuntimeOperationFacts } from '@agent-device/contracts/viewport- import { deviceShape } from '@agent-device/kernel/device'; import { makeSession } from './support/session-factories.ts'; import { makeSessionStore } from './support/store-factory.ts'; -import { createTestDeviceInventoryGateways } from '../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; import { LeaseRegistry } from '../lease-registry.ts'; import { activateCompleteRefFrame, refFrameState } from '../ref-frame.ts'; import type { BindDeviceRuntime, InspectDeviceRuntimeFacts } from '../request-runtime-binding.ts'; diff --git a/src/daemon/handlers/__tests__/session-appstate-device-runtime.test.ts b/src/daemon/handlers/__tests__/session-appstate-device-runtime.test.ts index d968bc0c2f..f32d27aa9d 100644 --- a/src/daemon/handlers/__tests__/session-appstate-device-runtime.test.ts +++ b/src/daemon/handlers/__tests__/session-appstate-device-runtime.test.ts @@ -3,7 +3,7 @@ import { test, expect, vi } from 'vitest'; import { handleAppStateCommand } from '../session-appstate.ts'; import { makeSessionStore } from '../../__tests__/support/store-factory.ts'; -import { withTestDeviceInventory } from '../../../__tests__/test-utils/device-inventory-gateways.ts'; +import { withTestDeviceInventory } from '../../__tests__/support/device-inventory-gateways.ts'; import { unavailableDeploymentSnapshotAndShutdownOperationFacts } from '@agent-device/test-kit/runtime-operation-facts'; import { applicationLifecycleOperationFacts } from '@agent-device/contracts/application-lifecycle-runtime'; import { diff --git a/src/daemon/handlers/__tests__/session-command-harness.ts b/src/daemon/handlers/__tests__/session-command-harness.ts index 9600a43f45..6f9276522b 100644 --- a/src/daemon/handlers/__tests__/session-command-harness.ts +++ b/src/daemon/handlers/__tests__/session-command-harness.ts @@ -23,7 +23,7 @@ import type { TargetShutdownResult } from '@agent-device/contracts/device'; import { isIosFamily, type DeviceInfo } from '@agent-device/kernel/device'; import { beforeEach, vi } from 'vitest'; import { applicationLifecycleRuntimeFixture } from '../../__tests__/application-lifecycle-runtime-fixture.ts'; -import { withClientReplayScriptSources } from '../../../__tests__/test-utils/replay-script-source.ts'; +import { withClientReplayScriptSources } from '../../__tests__/support/replay-script-source.ts'; import { createUnavailableRuntimeFactsForTest } from '@agent-device/test-kit/runtime-operation-facts'; import { platformResourceCleanupFixture } from '../../__tests__/support/platform-resource-cleanup-fixture.ts'; diff --git a/src/daemon/handlers/__tests__/session-command-replay.test.ts b/src/daemon/handlers/__tests__/session-command-replay.test.ts index 099b8272c9..c75f3a2b4f 100644 --- a/src/daemon/handlers/__tests__/session-command-replay.test.ts +++ b/src/daemon/handlers/__tests__/session-command-replay.test.ts @@ -6,7 +6,7 @@ import { makeSessionStore } from './session-test-harness.ts'; import { handleSessionCommands } from './session-command-harness.ts'; import type { DaemonRequest } from '../../daemon-request.ts'; -import { replayScriptSourceBundleFor } from '../../../__tests__/test-utils/replay-script-source.ts'; +import { replayScriptSourceBundleFor } from '../../__tests__/support/replay-script-source.ts'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; import { fakeCreateCommandSurface } from '../../__tests__/support/command-surface-fake.ts'; diff --git a/src/daemon/handlers/__tests__/session-doctor-device.test.ts b/src/daemon/handlers/__tests__/session-doctor-device.test.ts index 86860aacba..62824a39ad 100644 --- a/src/daemon/handlers/__tests__/session-doctor-device.test.ts +++ b/src/daemon/handlers/__tests__/session-doctor-device.test.ts @@ -3,7 +3,7 @@ import { test } from 'vitest'; import { withTestDeviceInventory, withTestDeviceInventoryProvider as withDeviceInventoryProvider, -} from '../../../__tests__/test-utils/device-inventory-gateways.ts'; +} from '../../__tests__/support/device-inventory-gateways.ts'; import { AppError } from '@agent-device/kernel/errors'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { attachAdbFailureHint } from '@agent-device/platform-android/mechanics'; diff --git a/src/daemon/handlers/__tests__/session-doctor-warmup.test.ts b/src/daemon/handlers/__tests__/session-doctor-warmup.test.ts index 34bc9833ee..686792159f 100644 --- a/src/daemon/handlers/__tests__/session-doctor-warmup.test.ts +++ b/src/daemon/handlers/__tests__/session-doctor-warmup.test.ts @@ -4,7 +4,7 @@ import { isActiveProviderDevice } from '../../provider-device-admission.ts'; import { handleDoctorCommand } from '../session-doctor.ts'; import { createHostDiagnostics } from '../../../platform-runtime-host-diagnostics.ts'; import { makeSessionStore } from '../../__tests__/support/store-factory.ts'; -import { withTestDeviceInventory } from '../../../__tests__/test-utils/device-inventory-gateways.ts'; +import { withTestDeviceInventory } from '../../__tests__/support/device-inventory-gateways.ts'; import type { DaemonResponse } from '../../daemon-request.ts'; const { mockAppleRunnerWarmupCheck } = vi.hoisted(() => ({ diff --git a/src/daemon/handlers/__tests__/session-relaunch-close.test.ts b/src/daemon/handlers/__tests__/session-relaunch-close.test.ts index cf4dacf64b..eccbf7c495 100644 --- a/src/daemon/handlers/__tests__/session-relaunch-close.test.ts +++ b/src/daemon/handlers/__tests__/session-relaunch-close.test.ts @@ -7,7 +7,7 @@ import { fakeCreateCommandSurface } from '../../__tests__/support/command-surfac // The daemon reads provider ownership through its own typed admission seam; these suites // install the seam over runtime fakes, the way root composition installs the real one. const providerAdmission = providerDeviceAdmissionFixture(); -import { createTestDeviceInventoryGateways } from '../../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '../../__tests__/support/device-inventory-gateways.ts'; import { makeSessionStore } from '../../__tests__/support/store-factory.ts'; import { makeSession } from '../../__tests__/support/session-factories.ts'; import { getResolveTargetDeviceMock } from '../../__tests__/request-router-dispatch-mocks.ts'; diff --git a/src/daemon/session-lifecycle/internal/__tests__/session-capabilities.test.ts b/src/daemon/session-lifecycle/internal/__tests__/session-capabilities.test.ts index 1a923ae3dd..55cdf4056e 100644 --- a/src/daemon/session-lifecycle/internal/__tests__/session-capabilities.test.ts +++ b/src/daemon/session-lifecycle/internal/__tests__/session-capabilities.test.ts @@ -6,7 +6,7 @@ import { PUBLIC_COMMANDS } from '@agent-device/command-registry/catalog'; import { LINUX_DEVICE, WEB_DESKTOP_DEVICE } from '@agent-device/test-kit/device-fixtures'; import { makeAndroidSession, makeSession } from '../../../__tests__/support/session-factories.ts'; import { makeSessionStore } from '../../../__tests__/support/store-factory.ts'; -import { withTestDeviceInventoryProvider as withTargetDeviceResolutionScope } from '../../../../__tests__/test-utils/device-inventory-gateways.ts'; +import { withTestDeviceInventoryProvider as withTargetDeviceResolutionScope } from '../../../__tests__/support/device-inventory-gateways.ts'; import { unavailableDeploymentSnapshotAndShutdownOperationFacts } from '@agent-device/test-kit/runtime-operation-facts'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { applicationLifecycleOperationFacts } from '@agent-device/contracts/application-lifecycle-runtime'; diff --git a/src/daemon/session-lifecycle/internal/__tests__/session-devices-batch-runtime.test.ts b/src/daemon/session-lifecycle/internal/__tests__/session-devices-batch-runtime.test.ts index 56592b2925..390574aec1 100644 --- a/src/daemon/session-lifecycle/internal/__tests__/session-devices-batch-runtime.test.ts +++ b/src/daemon/session-lifecycle/internal/__tests__/session-devices-batch-runtime.test.ts @@ -21,7 +21,7 @@ import { makeSessionStore } from '../../../__tests__/support/store-factory.ts'; import { makeSession } from '../../../__tests__/support/session-factories.ts'; import type { DaemonRequest, DaemonResponse } from '../../../daemon-request.ts'; -import { withTestDeviceInventory } from '../../../../__tests__/test-utils/device-inventory-gateways.ts'; +import { withTestDeviceInventory } from '../../../__tests__/support/device-inventory-gateways.ts'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { readCurrentOwnerIdentity } from '@agent-device/host-kit/process'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; diff --git a/src/daemon/session-lifecycle/internal/__tests__/session-open-url-prewarm.test.ts b/src/daemon/session-lifecycle/internal/__tests__/session-open-url-prewarm.test.ts index 893efe1b5a..61940bdf69 100644 --- a/src/daemon/session-lifecycle/internal/__tests__/session-open-url-prewarm.test.ts +++ b/src/daemon/session-lifecycle/internal/__tests__/session-open-url-prewarm.test.ts @@ -49,7 +49,7 @@ import { } from '../../../__tests__/test-device-runtime-gateway.ts'; import { dispatchApplicationLifecycleEffect } from '../../../__tests__/application-lifecycle-runtime-fixture.ts'; import { getResolveTargetDeviceMock } from '../../../__tests__/request-router-dispatch-mocks.ts'; -import { createTestDeviceInventoryGateways } from '../../../../__tests__/test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '../../../__tests__/support/device-inventory-gateways.ts'; import { makeSessionStore } from '../../../__tests__/support/store-factory.ts'; import { makeSession } from '../../../__tests__/support/session-factories.ts'; import { LeaseRegistry } from '../../../lease-registry.ts'; diff --git a/src/plugins/load.test.ts b/src/plugins/load.test.ts index 3bf512dee2..ae83608b3b 100644 --- a/src/plugins/load.test.ts +++ b/src/plugins/load.test.ts @@ -8,7 +8,7 @@ import { selectPlugin, registrationSource, webDriverPluginSource, -} from './plugin.fixtures.ts'; +} from '@agent-device/test-kit/plugin.fixtures'; import { AppError } from '@agent-device/kernel/errors'; import type { ProviderPluginHost } from '../sdk/plugins.ts'; import type { DeviceLease, ProviderDeviceRuntime } from '@agent-device/contracts/device'; diff --git a/src/plugins/manifest.test.ts b/src/plugins/manifest.test.ts index 58767eb244..dfbac3e776 100644 --- a/src/plugins/manifest.test.ts +++ b/src/plugins/manifest.test.ts @@ -4,7 +4,7 @@ import path from 'node:path'; import { test } from 'vitest'; import { readPluginManifest, RESERVED_PLUGIN_PROVIDERS } from './manifest.ts'; import { DEFAULT_PROVIDER_RUNTIME_REQUIRED_IDS } from '../provider-device-runtimes.ts'; -import { pluginHome, writePlugin } from './plugin.fixtures.ts'; +import { pluginHome, writePlugin } from '@agent-device/test-kit/plugin.fixtures'; test('manifest compatibility is checked without evaluating plugin code', () => { const { home } = pluginHome(); diff --git a/src/plugins/store.test.ts b/src/plugins/store.test.ts index d10648a187..f111359af3 100644 --- a/src/plugins/store.test.ts +++ b/src/plugins/store.test.ts @@ -4,7 +4,7 @@ import path from 'node:path'; import { afterEach, test, vi } from 'vitest'; import { runCmd } from '@agent-device/host-kit/command'; import { changePlugin, installedPlugins, listPlugins } from './store.ts'; -import { pluginHome, writePlugin, selectPlugin } from './plugin.fixtures.ts'; +import { pluginHome, writePlugin, selectPlugin } from '@agent-device/test-kit/plugin.fixtures'; vi.mock('@agent-device/host-kit/command', () => ({ runCmd: vi.fn() })); afterEach(() => vi.mocked(runCmd).mockReset()); diff --git a/src/provider-credential-fingerprint.test.ts b/src/provider-credential-fingerprint.test.ts index 6ce107c060..3b7415fcac 100644 --- a/src/provider-credential-fingerprint.test.ts +++ b/src/provider-credential-fingerprint.test.ts @@ -3,7 +3,7 @@ import { providerCredentialFingerprint, readDaemonProviderCredentials, } from './provider-credential-fingerprint.ts'; -import { pluginHome, selectPluginManifest } from './plugins/plugin.fixtures.ts'; +import { pluginHome, selectPluginManifest } from '@agent-device/test-kit/plugin.fixtures'; const BROWSERSTACK_ENV = { BROWSERSTACK_USERNAME: 'user', BROWSERSTACK_ACCESS_KEY: 'key-1' }; From 69b2d31e7f923f65c196ced49b6f510beffe9717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 12 Oct 2026 19:04:40 +0200 Subject: [PATCH 02/14] chore(gates): retarget the affected-lanes fixture pin to its moved path --- scripts/check-affected/device-lanes.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check-affected/device-lanes.test.ts b/scripts/check-affected/device-lanes.test.ts index b4552b6e9e..25f98fce63 100644 --- a/scripts/check-affected/device-lanes.test.ts +++ b/scripts/check-affected/device-lanes.test.ts @@ -137,7 +137,7 @@ test('unit tests under src/ and packages/*/src own no lane', () => { assert.deepEqual(lanes(file), [], file); } // Non-TypeScript under __tests__ is a fixture the selector cannot place, not a unit test. - assert.ok(!isUnitTest('src/__tests__/test-utils/android-ime-capture.raw.json')); + assert.ok(!isUnitTest('src/daemon/__tests__/support/android-ime-capture.raw.json')); // Package tests outside src/ have no owner and keep failing open. assert.ok(!isUnitTest('packages/maestro/test/conformance/verify.test.ts')); assert.equal( From 48b57280d97a82fc43048c97fa84cc19c2d5cfa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 12 Oct 2026 20:56:11 +0200 Subject: [PATCH 03/14] refactor(daemon): one owner each for the inventory fake and the replay source collector Owner review on #3517: the two daemon-side fakes restated logic that already had owners. Both are replaced by single importable owners: - The device-selection test fake becomes the one inventory-gateways fixture: moved to packages/device-selection/src/inventory-gateways.fixtures.ts and exported as ./inventory-gateways-fixtures (with findInstalledApp already in its options and createTestDeviceInventoryGatewaysFromProvider now exported). The daemon copy, the root fake, and its pin (the real composer's fail-closed contract is pinned in platform-runtime-device-inventory.test.ts) are deleted; daemon, root, device-selection, and integration consumers all import the owner. - The real client collector moves into its package: script-source-bundle and source-discovery move from src/commands/replay into replay-port (own exports ./script-source-bundle, ./source-discovery). The bundle helpers live once in packages/replay-port/src/daemon-port/replay-script-source.fixtures.ts (./replay-script-source-fixtures), typed on DaemonWireRequest; both test trees import it and run the same globs, size cap, AppErrors and ranking. - The Android capture fixtures move to platform-android test-only entries (./snapshot-capture-fixtures, ./ui-hierarchy-fixtures) so no test tree reaches sideways; the archived raw JSONs move with them. --- packages/device-selection/package.json | 4 + .../device-selection-resolver.test.ts | 2 +- .../src/__tests__/dispatch-resolve.test.ts | 2 +- .../test-utils/device-inventory-gateways.ts | 148 ----------------- .../src/inventory-gateways.fixtures.ts | 36 ++--- packages/platform-android/package.json | 8 + .../src}/android-ime-capture.raw.json | 0 .../src}/android-qs-shade-capture.raw.json | 0 .../src/snapshot-capture.fixtures.ts | 0 .../src/ui-hierarchy.fixtures.ts | 0 packages/replay-port/package.json | 12 ++ .../__tests__}/script-source-bundle.test.ts | 2 +- .../__tests__}/source-discovery.test.ts | 2 +- .../replay-script-source.fixtures.ts | 17 +- .../src/daemon-port}/script-source-bundle.ts | 0 .../src/daemon-port}/source-discovery.ts | 0 scripts/__tests__/eager-closure-budgets.ts | 38 ++++- scripts/layering/package-boundaries.test.ts | 3 + .../android-ui-hierarchy-scope.test.ts | 2 +- src/__tests__/android-ui-hierarchy.test.ts | 2 +- .../cli-startup-import-closure.test.ts | 5 +- .../snapshot-alert-android-occlusion.test.ts | 2 +- .../http-server-limrun-uploaded-apps.test.ts | 2 +- .../interaction-ios-tap-outcome.test.ts | 2 +- .../daemon-composition/is-runtime.test.ts | 2 +- .../request-dispatch-disclosure.test.ts | 2 +- .../request-platform-providers.test.ts | 2 +- .../request-router-android-modal.test.ts | 2 +- .../request-router-android-perf.test.ts | 2 +- ...est-router-android-snapshot-helper.test.ts | 2 +- .../request-router-fill-text-stdin.test.ts | 4 +- .../request-router-lock-policy.test.ts | 2 +- .../request-router-macos-app-lease.test.ts | 2 +- ...equest-router-provider-credentials.test.ts | 2 +- ...request-router-record-runtime-lock.test.ts | 2 +- .../request-router-recording-health.test.ts | 2 +- .../request-router-replay-scope.test.ts | 2 +- .../request-router-replay-test-ime.test.ts | 4 +- .../request-router-screenshot.test.ts | 2 +- .../platform-runtime-apple-resources.test.ts | 2 +- .../device-inventory-gateways.test.ts | 20 --- .../test-utils/device-inventory-gateways.ts | 119 -------------- .../session-test-reporter-values.test.ts | 2 +- src/commands/replay/index.ts | 4 +- .../snapshot-chrome-android-statusbar.test.ts | 2 +- src/core/interactors/android.test.ts | 2 +- .../android-system-dialog-occlusion.test.ts | 2 +- .../android-system-dialog-ref-frame.test.ts | 2 +- src/daemon/__tests__/back-runtime.test.ts | 2 +- src/daemon/__tests__/daemon-policy.test.ts | 2 +- src/daemon/__tests__/focus-runtime.test.ts | 2 +- .../__tests__/human-control-router-fixture.ts | 2 +- .../__tests__/orientation-runtime.test.ts | 2 +- .../__tests__/replay-device-selection.test.ts | 2 +- ...eplay-divergence-android-occlusion.test.ts | 2 +- ...on-replay-divergence-chrome-filter.test.ts | 2 +- .../session-replay-divergence-overlay.test.ts | 2 +- ...on-replay-maestro-error-projection.test.ts | 2 +- .../session-replay-maestro-failure.test.ts | 2 +- ...n-replay-maestro-remote-evalscript.test.ts | 2 +- ...ion-replay-maestro-session-address.test.ts | 2 +- ...on-replay-runtime-maestro-dispatch.test.ts | 2 +- ...ay-runtime-maestro-shell-artifacts.test.ts | 2 +- ...session-replay-runtime-maestro.fixtures.ts | 2 +- .../replay-runtime/application.test.ts | 2 +- .../session-replay-runtime-failure.test.ts | 2 +- .../session-replay-runtime.fixtures.ts | 2 +- .../session-replay-script-source.test.ts | 2 +- .../session-test-command-trust.test.ts | 2 +- .../session-test-source-discovery.test.ts | 2 +- .../session-test-suite-command-video.test.ts | 2 +- .../replay-suite/session-test-suite.test.ts | 2 +- .../__tests__/request-handler-catalog.test.ts | 2 +- .../request-router-apps-admission.test.ts | 2 +- .../request-router-artifacts-web.test.ts | 2 +- .../__tests__/request-router-cost.test.ts | 2 +- ...request-router-custom-action-flags.test.ts | 2 +- .../request-router-developer-dir.test.ts | 2 +- .../request-router-error-redaction.test.ts | 2 +- .../__tests__/request-router-events.test.ts | 2 +- .../request-router-idle-expired.test.ts | 2 +- .../request-router-open-claim.test.ts | 2 +- .../__tests__/request-router-open.test.ts | 4 +- .../request-router-record-flags.test.ts | 2 +- .../request-router-repair-expired.test.ts | 2 +- .../request-router-replay-env.test.ts | 4 +- .../request-router-response-level.test.ts | 2 +- .../request-router-session-address.test.ts | 2 +- .../request-router-typed-error.test.ts | 2 +- .../request-runtime-binding-router.test.ts | 2 +- .../request-save-script-transports.test.ts | 2 +- .../selector-capture-binding.test.ts | 2 +- ...shot-custom-actions-platform-guard.test.ts | 2 +- .../__tests__/support/replay-script-source.ts | 152 ------------------ .../__tests__/system-button-runtime.test.ts | 2 +- .../__tests__/test-device-runtime-gateway.ts | 2 +- .../__tests__/tv-remote-runtime.test.ts | 2 +- src/daemon/__tests__/viewport-runtime.test.ts | 2 +- .../session-appstate-device-runtime.test.ts | 2 +- .../__tests__/session-command-harness.ts | 2 +- .../__tests__/session-command-replay.test.ts | 2 +- .../__tests__/session-doctor-device.test.ts | 2 +- .../__tests__/session-doctor-warmup.test.ts | 2 +- .../__tests__/session-relaunch-close.test.ts | 2 +- .../__tests__/session-capabilities.test.ts | 2 +- .../session-devices-batch-runtime.test.ts | 2 +- .../session-open-url-prewarm.test.ts | 2 +- src/platform-runtime-gateway.test.ts | 2 +- .../nightly/concurrency-torture/bindings.ts | 2 +- .../real-scope-serialization.ts | 2 +- .../integration/provider-scenarios/harness.ts | 4 +- 111 files changed, 197 insertions(+), 563 deletions(-) delete mode 100644 packages/device-selection/src/__tests__/test-utils/device-inventory-gateways.ts rename src/daemon/__tests__/support/device-inventory-gateways.ts => packages/device-selection/src/inventory-gateways.fixtures.ts (88%) rename {src/daemon/__tests__/support => packages/platform-android/src}/android-ime-capture.raw.json (100%) rename {src/daemon/__tests__/support => packages/platform-android/src}/android-qs-shade-capture.raw.json (100%) rename src/daemon/__tests__/support/android-snapshot-capture.ts => packages/platform-android/src/snapshot-capture.fixtures.ts (100%) rename src/daemon/__tests__/support/android-ui-hierarchy-fixtures.ts => packages/platform-android/src/ui-hierarchy.fixtures.ts (100%) rename {src/commands/replay => packages/replay-port/src/daemon-port/__tests__}/script-source-bundle.test.ts (98%) rename {src/commands/replay => packages/replay-port/src/daemon-port/__tests__}/source-discovery.test.ts (99%) rename src/__tests__/test-utils/replay-script-source.ts => packages/replay-port/src/daemon-port/replay-script-source.fixtures.ts (82%) rename {src/commands/replay => packages/replay-port/src/daemon-port}/script-source-bundle.ts (100%) rename {src/commands/replay => packages/replay-port/src/daemon-port}/source-discovery.ts (100%) delete mode 100644 src/__tests__/test-utils/device-inventory-gateways.test.ts delete mode 100644 src/__tests__/test-utils/device-inventory-gateways.ts delete mode 100644 src/daemon/__tests__/support/replay-script-source.ts diff --git a/packages/device-selection/package.json b/packages/device-selection/package.json index 57e6e8e52e..8ade295ca0 100644 --- a/packages/device-selection/package.json +++ b/packages/device-selection/package.json @@ -26,6 +26,10 @@ "./open-target": { "types": "./src/open-target.ts", "default": "./src/open-target.ts" + }, + "./inventory-gateways-fixtures": { + "types": "./src/inventory-gateways.fixtures.ts", + "default": "./src/inventory-gateways.fixtures.ts" } } } diff --git a/packages/device-selection/src/__tests__/device-selection-resolver.test.ts b/packages/device-selection/src/__tests__/device-selection-resolver.test.ts index 2d1840a0ea..4a8a338785 100644 --- a/packages/device-selection/src/__tests__/device-selection-resolver.test.ts +++ b/packages/device-selection/src/__tests__/device-selection-resolver.test.ts @@ -7,7 +7,7 @@ import { resolveExistingSessionDeviceSelection, resolveInventoryDeviceSelection, } from '../device-selection-resolver.ts'; -import { withTestDeviceInventory } from './test-utils/device-inventory-gateways.ts'; +import { withTestDeviceInventory } from '../inventory-gateways.fixtures.ts'; import { ANDROID_EMULATOR, IOS_SIMULATOR, diff --git a/packages/device-selection/src/__tests__/dispatch-resolve.test.ts b/packages/device-selection/src/__tests__/dispatch-resolve.test.ts index f6821dd8fa..f983d4e5fe 100644 --- a/packages/device-selection/src/__tests__/dispatch-resolve.test.ts +++ b/packages/device-selection/src/__tests__/dispatch-resolve.test.ts @@ -11,7 +11,7 @@ import { import { withTestDeviceInventory, withTestDeviceInventoryProvider as withDeviceInventoryProvider, -} from './test-utils/device-inventory-gateways.ts'; +} from '../inventory-gateways.fixtures.ts'; import type { DeviceInfo } from '@agent-device/kernel/device'; import type { DeviceInventoryRequest } from '@agent-device/contracts/device'; import { AppError } from '@agent-device/kernel/errors'; diff --git a/packages/device-selection/src/__tests__/test-utils/device-inventory-gateways.ts b/packages/device-selection/src/__tests__/test-utils/device-inventory-gateways.ts deleted file mode 100644 index f67f628b9d..0000000000 --- a/packages/device-selection/src/__tests__/test-utils/device-inventory-gateways.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { - filterDeviceInventoryProjection, - LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS, - projectProviderDeviceInventoryRequest, - type DeviceInventoryProvider, - type DeviceInventoryRequest, - type ProviderDeviceInventorySource, -} from '@agent-device/contracts/device'; -import type { - ComposedDeviceInventoryGateways, - DeviceInventoryGateway, - InstalledAppProbe, - ProviderAwareDeviceInventoryGateway, -} from '@agent-device/contracts/platform-module'; -import type { PlatformRequestScope } from '@agent-device/contracts/platform-runtime-host'; -import { - isApplePlatform, - type DeviceInfo, - type Platform, - type PlatformSelector, -} from '@agent-device/kernel/device'; -import { withDeviceInventoryContext } from '../../device-inventory-context.ts'; - -export type TestDeviceInventoryOptions = Readonly<{ - provider?: ProviderDeviceInventorySource; - local?: (request: Readonly) => Promise; - findInstalledApp?: InstalledAppProbe; -}>; - -const testRequestScope: PlatformRequestScope = Object.freeze({ - signal: new AbortController().signal, - diagnostics: Object.freeze({ emit: () => {} }), - progress: Object.freeze({ report: () => {} }), -}); - -type LocalDiscover = (request: Readonly) => Promise; - -async function discoverLocalFamily( - localDiscover: LocalDiscover, - platform: PlatformSelector, - request: Readonly, - scope: PlatformRequestScope, -): Promise { - scope.signal.throwIfAborted(); - const family: Platform = isApplePlatform(platform) ? 'apple' : platform; - const devices = (await localDiscover(request)).filter((device) => device.platform === family); - return filterDeviceInventoryProjection(devices, request); -} - -async function discoverLocal( - localDiscover: LocalDiscover, - request: Readonly, - scope: PlatformRequestScope, -): Promise { - scope.signal.throwIfAborted(); - if (request.platform) { - return await discoverLocalFamily(localDiscover, request.platform, request, scope); - } - const perFamily = await Promise.all( - LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS.map(async (selector) => { - try { - return await discoverLocalFamily( - localDiscover, - selector, - { ...request, platform: selector }, - scope, - ); - } catch { - return []; - } - }), - ); - scope.signal.throwIfAborted(); - return perFamily.flat(); -} - -function createTestDeviceInventoryGateways( - options: TestDeviceInventoryOptions = {}, -): ComposedDeviceInventoryGateways { - const localDiscover = options.local ?? (async () => [] as readonly DeviceInfo[]); - const localOnly: DeviceInventoryGateway = Object.freeze({ - discover: (request, scope) => discoverLocal(localDiscover, request, scope), - }); - const provider = options.provider; - const discoverWithSource: ProviderAwareDeviceInventoryGateway['discoverWithSource'] = async ( - request, - scope, - ) => { - if (provider) { - scope.signal.throwIfAborted(); - const result = await provider.discover( - projectProviderDeviceInventoryRequest(request), - scope.signal, - ); - scope.signal.throwIfAborted(); - if (result.kind === 'inventory') { - return { - devices: filterDeviceInventoryProjection( - result.devices.map((device) => ({ ...device })), - request, - ), - source: 'provider', - }; - } - } - return { devices: await localOnly.discover(request, scope), source: 'local' }; - }; - const providerFirst: ProviderAwareDeviceInventoryGateway = Object.freeze({ - discover: async (request, scope) => (await discoverWithSource(request, scope)).devices, - discoverWithSource, - }); - return Object.freeze({ - providerFirst, - localOnly, - findInstalledApp: options.findInstalledApp, - }); -} - -export async function withTestDeviceInventory( - options: TestDeviceInventoryOptions, - task: () => Promise, -): Promise { - return await withDeviceInventoryContext( - { ...createTestDeviceInventoryGateways(options), requestScope: testRequestScope }, - task, - ); -} - -/** Test-only bridge for fixtures that still implement the public nullable provider port. */ -export async function withTestDeviceInventoryProvider( - provider: DeviceInventoryProvider, - task: () => Promise, -): Promise { - return await withTestDeviceInventory( - { - provider: { - discover: async (request, signal) => { - signal.throwIfAborted(); - const devices = await provider(request, signal); - return devices === null || devices === undefined - ? { kind: 'declined' } - : { kind: 'inventory', devices }; - }, - }, - }, - task, - ); -} diff --git a/src/daemon/__tests__/support/device-inventory-gateways.ts b/packages/device-selection/src/inventory-gateways.fixtures.ts similarity index 88% rename from src/daemon/__tests__/support/device-inventory-gateways.ts rename to packages/device-selection/src/inventory-gateways.fixtures.ts index c8aabcf676..559002b3c1 100644 --- a/src/daemon/__tests__/support/device-inventory-gateways.ts +++ b/packages/device-selection/src/inventory-gateways.fixtures.ts @@ -1,9 +1,3 @@ -// Daemon-side fake for the composed device-inventory gateway port. The root -// `src/__tests__/test-utils/device-inventory-gateways.ts` builds through the real -// `createComposedDeviceInventoryGateways` composition (platform-runtime-device-inventory); -// daemon policy tests only need the port's behavior, so this fixture composes the same -// package-owned rules (contracts projection + provider request scoping) without reaching -// into root composition. import { filterDeviceInventoryProjection, LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS, @@ -15,6 +9,7 @@ import { import type { ComposedDeviceInventoryGateways, DeviceInventoryGateway, + InstalledAppProbe, ProviderAwareDeviceInventoryGateway, } from '@agent-device/contracts/platform-module'; import type { PlatformRequestScope } from '@agent-device/contracts/platform-runtime-host'; @@ -24,11 +19,12 @@ import { type Platform, type PlatformSelector, } from '@agent-device/kernel/device'; -import { withDeviceInventoryContext } from '@agent-device/device-selection/device-inventory-context'; +import { withDeviceInventoryContext } from './device-inventory-context.ts'; export type TestDeviceInventoryOptions = Readonly<{ provider?: ProviderDeviceInventorySource; local?: (request: Readonly) => Promise; + findInstalledApp?: InstalledAppProbe; }>; const testRequestScope: PlatformRequestScope = Object.freeze({ @@ -113,7 +109,21 @@ export function createTestDeviceInventoryGateways( discover: async (request, scope) => (await discoverWithSource(request, scope)).devices, discoverWithSource, }); - return Object.freeze({ providerFirst, localOnly }); + return Object.freeze({ + providerFirst, + localOnly, + findInstalledApp: options.findInstalledApp, + }); +} + +export async function withTestDeviceInventory( + options: TestDeviceInventoryOptions, + task: () => Promise, +): Promise { + return await withDeviceInventoryContext( + { ...createTestDeviceInventoryGateways(options), requestScope: testRequestScope }, + task, + ); } /** Test-only bridge for fixtures that still implement the public nullable provider port. */ @@ -133,16 +143,6 @@ export function createTestDeviceInventoryGatewaysFromProvider( }); } -export async function withTestDeviceInventory( - options: TestDeviceInventoryOptions, - task: () => Promise, -): Promise { - return await withDeviceInventoryContext( - { ...createTestDeviceInventoryGateways(options), requestScope: testRequestScope }, - task, - ); -} - export async function withTestDeviceInventoryProvider( provider: DeviceInventoryProvider, task: () => Promise, diff --git a/packages/platform-android/package.json b/packages/platform-android/package.json index 336b10b9b3..aed398ea9a 100644 --- a/packages/platform-android/package.json +++ b/packages/platform-android/package.json @@ -31,6 +31,14 @@ "./mechanics": { "types": "./src/mechanics.ts", "default": "./src/mechanics.ts" + }, + "./snapshot-capture-fixtures": { + "types": "./src/snapshot-capture.fixtures.ts", + "default": "./src/snapshot-capture.fixtures.ts" + }, + "./ui-hierarchy-fixtures": { + "types": "./src/ui-hierarchy.fixtures.ts", + "default": "./src/ui-hierarchy-fixtures.ts" } } } diff --git a/src/daemon/__tests__/support/android-ime-capture.raw.json b/packages/platform-android/src/android-ime-capture.raw.json similarity index 100% rename from src/daemon/__tests__/support/android-ime-capture.raw.json rename to packages/platform-android/src/android-ime-capture.raw.json diff --git a/src/daemon/__tests__/support/android-qs-shade-capture.raw.json b/packages/platform-android/src/android-qs-shade-capture.raw.json similarity index 100% rename from src/daemon/__tests__/support/android-qs-shade-capture.raw.json rename to packages/platform-android/src/android-qs-shade-capture.raw.json diff --git a/src/daemon/__tests__/support/android-snapshot-capture.ts b/packages/platform-android/src/snapshot-capture.fixtures.ts similarity index 100% rename from src/daemon/__tests__/support/android-snapshot-capture.ts rename to packages/platform-android/src/snapshot-capture.fixtures.ts diff --git a/src/daemon/__tests__/support/android-ui-hierarchy-fixtures.ts b/packages/platform-android/src/ui-hierarchy.fixtures.ts similarity index 100% rename from src/daemon/__tests__/support/android-ui-hierarchy-fixtures.ts rename to packages/platform-android/src/ui-hierarchy.fixtures.ts diff --git a/packages/replay-port/package.json b/packages/replay-port/package.json index 56951e62f9..91eef8d584 100644 --- a/packages/replay-port/package.json +++ b/packages/replay-port/package.json @@ -52,6 +52,10 @@ "types": "./src/daemon-port/script-source.ts", "default": "./src/daemon-port/script-source.ts" }, + "./script-source-bundle": { + "types": "./src/daemon-port/script-source-bundle.ts", + "default": "./src/daemon-port/script-source-bundle.ts" + }, "./session-replay-action-runtime": { "types": "./src/daemon-port/session-replay-action-runtime.ts", "default": "./src/daemon-port/session-replay-action-runtime.ts" @@ -151,6 +155,14 @@ "./test-command": { "types": "./src/daemon-port/test-command.ts", "default": "./src/daemon-port/test-command.ts" + }, + "./source-discovery": { + "types": "./src/daemon-port/source-discovery.ts", + "default": "./src/daemon-port/source-discovery.ts" + }, + "./replay-script-source-fixtures": { + "types": "./src/daemon-port/replay-script-source.fixtures.ts", + "default": "./src/daemon-port/replay-script-source.fixtures.ts" } } } diff --git a/src/commands/replay/script-source-bundle.test.ts b/packages/replay-port/src/daemon-port/__tests__/script-source-bundle.test.ts similarity index 98% rename from src/commands/replay/script-source-bundle.test.ts rename to packages/replay-port/src/daemon-port/__tests__/script-source-bundle.test.ts index 0a73c558a7..00caed12ee 100644 --- a/src/commands/replay/script-source-bundle.test.ts +++ b/packages/replay-port/src/daemon-port/__tests__/script-source-bundle.test.ts @@ -4,7 +4,7 @@ import { expect, test } from 'vitest'; import { loadReplayScriptSourceBundle, REPLAY_SCRIPT_SOURCE_BUNDLE_MAX_BYTES, -} from './script-source-bundle.ts'; +} from '@agent-device/replay-port/script-source-bundle'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; // #1802: the caller reads every script file a replay run needs and sends the text. These pin the diff --git a/src/commands/replay/source-discovery.test.ts b/packages/replay-port/src/daemon-port/__tests__/source-discovery.test.ts similarity index 99% rename from src/commands/replay/source-discovery.test.ts rename to packages/replay-port/src/daemon-port/__tests__/source-discovery.test.ts index b1ad3687b3..310beb74cf 100644 --- a/src/commands/replay/source-discovery.test.ts +++ b/packages/replay-port/src/daemon-port/__tests__/source-discovery.test.ts @@ -5,7 +5,7 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { AppError } from '@agent-device/kernel/errors'; -import { discoverReplaySourcePaths } from './source-discovery.ts'; +import { discoverReplaySourcePaths } from '@agent-device/replay-port/source-discovery'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; // #1802: path expansion, traversal ordering and file-type routing are CALLER work — `test` diff --git a/src/__tests__/test-utils/replay-script-source.ts b/packages/replay-port/src/daemon-port/replay-script-source.fixtures.ts similarity index 82% rename from src/__tests__/test-utils/replay-script-source.ts rename to packages/replay-port/src/daemon-port/replay-script-source.fixtures.ts index f043da01c4..660cbf9754 100644 --- a/src/__tests__/test-utils/replay-script-source.ts +++ b/packages/replay-port/src/daemon-port/replay-script-source.fixtures.ts @@ -1,10 +1,12 @@ +// The single owner of the client-collected replay source bundles test requests carry (#1802): +// the same collector the real client runs, imported through this package's own subpaths. import fs from 'node:fs'; import type { ReplayScriptSourceBundle } from '@agent-device/contracts/replay'; +import type { DaemonWireRequest } from '@agent-device/contracts/command'; import type { MaestroSourceReader } from '@agent-device/maestro'; import { resolveUserPath } from '@agent-device/host-kit/file'; -import type { DaemonRequest } from '../../daemon/daemon-request.ts'; -import { loadReplayScriptSourceBundle } from '../../commands/replay/script-source-bundle.ts'; -import { discoverReplaySourcePaths } from '../../commands/replay/source-discovery.ts'; +import { loadReplayScriptSourceBundle } from './script-source-bundle.ts'; +import { discoverReplaySourcePaths } from './source-discovery.ts'; /** A native `.ad` script's one-entry source bundle for test requests. */ export function replayScriptSourceBundleFor(filePath: string): ReplayScriptSourceBundle { @@ -44,7 +46,9 @@ export const noMaestroIncludeSources: MaestroSourceReader = (resolvedPath) => { * the CLI boundary (pinned in `cli-client-commands.test.ts`), and a daemon-level harness must * still deliver the request so the daemon's own response is what the case observes. */ -export async function withClientReplayScriptSources(req: DaemonRequest): Promise { +export async function withClientReplayScriptSources( + req: DaemonWireRequest, +): Promise { if (req.command !== 'replay' && req.command !== 'test') return req; const inputs = req.positionals ?? []; const entryPath = inputs[0]; @@ -76,6 +80,9 @@ export async function withClientReplayScriptSources(req: DaemonRequest): Promise } } -function withFlags(req: DaemonRequest, flags: Partial): DaemonRequest { +function withFlags( + req: DaemonWireRequest, + flags: Partial>, +): DaemonWireRequest { return { ...req, flags: { ...(req.flags ?? {}), ...flags } }; } diff --git a/src/commands/replay/script-source-bundle.ts b/packages/replay-port/src/daemon-port/script-source-bundle.ts similarity index 100% rename from src/commands/replay/script-source-bundle.ts rename to packages/replay-port/src/daemon-port/script-source-bundle.ts diff --git a/src/commands/replay/source-discovery.ts b/packages/replay-port/src/daemon-port/source-discovery.ts similarity index 100% rename from src/commands/replay/source-discovery.ts rename to packages/replay-port/src/daemon-port/source-discovery.ts diff --git a/scripts/__tests__/eager-closure-budgets.ts b/scripts/__tests__/eager-closure-budgets.ts index 351a00821d..5c0520a4f2 100644 --- a/scripts/__tests__/eager-closure-budgets.ts +++ b/scripts/__tests__/eager-closure-budgets.ts @@ -132,7 +132,39 @@ export const NEW_ENTRY_CEILINGS: Readonly> = Objec */ export const APPROVED_OVER_CEILING: Readonly< Record -> = Object.freeze({}); +> = Object.freeze({ + // The single owner of the client-collected replay source bundles tests carry (#1802). Its + // weight is the real collector it exists to share, not fixture code. + 'packages/replay-port/src/daemon-port/replay-script-source.fixtures.ts': { + issue: '#3517', + reason: + 'Test-only request fixture whose whole contract is running the real client collector ' + + '(script-source-bundle + source-discovery) so the root and daemon test trees cannot ' + + 'drift from it; the weight is the ad-script format parser behind resolveReplayFormat. ' + + 'Making that lazy would fork a second collector, which is the duplication #3517 removes. ' + + 'No platform implementation; never bundled.', + owner: 'replay-port', + }, + // The two Android fixture subpaths are by design the REAL walker on archived device trees: + // their whole contract is running the shipped projection over captures no hand-written node + // could stand in for, so their closure IS the mechanics the subpath exposes. + 'packages/platform-android/src/snapshot-capture.fixtures.ts': { + issue: '#3517', + reason: + 'Test-only Android capture builders over the package mechanics facade, shared by the ' + + 'daemon and root test trees; a fixture that simulated the walk would prove nothing about ' + + 'production inclusion decisions. Test-only subpath; never bundled.', + owner: 'platform-android', + }, + 'packages/platform-android/src/ui-hierarchy.fixtures.ts': { + issue: '#3517', + reason: + 'Test-only archived device trees (IME, QS shade) plus the real non-raw/interactive walks ' + + 'over them, shared by the daemon and root test trees; the closure is the mechanics facade ' + + 'the fixtures must run. Test-only subpath; never bundled.', + owner: 'platform-android', + }, +}); /** The category is a function of the path, never a hand-written column. */ export function entryCategoryOf(entryFile: string): EntryCategory { @@ -151,6 +183,10 @@ export function entryCategoryOf(entryFile: string): EntryCategory { const PLATFORM_MECHANICS_ENTRY_PREFIXES = [ 'packages/platform-apple/src/runner/', 'packages/platform-android/src/mechanics.ts', + // The shared Android test fixtures ARE the mechanics exercised on archived device trees + // (#3517); like the mechanics facet, their closure is the implementation they export. + 'packages/platform-android/src/snapshot-capture.fixtures.ts', + 'packages/platform-android/src/ui-hierarchy.fixtures.ts', ] as const; const APPLE_DOMAIN_MECHANICS_ENTRY_FILES: ReadonlySet = new Set([ diff --git a/scripts/layering/package-boundaries.test.ts b/scripts/layering/package-boundaries.test.ts index b732afa650..981f1c4b2b 100644 --- a/scripts/layering/package-boundaries.test.ts +++ b/scripts/layering/package-boundaries.test.ts @@ -622,6 +622,9 @@ test('the real tree parses, declares, and passes R11', () => { '@agent-device/platform-android/adb-host', '@agent-device/platform-android/device-boot', '@agent-device/platform-android/mechanics', + // Test-only entries: the shared Android capture/walk fixtures (#3517). + '@agent-device/platform-android/snapshot-capture-fixtures', + '@agent-device/platform-android/ui-hierarchy-fixtures', ]); assert.deepEqual([...platformAndroidPackage.workspaceDependencies].sort(), [ '@agent-device/capture-kit', diff --git a/src/__tests__/android-ui-hierarchy-scope.test.ts b/src/__tests__/android-ui-hierarchy-scope.test.ts index e954dfd01c..a9f2047ac7 100644 --- a/src/__tests__/android-ui-hierarchy-scope.test.ts +++ b/src/__tests__/android-ui-hierarchy-scope.test.ts @@ -3,7 +3,7 @@ import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import { buildSnapshotState } from '@agent-device/capture-kit/snapshot-state'; -import { parseUiHierarchy } from '../daemon/__tests__/support/android-ui-hierarchy-fixtures.ts'; +import { parseUiHierarchy } from '@agent-device/platform-android/ui-hierarchy-fixtures'; import { AndroidSnapshotPresentationFailure, isAndroidSnapshotPresentationFailure, diff --git a/src/__tests__/android-ui-hierarchy.test.ts b/src/__tests__/android-ui-hierarchy.test.ts index be2170a61e..64cfdb7a89 100644 --- a/src/__tests__/android-ui-hierarchy.test.ts +++ b/src/__tests__/android-ui-hierarchy.test.ts @@ -9,7 +9,7 @@ import { androidUiNodes, parseUiHierarchyTree, } from '@agent-device/platform-android/mechanics'; -import { parseUiHierarchy } from '../daemon/__tests__/support/android-ui-hierarchy-fixtures.ts'; +import { parseUiHierarchy } from '@agent-device/platform-android/ui-hierarchy-fixtures'; function publishUiHierarchy(xml: string) { const captured = parseUiHierarchy(xml, 800, {}); diff --git a/src/__tests__/cli-startup-import-closure.test.ts b/src/__tests__/cli-startup-import-closure.test.ts index 416d482fd2..53a8635b24 100644 --- a/src/__tests__/cli-startup-import-closure.test.ts +++ b/src/__tests__/cli-startup-import-closure.test.ts @@ -121,7 +121,10 @@ test('the CLI startup import closure never evaluates the Maestro engine', () => test('the replay command source closure keeps Maestro behind flow selection', () => { const replayEntry = path.join(srcRoot, 'commands/replay/index.ts'); const replayClosure = eagerClosureOf(replayEntry); - const sourceBundle = path.join(srcRoot, 'commands/replay/script-source-bundle.ts'); + const sourceBundle = path.join( + srcRoot, + '../packages/replay-port/src/daemon-port/script-source-bundle.ts', + ); const offenders: string[] = []; expect(replayClosure).toContain(sourceBundle); diff --git a/src/__tests__/daemon-composition/handlers/snapshot-alert-android-occlusion.test.ts b/src/__tests__/daemon-composition/handlers/snapshot-alert-android-occlusion.test.ts index 8aa00780a0..84c8e6e23e 100644 --- a/src/__tests__/daemon-composition/handlers/snapshot-alert-android-occlusion.test.ts +++ b/src/__tests__/daemon-composition/handlers/snapshot-alert-android-occlusion.test.ts @@ -13,7 +13,7 @@ vi.mock('@agent-device/platform-android/mechanics', async (importOriginal) => { import { pressAndroid, snapshotAndroid } from '@agent-device/platform-android/mechanics'; import { ANDROID_EMULATOR } from '@agent-device/test-kit/device-fixtures'; import { createAndroidInteractor } from '../../../core/interactors/android.ts'; -import { makeAndroidSnapshotCapture } from '../../../daemon/__tests__/support/android-snapshot-capture.ts'; +import { makeAndroidSnapshotCapture } from '@agent-device/platform-android/snapshot-capture-fixtures'; // R59 moved the alert legs onto the Android interactor, which is where the occlusion reading // they depend on lives: the legs capture through the same presentation pass `snapshot` publishes, diff --git a/src/__tests__/daemon-composition/http-server-limrun-uploaded-apps.test.ts b/src/__tests__/daemon-composition/http-server-limrun-uploaded-apps.test.ts index eec1b4e051..742de15b8a 100644 --- a/src/__tests__/daemon-composition/http-server-limrun-uploaded-apps.test.ts +++ b/src/__tests__/daemon-composition/http-server-limrun-uploaded-apps.test.ts @@ -4,7 +4,7 @@ import path from 'node:path'; import { test, vi } from 'vitest'; import { createLimrunRuntime } from '@agent-device/provider-limrun'; import { createProviderDeviceRuntimeRequestProviders } from '../../provider-device-runtime.ts'; -import { createTestDeviceInventoryGateways } from '../test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { closeLoopbackServer, listenOnLoopback, diff --git a/src/__tests__/daemon-composition/interaction/interaction-ios-tap-outcome.test.ts b/src/__tests__/daemon-composition/interaction/interaction-ios-tap-outcome.test.ts index 8cc4d79428..2695a56e01 100644 --- a/src/__tests__/daemon-composition/interaction/interaction-ios-tap-outcome.test.ts +++ b/src/__tests__/daemon-composition/interaction/interaction-ios-tap-outcome.test.ts @@ -15,7 +15,7 @@ import { import { makeSessionStore } from '../../../daemon/__tests__/support/store-factory.ts'; import { SessionScriptWriter } from '../../../daemon/session-script-writer.ts'; import { runReplayForTest } from '../../../daemon/__tests__/replay-runtime/replay-command-fixture.ts'; -import { replayScriptSourceBundleFor } from '../../test-utils/replay-script-source.ts'; +import { replayScriptSourceBundleFor } from '@agent-device/replay-port/replay-script-source-fixtures'; import { imageViewerNodes, profileNodes, diff --git a/src/__tests__/daemon-composition/is-runtime.test.ts b/src/__tests__/daemon-composition/is-runtime.test.ts index c413e78ce9..a319bb5bc6 100644 --- a/src/__tests__/daemon-composition/is-runtime.test.ts +++ b/src/__tests__/daemon-composition/is-runtime.test.ts @@ -9,7 +9,7 @@ import { makeIosSession, } from '../../daemon/__tests__/support/session-factories.ts'; import { makeSessionStore } from '../../daemon/__tests__/support/store-factory.ts'; -import { withTestDeviceInventory } from '../test-utils/device-inventory-gateways.ts'; +import { withTestDeviceInventory } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { makeSnapshotState } from '@agent-device/selectors/snapshot-geometry-fixtures'; import type { PostGestureOutcome } from '@agent-device/kernel/snapshot'; import type { DaemonRequest } from '../../daemon/daemon-request.ts'; diff --git a/src/__tests__/daemon-composition/request-dispatch-disclosure.test.ts b/src/__tests__/daemon-composition/request-dispatch-disclosure.test.ts index 0099365042..47e143a0a9 100644 --- a/src/__tests__/daemon-composition/request-dispatch-disclosure.test.ts +++ b/src/__tests__/daemon-composition/request-dispatch-disclosure.test.ts @@ -24,7 +24,7 @@ import { DISPATCH_DISCLOSURE_TABLE_PATH, dispatchDisclosureRowsOwnedBy, } from '@agent-device/contracts/dispatch-disclosure-fixtures'; -import { createTestDeviceInventoryGateways } from '../test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { makeAndroidSession, makeIosAppSession, diff --git a/src/__tests__/daemon-composition/request-platform-providers.test.ts b/src/__tests__/daemon-composition/request-platform-providers.test.ts index 7e1e44888c..72043c1ce2 100644 --- a/src/__tests__/daemon-composition/request-platform-providers.test.ts +++ b/src/__tests__/daemon-composition/request-platform-providers.test.ts @@ -12,7 +12,7 @@ import { makeMacOsSession, makeSession, } from '../../daemon/__tests__/support/session-factories.ts'; -import { withTestDeviceInventoryProvider as withTargetDeviceResolutionScope } from '../test-utils/device-inventory-gateways.ts'; +import { withTestDeviceInventoryProvider as withTargetDeviceResolutionScope } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { createLocalAppleToolProvider, runXcrun } from '@agent-device/platform-apple/tool-provider'; import { buildSimctlArgsForDevice } from '@agent-device/platform-apple/simctl'; import type { AndroidAdbExecutor } from '@agent-device/platform-android/mechanics'; diff --git a/src/__tests__/daemon-composition/request-router-android-modal.test.ts b/src/__tests__/daemon-composition/request-router-android-modal.test.ts index 6c9b67a872..3acd1fa2b6 100644 --- a/src/__tests__/daemon-composition/request-router-android-modal.test.ts +++ b/src/__tests__/daemon-composition/request-router-android-modal.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from '../test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { test, expect, vi } from 'vitest'; import path from 'node:path'; diff --git a/src/__tests__/daemon-composition/request-router-android-perf.test.ts b/src/__tests__/daemon-composition/request-router-android-perf.test.ts index c9a159791e..30a52c1dc1 100644 --- a/src/__tests__/daemon-composition/request-router-android-perf.test.ts +++ b/src/__tests__/daemon-composition/request-router-android-perf.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from '../test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { expect, test } from 'vitest'; import { createRequestHandler } from '../../daemon/__tests__/test-device-runtime-gateway.ts'; import { LeaseRegistry } from '../../daemon/lease-registry.ts'; diff --git a/src/__tests__/daemon-composition/request-router-android-snapshot-helper.test.ts b/src/__tests__/daemon-composition/request-router-android-snapshot-helper.test.ts index 9aa08ef42d..7c6cc38fb4 100644 --- a/src/__tests__/daemon-composition/request-router-android-snapshot-helper.test.ts +++ b/src/__tests__/daemon-composition/request-router-android-snapshot-helper.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from '../test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { beforeEach, expect, test } from 'vitest'; import { createRequestHandler } from '../../daemon/__tests__/test-device-runtime-gateway.ts'; import { LeaseRegistry } from '../../daemon/lease-registry.ts'; diff --git a/src/__tests__/daemon-composition/request-router-fill-text-stdin.test.ts b/src/__tests__/daemon-composition/request-router-fill-text-stdin.test.ts index 297075a27f..629b4e880f 100644 --- a/src/__tests__/daemon-composition/request-router-fill-text-stdin.test.ts +++ b/src/__tests__/daemon-composition/request-router-fill-text-stdin.test.ts @@ -7,7 +7,7 @@ import { expect, test } from 'vitest'; import fs from 'node:fs'; import path from 'node:path'; -import { createTestDeviceInventoryGateways } from '../test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import type { WebProvider } from '@agent-device/platform-web'; import type { DaemonRequest } from '../../daemon/daemon-request.ts'; import type { SessionState } from '../../daemon/session-state.ts'; @@ -21,7 +21,7 @@ import { import { WEB_DESKTOP_DEVICE } from '@agent-device/test-kit/device-fixtures'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; import { testCreateCommandSurface } from './command-surface-fixture.ts'; -import { replayScriptSourceBundleFor } from '../test-utils/replay-script-source.ts'; +import { replayScriptSourceBundleFor } from '@agent-device/replay-port/replay-script-source-fixtures'; import { createPlatformRuntimeGateway, createRequestPlatformProviders, diff --git a/src/__tests__/daemon-composition/request-router-lock-policy.test.ts b/src/__tests__/daemon-composition/request-router-lock-policy.test.ts index 9c658461ce..1c67eb48fb 100644 --- a/src/__tests__/daemon-composition/request-router-lock-policy.test.ts +++ b/src/__tests__/daemon-composition/request-router-lock-policy.test.ts @@ -1,7 +1,7 @@ import { createTestDeviceInventoryGateways, createTestDeviceInventoryGatewaysFromProvider, -} from '../test-utils/device-inventory-gateways.ts'; +} from '@agent-device/device-selection/inventory-gateways-fixtures'; import { test, expect, vi, beforeEach } from 'vitest'; import { legacyDispatchCapture } from '../../daemon/__tests__/legacy-snapshot-capture-fixture.ts'; import path from 'node:path'; diff --git a/src/__tests__/daemon-composition/request-router-macos-app-lease.test.ts b/src/__tests__/daemon-composition/request-router-macos-app-lease.test.ts index 4875c8f609..02604ce4ff 100644 --- a/src/__tests__/daemon-composition/request-router-macos-app-lease.test.ts +++ b/src/__tests__/daemon-composition/request-router-macos-app-lease.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, expect, test, vi } from 'vitest'; import path from 'node:path'; -import { createTestDeviceInventoryGateways } from '../test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { MACOS_DEVICE } from '@agent-device/test-kit/device-fixtures'; import { makeSessionStore } from '../../daemon/__tests__/support/store-factory.ts'; import { makeMacOsSession } from '../../daemon/__tests__/support/session-factories.ts'; diff --git a/src/__tests__/daemon-composition/request-router-provider-credentials.test.ts b/src/__tests__/daemon-composition/request-router-provider-credentials.test.ts index 64df4cf00a..fe5f0d00af 100644 --- a/src/__tests__/daemon-composition/request-router-provider-credentials.test.ts +++ b/src/__tests__/daemon-composition/request-router-provider-credentials.test.ts @@ -1,6 +1,6 @@ import path from 'node:path'; import { expect, test } from 'vitest'; -import { createTestDeviceInventoryGateways } from '../test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { makeSessionStore } from '../../daemon/__tests__/support/store-factory.ts'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; import { diff --git a/src/__tests__/daemon-composition/request-router-record-runtime-lock.test.ts b/src/__tests__/daemon-composition/request-router-record-runtime-lock.test.ts index c9bd99c4d7..96433041cd 100644 --- a/src/__tests__/daemon-composition/request-router-record-runtime-lock.test.ts +++ b/src/__tests__/daemon-composition/request-router-record-runtime-lock.test.ts @@ -10,7 +10,7 @@ import type { PlatformRuntimeOperations } from '@agent-device/contracts/platform import type { ScreenRecordingLiveHandle } from '@agent-device/contracts/screen-recording-runtime'; import { createDurableResourceEnvelope } from '@agent-device/capture-kit'; import type { DeviceInfo } from '@agent-device/kernel/device'; -import { createTestDeviceInventoryGateways } from '../test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { unavailableDeploymentSnapshotAndShutdownOperationFacts } from '@agent-device/test-kit/runtime-operation-facts'; import { makeSessionStore } from '../../daemon/__tests__/support/store-factory.ts'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; diff --git a/src/__tests__/daemon-composition/request-router-recording-health.test.ts b/src/__tests__/daemon-composition/request-router-recording-health.test.ts index b14d4c3070..e3109cb15e 100644 --- a/src/__tests__/daemon-composition/request-router-recording-health.test.ts +++ b/src/__tests__/daemon-composition/request-router-recording-health.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from '../test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { legacyDispatchCapture } from '../../daemon/__tests__/legacy-snapshot-capture-fixture.ts'; import { test, expect, vi, beforeEach } from 'vitest'; diff --git a/src/__tests__/daemon-composition/request-router-replay-scope.test.ts b/src/__tests__/daemon-composition/request-router-replay-scope.test.ts index f41c2b8e6c..20ed0e0ee3 100644 --- a/src/__tests__/daemon-composition/request-router-replay-scope.test.ts +++ b/src/__tests__/daemon-composition/request-router-replay-scope.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from '../test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { legacyDispatchCapture } from '../../daemon/__tests__/legacy-snapshot-capture-fixture.ts'; import { beforeEach, expect, test, vi } from 'vitest'; import fs from 'node:fs'; diff --git a/src/__tests__/daemon-composition/request-router-replay-test-ime.test.ts b/src/__tests__/daemon-composition/request-router-replay-test-ime.test.ts index 9761d3e8e0..8f02895b41 100644 --- a/src/__tests__/daemon-composition/request-router-replay-test-ime.test.ts +++ b/src/__tests__/daemon-composition/request-router-replay-test-ime.test.ts @@ -5,7 +5,7 @@ * lifecycle binding, where the platform host performs the activation. Physical-device * defaults stay off: the activation seam fires only for an explicit opt-in. */ -import { createTestDeviceInventoryGateways } from '../test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { beforeEach, expect, test, vi } from 'vitest'; import fs from 'node:fs'; @@ -38,7 +38,7 @@ vi.mock('@agent-device/platform-android/mechanics', async (importOriginal) => { import { ANDROID_DEVICE } from '@agent-device/test-kit/device-fixtures'; import { makeSessionStore } from '../../daemon/__tests__/support/store-factory.ts'; -import { replayScriptSourceBundleFor } from '../test-utils/replay-script-source.ts'; +import { replayScriptSourceBundleFor } from '@agent-device/replay-port/replay-script-source-fixtures'; import { LeaseRegistry } from '../../daemon/lease-registry.ts'; import { createRequestHandler, diff --git a/src/__tests__/daemon-composition/request-router-screenshot.test.ts b/src/__tests__/daemon-composition/request-router-screenshot.test.ts index aea0c395d2..636187e1ca 100644 --- a/src/__tests__/daemon-composition/request-router-screenshot.test.ts +++ b/src/__tests__/daemon-composition/request-router-screenshot.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from '../test-utils/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { legacyDispatchCapture } from '../../daemon/__tests__/legacy-snapshot-capture-fixture.ts'; import { test, expect, vi, beforeEach } from 'vitest'; import fs from 'node:fs'; diff --git a/src/__tests__/platform-runtime-apple-resources.test.ts b/src/__tests__/platform-runtime-apple-resources.test.ts index 9bce56fd11..94dbeae82e 100644 --- a/src/__tests__/platform-runtime-apple-resources.test.ts +++ b/src/__tests__/platform-runtime-apple-resources.test.ts @@ -1,7 +1,7 @@ import { expect, test, vi } from 'vitest'; import { AppError } from '@agent-device/kernel/errors'; import { appleSessionObservation } from '../platform-runtime-apple-resources.ts'; -import { withTestDeviceInventory } from './test-utils/device-inventory-gateways.ts'; +import { withTestDeviceInventory } from '@agent-device/device-selection/inventory-gateways-fixtures'; test('foreground observation uses request-local inventory without consulting a provider', async () => { const local = vi.fn(async () => []); diff --git a/src/__tests__/test-utils/device-inventory-gateways.test.ts b/src/__tests__/test-utils/device-inventory-gateways.test.ts deleted file mode 100644 index e4cc480ba5..0000000000 --- a/src/__tests__/test-utils/device-inventory-gateways.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { ProviderDeviceInventorySource } from '@agent-device/contracts/device'; -import type { PlatformRequestScope } from '@agent-device/contracts/platform-runtime-host'; -import { expect, test } from 'vitest'; -import { createTestDeviceInventoryGateways } from './device-inventory-gateways.ts'; - -const scope: PlatformRequestScope = Object.freeze({ - signal: new AbortController().signal, - diagnostics: Object.freeze({ emit: () => {} }), - progress: Object.freeze({ report: () => {} }), -}); - -test('test inventory gateways preserve the production fail-closed provider contract', async () => { - const gateways = createTestDeviceInventoryGateways({ - provider: { discover: async () => undefined } as unknown as ProviderDeviceInventorySource, - }); - - await expect(gateways.providerFirst.discover({}, scope)).rejects.toMatchObject({ - details: { reason: 'provider_inventory_contract_violation' }, - }); -}); diff --git a/src/__tests__/test-utils/device-inventory-gateways.ts b/src/__tests__/test-utils/device-inventory-gateways.ts deleted file mode 100644 index e8f49766b1..0000000000 --- a/src/__tests__/test-utils/device-inventory-gateways.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { - type DeviceInventoryProvider, - type DeviceInventoryRequest, - type ProviderDeviceInventorySource, -} from '@agent-device/contracts/device'; -import { - type ComposedDeviceInventoryGateways, - type DeviceInventorySource, - type InventoryPlatformModule, - createPlatformModuleRegistry, -} from '@agent-device/contracts/platform-module'; -import type { - DeviceInventoryHost, - PlatformRequestScope, -} from '@agent-device/contracts/platform-runtime-host'; -import type { DeviceInfo, Platform } from '@agent-device/kernel/device'; -import { withDeviceInventoryContext } from '@agent-device/device-selection/device-inventory-context'; -import { createComposedDeviceInventoryGateways } from '../../platform-runtime-device-inventory.ts'; - -type TestDeviceInventoryOptions = Readonly<{ - provider?: ProviderDeviceInventorySource; - local?: (request: Readonly) => Promise; -}>; - -const testRequestScope: PlatformRequestScope = Object.freeze({ - signal: new AbortController().signal, - diagnostics: Object.freeze({ emit: () => {} }), - progress: Object.freeze({ report: () => {} }), -}); - -export function createTestDeviceInventoryGateways( - options: TestDeviceInventoryOptions = {}, -): ComposedDeviceInventoryGateways { - const localDiscover = options.local ?? (async () => []); - return createComposedDeviceInventoryGateways({ - registry: createPlatformModuleRegistry(testInventoryModules(localDiscover)), - loadHost: async () => testInventoryHost, - provider: options.provider, - }); -} - -const testInventoryHost: DeviceInventoryHost = Object.freeze({ - commands: Object.freeze({ - which: async () => undefined, - run: async () => ({ stdout: '', stderr: '', exitCode: 0 }), - }), - appleTools: Object.freeze({ - isXcrunAvailable: async () => false, - run: async () => ({ stdout: '', stderr: '', exitCode: 0 }), - }), - toolchains: Object.freeze({ prepare: async () => {} }), - files: Object.freeze({ - isExecutable: async () => false, - createTemporaryTextFile: async () => { - throw new Error('unused test inventory temporary file'); - }, - }), - hostOs: 'other', - hostName: 'test-host', - homeDirectory: '/tmp/test-home', - observations: Object.freeze({ deviceBooted: async () => {} }), -}); - -function testInventoryModules( - discover: (request: Readonly) => Promise, -): readonly InventoryPlatformModule[] { - const source = (family: Platform): DeviceInventorySource => ({ - discover: async (request) => - (await discover(request)).filter((device) => device.platform === family), - }); - return [ - { family: 'apple', loadInventory: async () => source('apple') }, - { family: 'android', loadInventory: async () => source('android') }, - { family: 'harmonyos', loadInventory: async () => source('harmonyos') }, - { family: 'vega', loadInventory: async () => source('vega') }, - { family: 'linux', loadInventory: async () => source('linux') }, - { family: 'web', loadInventory: async () => source('web') }, - ]; -} - -/** Test-only bridge for fixtures that still implement the public nullable provider port. */ -export function createTestDeviceInventoryGatewaysFromProvider( - provider: DeviceInventoryProvider, -): ComposedDeviceInventoryGateways { - return createTestDeviceInventoryGateways({ - provider: { - discover: async (request, signal) => { - signal.throwIfAborted(); - const devices = await provider(request, signal); - return devices === null || devices === undefined - ? { kind: 'declined' } - : { kind: 'inventory', devices }; - }, - }, - }); -} - -export async function withTestDeviceInventory( - options: TestDeviceInventoryOptions, - task: () => Promise, -): Promise { - return await withDeviceInventoryContext( - { ...createTestDeviceInventoryGateways(options), requestScope: testRequestScope }, - task, - ); -} - -export async function withTestDeviceInventoryProvider( - provider: DeviceInventoryProvider, - task: () => Promise, -): Promise { - return await withDeviceInventoryContext( - { - ...createTestDeviceInventoryGatewaysFromProvider(provider), - requestScope: testRequestScope, - }, - task, - ); -} diff --git a/src/cli/replay-test/__tests__/session-test-reporter-values.test.ts b/src/cli/replay-test/__tests__/session-test-reporter-values.test.ts index 3e6f4153c4..02b2d2ca17 100644 --- a/src/cli/replay-test/__tests__/session-test-reporter-values.test.ts +++ b/src/cli/replay-test/__tests__/session-test-reporter-values.test.ts @@ -28,7 +28,7 @@ import { handleSessionCommands } from '../../../daemon/handlers/__tests__/sessio import { SessionStore } from '../../../daemon/session-store.ts'; import type { DaemonRequest, DaemonResponse } from '../../../daemon/daemon-request.ts'; import { withRequestProgressSink } from '@agent-device/host-kit/request'; -import { withTestDeviceInventoryProvider as withDeviceInventoryProvider } from '../../../__tests__/test-utils/device-inventory-gateways.ts'; +import { withTestDeviceInventoryProvider as withDeviceInventoryProvider } from '@agent-device/device-selection/inventory-gateways-fixtures'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { getReplayTestReporterExitCode, diff --git a/src/commands/replay/index.ts b/src/commands/replay/index.ts index 9afae42b3d..28e13047f8 100644 --- a/src/commands/replay/index.ts +++ b/src/commands/replay/index.ts @@ -32,8 +32,8 @@ import { parseReplayCliEnvEntries, readReplayCliEnvEntries, } from '@agent-device/ad-script'; -import { loadReplayScriptSourceBundle } from './script-source-bundle.ts'; -import { discoverReplaySourcePaths } from './source-discovery.ts'; +import { loadReplayScriptSourceBundle } from '@agent-device/replay-port/script-source-bundle'; +import { discoverReplaySourcePaths } from '@agent-device/replay-port/source-discovery'; const REPLAY_COMMAND_NAME = 'replay'; const TEST_COMMAND_NAME = 'test'; diff --git a/src/core/__tests__/snapshot-chrome-android-statusbar.test.ts b/src/core/__tests__/snapshot-chrome-android-statusbar.test.ts index 453c421ca0..3339bfb367 100644 --- a/src/core/__tests__/snapshot-chrome-android-statusbar.test.ts +++ b/src/core/__tests__/snapshot-chrome-android-statusbar.test.ts @@ -15,7 +15,7 @@ import { ANDROID_QS_SHADE_CAPTURE_RAW_NODES, walkInteractiveOnlyAndroidFixture, walkNonRawAndroidFixture, -} from '../../daemon/__tests__/support/android-ui-hierarchy-fixtures.ts'; +} from '@agent-device/platform-android/ui-hierarchy-fixtures'; import { isAndroidSystemChromeWindowResourceId } from '@agent-device/contracts/android-system-chrome'; /** diff --git a/src/core/interactors/android.test.ts b/src/core/interactors/android.test.ts index b201174560..79a6a0a4d8 100644 --- a/src/core/interactors/android.test.ts +++ b/src/core/interactors/android.test.ts @@ -10,7 +10,7 @@ import { scrollAndroid, snapshotAndroid, } from '@agent-device/platform-android/mechanics'; -import { makeAndroidSnapshotCapture } from '../../daemon/__tests__/support/android-snapshot-capture.ts'; +import { makeAndroidSnapshotCapture } from '@agent-device/platform-android/snapshot-capture-fixtures'; vi.mock('@agent-device/platform-android/mechanics', async (importOriginal) => { const actual = await importOriginal(); diff --git a/src/daemon/__tests__/android-system-dialog-occlusion.test.ts b/src/daemon/__tests__/android-system-dialog-occlusion.test.ts index 71b75e9ea2..58f2005c0b 100644 --- a/src/daemon/__tests__/android-system-dialog-occlusion.test.ts +++ b/src/daemon/__tests__/android-system-dialog-occlusion.test.ts @@ -14,7 +14,7 @@ import { recoverAndroidBlockingSystemDialog as recoverOwnedAndroidBlockingSystem import { androidObservation } from '../../platform-runtime.ts'; import { makeAndroidSession } from './support/session-factories.ts'; import { makeTestScreenRecordingResource } from './support/screen-recording-live-handle.ts'; -import { makeAndroidSnapshotCapture } from './support/android-snapshot-capture.ts'; +import { makeAndroidSnapshotCapture } from '@agent-device/platform-android/snapshot-capture-fixtures'; const recoverAndroidBlockingSystemDialog = ( params: Omit[0], 'observation'>, diff --git a/src/daemon/__tests__/android-system-dialog-ref-frame.test.ts b/src/daemon/__tests__/android-system-dialog-ref-frame.test.ts index d5f632894d..6e787f6b7b 100644 --- a/src/daemon/__tests__/android-system-dialog-ref-frame.test.ts +++ b/src/daemon/__tests__/android-system-dialog-ref-frame.test.ts @@ -12,7 +12,7 @@ import { recoverAndroidBlockingSystemDialog as recoverOwnedAndroidBlockingSystem import { androidObservation } from '../../platform-runtime.ts'; import { makeAndroidSession } from './support/session-factories.ts'; import { makeTestScreenRecordingResource } from './support/screen-recording-live-handle.ts'; -import { makeAndroidSnapshotCapture } from './support/android-snapshot-capture.ts'; +import { makeAndroidSnapshotCapture } from '@agent-device/platform-android/snapshot-capture-fixtures'; import { refFrameState } from '../ref-frame.ts'; const recoverAndroidBlockingSystemDialog = ( diff --git a/src/daemon/__tests__/back-runtime.test.ts b/src/daemon/__tests__/back-runtime.test.ts index 327a339669..5dc2420152 100644 --- a/src/daemon/__tests__/back-runtime.test.ts +++ b/src/daemon/__tests__/back-runtime.test.ts @@ -16,7 +16,7 @@ import { import { deviceShape } from '@agent-device/kernel/device'; import { makeSession } from './support/session-factories.ts'; import { makeSessionStore } from './support/store-factory.ts'; -import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { LeaseRegistry } from '../lease-registry.ts'; import { activateCompleteRefFrame, refFrameState } from '../ref-frame.ts'; import type { BindDeviceRuntime, InspectDeviceRuntimeFacts } from '../request-runtime-binding.ts'; diff --git a/src/daemon/__tests__/daemon-policy.test.ts b/src/daemon/__tests__/daemon-policy.test.ts index b4bbec166a..a3f85fb172 100644 --- a/src/daemon/__tests__/daemon-policy.test.ts +++ b/src/daemon/__tests__/daemon-policy.test.ts @@ -8,7 +8,7 @@ vi.mock('../device/device-ready.ts', () => ({ ensureDeviceReady: vi.fn(async () import type { DeviceInfo } from '@agent-device/kernel/device'; import { ANDROID_EMULATOR, IOS_SIMULATOR } from '@agent-device/test-kit/device-fixtures'; -import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { makeIosSession } from './support/session-factories.ts'; import { makeSessionStore } from './support/store-factory.ts'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; diff --git a/src/daemon/__tests__/focus-runtime.test.ts b/src/daemon/__tests__/focus-runtime.test.ts index d0ac6fd933..8a97b29908 100644 --- a/src/daemon/__tests__/focus-runtime.test.ts +++ b/src/daemon/__tests__/focus-runtime.test.ts @@ -16,7 +16,7 @@ import { import { deviceShape } from '@agent-device/kernel/device'; import { makeSession } from './support/session-factories.ts'; import { makeSessionStore } from './support/store-factory.ts'; -import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { LeaseRegistry } from '../lease-registry.ts'; import { activateCompleteRefFrame, refFrameState } from '../ref-frame.ts'; import type { BindDeviceRuntime, InspectDeviceRuntimeFacts } from '../request-runtime-binding.ts'; diff --git a/src/daemon/__tests__/human-control-router-fixture.ts b/src/daemon/__tests__/human-control-router-fixture.ts index b001cdc289..095fa4d8fb 100644 --- a/src/daemon/__tests__/human-control-router-fixture.ts +++ b/src/daemon/__tests__/human-control-router-fixture.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { daemonPlatformServicesFixture, daemonClaimRecoveryGatewayFixture, diff --git a/src/daemon/__tests__/orientation-runtime.test.ts b/src/daemon/__tests__/orientation-runtime.test.ts index 006b715332..d0a4032a73 100644 --- a/src/daemon/__tests__/orientation-runtime.test.ts +++ b/src/daemon/__tests__/orientation-runtime.test.ts @@ -39,7 +39,7 @@ import { import { deviceShape } from '@agent-device/kernel/device'; import { makeSession } from './support/session-factories.ts'; import { makeSessionStore } from './support/store-factory.ts'; -import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { LeaseRegistry } from '../lease-registry.ts'; import { activateCompleteRefFrame, refFrameState } from '../ref-frame.ts'; import type { BindDeviceRuntime, InspectDeviceRuntimeFacts } from '../request-runtime-binding.ts'; diff --git a/src/daemon/__tests__/replay-device-selection.test.ts b/src/daemon/__tests__/replay-device-selection.test.ts index 439892cad7..6cace56ba2 100644 --- a/src/daemon/__tests__/replay-device-selection.test.ts +++ b/src/daemon/__tests__/replay-device-selection.test.ts @@ -12,7 +12,7 @@ import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; import { maestroScriptSourceBundleFor, replayScriptSourceBundleFor, -} from './support/replay-script-source.ts'; +} from '@agent-device/replay-port/replay-script-source-fixtures'; test('replay leaves deep-link opens to normal device resolution', async () => { const root = mkdtempForTestSync('agent-device-replay-device-selection-'); diff --git a/src/daemon/__tests__/replay-divergence/session-replay-divergence-android-occlusion.test.ts b/src/daemon/__tests__/replay-divergence/session-replay-divergence-android-occlusion.test.ts index f688b30af3..3a68f14d51 100644 --- a/src/daemon/__tests__/replay-divergence/session-replay-divergence-android-occlusion.test.ts +++ b/src/daemon/__tests__/replay-divergence/session-replay-divergence-android-occlusion.test.ts @@ -5,7 +5,7 @@ import { makeAndroidSession } from '../support/session-factories.ts'; import { ANDROID_QS_SHADE_CAPTURE_RAW_NODES, walkNonRawAndroidFixture, -} from '../support/android-ui-hierarchy-fixtures.ts'; +} from '@agent-device/platform-android/ui-hierarchy-fixtures'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; import { SessionStore } from '../../session-store.ts'; import { replayCoordinatorForTest, replaySessionForTest } from './replay-session-fixture.ts'; diff --git a/src/daemon/__tests__/replay-divergence/session-replay-divergence-chrome-filter.test.ts b/src/daemon/__tests__/replay-divergence/session-replay-divergence-chrome-filter.test.ts index c749cb288d..9182a31436 100644 --- a/src/daemon/__tests__/replay-divergence/session-replay-divergence-chrome-filter.test.ts +++ b/src/daemon/__tests__/replay-divergence/session-replay-divergence-chrome-filter.test.ts @@ -5,7 +5,7 @@ import { makeAndroidSession, makeIosSession } from '../support/session-factories import { ANDROID_IME_CAPTURE_RAW_NODES, walkNonRawAndroidFixture, -} from '../support/android-ui-hierarchy-fixtures.ts'; +} from '@agent-device/platform-android/ui-hierarchy-fixtures'; import { SessionStore } from '../../session-store.ts'; import { buildReplayFailureDivergence } from '@agent-device/replay-port/session-replay-divergence'; import { replayDivergenceForTest } from './replay-session-fixture.ts'; diff --git a/src/daemon/__tests__/replay-divergence/session-replay-divergence-overlay.test.ts b/src/daemon/__tests__/replay-divergence/session-replay-divergence-overlay.test.ts index 6d76ba5d6d..f4d6cc3876 100644 --- a/src/daemon/__tests__/replay-divergence/session-replay-divergence-overlay.test.ts +++ b/src/daemon/__tests__/replay-divergence/session-replay-divergence-overlay.test.ts @@ -5,7 +5,7 @@ import { makeAndroidSession } from '../support/session-factories.ts'; import { ANDROID_IME_CAPTURE_RAW_NODES, walkNonRawAndroidFixture, -} from '../support/android-ui-hierarchy-fixtures.ts'; +} from '@agent-device/platform-android/ui-hierarchy-fixtures'; import { SessionStore } from '../../session-store.ts'; import { buildReplayFailureDivergence } from '@agent-device/replay-port/session-replay-divergence'; import { replayDivergenceForTest } from './replay-session-fixture.ts'; diff --git a/src/daemon/__tests__/replay-maestro/session-replay-maestro-error-projection.test.ts b/src/daemon/__tests__/replay-maestro/session-replay-maestro-error-projection.test.ts index 8f92e4eb90..96e659ae46 100644 --- a/src/daemon/__tests__/replay-maestro/session-replay-maestro-error-projection.test.ts +++ b/src/daemon/__tests__/replay-maestro/session-replay-maestro-error-projection.test.ts @@ -6,7 +6,7 @@ import { runTypedMaestroReplay } from '@agent-device/replay-port/session-replay- import { SessionStore } from '../../session-store.ts'; import { replayCommandForTest } from '../replay-runtime/replay-command-fixture.ts'; import { makeIosSession } from '../support/session-factories.ts'; -import { maestroScriptSourceBundleFor } from '../support/replay-script-source.ts'; +import { maestroScriptSourceBundleFor } from '@agent-device/replay-port/replay-script-source-fixtures'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; import type { DaemonRequest } from '../../daemon-request.ts'; diff --git a/src/daemon/__tests__/replay-maestro/session-replay-maestro-failure.test.ts b/src/daemon/__tests__/replay-maestro/session-replay-maestro-failure.test.ts index 90e42da114..a35138f136 100644 --- a/src/daemon/__tests__/replay-maestro/session-replay-maestro-failure.test.ts +++ b/src/daemon/__tests__/replay-maestro/session-replay-maestro-failure.test.ts @@ -1,4 +1,4 @@ -import { noMaestroIncludeSources } from '../support/replay-script-source.ts'; +import { noMaestroIncludeSources } from '@agent-device/replay-port/replay-script-source-fixtures'; import { beforeEach, expect, test, vi } from 'vitest'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; diff --git a/src/daemon/__tests__/replay-maestro/session-replay-maestro-remote-evalscript.test.ts b/src/daemon/__tests__/replay-maestro/session-replay-maestro-remote-evalscript.test.ts index ea0ee6ceb0..cb6c0a952a 100644 --- a/src/daemon/__tests__/replay-maestro/session-replay-maestro-remote-evalscript.test.ts +++ b/src/daemon/__tests__/replay-maestro/session-replay-maestro-remote-evalscript.test.ts @@ -7,7 +7,7 @@ import { LeaseRegistry } from '../../lease-registry.ts'; import { platformResourceCleanupFixture } from '../support/platform-resource-cleanup-fixture.ts'; import { SessionStore } from '../../session-store.ts'; import { makeIosSession } from '../support/session-factories.ts'; -import { maestroScriptSourceBundleFor } from '../support/replay-script-source.ts'; +import { maestroScriptSourceBundleFor } from '@agent-device/replay-port/replay-script-source-fixtures'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; import type { DaemonRequest } from '../../daemon-request.ts'; import { diff --git a/src/daemon/__tests__/replay-maestro/session-replay-maestro-session-address.test.ts b/src/daemon/__tests__/replay-maestro/session-replay-maestro-session-address.test.ts index 49421eab9d..8880859e54 100644 --- a/src/daemon/__tests__/replay-maestro/session-replay-maestro-session-address.test.ts +++ b/src/daemon/__tests__/replay-maestro/session-replay-maestro-session-address.test.ts @@ -6,7 +6,7 @@ import { runTypedMaestroReplay } from '@agent-device/replay-port/session-replay- import { SessionStore } from '../../session-store.ts'; import { replayCommandForTest } from '../replay-runtime/replay-command-fixture.ts'; import { makeIosSession } from '../support/session-factories.ts'; -import { maestroScriptSourceBundleFor } from '../support/replay-script-source.ts'; +import { maestroScriptSourceBundleFor } from '@agent-device/replay-port/replay-script-source-fixtures'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; import type { DaemonRequest } from '../../daemon-request.ts'; diff --git a/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro-dispatch.test.ts b/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro-dispatch.test.ts index 5398e2a545..3cc75220c7 100644 --- a/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro-dispatch.test.ts +++ b/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro-dispatch.test.ts @@ -6,7 +6,7 @@ import { test } from 'vitest'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; import type { DaemonRequest, DaemonResponse } from '../../daemon-request.ts'; import { SessionStore } from '../../session-store.ts'; -import { replayScriptSourceBundleFor } from '../support/replay-script-source.ts'; +import { replayScriptSourceBundleFor } from '@agent-device/replay-port/replay-script-source-fixtures'; import { makeIosSession } from '../support/session-factories.ts'; const { runReplayFixture, runReplayForTest, assertNoUnresolvedInterpolation } = diff --git a/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro-shell-artifacts.test.ts b/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro-shell-artifacts.test.ts index 62657b35a3..35f904e81a 100644 --- a/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro-shell-artifacts.test.ts +++ b/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro-shell-artifacts.test.ts @@ -5,7 +5,7 @@ import path from 'node:path'; import { test } from 'vitest'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; import { SessionStore } from '../../session-store.ts'; -import { replayScriptSourceBundleFor } from '../support/replay-script-source.ts'; +import { replayScriptSourceBundleFor } from '@agent-device/replay-port/replay-script-source-fixtures'; const { runReplayFixture, runReplayForTest } = maestroReplayFixture; diff --git a/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro.fixtures.ts b/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro.fixtures.ts index d3d6cf244a..c99e275462 100644 --- a/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro.fixtures.ts +++ b/src/daemon/__tests__/replay-maestro/session-replay-runtime-maestro.fixtures.ts @@ -51,7 +51,7 @@ import type { ReplayScriptSourceBundle } from '@agent-device/contracts/replay'; import { maestroScriptSourceBundleFor, replayScriptSourceBundleFor, -} from '../support/replay-script-source.ts'; +} from '@agent-device/replay-port/replay-script-source-fixtures'; import { captureSnapshotThroughLegacyDispatchFixture } from '../legacy-snapshot-capture-fixture.ts'; import { captureSnapshotWithInteractor } from '../../snapshot-interactor-capture.ts'; import { seedReplayFixtureSession } from '../replay-runtime/session-replay-runtime.fixtures.ts'; diff --git a/src/daemon/__tests__/replay-runtime/application.test.ts b/src/daemon/__tests__/replay-runtime/application.test.ts index a87f605558..33bd6be350 100644 --- a/src/daemon/__tests__/replay-runtime/application.test.ts +++ b/src/daemon/__tests__/replay-runtime/application.test.ts @@ -10,7 +10,7 @@ import { } from '@agent-device/host-kit/request'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; import { makeIosSession } from '../support/session-factories.ts'; -import { withClientReplayScriptSources } from '../support/replay-script-source.ts'; +import { withClientReplayScriptSources } from '@agent-device/replay-port/replay-script-source-fixtures'; import type { DaemonRequest, DaemonResponse } from '../../daemon-request.ts'; import { SessionStore } from '../../session-store.ts'; import { diff --git a/src/daemon/__tests__/replay-runtime/session-replay-runtime-failure.test.ts b/src/daemon/__tests__/replay-runtime/session-replay-runtime-failure.test.ts index a5547e6ec5..08aa5b5960 100644 --- a/src/daemon/__tests__/replay-runtime/session-replay-runtime-failure.test.ts +++ b/src/daemon/__tests__/replay-runtime/session-replay-runtime-failure.test.ts @@ -16,7 +16,7 @@ import { SessionStore } from '../../session-store.ts'; import type { DaemonResponse } from '../../daemon-request.ts'; import { makeIosSession } from '../support/session-factories.ts'; import { formatReplayDivergenceReport } from '@agent-device/ad-replay/divergence'; -import { maestroScriptSourceBundleFor } from '../support/replay-script-source.ts'; +import { maestroScriptSourceBundleFor } from '@agent-device/replay-port/replay-script-source-fixtures'; import { captureSnapshotThroughLegacyDispatchFixture, legacyDispatchCapture, diff --git a/src/daemon/__tests__/replay-runtime/session-replay-runtime.fixtures.ts b/src/daemon/__tests__/replay-runtime/session-replay-runtime.fixtures.ts index ba5361b210..016cb7a884 100644 --- a/src/daemon/__tests__/replay-runtime/session-replay-runtime.fixtures.ts +++ b/src/daemon/__tests__/replay-runtime/session-replay-runtime.fixtures.ts @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { makeAndroidSession, makeIosSession } from '../support/session-factories.ts'; -import { replayScriptSourceBundleFor } from '../support/replay-script-source.ts'; +import { replayScriptSourceBundleFor } from '@agent-device/replay-port/replay-script-source-fixtures'; import type { SessionStore } from '../../session-store.ts'; import type { DaemonRequest } from '../../daemon-request.ts'; diff --git a/src/daemon/__tests__/replay-runtime/session-replay-script-source.test.ts b/src/daemon/__tests__/replay-runtime/session-replay-script-source.test.ts index 0baef9cfa6..0521fb3dea 100644 --- a/src/daemon/__tests__/replay-runtime/session-replay-script-source.test.ts +++ b/src/daemon/__tests__/replay-runtime/session-replay-script-source.test.ts @@ -25,7 +25,7 @@ import { makeIosSession } from '../support/session-factories.ts'; import { maestroScriptSourceBundleFor, replayScriptSourceBundleFor, -} from '../support/replay-script-source.ts'; +} from '@agent-device/replay-port/replay-script-source-fixtures'; import { REPLAY_SCRIPT_SOURCE_REQUIRED_MESSAGE } from '@agent-device/replay-port/script-source'; import { captureSnapshotThroughLegacyDispatchFixture, diff --git a/src/daemon/__tests__/replay-suite/session-test-command-trust.test.ts b/src/daemon/__tests__/replay-suite/session-test-command-trust.test.ts index 0ce50f4a80..54432d01cc 100644 --- a/src/daemon/__tests__/replay-suite/session-test-command-trust.test.ts +++ b/src/daemon/__tests__/replay-suite/session-test-command-trust.test.ts @@ -12,7 +12,7 @@ import { } from '../../handlers/session-replay-command.ts'; import { runReplayTestCommand } from '@agent-device/replay-port/test-command'; import type { ReplayCommand, ReplayTestCommand } from '@agent-device/replay-port/command-types'; -import { replayScriptSourceBundleFor } from '../support/replay-script-source.ts'; +import { replayScriptSourceBundleFor } from '@agent-device/replay-port/replay-script-source-fixtures'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; import type { DaemonInvokeFn, DaemonRequest } from '../../daemon-request.ts'; diff --git a/src/daemon/__tests__/replay-suite/session-test-source-discovery.test.ts b/src/daemon/__tests__/replay-suite/session-test-source-discovery.test.ts index 3b2afb5505..8f95c8d4ee 100644 --- a/src/daemon/__tests__/replay-suite/session-test-source-discovery.test.ts +++ b/src/daemon/__tests__/replay-suite/session-test-source-discovery.test.ts @@ -7,7 +7,7 @@ import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; import { maestroScriptSourceBundleFor, replayScriptSourceBundleFor, -} from '../support/replay-script-source.ts'; +} from '@agent-device/replay-port/replay-script-source-fixtures'; // #1478 P3b: the daemon adapter's inspection capability. #1802 moved expansion and file reading // to the caller, so what is left here is exactly format routing plus per-engine manifest diff --git a/src/daemon/__tests__/replay-suite/session-test-suite-command-video.test.ts b/src/daemon/__tests__/replay-suite/session-test-suite-command-video.test.ts index 3fcacc2f64..7b202e88c4 100644 --- a/src/daemon/__tests__/replay-suite/session-test-suite-command-video.test.ts +++ b/src/daemon/__tests__/replay-suite/session-test-suite-command-video.test.ts @@ -13,7 +13,7 @@ import { splitReplayCommandRequest, } from '@agent-device/replay-port/replay-dispatch-envelope'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; -import { replayScriptSourceBundleFor } from '../support/replay-script-source.ts'; +import { replayScriptSourceBundleFor } from '@agent-device/replay-port/replay-script-source-fixtures'; import { unavailableBindDevice, unavailableBindExactDevice, diff --git a/src/daemon/__tests__/replay-suite/session-test-suite.test.ts b/src/daemon/__tests__/replay-suite/session-test-suite.test.ts index f68728fb5e..763620a45f 100644 --- a/src/daemon/__tests__/replay-suite/session-test-suite.test.ts +++ b/src/daemon/__tests__/replay-suite/session-test-suite.test.ts @@ -32,7 +32,7 @@ import { registerRequestAbort, } from '@agent-device/host-kit/request'; -import { withTestDeviceInventoryProvider as withDeviceInventoryProvider } from '../support/device-inventory-gateways.ts'; +import { withTestDeviceInventoryProvider as withDeviceInventoryProvider } from '@agent-device/device-selection/inventory-gateways-fixtures'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { makeAndroidSession, makeMacOsSession } from '../support/session-factories.ts'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; diff --git a/src/daemon/__tests__/request-handler-catalog.test.ts b/src/daemon/__tests__/request-handler-catalog.test.ts index 53e0c28cac..e38d8a1c99 100644 --- a/src/daemon/__tests__/request-handler-catalog.test.ts +++ b/src/daemon/__tests__/request-handler-catalog.test.ts @@ -3,7 +3,7 @@ import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import { test } from 'vitest'; -import { withTestDeviceInventoryProvider as withTargetDeviceResolutionScope } from './support/device-inventory-gateways.ts'; +import { withTestDeviceInventoryProvider as withTargetDeviceResolutionScope } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { INTERNAL_COMMANDS, PUBLIC_COMMANDS } from '@agent-device/command-registry/catalog'; import { commandDescriptors } from '@agent-device/command-registry/registry'; import { isRequestCanceledError, type AppError } from '@agent-device/kernel/errors'; diff --git a/src/daemon/__tests__/request-router-apps-admission.test.ts b/src/daemon/__tests__/request-router-apps-admission.test.ts index e2b53fccb4..f80c094e72 100644 --- a/src/daemon/__tests__/request-router-apps-admission.test.ts +++ b/src/daemon/__tests__/request-router-apps-admission.test.ts @@ -3,7 +3,7 @@ import { expect, test, vi } from 'vitest'; import path from 'node:path'; import type { DeviceRuntimeGateway } from '@agent-device/contracts/platform-runtime'; import type { PlatformRuntimeOperations } from '@agent-device/contracts/platform-runtime-operations'; -import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { makeSessionStore } from './support/store-factory.ts'; import { LeaseRegistry } from '../lease-registry.ts'; import type { DaemonRequest } from '../daemon-request.ts'; diff --git a/src/daemon/__tests__/request-router-artifacts-web.test.ts b/src/daemon/__tests__/request-router-artifacts-web.test.ts index 3ffb59668e..332d4e24e9 100644 --- a/src/daemon/__tests__/request-router-artifacts-web.test.ts +++ b/src/daemon/__tests__/request-router-artifacts-web.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { test, expect } from 'vitest'; import fs from 'node:fs'; diff --git a/src/daemon/__tests__/request-router-cost.test.ts b/src/daemon/__tests__/request-router-cost.test.ts index d4dace0607..e6acc3a174 100644 --- a/src/daemon/__tests__/request-router-cost.test.ts +++ b/src/daemon/__tests__/request-router-cost.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { test, expect, vi, beforeEach } from 'vitest'; import path from 'node:path'; diff --git a/src/daemon/__tests__/request-router-custom-action-flags.test.ts b/src/daemon/__tests__/request-router-custom-action-flags.test.ts index eee07bcd4a..ecf17314ac 100644 --- a/src/daemon/__tests__/request-router-custom-action-flags.test.ts +++ b/src/daemon/__tests__/request-router-custom-action-flags.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; /** * `--actions` reads custom actions through the private-AX snapshot path, which * a raw capture does not use. Answering the pair with a capture that diff --git a/src/daemon/__tests__/request-router-developer-dir.test.ts b/src/daemon/__tests__/request-router-developer-dir.test.ts index c6447ad5a9..4fd02d8867 100644 --- a/src/daemon/__tests__/request-router-developer-dir.test.ts +++ b/src/daemon/__tests__/request-router-developer-dir.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import path from 'node:path'; import { expect, test } from 'vitest'; import { runCmd } from '@agent-device/host-kit/command'; diff --git a/src/daemon/__tests__/request-router-error-redaction.test.ts b/src/daemon/__tests__/request-router-error-redaction.test.ts index f49ed9d033..e75cdcd043 100644 --- a/src/daemon/__tests__/request-router-error-redaction.test.ts +++ b/src/daemon/__tests__/request-router-error-redaction.test.ts @@ -18,7 +18,7 @@ vi.mock('../device/device-ready.ts', () => ({ ensureDeviceReady: vi.fn(async () import { AppError } from '@agent-device/kernel/errors'; import type { DeviceRuntimeGateway, RuntimeFacts } from '@agent-device/contracts/platform-runtime'; import type { PlatformRuntimeOperations } from '@agent-device/contracts/platform-runtime-operations'; -import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { makeIosAppSession } from './support/session-factories.ts'; import { makeSessionStore } from './support/store-factory.ts'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; diff --git a/src/daemon/__tests__/request-router-events.test.ts b/src/daemon/__tests__/request-router-events.test.ts index 84f816a1d7..82af3eab62 100644 --- a/src/daemon/__tests__/request-router-events.test.ts +++ b/src/daemon/__tests__/request-router-events.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { test, expect } from 'vitest'; import fs from 'node:fs'; diff --git a/src/daemon/__tests__/request-router-idle-expired.test.ts b/src/daemon/__tests__/request-router-idle-expired.test.ts index dfb67e1ae2..394336d6af 100644 --- a/src/daemon/__tests__/request-router-idle-expired.test.ts +++ b/src/daemon/__tests__/request-router-idle-expired.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; /** * #2833: when a command finds no session because this daemon already expired it for idleness, the * answer stays `SESSION_NOT_FOUND` but carries the typed reason, the window it missed, and the device diff --git a/src/daemon/__tests__/request-router-open-claim.test.ts b/src/daemon/__tests__/request-router-open-claim.test.ts index c6d011e490..65ddd3e71a 100644 --- a/src/daemon/__tests__/request-router-open-claim.test.ts +++ b/src/daemon/__tests__/request-router-open-claim.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { test, expect, vi, beforeEach } from 'vitest'; import fs from 'node:fs'; diff --git a/src/daemon/__tests__/request-router-open.test.ts b/src/daemon/__tests__/request-router-open.test.ts index 3bbce80a3b..d8f52d2688 100644 --- a/src/daemon/__tests__/request-router-open.test.ts +++ b/src/daemon/__tests__/request-router-open.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { daemonPlatformServicesFixture, daemonClaimRecoveryGatewayFixture, @@ -10,7 +10,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { getResolveTargetDeviceMock } from './request-router-dispatch-mocks.ts'; -import { replayScriptSourceBundleFor } from './support/replay-script-source.ts'; +import { replayScriptSourceBundleFor } from '@agent-device/replay-port/replay-script-source-fixtures'; import { fakeCreateCommandSurface } from './support/command-surface-fake.ts'; vi.mock('../device/device-ready.ts', () => ({ ensureDeviceReady: vi.fn(async () => {}) })); diff --git a/src/daemon/__tests__/request-router-record-flags.test.ts b/src/daemon/__tests__/request-router-record-flags.test.ts index 388fbf3850..b7502bae68 100644 --- a/src/daemon/__tests__/request-router-record-flags.test.ts +++ b/src/daemon/__tests__/request-router-record-flags.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; /** * #1271 stage 2 (ADR 0012 amendment): `--record` and `--no-record` express * opposite recording intents for the same action, so both together is diff --git a/src/daemon/__tests__/request-router-repair-expired.test.ts b/src/daemon/__tests__/request-router-repair-expired.test.ts index c96207d998..98b6d7f943 100644 --- a/src/daemon/__tests__/request-router-repair-expired.test.ts +++ b/src/daemon/__tests__/request-router-repair-expired.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; /** * ADR 0012 decision 6, R7 (C5a): when a repair session was reaped before it was * finalized, the request router rewrites the resulting `SESSION_NOT_FOUND` into diff --git a/src/daemon/__tests__/request-router-replay-env.test.ts b/src/daemon/__tests__/request-router-replay-env.test.ts index 277891664c..7d9e59e65c 100644 --- a/src/daemon/__tests__/request-router-replay-env.test.ts +++ b/src/daemon/__tests__/request-router-replay-env.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import fs from 'node:fs'; import path from 'node:path'; import { expect, test } from 'vitest'; @@ -6,7 +6,7 @@ import { makeSessionStore } from './support/store-factory.ts'; import { LeaseRegistry } from '../lease-registry.ts'; import { createRequestHandler } from './test-device-runtime-gateway.ts'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; -import { replayScriptSourceBundleFor } from './support/replay-script-source.ts'; +import { replayScriptSourceBundleFor } from '@agent-device/replay-port/replay-script-source-fixtures'; import { fakeCreateCommandSurface } from './support/command-surface-fake.ts'; function createHarness() { diff --git a/src/daemon/__tests__/request-router-response-level.test.ts b/src/daemon/__tests__/request-router-response-level.test.ts index 95e5c82ad5..344bad3d7b 100644 --- a/src/daemon/__tests__/request-router-response-level.test.ts +++ b/src/daemon/__tests__/request-router-response-level.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { test, expect, vi, beforeEach } from 'vitest'; import path from 'node:path'; diff --git a/src/daemon/__tests__/request-router-session-address.test.ts b/src/daemon/__tests__/request-router-session-address.test.ts index b39b710f60..3e0dd2d3d7 100644 --- a/src/daemon/__tests__/request-router-session-address.test.ts +++ b/src/daemon/__tests__/request-router-session-address.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { legacyDispatchCapture } from './legacy-snapshot-capture-fixture.ts'; import { test, expect, vi, beforeEach } from 'vitest'; diff --git a/src/daemon/__tests__/request-router-typed-error.test.ts b/src/daemon/__tests__/request-router-typed-error.test.ts index 02dd7655a3..3f364c27a0 100644 --- a/src/daemon/__tests__/request-router-typed-error.test.ts +++ b/src/daemon/__tests__/request-router-typed-error.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { legacyDispatchCapture } from './legacy-snapshot-capture-fixture.ts'; import { test, expect, vi, beforeEach } from 'vitest'; import fs from 'node:fs'; diff --git a/src/daemon/__tests__/request-runtime-binding-router.test.ts b/src/daemon/__tests__/request-runtime-binding-router.test.ts index 911492bf8d..25d1be7565 100644 --- a/src/daemon/__tests__/request-runtime-binding-router.test.ts +++ b/src/daemon/__tests__/request-runtime-binding-router.test.ts @@ -9,7 +9,7 @@ import { import type { PlatformRuntimeOperations } from '@agent-device/contracts/platform-runtime-operations'; import { createAppLogStartResult, createDurableResourceEnvelope } from '@agent-device/capture-kit'; import { createTestAppLogLiveHandle } from '@agent-device/test-kit/app-log-live-handle'; -import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { unavailableDeploymentSnapshotAndShutdownOperationFacts } from '@agent-device/test-kit/runtime-operation-facts'; import { makeSessionStore } from './support/store-factory.ts'; import { LeaseRegistry } from '../lease-registry.ts'; diff --git a/src/daemon/__tests__/request-save-script-transports.test.ts b/src/daemon/__tests__/request-save-script-transports.test.ts index 80a04e3414..c157f0c050 100644 --- a/src/daemon/__tests__/request-save-script-transports.test.ts +++ b/src/daemon/__tests__/request-save-script-transports.test.ts @@ -1,6 +1,6 @@ import { storeSessionForTest } from './support/store-factory.ts'; import { isSessionRecording } from '../session-script-publication-capability.ts'; -import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; /** * #1478 (P4-pre): raw-wire counterfactuals for the `flags.saveScript` seam. * diff --git a/src/daemon/__tests__/selector-capture-binding.test.ts b/src/daemon/__tests__/selector-capture-binding.test.ts index 2bf5361a32..f38b1a7101 100644 --- a/src/daemon/__tests__/selector-capture-binding.test.ts +++ b/src/daemon/__tests__/selector-capture-binding.test.ts @@ -3,7 +3,7 @@ import { expect, test } from 'vitest'; import { ANDROID_EMULATOR, IOS_SIMULATOR } from '@agent-device/test-kit/device-fixtures'; import { makeAndroidSession, makeIosSession } from './support/session-factories.ts'; import { makeSessionStore } from './support/store-factory.ts'; -import { withTestDeviceInventory } from './support/device-inventory-gateways.ts'; +import { withTestDeviceInventory } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { resolveBoundSelectorCapture } from '../selector-capture-binding.ts'; import { createBoundSelectorRuntime } from '../selector-runtime-backend.ts'; import { selectorCaptureFixture } from './selector-capture-fixture.ts'; diff --git a/src/daemon/__tests__/snapshot-custom-actions-platform-guard.test.ts b/src/daemon/__tests__/snapshot-custom-actions-platform-guard.test.ts index f8d494c242..addc1883ac 100644 --- a/src/daemon/__tests__/snapshot-custom-actions-platform-guard.test.ts +++ b/src/daemon/__tests__/snapshot-custom-actions-platform-guard.test.ts @@ -1,4 +1,4 @@ -import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; /** * Custom actions are read through the private-AX snapshot backend, which only * the iOS simulator has. Every other target would accept `--actions`, take an diff --git a/src/daemon/__tests__/support/replay-script-source.ts b/src/daemon/__tests__/support/replay-script-source.ts deleted file mode 100644 index 57c5ed2853..0000000000 --- a/src/daemon/__tests__/support/replay-script-source.ts +++ /dev/null @@ -1,152 +0,0 @@ -// Daemon-side stand-in for the client's replay source collection (#1802). The root -// `src/__tests__/test-utils/replay-script-source.ts` runs the real client-side collector -// (`src/commands/replay/*`); daemon policy tests only need the SHAPE of the bundles a real -// request carries, so this builds them from the package-owned format resolution and -// Maestro include engine without reaching into the root command layer. Suite expansion -// stands in with a deterministic walk; the daemon never expands inputs itself (#1802). -import fs from 'node:fs'; -import path from 'node:path'; -import type { ReplayScriptSourceBundle } from '@agent-device/contracts/replay'; -import type { MaestroSourceReader } from '@agent-device/maestro'; -import { resolveUserPath } from '@agent-device/host-kit/file'; -import { resolveReplayFormat } from '@agent-device/ad-script'; -import type { DaemonRequest } from '../../daemon-request.ts'; - -/** A native `.ad` script's one-entry source bundle for test requests. */ -export function replayScriptSourceBundleFor(filePath: string): ReplayScriptSourceBundle { - const entry = resolveUserPath(filePath, { cwd: process.cwd() }); - return { entry, files: { [entry]: fs.readFileSync(entry, 'utf8') } }; -} - -/** A Maestro flow's bundle — async because the engine that walks its includes loads on demand. */ -export async function maestroScriptSourceBundleFor( - filePath: string, -): Promise { - return await collectReplayScriptSourceBundle({ - inputPath: filePath, - cwd: process.cwd(), - replayBackend: 'maestro', - }); -} - -/** - * `readSource` for a flow that declares no `runFlow` includes: the engine must - * never reach for one, so a call here means the fixture drifted rather than a - * missing file. - */ -export const noMaestroIncludeSources: MaestroSourceReader = (resolvedPath) => { - throw new Error(`unexpected Maestro include read: ${resolvedPath}`); -}; - -/** - * Attaches the script sources a real client would have collected for a `replay`/`test` request - * (#1802): the daemon opens no caller path, so every request that reaches one carries them. - * Test harnesses that stand in for "a request as it arrives at the daemon" call this so their - * cases keep expressing what they are about; a request that already carries its own sources (or - * deliberately carries none, to pin the daemon's refusal) is returned untouched. - * - * Collection that cannot succeed — a case driving `replay` at a path that deliberately does not - * exist — leaves the request alone rather than throwing: the real client raises that failure at - * the CLI boundary (pinned in `cli-client-commands.test.ts`), and a daemon-level harness must - * still deliver the request so the daemon's own response is what the case observes. - */ -export async function withClientReplayScriptSources(req: DaemonRequest): Promise { - if (req.command !== 'replay' && req.command !== 'test') return req; - const inputs = req.positionals ?? []; - const entryPath = inputs[0]; - if (entryPath === undefined) return req; - const cwd = req.meta?.cwd ?? process.cwd(); - const replayBackend = req.flags?.replayBackend; - try { - if (req.command === 'replay') { - if (req.flags?.replayScriptSource) return req; - return withFlags(req, { - replayScriptSource: await collectReplayScriptSourceBundle({ - inputPath: entryPath, - cwd, - replayBackend, - }), - }); - } - if (req.flags?.replayScriptSources) return req; - return withFlags(req, { - replayScriptSources: await Promise.all( - discoverReplaySourcePaths({ inputs, cwd, replayBackend }).map( - async (inputPath) => - await collectReplayScriptSourceBundle({ inputPath, cwd, replayBackend }), - ), - ), - }); - } catch { - return req; - } -} - -function withFlags(req: DaemonRequest, flags: Partial): DaemonRequest { - return { ...req, flags: { ...(req.flags ?? {}), ...flags } }; -} - -async function collectReplayScriptSourceBundle(params: { - inputPath: string; - cwd: string; - replayBackend?: string; -}): Promise { - const entry = resolveUserPath(params.inputPath, { cwd: params.cwd }); - const entrySource = fs.readFileSync(entry, 'utf8'); - const backendId = resolveReplayFormat(entry, params.replayBackend); - if (backendId === 'ad') return { entry, files: { [entry]: entrySource } }; - // Same lazy-load shape as the client collector: the Maestro engine (and its include walker) - // sits behind the package backend registry, never behind root command code. - const backend = await ( - await import('@agent-device/replay-port/replay-backend-registry') - ).getReplayBackend(backendId); - return { - entry, - files: backend.collectSourceFiles({ - entryPath: entry, - entrySource, - readSource: (resolvedPath: string) => { - try { - return fs.readFileSync(resolvedPath, 'utf8'); - } catch { - return undefined; - } - }, - }), - }; -} - -function discoverReplaySourcePaths(params: { - inputs: string[]; - cwd: string; - replayBackend?: string; -}): string[] { - const extensions = - params.replayBackend === 'maestro' ? new Set(['.yaml', '.yml', '.ad']) : new Set(['.ad']); - const paths: string[] = []; - for (const input of params.inputs) { - const resolved = resolveUserPath(input, { cwd: params.cwd }); - if (!fs.existsSync(resolved)) { - throw new Error(`test input not found: ${input}`); - } - if (fs.statSync(resolved).isDirectory()) { - paths.push(...listReplayFiles(resolved, extensions)); - } else if (extensions.has(path.extname(resolved).toLowerCase())) { - paths.push(path.normalize(resolved)); - } else { - throw new Error(`test does not support this file type: ${input}`); - } - } - return [...new Set(paths)]; -} - -function listReplayFiles(directory: string, extensions: ReadonlySet): string[] { - const found: string[] = []; - for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { - const filePath = path.join(directory, entry.name); - if (entry.isDirectory()) found.push(...listReplayFiles(filePath, extensions)); - else if (entry.isFile() && extensions.has(path.extname(entry.name).toLowerCase())) - found.push(filePath); - } - return found; -} diff --git a/src/daemon/__tests__/system-button-runtime.test.ts b/src/daemon/__tests__/system-button-runtime.test.ts index d7a2434890..7ea81ecb8d 100644 --- a/src/daemon/__tests__/system-button-runtime.test.ts +++ b/src/daemon/__tests__/system-button-runtime.test.ts @@ -19,7 +19,7 @@ import type { SystemButton } from '@agent-device/contracts/system-button-runtime import { deviceShape, type DeviceInfo } from '@agent-device/kernel/device'; import { makeSession } from './support/session-factories.ts'; import { makeSessionStore } from './support/store-factory.ts'; -import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { LeaseRegistry } from '../lease-registry.ts'; import { activateCompleteRefFrame, refFrameState } from '../ref-frame.ts'; import type { BindDeviceRuntime, InspectDeviceRuntimeFacts } from '../request-runtime-binding.ts'; diff --git a/src/daemon/__tests__/test-device-runtime-gateway.ts b/src/daemon/__tests__/test-device-runtime-gateway.ts index 0bf9382930..a51d2fec58 100644 --- a/src/daemon/__tests__/test-device-runtime-gateway.ts +++ b/src/daemon/__tests__/test-device-runtime-gateway.ts @@ -22,7 +22,7 @@ import type { BindDeviceRuntime, BindExactDeviceRuntime } from '../request-runti import type { DeviceInfo } from '@agent-device/kernel/device'; import { applicationLifecycleRuntimeFixture } from './application-lifecycle-runtime-fixture.ts'; import { createUnavailableRuntimeFactsForTest } from '@agent-device/test-kit/runtime-operation-facts'; -import { withClientReplayScriptSources } from './support/replay-script-source.ts'; +import { withClientReplayScriptSources } from '@agent-device/replay-port/replay-script-source-fixtures'; import type { DaemonInvokeFn } from '../daemon-request.ts'; import { clearAndroidObservationFixture } from './android-observation-fixture.ts'; import { daemonPlatformServicesFixture } from './platform-services-fixture.ts'; diff --git a/src/daemon/__tests__/tv-remote-runtime.test.ts b/src/daemon/__tests__/tv-remote-runtime.test.ts index 2cd7f431bb..2fd82435cc 100644 --- a/src/daemon/__tests__/tv-remote-runtime.test.ts +++ b/src/daemon/__tests__/tv-remote-runtime.test.ts @@ -16,7 +16,7 @@ import { tvRemoteRuntimeOperationFacts } from '@agent-device/contracts/tv-remote import { deviceShape } from '@agent-device/kernel/device'; import { makeSession } from './support/session-factories.ts'; import { makeSessionStore } from './support/store-factory.ts'; -import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { LeaseRegistry } from '../lease-registry.ts'; import { activateCompleteRefFrame, refFrameState } from '../ref-frame.ts'; import type { BindDeviceRuntime, InspectDeviceRuntimeFacts } from '../request-runtime-binding.ts'; diff --git a/src/daemon/__tests__/viewport-runtime.test.ts b/src/daemon/__tests__/viewport-runtime.test.ts index bebecea33b..4e74cd6a2f 100644 --- a/src/daemon/__tests__/viewport-runtime.test.ts +++ b/src/daemon/__tests__/viewport-runtime.test.ts @@ -16,7 +16,7 @@ import { viewportRuntimeOperationFacts } from '@agent-device/contracts/viewport- import { deviceShape } from '@agent-device/kernel/device'; import { makeSession } from './support/session-factories.ts'; import { makeSessionStore } from './support/store-factory.ts'; -import { createTestDeviceInventoryGateways } from './support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { LeaseRegistry } from '../lease-registry.ts'; import { activateCompleteRefFrame, refFrameState } from '../ref-frame.ts'; import type { BindDeviceRuntime, InspectDeviceRuntimeFacts } from '../request-runtime-binding.ts'; diff --git a/src/daemon/handlers/__tests__/session-appstate-device-runtime.test.ts b/src/daemon/handlers/__tests__/session-appstate-device-runtime.test.ts index f32d27aa9d..e5413faee7 100644 --- a/src/daemon/handlers/__tests__/session-appstate-device-runtime.test.ts +++ b/src/daemon/handlers/__tests__/session-appstate-device-runtime.test.ts @@ -3,7 +3,7 @@ import { test, expect, vi } from 'vitest'; import { handleAppStateCommand } from '../session-appstate.ts'; import { makeSessionStore } from '../../__tests__/support/store-factory.ts'; -import { withTestDeviceInventory } from '../../__tests__/support/device-inventory-gateways.ts'; +import { withTestDeviceInventory } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { unavailableDeploymentSnapshotAndShutdownOperationFacts } from '@agent-device/test-kit/runtime-operation-facts'; import { applicationLifecycleOperationFacts } from '@agent-device/contracts/application-lifecycle-runtime'; import { diff --git a/src/daemon/handlers/__tests__/session-command-harness.ts b/src/daemon/handlers/__tests__/session-command-harness.ts index 6f9276522b..112d3f7119 100644 --- a/src/daemon/handlers/__tests__/session-command-harness.ts +++ b/src/daemon/handlers/__tests__/session-command-harness.ts @@ -23,7 +23,7 @@ import type { TargetShutdownResult } from '@agent-device/contracts/device'; import { isIosFamily, type DeviceInfo } from '@agent-device/kernel/device'; import { beforeEach, vi } from 'vitest'; import { applicationLifecycleRuntimeFixture } from '../../__tests__/application-lifecycle-runtime-fixture.ts'; -import { withClientReplayScriptSources } from '../../__tests__/support/replay-script-source.ts'; +import { withClientReplayScriptSources } from '@agent-device/replay-port/replay-script-source-fixtures'; import { createUnavailableRuntimeFactsForTest } from '@agent-device/test-kit/runtime-operation-facts'; import { platformResourceCleanupFixture } from '../../__tests__/support/platform-resource-cleanup-fixture.ts'; diff --git a/src/daemon/handlers/__tests__/session-command-replay.test.ts b/src/daemon/handlers/__tests__/session-command-replay.test.ts index c75f3a2b4f..00a783d3a9 100644 --- a/src/daemon/handlers/__tests__/session-command-replay.test.ts +++ b/src/daemon/handlers/__tests__/session-command-replay.test.ts @@ -6,7 +6,7 @@ import { makeSessionStore } from './session-test-harness.ts'; import { handleSessionCommands } from './session-command-harness.ts'; import type { DaemonRequest } from '../../daemon-request.ts'; -import { replayScriptSourceBundleFor } from '../../__tests__/support/replay-script-source.ts'; +import { replayScriptSourceBundleFor } from '@agent-device/replay-port/replay-script-source-fixtures'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; import { fakeCreateCommandSurface } from '../../__tests__/support/command-surface-fake.ts'; diff --git a/src/daemon/handlers/__tests__/session-doctor-device.test.ts b/src/daemon/handlers/__tests__/session-doctor-device.test.ts index 62824a39ad..e113a25f84 100644 --- a/src/daemon/handlers/__tests__/session-doctor-device.test.ts +++ b/src/daemon/handlers/__tests__/session-doctor-device.test.ts @@ -3,7 +3,7 @@ import { test } from 'vitest'; import { withTestDeviceInventory, withTestDeviceInventoryProvider as withDeviceInventoryProvider, -} from '../../__tests__/support/device-inventory-gateways.ts'; +} from '@agent-device/device-selection/inventory-gateways-fixtures'; import { AppError } from '@agent-device/kernel/errors'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { attachAdbFailureHint } from '@agent-device/platform-android/mechanics'; diff --git a/src/daemon/handlers/__tests__/session-doctor-warmup.test.ts b/src/daemon/handlers/__tests__/session-doctor-warmup.test.ts index 686792159f..c584f3b333 100644 --- a/src/daemon/handlers/__tests__/session-doctor-warmup.test.ts +++ b/src/daemon/handlers/__tests__/session-doctor-warmup.test.ts @@ -4,7 +4,7 @@ import { isActiveProviderDevice } from '../../provider-device-admission.ts'; import { handleDoctorCommand } from '../session-doctor.ts'; import { createHostDiagnostics } from '../../../platform-runtime-host-diagnostics.ts'; import { makeSessionStore } from '../../__tests__/support/store-factory.ts'; -import { withTestDeviceInventory } from '../../__tests__/support/device-inventory-gateways.ts'; +import { withTestDeviceInventory } from '@agent-device/device-selection/inventory-gateways-fixtures'; import type { DaemonResponse } from '../../daemon-request.ts'; const { mockAppleRunnerWarmupCheck } = vi.hoisted(() => ({ diff --git a/src/daemon/handlers/__tests__/session-relaunch-close.test.ts b/src/daemon/handlers/__tests__/session-relaunch-close.test.ts index eccbf7c495..9c624929f1 100644 --- a/src/daemon/handlers/__tests__/session-relaunch-close.test.ts +++ b/src/daemon/handlers/__tests__/session-relaunch-close.test.ts @@ -7,7 +7,7 @@ import { fakeCreateCommandSurface } from '../../__tests__/support/command-surfac // The daemon reads provider ownership through its own typed admission seam; these suites // install the seam over runtime fakes, the way root composition installs the real one. const providerAdmission = providerDeviceAdmissionFixture(); -import { createTestDeviceInventoryGateways } from '../../__tests__/support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { makeSessionStore } from '../../__tests__/support/store-factory.ts'; import { makeSession } from '../../__tests__/support/session-factories.ts'; import { getResolveTargetDeviceMock } from '../../__tests__/request-router-dispatch-mocks.ts'; diff --git a/src/daemon/session-lifecycle/internal/__tests__/session-capabilities.test.ts b/src/daemon/session-lifecycle/internal/__tests__/session-capabilities.test.ts index 55cdf4056e..509a78a4ed 100644 --- a/src/daemon/session-lifecycle/internal/__tests__/session-capabilities.test.ts +++ b/src/daemon/session-lifecycle/internal/__tests__/session-capabilities.test.ts @@ -6,7 +6,7 @@ import { PUBLIC_COMMANDS } from '@agent-device/command-registry/catalog'; import { LINUX_DEVICE, WEB_DESKTOP_DEVICE } from '@agent-device/test-kit/device-fixtures'; import { makeAndroidSession, makeSession } from '../../../__tests__/support/session-factories.ts'; import { makeSessionStore } from '../../../__tests__/support/store-factory.ts'; -import { withTestDeviceInventoryProvider as withTargetDeviceResolutionScope } from '../../../__tests__/support/device-inventory-gateways.ts'; +import { withTestDeviceInventoryProvider as withTargetDeviceResolutionScope } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { unavailableDeploymentSnapshotAndShutdownOperationFacts } from '@agent-device/test-kit/runtime-operation-facts'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { applicationLifecycleOperationFacts } from '@agent-device/contracts/application-lifecycle-runtime'; diff --git a/src/daemon/session-lifecycle/internal/__tests__/session-devices-batch-runtime.test.ts b/src/daemon/session-lifecycle/internal/__tests__/session-devices-batch-runtime.test.ts index 390574aec1..1766eefa55 100644 --- a/src/daemon/session-lifecycle/internal/__tests__/session-devices-batch-runtime.test.ts +++ b/src/daemon/session-lifecycle/internal/__tests__/session-devices-batch-runtime.test.ts @@ -21,7 +21,7 @@ import { makeSessionStore } from '../../../__tests__/support/store-factory.ts'; import { makeSession } from '../../../__tests__/support/session-factories.ts'; import type { DaemonRequest, DaemonResponse } from '../../../daemon-request.ts'; -import { withTestDeviceInventory } from '../../../__tests__/support/device-inventory-gateways.ts'; +import { withTestDeviceInventory } from '@agent-device/device-selection/inventory-gateways-fixtures'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { readCurrentOwnerIdentity } from '@agent-device/host-kit/process'; import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; diff --git a/src/daemon/session-lifecycle/internal/__tests__/session-open-url-prewarm.test.ts b/src/daemon/session-lifecycle/internal/__tests__/session-open-url-prewarm.test.ts index 61940bdf69..ecde5dd772 100644 --- a/src/daemon/session-lifecycle/internal/__tests__/session-open-url-prewarm.test.ts +++ b/src/daemon/session-lifecycle/internal/__tests__/session-open-url-prewarm.test.ts @@ -49,7 +49,7 @@ import { } from '../../../__tests__/test-device-runtime-gateway.ts'; import { dispatchApplicationLifecycleEffect } from '../../../__tests__/application-lifecycle-runtime-fixture.ts'; import { getResolveTargetDeviceMock } from '../../../__tests__/request-router-dispatch-mocks.ts'; -import { createTestDeviceInventoryGateways } from '../../../__tests__/support/device-inventory-gateways.ts'; +import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { makeSessionStore } from '../../../__tests__/support/store-factory.ts'; import { makeSession } from '../../../__tests__/support/session-factories.ts'; import { LeaseRegistry } from '../../../lease-registry.ts'; diff --git a/src/platform-runtime-gateway.test.ts b/src/platform-runtime-gateway.test.ts index e3d6d83a47..96b1739359 100644 --- a/src/platform-runtime-gateway.test.ts +++ b/src/platform-runtime-gateway.test.ts @@ -20,7 +20,7 @@ import { handleAppStateCommand } from './daemon/handlers/session-appstate.ts'; import { createRequestRuntimeBindings } from './daemon/request-runtime-binding.ts'; import { createRequestDispatchLedger } from './daemon/request-dispatch-ledger.ts'; import { makeSessionStore } from './daemon/__tests__/support/store-factory.ts'; -import { withTestDeviceInventory } from './__tests__/test-utils/device-inventory-gateways.ts'; +import { withTestDeviceInventory } from '@agent-device/device-selection/inventory-gateways-fixtures'; import { createComposedPlatformRuntimeGateway } from './platform-runtime-gateway.ts'; import { gatewayFixture as gateway, diff --git a/test/integration/nightly/concurrency-torture/bindings.ts b/test/integration/nightly/concurrency-torture/bindings.ts index 267aac83c9..319815c18a 100644 --- a/test/integration/nightly/concurrency-torture/bindings.ts +++ b/test/integration/nightly/concurrency-torture/bindings.ts @@ -26,7 +26,7 @@ import type { SessionStore } from '../../../../src/daemon/session-store.ts'; import { resolveRequestExecutionLockPlan } from '../../../../src/daemon/request-binding.ts'; import { shouldLockSessionExecution } from '../../../../src/daemon/daemon-command-registry.ts'; import { PUBLIC_COMMANDS } from '@agent-device/command-registry/catalog'; -import { withTestDeviceInventoryProvider as withDeviceInventoryProvider } from '../../../../src/__tests__/test-utils/device-inventory-gateways.ts'; +import { withTestDeviceInventoryProvider as withDeviceInventoryProvider } from '@agent-device/device-selection/inventory-gateways-fixtures'; import type { LockKey } from './deterministic-scheduler.ts'; diff --git a/test/integration/nightly/concurrency-torture/real-scope-serialization.ts b/test/integration/nightly/concurrency-torture/real-scope-serialization.ts index af33d3da68..567a50a7ce 100644 --- a/test/integration/nightly/concurrency-torture/real-scope-serialization.ts +++ b/test/integration/nightly/concurrency-torture/real-scope-serialization.ts @@ -22,7 +22,7 @@ import path from 'node:path'; import { createRequestExecutionScope } from '../../../../src/daemon/request-execution-scope.ts'; import { LeaseRegistry } from '../../../../src/daemon/lease-registry.ts'; import { SessionStore } from '../../../../src/daemon/session-store.ts'; -import { withTestDeviceInventoryProvider as withDeviceInventoryProvider } from '../../../../src/__tests__/test-utils/device-inventory-gateways.ts'; +import { withTestDeviceInventoryProvider as withDeviceInventoryProvider } from '@agent-device/device-selection/inventory-gateways-fixtures'; import type { CommandFlags } from '@agent-device/contracts/command'; import type { DaemonRequest } from '../../../../src/daemon/daemon-request.ts'; diff --git a/test/integration/provider-scenarios/harness.ts b/test/integration/provider-scenarios/harness.ts index c3a46626d1..f2803bf4a9 100644 --- a/test/integration/provider-scenarios/harness.ts +++ b/test/integration/provider-scenarios/harness.ts @@ -32,7 +32,7 @@ import type { import type { SessionState } from '../../../src/daemon/session-state.ts'; import { runCmdBackground } from '@agent-device/host-kit/command'; import { createOwnedProcessRecordStore } from '@agent-device/host-kit/process'; -import { withClientReplayScriptSources } from '../../../src/__tests__/test-utils/replay-script-source.ts'; +import { withClientReplayScriptSources } from '@agent-device/replay-port/replay-script-source-fixtures'; import type { DeviceInventoryProvider, ProviderDeviceRuntime, @@ -41,7 +41,7 @@ import type { import { createTestDeviceInventoryGateways, createTestDeviceInventoryGatewaysFromProvider, -} from '../../../src/__tests__/test-utils/device-inventory-gateways.ts'; +} from '@agent-device/device-selection/inventory-gateways-fixtures'; import { createHostDiagnostics } from '../../../src/platform-runtime-host-diagnostics.ts'; import type { PlatformRuntimeProviderRegistration } from '../../../src/platform-runtime-gateway.ts'; import { createProviderPlatformRuntimeRegistrations } from '../../../src/provider-device-runtimes.ts'; From 8faacca32f1b8e14fa89c69db902e1557b214b77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 12 Oct 2026 21:00:23 +0200 Subject: [PATCH 04/14] refactor(daemon): share the Android capture fixtures through platform-android test entries --- packages/platform-android/package.json | 2 +- .../src/{ => fixtures}/android-ime-capture.raw.json | 0 .../{ => fixtures}/android-qs-shade-capture.raw.json | 0 .../platform-android/src/snapshot-capture.fixtures.ts | 5 +---- packages/platform-android/src/ui-hierarchy.fixtures.ts | 6 +++--- .../daemon-port/__tests__/script-source-bundle.test.ts | 2 +- .../src/daemon-port/__tests__/source-discovery.test.ts | 2 +- .../src/daemon-port/replay-script-source.fixtures.ts | 3 +-- scripts/check-affected/device-lanes.test.ts | 2 +- scripts/layering/platform-package-policy.test.ts | 2 ++ scripts/layering/platform-package-policy.ts | 10 +++++++++- 11 files changed, 20 insertions(+), 14 deletions(-) rename packages/platform-android/src/{ => fixtures}/android-ime-capture.raw.json (100%) rename packages/platform-android/src/{ => fixtures}/android-qs-shade-capture.raw.json (100%) diff --git a/packages/platform-android/package.json b/packages/platform-android/package.json index aed398ea9a..408fef48cc 100644 --- a/packages/platform-android/package.json +++ b/packages/platform-android/package.json @@ -38,7 +38,7 @@ }, "./ui-hierarchy-fixtures": { "types": "./src/ui-hierarchy.fixtures.ts", - "default": "./src/ui-hierarchy-fixtures.ts" + "default": "./src/ui-hierarchy.fixtures.ts" } } } diff --git a/packages/platform-android/src/android-ime-capture.raw.json b/packages/platform-android/src/fixtures/android-ime-capture.raw.json similarity index 100% rename from packages/platform-android/src/android-ime-capture.raw.json rename to packages/platform-android/src/fixtures/android-ime-capture.raw.json diff --git a/packages/platform-android/src/android-qs-shade-capture.raw.json b/packages/platform-android/src/fixtures/android-qs-shade-capture.raw.json similarity index 100% rename from packages/platform-android/src/android-qs-shade-capture.raw.json rename to packages/platform-android/src/fixtures/android-qs-shade-capture.raw.json diff --git a/packages/platform-android/src/snapshot-capture.fixtures.ts b/packages/platform-android/src/snapshot-capture.fixtures.ts index fc2e91c795..288d437d36 100644 --- a/packages/platform-android/src/snapshot-capture.fixtures.ts +++ b/packages/platform-android/src/snapshot-capture.fixtures.ts @@ -3,10 +3,7 @@ import type { SnapshotOcclusionContextEvidence, } from '@agent-device/contracts/capture'; import type { RawSnapshotNode } from '@agent-device/kernel/snapshot'; -import { - createAndroidSnapshotCapture, - type AndroidSnapshotCapture, -} from '@agent-device/platform-android/mechanics'; +import { createAndroidSnapshotCapture, type AndroidSnapshotCapture } from './mechanics.ts'; export function makeAndroidSnapshotCapture( nodes: RawSnapshotNode[], diff --git a/packages/platform-android/src/ui-hierarchy.fixtures.ts b/packages/platform-android/src/ui-hierarchy.fixtures.ts index 1ddb747243..f483518d74 100644 --- a/packages/platform-android/src/ui-hierarchy.fixtures.ts +++ b/packages/platform-android/src/ui-hierarchy.fixtures.ts @@ -1,8 +1,8 @@ // Real archived device trees, regenerated from a device and never hand-edited (a // hand-written id is how a fixture stops proving anything). Static import, not an // `fs` read: it typechecks each tree against `RawSnapshotNode[]`. -import imeCapture from './android-ime-capture.raw.json' with { type: 'json' }; -import qsShadeCapture from './android-qs-shade-capture.raw.json' with { type: 'json' }; +import imeCapture from './fixtures/android-ime-capture.raw.json' with { type: 'json' }; +import qsShadeCapture from './fixtures/android-qs-shade-capture.raw.json' with { type: 'json' }; import { buildUiHierarchySnapshot, parseUiHierarchyTree, @@ -10,7 +10,7 @@ import { buildAndroidSnapshotClickabilityEvidence, type AndroidUiHierarchy, type AndroidUiHierarchySnapshotOptions, -} from '@agent-device/platform-android/mechanics'; +} from './mechanics.ts'; import type { RawSnapshotNode } from '@agent-device/kernel/snapshot'; /** diff --git a/packages/replay-port/src/daemon-port/__tests__/script-source-bundle.test.ts b/packages/replay-port/src/daemon-port/__tests__/script-source-bundle.test.ts index 00caed12ee..8f7597437f 100644 --- a/packages/replay-port/src/daemon-port/__tests__/script-source-bundle.test.ts +++ b/packages/replay-port/src/daemon-port/__tests__/script-source-bundle.test.ts @@ -5,7 +5,7 @@ import { loadReplayScriptSourceBundle, REPLAY_SCRIPT_SOURCE_BUNDLE_MAX_BYTES, } from '@agent-device/replay-port/script-source-bundle'; -import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; +import { mkdtempForTestSync } from '../../tmp-dir.fixtures.ts'; // #1802: the caller reads every script file a replay run needs and sends the text. These pin the // collection itself; that the daemon consumes only the result is pinned beside the replay handler. diff --git a/packages/replay-port/src/daemon-port/__tests__/source-discovery.test.ts b/packages/replay-port/src/daemon-port/__tests__/source-discovery.test.ts index 310beb74cf..12e5615180 100644 --- a/packages/replay-port/src/daemon-port/__tests__/source-discovery.test.ts +++ b/packages/replay-port/src/daemon-port/__tests__/source-discovery.test.ts @@ -6,7 +6,7 @@ import os from 'node:os'; import path from 'node:path'; import { AppError } from '@agent-device/kernel/errors'; import { discoverReplaySourcePaths } from '@agent-device/replay-port/source-discovery'; -import { mkdtempForTestSync } from '@agent-device/test-kit/tmp-dir'; +import { mkdtempForTestSync } from '../../tmp-dir.fixtures.ts'; // #1802: path expansion, traversal ordering and file-type routing are CALLER work — `test` // inputs name files on the machine that typed the command. These pin the traversal itself; diff --git a/packages/replay-port/src/daemon-port/replay-script-source.fixtures.ts b/packages/replay-port/src/daemon-port/replay-script-source.fixtures.ts index 660cbf9754..d10529d1c2 100644 --- a/packages/replay-port/src/daemon-port/replay-script-source.fixtures.ts +++ b/packages/replay-port/src/daemon-port/replay-script-source.fixtures.ts @@ -1,5 +1,4 @@ -// The single owner of the client-collected replay source bundles test requests carry (#1802): -// the same collector the real client runs, imported through this package's own subpaths. +// The single owner of the client-collected replay source bundles test requests carry (#1802). import fs from 'node:fs'; import type { ReplayScriptSourceBundle } from '@agent-device/contracts/replay'; import type { DaemonWireRequest } from '@agent-device/contracts/command'; diff --git a/scripts/check-affected/device-lanes.test.ts b/scripts/check-affected/device-lanes.test.ts index 25f98fce63..a49b1d9763 100644 --- a/scripts/check-affected/device-lanes.test.ts +++ b/scripts/check-affected/device-lanes.test.ts @@ -137,7 +137,7 @@ test('unit tests under src/ and packages/*/src own no lane', () => { assert.deepEqual(lanes(file), [], file); } // Non-TypeScript under __tests__ is a fixture the selector cannot place, not a unit test. - assert.ok(!isUnitTest('src/daemon/__tests__/support/android-ime-capture.raw.json')); + assert.ok(!isUnitTest('packages/platform-android/src/android-ime-capture.raw.json')); // Package tests outside src/ have no owner and keep failing open. assert.ok(!isUnitTest('packages/maestro/test/conformance/verify.test.ts')); assert.equal( diff --git a/scripts/layering/platform-package-policy.test.ts b/scripts/layering/platform-package-policy.test.ts index 79a1436771..d689f6cb34 100644 --- a/scripts/layering/platform-package-policy.test.ts +++ b/scripts/layering/platform-package-policy.test.ts @@ -54,6 +54,8 @@ function declarations(): PlatformPackageDeclaration[] { '@agent-device/platform-android/adb-host', '@agent-device/platform-android/device-boot', '@agent-device/platform-android/mechanics', + '@agent-device/platform-android/snapshot-capture-fixtures', + '@agent-device/platform-android/ui-hierarchy-fixtures', ] : [`@agent-device/platform-${family}`], })); diff --git a/scripts/layering/platform-package-policy.ts b/scripts/layering/platform-package-policy.ts index 4f276650b1..32f9de7047 100644 --- a/scripts/layering/platform-package-policy.ts +++ b/scripts/layering/platform-package-policy.ts @@ -95,7 +95,15 @@ const MECHANICS_FACET_SUBPATHS: Readonly Date: Tue, 13 Oct 2026 01:21:52 +0200 Subject: [PATCH 05/14] refactor(contracts): share the inventory composition assertions with the test fake Move assertProviderOutcome and assertSourceFamily from src/platform-runtime-device-inventory.ts into @agent-device/contracts/device (device-inventory.ts, exported through ./device). The test fake in device-selection previously restated the provider flow without the outcome assertion, so a malformed provider outcome surfaced as a TypeError or fell through instead of the provider_inventory_contract_violation AppError. Both the production composer and the fake now call the same validators, and a fixture test pins the fail-closed behavior on the fake. The fake's local path keeps its per-family filter: the local option is a flat whole-inventory discoverer fanned out per family, so filtering there is the adapter to the per-family source contract rather than a contract the fake would assert against itself. --- packages/contracts/src/device-inventory.ts | 36 +++++++++++++++++++ packages/contracts/src/facades/device.ts | 2 ++ .../inventory-gateways.fixtures.test.ts | 35 ++++++++++++++++++ .../src/inventory-gateways.fixtures.ts | 4 +++ src/platform-runtime-device-inventory.ts | 26 ++------------ 5 files changed, 79 insertions(+), 24 deletions(-) create mode 100644 packages/device-selection/src/__tests__/inventory-gateways.fixtures.test.ts diff --git a/packages/contracts/src/device-inventory.ts b/packages/contracts/src/device-inventory.ts index 3f9bda99b8..bd42a622a4 100644 --- a/packages/contracts/src/device-inventory.ts +++ b/packages/contracts/src/device-inventory.ts @@ -5,8 +5,11 @@ import { type DeviceInfo, type DeviceKind, type DeviceTarget, + type Platform, type PlatformSelector, } from '@agent-device/kernel/device'; +import { AppError } from '@agent-device/kernel/errors'; +import type { ProviderDeviceInventorySource } from './device-provider.ts'; export const LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS = [ 'android', @@ -69,6 +72,39 @@ export function countDeviceInventoryByGroup(devices: DeviceInfo[]): DeviceInvent return counts; } +/** + * The composition-level contract every device-inventory source must honor: a family's + * source only ever reports devices of its own family. Both the production composer and + * the test fake enforce this through one implementation. + */ +export function assertSourceFamily(family: Platform, devices: readonly DeviceInfo[]): void { + const outside = devices.find((device) => device.platform !== family); + if (!outside) return; + throw new AppError( + 'COMMAND_FAILED', + `Platform inventory source ${family} returned a ${outside.platform} device.`, + { expectedFamily: family, actualFamily: outside.platform, deviceId: outside.id }, + ); +} + +/** + * Validates a raw provider discover result before the composer trusts it. A malformed + * outcome (not a declined/inventory tagged object) is a provider contract violation, not + * a local-inventory fallback. + */ +export function assertProviderOutcome( + value: unknown, +): asserts value is Awaited> { + if (value !== null && typeof value === 'object') { + const outcome = value as { kind?: unknown; devices?: unknown }; + if (outcome.kind === 'declined' && !('devices' in outcome)) return; + if (outcome.kind === 'inventory' && Array.isArray(outcome.devices)) return; + } + throw new AppError('COMMAND_FAILED', 'Provider device inventory returned an invalid outcome.', { + reason: 'provider_inventory_contract_violation', + }); +} + export function filterDeviceInventoryProjection( devices: readonly DeviceInfo[], request: Pick, diff --git a/packages/contracts/src/facades/device.ts b/packages/contracts/src/facades/device.ts index 5df44feb85..b78fdba9dd 100644 --- a/packages/contracts/src/facades/device.ts +++ b/packages/contracts/src/facades/device.ts @@ -5,6 +5,8 @@ export type { AppStateCommandResult } from '../app-state.ts'; export { LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS, WEB_DESKTOP_DEVICE, + assertProviderOutcome, + assertSourceFamily, countDeviceInventoryByGroup, filterDeviceInventoryProjection, isDeviceClaimConflictReason, diff --git a/packages/device-selection/src/__tests__/inventory-gateways.fixtures.test.ts b/packages/device-selection/src/__tests__/inventory-gateways.fixtures.test.ts new file mode 100644 index 0000000000..94f4052cae --- /dev/null +++ b/packages/device-selection/src/__tests__/inventory-gateways.fixtures.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, test } from 'vitest'; +import type { ProviderDeviceInventorySource } from '@agent-device/contracts/device'; +import { createTestDeviceInventoryGateways } from '../inventory-gateways.fixtures.ts'; + +const scope = { + signal: new AbortController().signal, + diagnostics: { emit: () => {} }, + progress: { report: () => {} }, +} as const; + +describe('test device inventory gateways', () => { + test('a malformed provider outcome fails closed through the shared composer assertion', async () => { + const gateways = createTestDeviceInventoryGateways({ + provider: { discover: async () => undefined } as unknown as ProviderDeviceInventorySource, + }); + await expect( + gateways.providerFirst.discover({ platform: 'android' }, scope), + ).rejects.toMatchObject({ details: { reason: 'provider_inventory_contract_violation' } }); + }); + + test('a declined provider falls through to the local inventory', async () => { + const device = { + platform: 'android' as const, + id: 'emulator-5554', + name: 'Pixel', + kind: 'emulator' as const, + booted: true, + }; + const gateways = createTestDeviceInventoryGateways({ + provider: { discover: async () => ({ kind: 'declined' }) }, + local: async () => [device], + }); + expect(await gateways.providerFirst.discover({ platform: 'android' }, scope)).toEqual([device]); + }); +}); diff --git a/packages/device-selection/src/inventory-gateways.fixtures.ts b/packages/device-selection/src/inventory-gateways.fixtures.ts index 559002b3c1..3d0b31e0ea 100644 --- a/packages/device-selection/src/inventory-gateways.fixtures.ts +++ b/packages/device-selection/src/inventory-gateways.fixtures.ts @@ -1,4 +1,5 @@ import { + assertProviderOutcome, filterDeviceInventoryProjection, LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS, projectProviderDeviceInventoryRequest, @@ -43,6 +44,8 @@ async function discoverLocalFamily( ): Promise { scope.signal.throwIfAborted(); const family: Platform = isApplePlatform(platform) ? 'apple' : platform; + // `local` is a flat whole-inventory discoverer fanned out per family, so this filter is + // the adapter to the per-family source contract, not a place production would assert. const devices = (await localDiscover(request)).filter((device) => device.platform === family); return filterDeviceInventoryProjection(devices, request); } @@ -93,6 +96,7 @@ export function createTestDeviceInventoryGateways( scope.signal, ); scope.signal.throwIfAborted(); + assertProviderOutcome(result); if (result.kind === 'inventory') { return { devices: filterDeviceInventoryProjection( diff --git a/src/platform-runtime-device-inventory.ts b/src/platform-runtime-device-inventory.ts index f6f3c22792..31c53b68e5 100644 --- a/src/platform-runtime-device-inventory.ts +++ b/src/platform-runtime-device-inventory.ts @@ -1,4 +1,6 @@ import { + assertProviderOutcome, + assertSourceFamily, filterDeviceInventoryProjection, LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS, projectProviderDeviceInventoryRequest, @@ -24,7 +26,6 @@ import { type Platform, type PlatformSelector, } from '@agent-device/kernel/device'; -import { AppError } from '@agent-device/kernel/errors'; export function createComposedDeviceInventoryGateways( options: Readonly<{ @@ -210,26 +211,3 @@ async function discoverFamily( function familyForSelector(selector: PlatformSelector): Platform { return isApplePlatform(selector) ? 'apple' : selector; } - -function assertSourceFamily(family: Platform, devices: readonly DeviceInfo[]): void { - const outside = devices.find((device) => device.platform !== family); - if (!outside) return; - throw new AppError( - 'COMMAND_FAILED', - `Platform inventory source ${family} returned a ${outside.platform} device.`, - { expectedFamily: family, actualFamily: outside.platform, deviceId: outside.id }, - ); -} - -function assertProviderOutcome( - value: unknown, -): asserts value is Awaited> { - if (value !== null && typeof value === 'object') { - const outcome = value as { kind?: unknown; devices?: unknown }; - if (outcome.kind === 'declined' && !('devices' in outcome)) return; - if (outcome.kind === 'inventory' && Array.isArray(outcome.devices)) return; - } - throw new AppError('COMMAND_FAILED', 'Provider device inventory returned an invalid outcome.', { - reason: 'provider_inventory_contract_violation', - }); -} From 389eede09853e64ac251202e27d23b8322d9f8f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Tue, 13 Oct 2026 11:14:31 +0200 Subject: [PATCH 06/14] refactor(replay-port): keep the caller-path collector out of the daemon port The replay source bundle loader and caller-side path discovery run on the client (#1802: the daemon opens no caller path), yet they sat under packages/replay-port/src/daemon-port/, where nothing stopped daemon code from importing them. Move script-source-bundle, source-discovery, and the request fixture to the replay-port source root, which stays outside the engine-prefix policy and the daemon-port zone; the subpath names are unchanged, so consumers and the CLI closure guard retarget by path only. --- packages/replay-port/package.json | 12 ++++++------ .../__tests__/script-source-bundle.test.ts | 2 +- .../__tests__/source-discovery.test.ts | 2 +- .../replay-script-source.fixtures.ts | 0 .../src/{daemon-port => }/script-source-bundle.ts | 0 .../src/{daemon-port => }/source-discovery.ts | 0 scripts/__tests__/eager-closure-budgets.ts | 2 +- src/__tests__/cli-startup-import-closure.test.ts | 5 +---- 8 files changed, 10 insertions(+), 13 deletions(-) rename packages/replay-port/src/{daemon-port => }/__tests__/script-source-bundle.test.ts (97%) rename packages/replay-port/src/{daemon-port => }/__tests__/source-discovery.test.ts (99%) rename packages/replay-port/src/{daemon-port => }/replay-script-source.fixtures.ts (100%) rename packages/replay-port/src/{daemon-port => }/script-source-bundle.ts (100%) rename packages/replay-port/src/{daemon-port => }/source-discovery.ts (100%) diff --git a/packages/replay-port/package.json b/packages/replay-port/package.json index 91eef8d584..88eb85c27f 100644 --- a/packages/replay-port/package.json +++ b/packages/replay-port/package.json @@ -53,8 +53,8 @@ "default": "./src/daemon-port/script-source.ts" }, "./script-source-bundle": { - "types": "./src/daemon-port/script-source-bundle.ts", - "default": "./src/daemon-port/script-source-bundle.ts" + "types": "./src/script-source-bundle.ts", + "default": "./src/script-source-bundle.ts" }, "./session-replay-action-runtime": { "types": "./src/daemon-port/session-replay-action-runtime.ts", @@ -157,12 +157,12 @@ "default": "./src/daemon-port/test-command.ts" }, "./source-discovery": { - "types": "./src/daemon-port/source-discovery.ts", - "default": "./src/daemon-port/source-discovery.ts" + "types": "./src/source-discovery.ts", + "default": "./src/source-discovery.ts" }, "./replay-script-source-fixtures": { - "types": "./src/daemon-port/replay-script-source.fixtures.ts", - "default": "./src/daemon-port/replay-script-source.fixtures.ts" + "types": "./src/replay-script-source.fixtures.ts", + "default": "./src/replay-script-source.fixtures.ts" } } } diff --git a/packages/replay-port/src/daemon-port/__tests__/script-source-bundle.test.ts b/packages/replay-port/src/__tests__/script-source-bundle.test.ts similarity index 97% rename from packages/replay-port/src/daemon-port/__tests__/script-source-bundle.test.ts rename to packages/replay-port/src/__tests__/script-source-bundle.test.ts index 8f7597437f..0c5592373a 100644 --- a/packages/replay-port/src/daemon-port/__tests__/script-source-bundle.test.ts +++ b/packages/replay-port/src/__tests__/script-source-bundle.test.ts @@ -5,7 +5,7 @@ import { loadReplayScriptSourceBundle, REPLAY_SCRIPT_SOURCE_BUNDLE_MAX_BYTES, } from '@agent-device/replay-port/script-source-bundle'; -import { mkdtempForTestSync } from '../../tmp-dir.fixtures.ts'; +import { mkdtempForTestSync } from '../tmp-dir.fixtures.ts'; // #1802: the caller reads every script file a replay run needs and sends the text. These pin the // collection itself; that the daemon consumes only the result is pinned beside the replay handler. diff --git a/packages/replay-port/src/daemon-port/__tests__/source-discovery.test.ts b/packages/replay-port/src/__tests__/source-discovery.test.ts similarity index 99% rename from packages/replay-port/src/daemon-port/__tests__/source-discovery.test.ts rename to packages/replay-port/src/__tests__/source-discovery.test.ts index 12e5615180..2e8ead6c14 100644 --- a/packages/replay-port/src/daemon-port/__tests__/source-discovery.test.ts +++ b/packages/replay-port/src/__tests__/source-discovery.test.ts @@ -6,7 +6,7 @@ import os from 'node:os'; import path from 'node:path'; import { AppError } from '@agent-device/kernel/errors'; import { discoverReplaySourcePaths } from '@agent-device/replay-port/source-discovery'; -import { mkdtempForTestSync } from '../../tmp-dir.fixtures.ts'; +import { mkdtempForTestSync } from '../tmp-dir.fixtures.ts'; // #1802: path expansion, traversal ordering and file-type routing are CALLER work — `test` // inputs name files on the machine that typed the command. These pin the traversal itself; diff --git a/packages/replay-port/src/daemon-port/replay-script-source.fixtures.ts b/packages/replay-port/src/replay-script-source.fixtures.ts similarity index 100% rename from packages/replay-port/src/daemon-port/replay-script-source.fixtures.ts rename to packages/replay-port/src/replay-script-source.fixtures.ts diff --git a/packages/replay-port/src/daemon-port/script-source-bundle.ts b/packages/replay-port/src/script-source-bundle.ts similarity index 100% rename from packages/replay-port/src/daemon-port/script-source-bundle.ts rename to packages/replay-port/src/script-source-bundle.ts diff --git a/packages/replay-port/src/daemon-port/source-discovery.ts b/packages/replay-port/src/source-discovery.ts similarity index 100% rename from packages/replay-port/src/daemon-port/source-discovery.ts rename to packages/replay-port/src/source-discovery.ts diff --git a/scripts/__tests__/eager-closure-budgets.ts b/scripts/__tests__/eager-closure-budgets.ts index 5c0520a4f2..e17b81e142 100644 --- a/scripts/__tests__/eager-closure-budgets.ts +++ b/scripts/__tests__/eager-closure-budgets.ts @@ -135,7 +135,7 @@ export const APPROVED_OVER_CEILING: Readonly< > = Object.freeze({ // The single owner of the client-collected replay source bundles tests carry (#1802). Its // weight is the real collector it exists to share, not fixture code. - 'packages/replay-port/src/daemon-port/replay-script-source.fixtures.ts': { + 'packages/replay-port/src/replay-script-source.fixtures.ts': { issue: '#3517', reason: 'Test-only request fixture whose whole contract is running the real client collector ' + diff --git a/src/__tests__/cli-startup-import-closure.test.ts b/src/__tests__/cli-startup-import-closure.test.ts index 53a8635b24..598c5263b5 100644 --- a/src/__tests__/cli-startup-import-closure.test.ts +++ b/src/__tests__/cli-startup-import-closure.test.ts @@ -121,10 +121,7 @@ test('the CLI startup import closure never evaluates the Maestro engine', () => test('the replay command source closure keeps Maestro behind flow selection', () => { const replayEntry = path.join(srcRoot, 'commands/replay/index.ts'); const replayClosure = eagerClosureOf(replayEntry); - const sourceBundle = path.join( - srcRoot, - '../packages/replay-port/src/daemon-port/script-source-bundle.ts', - ); + const sourceBundle = path.join(srcRoot, '../packages/replay-port/src/script-source-bundle.ts'); const offenders: string[] = []; expect(replayClosure).toContain(sourceBundle); From 6c3fb0d152917d5881c94071cf7ca97bc4e422e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Tue, 13 Oct 2026 11:49:42 +0200 Subject: [PATCH 07/14] refactor(device-selection): compose the device inventory once in the package The composed-gateways flow (provider-first discovery, declined-to-local fallback, request projection, per-family fan-out) moves from the root composition into @agent-device/device-selection/composed-inventory-gateways with the installed-app probe injected rather than hardcoded, exported as a production subpath. The test fake now builds its gateways through that real composer with fake registry sources instead of restating the flow, so the root, daemon-composition and integration consumers exercise production behavior rather than a copy that could drift from it. src/platform-runtime-device-inventory.ts keeps its path as the production wiring that attaches the Apple simulator probe, importing the package composer function-scoped so the composition root's eager closure does not grow, and its root-side suite pins that factory composition. --- packages/device-selection/package.json | 4 + .../composed-inventory-gateways.test.ts | 332 ++++++++++++++++++ .../src/composed-inventory-gateways.ts | 212 +++++++++++ .../src/inventory-gateways.fixtures.ts | 166 ++++----- src/daemon-composition.ts | 2 +- src/platform-runtime-device-inventory.test.ts | 294 ++-------------- src/platform-runtime-device-inventory.ts | 222 ++---------- src/platform-runtime.test.ts | 4 +- src/platform-runtime.ts | 6 +- 9 files changed, 669 insertions(+), 573 deletions(-) create mode 100644 packages/device-selection/src/__tests__/composed-inventory-gateways.test.ts create mode 100644 packages/device-selection/src/composed-inventory-gateways.ts diff --git a/packages/device-selection/package.json b/packages/device-selection/package.json index 8ade295ca0..4c7c335d56 100644 --- a/packages/device-selection/package.json +++ b/packages/device-selection/package.json @@ -11,6 +11,10 @@ "@agent-device/kernel": "workspace:*" }, "exports": { + "./composed-inventory-gateways": { + "types": "./src/composed-inventory-gateways.ts", + "default": "./src/composed-inventory-gateways.ts" + }, "./dispatch-resolve": { "types": "./src/dispatch-resolve.ts", "default": "./src/dispatch-resolve.ts" diff --git a/packages/device-selection/src/__tests__/composed-inventory-gateways.test.ts b/packages/device-selection/src/__tests__/composed-inventory-gateways.test.ts new file mode 100644 index 0000000000..8a1868c395 --- /dev/null +++ b/packages/device-selection/src/__tests__/composed-inventory-gateways.test.ts @@ -0,0 +1,332 @@ +import type { + DeviceInventoryRequest, + ProviderDeviceInventorySource, +} from '@agent-device/contracts/device'; +import { + type DeviceInventorySource, + type InventoryPlatformModule, + createPlatformModuleRegistry, +} from '@agent-device/contracts/platform-module'; +import type { + DeviceInventoryHost, + PlatformRequestScope, +} from '@agent-device/contracts/platform-runtime-host'; +import { resolveTargetDeviceSelection } from '../dispatch-resolve.ts'; +import { withDeviceInventoryContext } from '../device-inventory-context.ts'; +import { PLATFORMS, type DeviceInfo, type Platform } from '@agent-device/kernel/device'; +import { describe, expect, test, vi } from 'vitest'; +import { createComposedDeviceInventoryGateways } from '../composed-inventory-gateways.ts'; + +const scope: PlatformRequestScope = Object.freeze({ + signal: new AbortController().signal, + diagnostics: Object.freeze({ emit: () => {} }), + progress: Object.freeze({ report: () => {} }), +}); + +const host: DeviceInventoryHost = Object.freeze({ + commands: Object.freeze({ + which: async () => undefined, + run: async () => ({ stdout: '', stderr: '', exitCode: 0 }), + }), + appleTools: Object.freeze({ + isXcrunAvailable: async () => false, + run: async () => ({ stdout: '', stderr: '', exitCode: 0 }), + }), + toolchains: Object.freeze({ prepare: async () => {} }), + files: Object.freeze({ + isExecutable: async () => false, + createTemporaryTextFile: async () => { + throw new Error('unused'); + }, + }), + hostOs: 'darwin', + hostName: 'test-host', + homeDirectory: '/tmp/test-home', + observations: Object.freeze({ deviceBooted: async () => {} }), +}); + +describe('composed device inventory gateway', () => { + test('an authoritative provider inventory, including empty, loads no local host or source', async () => { + for (const devices of [[], [device('android', 'remote')]]) { + const local = inventoryWorld(); + const provider: ProviderDeviceInventorySource = { + discover: async () => ({ kind: 'inventory', devices }), + }; + const gateways = createComposedDeviceInventoryGateways({ + registry: local.registry, + loadHost: local.loadHost, + provider, + }); + + expect(await gateways.providerFirst.discover({ platform: 'android' }, scope)).toEqual( + devices, + ); + expect(local.hostLoads()).toBe(0); + expect(local.sourceLoads()).toEqual([]); + } + }); + + test('declined providers fall through, while malformed outcomes fail closed', async () => { + const local = inventoryWorld(); + const declined = createComposedDeviceInventoryGateways({ + registry: local.registry, + loadHost: local.loadHost, + provider: { discover: async () => ({ kind: 'declined' }) }, + }); + expect(await declined.providerFirst.discover({ platform: 'android' }, scope)).toEqual([ + device('android'), + ]); + + const malformed = createComposedDeviceInventoryGateways({ + registry: local.registry, + loadHost: local.loadHost, + provider: { discover: async () => undefined } as unknown as ProviderDeviceInventorySource, + }); + await expect( + malformed.providerFirst.discover({ platform: 'android' }, scope), + ).rejects.toMatchObject({ details: { reason: 'provider_inventory_contract_violation' } }); + }); + + test('forwards the request signal into an in-flight provider source', async () => { + const controller = new AbortController(); + const requestScope = { ...scope, signal: controller.signal }; + const observed = vi.fn(); + const provider: ProviderDeviceInventorySource = { + discover: async (_request, signal) => { + observed(signal); + return await new Promise((_resolve, reject) => { + signal.addEventListener('abort', () => reject(signal.reason), { once: true }); + }); + }, + }; + const local = inventoryWorld(); + const gateways = createComposedDeviceInventoryGateways({ + registry: local.registry, + loadHost: local.loadHost, + provider, + }); + const pending = gateways.providerFirst.discover({ platform: 'android' }, requestScope); + controller.abort(new Error('cancelled')); + + await expect(pending).rejects.toThrow('cancelled'); + expect(observed).toHaveBeenCalledWith(controller.signal); + expect(local.hostLoads()).toBe(0); + }); + + test('keeps local projection policy out of the provider request', async () => { + const received: unknown[] = []; + const provider: ProviderDeviceInventorySource = { + discover: async (request) => { + received.push(request); + return { kind: 'inventory', devices: [] }; + }, + }; + const local = inventoryWorld(); + const gateways = createComposedDeviceInventoryGateways({ + registry: local.registry, + loadHost: local.loadHost, + provider, + }); + + await gateways.providerFirst.discover( + { + platform: 'ios', + kind: 'simulator', + booted: true, + iosSimulatorSetPath: '/tmp/isolated-simulators', + androidSerialAllowlist: ['emulator-5554'], + }, + scope, + ); + + expect(received).toEqual([ + { + platform: 'ios', + iosSimulatorSetPath: '/tmp/isolated-simulators', + androidSerialAllowlist: ['emulator-5554'], + }, + ]); + expect(local.hostLoads()).toBe(0); + }); + + test('applies public Apple aliases and all internal projection filters centrally', async () => { + const devices = [ + { ...device('apple', 'ios-default'), appleOs: 'ios' as const, target: undefined }, + { ...device('apple', 'mac'), appleOs: 'macos' as const, target: 'desktop' as const }, + { ...device('apple', 'shutdown'), appleOs: 'ios' as const, booted: false }, + device('android', 'other-family'), + ]; + const provider: ProviderDeviceInventorySource = { + discover: async () => ({ kind: 'inventory', devices }), + }; + const local = inventoryWorld(); + const gateways = createComposedDeviceInventoryGateways({ + registry: local.registry, + loadHost: local.loadHost, + provider, + }); + + const projected = await gateways.providerFirst.discover( + { platform: 'ios', target: 'mobile', kind: 'device', booted: true }, + scope, + ); + expect(projected.map(({ id }) => id)).toEqual(['ios-default']); + }); + + test('loads only the selected family and retries failed host and source loads', async () => { + let hostAttempts = 0; + let androidAttempts = 0; + const local = inventoryWorld({ + android: async () => { + androidAttempts += 1; + if (androidAttempts === 1) throw new Error('source load failed'); + return source([device('android')]); + }, + }); + const gateways = createComposedDeviceInventoryGateways({ + registry: local.registry, + loadHost: async () => { + hostAttempts += 1; + if (hostAttempts === 1) throw new Error('host load failed'); + return host; + }, + }); + + await expect(gateways.localOnly.discover({ platform: 'android' }, scope)).rejects.toThrow( + 'host load failed', + ); + await expect(gateways.localOnly.discover({ platform: 'android' }, scope)).rejects.toThrow( + 'source load failed', + ); + expect(await gateways.localOnly.discover({ platform: 'android' }, scope)).toEqual([ + device('android'), + ]); + expect(hostAttempts).toBe(2); + expect(androidAttempts).toBe(2); + expect(local.sourceLoads()).toEqual(['android', 'android']); + }); + + test('unfiltered local discovery is fail-soft and preserves selector order', async () => { + const local = inventoryWorld({ + harmonyos: async () => source([], new Error('hdc unavailable')), + }); + const gateways = createComposedDeviceInventoryGateways({ + registry: local.registry, + loadHost: local.loadHost, + }); + + const devices = await gateways.localOnly.discover({}, scope); + expect(devices.map(({ platform }) => platform)).toEqual(['android', 'apple', 'vega', 'linux']); + expect(local.sourceLoads()).toEqual(['android', 'harmonyos', 'apple', 'vega', 'linux']); + }); + + test('explicit local failures propagate and family sources cannot return another family', async () => { + const failing = inventoryWorld({ android: async () => source([], new Error('adb failed')) }); + await expect( + createComposedDeviceInventoryGateways({ + registry: failing.registry, + loadHost: failing.loadHost, + }).localOnly.discover({ platform: 'android' }, scope), + ).rejects.toThrow('adb failed'); + + const lying = inventoryWorld({ + android: async () => source([device('linux', 'wrong-family')]), + }); + await expect( + createComposedDeviceInventoryGateways({ + registry: lying.registry, + loadHost: lying.loadHost, + }).localOnly.discover({ platform: 'android' }, scope), + ).rejects.toMatchObject({ + details: { expectedFamily: 'android', actualFamily: 'linux' }, + }); + }); + + test('narrows app-based simulator selection through the injected probe', async () => { + // The composer builds on the caller-supplied probe; production wiring attaches the real + // Apple one and src/platform-runtime-device-inventory.test.ts pins that composition. + const installedApps: Record = { 'sim-b': 'com.example.demo' }; + const calls: string[] = []; + const local = inventoryWorld({ + apple: async () => source([simulator('sim-a'), simulator('sim-b')]), + }); + const gateways = createComposedDeviceInventoryGateways({ + registry: local.registry, + loadHost: local.loadHost, + findInstalledApp: async (device) => { + calls.push(device.id); + return installedApps[device.id]; + }, + }); + + const selection = await withDeviceInventoryContext({ ...gateways, requestScope: scope }, () => + resolveTargetDeviceSelection( + { platform: 'ios' }, + { + appleSimulatorAppTarget: 'com.example.demo', + }, + ), + ); + + expect(selection.device.id).toBe('sim-b'); + expect(selection.reason).toBe('single-app-installed-local'); + expect(selection.source).toBe('local'); + expect(selection.candidateCount).toBe(1); + expect([...calls].sort()).toEqual(['sim-a', 'sim-b']); + }); +}); + +function inventoryWorld( + overrides: Partial Promise>> = {}, +) { + const loaded: Platform[] = []; + let hosts = 0; + const modules: InventoryPlatformModule[] = PLATFORMS.map((family) => ({ + family, + loadInventory: async () => { + loaded.push(family); + return await (overrides[family]?.() ?? Promise.resolve(source([device(family)]))); + }, + })); + return { + registry: createPlatformModuleRegistry(modules), + loadHost: async () => { + hosts += 1; + return host; + }, + hostLoads: () => hosts, + sourceLoads: () => [...loaded], + }; +} + +function source(devices: readonly DeviceInfo[], failure?: Error): DeviceInventorySource { + return { + discover: async (_request: Readonly) => { + if (failure) throw failure; + return devices; + }, + }; +} + +function device(platform: Platform, id = `${platform}-device`): DeviceInfo { + return { + platform, + id, + name: id, + kind: 'device', + target: platform === 'vega' ? 'tv' : 'mobile', + booted: true, + }; +} + +function simulator(id: string): DeviceInfo { + return { + platform: 'apple', + id, + name: id, + kind: 'simulator', + appleOs: 'ios', + target: 'mobile', + booted: true, + }; +} diff --git a/packages/device-selection/src/composed-inventory-gateways.ts b/packages/device-selection/src/composed-inventory-gateways.ts new file mode 100644 index 0000000000..9332db58e1 --- /dev/null +++ b/packages/device-selection/src/composed-inventory-gateways.ts @@ -0,0 +1,212 @@ +import { + assertProviderOutcome, + assertSourceFamily, + filterDeviceInventoryProjection, + LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS, + projectProviderDeviceInventoryRequest, + type ProviderDeviceInventorySource, +} from '@agent-device/contracts/device'; +import type { + ComposedDeviceInventoryGateways, + DeviceInventoryGateway, + DeviceInventorySource, + InstalledAppProbe, + InventoryPlatformModule, + PlatformModuleRegistry, + ProviderAwareDeviceInventoryGateway, +} from '@agent-device/contracts/platform-module'; +import type { + DeviceInventoryHost, + DeviceInventoryHostFor, + PlatformRequestScope, +} from '@agent-device/contracts/platform-runtime-host'; +import { + isApplePlatform, + type DeviceInfo, + type Platform, + type PlatformSelector, +} from '@agent-device/kernel/device'; + +export function createComposedDeviceInventoryGateways( + options: Readonly<{ + registry: PlatformModuleRegistry; + loadHost: () => Promise; + provider?: ProviderDeviceInventorySource; + /** + * Installed-app probe device selection uses to narrow several booted simulators before + * committing to one. The host composes one in (production passes the Apple simulator + * probe); tests inject or omit one. Absent, selection falls back to the inventory rules. + */ + findInstalledApp?: InstalledAppProbe; + }>, +): ComposedDeviceInventoryGateways { + const localOnly = createLocalDeviceInventoryGateway(options.registry, options.loadHost); + const provider = options.provider; + const discoverWithSource: ProviderAwareDeviceInventoryGateway['discoverWithSource'] = provider + ? async (request, scope) => { + scope.signal.throwIfAborted(); + const provided = await provider.discover( + projectProviderDeviceInventoryRequest(request), + scope.signal, + ); + scope.signal.throwIfAborted(); + assertProviderOutcome(provided); + if (provided.kind === 'inventory') { + return { + devices: filterDeviceInventoryProjection( + provided.devices.map((device) => ({ ...device })), + request, + ), + source: 'provider', + }; + } + return { devices: await localOnly.discover(request, scope), source: 'local' }; + } + : async (request, scope) => ({ + devices: await localOnly.discover(request, scope), + source: 'local', + }); + const providerFirst: ProviderAwareDeviceInventoryGateway = Object.freeze({ + discover: async (request, scope) => (await discoverWithSource(request, scope)).devices, + discoverWithSource, + }); + return Object.freeze({ providerFirst, localOnly, findInstalledApp: options.findInstalledApp }); +} + +function createLocalDeviceInventoryGateway( + registry: PlatformModuleRegistry, + loadHost: () => Promise, +): DeviceInventoryGateway { + let hostLoad: Promise | undefined; + const requiredHost = async (): Promise => { + hostLoad ??= loadHost(); + try { + return await hostLoad; + } catch (error) { + hostLoad = undefined; + throw error; + } + }; + const sourceLoads = new Map>(); + const loadSource = async (family: Platform): Promise => { + const existing = sourceLoads.get(family); + if (existing) return await existing; + const pending = loadFamilyInventory(registry.get(family), requiredHost); + sourceLoads.set(family, pending); + try { + return await pending; + } catch (error) { + if (sourceLoads.get(family) === pending) sourceLoads.delete(family); + throw error; + } + }; + + return Object.freeze({ + discover: async (request, scope) => { + scope.signal.throwIfAborted(); + if (request.platform) { + return await discoverFamily( + loadSource, + familyForSelector(request.platform), + request, + scope, + ); + } + + const perFamily = await Promise.all( + LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS.map(async (selector) => { + try { + const devices = await discoverFamily( + loadSource, + familyForSelector(selector), + { ...request, platform: selector }, + scope, + ); + return Array.isArray(devices) ? devices : []; + } catch { + return []; + } + }), + ); + scope.signal.throwIfAborted(); + return perFamily.flat(); + }, + }); +} + +async function loadFamilyInventory( + module: InventoryPlatformModule, + requiredHost: () => Promise, +): Promise { + switch (module.family) { + case 'apple': + return await module.loadInventory(appleInventoryHost(await requiredHost())); + case 'android': + return await module.loadInventory(androidInventoryHost(await requiredHost())); + case 'harmonyos': + return await module.loadInventory(harmonyInventoryHost(await requiredHost())); + case 'vega': + return await module.loadInventory(vegaInventoryHost(await requiredHost())); + case 'linux': + return await module.loadInventory(linuxInventoryHost(await requiredHost())); + case 'web': + return await module.loadInventory(); + } +} + +function appleInventoryHost(host: DeviceInventoryHost): DeviceInventoryHostFor<'apple'> { + return Object.freeze({ + appleTools: host.appleTools, + files: host.files, + hostName: host.hostName, + hostOs: host.hostOs, + observations: host.observations, + }); +} + +function androidInventoryHost(host: DeviceInventoryHost): DeviceInventoryHostFor<'android'> { + return Object.freeze({ + commands: host.commands, + files: host.files, + homeDirectory: host.homeDirectory, + toolchains: host.toolchains, + }); +} + +function harmonyInventoryHost(host: DeviceInventoryHost): DeviceInventoryHostFor<'harmonyos'> { + return Object.freeze({ + commands: host.commands, + files: host.files, + toolchains: host.toolchains, + }); +} + +function vegaInventoryHost(host: DeviceInventoryHost): DeviceInventoryHostFor<'vega'> { + return Object.freeze({ + commands: host.commands, + files: host.files, + homeDirectory: host.homeDirectory, + }); +} + +function linuxInventoryHost(host: DeviceInventoryHost): DeviceInventoryHostFor<'linux'> { + return Object.freeze({ hostName: host.hostName, hostOs: host.hostOs }); +} + +async function discoverFamily( + loadSource: (family: Platform) => Promise, + family: Platform, + request: Parameters[0], + scope: PlatformRequestScope, +): Promise { + const source = await loadSource(family); + scope.signal.throwIfAborted(); + const devices = await source.discover(request, scope); + scope.signal.throwIfAborted(); + assertSourceFamily(family, devices); + return filterDeviceInventoryProjection(devices, request); +} + +function familyForSelector(selector: PlatformSelector): Platform { + return isApplePlatform(selector) ? 'apple' : selector; +} diff --git a/packages/device-selection/src/inventory-gateways.fixtures.ts b/packages/device-selection/src/inventory-gateways.fixtures.ts index 3d0b31e0ea..0d9dc963e7 100644 --- a/packages/device-selection/src/inventory-gateways.fixtures.ts +++ b/packages/device-selection/src/inventory-gateways.fixtures.ts @@ -1,25 +1,21 @@ import { - assertProviderOutcome, - filterDeviceInventoryProjection, - LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS, - projectProviderDeviceInventoryRequest, type DeviceInventoryProvider, type DeviceInventoryRequest, type ProviderDeviceInventorySource, } from '@agent-device/contracts/device'; -import type { - ComposedDeviceInventoryGateways, - DeviceInventoryGateway, - InstalledAppProbe, - ProviderAwareDeviceInventoryGateway, -} from '@agent-device/contracts/platform-module'; -import type { PlatformRequestScope } from '@agent-device/contracts/platform-runtime-host'; import { - isApplePlatform, - type DeviceInfo, - type Platform, - type PlatformSelector, -} from '@agent-device/kernel/device'; + type ComposedDeviceInventoryGateways, + type DeviceInventorySource, + type InstalledAppProbe, + type InventoryPlatformModule, + createPlatformModuleRegistry, +} from '@agent-device/contracts/platform-module'; +import type { + DeviceInventoryHost, + PlatformRequestScope, +} from '@agent-device/contracts/platform-runtime-host'; +import type { DeviceInfo, Platform } from '@agent-device/kernel/device'; +import { createComposedDeviceInventoryGateways } from './composed-inventory-gateways.ts'; import { withDeviceInventoryContext } from './device-inventory-context.ts'; export type TestDeviceInventoryOptions = Readonly<{ @@ -34,102 +30,58 @@ const testRequestScope: PlatformRequestScope = Object.freeze({ progress: Object.freeze({ report: () => {} }), }); -type LocalDiscover = (request: Readonly) => Promise; - -async function discoverLocalFamily( - localDiscover: LocalDiscover, - platform: PlatformSelector, - request: Readonly, - scope: PlatformRequestScope, -): Promise { - scope.signal.throwIfAborted(); - const family: Platform = isApplePlatform(platform) ? 'apple' : platform; - // `local` is a flat whole-inventory discoverer fanned out per family, so this filter is - // the adapter to the per-family source contract, not a place production would assert. - const devices = (await localDiscover(request)).filter((device) => device.platform === family); - return filterDeviceInventoryProjection(devices, request); -} +const testInventoryHost: DeviceInventoryHost = Object.freeze({ + commands: Object.freeze({ + which: async () => undefined, + run: async () => ({ stdout: '', stderr: '', exitCode: 0 }), + }), + appleTools: Object.freeze({ + isXcrunAvailable: async () => false, + run: async () => ({ stdout: '', stderr: '', exitCode: 0 }), + }), + toolchains: Object.freeze({ prepare: async () => {} }), + files: Object.freeze({ + isExecutable: async () => false, + createTemporaryTextFile: async () => { + throw new Error('unused test inventory temporary file'); + }, + }), + hostOs: 'other', + hostName: 'test-host', + homeDirectory: '/tmp/test-home', + observations: Object.freeze({ deviceBooted: async () => {} }), +}); -async function discoverLocal( - localDiscover: LocalDiscover, - request: Readonly, - scope: PlatformRequestScope, -): Promise { - scope.signal.throwIfAborted(); - if (request.platform) { - return await discoverLocalFamily(localDiscover, request.platform, request, scope); - } - const perFamily = await Promise.all( - LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS.map(async (selector) => { - try { - return await discoverLocalFamily( - localDiscover, - selector, - { ...request, platform: selector }, - scope, - ); - } catch { - return []; - } - }), - ); - scope.signal.throwIfAborted(); - return perFamily.flat(); +function testInventoryModules( + discover: (request: Readonly) => Promise, +): readonly InventoryPlatformModule[] { + const source = (family: Platform): DeviceInventorySource => ({ + discover: async (request) => + (await discover(request)).filter((device) => device.platform === family), + }); + return [ + { family: 'apple', loadInventory: async () => source('apple') }, + { family: 'android', loadInventory: async () => source('android') }, + { family: 'harmonyos', loadInventory: async () => source('harmonyos') }, + { family: 'vega', loadInventory: async () => source('vega') }, + { family: 'linux', loadInventory: async () => source('linux') }, + { family: 'web', loadInventory: async () => source('web') }, + ]; } export function createTestDeviceInventoryGateways( options: TestDeviceInventoryOptions = {}, ): ComposedDeviceInventoryGateways { - const localDiscover = options.local ?? (async () => [] as readonly DeviceInfo[]); - const localOnly: DeviceInventoryGateway = Object.freeze({ - discover: (request, scope) => discoverLocal(localDiscover, request, scope), - }); - const provider = options.provider; - const discoverWithSource: ProviderAwareDeviceInventoryGateway['discoverWithSource'] = async ( - request, - scope, - ) => { - if (provider) { - scope.signal.throwIfAborted(); - const result = await provider.discover( - projectProviderDeviceInventoryRequest(request), - scope.signal, - ); - scope.signal.throwIfAborted(); - assertProviderOutcome(result); - if (result.kind === 'inventory') { - return { - devices: filterDeviceInventoryProjection( - result.devices.map((device) => ({ ...device })), - request, - ), - source: 'provider', - }; - } - } - return { devices: await localOnly.discover(request, scope), source: 'local' }; - }; - const providerFirst: ProviderAwareDeviceInventoryGateway = Object.freeze({ - discover: async (request, scope) => (await discoverWithSource(request, scope)).devices, - discoverWithSource, - }); - return Object.freeze({ - providerFirst, - localOnly, + return createComposedDeviceInventoryGateways({ + registry: createPlatformModuleRegistry( + testInventoryModules(options.local ?? (async () => [] as readonly DeviceInfo[])), + ), + loadHost: async () => testInventoryHost, + provider: options.provider, findInstalledApp: options.findInstalledApp, }); } -export async function withTestDeviceInventory( - options: TestDeviceInventoryOptions, - task: () => Promise, -): Promise { - return await withDeviceInventoryContext( - { ...createTestDeviceInventoryGateways(options), requestScope: testRequestScope }, - task, - ); -} - /** Test-only bridge for fixtures that still implement the public nullable provider port. */ export function createTestDeviceInventoryGatewaysFromProvider( provider: DeviceInventoryProvider, @@ -147,6 +99,16 @@ export function createTestDeviceInventoryGatewaysFromProvider( }); } +export async function withTestDeviceInventory( + options: TestDeviceInventoryOptions, + task: () => Promise, +): Promise { + return await withDeviceInventoryContext( + { ...createTestDeviceInventoryGateways(options), requestScope: testRequestScope }, + task, + ); +} + export async function withTestDeviceInventoryProvider( provider: DeviceInventoryProvider, task: () => Promise, diff --git a/src/daemon-composition.ts b/src/daemon-composition.ts index 8ce83f5649..41f0c1a13a 100644 --- a/src/daemon-composition.ts +++ b/src/daemon-composition.ts @@ -118,7 +118,7 @@ export const createDaemonRuntimeDeps: ComposeDaemonRuntimeDeps = async ( ownedProcessRecords, }, }); - const deviceInventoryGateways = createPlatformDeviceInventoryGateways( + const deviceInventoryGateways = await createPlatformDeviceInventoryGateways( providerRuntimeProviders.deviceInventorySource, ); // Root composition of the platform-services port and the owner-scoped claim-recovery gateway. diff --git a/src/platform-runtime-device-inventory.test.ts b/src/platform-runtime-device-inventory.test.ts index f4e0378688..752843db69 100644 --- a/src/platform-runtime-device-inventory.test.ts +++ b/src/platform-runtime-device-inventory.test.ts @@ -1,7 +1,3 @@ -import type { - DeviceInventoryRequest, - ProviderDeviceInventorySource, -} from '@agent-device/contracts/device'; import { type DeviceInventorySource, type InventoryPlatformModule, @@ -13,7 +9,7 @@ import type { } from '@agent-device/contracts/platform-runtime-host'; import { resolveTargetDeviceSelection } from '@agent-device/device-selection/dispatch-resolve'; import { withDeviceInventoryContext } from '@agent-device/device-selection/device-inventory-context'; -import { PLATFORMS, type DeviceInfo, type Platform } from '@agent-device/kernel/device'; +import { PLATFORMS, type DeviceInfo } from '@agent-device/kernel/device'; import { describe, expect, test, vi } from 'vitest'; import { createComposedDeviceInventoryGateways } from './platform-runtime-device-inventory.ts'; @@ -53,227 +49,50 @@ const host: DeviceInventoryHost = Object.freeze({ files: Object.freeze({ isExecutable: async () => false, createTemporaryTextFile: async () => { - throw new Error('unused'); + throw new Error('unused test inventory temporary file'); }, }), - hostOs: 'darwin', + hostOs: 'other', hostName: 'test-host', homeDirectory: '/tmp/test-home', observations: Object.freeze({ deviceBooted: async () => {} }), }); -describe('composed device inventory gateway', () => { - test('an authoritative provider inventory, including empty, loads no local host or source', async () => { - for (const devices of [[], [device('android', 'remote')]]) { - const local = inventoryWorld(); - const provider: ProviderDeviceInventorySource = { - discover: async () => ({ kind: 'inventory', devices }), - }; - const gateways = createComposedDeviceInventoryGateways({ - registry: local.registry, - loadHost: local.loadHost, - provider, - }); - - expect(await gateways.providerFirst.discover({ platform: 'android' }, scope)).toEqual( - devices, - ); - expect(local.hostLoads()).toBe(0); - expect(local.sourceLoads()).toEqual([]); - } - }); - - test('declined providers fall through, while malformed outcomes fail closed', async () => { - const local = inventoryWorld(); - const declined = createComposedDeviceInventoryGateways({ - registry: local.registry, - loadHost: local.loadHost, - provider: { discover: async () => ({ kind: 'declined' }) }, - }); - expect(await declined.providerFirst.discover({ platform: 'android' }, scope)).toEqual([ - device('android'), - ]); - - const malformed = createComposedDeviceInventoryGateways({ - registry: local.registry, - loadHost: local.loadHost, - provider: { discover: async () => undefined } as unknown as ProviderDeviceInventorySource, - }); - await expect( - malformed.providerFirst.discover({ platform: 'android' }, scope), - ).rejects.toMatchObject({ details: { reason: 'provider_inventory_contract_violation' } }); - }); - - test('forwards the request signal into an in-flight provider source', async () => { - const controller = new AbortController(); - const requestScope = { ...scope, signal: controller.signal }; - const observed = vi.fn(); - const provider: ProviderDeviceInventorySource = { - discover: async (_request, signal) => { - observed(signal); - return await new Promise((_resolve, reject) => { - signal.addEventListener('abort', () => reject(signal.reason), { once: true }); - }); - }, - }; - const local = inventoryWorld(); - const gateways = createComposedDeviceInventoryGateways({ - registry: local.registry, - loadHost: local.loadHost, - provider, - }); - const pending = gateways.providerFirst.discover({ platform: 'android' }, requestScope); - controller.abort(new Error('cancelled')); - - await expect(pending).rejects.toThrow('cancelled'); - expect(observed).toHaveBeenCalledWith(controller.signal); - expect(local.hostLoads()).toBe(0); - }); - - test('keeps local projection policy out of the provider request', async () => { - const received: unknown[] = []; - const provider: ProviderDeviceInventorySource = { - discover: async (request) => { - received.push(request); - return { kind: 'inventory', devices: [] }; - }, - }; - const local = inventoryWorld(); - const gateways = createComposedDeviceInventoryGateways({ - registry: local.registry, - loadHost: local.loadHost, - provider, - }); - - await gateways.providerFirst.discover( - { - platform: 'ios', - kind: 'simulator', - booted: true, - iosSimulatorSetPath: '/tmp/isolated-simulators', - androidSerialAllowlist: ['emulator-5554'], - }, - scope, - ); - - expect(received).toEqual([ - { - platform: 'ios', - iosSimulatorSetPath: '/tmp/isolated-simulators', - androidSerialAllowlist: ['emulator-5554'], - }, - ]); - expect(local.hostLoads()).toBe(0); - }); - - test('applies public Apple aliases and all internal projection filters centrally', async () => { - const devices = [ - { ...device('apple', 'ios-default'), appleOs: 'ios' as const, target: undefined }, - { ...device('apple', 'mac'), appleOs: 'macos' as const, target: 'desktop' as const }, - { ...device('apple', 'shutdown'), appleOs: 'ios' as const, booted: false }, - device('android', 'other-family'), - ]; - const provider: ProviderDeviceInventorySource = { - discover: async () => ({ kind: 'inventory', devices }), - }; - const local = inventoryWorld(); - const gateways = createComposedDeviceInventoryGateways({ - registry: local.registry, - loadHost: local.loadHost, - provider, - }); - - const projected = await gateways.providerFirst.discover( - { platform: 'ios', target: 'mobile', kind: 'device', booted: true }, - scope, - ); - expect(projected.map(({ id }) => id)).toEqual(['ios-default']); - }); - - test('loads only the selected family and retries failed host and source loads', async () => { - let hostAttempts = 0; - let androidAttempts = 0; - const local = inventoryWorld({ - android: async () => { - androidAttempts += 1; - if (androidAttempts === 1) throw new Error('source load failed'); - return source([device('android')]); - }, - }); - const gateways = createComposedDeviceInventoryGateways({ - registry: local.registry, - loadHost: async () => { - hostAttempts += 1; - if (hostAttempts === 1) throw new Error('host load failed'); - return host; - }, - }); - - await expect(gateways.localOnly.discover({ platform: 'android' }, scope)).rejects.toThrow( - 'host load failed', - ); - await expect(gateways.localOnly.discover({ platform: 'android' }, scope)).rejects.toThrow( - 'source load failed', - ); - expect(await gateways.localOnly.discover({ platform: 'android' }, scope)).toEqual([ - device('android'), - ]); - expect(hostAttempts).toBe(2); - expect(androidAttempts).toBe(2); - expect(local.sourceLoads()).toEqual(['android', 'android']); - }); - - test('unfiltered local discovery is fail-soft and preserves selector order', async () => { - const local = inventoryWorld({ - harmonyos: async () => source([], new Error('hdc unavailable')), - }); - const gateways = createComposedDeviceInventoryGateways({ - registry: local.registry, - loadHost: local.loadHost, - }); - - const devices = await gateways.localOnly.discover({}, scope); - expect(devices.map(({ platform }) => platform)).toEqual(['android', 'apple', 'vega', 'linux']); - expect(local.sourceLoads()).toEqual(['android', 'harmonyos', 'apple', 'vega', 'linux']); - }); - - test('explicit local failures propagate and family sources cannot return another family', async () => { - const failing = inventoryWorld({ android: async () => source([], new Error('adb failed')) }); - await expect( - createComposedDeviceInventoryGateways({ - registry: failing.registry, - loadHost: failing.loadHost, - }).localOnly.discover({ platform: 'android' }, scope), - ).rejects.toThrow('adb failed'); +function simulator(id: string): DeviceInfo { + return { + platform: 'apple', + id, + name: id, + kind: 'simulator', + appleOs: 'ios', + target: 'mobile', + booted: true, + }; +} - const lying = inventoryWorld({ - android: async () => source([device('linux', 'wrong-family')]), - }); - await expect( - createComposedDeviceInventoryGateways({ - registry: lying.registry, - loadHost: lying.loadHost, - }).localOnly.discover({ platform: 'android' }, scope), - ).rejects.toMatchObject({ - details: { expectedFamily: 'android', actualFamily: 'linux' }, - }); - }); +function source(devices: readonly DeviceInfo[]): DeviceInventorySource { + return { discover: async () => devices }; +} +describe('production device inventory composition', () => { test('narrows app-based simulator selection through the factory-installed probe', async () => { - // Guards the production wiring: the package-level selection tests inject their own - // probe, so only this path proves the factory attaches one to the request context. + // Guards the production wiring: the package-level composer only knows the injected + // probe, so only this path proves the root factory attaches the Apple one to the + // request context. simctl.listapps = [ { deviceId: 'sim-b', apps: { 'com.example.demo': { Bundle: 'com.example.demo', CFBundleName: 'Demo' } }, }, ]; - const local = inventoryWorld({ - apple: async () => source([simulator('sim-a'), simulator('sim-b')]), - }); - const gateways = createComposedDeviceInventoryGateways({ - registry: local.registry, - loadHost: local.loadHost, + const modules: InventoryPlatformModule[] = PLATFORMS.map((family) => ({ + family, + loadInventory: async () => + source(family === 'apple' ? [simulator('sim-a'), simulator('sim-b')] : []), + })); + const gateways = await createComposedDeviceInventoryGateways({ + registry: createPlatformModuleRegistry(modules), + loadHost: async () => host, }); const selection = await withDeviceInventoryContext({ ...gateways, requestScope: scope }, () => @@ -292,58 +111,3 @@ describe('composed device inventory gateway', () => { expect([...simctl.calls].sort()).toEqual(['sim-a', 'sim-b']); }); }); - -function inventoryWorld( - overrides: Partial Promise>> = {}, -) { - const loaded: Platform[] = []; - let hosts = 0; - const modules: InventoryPlatformModule[] = PLATFORMS.map((family) => ({ - family, - loadInventory: async () => { - loaded.push(family); - return await (overrides[family]?.() ?? Promise.resolve(source([device(family)]))); - }, - })); - return { - registry: createPlatformModuleRegistry(modules), - loadHost: async () => { - hosts += 1; - return host; - }, - hostLoads: () => hosts, - sourceLoads: () => [...loaded], - }; -} - -function source(devices: readonly DeviceInfo[], failure?: Error): DeviceInventorySource { - return { - discover: async (_request: Readonly) => { - if (failure) throw failure; - return devices; - }, - }; -} - -function device(platform: Platform, id = `${platform}-device`): DeviceInfo { - return { - platform, - id, - name: id, - kind: 'device', - target: platform === 'vega' ? 'tv' : 'mobile', - booted: true, - }; -} - -function simulator(id: string): DeviceInfo { - return { - platform: 'apple', - id, - name: id, - kind: 'simulator', - appleOs: 'ios', - target: 'mobile', - booted: true, - }; -} diff --git a/src/platform-runtime-device-inventory.ts b/src/platform-runtime-device-inventory.ts index 31c53b68e5..48ae39ef83 100644 --- a/src/platform-runtime-device-inventory.ts +++ b/src/platform-runtime-device-inventory.ts @@ -1,31 +1,15 @@ -import { - assertProviderOutcome, - assertSourceFamily, - filterDeviceInventoryProjection, - LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS, - projectProviderDeviceInventoryRequest, - type ProviderDeviceInventorySource, -} from '@agent-device/contracts/device'; import type { ComposedDeviceInventoryGateways, - DeviceInventoryGateway, - DeviceInventorySource, InstalledAppProbe, InventoryPlatformModule, PlatformModuleRegistry, - ProviderAwareDeviceInventoryGateway, } from '@agent-device/contracts/platform-module'; -import type { - DeviceInventoryHost, - DeviceInventoryHostFor, - PlatformRequestScope, -} from '@agent-device/contracts/platform-runtime-host'; -import { - isApplePlatform, - type DeviceInfo, - type Platform, - type PlatformSelector, -} from '@agent-device/kernel/device'; +import type { DeviceInventoryHost } from '@agent-device/contracts/platform-runtime-host'; +import type { ProviderDeviceInventorySource } from '@agent-device/contracts/device'; + +// The composition flow lives in @agent-device/device-selection/composed-inventory-gateways, so +// the test fake there builds on the real one. This module is the production wiring: it attaches +// the Apple simulator app probe the host owns. export function createComposedDeviceInventoryGateways( options: Readonly<{ @@ -33,181 +17,19 @@ export function createComposedDeviceInventoryGateways( loadHost: () => Promise; provider?: ProviderDeviceInventorySource; }>, -): ComposedDeviceInventoryGateways { - const localOnly = createLocalDeviceInventoryGateway(options.registry, options.loadHost); - const provider = options.provider; - const discoverWithSource: ProviderAwareDeviceInventoryGateway['discoverWithSource'] = provider - ? async (request, scope) => { - scope.signal.throwIfAborted(); - const provided = await provider.discover( - projectProviderDeviceInventoryRequest(request), - scope.signal, - ); - scope.signal.throwIfAborted(); - assertProviderOutcome(provided); - if (provided.kind === 'inventory') { - return { - devices: filterDeviceInventoryProjection( - provided.devices.map((device) => ({ ...device })), - request, - ), - source: 'provider', - }; - } - return { devices: await localOnly.discover(request, scope), source: 'local' }; - } - : async (request, scope) => ({ - devices: await localOnly.discover(request, scope), - source: 'local', - }); - const providerFirst: ProviderAwareDeviceInventoryGateway = Object.freeze({ - discover: async (request, scope) => (await discoverWithSource(request, scope)).devices, - discoverWithSource, - }); - // Function-scoped: device selection reads this probe from the request context, and loading it - // must not evaluate Apple app-resolution mechanics until a selection actually narrows. - const findInstalledApp: InstalledAppProbe = async (device, appTarget) => { - const { findIosSimulatorInstalledApp } = - await import('@agent-device/platform-apple/app-resolution'); - return await findIosSimulatorInstalledApp(device, appTarget); - }; - return Object.freeze({ providerFirst, localOnly, findInstalledApp }); -} - -function createLocalDeviceInventoryGateway( - registry: PlatformModuleRegistry, - loadHost: () => Promise, -): DeviceInventoryGateway { - let hostLoad: Promise | undefined; - const requiredHost = async (): Promise => { - hostLoad ??= loadHost(); - try { - return await hostLoad; - } catch (error) { - hostLoad = undefined; - throw error; - } - }; - const sourceLoads = new Map>(); - const loadSource = async (family: Platform): Promise => { - const existing = sourceLoads.get(family); - if (existing) return await existing; - const pending = loadFamilyInventory(registry.get(family), requiredHost); - sourceLoads.set(family, pending); - try { - return await pending; - } catch (error) { - if (sourceLoads.get(family) === pending) sourceLoads.delete(family); - throw error; - } - }; - - return Object.freeze({ - discover: async (request, scope) => { - scope.signal.throwIfAborted(); - if (request.platform) { - return await discoverFamily( - loadSource, - familyForSelector(request.platform), - request, - scope, - ); - } - - const perFamily = await Promise.all( - LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS.map(async (selector) => { - try { - const devices = await discoverFamily( - loadSource, - familyForSelector(selector), - { ...request, platform: selector }, - scope, - ); - return Array.isArray(devices) ? devices : []; - } catch { - return []; - } - }), - ); - scope.signal.throwIfAborted(); - return perFamily.flat(); - }, - }); -} - -async function loadFamilyInventory( - module: InventoryPlatformModule, - requiredHost: () => Promise, -): Promise { - switch (module.family) { - case 'apple': - return await module.loadInventory(appleInventoryHost(await requiredHost())); - case 'android': - return await module.loadInventory(androidInventoryHost(await requiredHost())); - case 'harmonyos': - return await module.loadInventory(harmonyInventoryHost(await requiredHost())); - case 'vega': - return await module.loadInventory(vegaInventoryHost(await requiredHost())); - case 'linux': - return await module.loadInventory(linuxInventoryHost(await requiredHost())); - case 'web': - return await module.loadInventory(); - } -} - -function appleInventoryHost(host: DeviceInventoryHost): DeviceInventoryHostFor<'apple'> { - return Object.freeze({ - appleTools: host.appleTools, - files: host.files, - hostName: host.hostName, - hostOs: host.hostOs, - observations: host.observations, - }); -} - -function androidInventoryHost(host: DeviceInventoryHost): DeviceInventoryHostFor<'android'> { - return Object.freeze({ - commands: host.commands, - files: host.files, - homeDirectory: host.homeDirectory, - toolchains: host.toolchains, - }); -} - -function harmonyInventoryHost(host: DeviceInventoryHost): DeviceInventoryHostFor<'harmonyos'> { - return Object.freeze({ - commands: host.commands, - files: host.files, - toolchains: host.toolchains, - }); -} - -function vegaInventoryHost(host: DeviceInventoryHost): DeviceInventoryHostFor<'vega'> { - return Object.freeze({ - commands: host.commands, - files: host.files, - homeDirectory: host.homeDirectory, - }); -} - -function linuxInventoryHost(host: DeviceInventoryHost): DeviceInventoryHostFor<'linux'> { - return Object.freeze({ hostName: host.hostName, hostOs: host.hostOs }); -} - -async function discoverFamily( - loadSource: (family: Platform) => Promise, - family: Platform, - request: Parameters[0], - scope: PlatformRequestScope, -): Promise { - const source = await loadSource(family); - scope.signal.throwIfAborted(); - const devices = await source.discover(request, scope); - scope.signal.throwIfAborted(); - assertSourceFamily(family, devices); - return filterDeviceInventoryProjection(devices, request); -} - -function familyForSelector(selector: PlatformSelector): Platform { - return isApplePlatform(selector) ? 'apple' : selector; -} +): Promise { + // Function-scoped: the composition root's eager closure is the hub every daemon start and CLI + // run pays for, so the package composer is evaluated only when an inventory composition runs. + return import('@agent-device/device-selection/composed-inventory-gateways').then( + ({ createComposedDeviceInventoryGateways: compose }) => + compose({ ...options, findInstalledApp: appleInstalledAppProbe }), + ); +} + +// Function-scoped: device selection reads this probe from the request context, and loading it +// must not evaluate Apple app-resolution mechanics until a selection actually narrows. +const appleInstalledAppProbe: InstalledAppProbe = async (device, appTarget) => { + const { findIosSimulatorInstalledApp } = + await import('@agent-device/platform-apple/app-resolution'); + return await findIosSimulatorInstalledApp(device, appTarget); +}; diff --git a/src/platform-runtime.test.ts b/src/platform-runtime.test.ts index 0401324ecd..373be8b3a8 100644 --- a/src/platform-runtime.test.ts +++ b/src/platform-runtime.test.ts @@ -61,7 +61,7 @@ describe('platform module metadata composition', () => { diagnostics: { emit: () => {} }, progress: { report: () => {} }, } as const; - const providerGateways = createPlatformDeviceInventoryGateways({ + const providerGateways = await createPlatformDeviceInventoryGateways({ discover: async () => ({ kind: 'inventory', devices: [ @@ -81,7 +81,7 @@ describe('platform module metadata composition', () => { ); expect(hostAdapter.evaluations).toBe(0); - const localGateways = createPlatformDeviceInventoryGateways(); + const localGateways = await createPlatformDeviceInventoryGateways(); expect(await localGateways.localOnly.discover({ platform: 'web' }, scope)).toHaveLength(1); expect(hostAdapter.evaluations).toBe(0); diff --git a/src/platform-runtime.ts b/src/platform-runtime.ts index 2cf772f07a..b73cdb04cf 100644 --- a/src/platform-runtime.ts +++ b/src/platform-runtime.ts @@ -101,10 +101,10 @@ export const platformModuleRegistry = createPlatformModuleRegistry([ webInventoryModule, ]); -export function createPlatformDeviceInventoryGateways( +export async function createPlatformDeviceInventoryGateways( provider?: ProviderDeviceInventorySource, -): ComposedDeviceInventoryGateways { - return createComposedDeviceInventoryGateways({ +): Promise { + return await createComposedDeviceInventoryGateways({ registry: platformModuleRegistry, loadHost: async () => { const { createDeviceInventoryHost } = await import('./platform-runtime-host.ts'); From b8a1c693de866507c0e39043cd5611f126723e60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Tue, 13 Oct 2026 12:56:12 +0200 Subject: [PATCH 08/14] chore(gates): tighten the fixture seams the review flagged - The relocated archived-capture pin in the affected-lanes test names its real path: packages/platform-android/src/fixtures/android-ime-capture.raw.json. - The two Android test-only fixture subpaths keep export validation but leave the general mechanics-facet consumer allowlist: only non-production files may import them, so production code (src/commands/ style) importing a fixture now trips R13 instead of riding the facet seam. - R71's guidance and owner list follow the caller collector to packages/replay-port/src/; the retired src/commands/replay/ path was deleted by the collector move and left the policy naming a stranded owner. --- scripts/check-affected/device-lanes.test.ts | 2 +- scripts/layering/platform-package-policy.ts | 25 +++++++++++-------- scripts/layering/retired-paths-policy.test.ts | 4 +-- scripts/layering/retired-paths-policy.ts | 2 +- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/scripts/check-affected/device-lanes.test.ts b/scripts/check-affected/device-lanes.test.ts index a49b1d9763..b3f3eb388d 100644 --- a/scripts/check-affected/device-lanes.test.ts +++ b/scripts/check-affected/device-lanes.test.ts @@ -137,7 +137,7 @@ test('unit tests under src/ and packages/*/src own no lane', () => { assert.deepEqual(lanes(file), [], file); } // Non-TypeScript under __tests__ is a fixture the selector cannot place, not a unit test. - assert.ok(!isUnitTest('packages/platform-android/src/android-ime-capture.raw.json')); + assert.ok(!isUnitTest('packages/platform-android/src/fixtures/android-ime-capture.raw.json')); // Package tests outside src/ have no owner and keep failing open. assert.ok(!isUnitTest('packages/maestro/test/conformance/verify.test.ts')); assert.equal( diff --git a/scripts/layering/platform-package-policy.ts b/scripts/layering/platform-package-policy.ts index 32f9de7047..ed5bbb27ca 100644 --- a/scripts/layering/platform-package-policy.ts +++ b/scripts/layering/platform-package-policy.ts @@ -95,19 +95,20 @@ const MECHANICS_FACET_SUBPATHS: Readonly !declaration.exportedSubpaths.includes(subpath)) diff --git a/scripts/layering/retired-paths-policy.test.ts b/scripts/layering/retired-paths-policy.test.ts index 675459bc69..67a1be18ea 100644 --- a/scripts/layering/retired-paths-policy.test.ts +++ b/scripts/layering/retired-paths-policy.test.ts @@ -80,7 +80,7 @@ test('R71 rejects a planted production file under retired src/replay by name', ( file: 'src/replay/planted-production-file.ts', line: 1, message: - 'src/replay/ is retired; caller source acquisition belongs under src/commands/replay/ ' + + 'src/replay/ is retired; caller source acquisition belongs under packages/replay-port/src/ ' + 'and replay-test presentation belongs under src/cli/replay-test/.', }, ]); @@ -95,7 +95,7 @@ test('R71 rejects a planted production file under retired src/replay by name', ( test('R71 accepts the current replay owners and ignores the src/utils zone', () => { assert.deepEqual( retiredPathRuleViolations('R71', [ - 'src/commands/replay/script-source-bundle.ts', + 'packages/replay-port/src/script-source-bundle.ts', 'src/cli/replay-test/reporting.ts', 'src/daemon/replay-script-source.ts', 'src/replay-next.ts', diff --git a/scripts/layering/retired-paths-policy.ts b/scripts/layering/retired-paths-policy.ts index 7bf0a43392..c4bb48bcb1 100644 --- a/scripts/layering/retired-paths-policy.ts +++ b/scripts/layering/retired-paths-policy.ts @@ -30,7 +30,7 @@ export const RETIRED_PATH_RULES: Readonly Date: Tue, 13 Oct 2026 13:16:47 +0200 Subject: [PATCH 09/14] test(gates): pin the Android test-only fixture consumer gate Mirror the mechanics/adb-host facet tests: a test or integration file importing @agent-device/platform-android/snapshot-capture-fixtures passes R13, while the same import from production files (src/commands/ and src/core/interactors/, the latter proving production files ride no fixture seam) is denied. Guards the ANDROID_TEST_ONLY_SUBPATHS predicate added for the review's P2. --- .../layering/platform-package-policy.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/scripts/layering/platform-package-policy.test.ts b/scripts/layering/platform-package-policy.test.ts index d689f6cb34..be84146d4f 100644 --- a/scripts/layering/platform-package-policy.test.ts +++ b/scripts/layering/platform-package-policy.test.ts @@ -430,6 +430,31 @@ test('the Android mechanics facet is the named consumer seam and adb host wiring ); }); +test('the Android test-only fixture subpaths accept test consumers and deny production ones', () => { + const fixtureImport = + "import { createAndroidSnapshotCapture } from '@agent-device/platform-android/snapshot-capture-fixtures';"; + const allowed = validSources(); + allowed.set('src/__tests__/android-ui-hierarchy.test.ts', fixtureImport); + allowed.set('src/daemon/__tests__/android-system-dialog-occlusion.test.ts', fixtureImport); + allowed.set('test/integration/provider-scenarios/android.test.ts', fixtureImport); + assert.deepEqual( + messages(allowed).filter((message) => message.includes('may import')), + [], + ); + + // Production consumers ride no facet seam: importing a fixture is an R13 violation, even + // from a file the mechanics facet would otherwise accept. + for (const file of ['src/commands/replay/index.ts', 'src/core/interactors/android.ts']) { + const denied = validSources(); + denied.set(file, fixtureImport); + assert.match( + messages(denied).join('\n'), + /may import '@agent-device\/platform-android\/snapshot-capture-fixtures'/, + file, + ); + } +}); + test('contracts never depend on a concrete platform package in production or tests', () => { for (const statement of [ "import { applePlatformMetadata } from '@agent-device/platform-apple';", From b2946073cf31406c43d6931833351d55f651ce73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Tue, 13 Oct 2026 19:23:39 +0200 Subject: [PATCH 10/14] refactor(device-selection): return the inventory assertions to private The test fake now composes through the real composer, so composed-inventory-gateways.ts is the only consumer of assertSourceFamily and assertProviderOutcome. They go back as private functions there, and the contracts device facade drops them along with the AppError import that only served the re-export. Answers the review's first simplicity question. --- packages/contracts/src/device-inventory.ts | 36 ------------------- packages/contracts/src/facades/device.ts | 2 -- .../src/composed-inventory-gateways.ts | 30 ++++++++++++++-- 3 files changed, 28 insertions(+), 40 deletions(-) diff --git a/packages/contracts/src/device-inventory.ts b/packages/contracts/src/device-inventory.ts index bd42a622a4..3f9bda99b8 100644 --- a/packages/contracts/src/device-inventory.ts +++ b/packages/contracts/src/device-inventory.ts @@ -5,11 +5,8 @@ import { type DeviceInfo, type DeviceKind, type DeviceTarget, - type Platform, type PlatformSelector, } from '@agent-device/kernel/device'; -import { AppError } from '@agent-device/kernel/errors'; -import type { ProviderDeviceInventorySource } from './device-provider.ts'; export const LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS = [ 'android', @@ -72,39 +69,6 @@ export function countDeviceInventoryByGroup(devices: DeviceInfo[]): DeviceInvent return counts; } -/** - * The composition-level contract every device-inventory source must honor: a family's - * source only ever reports devices of its own family. Both the production composer and - * the test fake enforce this through one implementation. - */ -export function assertSourceFamily(family: Platform, devices: readonly DeviceInfo[]): void { - const outside = devices.find((device) => device.platform !== family); - if (!outside) return; - throw new AppError( - 'COMMAND_FAILED', - `Platform inventory source ${family} returned a ${outside.platform} device.`, - { expectedFamily: family, actualFamily: outside.platform, deviceId: outside.id }, - ); -} - -/** - * Validates a raw provider discover result before the composer trusts it. A malformed - * outcome (not a declined/inventory tagged object) is a provider contract violation, not - * a local-inventory fallback. - */ -export function assertProviderOutcome( - value: unknown, -): asserts value is Awaited> { - if (value !== null && typeof value === 'object') { - const outcome = value as { kind?: unknown; devices?: unknown }; - if (outcome.kind === 'declined' && !('devices' in outcome)) return; - if (outcome.kind === 'inventory' && Array.isArray(outcome.devices)) return; - } - throw new AppError('COMMAND_FAILED', 'Provider device inventory returned an invalid outcome.', { - reason: 'provider_inventory_contract_violation', - }); -} - export function filterDeviceInventoryProjection( devices: readonly DeviceInfo[], request: Pick, diff --git a/packages/contracts/src/facades/device.ts b/packages/contracts/src/facades/device.ts index b78fdba9dd..5df44feb85 100644 --- a/packages/contracts/src/facades/device.ts +++ b/packages/contracts/src/facades/device.ts @@ -5,8 +5,6 @@ export type { AppStateCommandResult } from '../app-state.ts'; export { LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS, WEB_DESKTOP_DEVICE, - assertProviderOutcome, - assertSourceFamily, countDeviceInventoryByGroup, filterDeviceInventoryProjection, isDeviceClaimConflictReason, diff --git a/packages/device-selection/src/composed-inventory-gateways.ts b/packages/device-selection/src/composed-inventory-gateways.ts index 9332db58e1..96b4d2237b 100644 --- a/packages/device-selection/src/composed-inventory-gateways.ts +++ b/packages/device-selection/src/composed-inventory-gateways.ts @@ -1,6 +1,4 @@ import { - assertProviderOutcome, - assertSourceFamily, filterDeviceInventoryProjection, LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS, projectProviderDeviceInventoryRequest, @@ -26,6 +24,7 @@ import { type Platform, type PlatformSelector, } from '@agent-device/kernel/device'; +import { AppError } from '@agent-device/kernel/errors'; export function createComposedDeviceInventoryGateways( options: Readonly<{ @@ -207,6 +206,33 @@ async function discoverFamily( return filterDeviceInventoryProjection(devices, request); } +// The composition-level contract every device-inventory source must honor: a family's +// source only ever reports devices of its own family. +function assertSourceFamily(family: Platform, devices: readonly DeviceInfo[]): void { + const outside = devices.find((device) => device.platform !== family); + if (!outside) return; + throw new AppError( + 'COMMAND_FAILED', + `Platform inventory source ${family} returned a ${outside.platform} device.`, + { expectedFamily: family, actualFamily: outside.platform, deviceId: outside.id }, + ); +} + +// A malformed provider outcome (not a declined/inventory tagged object) is a provider +// contract violation, not a local-inventory fallback. +function assertProviderOutcome( + value: unknown, +): asserts value is Awaited> { + if (value !== null && typeof value === 'object') { + const outcome = value as { kind?: unknown; devices?: unknown }; + if (outcome.kind === 'declined' && !('devices' in outcome)) return; + if (outcome.kind === 'inventory' && Array.isArray(outcome.devices)) return; + } + throw new AppError('COMMAND_FAILED', 'Provider device inventory returned an invalid outcome.', { + reason: 'provider_inventory_contract_violation', + }); +} + function familyForSelector(selector: PlatformSelector): Platform { return isApplePlatform(selector) ? 'apple' : selector; } From f1d194f818ab76a84f4d6d0632268bfeab38ce69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Tue, 13 Oct 2026 19:23:39 +0200 Subject: [PATCH 11/14] refactor(root): name the inventory wrapper for the probe it attaches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit createComposedDeviceInventoryGateways at the root shared its name with the sync package composer it wraps while being an async wrapper that attaches the Apple installed-app probe; it is now attachAppleInstalledAppProbe and the where-the-flow-moved narration is cut to one line naming the probe. Folding it into platform-runtime.ts was evaluated and rejected: the composition import allowlist is category-based (contracts, platform facades, host adapters), so the fold would widen it for one dynamic import of the package composer — a looser gate than the wrapper file the zones and composition policy already carry. --- scripts/layering/platform-package-policy.test.ts | 4 ++-- src/platform-runtime-device-inventory.test.ts | 4 ++-- src/platform-runtime-device-inventory.ts | 14 +++++--------- src/platform-runtime.ts | 4 ++-- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/scripts/layering/platform-package-policy.test.ts b/scripts/layering/platform-package-policy.test.ts index be84146d4f..fb4c1270ee 100644 --- a/scripts/layering/platform-package-policy.test.ts +++ b/scripts/layering/platform-package-policy.test.ts @@ -591,12 +591,12 @@ test('composition imports are category-based for future contracts and host adapt "import { createAndroidObservationHost } from './platform-runtime-android-observation-host.ts';", "import { createPlatformRuntimeHost } from './platform-runtime-operation-host.ts';", "import { hostCommandRunner } from './platform-runtime-host/command.ts';", - "import { createComposedDeviceInventoryGateways } from './platform-runtime-device-inventory.ts';", + "import { attachAppleInstalledAppProbe } from './platform-runtime-device-inventory.ts';", 'void hostCommandRunner;', 'void createComposedPlatformRuntimeGateway;', 'void createAndroidObservationHost;', 'void createPlatformRuntimeHost;', - 'void createComposedDeviceInventoryGateways;', + 'void attachAppleInstalledAppProbe;', 'export type Scope = PlatformRequestScope;', ].join('\n'), ); diff --git a/src/platform-runtime-device-inventory.test.ts b/src/platform-runtime-device-inventory.test.ts index 752843db69..41119b45bd 100644 --- a/src/platform-runtime-device-inventory.test.ts +++ b/src/platform-runtime-device-inventory.test.ts @@ -11,7 +11,7 @@ import { resolveTargetDeviceSelection } from '@agent-device/device-selection/dis import { withDeviceInventoryContext } from '@agent-device/device-selection/device-inventory-context'; import { PLATFORMS, type DeviceInfo } from '@agent-device/kernel/device'; import { describe, expect, test, vi } from 'vitest'; -import { createComposedDeviceInventoryGateways } from './platform-runtime-device-inventory.ts'; +import { attachAppleInstalledAppProbe } from './platform-runtime-device-inventory.ts'; const simctl = vi.hoisted(() => ({ listapps: [] as Array<{ deviceId: string; apps: Record }>, @@ -90,7 +90,7 @@ describe('production device inventory composition', () => { loadInventory: async () => source(family === 'apple' ? [simulator('sim-a'), simulator('sim-b')] : []), })); - const gateways = await createComposedDeviceInventoryGateways({ + const gateways = await attachAppleInstalledAppProbe({ registry: createPlatformModuleRegistry(modules), loadHost: async () => host, }); diff --git a/src/platform-runtime-device-inventory.ts b/src/platform-runtime-device-inventory.ts index 48ae39ef83..5b67320975 100644 --- a/src/platform-runtime-device-inventory.ts +++ b/src/platform-runtime-device-inventory.ts @@ -7,11 +7,8 @@ import type { import type { DeviceInventoryHost } from '@agent-device/contracts/platform-runtime-host'; import type { ProviderDeviceInventorySource } from '@agent-device/contracts/device'; -// The composition flow lives in @agent-device/device-selection/composed-inventory-gateways, so -// the test fake there builds on the real one. This module is the production wiring: it attaches -// the Apple simulator app probe the host owns. - -export function createComposedDeviceInventoryGateways( +// Composes the package-level gateways and attaches the Apple installed-app probe this host owns. +export async function attachAppleInstalledAppProbe( options: Readonly<{ registry: PlatformModuleRegistry; loadHost: () => Promise; @@ -20,10 +17,9 @@ export function createComposedDeviceInventoryGateways( ): Promise { // Function-scoped: the composition root's eager closure is the hub every daemon start and CLI // run pays for, so the package composer is evaluated only when an inventory composition runs. - return import('@agent-device/device-selection/composed-inventory-gateways').then( - ({ createComposedDeviceInventoryGateways: compose }) => - compose({ ...options, findInstalledApp: appleInstalledAppProbe }), - ); + const { createComposedDeviceInventoryGateways: compose } = + await import('@agent-device/device-selection/composed-inventory-gateways'); + return compose({ ...options, findInstalledApp: appleInstalledAppProbe }); } // Function-scoped: device selection reads this probe from the request context, and loading it diff --git a/src/platform-runtime.ts b/src/platform-runtime.ts index b73cdb04cf..d58deb05a0 100644 --- a/src/platform-runtime.ts +++ b/src/platform-runtime.ts @@ -53,7 +53,7 @@ import { createComposedPlatformRuntimeGateway, type PlatformRuntimeProviderRegistration, } from './platform-runtime-gateway.ts'; -import { createComposedDeviceInventoryGateways } from './platform-runtime-device-inventory.ts'; +import { attachAppleInstalledAppProbe } from './platform-runtime-device-inventory.ts'; import { createAndroidObservationHost } from './platform-runtime-android-observation-host.ts'; import type { RequestPlatformProviderOptions } from './platform-runtime/request-providers.ts'; @@ -104,7 +104,7 @@ export const platformModuleRegistry = createPlatformModuleRegistry([ export async function createPlatformDeviceInventoryGateways( provider?: ProviderDeviceInventorySource, ): Promise { - return await createComposedDeviceInventoryGateways({ + return await attachAppleInstalledAppProbe({ registry: platformModuleRegistry, loadHost: async () => { const { createDeviceInventoryHost } = await import('./platform-runtime-host.ts'); From b1a794d9778db7146432999df529a9c4ee1e9d61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Tue, 13 Oct 2026 19:23:39 +0200 Subject: [PATCH 12/14] chore(gates): pin the test-only fixture subpath consumers at R11 The new device-selection/inventory-gateways-fixtures and replay-port/replay-script-source-fixtures subpaths belong to non-platform packages, so R13's ANDROID_TEST_ONLY_SUBPATHS predicate cannot reach them; TEST_ONLY_FIXTURE_SUBPATHS makes the same non-production-consumer claim at R11, in both the package-internal and root scans, and a planted production import in src/daemon/ was confirmed to fire before it was removed. Mirrors the Android fixture accept/deny test. --- scripts/layering/package-boundaries.test.ts | 34 +++++++++++++++++ scripts/layering/package-boundaries.ts | 42 ++++++++++++++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/scripts/layering/package-boundaries.test.ts b/scripts/layering/package-boundaries.test.ts index 981f1c4b2b..513e6afd21 100644 --- a/scripts/layering/package-boundaries.test.ts +++ b/scripts/layering/package-boundaries.test.ts @@ -19,6 +19,7 @@ import { rootExternalDependencyRanges, rootWorkspaceDependencyNames, specifierSites, + TEST_ONLY_FIXTURE_SUBPATHS, workspacePackagesFromManifests, type WorkspacePackage, } from './package-boundaries.ts'; @@ -325,6 +326,39 @@ test('a cross-package import needs a workspace:* declaration; the exports map is assert.equal(checkPackageInternalSites(contracts, deep, ALL, true).length, 0); }); +test('the test-only fixture subpaths accept test consumers and deny production ones', () => { + // A consumer package declaring both fixture owners, so only the fixture predicate can fire. + const consumer: WorkspacePackage = { + dir: 'packages/daemon-side', + name: '@agent-device/daemon-side', + exportTargets: new Map(), + workspaceDependencies: new Set(['@agent-device/device-selection', '@agent-device/replay-port']), + workspaceDependencyField: 'dependencies', + externalDependencies: new Map(), + }; + const rootDeps = new Set(consumer.workspaceDependencies); + for (const specifier of TEST_ONLY_FIXTURE_SUBPATHS) { + const sites = (file: string) => specifierSites(file, `import { f } from '${specifier}';`); + for (const file of [ + 'src/daemon/__tests__/consumer.test.ts', + 'test/integration/consumer.ts', + 'packages/daemon-side/src/__tests__/consumer.test.ts', + ]) { + assert.deepEqual(checkRootSites(sites(file), ALL, rootDeps, true), [], file); + assert.deepEqual(checkPackageInternalSites(consumer, sites(file), ALL, true), [], file); + } + // Production consumers ride no seam: the same import from a root or package + // production file is an R11 violation in both scans. + for (const file of ['src/commands/inventory.ts', 'packages/daemon-side/src/inventory.ts']) { + const violations = file.startsWith('src/') + ? checkRootSites(sites(file), ALL, rootDeps, true) + : checkPackageInternalSites(consumer, sites(file), ALL, true); + assert.equal(violations.length, 1, file); + assert.match(violations[0]!.message, /test-only fixture subpath/); + } + } +}); + test('a root src file tunnelling into packages/*/src relatively is a violation', () => { // This `src/utils` path is an in-memory arbitrary parser fixture, not a live repository path. // It stays R11's own for every root file, compiled or not: #3279 planted the tunnel inside the diff --git a/scripts/layering/package-boundaries.ts b/scripts/layering/package-boundaries.ts index 6a5721f9f1..f9fbcf611a 100644 --- a/scripts/layering/package-boundaries.ts +++ b/scripts/layering/package-boundaries.ts @@ -8,7 +8,7 @@ // Evidence: 76453add71 (#1494, #1490 W0) established the workspace split this rule protects; // 83322a3f2f (#1574) pinned exact facade symbols for every workspace package; #3279 moved the // type-check onto `tsc -b` project references and retired the checks it now enforces. -// Cost: 447 LOC (rule) + 978 LOC (test). +// Cost: 510 LOC (rule) + 998 LOC (test). // Kill criterion: retire a remaining branch only when something owns its mechanism, not merely // its symptom. An undeclared `workspace:*` sibling is type-clean and runtime-green INSIDE // this repo because pnpm links every workspace package under the root `node_modules` — the @@ -25,6 +25,8 @@ // and via its package specifier instantiates twice in one process — duplicate AppError, broken // instanceof), and, for `scripts/` files which sit outside the type graph by design, the // root-side unknown-package and exports-map checks that compiled files get from TS2307. +// On top, `TEST_ONLY_FIXTURE_SUBPATHS` mirrors R13's Android fixture gate for the non-platform +// packages: a production file importing one of those fixture subpaths is an R11 violation. import fs from 'node:fs'; import path from 'node:path'; @@ -169,6 +171,24 @@ export function insideCompiledSources(file: string): boolean { return /^packages\/[^/]+\/src\//.test(file) || /^src\//.test(file) || /^test\//.test(file); } +/** + * Workspace fixture subpaths only non-production files may import — the same claim R13's + * `ANDROID_TEST_ONLY_SUBPATHS` makes for the platform packages, which R13 cannot express for + * non-platform packages. A production file (root or package) importing one is a violation. + */ +export const TEST_ONLY_FIXTURE_SUBPATHS: ReadonlySet = new Set([ + '@agent-device/device-selection/inventory-gateways-fixtures', + '@agent-device/replay-port/replay-script-source-fixtures', +]); + +/** Consumer side of `TEST_ONLY_FIXTURE_SUBPATHS`, mirroring R13's non-production predicate. */ +function importsTestOnlyFixtureFromProduction(file: string, specifier: string): boolean { + if (!TEST_ONLY_FIXTURE_SUBPATHS.has(specifier)) return false; + const isTestConsumer = + file.startsWith('test/') || file.endsWith('.test.ts') || file.includes('/__tests__/'); + return !isTestConsumer; +} + /** * Rules for files INSIDE a package. An undeclared sibling import is R11's alone: pnpm links * every workspace package under the root `node_modules`, so the import resolves, compiles, and @@ -217,6 +237,16 @@ export function checkPackageInternalSites( } const name = specifierPackageName(site.specifier); if (!name || !name.startsWith('@agent-device/')) continue; + if (importsTestOnlyFixtureFromProduction(site.file, site.specifier)) { + violations.push({ + rule: 'R11 package-boundaries', + file: site.file, + line: site.line, + message: + `'${site.specifier}' is a test-only fixture subpath — production code may not ` + + `import it; use the package's production subpath instead.`, + }); + } const target = packageByName(allPackages, name); if (!target) { if (!compiled) { @@ -288,6 +318,16 @@ export function checkRootSites( } const name = specifierPackageName(site.specifier); if (!name || !name.startsWith('@agent-device/')) continue; + if (importsTestOnlyFixtureFromProduction(site.file, site.specifier)) { + violations.push({ + rule: 'R11 package-boundaries', + file: site.file, + line: site.line, + message: + `'${site.specifier}' is a test-only fixture subpath — production code may not ` + + `import it; use the package's production subpath instead.`, + }); + } const target = packageByName(packages, name); if (!target) { if (!compiled) { From 6b4b1d98634c5fd855c496e3b77aa72466727673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Tue, 13 Oct 2026 19:40:52 +0200 Subject: [PATCH 13/14] chore(gates): harden the test-only fixture subpath pin - The R11 accept/deny test now pins TEST_ONLY_FIXTURE_SUBPATHS against an independent expectation before iterating it, so deleting an entry from the checker's set can no longer take its production-denial coverage with it. - The non-production consumer predicate recognizes package-local test trees (packages/*/test/), not just the root test/ directory: a package harness importing a fixture subpath is a test consumer, matching how R13's file vocabulary treats harness trees. Pin extends the accept list accordingly. --- scripts/layering/package-boundaries.test.ts | 7 +++++++ scripts/layering/package-boundaries.ts | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/scripts/layering/package-boundaries.test.ts b/scripts/layering/package-boundaries.test.ts index 513e6afd21..562246c7fb 100644 --- a/scripts/layering/package-boundaries.test.ts +++ b/scripts/layering/package-boundaries.test.ts @@ -337,12 +337,19 @@ test('the test-only fixture subpaths accept test consumers and deny production o externalDependencies: new Map(), }; const rootDeps = new Set(consumer.workspaceDependencies); + // Pin the set independently: iterating the checker's own set would let deleting an entry + // silently delete its accept/deny coverage with it. + assert.deepEqual([...TEST_ONLY_FIXTURE_SUBPATHS].sort(), [ + '@agent-device/device-selection/inventory-gateways-fixtures', + '@agent-device/replay-port/replay-script-source-fixtures', + ]); for (const specifier of TEST_ONLY_FIXTURE_SUBPATHS) { const sites = (file: string) => specifierSites(file, `import { f } from '${specifier}';`); for (const file of [ 'src/daemon/__tests__/consumer.test.ts', 'test/integration/consumer.ts', 'packages/daemon-side/src/__tests__/consumer.test.ts', + 'packages/daemon-side/test/harness.ts', ]) { assert.deepEqual(checkRootSites(sites(file), ALL, rootDeps, true), [], file); assert.deepEqual(checkPackageInternalSites(consumer, sites(file), ALL, true), [], file); diff --git a/scripts/layering/package-boundaries.ts b/scripts/layering/package-boundaries.ts index f9fbcf611a..2ee38883b3 100644 --- a/scripts/layering/package-boundaries.ts +++ b/scripts/layering/package-boundaries.ts @@ -184,8 +184,10 @@ export const TEST_ONLY_FIXTURE_SUBPATHS: ReadonlySet = new Set([ /** Consumer side of `TEST_ONLY_FIXTURE_SUBPATHS`, mirroring R13's non-production predicate. */ function importsTestOnlyFixtureFromProduction(file: string, specifier: string): boolean { if (!TEST_ONLY_FIXTURE_SUBPATHS.has(specifier)) return false; + // Root `test/`, package-local `packages/*/test/`, *.test.ts, and __tests__ trees are all + // test consumers; anything else is production. const isTestConsumer = - file.startsWith('test/') || file.endsWith('.test.ts') || file.includes('/__tests__/'); + /(?:^|\/)test\//.test(file) || file.endsWith('.test.ts') || file.includes('/__tests__/'); return !isTestConsumer; } From 1ae90cfd8ed7e88cde576350f2abd78b779f491e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Tue, 13 Oct 2026 21:43:28 +0200 Subject: [PATCH 14/14] test(gates): move the fixture-subpath pins to their own test module package-boundaries.test.ts is over the 1,000-line tripwire (961 at the merge-base), so the size ratchet rejects the consumer-gate pins these branches add to it. Tests mirror source topology: the test-only fixture subpath inventory (R11's TEST_ONLY_FIXTURE_SUBPATHS accept/deny gate and the platform-android manifest's fixture export entries, both owned by package-boundaries.ts) moves with its rule into scripts/layering/test-only-fixture-subpaths.test.ts, mirroring the R13-side pin in platform-package-policy.test.ts. Move-only: the accept/ deny test is byte-identical and the manifest assertion keeps its full sorted key list; it reads the tree itself instead of sharing the real- tree test's package list. package-boundaries.test.ts ends at 955 lines, under its base length. --- scripts/layering/package-boundaries.test.ts | 50 -------- .../test-only-fixture-subpaths.test.ts | 107 ++++++++++++++++++ 2 files changed, 107 insertions(+), 50 deletions(-) create mode 100644 scripts/layering/test-only-fixture-subpaths.test.ts diff --git a/scripts/layering/package-boundaries.test.ts b/scripts/layering/package-boundaries.test.ts index 562246c7fb..87ff27dfe7 100644 --- a/scripts/layering/package-boundaries.test.ts +++ b/scripts/layering/package-boundaries.test.ts @@ -19,7 +19,6 @@ import { rootExternalDependencyRanges, rootWorkspaceDependencyNames, specifierSites, - TEST_ONLY_FIXTURE_SUBPATHS, workspacePackagesFromManifests, type WorkspacePackage, } from './package-boundaries.ts'; @@ -326,46 +325,6 @@ test('a cross-package import needs a workspace:* declaration; the exports map is assert.equal(checkPackageInternalSites(contracts, deep, ALL, true).length, 0); }); -test('the test-only fixture subpaths accept test consumers and deny production ones', () => { - // A consumer package declaring both fixture owners, so only the fixture predicate can fire. - const consumer: WorkspacePackage = { - dir: 'packages/daemon-side', - name: '@agent-device/daemon-side', - exportTargets: new Map(), - workspaceDependencies: new Set(['@agent-device/device-selection', '@agent-device/replay-port']), - workspaceDependencyField: 'dependencies', - externalDependencies: new Map(), - }; - const rootDeps = new Set(consumer.workspaceDependencies); - // Pin the set independently: iterating the checker's own set would let deleting an entry - // silently delete its accept/deny coverage with it. - assert.deepEqual([...TEST_ONLY_FIXTURE_SUBPATHS].sort(), [ - '@agent-device/device-selection/inventory-gateways-fixtures', - '@agent-device/replay-port/replay-script-source-fixtures', - ]); - for (const specifier of TEST_ONLY_FIXTURE_SUBPATHS) { - const sites = (file: string) => specifierSites(file, `import { f } from '${specifier}';`); - for (const file of [ - 'src/daemon/__tests__/consumer.test.ts', - 'test/integration/consumer.ts', - 'packages/daemon-side/src/__tests__/consumer.test.ts', - 'packages/daemon-side/test/harness.ts', - ]) { - assert.deepEqual(checkRootSites(sites(file), ALL, rootDeps, true), [], file); - assert.deepEqual(checkPackageInternalSites(consumer, sites(file), ALL, true), [], file); - } - // Production consumers ride no seam: the same import from a root or package - // production file is an R11 violation in both scans. - for (const file of ['src/commands/inventory.ts', 'packages/daemon-side/src/inventory.ts']) { - const violations = file.startsWith('src/') - ? checkRootSites(sites(file), ALL, rootDeps, true) - : checkPackageInternalSites(consumer, sites(file), ALL, true); - assert.equal(violations.length, 1, file); - assert.match(violations[0]!.message, /test-only fixture subpath/); - } - } -}); - test('a root src file tunnelling into packages/*/src relatively is a violation', () => { // This `src/utils` path is an in-memory arbitrary parser fixture, not a live repository path. // It stays R11's own for every root file, compiled or not: #3279 planted the tunnel inside the @@ -658,15 +617,6 @@ test('the real tree parses, declares, and passes R11', () => { (pkg) => pkg.name === '@agent-device/platform-android', ); assert.ok(platformAndroidPackage, 'platform-android package must exist'); - assert.deepEqual([...platformAndroidPackage.exportTargets.keys()].sort(), [ - '@agent-device/platform-android', - '@agent-device/platform-android/adb-host', - '@agent-device/platform-android/device-boot', - '@agent-device/platform-android/mechanics', - // Test-only entries: the shared Android capture/walk fixtures (#3517). - '@agent-device/platform-android/snapshot-capture-fixtures', - '@agent-device/platform-android/ui-hierarchy-fixtures', - ]); assert.deepEqual([...platformAndroidPackage.workspaceDependencies].sort(), [ '@agent-device/capture-kit', '@agent-device/contracts', diff --git a/scripts/layering/test-only-fixture-subpaths.test.ts b/scripts/layering/test-only-fixture-subpaths.test.ts new file mode 100644 index 0000000000..ae08b061f4 --- /dev/null +++ b/scripts/layering/test-only-fixture-subpaths.test.ts @@ -0,0 +1,107 @@ +// The workspace packages' test-only fixture subpaths: which subpaths the manifests expose as +// test-only, and which consumers R11 may import them. `TEST_ONLY_FIXTURE_SUBPATHS` in +// package-boundaries.ts makes the non-production-consumer claim for the non-platform packages +// R13 cannot reach; the platform fixture entries and their R13 predicate are pinned in +// platform-package-policy.test.ts. Split out of package-boundaries.test.ts when the R11 family +// crossed the test-size tripwire, move-only (the facade-surface.test.ts precedent); the +// platform-android manifest pin moved with them, so this file holds the whole fixture-subpath +// inventory question in one place. + +import assert from 'node:assert/strict'; +import path from 'node:path'; +import { test } from 'node:test'; +import { + checkPackageInternalSites, + checkRootSites, + readWorkspacePackages, + specifierSites, + TEST_ONLY_FIXTURE_SUBPATHS, + type WorkspacePackage, +} from './package-boundaries.ts'; + +const repoRoot = path.resolve(import.meta.dirname, '../..'); + +const kernel: WorkspacePackage = { + dir: 'packages/kernel', + name: '@agent-device/kernel', + exportTargets: new Map([ + ['@agent-device/kernel/errors', 'packages/kernel/src/errors.ts'], + ['@agent-device/kernel/device', 'packages/kernel/src/device.ts'], + ]), + workspaceDependencies: new Set(), + workspaceDependencyField: 'dependencies', + externalDependencies: new Map(), +}; + +const contracts: WorkspacePackage = { + dir: 'packages/contracts', + name: '@agent-device/contracts', + exportTargets: new Map([ + ['@agent-device/contracts/interaction', 'packages/contracts/src/interaction.ts'], + ]), + workspaceDependencies: new Set(['@agent-device/kernel']), + workspaceDependencyField: 'dependencies', + externalDependencies: new Map(), +}; + +const ALL = [kernel, contracts]; + +test('the test-only fixture subpaths accept test consumers and deny production ones', () => { + // A consumer package declaring both fixture owners, so only the fixture predicate can fire. + const consumer: WorkspacePackage = { + dir: 'packages/daemon-side', + name: '@agent-device/daemon-side', + exportTargets: new Map(), + workspaceDependencies: new Set(['@agent-device/device-selection', '@agent-device/replay-port']), + workspaceDependencyField: 'dependencies', + externalDependencies: new Map(), + }; + const rootDeps = new Set(consumer.workspaceDependencies); + // Pin the set independently: iterating the checker's own set would let deleting an entry + // silently delete its accept/deny coverage with it. + assert.deepEqual([...TEST_ONLY_FIXTURE_SUBPATHS].sort(), [ + '@agent-device/device-selection/inventory-gateways-fixtures', + '@agent-device/replay-port/replay-script-source-fixtures', + ]); + for (const specifier of TEST_ONLY_FIXTURE_SUBPATHS) { + const sites = (file: string) => specifierSites(file, `import { f } from '${specifier}';`); + for (const file of [ + 'src/daemon/__tests__/consumer.test.ts', + 'test/integration/consumer.ts', + 'packages/daemon-side/src/__tests__/consumer.test.ts', + 'packages/daemon-side/test/harness.ts', + ]) { + assert.deepEqual(checkRootSites(sites(file), ALL, rootDeps, true), [], file); + assert.deepEqual(checkPackageInternalSites(consumer, sites(file), ALL, true), [], file); + } + // Production consumers ride no seam: the same import from a root or package + // production file is an R11 violation in both scans. + for (const file of ['src/commands/inventory.ts', 'packages/daemon-side/src/inventory.ts']) { + const violations = file.startsWith('src/') + ? checkRootSites(sites(file), ALL, rootDeps, true) + : checkPackageInternalSites(consumer, sites(file), ALL, true); + assert.equal(violations.length, 1, file); + assert.match(violations[0]!.message, /test-only fixture subpath/); + } + } +}); + +test('the platform-android manifest names its test-only fixture subpaths', () => { + // The shared Android capture/walk fixtures (#3517). R13 denies production consumers + // (platform-package-policy.test.ts); their place in the manifest's export list is the + // R11-side half of the same inventory, pinned here with the full key list rather than + // growing the R11 real-tree test past the size tripwire. + const platformAndroidPackage = readWorkspacePackages(repoRoot).find( + (pkg) => pkg.name === '@agent-device/platform-android', + ); + assert.ok(platformAndroidPackage, 'platform-android package must exist'); + assert.deepEqual([...platformAndroidPackage.exportTargets.keys()].sort(), [ + '@agent-device/platform-android', + '@agent-device/platform-android/adb-host', + '@agent-device/platform-android/device-boot', + '@agent-device/platform-android/mechanics', + // Test-only entries: the shared Android capture/walk fixtures (#3517). + '@agent-device/platform-android/snapshot-capture-fixtures', + '@agent-device/platform-android/ui-hierarchy-fixtures', + ]); +});