diff --git a/packages/platform-harmonyos/package.json b/packages/platform-harmonyos/package.json index 7416b8fd2d..71bbf78c78 100644 --- a/packages/platform-harmonyos/package.json +++ b/packages/platform-harmonyos/package.json @@ -15,6 +15,10 @@ ".": { "types": "./src/index.ts", "default": "./src/index.ts" + }, + "./interactor": { + "types": "./src/interactor-facade.ts", + "default": "./src/interactor-facade.ts" } } } diff --git a/packages/platform-harmonyos/src/interactor-facade.ts b/packages/platform-harmonyos/src/interactor-facade.ts new file mode 100644 index 0000000000..4aeee9f729 --- /dev/null +++ b/packages/platform-harmonyos/src/interactor-facade.ts @@ -0,0 +1,10 @@ +import type { DeviceInfo } from '@agent-device/kernel/device'; +import type { Interactor, RunnerContext } from '@agent-device/contracts/interactor-types'; + +export async function createHarmonyInteractor( + device: DeviceInfo, + runner?: RunnerContext, +): Promise { + const { createHarmonyInteractor: create } = await import('./interactor.ts'); + return create(device, runner); +} diff --git a/src/core/__tests__/harmonyos-interactor.test.ts b/packages/platform-harmonyos/src/interactor.test.ts similarity index 93% rename from src/core/__tests__/harmonyos-interactor.test.ts rename to packages/platform-harmonyos/src/interactor.test.ts index 3b4a407d62..09a21e84ec 100644 --- a/src/core/__tests__/harmonyos-interactor.test.ts +++ b/packages/platform-harmonyos/src/interactor.test.ts @@ -9,7 +9,7 @@ const { pressHarmonyKeyboardKey, snapshotHarmony } = vi.hoisted(() => ({ })), })); -vi.mock('@agent-device/platform-harmonyos', () => ({ +vi.mock('./index.ts', () => ({ appSwitcherHarmony: vi.fn(), backHarmony: vi.fn(), closeHarmonyApp: vi.fn(), @@ -30,7 +30,7 @@ vi.mock('@agent-device/platform-harmonyos', () => ({ typeHarmony: vi.fn(), })); -import { createHarmonyInteractor } from '../interactors/harmonyos.ts'; +import { createHarmonyInteractor } from './interactor.ts'; const device: DeviceInfo = { platform: 'harmonyos', diff --git a/src/core/interactors/harmonyos.ts b/packages/platform-harmonyos/src/interactor.ts similarity index 98% rename from src/core/interactors/harmonyos.ts rename to packages/platform-harmonyos/src/interactor.ts index 1c163ab313..698ef7ca80 100644 --- a/src/core/interactors/harmonyos.ts +++ b/packages/platform-harmonyos/src/interactor.ts @@ -21,7 +21,7 @@ import { setHarmonySetting, snapshotHarmony, typeHarmony, -} from '@agent-device/platform-harmonyos'; +} from './index.ts'; export function createHarmonyInteractor(device: DeviceInfo, _runner?: RunnerContext): Interactor { const unsupported = (name: string) => async (): Promise => { diff --git a/packages/platform-linux/package.json b/packages/platform-linux/package.json index e82b51f04e..283d45b657 100644 --- a/packages/platform-linux/package.json +++ b/packages/platform-linux/package.json @@ -14,6 +14,10 @@ ".": { "types": "./src/index.ts", "default": "./src/index.ts" + }, + "./interactor": { + "types": "./src/interactor-facade.ts", + "default": "./src/interactor-facade.ts" } } } diff --git a/packages/platform-linux/src/interactor-facade.ts b/packages/platform-linux/src/interactor-facade.ts new file mode 100644 index 0000000000..c873d59ef0 --- /dev/null +++ b/packages/platform-linux/src/interactor-facade.ts @@ -0,0 +1,6 @@ +import type { Interactor } from '@agent-device/contracts/interactor-types'; + +export async function createLinuxInteractor(): Promise { + const { createLinuxInteractor: create } = await import('./interactor.ts'); + return create(); +} diff --git a/src/core/interactors/linux.test.ts b/packages/platform-linux/src/interactor.test.ts similarity index 85% rename from src/core/interactors/linux.test.ts rename to packages/platform-linux/src/interactor.test.ts index 33e76bd302..2ac31104a3 100644 --- a/src/core/interactors/linux.test.ts +++ b/packages/platform-linux/src/interactor.test.ts @@ -1,9 +1,9 @@ import assert from 'node:assert/strict'; import { beforeEach, test, vi } from 'vitest'; -import { createLinuxInteractor } from './linux.ts'; -import { middleClickLinux, pressLinux, rightClickLinux } from '@agent-device/platform-linux'; +import { createLinuxInteractor } from './interactor.ts'; +import { middleClickLinux, pressLinux, rightClickLinux } from './index.ts'; -vi.mock('@agent-device/platform-linux', () => ({ +vi.mock('./index.ts', () => ({ backLinux: vi.fn(), captureLinuxSurfaceSnapshot: vi.fn(), closeLinuxApp: vi.fn(), diff --git a/src/core/interactors/linux.ts b/packages/platform-linux/src/interactor.ts similarity index 98% rename from src/core/interactors/linux.ts rename to packages/platform-linux/src/interactor.ts index e541ab9352..ef5790bd72 100644 --- a/src/core/interactors/linux.ts +++ b/packages/platform-linux/src/interactor.ts @@ -20,7 +20,7 @@ import { swipeLinux, typeLinux, writeLinuxClipboard, -} from '@agent-device/platform-linux'; +} from './index.ts'; import { singlePointerPlanEndpoints } from '@agent-device/contracts/gesture-plan'; import type { Interactor } from '@agent-device/contracts/interactor-types'; diff --git a/packages/platform-web/package.json b/packages/platform-web/package.json index 0cf32b2dd6..d1ff4ac9d3 100644 --- a/packages/platform-web/package.json +++ b/packages/platform-web/package.json @@ -15,6 +15,10 @@ ".": { "types": "./src/index.ts", "default": "./src/index.ts" + }, + "./interactor": { + "types": "./src/interactor-facade.ts", + "default": "./src/interactor-facade.ts" } }, "devDependencies": { diff --git a/src/core/interactors/__tests__/unsupported-interactor.test.ts b/packages/platform-web/src/__tests__/unsupported-interactor.test.ts similarity index 100% rename from src/core/interactors/__tests__/unsupported-interactor.test.ts rename to packages/platform-web/src/__tests__/unsupported-interactor.test.ts diff --git a/packages/platform-web/src/interactor-facade.ts b/packages/platform-web/src/interactor-facade.ts new file mode 100644 index 0000000000..388de123e6 --- /dev/null +++ b/packages/platform-web/src/interactor-facade.ts @@ -0,0 +1,7 @@ +import type { Interactor } from '@agent-device/contracts/interactor-types'; +import type { WebProvider } from './provider.ts'; + +export async function createWebInteractor(provider?: WebProvider): Promise { + const { createWebInteractor: create } = await import('./interactor.ts'); + return await create(provider); +} diff --git a/src/core/__tests__/web-interactor.test.ts b/packages/platform-web/src/interactor.test.ts similarity index 96% rename from src/core/__tests__/web-interactor.test.ts rename to packages/platform-web/src/interactor.test.ts index 82c35feaeb..9fb26f5aaf 100644 --- a/src/core/__tests__/web-interactor.test.ts +++ b/packages/platform-web/src/interactor.test.ts @@ -1,8 +1,8 @@ import assert from 'node:assert/strict'; import { test } from 'vitest'; -import { createWebInteractor } from '../interactors/web.ts'; +import { createWebInteractor } from './interactor.ts'; import { AppError } from '@agent-device/kernel/errors'; -import { withWebProvider, type WebProvider } from '@agent-device/platform-web'; +import { withWebProvider, type WebProvider } from './index.ts'; test('web interactor delegates first-slice operations to the scoped provider', async () => { const calls: string[] = []; diff --git a/src/core/interactors/web.ts b/packages/platform-web/src/interactor.ts similarity index 97% rename from src/core/interactors/web.ts rename to packages/platform-web/src/interactor.ts index 2f032b310d..fcd8ef92e0 100644 --- a/src/core/interactors/web.ts +++ b/packages/platform-web/src/interactor.ts @@ -2,7 +2,7 @@ import type { Interactor } from '@agent-device/contracts/interactor-types'; import { AppError } from '@agent-device/kernel/errors'; import { stripAtPrefix } from '@agent-device/selectors/interaction-positionals'; import { withDiagnosticTimer } from '@agent-device/host-kit/diagnostics'; -import { resolveWebProvider, type WebProvider } from '@agent-device/platform-web'; +import { resolveWebProvider, type WebProvider } from './index.ts'; import { createUnsupportedInteractor } from './unsupported-interactor.ts'; export async function createWebInteractor(provider?: WebProvider): Promise { diff --git a/src/core/interactors/unsupported-interactor.ts b/packages/platform-web/src/unsupported-interactor.ts similarity index 100% rename from src/core/interactors/unsupported-interactor.ts rename to packages/platform-web/src/unsupported-interactor.ts diff --git a/scripts/layering/platform-package-policy.test.ts b/scripts/layering/platform-package-policy.test.ts index cb5b5fc7a8..a25f962e3b 100644 --- a/scripts/layering/platform-package-policy.test.ts +++ b/scripts/layering/platform-package-policy.test.ts @@ -56,7 +56,9 @@ function declarations(): PlatformPackageDeclaration[] { '@agent-device/platform-android/snapshot-capture-fixtures', '@agent-device/platform-android/ui-hierarchy-fixtures', ] - : [`@agent-device/platform-${family}`], + : family === 'harmonyos' || family === 'linux' || family === 'web' + ? [`@agent-device/platform-${family}`, `@agent-device/platform-${family}/interactor`] + : [`@agent-device/platform-${family}`], })); } @@ -518,6 +520,88 @@ test('platform packages may use capture-kit but no unrelated workspace implement } }); +test('platform-web production source may use only the canonical positional-ref selectors subpath', () => { + const allowed = validSources(); + allowed.set( + 'packages/platform-web/src/interactor.ts', + "import { stripAtPrefix } from '@agent-device/selectors/interaction-positionals';", + ); + assert.deepEqual(checkPlatformPackagePolicy(allowed, declarations()), []); + + for (const [file, specifier] of [ + ['packages/platform-web/src/interactor.ts', '@agent-device/selectors'], + ['packages/platform-web/src/interactor.ts', '@agent-device/selectors/selector-pipeline'], + ['packages/platform-apple/src/probe.ts', '@agent-device/selectors/interaction-positionals'], + ['packages/platform-linux/src/probe.ts', '@agent-device/selectors/interaction-positionals'], + ] as const) { + const sources = validSources(); + sources.set(file, `import { forbidden } from '${specifier}';`); + assert.match( + messages(sources).join('\n'), + /may import workspace code only from capture-kit, host-kit, provision-kit, contracts, kernel, or xml/, + `${file} ${specifier}`, + ); + } + + for (const statement of [ + "import { x } from '../../platform-apple/src/index.ts';", + "import { x } from '@agent-device/platform-apple';", + "import { x } from '../../../src/core/interactors.ts';", + ]) { + const sources = validSources(); + sources.set( + 'packages/platform-web/src/interactor.ts', + `import { stripAtPrefix } from '@agent-device/selectors/interaction-positionals';\n${statement}`, + ); + assert.notDeepEqual(messages(sources), [], statement); + } +}); + +test('interactor subpaths are loaded only by the builtin registry, dynamically', () => { + for (const family of ['harmonyos', 'linux', 'web']) { + const allowed = validSources(); + allowed.set( + 'src/core/interactors/register-builtins.ts', + `const load = () => import('@agent-device/platform-${family}/interactor');`, + ); + assert.deepEqual(checkPlatformPackagePolicy(allowed, declarations()), [], family); + + for (const [file, source] of [ + [ + 'src/core/interactors/register-builtins.ts', + `import { x } from '@agent-device/platform-${family}/interactor';`, + ], + ['src/core/interactors.ts', `void import('@agent-device/platform-${family}/interactor');`], + [ + 'packages/daemon/src/probe.ts', + `void import('@agent-device/platform-${family}/interactor');`, + ], + [ + 'src/core/interactors/register-builtins.ts', + `void import('@agent-device/platform-${family}/snapshot');`, + ], + [ + `packages/platform-${family === 'web' ? 'linux' : 'web'}/src/probe.ts`, + `void import('@agent-device/platform-${family}/interactor');`, + ], + ] as const) { + const sources = validSources(); + sources.set(file, source); + assert.notDeepEqual(messages(sources), [], `${family} ${file} ${source}`); + } + } + + const undeclared = declarations().map((declaration) => + declaration.family === 'linux' + ? { ...declaration, exportedSubpaths: ['@agent-device/platform-linux'] } + : declaration, + ); + assert.match( + messages(validSources(), undeclared).join('\n'), + /platform-linux must export exactly its root façade.*mechanics subpaths/, + ); +}); + test('platform package tests may import test-kit, production source may not', () => { const tests = validSources(); tests.set( diff --git a/scripts/layering/platform-package-policy.ts b/scripts/layering/platform-package-policy.ts index 3fd775194d..eb77969eb1 100644 --- a/scripts/layering/platform-package-policy.ts +++ b/scripts/layering/platform-package-policy.ts @@ -95,7 +95,24 @@ const MECHANICS_FACET_SUBPATHS: Readonly { - const { createHarmonyInteractor } = await import('./harmonyos.ts'); + const { createHarmonyInteractor } = await import('@agent-device/platform-harmonyos/interactor'); return createHarmonyInteractor(device, runner); }, } as const satisfies PlatformPlugin; @@ -33,7 +33,7 @@ const linuxPlugin = { // linux tool provider, formerly gated by `device.platform === 'linux'`). providers: { platformGatedResolvers: ['linuxToolProvider'] }, createInteractor: async () => { - const { createLinuxInteractor } = await import('./linux.ts'); + const { createLinuxInteractor } = await import('@agent-device/platform-linux/interactor'); return createLinuxInteractor(); }, } as const satisfies PlatformPlugin; @@ -45,7 +45,7 @@ const webPlugin = { // provider, formerly gated by `device.platform === 'web'`). providers: { platformGatedResolvers: ['webProvider'] }, createInteractor: async () => { - const { createWebInteractor } = await import('./web.ts'); + const { createWebInteractor } = await import('@agent-device/platform-web/interactor'); return await createWebInteractor(); }, } as const satisfies PlatformPlugin; diff --git a/test/integration/command-coverage/declarations.ts b/test/integration/command-coverage/declarations.ts index 888fff4428..4039945061 100644 --- a/test/integration/command-coverage/declarations.ts +++ b/test/integration/command-coverage/declarations.ts @@ -1224,7 +1224,7 @@ const COMMAND_COVERAGE_DECLARATIONS = { macos: macos.gap('No command-specific macOS focus evidence exists yet'), tvos: tvos.gap('No tvOS-specific focus command evidence exists yet'), web: web.contract( - 'src/core/__tests__/web-interactor.test.ts', + 'packages/platform-web/src/interactor.test.ts', 'web interactor delegates first-slice operations to the scoped provider', 'web focus delegates to the scoped provider click primitive', ),