Skip to content

Commit c06bed9

Browse files
authored
refactor: extract platform device inventory runtime (#1699)
* refactor: extract platform inventory runtime * fix: preserve scoped Apple inventory tooling * fix: preserve Apple tool cancellation * refactor: tighten platform inventory boundaries
1 parent 44c298d commit c06bed9

181 files changed

Lines changed: 6921 additions & 3455 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CONTEXT.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,13 @@ axis and stages a deeper platform-runtime seam, one abandonment-safe command cut
411411
that earned ADR 0019's staged platform-runtime migration. Its substrate plus complete
412412
`devices`/`logs`/`network` checkpoint must validate the seam before any further command migration.
413413
These are ratchets, not permission to scaffold façades before a real seam has two adapters.
414+
- Platform package boundary (R13). ADR 0019 has exactly six private
415+
`@agent-device/platform-*` package façades and one root composition file,
416+
`src/platform-runtime.ts`. R13 pins that total registration, forbids contracts-to-platform,
417+
sibling-platform, root/daemon, and raw-process edges in every import form (including tests), and
418+
keeps package façades metadata-eager but inventory/runtime mechanics lazy. Composition cannot probe tools, prepare
419+
assets, or construct helpers. Each rule has a planted-red structural case; R11 still owns the
420+
general workspace exports/dependency boundary.
414421
- Zero-dep CI jobs (R8). Some jobs run scripts straight from a checkout with `install-deps: false`,
415422
so they have no `node_modules`. Nothing local can feel that constraint — every dev machine has
416423
`node_modules` sitting right there — so a script grows a package import, passes locally, and fails

docs/adr/0019-request-bound-platform-runtime.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ app-log disposal contract without changing `close`'s legacy platform-execution o
99
command's platform adapter must change, this Status must name its complete unit before substrate work
1010
begins. Broader migration requires this Status to record **continue**.
1111

12+
During the `devices` unit, doctor discovery, replay-test sharding, Apple simulator hints, and Android
13+
emulator lifecycle keep their existing command execution owners while consuming the same injected,
14+
neutral inventory capability. That coexistence moves discovery mechanics once; it does not migrate
15+
those descriptors or authorize a second local/provider chooser.
16+
1217
## Rules at a glance
1318

1419
- Daemon device-execution code depends on platform-neutral contracts. Concrete device mechanics

docs/agents/testing.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,10 @@ hand-maintained path map:
140140
left to CI by `--run` so a root checkout never installs Expo dependencies
141141
implicitly.
142142
- **Always-on gates** (`lint`, `typecheck`, `layering`, `fallow`, `format`) fire
143-
for their input categories and are never silently skipped. Platform source
144-
also selects the provider-integration and coverage gates required by the
145-
Testing Matrix.
143+
for their input categories and are never silently skipped. Legacy
144+
`src/platforms/` source also selects provider-integration and coverage.
145+
`packages/platform-*` source selects the shared runtime-contract unit lane,
146+
provider-integration, and coverage so a package move cannot narrow its evidence.
146147
- **Commands** are resolved from real `package.json` scripts, so a renamed
147148
script fails loudly instead of dropping a gate.
148149
- A **small explicit build-ownership layer** covers the paths whose owning build

fallow-baselines/health.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -374,11 +374,6 @@
374374
"count": 1
375375
}
376376
},
377-
"src/platforms/apple/core/devices.ts": {
378-
"crap_moderate": {
379-
"count": 1
380-
}
381-
},
382377
"src/platforms/apple/core/perf.ts": {
383378
"complexity_high": {
384379
"count": 1

package.json

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,10 @@
127127
"fallow:baseline": "fallow dead-code --save-baseline fallow-baselines/dead-code.json --summary && fallow health --report-only --save-baseline fallow-baselines/health.json --summary",
128128
"check:fallow": "fallow audit",
129129
"check:affected": "node --experimental-strip-types scripts/check-affected/run.ts",
130-
"check:affected:test": "node --experimental-strip-types scripts/node-test-tmpdir.ts --experimental-strip-types --test scripts/check-affected/model.test.ts scripts/check-affected/run.test.ts",
130+
"check:affected:test": "node --experimental-strip-types scripts/node-test-tmpdir.ts --experimental-strip-types --test scripts/check-affected/model.test.ts scripts/check-affected/platform-packages.test.ts scripts/check-affected/run.test.ts",
131131
"check:coverage-changed": "node --experimental-strip-types scripts/coverage-changed/run.ts",
132132
"check:coverage-changed:test": "node --experimental-strip-types scripts/node-test-tmpdir.ts --experimental-strip-types --test scripts/coverage-changed/model.test.ts scripts/coverage-changed/run.test.ts",
133-
"check:layering": "node --experimental-strip-types scripts/node-test-tmpdir.ts --experimental-strip-types --test scripts/layering/model.test.ts scripts/layering/zone-policy.test.ts scripts/layering/daemon-modularity.test.ts scripts/layering/package-boundaries.test.ts scripts/layering/facade-exports.test.ts scripts/layering/bin-alias-fast-path.test.ts && node --experimental-strip-types scripts/layering/check.ts",
133+
"check:layering": "node --experimental-strip-types scripts/node-test-tmpdir.ts --experimental-strip-types --test scripts/layering/model.test.ts scripts/layering/zone-policy.test.ts scripts/layering/daemon-modularity.test.ts scripts/layering/package-boundaries.test.ts scripts/layering/platform-package-policy.test.ts scripts/layering/platform-package-repository.test.ts scripts/layering/platform-package-source-policy.test.ts scripts/layering/device-inventory-cutover-policy.test.ts scripts/layering/facade-exports.test.ts scripts/layering/bin-alias-fast-path.test.ts && node --experimental-strip-types scripts/layering/check.ts",
134134
"depgraph": "node --experimental-strip-types scripts/depgraph/build.ts",
135135
"depgraph:test": "node --experimental-strip-types scripts/node-test-tmpdir.ts --experimental-strip-types --test scripts/depgraph/model.test.ts scripts/depgraph/affected.test.ts",
136136
"check:production-exports": "fallow dead-code --config fallow-production-exports.json --production --unused-exports --fail-on-issues",
@@ -149,7 +149,7 @@
149149
"check:unit": "pnpm check:contention-retry && pnpm test:unit && pnpm check:tmpdir-leaks && pnpm test:smoke",
150150
"check": "pnpm check:tooling && pnpm check:fallow && pnpm check:unit",
151151
"prepack": "pnpm check:mcp-metadata && pnpm package:npm",
152-
"typecheck": "tsc -b packages/xml packages/kernel packages/contracts packages/ad-script packages/selectors packages/ad-replay packages/maestro packages/replay-test packages/provider-webdriver packages/provider-limrun && tsc -p tsconfig.json && tsc -p examples/sdk/tsconfig.json",
152+
"typecheck": "tsc -b packages/xml packages/kernel packages/contracts packages/platform-apple packages/platform-android packages/platform-harmonyos packages/platform-vega packages/platform-linux packages/platform-web packages/ad-script packages/selectors packages/ad-replay packages/maestro packages/replay-test packages/provider-webdriver packages/provider-limrun && tsc -p tsconfig.json && tsc -p examples/sdk/tsconfig.json",
153153
"test-app:install": "pnpm install --dir examples/test-app",
154154
"test-app:start": "pnpm --dir examples/test-app start",
155155
"test-app:ios": "pnpm --dir examples/test-app ios",
@@ -173,7 +173,7 @@
173173
"test:output-economy": "vitest run --project output-economy",
174174
"test:smoke:web": "pnpm build && node --experimental-strip-types scripts/node-test-tmpdir.ts --test test/integration/smoke-web-platform.test.ts",
175175
"test:smoke": "node --experimental-strip-types scripts/node-test-tmpdir.ts --test test/integration/smoke-*.test.ts",
176-
"test:integration:node": "node --experimental-strip-types scripts/node-test-tmpdir.ts --test test/integration/*.test.ts",
176+
"test:integration:node": "node --experimental-strip-types scripts/node-test-tmpdir.ts --test --test-concurrency=1 test/integration/*.test.ts",
177177
"test:integration": "pnpm test:integration:node && pnpm test:integration:provider",
178178
"test:concurrency-torture": "node --experimental-strip-types scripts/node-test-tmpdir.ts --test test/integration/nightly/concurrency-torture.test.ts",
179179
"test:replay:ios": "node --experimental-strip-types src/bin.ts test test/integration/replays/ios/simulator",
@@ -257,6 +257,12 @@
257257
"@agent-device/contracts": "workspace:*",
258258
"@agent-device/kernel": "workspace:*",
259259
"@agent-device/maestro": "workspace:*",
260+
"@agent-device/platform-android": "workspace:*",
261+
"@agent-device/platform-apple": "workspace:*",
262+
"@agent-device/platform-harmonyos": "workspace:*",
263+
"@agent-device/platform-linux": "workspace:*",
264+
"@agent-device/platform-vega": "workspace:*",
265+
"@agent-device/platform-web": "workspace:*",
260266
"@agent-device/provider-limrun": "workspace:*",
261267
"@agent-device/provider-webdriver": "workspace:*",
262268
"@agent-device/replay-test": "workspace:*",
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import assert from 'node:assert/strict';
2+
import { test } from 'vitest';
3+
import { classifyAndroidAdbFailure } from './android-adb-failure.ts';
4+
5+
test('Android ADB failures keep transport on stderr and install verdicts on stdout', () => {
6+
assert.equal(
7+
classifyAndroidAdbFailure("adb server version (40) doesn't match this client (41); killing...")
8+
?.reason,
9+
'server_version_mismatch',
10+
);
11+
assert.equal(classifyAndroidAdbFailure('', 'log line: device offline detected'), undefined);
12+
assert.equal(
13+
classifyAndroidAdbFailure('', 'Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE]')?.reason,
14+
'install_update_incompatible',
15+
);
16+
});
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
type AndroidAdbFailureReason =
2+
| 'timeout'
3+
| 'device_offline'
4+
| 'device_unauthorized'
5+
| 'device_not_found'
6+
| 'multiple_devices'
7+
| 'no_devices'
8+
| 'connection_dropped'
9+
| 'server_version_mismatch'
10+
| 'install_insufficient_storage'
11+
| 'install_update_incompatible'
12+
| 'install_version_downgrade'
13+
| 'install_failed';
14+
15+
export type AndroidAdbFailureClassification = Readonly<{
16+
/** Machine-readable failure family attached to error details as `adbFailure`. */
17+
reason: AndroidAdbFailureReason;
18+
hint: string;
19+
/** Present only when an unchanged retry can succeed. */
20+
retriable?: boolean;
21+
}>;
22+
23+
type AndroidAdbFailureMatcher = AndroidAdbFailureClassification &
24+
Readonly<{
25+
pattern: RegExp;
26+
/** Android package-manager install verdicts may be emitted on stdout. */
27+
matchStdout?: boolean;
28+
}>;
29+
30+
const ANDROID_ADB_FAILURE_MATCHERS: readonly AndroidAdbFailureMatcher[] = [
31+
{
32+
reason: 'device_unauthorized',
33+
pattern: /device unauthorized|device still authorizing/,
34+
hint: 'USB debugging is not authorized — accept the authorization prompt on the device screen (re-plug the cable if none appears), then retry.',
35+
},
36+
{
37+
reason: 'device_offline',
38+
pattern: /device offline/,
39+
hint: 'The device is connected but offline — wait for it to finish booting or run adb reconnect, then retry.',
40+
retriable: true,
41+
},
42+
{
43+
reason: 'multiple_devices',
44+
pattern: /more than one (?:device\/emulator|device and emulator)/,
45+
hint: 'Multiple Android devices are connected — pass --serial <serial> (see adb devices) to select one.',
46+
},
47+
{
48+
reason: 'no_devices',
49+
pattern: /no devices\/emulators found|no devices found/,
50+
hint: 'No Android devices detected — boot an emulator or connect a device and verify it appears in adb devices.',
51+
},
52+
{
53+
reason: 'device_not_found',
54+
pattern: /device (?:'[^']*' )?not found/,
55+
hint: 'The device disconnected or is restarting — verify it is listed in adb devices, then retry.',
56+
retriable: true,
57+
},
58+
{
59+
reason: 'server_version_mismatch',
60+
pattern: /adb server version \(\d+\) doesn't match this client/,
61+
hint: 'Multiple adb installs conflict — adb restarts its server automatically, so retry; align PATH to a single adb to stop recurrences.',
62+
retriable: true,
63+
},
64+
{
65+
reason: 'connection_dropped',
66+
pattern: /transport error|connection reset|broken pipe|protocol fault/,
67+
hint: 'The adb connection dropped — retry; if it persists, run adb kill-server and reconnect the device.',
68+
retriable: true,
69+
},
70+
{
71+
reason: 'install_insufficient_storage',
72+
pattern: /install_failed_insufficient_storage/,
73+
hint: 'The device is out of storage — free up space or uninstall unused apps, then retry the install.',
74+
matchStdout: true,
75+
},
76+
{
77+
reason: 'install_update_incompatible',
78+
pattern: /install_failed_update_incompatible/,
79+
hint: 'The installed app has an incompatible signature — uninstall the existing app first, then retry the install.',
80+
matchStdout: true,
81+
},
82+
{
83+
reason: 'install_version_downgrade',
84+
pattern: /install_failed_version_downgrade/,
85+
hint: 'The APK is older than the installed app — uninstall the app first (or install with downgrade allowed), then retry.',
86+
matchStdout: true,
87+
},
88+
{
89+
reason: 'install_failed',
90+
pattern: /install_failed_\w+|install_parse_failed_\w+/,
91+
hint: 'The Android package installer rejected the APK — see the INSTALL_FAILED code in the error output for the exact cause.',
92+
matchStdout: true,
93+
},
94+
];
95+
96+
export const ANDROID_ADB_TIMEOUT_FAILURE: AndroidAdbFailureClassification = Object.freeze({
97+
reason: 'timeout',
98+
hint: 'adb timed out — the adb server may be wedged. Run adb kill-server && adb start-server, check adb devices, then retry.',
99+
});
100+
101+
/**
102+
* Classifies transport failures from stderr. Package-manager install verdicts
103+
* additionally inspect stdout because adb emits those semantic results there.
104+
*/
105+
export function classifyAndroidAdbFailure(
106+
stderr: string,
107+
stdout = '',
108+
): AndroidAdbFailureClassification | undefined {
109+
const stderrText = stderr.toLowerCase();
110+
const stdoutText = stdout.toLowerCase();
111+
for (const { pattern, matchStdout, ...classification } of ANDROID_ADB_FAILURE_MATCHERS) {
112+
if (pattern.test(stderrText) || (matchStdout && pattern.test(stdoutText))) {
113+
return classification;
114+
}
115+
}
116+
return undefined;
117+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import assert from 'node:assert/strict';
2+
import { test } from 'vitest';
3+
import { isAndroidEmulatorSerial, normalizeAndroidDeviceName } from './android-device.ts';
4+
5+
test('Android device identity normalizes AVD names and recognizes running emulator serials', () => {
6+
assert.equal(normalizeAndroidDeviceName(' Pixel_9 Pro '), 'pixel 9 pro');
7+
assert.equal(isAndroidEmulatorSerial('emulator-5554'), true);
8+
assert.equal(isAndroidEmulatorSerial('R58M123ABC'), false);
9+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const ANDROID_EMULATOR_SERIAL_PREFIX = 'emulator-';
2+
3+
export function isAndroidEmulatorSerial(serial: string): boolean {
4+
return serial.startsWith(ANDROID_EMULATOR_SERIAL_PREFIX);
5+
}
6+
7+
export function normalizeAndroidDeviceName(value: string): string {
8+
return value.toLowerCase().replace(/_/g, ' ').replace(/\s+/g, ' ').trim();
9+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { describe, expect, test } from 'vitest';
2+
import { inventoryUse } from './platform-module.ts';
3+
import { assertCommandPlatformExecution } from './command-platform-execution.ts';
4+
5+
describe('command platform execution declaration', () => {
6+
test.each([
7+
{ kind: 'legacy' },
8+
{ kind: 'inventory', use: inventoryUse },
9+
{ kind: 'device-runtime', use: { required: ['capture'], preferred: ['inspect'] } },
10+
])('accepts one closed execution shape: %j', (value) => {
11+
expect(() => assertCommandPlatformExecution(value)).not.toThrow();
12+
});
13+
14+
test.each([
15+
{},
16+
{ kind: 'legacy', use: inventoryUse },
17+
{ kind: 'inventory' },
18+
{ kind: 'inventory', use: inventoryUse, legacy: true },
19+
{ kind: 'device-runtime', use: { required: [], preferred: [] }, inventory: true },
20+
{ kind: 'device-runtime', use: { required: ['capture', 'capture'], preferred: [] } },
21+
{ kind: 'device-runtime', use: { required: ['capture'], preferred: ['capture'] } },
22+
])('rejects neither, mixed, widened, duplicate, or overlapping declarations: %j', (value) => {
23+
expect(() => assertCommandPlatformExecution(value)).toThrow(/exactly one/);
24+
});
25+
});

0 commit comments

Comments
 (0)