Skip to content

Commit 7e14dec

Browse files
authored
fix: improve Android text entry stability (#540)
* fix: improve android text entry stability * fix: stabilize android daemon diagnostics * chore: refresh fallow baselines * refactor: tighten android text fallback * fix: tighten android input ownership diagnostics * fix: require settled android fill verification * test: cover android fill prefix verification
1 parent 83efe54 commit 7e14dec

33 files changed

Lines changed: 1273 additions & 533 deletions

fallow-baselines/dupes.json

Lines changed: 176 additions & 179 deletions
Large diffs are not rendered by default.

fallow-baselines/health.json

Lines changed: 28 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@
5151
"count": 1
5252
}
5353
},
54+
"src/cli/commands/client-command.ts": {
55+
"crap_moderate": {
56+
"count": 1
57+
}
58+
},
5459
"src/cli/commands/connection-runtime.ts": {
5560
"complexity_high": {
5661
"count": 1
@@ -73,20 +78,6 @@
7378
"count": 1
7479
}
7580
},
76-
"src/cli/commands/generic.ts": {
77-
"complexity_critical": {
78-
"count": 1
79-
},
80-
"crap_critical": {
81-
"count": 1
82-
},
83-
"crap_high": {
84-
"count": 1
85-
},
86-
"crap_moderate": {
87-
"count": 1
88-
}
89-
},
9081
"src/cli/commands/output.ts": {
9182
"complexity_moderate": {
9283
"count": 1
@@ -100,11 +91,6 @@
10091
"count": 1
10192
}
10293
},
103-
"src/client-commands.ts": {
104-
"crap_moderate": {
105-
"count": 1
106-
}
107-
},
10894
"src/client-companion-tunnel-worker.ts": {
10995
"complexity_high": {
11096
"count": 1
@@ -143,11 +129,6 @@
143129
"count": 1
144130
}
145131
},
146-
"src/client.ts": {
147-
"crap_moderate": {
148-
"count": 1
149-
}
150-
},
151132
"src/commands/admin.ts": {
152133
"crap_high": {
153134
"count": 1
@@ -212,16 +193,13 @@
212193
},
213194
"src/core/dispatch.ts": {
214195
"complexity_critical": {
215-
"count": 2
196+
"count": 1
216197
},
217198
"crap_critical": {
218-
"count": 2
219-
},
220-
"crap_high": {
221-
"count": 4
199+
"count": 1
222200
},
223201
"crap_moderate": {
224-
"count": 5
202+
"count": 3
225203
}
226204
},
227205
"src/daemon-client.ts": {
@@ -459,15 +437,12 @@
459437
},
460438
"src/daemon/handlers/session-replay-script.ts": {
461439
"complexity_critical": {
462-
"count": 2
440+
"count": 1
463441
},
464442
"complexity_high": {
465-
"count": 2
443+
"count": 1
466444
},
467445
"crap_critical": {
468-
"count": 2
469-
},
470-
"crap_high": {
471446
"count": 1
472447
}
473448
},
@@ -558,14 +533,6 @@
558533
"count": 1
559534
}
560535
},
561-
"src/daemon/request-router.ts": {
562-
"crap_high": {
563-
"count": 2
564-
},
565-
"crap_moderate": {
566-
"count": 3
567-
}
568-
},
569536
"src/daemon/runtime-hints.ts": {
570537
"complexity_moderate": {
571538
"count": 1
@@ -623,14 +590,6 @@
623590
"count": 1
624591
}
625592
},
626-
"src/daemon/session-store.ts": {
627-
"complexity_critical": {
628-
"count": 2
629-
},
630-
"crap_critical": {
631-
"count": 2
632-
}
633-
},
634593
"src/platforms/android/app-lifecycle.ts": {
635594
"complexity_moderate": {
636595
"count": 1
@@ -647,9 +606,6 @@
647606
}
648607
},
649608
"src/platforms/android/input-actions.ts": {
650-
"complexity_high": {
651-
"count": 1
652-
},
653609
"complexity_moderate": {
654610
"count": 1
655611
}
@@ -771,14 +727,11 @@
771727
}
772728
},
773729
"src/platforms/ios/runner-xctestrun.ts": {
774-
"complexity_high": {
775-
"count": 1
776-
},
777730
"complexity_moderate": {
778-
"count": 4
731+
"count": 2
779732
},
780733
"crap_moderate": {
781-
"count": 4
734+
"count": 2
782735
}
783736
},
784737
"src/platforms/ios/screenshot-status-bar.ts": {
@@ -1002,44 +955,42 @@
1002955
"runtime_coverage_findings": [],
1003956
"target_keys": [
1004957
"src/daemon/handlers/snapshot-capture.ts:high impact",
1005-
"src/daemon/handlers/session.ts:complexity",
1006958
"src/daemon/handlers/session-device-utils.ts:high impact",
959+
"src/utils/process-identity.ts:high impact",
960+
"src/daemon/handlers/session.ts:complexity",
1007961
"src/client-shared.ts:high impact",
962+
"src/daemon/context.ts:high impact",
963+
"src/daemon/handlers/session-replay-heal.ts:complexity",
964+
"src/cli/commands/connection-runtime.ts:complexity",
1008965
"src/daemon/handlers/session-replay-script.ts:complexity",
1009966
"src/commands/selector-read-utils.ts:high impact",
1010-
"src/daemon/config.ts:high impact",
1011-
"src/daemon/handlers/session-replay-heal.ts:complexity",
1012967
"src/daemon/android-snapshot-freshness.ts:high impact",
1013-
"src/cli/commands/connection-runtime.ts:complexity",
1014-
"src/platforms/boot-diagnostics.ts:complexity",
1015-
"src/utils/args.ts:high impact",
1016-
"src/daemon/session-store.ts:complexity",
968+
"src/daemon/config.ts:high impact",
1017969
"src/daemon/handlers/session-open-target.ts:high impact",
1018970
"src/daemon/handlers/snapshot-alert.ts:complexity",
1019-
"src/utils/success-text.ts:high impact",
971+
"src/daemon/script-utils.ts:high impact",
972+
"src/platforms/boot-diagnostics.ts:complexity",
1020973
"src/daemon/handlers/session-inventory.ts:complexity",
974+
"src/utils/success-text.ts:high impact",
975+
"src/cli/commands/output.ts:untested risk",
1021976
"src/daemon/handlers/install-source.ts:complexity",
1022977
"src/utils/snapshot-processing.ts:high impact",
1023-
"src/cli/commands/output.ts:untested risk",
1024-
"src/daemon/script-utils.ts:high impact",
1025-
"src/utils/process-identity.ts:high impact",
1026-
"src/utils/output.ts:high impact",
1027-
"src/cli.ts:complexity",
1028-
"src/platforms/ios/xml.ts:high impact",
1029978
"src/daemon/handlers/session-state.ts:complexity",
1030979
"src/daemon/handlers/session-open.ts:complexity",
980+
"src/utils/snapshot-lines.ts:high impact",
981+
"src/utils/output.ts:high impact",
982+
"src/platforms/ios/xml.ts:high impact",
1031983
"src/daemon/request-cancel.ts:high impact",
984+
"src/utils/args.ts:high impact",
985+
"src/cli.ts:complexity",
986+
"src/client-metro.ts:complexity",
1032987
"src/utils/device.ts:high impact",
1033-
"src/utils/snapshot-lines.ts:high impact",
1034988
"src/daemon/app-log-process.ts:high impact",
1035989
"src/utils/selector-build.ts:high impact",
1036-
"src/client-metro.ts:complexity",
1037-
"src/platforms/android/input-actions.ts:complexity",
1038990
"src/utils/text-surface.ts:high impact",
1039991
"src/cli-test.ts:untested risk",
1040992
"src/utils/keyed-lock.ts:high impact",
1041993
"src/core/batch.ts:complexity",
1042-
"src/platforms/ios/runner-xctestrun.ts:complexity",
1043994
"src/daemon/app-log-stream.ts:high impact",
1044995
"src/platforms/android/sdk.ts:high impact",
1045996
"src/utils/source-value.ts:high impact",

src/__tests__/cli-client-commands.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,48 @@ test('clipboard read keeps human text output through the typed client command AP
637637
assert.equal(stdout, 'hello\n');
638638
});
639639

640+
test('keyboard status prints Android input ownership in human output', async () => {
641+
const client = createStubClient({
642+
installFromSource: async () => {
643+
throw new Error('unexpected install call');
644+
},
645+
});
646+
client.command.keyboard = async () => ({
647+
platform: 'android',
648+
action: 'status',
649+
visible: true,
650+
type: 'text',
651+
inputMethodPackage: 'com.google.android.inputmethod.latin',
652+
focusedPackage: 'com.google.android.inputmethod.latin',
653+
focusedResourceId: 'com.google.android.inputmethod.latin:id/handwriting',
654+
inputOwner: 'ime',
655+
});
656+
657+
const stdout = await captureStdout(async () => {
658+
const handled = await tryRunClientBackedCommand({
659+
command: 'keyboard',
660+
positionals: ['status'],
661+
flags: {
662+
json: false,
663+
help: false,
664+
version: false,
665+
},
666+
client,
667+
});
668+
assert.equal(handled, true);
669+
});
670+
671+
assert.match(stdout, /Keyboard visible: yes/);
672+
assert.match(stdout, /Input type: text/);
673+
assert.match(stdout, /Input owner: ime/);
674+
assert.match(stdout, /Input method: com\.google\.android\.inputmethod\.latin/);
675+
assert.match(
676+
stdout,
677+
/Focused resource: com\.google\.android\.inputmethod\.latin:id\/handwriting/,
678+
);
679+
assert.match(stdout, /Next action: Focused input appears to be owned by the keyboard\/IME/);
680+
});
681+
640682
test('metro prepare wraps output in the standard success envelope for --json', async () => {
641683
const client = createStubClient({
642684
installFromSource: async () => {

src/__tests__/cli-diagnostics.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,38 @@ test('cli does not tail local daemon log when remote daemon base URL is set', as
7575
}
7676
});
7777

78+
test('cli debug log tail starts at the current daemon log end', async () => {
79+
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-cli-tail-'));
80+
const daemonPaths = resolveDaemonPaths(stateDir);
81+
fs.mkdirSync(path.dirname(daemonPaths.logPath), { recursive: true });
82+
fs.writeFileSync(daemonPaths.logPath, 'OLD_TAIL_SENTINEL\n', 'utf8');
83+
84+
try {
85+
const result = await captureCli(
86+
['clipboard', 'write', 'hello', '--debug', '--state-dir', stateDir],
87+
async () => {
88+
await new Promise((resolve) => setTimeout(resolve, 250));
89+
fs.appendFileSync(daemonPaths.logPath, 'NEW_TAIL_SENTINEL\n', 'utf8');
90+
await new Promise((resolve) => setTimeout(resolve, 250));
91+
return {
92+
ok: true,
93+
data: { action: 'write', message: 'Clipboard updated' },
94+
};
95+
},
96+
{
97+
stateDirPrefix: 'agent-device-cli-diagnostics-',
98+
},
99+
);
100+
101+
assert.equal(result.code, null);
102+
assert.equal(result.stdout.includes('OLD_TAIL_SENTINEL'), false);
103+
assert.equal(result.stdout.includes('NEW_TAIL_SENTINEL'), true);
104+
assert.match(result.stdout, /Clipboard updated/);
105+
} finally {
106+
fs.rmSync(stateDir, { recursive: true, force: true });
107+
}
108+
});
109+
78110
test('cli returns normalized JSON failures with diagnostics fields', async () => {
79111
const result = await runCliCapture(['open', 'settings', '--json'], async () => ({
80112
ok: false,

src/__tests__/cli-help.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ test('help workflow preserves known device workaround guidance', async () => {
8383
assert.match(result.stdout, /do not write network log headers/);
8484
assert.match(result.stdout, /iOS Allow Paste prompt cannot be exercised under XCUITest/);
8585
assert.match(result.stdout, /agent-device clipboard write "some text"/);
86-
assert.match(result.stdout, /trusted ADB keyboard IME/);
86+
assert.match(result.stdout, /provider-native text injection when available/);
87+
assert.match(result.stdout, /Do not switch to raw adb, clipboard, or paste as an agent fallback/);
8788
});
8889

8990
test('help unknown command prints error plus global usage and skips daemon dispatch', async () => {

src/__tests__/client-companion-tunnel-worker.test.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -236,15 +236,22 @@ async function listenNotFoundServer(): Promise<number> {
236236
}
237237

238238
async function stopChild(child: ReturnType<typeof spawn>): Promise<void> {
239-
if (child.exitCode !== null || child.killed) return;
239+
if (child.exitCode !== null || child.signalCode !== null) return;
240+
const waitForClose = () =>
241+
new Promise<boolean>((resolve) => {
242+
if (child.exitCode !== null || child.signalCode !== null) {
243+
resolve(true);
244+
return;
245+
}
246+
child.once('close', () => resolve(true));
247+
});
248+
const closePromise = waitForClose();
240249
child.kill('SIGTERM');
241-
const exited = await Promise.race([
242-
new Promise<boolean>((resolve) => child.once('close', () => resolve(true))),
243-
delay(2_000).then(() => false),
244-
]);
250+
const exited = await Promise.race([closePromise, delay(2_000).then(() => false)]);
245251
if (exited) return;
252+
const killClosePromise = waitForClose();
246253
child.kill('SIGKILL');
247-
await new Promise<void>((resolve) => child.once('close', () => resolve()));
254+
await killClosePromise;
248255
}
249256

250257
function spawnMetroCompanionWorker(options: {

src/__tests__/runtime-snapshot.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ test('runtime snapshot emits filtered Android guidance from backend analysis', a
103103
});
104104

105105
assert.deepEqual(result.warnings, [
106-
'Interactive snapshot is empty after filtering 42 raw Android nodes. Likely causes: depth too low, transient route change, or collector filtering.',
106+
'Interactive snapshot is empty after filtering 42 raw Android nodes. Likely causes: the app content is not accessibility-visible yet, a transient route change, or depth/filter options hid the target.',
107107
'Interactive output is empty at depth 3; retry without -d.',
108108
]);
109109
});

src/android-adb.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export {
1111
type AndroidAdbPuller,
1212
type AndroidAdbSpawner,
1313
type AndroidAdbTransferOptions,
14+
type AndroidTextInjectionRequest,
15+
type AndroidTextInjector,
1416
type AndroidPortReverseEndpoint,
1517
type AndroidPortReverseMapping,
1618
type AndroidPortReverseOptions,

src/cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,7 @@ if (isDirectRun) {
506506

507507
function startDaemonLogTail(logPath: string): (() => void) | null {
508508
try {
509-
let offset = 0;
509+
let offset = fs.existsSync(logPath) ? fs.statSync(logPath).size : 0;
510510
let stopped = false;
511511
const interval = setInterval(() => {
512512
if (stopped) return;

0 commit comments

Comments
 (0)