Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/platform-harmonyos/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
},
"./interactor": {
"types": "./src/interactor-facade.ts",
"default": "./src/interactor-facade.ts"
}
}
}
10 changes: 10 additions & 0 deletions packages/platform-harmonyos/src/interactor-facade.ts
Original file line number Diff line number Diff line change
@@ -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<Interactor> {
const { createHarmonyInteractor: create } = await import('./interactor.ts');
return create(device, runner);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<never> => {
Expand Down
4 changes: 4 additions & 0 deletions packages/platform-linux/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
},
"./interactor": {
"types": "./src/interactor-facade.ts",
"default": "./src/interactor-facade.ts"
}
}
}
6 changes: 6 additions & 0 deletions packages/platform-linux/src/interactor-facade.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { Interactor } from '@agent-device/contracts/interactor-types';

export async function createLinuxInteractor(): Promise<Interactor> {
const { createLinuxInteractor: create } = await import('./interactor.ts');
return create();
}
Original file line number Diff line number Diff line change
@@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
4 changes: 4 additions & 0 deletions packages/platform-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
},
"./interactor": {
"types": "./src/interactor-facade.ts",
"default": "./src/interactor-facade.ts"
}
},
"devDependencies": {
Expand Down
7 changes: 7 additions & 0 deletions packages/platform-web/src/interactor-facade.ts
Original file line number Diff line number Diff line change
@@ -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<Interactor> {
const { createWebInteractor: create } = await import('./interactor.ts');
return await create(provider);
}
Original file line number Diff line number Diff line change
@@ -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[] = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Interactor> {
Expand Down
86 changes: 85 additions & 1 deletion scripts/layering/platform-package-policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`],
}));
}

Expand Down Expand Up @@ -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(
Expand Down
33 changes: 27 additions & 6 deletions scripts/layering/platform-package-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,24 @@ const MECHANICS_FACET_SUBPATHS: Readonly<Partial<Record<PlatformFamily, readonly
'@agent-device/platform-apple/tool-provider',
],
android: [ANDROID_HOST_FACET, ANDROID_MECHANICS_FACADE, ANDROID_DEVICE_BOOT_FACADE],
harmonyos: ['@agent-device/platform-harmonyos/interactor'],
linux: ['@agent-device/platform-linux/interactor'],
web: ['@agent-device/platform-web/interactor'],
};
const INTERACTOR_COMPOSITION_FILE = 'src/core/interactors/register-builtins.ts';

function isInteractorSubpathImport(
file: string,
site: { spec: string; dynamic: boolean },
family: string,
): boolean {
return (
site.spec === `@agent-device/platform-${family}/interactor` &&
(MECHANICS_FACET_SUBPATHS[family as PlatformFamily] ?? []).includes(site.spec) &&
file === INTERACTOR_COMPOSITION_FILE &&
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
site.dynamic
);
}

function isAndroidFacetSubpathImport(file: string, specifier: string): boolean {
if (specifier === ANDROID_HOST_FACET) return file === ANDROID_HOST_BINDING;
Expand Down Expand Up @@ -166,10 +183,13 @@ function isPackageOwnedFacadeTest(file: string, family: string, specifier: strin
);
}

function isWebPackageTestSelectorImport(file: string, family: string, specifier: string): boolean {
// The web interactor reuses the selectors package's canonical positional-ref normalizer.
const WEB_SELECTORS_PRODUCTION_SUBPATH = '@agent-device/selectors/interaction-positionals';

function isWebPackageSelectorImport(file: string, family: string, specifier: string): boolean {
if (family !== 'web' || !file.startsWith('packages/platform-web/')) return false;
if (specifier === WEB_SELECTORS_PRODUCTION_SUBPATH) return true;
return (
family === 'web' &&
file.startsWith('packages/platform-web/') &&
(file.endsWith('.test.ts') || file.includes('/__tests__/')) &&
specifier === '@agent-device/selectors'
);
Expand Down Expand Up @@ -298,7 +318,8 @@ function checkSource(file: string, source: string): LayeringViolation[] {
!isAppleFacadeSubpathImport(site.spec) &&
!isAllowedPlatformRootImport(file, site, importedFamily) &&
!isPackageOwnedFacadeTest(file, importedFamily, site.spec) &&
!isAndroidFacetSubpathImport(file, site.spec)
!isAndroidFacetSubpathImport(file, site.spec) &&
!isInteractorSubpathImport(file, site, importedFamily)
) {
violations.push(
violation(
Expand All @@ -322,7 +343,7 @@ function checkSource(file: string, source: string): LayeringViolation[] {
site.spec !== '@agent-device/xml' &&
!isAppleFacadeSubpathImport(site.spec) &&
!isPackageOwnedFacadeTest(file, ownerFamily, site.spec) &&
!isWebPackageTestSelectorImport(file, ownerFamily, site.spec) &&
!isWebPackageSelectorImport(file, ownerFamily, site.spec) &&
!isTestKitTestImport(file, site.spec)
) {
violations.push(
Expand Down Expand Up @@ -409,5 +430,5 @@ export function checkPlatformPackagePolicy(
}

export function platformPackagePolicySummary(): string {
return 'R13 holds six private implementation-lazy platform packages above capture-kit behind one canonical composition root and its single private provider-composition submodule, with the apple runner and android mechanics facets behind their enumerated seams; deferred or type-only edges are limited to the approved platform-runtime host watchlist';
return `R13 holds six private implementation-lazy platform packages above capture-kit behind one canonical composition root and its single private provider-composition submodule, with the apple runner and android mechanics facets behind their enumerated seams; deferred or type-only edges are limited to the approved platform-runtime host watchlist and the dynamic-only HarmonyOS, Linux and Web interactor subpaths imported by ${INTERACTOR_COMPOSITION_FILE}`;
}
6 changes: 3 additions & 3 deletions src/core/interactors/register-builtins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const harmonyosPlugin = {
id: 'harmonyos',
platforms: ['harmonyos'],
createInteractor: async (device: DeviceInfo, runner: RunnerContext) => {
const { createHarmonyInteractor } = await import('./harmonyos.ts');
const { createHarmonyInteractor } = await import('@agent-device/platform-harmonyos/interactor');
return createHarmonyInteractor(device, runner);
},
} as const satisfies PlatformPlugin;
Expand All @@ -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;
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion test/integration/command-coverage/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
),
Expand Down
Loading