@@ -16,180 +16,23 @@ import {
1616 type PlatformRuntimeHost ,
1717} from '@agent-device/contracts/platform-runtime-operations' ;
1818import { deployAppUse } from '@agent-device/contracts/app-deployment-runtime-plan' ;
19- import { describe , expect , test , vi } from 'vitest' ;
20- import fs from 'node:fs' ;
21- import path from 'node:path' ;
22- import { runCmd } from '@agent-device/host-kit/command' ;
23- import { sleep } from '@agent-device/host-kit/retry' ;
24- import { withAppleToolProvider } from '@agent-device/platform-apple/tool-provider' ;
25- import { createRecordingAppleToolProvider } from '../test/integration/provider-scenarios/providers.ts' ;
26- import { createDemoIosApp } from '../test/integration/provider-scenarios/fixtures.ts' ;
27- import {
28- AUTOMATION_APP_ID ,
29- AUTOMATION_PNG ,
30- managedAutomationBinding ,
31- managedAutomationApk ,
32- withManagedAdbFixture ,
33- } from './platform-runtime-managed-owner.fixtures.ts' ;
19+ import { describe , expect , test } from 'vitest' ;
3420import { createManagedLocalRuntimeOwner } from './platform-runtime-managed-owner.ts' ;
3521import {
3622 gatewayFixtureDevice as device ,
3723 gatewayFixtureScope as ordinaryScope ,
3824 managedGatewayScope ,
3925 localFamilyRuntimeFixture ,
40- MANAGED_RETAINED_OPERATION ,
26+ REVIEWED_MANAGED_OPERATION ,
4127} from './platform-runtime-gateway.fixtures.ts' ;
4228
43- vi . mock ( '@agent-device/host-kit/retry' , async ( importOriginal ) => ( {
44- ...( await importOriginal < typeof import ( '@agent-device/host-kit/retry' ) > ( ) ) ,
45- sleep : vi . fn ( async ( ) => { } ) ,
46- } ) ) ;
47-
4829const managed = managedLocalRuntimeOwner ( 'sim-a' ) ;
4930const fence = managedBindingFence ( {
5031 requesterId : 'requester-a' ,
5132 requestGeneration : 1 ,
5233 identityIncarnationId : 'incarnation-a' ,
5334} ) ;
5435
55- test ( 'managed iOS delegates deployment and system operations without local readiness or runner startup' , async ( ) => {
56- const app = createDemoIosApp ( 'managed-ios-automation-' ) ;
57- let biometricAttempts = 0 ;
58- const native = createRecordingAppleToolProvider ( {
59- plist : {
60- readJson : async ( ) => ( { CFBundleIdentifier : AUTOMATION_APP_ID , CFBundleName : 'Demo' } ) ,
61- } ,
62- simctl : async ( args ) => {
63- expect ( args . slice ( 0 , 2 ) ) . toEqual ( [ '--set' , '/managed/set' ] ) ;
64- const command = args [ 2 ] ;
65- if ( command === 'list' )
66- return {
67- stdout : JSON . stringify ( {
68- devices : { ios : [ { udid : 'managed-ios' , state : 'Shutdown' } ] } ,
69- } ) ,
70- stderr : '' ,
71- exitCode : 0 ,
72- } ;
73- expect ( [ 'install' , 'uninstall' , 'pbpaste' , 'pbcopy' , 'ui' , 'biometric' , 'push' ] ) . toContain (
74- command ,
75- ) ;
76- if ( command === 'biometric' && ++ biometricAttempts === 1 )
77- return { stdout : '' , stderr : 'unknown command' , exitCode : 1 } ;
78- return {
79- stdout : command === 'pbpaste' ? 'managed clipboard\n' : '' ,
80- stderr : '' ,
81- exitCode : 0 ,
82- } ;
83- } ,
84- } ) ;
85- try {
86- await withAppleToolProvider ( native . provider , async ( ) => {
87- const { binding, allocator, admission } = await managedAutomationBinding ( 'ios' ) ;
88- expect ( allocator . calls . map ( ( { method } ) => method ) ) . toEqual ( [ 'requestLease' ] ) ;
89- const ops = binding . operations ;
90- await ops . ensureReady ! ( { } ) ;
91- await ops . deployApp ! ( {
92- app : AUTOMATION_APP_ID ,
93- appPath : app . appPath ,
94- replaceExisting : false ,
95- } ) ;
96- await ops . deployApp ! ( { app : AUTOMATION_APP_ID , appPath : app . appPath , replaceExisting : true } ) ;
97- const artifact = await ops . materializeAppSource ! ( {
98- source : { kind : 'path' , path : app . appPath } ,
99- } ) ;
100- try {
101- await ops . deployMaterializedApp ! ( { artifact } ) ;
102- } finally {
103- await artifact . cleanup ( ) ;
104- }
105- await ops . sendPushNotification ! ( { appId : AUTOMATION_APP_ID , payload : { } } ) ;
106- expect ( await ops . readClipboard ! ( { } ) ) . toBe ( 'managed clipboard' ) ;
107- await ops . writeClipboard ! ( { text : 'managed' } ) ;
108- await ops . setSetting ! ( { setting : 'appearance' , state : 'dark' } ) ;
109- await ops . setSetting ! ( { setting : 'faceid' , state : 'match' } ) ;
110- expect ( biometricAttempts ) . toBe ( 2 ) ;
111- expect ( native . calls . filter ( ( call ) => call [ 3 ] === 'install' ) ) . toHaveLength ( 3 ) ;
112- expect (
113- native . calls . some ( ( call ) => [ 'list' , 'boot' , 'bootstatus' , 'shutdown' ] . includes ( call [ 3 ] ! ) ) ,
114- ) . toBe ( false ) ;
115- expect ( allocator . calls . map ( ( { method } ) => method ) ) . toEqual ( [ 'requestLease' , 'renewLease' ] ) ;
116- const before = native . calls . length ;
117- admission . fenceBinding ( 'replaced' ) ;
118- await expect ( ops . readClipboard ! ( { } ) ) . rejects . toMatchObject ( {
119- details : { reason : 'teardown-required' } ,
120- } ) ;
121- await binding [ Symbol . asyncDispose ] ( ) ;
122- expect ( native . calls ) . toHaveLength ( before ) ;
123- } ) ;
124- } finally {
125- fs . rmSync ( app . tempRoot , { recursive : true , force : true } ) ;
126- }
127- } ) ;
128-
129- test . skipIf ( process . platform === 'win32' ) (
130- 'managed Android contains bind probes, APK deployment, settings, clipboard and screenshot' ,
131- async ( ) => {
132- await withManagedAdbFixture ( async ( native ) => {
133- const { binding, host, allocator, admission } = await managedAutomationBinding ( 'android' ) ;
134- expect ( native . calls ( ) ) . toHaveLength ( 1 ) ;
135- expect ( allocator . calls . map ( ( { method } ) => method ) ) . toEqual ( [ 'requestLease' ] ) ;
136- const ops = binding . operations ;
137- const apkPath = await managedAutomationApk ( native . root ) ;
138- await ops . ensureReady ! ( { } ) ;
139- await ops . deployApp ! ( { app : AUTOMATION_APP_ID , appPath : apkPath , replaceExisting : false } ) ;
140- await ops . deployApp ! ( { app : AUTOMATION_APP_ID , appPath : apkPath , replaceExisting : true } ) ;
141- const artifact = await ops . materializeAppSource ! ( { source : { kind : 'path' , path : apkPath } } ) ;
142- try {
143- await ops . deployMaterializedApp ! ( { artifact } ) ;
144- } finally {
145- await artifact . cleanup ( ) ;
146- }
147- await ops . sendPushNotification ! ( { appId : AUTOMATION_APP_ID , payload : { } } ) ;
148- expect ( await ops . readClipboard ! ( { } ) ) . toBe ( 'managed clipboard' ) ;
149- await ops . writeClipboard ! ( { text : 'managed' } ) ;
150- await ops . setSetting ! ( { setting : 'fingerprint' , state : 'match' } ) ;
151- await ops . setSetting ! ( {
152- setting : 'clear-app-state' ,
153- state : 'clear' ,
154- appBundleId : AUTOMATION_APP_ID ,
155- } ) ;
156- const outPath = path . join ( native . root , 'capture.png' ) ;
157- await ops . captureScreenshot ! ( { outPath, options : { stabilize : false } } ) ;
158- expect ( fs . readFileSync ( outPath ) ) . toEqual ( AUTOMATION_PNG ) ;
159- const calls = native . calls ( ) ;
160- expect ( calls . filter ( ( args ) => args [ 4 ] === 'install' ) ) . toHaveLength ( 3 ) ;
161- expect ( calls . some ( ( args ) => args . slice ( 4 ) . join ( ' ' ) === 'emu finger touch 1' ) ) . toBe ( true ) ;
162- for ( const args of calls )
163- expect ( args . slice ( 0 , 4 ) ) . toEqual ( [ '-P' , '15037' , '-s' , 'emulator-15037' ] ) ;
164- const bundle = path . join ( native . root , 'App.aab' ) ;
165- fs . writeFileSync ( bundle , 'bundle' ) ;
166- const archive = path . join ( native . root , 'bundle.zip' ) ;
167- await runCmd ( 'zip' , [ '-q' , archive , 'App.aab' ] , { cwd : native . root } ) ;
168- const commands = vi . spyOn ( host . commands , 'run' ) ;
169- for ( const replaceExisting of [ false , true ] ) {
170- await expect (
171- ops . deployApp ! ( { app : AUTOMATION_APP_ID , appPath : bundle , replaceExisting } ) ,
172- ) . rejects . toMatchObject ( { details : { reason : 'managed-bundle-install-unavailable' } } ) ;
173- }
174- await expect (
175- ops . deployApp ! ( { app : AUTOMATION_APP_ID , appPath : archive , replaceExisting : true } ) ,
176- ) . rejects . toMatchObject ( { details : { reason : 'managed-bundle-install-unavailable' } } ) ;
177- await expect (
178- ops . deployMaterializedApp ! ( {
179- artifact : { installablePath : bundle , cleanup : async ( ) => { } } ,
180- } ) ,
181- ) . rejects . toMatchObject ( { details : { reason : 'managed-bundle-install-unavailable' } } ) ;
182- expect ( commands ) . not . toHaveBeenCalled ( ) ;
183- expect ( native . calls ( ) ) . toEqual ( calls ) ;
184- admission . fenceBinding ( 'released' ) ;
185- await expect ( ops . readClipboard ! ( { } ) ) . rejects . toMatchObject ( {
186- details : { reason : 'teardown-required' } ,
187- } ) ;
188- await binding [ Symbol . asyncDispose ] ( ) ;
189- expect ( native . calls ( ) ) . toEqual ( calls ) ;
190- } ) ;
191- } ,
192- ) ;
19336const scope = managedGatewayScope ( device , managed , fence ) ;
19437
19538function exactly (
@@ -217,28 +60,6 @@ function thrownBy(run: () => unknown): unknown {
21760 throw new Error ( 'expected the runtime use to be refused' ) ;
21861}
21962
220- test . skipIf ( process . platform === 'win32' ) (
221- 'managed screenshot failure keeps demo-mode cleanup on its private transport' ,
222- async ( ) => {
223- await withManagedAdbFixture ( async ( native ) => {
224- const { binding } = await managedAutomationBinding ( 'android' ) ;
225- vi . mocked ( sleep ) . mockClear ( ) ;
226- fs . writeFileSync ( path . join ( native . root , 'corrupt-screenshot' ) , '' ) ;
227- await expect (
228- binding . operations . captureScreenshot ! ( { outPath : path . join ( native . root , 'bad.png' ) } ) ,
229- ) . rejects . toThrow ( 'valid PNG header' ) ;
230- expect ( sleep ) . toHaveBeenCalledOnce ( ) ;
231- const calls = native . calls ( ) ;
232- expect ( calls . at ( - 1 ) ?. at ( - 1 ) ) . toBe (
233- 'am broadcast -a com.android.systemui.demo -e command exit' ,
234- ) ;
235- for ( const args of calls )
236- expect ( args . slice ( 0 , 4 ) ) . toEqual ( [ '-P' , '15037' , '-s' , 'emulator-15037' ] ) ;
237- await binding [ Symbol . asyncDispose ] ( ) ;
238- } ) ;
239- } ,
240- ) ;
241-
24263describe ( 'managed local runtime owner' , ( ) => {
24364 test ( 'dispatches the real operation inside admission before authority can be fenced' , async ( ) => {
24465 const events : string [ ] = [ ] ;
@@ -322,8 +143,8 @@ describe('managed local runtime owner', () => {
322143 } ) ;
323144 expect ( binding . operations [ key ] ) . toBeUndefined ( ) ;
324145 }
325- expect ( binding . facts . operations [ MANAGED_RETAINED_OPERATION ] ) . toEqual ( { available : true } ) ;
326- expect ( binding . operations [ MANAGED_RETAINED_OPERATION ] ) . toBeTypeOf ( 'function' ) ;
146+ expect ( binding . facts . operations [ REVIEWED_MANAGED_OPERATION ] ) . toEqual ( { available : true } ) ;
147+ expect ( binding . operations [ REVIEWED_MANAGED_OPERATION ] ) . toBeTypeOf ( 'function' ) ;
327148 } ) ;
328149
329150 // The exclusion covers binding cells; request-runtime binding owns the separate pre-binding
@@ -391,7 +212,7 @@ describe('managed local runtime owner', () => {
391212 expect ( binding . owner ) . toEqual ( managed ) ;
392213 } ) ;
393214
394- test ( 'never claims a device and projects the same withholding onto inspected facts ' , async ( ) => {
215+ test ( 'never claims a device and leaves unbound inspection entirely unavailable ' , async ( ) => {
395216 const { owner } = managedOwnerFixture ( ) ;
396217
397218 expect ( owner . ownsDevice ( device ) ) . toBe ( false ) ;
@@ -400,7 +221,7 @@ describe('managed local runtime owner', () => {
400221 available : false ,
401222 reason : 'owner-capability-missing' ,
402223 } ) ;
403- expect ( facts . operations [ MANAGED_RETAINED_OPERATION ] . available ) . toBe ( false ) ;
224+ expect ( facts . operations [ REVIEWED_MANAGED_OPERATION ] . available ) . toBe ( false ) ;
404225 } ) ;
405226
406227 test ( 'reports the family owner provider mode it was given, unlaundered' , async ( ) => {
0 commit comments