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/contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,10 @@
"types": "./src/platform-runtime-operations.ts",
"default": "./src/platform-runtime-operations.ts"
},
"./platform-resource-cleanup": {
"types": "./src/platform-resource-cleanup.ts",
"default": "./src/platform-resource-cleanup.ts"
},
"./platform-runtime-unavailable": {
"types": "./src/platform-runtime-unavailable.ts",
"default": "./src/platform-runtime-unavailable.ts"
Expand Down
21 changes: 21 additions & 0 deletions packages/contracts/src/platform-resource-cleanup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { DeviceInfo } from '@agent-device/kernel/device';

/** Platform-owned resource finalization invoked by neutral daemon orchestration. */
export type PlatformResourceCleanup = Readonly<{
stopSnapshotHelper(device: DeviceInfo): Promise<void>;
closeManagedBrowser(
params: Readonly<{
device: DeviceInfo;
sessionName: string;
stateDir: string;
openSessionNames: () => readonly string[];
}>,
): Promise<void>;
cleanupSessionlessExecutionHost(device: DeviceInfo): Promise<void>;
retainExecutionHostAfterClose(params: {
device: DeviceInfo;
shutdownRequested: boolean;
hasScreenRecording: boolean;
hasLease: boolean;
}): boolean;
}>;
2 changes: 1 addition & 1 deletion scripts/__tests__/test-file-size-ratchet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const TRIPWIRE_LINES = 1_000;
// Exact current lengths. Lower a pin when its file shrinks; never raise one — extract instead.
const PINNED_TEST_FILE_LINES: Readonly<Record<string, number>> = Object.freeze({
'src/__tests__/remote-connection.test.ts': 2973,
'src/daemon/handlers/__tests__/snapshot-handler.test.ts': 2284,
'src/daemon/handlers/__tests__/snapshot-handler.test.ts': 2242,
'src/commands/interaction/runtime/settle.test.ts': 2359,
'src/daemon/handlers/__tests__/session-replay-runtime-maestro.test.ts': 1963,
'packages/platform-apple/src/runner/__tests__/runner-session.test.ts': 1957,
Expand Down
12 changes: 10 additions & 2 deletions scripts/layering/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
// an import whose source zone outranks its target zone, plus a ratchet on the
// same inversion measured over TYPE-ONLY edges (R6).
// - Over the DAEMON only: SessionState field ownership (R7), because the session
// record is store-owned mutable state that any daemon module can write.
// record is store-owned mutable state that any daemon module can write; and the terminal
// concrete-platform boundary (R65), which rejects every import form into src/platforms or a
// platform package.
// - Over the TYPE GRAPH: the largest type-level import cycle is pinned by
// equality (R9). R4 keeps the value graph acyclic, so these cycles are free at
// runtime but bound what can be read in isolation; growth fails, and so does a
Expand Down Expand Up @@ -98,6 +100,7 @@ import { policyLead, policyViolation, ZONE_POLICIES } from './zone-policy.ts';
import { contractsImplementationAuthorityViolations } from './contracts-implementation-policy.ts';
import { selectorPipelineOwnershipViolations } from './selector-pipeline-ownership.ts';
import { recordRuntimeRegistryJoinViolations } from './record-runtime-registry-policy.ts';
import { checkDaemonPlatformBoundary } from './daemon-platform-boundary.ts';
import { listTrackedProductionSources, listTrackedTypeScriptFiles } from './tracked-sources.ts';

const repoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
Expand Down Expand Up @@ -485,7 +488,9 @@ function report(
`inside its declared owner (R7); the largest type-level cycle is ${typeCycle} files ` +
`(R9); ${daemonModularitySummary()}; ` +
`${packageBoundariesSummary(repoRoot)}; ${platformPackagePolicySummary()}; ` +
`${runtimeCommandCutoverSummary()}; and bin.ts imports normalizeCliCommandAlias, ` +
`${runtimeCommandCutoverSummary()}; R65 keeps production src/daemon free of concrete ` +
`platform imports in every executable and type-only form; and bin.ts imports ` +
`normalizeCliCommandAlias, ` +
`actually passes it into buildCommandUsageText, and holds no local alias literals ` +
`(R12).\n`,
);
Expand Down Expand Up @@ -543,6 +548,7 @@ export const LAYERING_RULE_IDS = [
'type-spine-inversions',
'session-state-ownership',
'daemon-modularity-ratchets',
'daemon-platform-boundary',
'bin-alias-fast-path',
'package-boundaries',
'platform-package-policy',
Expand All @@ -564,6 +570,8 @@ export const LAYERING_RULES: Readonly<Record<LayeringRuleId, LayeringRule>> = {
'session-state-ownership': (context) => checkSessionStateOwnership(context.sources),
'daemon-modularity-ratchets': (context) =>
checkDaemonModularityRatchets(context.edges, context.typeCycleMembers),
'daemon-platform-boundary': (context) =>
checkDaemonPlatformBoundary([...context.sources].map(([path, source]) => ({ path, source }))),
'bin-alias-fast-path': (context) => checkBinAliasFastPath(context.sources),
'package-boundaries': () => checkPackageBoundaries(repoRoot),
'platform-package-policy': (context) =>
Expand Down
292 changes: 292 additions & 0 deletions scripts/layering/daemon-platform-boundary.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import {
DAEMON_PLATFORM_BOUNDARY_RULE,
daemonPlatformBoundaryViolations,
findDaemonPlatformDependencies,
} from './daemon-platform-boundary.ts';

const daemonFile = 'src/daemon/terminal-boundary-fixture.ts';

function dependencies(source: string, file = daemonFile) {
return findDaemonPlatformDependencies([{ path: file, source }]);
}

function violations(source: string, file = daemonFile) {
return daemonPlatformBoundaryViolations([{ path: file, source }]);
}

test('R65 rejects static, type-only, dynamic, type, and re-export dependencies with source lines', () => {
const source = [
"import { old } from '../platforms/android/old.ts';",
"import type { OldType } from '../platforms/android/types.ts';",
"const lazy = import('../platforms/android/lazy.ts');",
"type ImportedType = import('../platforms/android/type.ts').ImportedType;",
"export { old } from '../platforms/android/re-export.ts';",
"export type { OldType } from '@agent-device/platform-android/types';",
"export * from '@agent-device/platform-apple';",
].join('\n');

assert.deepEqual(
dependencies(source).map(({ kind, spec, line, target }) => ({ kind, spec, line, target })),
[
{
kind: 'static import',
spec: '../platforms/android/old.ts',
line: 1,
target: 'src/platforms/android/old.ts',
},
{
kind: 'type-only import',
spec: '../platforms/android/types.ts',
line: 2,
target: 'src/platforms/android/types.ts',
},
{
kind: 'dynamic import',
spec: '../platforms/android/lazy.ts',
line: 3,
target: 'src/platforms/android/lazy.ts',
},
{
kind: 'type import',
spec: '../platforms/android/type.ts',
line: 4,
target: 'src/platforms/android/type.ts',
},
{
kind: 're-export',
spec: '../platforms/android/re-export.ts',
line: 5,
target: 'src/platforms/android/re-export.ts',
},
{
kind: 'type-only re-export',
spec: '@agent-device/platform-android/types',
line: 6,
target: '@agent-device/platform-android/types',
},
{
kind: 're-export',
spec: '@agent-device/platform-apple',
line: 7,
target: '@agent-device/platform-apple',
},
],
);

assert.deepEqual(
violations(source).map(({ rule, file, line }) => ({ rule, file, line })),
Array.from({ length: 7 }, (_, index) => ({
rule: DAEMON_PLATFORM_BOUNDARY_RULE,
file: daemonFile,
line: index + 1,
})),
);
});

test('R65 recognizes side-effect and aliased imports, including nested daemon paths', () => {
const source = [
"import '../../platforms/web/runtime.ts';",
"import { runtime as platformRuntime } from '@agent-device/platform-web/runtime';",
].join('\n');

assert.deepEqual(
dependencies(source, 'src/daemon/handlers/terminal-boundary-fixture.ts').map((found) => ({
kind: found.kind,
line: found.line,
spec: found.spec,
target: found.target,
})),
[
{
kind: 'static import',
line: 1,
spec: '../../platforms/web/runtime.ts',
target: 'src/platforms/web/runtime.ts',
},
{
kind: 'static import',
line: 2,
spec: '@agent-device/platform-web/runtime',
target: '@agent-device/platform-web/runtime',
},
],
);
});

test('R65 ignores comments, ordinary strings, unresolved dynamic imports, and lookalike names', () => {
const source = [
'const documentation = "import(\'../platforms/android/comment.ts\'); @agent-device/platform-android";',
"// import { ignored } from '../platforms/android/comment.ts';",
"const packageName = '@agent-device/platform-android';",
"const relativeName = '../platforms/android/not-an-import.ts';",
'const computed = import(platformSpecifier);',
"import '../platforms-sibling/not-platform.ts';",
"import '@agent-device/platforms';",
"import '@agent-device/platform';",
].join('\n');

assert.deepEqual(dependencies(source), []);
});

test('R65 rejects require, import-equals, and template-literal type imports', () => {
const source = [
"const android = require('@agent-device/platform-android');",
'const runtime = require(`../platforms/android/runtime.ts`);',
"import web = require('@agent-device/platform-web');",
'type Apple = import(`@agent-device/platform-apple`).Apple;',
'type Android = import(`../platforms/android/types.ts`).Android;',
].join('\n');

assert.deepEqual(
dependencies(source).map(({ kind, spec, line }) => ({ kind, spec, line })),
[
{ kind: 'require', spec: '@agent-device/platform-android', line: 1 },
{ kind: 'require', spec: '../platforms/android/runtime.ts', line: 2 },
{ kind: 'import equals', spec: '@agent-device/platform-web', line: 3 },
{ kind: 'type import', spec: '@agent-device/platform-apple', line: 4 },
{ kind: 'type import', spec: '../platforms/android/types.ts', line: 5 },
],
);
});

test('R65 folds statically constructed dynamic platform specifiers', () => {
const source = [
"const apple = import('@agent-device/' + 'platform-apple');",
'const android = import(`../platforms/${"android"}/runtime.ts`);',
"const wrapped = import((('@agent-device/' + 'platform-android')));",
'const required = require((`@agent-device/platform-web`));',
].join('\n');

assert.deepEqual(
dependencies(source).map(({ kind, spec, line }) => ({ kind, spec, line })),
[
{ kind: 'dynamic import', spec: '@agent-device/platform-apple', line: 1 },
{ kind: 'dynamic import', spec: '../platforms/android/runtime.ts', line: 2 },
{ kind: 'dynamic import', spec: '@agent-device/platform-android', line: 3 },
{ kind: 'require', spec: '@agent-device/platform-web', line: 4 },
],
);
});

test('R65 unwraps erased TypeScript expressions around executable specifiers', () => {
const source = [
"void import('@agent-device/platform-android' as string);",
"require('@agent-device/platform-web' satisfies string);",
"void import(<string>'../platforms/apple/runtime.ts');",
].join('\n');

assert.deepEqual(
dependencies(source).map(({ kind, spec, line }) => ({ kind, spec, line })),
[
{ kind: 'dynamic import', spec: '@agent-device/platform-android', line: 1 },
{ kind: 'require', spec: '@agent-device/platform-web', line: 2 },
{ kind: 'dynamic import', spec: '../platforms/apple/runtime.ts', line: 3 },
],
);
});

test('R65 follows common createRequire and require aliases', () => {
const source = [
"import { createRequire as makeRequire } from 'node:module';",
"import * as moduleApi from 'node:module';",
"import moduleDefault from 'node:module';",
'const load = makeRequire(import.meta.url);',
'const loadAgain = load;',
"loadAgain('@agent-device/platform-android');",
"makeRequire(import.meta.url)('../platforms/web/runtime.ts');",
"moduleApi.createRequire(import.meta.url)('@agent-device/platform-apple');",
"moduleDefault.createRequire(import.meta.url)('@agent-device/platform-web');",
'const loadAlias = require;',
"loadAlias('@agent-device/platform-linux');",
"module.require('@agent-device/platform-vega');",
"const { createRequire: fromCjs } = require('node:module');",
"fromCjs(import.meta.url)('@agent-device/platform-harmonyos');",
].join('\n');

assert.deepEqual(
dependencies(source).map(({ spec, line }) => ({ spec, line })),
[
{ spec: '@agent-device/platform-android', line: 6 },
{ spec: '../platforms/web/runtime.ts', line: 7 },
{ spec: '@agent-device/platform-apple', line: 8 },
{ spec: '@agent-device/platform-web', line: 9 },
{ spec: '@agent-device/platform-linux', line: 11 },
{ spec: '@agent-device/platform-vega', line: 12 },
{ spec: '@agent-device/platform-harmonyos', line: 14 },
],
);
});

test('R65 rejects triple-slash concrete-platform type references', () => {
const source = [
'/// <reference types="@agent-device/platform-android" />',
'/// <reference path = "../platforms/apple/types.ts" />',
'/*',
'/// <reference types="@agent-device/platform-web" />',
'*/',
'export {};',
].join('\n');
assert.deepEqual(
dependencies(source).map(({ kind, spec, line }) => ({ kind, spec, line })),
[
{ kind: 'type import', spec: '@agent-device/platform-android', line: 1 },
{ kind: 'type import', spec: '../platforms/apple/types.ts', line: 2 },
],
);
});

test('R65 rejects platform selection inside cleanup orchestrators', () => {
const direct = violations(
"export const cleanup = (session: any) => session.device.platform === 'android';",
'src/daemon/session-teardown.ts',
);
assert.match(direct[0]?.message ?? '', /may not select a concrete platform/);

const predicate = violations(
'export const cleanup = (device: any) => isIosFamily(device);',
'src/daemon/handlers/snapshot-session.ts',
);
assert.match(predicate[0]?.message ?? '', /typed root-composed cleanup capability/);

const destructured = violations(
"export const cleanup = (session: any) => { const { platform } = session.device; return platform === 'android'; };",
'src/daemon/handlers/session-close-lifecycle-teardown.ts',
);
assert.match(destructured[0]?.message ?? '', /may not select a concrete platform/);

assert.deepEqual(
violations(
'export const cleanup = (owner: any, device: any) => owner.cleanupSessionlessExecutionHost(device);',
'src/daemon/handlers/snapshot-session.ts',
),
[],
);
});

test('R65 ignores non-daemon and test-shaped records even when their syntax is red', () => {
const source = "import { platform } from '../platforms/android/runtime.ts';";
assert.deepEqual(dependencies(source, 'src/core/terminal-boundary-fixture.ts'), []);
assert.deepEqual(dependencies(source, 'src/daemon/terminal-boundary-fixture.test.ts'), []);
assert.deepEqual(dependencies(source, 'src/daemon/__tests__/terminal-boundary-fixture.ts'), []);
});

test('R65 treats a relative specifier as legacy platform code only after path resolution', () => {
const source = [
"import { platform } from '../../platforms/android/runtime.ts';",
"import { sibling } from '../platforms-sibling/runtime.ts';",
"import { exact } from '../platforms';",
].join('\n');

assert.deepEqual(
dependencies(source).map(({ spec, target, line }) => ({ spec, target, line })),
[
{
spec: '../platforms',
target: 'src/platforms',
line: 3,
},
],
);
});
Loading
Loading