From 7b6bd5474d5e0700453010c9ae2c5c42a5e4466b Mon Sep 17 00:00:00 2001 From: artin Date: Thu, 3 Sep 2026 18:10:24 +0800 Subject: [PATCH 01/11] feat(remote): add HarmonyOS proxy lease backend Signed-off-by: Ark --- packages/kernel/src/contracts.ts | 4 ++-- src/cli/commands/connection-runtime.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/kernel/src/contracts.ts b/packages/kernel/src/contracts.ts index 7581203438..1a57bd3a63 100644 --- a/packages/kernel/src/contracts.ts +++ b/packages/kernel/src/contracts.ts @@ -5,7 +5,7 @@ export { defaultHintForCode, normalizeError } from './errors.ts'; import type { PlatformSelector } from './device.ts'; export type SessionRuntimeHints = { - platform?: 'ios' | 'android'; + platform?: 'ios' | 'android' | 'harmonyos'; metroHost?: string; metroPort?: number; bundleUrl?: string; @@ -44,7 +44,7 @@ export type LocalInstallSource = Extract Date: Thu, 3 Sep 2026 18:17:44 +0800 Subject: [PATCH 02/11] feat(remote): add HarmonyOS proxy lease backend Signed-off-by: Ark --- src/cli/commands/connection-runtime.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cli/commands/connection-runtime.ts b/src/cli/commands/connection-runtime.ts index fd1fa4cea6..1621c3050b 100644 --- a/src/cli/commands/connection-runtime.ts +++ b/src/cli/commands/connection-runtime.ts @@ -689,6 +689,7 @@ export function resolveRequestedLeaseBackend(flags: CliFlags): LeaseBackend | un if (flags.leaseBackend) return flags.leaseBackend; if (flags.platform === 'android') return 'android-instance'; if (flags.platform === 'ios') return 'ios-instance'; + if (flags.platform === 'harmonyos') return 'harmonyos-instance'; return undefined; } @@ -697,7 +698,7 @@ function requireRequestedLeaseBackend(flags: CliFlags, command: string): LeaseBa if (leaseBackend) return leaseBackend; throw new AppError( 'INVALID_ARGS', - `${command} requires --platform ios|android or --lease-backend when the remote connection has not resolved a lease yet.`, + `${command} requires --platform ios|android|harmonyos or --lease-backend when the remote connection has not resolved a lease yet.`, ); } From 3df3eac38dad413f47bcfbeeea6163aa28feb2a1 Mon Sep 17 00:00:00 2001 From: artin Date: Thu, 3 Sep 2026 20:14:59 +0800 Subject: [PATCH 03/11] feat(remote): complete HarmonyOS lease backend wiring Signed-off-by: Ark --- src/__tests__/remote-connection.test.ts | 6 ++++++ src/cli/commands/connection-runtime.ts | 2 +- src/commands/cli-grammar/flag-definitions-connection.ts | 4 ++-- src/remote/remote-config-schema.ts | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/__tests__/remote-connection.test.ts b/src/__tests__/remote-connection.test.ts index c97a819e47..58486f992f 100644 --- a/src/__tests__/remote-connection.test.ts +++ b/src/__tests__/remote-connection.test.ts @@ -34,7 +34,9 @@ import { materializeRemoteConnectionForCommand, CLOUD_WEBDRIVER_REMOTE_LEASE_TTL_MS, PROXY_REMOTE_LEASE_TTL_MS, + resolveRequestedLeaseBackend, } from '../cli/commands/connection-runtime.ts'; + import { stopMetroCompanion } from '../metro/client-metro-companion.ts'; import { AppError } from '@agent-device/kernel/errors'; import { @@ -45,6 +47,10 @@ import { } from '../remote/remote-connection-state.ts'; import type { AgentDeviceClient } from '../agent-device-client.ts'; +test('HarmonyOS platform resolves to its proxy lease backend', () => { + assert.equal(resolveRequestedLeaseBackend({ platform: 'harmonyos' } as never), 'harmonyos-instance'); +}); + afterEach(() => { vi.clearAllMocks(); vi.restoreAllMocks(); diff --git a/src/cli/commands/connection-runtime.ts b/src/cli/commands/connection-runtime.ts index 1621c3050b..a1844cd4a2 100644 --- a/src/cli/commands/connection-runtime.ts +++ b/src/cli/commands/connection-runtime.ts @@ -734,7 +734,7 @@ function isRuntimeCompatibleWithPlatform( runtime: SessionRuntimeHints, platform: CliFlags['platform'], ): boolean { - if (!runtime.platform || !platform || (platform !== 'ios' && platform !== 'android')) { + if (!runtime.platform || !platform) { return true; } return runtime.platform === platform; diff --git a/src/commands/cli-grammar/flag-definitions-connection.ts b/src/commands/cli-grammar/flag-definitions-connection.ts index c59893b4f3..d2ab5cfd66 100644 --- a/src/commands/cli-grammar/flag-definitions-connection.ts +++ b/src/commands/cli-grammar/flag-definitions-connection.ts @@ -105,8 +105,8 @@ export const CONNECTION_FLAG_DEFINITIONS: readonly FlagDefinition[] = [ key: 'leaseBackend', names: ['--lease-backend'], type: 'enum', - enumValues: ['ios-simulator', 'ios-instance', 'android-instance'], - usageLabel: '--lease-backend ios-simulator|ios-instance|android-instance', + enumValues: ['ios-simulator', 'ios-instance', 'android-instance', 'harmonyos-instance'], + usageLabel: '--lease-backend ios-simulator|ios-instance|android-instance|harmonyos-instance', usageDescription: 'Lease backend for remote tenant connection admission', }, { diff --git a/src/remote/remote-config-schema.ts b/src/remote/remote-config-schema.ts index 6c33f206dd..f3cfc27527 100644 --- a/src/remote/remote-config-schema.ts +++ b/src/remote/remote-config-schema.ts @@ -61,7 +61,7 @@ export const REMOTE_CONFIG_FIELD_SPECS = [ { key: 'leaseBackend', type: 'enum', - enumValues: ['ios-simulator', 'ios-instance', 'android-instance'], + enumValues: ['ios-simulator', 'ios-instance', 'android-instance', 'harmonyos-instance'], }, { key: 'platform', type: 'enum', enumValues: PLATFORM_SELECTORS }, { key: 'target', type: 'enum', enumValues: ['mobile', 'tv', 'desktop'] }, From d58fd1c53030c8a8653cbf0a3e2135dbf794a2ad Mon Sep 17 00:00:00 2001 From: artin Date: Thu, 3 Sep 2026 20:20:16 +0800 Subject: [PATCH 04/11] feat(remote): complete HarmonyOS lease backend wiring Signed-off-by: Ark --- CHANGELOG.md | 3 +++ src/daemon/__tests__/lease-registry.test.ts | 5 +++++ src/daemon/lease-registry-scope.ts | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b3ae3024b..eede8e5948 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,9 @@ are unchanged at schema v2. Two notes for mixed installations: a daemon older than this release reads a v3 file as an unreadable claim record and fails closed rather than clearing it, and `devices` reports no `claimedBy` for such a device until the managed-inventory filter lands. +- Added the `harmonyos-instance` remote lease backend so HarmonyOS proxy devices can participate in + the same explicit allocation, heartbeat, and close lifecycle as other remote instances (#2266). + - Fixed: `settings airplane on|off` now takes an Android device offline. It is applied through the connectivity service (`cmd connectivity airplane-mode`), which drives the radios, instead of writing `airplane_mode_on` and broadcasting `ACTION_AIRPLANE_MODE_CHANGED` — a broadcast Android diff --git a/src/daemon/__tests__/lease-registry.test.ts b/src/daemon/__tests__/lease-registry.test.ts index 61dbffbe65..fe3e6c8141 100644 --- a/src/daemon/__tests__/lease-registry.test.ts +++ b/src/daemon/__tests__/lease-registry.test.ts @@ -2,6 +2,7 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; import { createRequestCanceledError } from '@agent-device/kernel/errors'; import { LeaseRegistry } from '../lease-registry.ts'; +import { normalizeLeaseBackend } from '../lease-registry-scope.ts'; import { HUMAN_CONTROL_LEASE_REQUEST, HUMAN_CONTROL_SCOPE, @@ -9,6 +10,10 @@ import { createControlLatch, } from './human-control-fixtures.ts'; +test('normalizeLeaseBackend accepts HarmonyOS instance backend', () => { + assert.equal(normalizeLeaseBackend('harmonyos-instance'), 'harmonyos-instance'); +}); + test('allocateLease creates lease and enforces tenant/run validation', () => { const registry = new LeaseRegistry(); const lease = registry.allocateLease({ diff --git a/src/daemon/lease-registry-scope.ts b/src/daemon/lease-registry-scope.ts index 614afd2449..f3d6ca59e3 100644 --- a/src/daemon/lease-registry-scope.ts +++ b/src/daemon/lease-registry-scope.ts @@ -133,7 +133,7 @@ export function normalizeRequiredLeaseId(raw: string | undefined): string { export function normalizeLeaseBackend(raw: string | undefined): LeaseBackend { const value = (raw ?? '').trim().toLowerCase(); if (!value || value === 'ios-simulator') return 'ios-simulator'; - if (value === 'ios-instance' || value === 'android-instance') return value; + if (value === 'ios-instance' || value === 'android-instance' || value === 'harmonyos-instance') return value; throw new AppError('INVALID_ARGS', `Unsupported lease backend: ${raw ?? ''}`); } From 9f8007a8917f4032c5ac5f7298a5da109ba5a15f Mon Sep 17 00:00:00 2001 From: artin Date: Thu, 3 Sep 2026 20:24:29 +0800 Subject: [PATCH 05/11] fix(remote): accept Harmony runtime hints Signed-off-by: Ark --- packages/kernel/src/contracts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kernel/src/contracts.ts b/packages/kernel/src/contracts.ts index 1a57bd3a63..17a100dc90 100644 --- a/packages/kernel/src/contracts.ts +++ b/packages/kernel/src/contracts.ts @@ -272,7 +272,7 @@ function optionalEnum( export const daemonRuntimeSchema = schema((input, path) => { const record = expectObject(input, path); return { - platform: optionalEnum(record, 'platform', ['ios', 'android'] as const, path), + platform: optionalEnum(record, 'platform', ['ios', 'android', 'harmonyos'] as const, path), metroHost: optionalString(record, 'metroHost', path), metroPort: optionalInteger(record, 'metroPort', path), bundleUrl: optionalString(record, 'bundleUrl', path), From 64684121fc4c292acf5ea22dfbb388ae228aaac8 Mon Sep 17 00:00:00 2001 From: artin Date: Thu, 3 Sep 2026 21:02:46 +0800 Subject: [PATCH 06/11] fix(remote): complete Harmony runtime lease plumbing Signed-off-by: Ark --- CHANGELOG.md | 5 +++-- src/cli/commands/connection-runtime.ts | 6 +++++- src/daemon/session-runtime.ts | 6 +++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eede8e5948..281a38e7a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,8 +44,9 @@ are unchanged at schema v2. Two notes for mixed installations: a daemon older than this release reads a v3 file as an unreadable claim record and fails closed rather than clearing it, and `devices` reports no `claimedBy` for such a device until the managed-inventory filter lands. -- Added the `harmonyos-instance` remote lease backend so HarmonyOS proxy devices can participate in - the same explicit allocation, heartbeat, and close lifecycle as other remote instances (#2266). +- Added the `harmonyos-instance` lease contract and CLI/runtime plumbing as a prerequisite for + HarmonyOS proxy support; provider/daemon allocation remains gated until its end-to-end lifecycle + is implemented and validated (#2266). - Fixed: `settings airplane on|off` now takes an Android device offline. It is applied through the connectivity service (`cmd connectivity airplane-mode`), which drives the radios, instead of diff --git a/src/cli/commands/connection-runtime.ts b/src/cli/commands/connection-runtime.ts index a1844cd4a2..52cf26c1f3 100644 --- a/src/cli/commands/connection-runtime.ts +++ b/src/cli/commands/connection-runtime.ts @@ -734,7 +734,11 @@ function isRuntimeCompatibleWithPlatform( runtime: SessionRuntimeHints, platform: CliFlags['platform'], ): boolean { - if (!runtime.platform || !platform) { + if ( + !runtime.platform || + !platform || + (platform !== 'ios' && platform !== 'android' && platform !== 'harmonyos') + ) { return true; } return runtime.platform === platform; diff --git a/src/daemon/session-runtime.ts b/src/daemon/session-runtime.ts index 4387a934e5..7465a4aec5 100644 --- a/src/daemon/session-runtime.ts +++ b/src/daemon/session-runtime.ts @@ -87,10 +87,10 @@ function normalizeRuntimePlatformInput( platform?: RuntimePlatform, ): RuntimePlatform | undefined { if (value === undefined) return platform; - if (value !== 'ios' && value !== 'android') { + if (value !== 'ios' && value !== 'android' && value !== 'harmonyos') { throw new AppError( 'INVALID_ARGS', - `Invalid open runtime platform: ${String(value)}. Use "ios" or "android".`, + `Invalid open runtime platform: ${String(value)}. Use "ios", "android", or "harmonyos".`, ); } if (platform && value !== platform) { @@ -105,7 +105,7 @@ function normalizeRuntimePlatformInput( export function toRuntimePlatform( platform: CommandFlags['platform'] | DeviceInfo['platform'] | 'apple' | undefined, ): RuntimePlatform | undefined { - if (platform === 'ios' || platform === 'android') { + if (platform === 'ios' || platform === 'android' || platform === 'harmonyos') { return platform; } return undefined; From 0e172c894f9f04b42aba4dc8a32ab47ecca79296 Mon Sep 17 00:00:00 2001 From: artin Date: Thu, 3 Sep 2026 21:14:39 +0800 Subject: [PATCH 07/11] test(wire): acknowledge Harmony lease additions Signed-off-by: Ark --- test/wire-compat/ledger.json | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/test/wire-compat/ledger.json b/test/wire-compat/ledger.json index 63f0071c03..b100ea2f07 100644 --- a/test/wire-compat/ledger.json +++ b/test/wire-compat/ledger.json @@ -26,16 +26,16 @@ "packages/kernel/src/contracts.ts#DaemonResponseData": "sha256:f16e61b7f0ea8cdae82349508383af2f97a4c16c8aa21952d833094673f86afc", "packages/kernel/src/contracts.ts#JsonRpcId": "sha256:5a5fdff1cd85971214117d69a8278d0ecaf21a6fa3f4582155d9b3d9cad1fbd0", "packages/kernel/src/contracts.ts#JsonRpcRequestEnvelope": "sha256:d6328309fc4f8b310b88680181e46c802329de9b0e45682dd2e0f0c3e8d090e4", - "packages/kernel/src/contracts.ts#LEASE_BACKENDS": "sha256:9fb296dcfd56556e4dd020bcb40f3b70890b274600e634aa2c380d860b38c022", + "packages/kernel/src/contracts.ts#LEASE_BACKENDS": "sha256:6bec996666d1043a8d23e60d5b1e50ef9341909a02f19060a3132bb72dcc3155", "packages/kernel/src/contracts.ts#LeaseBackend": "sha256:dce397343cda82ce4e3e43b534b1f642cd37568ed304f4783caddc3524c1cae5", "packages/kernel/src/contracts.ts#RESPONSE_LEVELS": "sha256:cef29c3b56c299e35a7fee8058b90dc4a6f65a46a420e4f11f77c8ca7d0359c8", "packages/kernel/src/contracts.ts#ResponseCost": "sha256:2a44de54e8b5c886b36da1500e22d634ec7e12961d2c93b689d2b1aa8c15f186", "packages/kernel/src/contracts.ts#ResponseLevel": "sha256:c7329d28e745b5e69b86aa1a686befa314b98248afbe5e0e2d7d395f0d777f9b", "packages/kernel/src/contracts.ts#SESSION_ISOLATION_MODES": "sha256:3d4b3048e22bb0ceccc98cf8fd93a87a1a1e2dad96fa03cffff6d0ecedb58689", "packages/kernel/src/contracts.ts#SessionIsolationMode": "sha256:beea941fe57b000902da28af15a27aa18d0c957408209e3ec372fcf7f605fa7c", - "packages/kernel/src/contracts.ts#SessionRuntimeHints": "sha256:6b945688ad5ab68370ec001d962974e1397c39d47c6f7ca2a65b03f5ccdd91c6", + "packages/kernel/src/contracts.ts#SessionRuntimeHints": "sha256:7e055a99bc6adbb3efc37b0691055f5370f35f8b96ae6af73295ccce61e05dfe", "packages/kernel/src/contracts.ts#commandRpcParamsSchema": "sha256:d0ce05c236ce5b0b52631ab7b8a75d38859fbfba58cfb28dd3f9d84f2dba2c1f", - "packages/kernel/src/contracts.ts#daemonRuntimeSchema": "sha256:010c171164ce9516e1c1b00e89e698a11842ff9c3bac3f2e8e3814de7c8be78c", + "packages/kernel/src/contracts.ts#daemonRuntimeSchema": "sha256:859eabaf6679d117feb2b4873b6ac69731cc06989645080817d39c43a24deea5", "packages/kernel/src/contracts.ts#jsonRpcRequestSchema": "sha256:67e6b8a28b39a3883424a565ae7033c336dc2ea6b193e9b32bb98eb3e18dca7c", "packages/kernel/src/device.ts#PLATFORM_SELECTORS": "sha256:36e9da1cc660c0ddfb4cf52521f7f230341ff20e3ebda3405ba7c1799476c42d", "packages/kernel/src/device.ts#PlatformSelector": "sha256:61de3f003507ea2f53b396b0146c17670bf674a2db2132e19c462ca6c10cc8fd", @@ -283,6 +283,21 @@ "declaration": "src/daemon/request-diagnostics-http.ts#handleRequestDiagnostics", "digest": "sha256:32c3d100176dd99ecbb61f8050d05b1cda64b7be1944a2cd16633e83fa78660b", "rationale": "#2198 context: asks isTenantAddressableSessionName instead of applying the prefix rule locally. Same UNAUTHORIZED code, same 'Session is outside this tenant' message and same 401 body for the attested-tenant case the rule still refuses; the only difference a peer can observe is that an unattested tenant now gets the 200 + ndjson record it already knows how to read instead of a 401 it could not act on." + }, + { + "declaration": "packages/kernel/src/contracts.ts#LEASE_BACKENDS", + "digest": "sha256:6bec996666d1043a8d23e60d5b1e50ef9341909a02f19060a3132bb72dcc3155", + "rationale": "#2266 adds the additive harmonyos-instance lease backend literal. Protocol-2 peers ignore the new backend unless explicitly requested; existing iOS/Android values and payloads remain unchanged." + }, + { + "declaration": "packages/kernel/src/contracts.ts#SessionRuntimeHints", + "digest": "sha256:7e055a99bc6adbb3efc37b0691055f5370f35f8b96ae6af73295ccce61e05dfe", + "rationale": "#2266 adds the optional HarmonyOS platform value to runtime hints. Existing iOS/Android hints retain their shape and released peers can ignore the additive value." + }, + { + "declaration": "packages/kernel/src/contracts.ts#daemonRuntimeSchema", + "digest": "sha256:859eabaf6679d117feb2b4873b6ac69731cc06989645080817d39c43a24deea5", + "rationale": "#2266 broadens runtime-hint validation to accept the additive HarmonyOS platform value; existing protocol-2 runtime hints remain valid and unchanged." } ] } From 0697f9cfda11bfc0f26a25061fa852ea492d0749 Mon Sep 17 00:00:00 2001 From: artin Date: Thu, 3 Sep 2026 21:46:33 +0800 Subject: [PATCH 08/11] test(remote): close HarmonyOS lease review gaps Signed-off-by: Ark --- src/__tests__/remote-connection.test.ts | 7 ++++++- src/daemon/__tests__/lease-registry-scope.test.ts | 5 +++++ src/daemon/__tests__/lease-registry.test.ts | 5 ----- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/__tests__/remote-connection.test.ts b/src/__tests__/remote-connection.test.ts index 58486f992f..216999ddf7 100644 --- a/src/__tests__/remote-connection.test.ts +++ b/src/__tests__/remote-connection.test.ts @@ -48,7 +48,12 @@ import { import type { AgentDeviceClient } from '../agent-device-client.ts'; test('HarmonyOS platform resolves to its proxy lease backend', () => { - assert.equal(resolveRequestedLeaseBackend({ platform: 'harmonyos' } as never), 'harmonyos-instance'); + assert.equal( + resolveRequestedLeaseBackend( + forceConnectFlags({ stateDir: '/tmp/agent-device', remoteConfig: '/tmp/remote.json', platform: 'harmonyos' }), + ), + 'harmonyos-instance', + ); }); afterEach(() => { diff --git a/src/daemon/__tests__/lease-registry-scope.test.ts b/src/daemon/__tests__/lease-registry-scope.test.ts index 1bb76af552..e89f0fc417 100644 --- a/src/daemon/__tests__/lease-registry-scope.test.ts +++ b/src/daemon/__tests__/lease-registry-scope.test.ts @@ -4,9 +4,14 @@ import { createLeaseTtlResolver, leaseDeviceBindingKey, normalizeAllocateLeaseRequest, + normalizeLeaseBackend, } from '../lease-registry-scope.ts'; import { HUMAN_CONTROL_LEASE_REQUEST, HUMAN_CONTROL_SCOPE } from './human-control-fixtures.ts'; +test('normalizeLeaseBackend accepts HarmonyOS instance backend', () => { + assert.equal(normalizeLeaseBackend('harmonyos-instance'), 'harmonyos-instance'); +}); + test('allocation and human control share the exact contention identity', () => { const scope = normalizeAllocateLeaseRequest(HUMAN_CONTROL_LEASE_REQUEST); assert.equal(leaseDeviceBindingKey(scope), leaseDeviceBindingKey(HUMAN_CONTROL_SCOPE)); diff --git a/src/daemon/__tests__/lease-registry.test.ts b/src/daemon/__tests__/lease-registry.test.ts index fe3e6c8141..61dbffbe65 100644 --- a/src/daemon/__tests__/lease-registry.test.ts +++ b/src/daemon/__tests__/lease-registry.test.ts @@ -2,7 +2,6 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; import { createRequestCanceledError } from '@agent-device/kernel/errors'; import { LeaseRegistry } from '../lease-registry.ts'; -import { normalizeLeaseBackend } from '../lease-registry-scope.ts'; import { HUMAN_CONTROL_LEASE_REQUEST, HUMAN_CONTROL_SCOPE, @@ -10,10 +9,6 @@ import { createControlLatch, } from './human-control-fixtures.ts'; -test('normalizeLeaseBackend accepts HarmonyOS instance backend', () => { - assert.equal(normalizeLeaseBackend('harmonyos-instance'), 'harmonyos-instance'); -}); - test('allocateLease creates lease and enforces tenant/run validation', () => { const registry = new LeaseRegistry(); const lease = registry.allocateLease({ From 4c3ad09ccb033f0f51cb57d1981462bf4243bc1c Mon Sep 17 00:00:00 2001 From: artin Date: Thu, 3 Sep 2026 21:53:47 +0800 Subject: [PATCH 09/11] fix(runtime): update HarmonyOS support error text Signed-off-by: Ark --- src/daemon/session-runtime.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/daemon/session-runtime.ts b/src/daemon/session-runtime.ts index 7465a4aec5..a33b7fd539 100644 --- a/src/daemon/session-runtime.ts +++ b/src/daemon/session-runtime.ts @@ -216,7 +216,7 @@ function resolveSessionRuntimeHints( if (runtime.platform && device && !deviceRuntimePlatform) { throw new AppError( 'INVALID_ARGS', - `Session runtime hints are only supported on iOS and Android sessions, but session "${sessionName}" is bound to ${boundPlatform}.`, + `Session runtime hints are only supported on iOS, Android, and HarmonyOS sessions, but session "${sessionName}" is bound to ${boundPlatform}.`, ); } if (runtime.platform && deviceRuntimePlatform && runtime.platform !== deviceRuntimePlatform) { From fa0b33c679820943bf15e7228cba3d0dedcfa033 Mon Sep 17 00:00:00 2001 From: Ark Date: Tue, 8 Sep 2026 01:47:21 +0800 Subject: [PATCH 10/11] fix(remote): preserve HarmonyOS runtime and proxy device identity Signed-off-by: Ark --- .../internal/__tests__/script-utils.test.ts | 19 +++ .../ad-script/src/internal/script-utils.ts | 32 +---- packages/kernel/src/contracts.ts | 18 ++- .../remote-connection-harmonyos.test.ts | 129 ++++++++++++++++++ src/__tests__/remote-connection.test.ts | 6 +- src/cli/commands/connection-runtime.ts | 14 +- src/client/client-normalizers.test.ts | 20 ++- src/client/client-normalizers.ts | 4 +- src/daemon/__tests__/session-runtime.test.ts | 31 +++++ .../handlers/session-runtime-command.ts | 2 +- src/daemon/lease-registry-scope.ts | 3 +- src/daemon/session-runtime.ts | 5 +- test/wire-compat/ledger.json | 16 ++- test/wire-compat/surface.ts | 2 + 14 files changed, 250 insertions(+), 51 deletions(-) create mode 100644 packages/ad-script/src/internal/__tests__/script-utils.test.ts create mode 100644 src/__tests__/remote-connection-harmonyos.test.ts create mode 100644 src/daemon/__tests__/session-runtime.test.ts diff --git a/packages/ad-script/src/internal/__tests__/script-utils.test.ts b/packages/ad-script/src/internal/__tests__/script-utils.test.ts new file mode 100644 index 0000000000..ebb9fd1901 --- /dev/null +++ b/packages/ad-script/src/internal/__tests__/script-utils.test.ts @@ -0,0 +1,19 @@ +import { expect, test } from 'vitest'; +import type { SessionAction } from '@agent-device/contracts/session'; +import { formatPortableActionLine } from '../script-formatting.ts'; +import { parseReplayScriptDetailed } from '../script.ts'; + +test.each(['ios', 'android', 'harmonyos'] as const)( + '%s runtime survives open and runtime set script roundtrips', + (platform) => { + const runtime = { platform, metroHost: 'localhost', metroPort: 8081 }; + const actions: SessionAction[] = [ + { ts: 0, command: 'runtime', positionals: ['set'], flags: runtime }, + { ts: 1, command: 'open', positionals: ['Demo'], flags: {}, runtime }, + ]; + const script = actions.map((action) => formatPortableActionLine(action)).join('\n'); + const parsed = parseReplayScriptDetailed(script); + expect(parsed.actions[0]?.flags).toMatchObject(runtime); + expect(parsed.actions[1]?.runtime).toMatchObject(runtime); + }, +); diff --git a/packages/ad-script/src/internal/script-utils.ts b/packages/ad-script/src/internal/script-utils.ts index d9566b1328..87f27f4e14 100644 --- a/packages/ad-script/src/internal/script-utils.ts +++ b/packages/ad-script/src/internal/script-utils.ts @@ -1,4 +1,5 @@ import type { SessionAction } from '@agent-device/contracts/session'; +import { isSessionRuntimePlatform, type SessionRuntimeHints } from '@agent-device/kernel/contracts'; import { appendScreenshotScriptFlags } from '@agent-device/contracts/capture'; import { splitRefGenerationSuffix } from '@agent-device/kernel/snapshot'; @@ -164,19 +165,10 @@ export function appendScriptSeriesFlags( export function appendRuntimeHintFlags( parts: string[], - flags: - | Pick['flags'] - | { - platform?: 'ios' | 'android'; - metroHost?: string; - metroPort?: number; - bundleUrl?: string; - launchUrl?: string; - } - | undefined, + flags: Pick['flags'] | SessionRuntimeHints | undefined, ): void { if (!flags) return; - if (flags.platform === 'ios' || flags.platform === 'android') { + if (isSessionRuntimePlatform(flags.platform)) { parts.push('--platform', flags.platform); } if (typeof flags.metroHost === 'string' && flags.metroHost.length > 0) { @@ -323,29 +315,17 @@ export function parseReplaySeriesFlags( // fallow-ignore-next-line complexity export function parseReplayRuntimeFlags(args: string[]): { positionals: string[]; - flags: { - platform?: 'ios' | 'android'; - metroHost?: string; - metroPort?: number; - bundleUrl?: string; - launchUrl?: string; - }; + flags: SessionRuntimeHints; } { const positionals: string[] = []; - const flags: { - platform?: 'ios' | 'android'; - metroHost?: string; - metroPort?: number; - bundleUrl?: string; - launchUrl?: string; - } = {}; + const flags: SessionRuntimeHints = {}; for (let index = 0; index < args.length; index += 1) { const token = args[index]!; const nextArg = args[index + 1]; if (token === '--platform' && nextArg !== undefined) { const platform = nextArg; - if (platform === 'ios' || platform === 'android') { + if (isSessionRuntimePlatform(platform)) { flags.platform = platform; } index += 1; diff --git a/packages/kernel/src/contracts.ts b/packages/kernel/src/contracts.ts index 17a100dc90..104a5c1f5d 100644 --- a/packages/kernel/src/contracts.ts +++ b/packages/kernel/src/contracts.ts @@ -4,8 +4,15 @@ export type { AppErrorCode } from './errors.ts'; export { defaultHintForCode, normalizeError } from './errors.ts'; import type { PlatformSelector } from './device.ts'; +const SESSION_RUNTIME_PLATFORMS = ['ios', 'android', 'harmonyos'] as const; +export type SessionRuntimePlatform = (typeof SESSION_RUNTIME_PLATFORMS)[number]; + +export function isSessionRuntimePlatform(value: unknown): value is SessionRuntimePlatform { + return SESSION_RUNTIME_PLATFORMS.some((platform) => platform === value); +} + export type SessionRuntimeHints = { - platform?: 'ios' | 'android' | 'harmonyos'; + platform?: SessionRuntimePlatform; metroHost?: string; metroPort?: number; bundleUrl?: string; @@ -44,7 +51,12 @@ export type LocalInstallSource = Extract( export const daemonRuntimeSchema = schema((input, path) => { const record = expectObject(input, path); return { - platform: optionalEnum(record, 'platform', ['ios', 'android', 'harmonyos'] as const, path), + platform: optionalEnum(record, 'platform', SESSION_RUNTIME_PLATFORMS, path), metroHost: optionalString(record, 'metroHost', path), metroPort: optionalInteger(record, 'metroPort', path), bundleUrl: optionalString(record, 'bundleUrl', path), diff --git a/src/__tests__/remote-connection-harmonyos.test.ts b/src/__tests__/remote-connection-harmonyos.test.ts new file mode 100644 index 0000000000..5eec997f71 --- /dev/null +++ b/src/__tests__/remote-connection-harmonyos.test.ts @@ -0,0 +1,129 @@ +import { expect, test } from 'vitest'; +import fs from 'node:fs'; +import { + connectionWorkspace, + createTestClient, + seedConnectionState, +} from './remote-connection.fixtures.ts'; +import { materializeRemoteConnectionForCommand } from '../cli/commands/connection-runtime.ts'; +import { disconnectCommand } from '../cli/commands/connection.ts'; +import { readRemoteConnectionState } from '../remote/remote-connection-state.ts'; +import { LeaseRegistry } from '../daemon/lease-registry.ts'; + +test.each(['apple', 'harmonyos', 'ios', 'android'] as const)( + 'stored Harmony runtime compatibility respects %s selection', + async (platform) => { + const { stateDir, remoteConfigPath } = connectionWorkspace('harmonyos-runtime-'); + fs.writeFileSync(remoteConfigPath, JSON.stringify({ daemonBaseUrl: 'https://daemon.example' })); + seedConnectionState({ + stateDir, + state: { + session: 'runtime-compat', + remoteConfigPath, + daemon: { baseUrl: 'https://daemon.example' }, + tenant: 'proxy', + runId: 'compat-run', + leaseId: 'compat-existing', + leaseBackend: 'harmonyos-instance', + runtime: { platform: 'harmonyos', launchUrl: 'demo://open' }, + }, + }); + const materialized = await materializeRemoteConnectionForCommand({ + command: 'snapshot', + client: createTestClient(), + flags: { + json: true, + help: false, + version: false, + stateDir, + remoteConfig: remoteConfigPath, + session: 'runtime-compat', + platform, + }, + }); + if (platform === 'apple' || platform === 'harmonyos') { + expect(materialized.runtime?.platform).toBe('harmonyos'); + } else { + expect(materialized.runtime).toBeUndefined(); + } + }, +); + +test('proxy HarmonyOS inventory materializes a scoped lease and closes that same lease', async () => { + const { stateDir, remoteConfigPath } = connectionWorkspace('harmonyos-proxy-'); + fs.writeFileSync(remoteConfigPath, JSON.stringify({ daemonBaseUrl: 'https://daemon.example' })); + seedConnectionState({ + stateDir, + state: { + session: 'harmony-proxy', + remoteConfigPath, + daemon: { baseUrl: 'https://daemon.example' }, + tenant: 'proxy', + runId: 'harmony-run', + leaseProvider: 'proxy', + clientId: 'harmony-client', + }, + }); + const registry = new LeaseRegistry(); + const allocated: string[] = []; + const released: string[] = []; + const client = createTestClient({ + listDevices: async () => [ + { + platform: 'harmonyos', + target: 'mobile', + kind: 'emulator', + id: '127.0.0.1:16001', + name: 'Harmony Emulator', + booted: true, + identifiers: { serial: '127.0.0.1:16001' }, + harmonyos: { serial: '127.0.0.1:16001' }, + }, + ], + allocate: async (request) => { + expect(request.leaseBackend).toBe('harmonyos-instance'); + expect(request.deviceKey).toBe('harmonyos:mobile:127.0.0.1:16001'); + const lease = registry.allocateLease({ ...request, tenantId: request.tenant }); + allocated.push(lease.leaseId); + return lease; + }, + heartbeat: async (request) => registry.heartbeatLease({ ...request, tenantId: request.tenant }), + release: async (request) => { + const result = registry.releaseLease({ ...request, tenantId: request.tenant }); + expect(result.released).toBe(true); + released.push(request.leaseId); + return result; + }, + }); + const flags = { + json: true, + help: false, + version: false, + stateDir, + remoteConfig: remoteConfigPath, + session: 'harmony-proxy', + }; + const materialized = await materializeRemoteConnectionForCommand({ + command: 'open', + flags, + client, + }); + expect(materialized.flags.platform).toBe('harmonyos'); + expect(materialized.flags.serial).toBe('127.0.0.1:16001'); + expect(materialized.flags.leaseBackend).toBe('harmonyos-instance'); + expect(materialized.flags.leaseId).toBe(allocated[0]); + expect(readRemoteConnectionState({ stateDir, session: flags.session })?.leaseId).toBe( + allocated[0], + ); + expect(() => + registry.assertLeaseAdmission({ + leaseId: allocated[0], + tenantId: 'proxy', + runId: 'harmony-run', + leaseBackend: 'android-instance', + }), + ).toThrow(); + await disconnectCommand({ positionals: [], flags, client }); + expect(released).toEqual(allocated); + expect(readRemoteConnectionState({ stateDir, session: flags.session })).toBeNull(); +}); diff --git a/src/__tests__/remote-connection.test.ts b/src/__tests__/remote-connection.test.ts index 216999ddf7..cf3ae4d3c3 100644 --- a/src/__tests__/remote-connection.test.ts +++ b/src/__tests__/remote-connection.test.ts @@ -50,7 +50,11 @@ import type { AgentDeviceClient } from '../agent-device-client.ts'; test('HarmonyOS platform resolves to its proxy lease backend', () => { assert.equal( resolveRequestedLeaseBackend( - forceConnectFlags({ stateDir: '/tmp/agent-device', remoteConfig: '/tmp/remote.json', platform: 'harmonyos' }), + forceConnectFlags({ + stateDir: '/tmp/agent-device', + remoteConfig: '/tmp/remote.json', + platform: 'harmonyos', + }), ), 'harmonyos-instance', ); diff --git a/src/cli/commands/connection-runtime.ts b/src/cli/commands/connection-runtime.ts index 52cf26c1f3..987f218823 100644 --- a/src/cli/commands/connection-runtime.ts +++ b/src/cli/commands/connection-runtime.ts @@ -28,7 +28,11 @@ import { import { profileToCliFlags } from '../remote-config-flags.ts'; import type { BatchStep } from '@agent-device/contracts/client'; import { AppError } from '@agent-device/kernel/errors'; -import type { LeaseBackend, SessionRuntimeHints } from '@agent-device/kernel/contracts'; +import { + isSessionRuntimePlatform, + type LeaseBackend, + type SessionRuntimeHints, +} from '@agent-device/kernel/contracts'; import type { CliFlags } from '@agent-device/contracts/command'; import type { AgentDeviceClient, Lease } from '../../agent-device-client.ts'; import type { CloudProviderSessionResult } from '@agent-device/contracts/observability'; @@ -734,11 +738,7 @@ function isRuntimeCompatibleWithPlatform( runtime: SessionRuntimeHints, platform: CliFlags['platform'], ): boolean { - if ( - !runtime.platform || - !platform || - (platform !== 'ios' && platform !== 'android' && platform !== 'harmonyos') - ) { + if (!runtime.platform || !platform || !isSessionRuntimePlatform(platform)) { return true; } return runtime.platform === platform; @@ -892,7 +892,7 @@ function applyResolvedDeviceSelector(flags: CliFlags, device: DeviceInfo): void flags.udid = device.id; return; } - if (device.platform === 'android') { + if (device.platform === 'android' || device.platform === 'harmonyos') { flags.serial = device.id; } } diff --git a/src/client/client-normalizers.test.ts b/src/client/client-normalizers.test.ts index 43fc56511e..09b286295e 100644 --- a/src/client/client-normalizers.test.ts +++ b/src/client/client-normalizers.test.ts @@ -1,6 +1,24 @@ import assert from 'node:assert/strict'; import { expect, test } from 'vitest'; -import { normalizeDevice, normalizeOpenForegroundComposition } from './client-normalizers.ts'; +import { + normalizeDevice, + normalizeOpenForegroundComposition, + normalizeRuntimeHints, +} from './client-normalizers.ts'; + +test.each(['ios', 'android', 'harmonyos'])('runtime response preserves %s platform', (platform) => { + expect(normalizeRuntimeHints({ platform, launchUrl: 'demo://open' })).toMatchObject({ + platform, + launchUrl: 'demo://open', + }); +}); + +test.each(['apple', 'unknown', 12])( + 'runtime response ignores non-runtime platform %s', + (platform) => { + expect(normalizeRuntimeHints({ platform })?.platform).toBeUndefined(); + }, +); test('embedded daemon errors sanitize an untrusted cause before client exposure', () => { const secret = 'adc_live_remote-secret'; diff --git a/src/client/client-normalizers.ts b/src/client/client-normalizers.ts index 7dd9056a2f..d7df742208 100644 --- a/src/client/client-normalizers.ts +++ b/src/client/client-normalizers.ts @@ -17,7 +17,7 @@ import { isSerialAddressablePlatform, type AppleOS, } from '@agent-device/kernel/device'; -import type { SessionRuntimeHints } from '@agent-device/kernel/contracts'; +import { isSessionRuntimePlatform, type SessionRuntimeHints } from '@agent-device/kernel/contracts'; import { AppError, type DaemonError } from '@agent-device/kernel/errors'; import { sanitizeErrorCause } from '@agent-device/kernel/redaction'; import type { SnapshotNode } from '@agent-device/kernel/snapshot'; @@ -200,7 +200,7 @@ export function normalizeRuntimeHints(value: unknown): SessionRuntimeHints | und const bundleUrl = readOptionalString(value, 'bundleUrl'); const launchUrl = readOptionalString(value, 'launchUrl'); return { - platform: platform === 'ios' || platform === 'android' ? platform : undefined, + platform: isSessionRuntimePlatform(platform) ? platform : undefined, metroHost, metroPort, bundleUrl, diff --git a/src/daemon/__tests__/session-runtime.test.ts b/src/daemon/__tests__/session-runtime.test.ts new file mode 100644 index 0000000000..2435bf51bd --- /dev/null +++ b/src/daemon/__tests__/session-runtime.test.ts @@ -0,0 +1,31 @@ +import { expect, test } from 'vitest'; +import { daemonRuntimeSchema } from '@agent-device/kernel/contracts'; +import { resolveEffectiveOpenRuntimeHints, toRuntimePlatform } from '../session-runtime.ts'; + +test.each(['ios', 'android', 'harmonyos'] as const)( + '%s runtime survives wire admission and open normalization', + (platform) => { + const runtime = daemonRuntimeSchema.parse({ platform, launchUrl: 'demo://open' }); + expect(toRuntimePlatform(platform)).toBe(platform); + const normalized = resolveEffectiveOpenRuntimeHints({ + req: { token: 'test', session: 'runtime-test', command: 'open', positionals: [], runtime }, + sessionName: 'runtime-test', + platform, + sessionStore: { getRuntimeHints: () => undefined }, + }); + expect(normalized).toMatchObject(runtime); + expect(() => + resolveEffectiveOpenRuntimeHints({ + req: { token: 'test', session: 'runtime-test', command: 'open', positionals: [], runtime }, + sessionName: 'runtime-test', + platform: platform === 'ios' ? 'android' : 'ios', + sessionStore: { getRuntimeHints: () => undefined }, + }), + ).toThrow(); + }, +); + +test('non-leaf apple selector is not a runtime platform', () => { + expect(toRuntimePlatform('apple')).toBeUndefined(); + expect(() => daemonRuntimeSchema.parse({ platform: 'apple' })).toThrow(); +}); diff --git a/src/daemon/handlers/session-runtime-command.ts b/src/daemon/handlers/session-runtime-command.ts index 4d1025f9be..d68e2c9ef1 100644 --- a/src/daemon/handlers/session-runtime-command.ts +++ b/src/daemon/handlers/session-runtime-command.ts @@ -178,7 +178,7 @@ function setRuntimeCommand(params: { if (!platform) { return errorResponse( 'INVALID_ARGS', - 'runtime set only supports iOS and Android sessions. Pass --platform ios|android or open an iOS/Android session first.', + 'runtime set only supports iOS, Android, and HarmonyOS sessions. Pass --platform ios|android|harmonyos or open a supported session first.', ); } if (sessionLeaf !== undefined && sessionLeaf !== platform) { diff --git a/src/daemon/lease-registry-scope.ts b/src/daemon/lease-registry-scope.ts index f3d6ca59e3..290bf86729 100644 --- a/src/daemon/lease-registry-scope.ts +++ b/src/daemon/lease-registry-scope.ts @@ -133,7 +133,8 @@ export function normalizeRequiredLeaseId(raw: string | undefined): string { export function normalizeLeaseBackend(raw: string | undefined): LeaseBackend { const value = (raw ?? '').trim().toLowerCase(); if (!value || value === 'ios-simulator') return 'ios-simulator'; - if (value === 'ios-instance' || value === 'android-instance' || value === 'harmonyos-instance') return value; + if (value === 'ios-instance' || value === 'android-instance' || value === 'harmonyos-instance') + return value; throw new AppError('INVALID_ARGS', `Unsupported lease backend: ${raw ?? ''}`); } diff --git a/src/daemon/session-runtime.ts b/src/daemon/session-runtime.ts index a33b7fd539..2dea4fd944 100644 --- a/src/daemon/session-runtime.ts +++ b/src/daemon/session-runtime.ts @@ -4,6 +4,7 @@ import { hasRuntimeTransportHintValues, } from '@agent-device/contracts/application-lifecycle-runtime'; import { AppError, asAppError } from '@agent-device/kernel/errors'; +import { isSessionRuntimePlatform } from '@agent-device/kernel/contracts'; import { publicPlatformString, type DeviceInfo } from '@agent-device/kernel/device'; import type { DaemonRequest } from './daemon-request.ts'; import type { SessionRuntimeHints, SessionState } from './session-state.ts'; @@ -87,7 +88,7 @@ function normalizeRuntimePlatformInput( platform?: RuntimePlatform, ): RuntimePlatform | undefined { if (value === undefined) return platform; - if (value !== 'ios' && value !== 'android' && value !== 'harmonyos') { + if (!isSessionRuntimePlatform(value)) { throw new AppError( 'INVALID_ARGS', `Invalid open runtime platform: ${String(value)}. Use "ios", "android", or "harmonyos".`, @@ -105,7 +106,7 @@ function normalizeRuntimePlatformInput( export function toRuntimePlatform( platform: CommandFlags['platform'] | DeviceInfo['platform'] | 'apple' | undefined, ): RuntimePlatform | undefined { - if (platform === 'ios' || platform === 'android' || platform === 'harmonyos') { + if (isSessionRuntimePlatform(platform)) { return platform; } return undefined; diff --git a/test/wire-compat/ledger.json b/test/wire-compat/ledger.json index b100ea2f07..9ace1b3b7c 100644 --- a/test/wire-compat/ledger.json +++ b/test/wire-compat/ledger.json @@ -26,16 +26,18 @@ "packages/kernel/src/contracts.ts#DaemonResponseData": "sha256:f16e61b7f0ea8cdae82349508383af2f97a4c16c8aa21952d833094673f86afc", "packages/kernel/src/contracts.ts#JsonRpcId": "sha256:5a5fdff1cd85971214117d69a8278d0ecaf21a6fa3f4582155d9b3d9cad1fbd0", "packages/kernel/src/contracts.ts#JsonRpcRequestEnvelope": "sha256:d6328309fc4f8b310b88680181e46c802329de9b0e45682dd2e0f0c3e8d090e4", - "packages/kernel/src/contracts.ts#LEASE_BACKENDS": "sha256:6bec996666d1043a8d23e60d5b1e50ef9341909a02f19060a3132bb72dcc3155", + "packages/kernel/src/contracts.ts#LEASE_BACKENDS": "sha256:1b29207af3c9ec65b999e4d3f9ab839004472897f748f8d4f4ab964de54efc9f", "packages/kernel/src/contracts.ts#LeaseBackend": "sha256:dce397343cda82ce4e3e43b534b1f642cd37568ed304f4783caddc3524c1cae5", "packages/kernel/src/contracts.ts#RESPONSE_LEVELS": "sha256:cef29c3b56c299e35a7fee8058b90dc4a6f65a46a420e4f11f77c8ca7d0359c8", "packages/kernel/src/contracts.ts#ResponseCost": "sha256:2a44de54e8b5c886b36da1500e22d634ec7e12961d2c93b689d2b1aa8c15f186", "packages/kernel/src/contracts.ts#ResponseLevel": "sha256:c7329d28e745b5e69b86aa1a686befa314b98248afbe5e0e2d7d395f0d777f9b", "packages/kernel/src/contracts.ts#SESSION_ISOLATION_MODES": "sha256:3d4b3048e22bb0ceccc98cf8fd93a87a1a1e2dad96fa03cffff6d0ecedb58689", "packages/kernel/src/contracts.ts#SessionIsolationMode": "sha256:beea941fe57b000902da28af15a27aa18d0c957408209e3ec372fcf7f605fa7c", - "packages/kernel/src/contracts.ts#SessionRuntimeHints": "sha256:7e055a99bc6adbb3efc37b0691055f5370f35f8b96ae6af73295ccce61e05dfe", + "packages/kernel/src/contracts.ts#SESSION_RUNTIME_PLATFORMS": "sha256:6164fc8ca226b5beb3ba52a8dddd3c11633a8accec58b9f8972c44c8339a1bf7", + "packages/kernel/src/contracts.ts#SessionRuntimePlatform": "sha256:694b301564f2c9da5d252be7e26c20bd53eed715b27033c2162c2a11606a9092", + "packages/kernel/src/contracts.ts#SessionRuntimeHints": "sha256:94b4d9778b6bd3c5fb401fab6fe253473e4950f477e2d7c2fc02630ef7fdbb62", "packages/kernel/src/contracts.ts#commandRpcParamsSchema": "sha256:d0ce05c236ce5b0b52631ab7b8a75d38859fbfba58cfb28dd3f9d84f2dba2c1f", - "packages/kernel/src/contracts.ts#daemonRuntimeSchema": "sha256:859eabaf6679d117feb2b4873b6ac69731cc06989645080817d39c43a24deea5", + "packages/kernel/src/contracts.ts#daemonRuntimeSchema": "sha256:3b99926ce9deb5d66186a55b1168011dc1ae4d010ada5ac5d79c4c7225a1f80f", "packages/kernel/src/contracts.ts#jsonRpcRequestSchema": "sha256:67e6b8a28b39a3883424a565ae7033c336dc2ea6b193e9b32bb98eb3e18dca7c", "packages/kernel/src/device.ts#PLATFORM_SELECTORS": "sha256:36e9da1cc660c0ddfb4cf52521f7f230341ff20e3ebda3405ba7c1799476c42d", "packages/kernel/src/device.ts#PlatformSelector": "sha256:61de3f003507ea2f53b396b0146c17670bf674a2db2132e19c462ca6c10cc8fd", @@ -286,17 +288,17 @@ }, { "declaration": "packages/kernel/src/contracts.ts#LEASE_BACKENDS", - "digest": "sha256:6bec996666d1043a8d23e60d5b1e50ef9341909a02f19060a3132bb72dcc3155", + "digest": "sha256:1b29207af3c9ec65b999e4d3f9ab839004472897f748f8d4f4ab964de54efc9f", "rationale": "#2266 adds the additive harmonyos-instance lease backend literal. Protocol-2 peers ignore the new backend unless explicitly requested; existing iOS/Android values and payloads remain unchanged." }, { "declaration": "packages/kernel/src/contracts.ts#SessionRuntimeHints", - "digest": "sha256:7e055a99bc6adbb3efc37b0691055f5370f35f8b96ae6af73295ccce61e05dfe", - "rationale": "#2266 adds the optional HarmonyOS platform value to runtime hints. Existing iOS/Android hints retain their shape and released peers can ignore the additive value." + "digest": "sha256:94b4d9778b6bd3c5fb401fab6fe253473e4950f477e2d7c2fc02630ef7fdbb62", + "rationale": "#2266 derives the optional runtime platform from one shared vocabulary, adding harmonyos without changing existing iOS/Android payloads. Older peers can refuse the new value with the existing validation error." }, { "declaration": "packages/kernel/src/contracts.ts#daemonRuntimeSchema", - "digest": "sha256:859eabaf6679d117feb2b4873b6ac69731cc06989645080817d39c43a24deea5", + "digest": "sha256:3b99926ce9deb5d66186a55b1168011dc1ae4d010ada5ac5d79c4c7225a1f80f", "rationale": "#2266 broadens runtime-hint validation to accept the additive HarmonyOS platform value; existing protocol-2 runtime hints remain valid and unchanged." } ] diff --git a/test/wire-compat/surface.ts b/test/wire-compat/surface.ts index 1fcf84081f..3043bf34ec 100644 --- a/test/wire-compat/surface.ts +++ b/test/wire-compat/surface.ts @@ -177,6 +177,8 @@ export const WIRE_SURFACE: readonly WireSurfaceGroup[] = [ 'DaemonRequest', 'DaemonRequestMeta', 'SessionRuntimeHints', + 'SESSION_RUNTIME_PLATFORMS', + 'SessionRuntimePlatform', 'daemonRuntimeSchema', 'DaemonInstallSource', 'DAEMON_LOCK_POLICIES', From 0baaaa0a18cf58f459c904c1c082cf3ac2fb232c Mon Sep 17 00:00:00 2001 From: Ark Date: Tue, 8 Sep 2026 18:45:20 +0800 Subject: [PATCH 11/11] fix(script): avoid eager contracts import and relocate Harmony test Signed-off-by: Ark --- .../ad-script/src/internal/script-utils.ts | 18 +++++++++++++++--- .../remote-connection-harmonyos.test.ts | 18 +++++++++++++++++- src/__tests__/remote-connection.test.ts | 15 --------------- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/packages/ad-script/src/internal/script-utils.ts b/packages/ad-script/src/internal/script-utils.ts index 87f27f4e14..b8491fa3aa 100644 --- a/packages/ad-script/src/internal/script-utils.ts +++ b/packages/ad-script/src/internal/script-utils.ts @@ -1,8 +1,20 @@ import type { SessionAction } from '@agent-device/contracts/session'; -import { isSessionRuntimePlatform, type SessionRuntimeHints } from '@agent-device/kernel/contracts'; +import type { SessionRuntimeHints } from '@agent-device/kernel/contracts'; import { appendScreenshotScriptFlags } from '@agent-device/contracts/capture'; import { splitRefGenerationSuffix } from '@agent-device/kernel/snapshot'; +const SCRIPT_RUNTIME_PLATFORMS = { + ios: true, + android: true, + harmonyos: true, +} satisfies Record, true>; + +function isScriptRuntimePlatform( + value: unknown, +): value is NonNullable { + return typeof value === 'string' && Object.hasOwn(SCRIPT_RUNTIME_PLATFORMS, value); +} + /** * #1076 versioned refs: a recorded ref positional may carry a `~s` * pin from the client that issued it (`@e12~s3`). Generations are meaningless @@ -168,7 +180,7 @@ export function appendRuntimeHintFlags( flags: Pick['flags'] | SessionRuntimeHints | undefined, ): void { if (!flags) return; - if (isSessionRuntimePlatform(flags.platform)) { + if (isScriptRuntimePlatform(flags.platform)) { parts.push('--platform', flags.platform); } if (typeof flags.metroHost === 'string' && flags.metroHost.length > 0) { @@ -325,7 +337,7 @@ export function parseReplayRuntimeFlags(args: string[]): { const nextArg = args[index + 1]; if (token === '--platform' && nextArg !== undefined) { const platform = nextArg; - if (isSessionRuntimePlatform(platform)) { + if (isScriptRuntimePlatform(platform)) { flags.platform = platform; } index += 1; diff --git a/src/__tests__/remote-connection-harmonyos.test.ts b/src/__tests__/remote-connection-harmonyos.test.ts index 5eec997f71..f35b4a0923 100644 --- a/src/__tests__/remote-connection-harmonyos.test.ts +++ b/src/__tests__/remote-connection-harmonyos.test.ts @@ -3,13 +3,29 @@ import fs from 'node:fs'; import { connectionWorkspace, createTestClient, + forceConnectFlags, seedConnectionState, } from './remote-connection.fixtures.ts'; -import { materializeRemoteConnectionForCommand } from '../cli/commands/connection-runtime.ts'; +import { + materializeRemoteConnectionForCommand, + resolveRequestedLeaseBackend, +} from '../cli/commands/connection-runtime.ts'; import { disconnectCommand } from '../cli/commands/connection.ts'; import { readRemoteConnectionState } from '../remote/remote-connection-state.ts'; import { LeaseRegistry } from '../daemon/lease-registry.ts'; +test('HarmonyOS platform resolves to its proxy lease backend', () => { + expect( + resolveRequestedLeaseBackend( + forceConnectFlags({ + stateDir: '/tmp/agent-device', + remoteConfig: '/tmp/remote.json', + platform: 'harmonyos', + }), + ), + ).toBe('harmonyos-instance'); +}); + test.each(['apple', 'harmonyos', 'ios', 'android'] as const)( 'stored Harmony runtime compatibility respects %s selection', async (platform) => { diff --git a/src/__tests__/remote-connection.test.ts b/src/__tests__/remote-connection.test.ts index cf3ae4d3c3..c97a819e47 100644 --- a/src/__tests__/remote-connection.test.ts +++ b/src/__tests__/remote-connection.test.ts @@ -34,9 +34,7 @@ import { materializeRemoteConnectionForCommand, CLOUD_WEBDRIVER_REMOTE_LEASE_TTL_MS, PROXY_REMOTE_LEASE_TTL_MS, - resolveRequestedLeaseBackend, } from '../cli/commands/connection-runtime.ts'; - import { stopMetroCompanion } from '../metro/client-metro-companion.ts'; import { AppError } from '@agent-device/kernel/errors'; import { @@ -47,19 +45,6 @@ import { } from '../remote/remote-connection-state.ts'; import type { AgentDeviceClient } from '../agent-device-client.ts'; -test('HarmonyOS platform resolves to its proxy lease backend', () => { - assert.equal( - resolveRequestedLeaseBackend( - forceConnectFlags({ - stateDir: '/tmp/agent-device', - remoteConfig: '/tmp/remote.json', - platform: 'harmonyos', - }), - ), - 'harmonyos-instance', - ); -}); - afterEach(() => { vi.clearAllMocks(); vi.restoreAllMocks();