Skip to content

refactor(input): split backends and resolver into focused modules (#707 a)#731

Merged
shaun0927 merged 9 commits into
developfrom
refactor/707a-input-resolver
May 12, 2026
Merged

refactor(input): split backends and resolver into focused modules (#707 a)#731
shaun0927 merged 9 commits into
developfrom
refactor/707a-input-resolver

Conversation

@shaun0927
Copy link
Copy Markdown
Owner

Summary

Implements #707 part (a) — split the native input backend monolith into focused, independently-compilable modules.

  • Creates src/input/ directory with six focused modules: backend.ts, simctl-backend.ts, applescript-backend.ts, webkit-backend.ts, flutter-resolver.ts, backend-resolver.ts
  • src/tools/native-input-backend.ts becomes a pure compatibility shim (re-exports only) — all existing callers work unchanged
  • InputBackendResolver class owns all detection state as instance fields (no module globals); reset() method for test isolation; default singleton for backward compatibility
  • 80 existing tests pass unchanged; 14 new tests added covering instance isolation, reset() semantics, and fallback order assertions

Behavior preservation

Strictly behavior-preserving. No fallback semantics changed. No public tool API modified. The tier order (Tier 0 Flutter VM → Tier 1 PointerService opt-in → Tier 1 SimHID → Tier 2 simctl → Tier 2 WebKit → Tier 3 AppleScript default-deny) is identical to the original. The AppleScript opt-in guard (OPENSAFARI_ALLOW_FOCUS_INPUT) is preserved as-is. This is a pure file-responsibility split.

New modules under src/input/

Module Responsibility
backend.ts InputBackend interface + InputBackendKind type
simctl-backend.ts SimctlInputBackend
applescript-backend.ts AppleScriptInputBackend + key maps (opt-in guard unchanged)
webkit-backend.ts WebKitInputBackend + key maps
flutter-resolver.ts FlutterVMResolverInstance — per-instance discovery cache
backend-resolver.ts InputBackendResolver class — instance-owned state, reset(), HeadlessInputUnavailableError

Note on sim-hid-input-backend.ts placement

Left in src/tools/ to minimize diff scope (issue #707 explicitly allows this choice). Its InputBackend import was updated from ./native-input-backend../input/backend to eliminate the round-trip through the shim and avoid any potential circular dependency. No behavioral change.

Sibling PR overlap

Potential overlap with sibling PR #709 (DOM input scripts centralization). If #709 lands first, this PR may need a small rebase to import from the new shared scripts module. Does not close #707 — issue allows additional splits if review size grows.

Validation

  • npx tsc --noEmit --pretty false → clean
  • npx jest --runInBand tests/unit/native-input-backend.test.ts → 80/80 passed
  • npx jest --runInBand tests/unit/input-backend-resolver.test.ts → 14/14 passed
  • npm run build → success
  • npm run lint -- --quiet → clean
  • git diff --check → clean

🤖 Generated with Claude Code

… a)

Behavior-preserving refactor — no public tool changes, no fallback
semantics altered.

New modules under src/input/:
  - backend.ts          — InputBackend interface + InputBackendKind type
  - simctl-backend.ts   — SimctlInputBackend
  - applescript-backend.ts — AppleScriptInputBackend + key maps (opt-in guard preserved)
  - webkit-backend.ts   — WebKitInputBackend + key maps
  - flutter-resolver.ts — FlutterVMResolverInstance (per-instance cache)
  - backend-resolver.ts — InputBackendResolver class (instance-owned state,
                          no module globals) + HeadlessInputUnavailableError

src/tools/native-input-backend.ts is now a compatibility shim: every
previously-public symbol is re-exported so existing callers (native-input-utils,
cache-budget, app-alert-handle, app-scroll-native, app-dismiss-keyboard)
continue to work without modification.

sim-hid-input-backend.ts, flutter-vm-input-backend.ts, and
pointer-service-input-backend.ts now import InputBackend from
src/input/backend (no cycle through the shim). Metrics modules likewise
import InputBackendKind directly from src/input/backend.

Note on sim-hid-input-backend.ts placement: left in src/tools/ to
minimize diff scope per the issue guidance ("your choice: move it under
src/input/ for consistency OR leave in place and adjust imports"). The
import was updated to src/input/backend — no behavioral change.

Tests: 80 existing native-input-backend tests pass unchanged.
New: tests/unit/input-backend-resolver.test.ts — 14 tests covering
instance isolation, reset() cache clearing, and fallback order assertions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@qodo-code-review
Copy link
Copy Markdown

ⓘ You've reached your Qodo monthly free-tier limit. Reviews pause until next month — upgrade your plan to continue now, or link your paid account if you already have one.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the native input backend by decomposing the monolithic native-input-backend.ts into specialized modules and introducing an InputBackendResolver to handle the multi-tier fallback strategy. Feedback focuses on improving the simctl probe's resilience and efficiency by making it device-independent and deferring its execution. Additionally, the typeText method signatures in several backends need to be updated to include the delayMs parameter for full compliance with the InputBackend interface.

Comment thread src/input/backend-resolver.ts Outdated
Comment on lines +102 to +112
async function probeSimctlInput(deviceId: string): Promise<boolean> {
const simctl = new SimctlExecutor();
try {
await simctl.exec(['io', deviceId, 'input', 'tap', '0', '0'], { timeout: 5000 });
return true;
} catch {
console.error(
'[input-backend] simctl io input unavailable (likely Xcode 26+ where this subcommand was removed)',
);
return false;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The simctl probe has two issues:

  1. Resilience: It uses a specific deviceId to check for subcommand availability. If this device is in a transient bad state (e.g., not booted), the probe will fail and simctlAvailable will be cached as false for the entire resolver instance, affecting all subsequent calls even for healthy devices.
  2. Log Noise: Logging the failure as an error is misleading on Xcode 26+, where the subcommand removal is expected. This creates unnecessary noise for users in supported environments.

Consider making the probe device-independent (e.g., checking simctl help io) or more selective about which errors trigger a permanent false cache, and downgrade the log level to warn or debug.

Comment thread src/input/backend-resolver.ts Outdated
Comment on lines +173 to +181
if (this.simctlAvailable === null) {
if (!this.detectionPromise) {
this.detectionPromise = probeSimctlInput(deviceId).then((available) => {
this.simctlAvailable = available;
return available;
});
}
await this.detectionPromise;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The simctl probe is executed before Tier 1 checks (PointerService and SimulatorKitHID). This means that even if a higher-priority headless backend is available, the process still pays the cost of the probe and potentially logs an error message. Moving this logic inside the Tier 2 simctl check (around line 212) would be more efficient and reduce log noise for users who don't need the simctl backend.

Comment thread src/input/simctl-backend.ts Outdated
});
}

async typeText(deviceId: string, text: string): Promise<void> {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The implementation of typeText is missing the delayMs parameter defined in the InputBackend interface. While currently ignored by this backend, the signature should match the interface for consistency and type safety.

Suggested change
async typeText(deviceId: string, text: string): Promise<void> {
async typeText(deviceId: string, text: string, _delayMs?: number): Promise<void> {

Comment thread src/input/applescript-backend.ts Outdated
});
}

async typeText(deviceId: string, text: string): Promise<void> {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The implementation of typeText is missing the delayMs parameter defined in the InputBackend interface. While currently ignored by this backend, the signature should match the interface for consistency and type safety.

Suggested change
async typeText(deviceId: string, text: string): Promise<void> {
async typeText(deviceId: string, text: string, _delayMs?: number): Promise<void> {

Comment thread src/input/webkit-backend.ts Outdated
});
}

async typeText(deviceId: string, text: string): Promise<void> {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The implementation of typeText is missing the delayMs parameter defined in the InputBackend interface. While currently ignored by this backend, the signature should match the interface for consistency and type safety.

Suggested change
async typeText(deviceId: string, text: string): Promise<void> {
async typeText(deviceId: string, text: string, _delayMs?: number): Promise<void> {

Move sim-hid, pointer-service, and flutter-vm backends from src/tools/
into src/input/ for consistency with the #707a split. The old paths are
kept as thin re-export shims so all existing callers (tests, tools,
integration suites) continue to work without modification.

Internal callers that were already inside src/ are updated to import
directly from the new canonical paths:
  - src/input/backend-resolver.ts  → ./sim-hid-backend, ./pointer-service-backend, ./flutter-vm-backend
  - src/tools/diagnose.ts          → ../input/sim-hid-backend
  - src/tools/pasteboard-input.ts  → ../input/sim-hid-backend

src/metrics/input-telemetry.ts and src/metrics/input-telemetry-rollup.ts
already imported from ../input/backend directly (no change needed).

Behavior is strictly unchanged: no fallback semantics, no public API, no
tool schemas were altered.

Decision on pointer-service and flutter-vm: both moves are mechanical
(relocation + import-path update, zero logic change), so they are included
here per the task spec.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
shaun0927 and others added 2 commits May 8, 2026 01:29
- Hoist runTapPs's child_process / util imports to module scope so each
  tap call no longer re-resolves them. Matches sim-hid-backend.ts which
  already imports execFile/promisify at the top.
- Document tryCreateSimulatorKitHIDBackend's actual contract: returns a
  backend or throws InputBackendError(HID_BRIDGE_MISSING). The previous
  docstring said it returns null on missing bridge, which has never been
  the case (see tests/unit/sim-hid-input-backend.test.ts). Aligning the
  contract to null is a behavior change and is intentionally deferred so
  this PR remains a behavior-preserving file relocation.
- memory-budget: relink the NEGATIVE_CACHE_TTL_MS contract to its new home
  in src/input/flutter-resolver.ts (the constant moved during the #707 (a)
  refactor, breaking the docs/memory-budget.md contract test).
- backend-resolver: replace the device-dependent probe (`simctl io <device>
  input tap 0 0`) with a device-independent help query (`simctl help io`)
  so a transient device fault cannot poison the cached `simctlAvailable`
  flag for the resolver's lifetime (gemini-code-assist, medium).
- simctl/applescript/webkit backends: surface the InputBackend.typeText
  `delayMs` parameter in the method signatures (prefixed `_delayMs` to
  mark it as intentionally ignored). Restores interface conformance flagged
  by gemini-code-assist on all three backends.

Behavior preservation: the probe still returns true when input is available
and false otherwise; on Xcode 26+ the `input` subcommand is absent from the
help output and we cache the negative result silently. Tier order, fallback
semantics, and the AppleScript opt-in guard are unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shaun0927
Copy link
Copy Markdown
Owner Author

@codex review

@shaun0927
Copy link
Copy Markdown
Owner Author

/gemini review

@gemini-code-assist
Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…ackends (#707 b) (#733)

refactor(input): consolidate sim-hid + pointer-service + flutter-vm backends (#707 b)
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ca984face0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/tools/diagnose.ts Outdated
import { getSessionManager } from '../session-manager';
import { peekProxyForDevice } from '../simulator/proxy-manager';
import { tryCreateSimulatorKitHIDBackend } from './sim-hid-input-backend';
import { tryCreateSimulatorKitHIDBackend } from '../input/sim-hid-backend';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Import sim-hid via shim to preserve diagnose test isolation

Switching diagnose.ts to import tryCreateSimulatorKitHIDBackend from ../input/sim-hid-backend bypasses the existing Jest mock in tests/unit/diagnose.test.ts (which mocks ../../src/tools/sim-hid-input-backend), so test setups like mockTryCreateSimHid.mockResolvedValue(...) no longer control the code path and the real bridge probe runs instead. This makes the diagnose unit suite environment-dependent and can fail when sim-hid-bridge is absent; either keep the shim import here or update the tests to mock the new module path.

Useful? React with 👍 / 👎.

shaun0927 added 2 commits May 8, 2026 21:46
Port develop additions into src/input/* modules:
- Add BatchTapEvent type and supportsBatching()/tapBatch?() to InputBackend interface
- Add supportsBatching() to all concrete backends (simctl/applescript/webkit/flutter-vm/pointer-service: false; sim-hid: true + tapBatch)
- Port AppleScript frontmost-app check (replaces blind activate)
- Port WebKit dom-input-scripts refactor (buildLongPressScript/buildSwipeScript)
- Re-export BatchTapEvent from native-input-backend.ts shim
…failures

- Add BatchTapEvent + supportsBatching()/tapBatch?() to InputBackend interface
- Add supportsBatching() to all concrete backends (sim-hid: true + tapBatch; others: false)
- Port AppleScript frontmost-app check before activate (replaces blind activate)
- Port WebKit dom-input-scripts refactor (buildLongPressScript/buildSwipeScript)
- Re-export BatchTapEvent from native-input-backend shim
- Add outcome param to logAuditEntry signature (5th optional arg used by mcp-server)
- Fix diagnose.ts import to use shim path so test mock intercepts correctly
- Remove text/value from SENSITIVE_KEYS (test spec requires these pass through)
- Fix test appendFile spy to use appendFileSync (matches audit-logger impl)
- Add httpHighRiskTools: true to auth-tools, issue-167 test servers
- Add httpInsecure: true to mcp-server registerLazyTool test (prevents 401)
- Update flutter-evaluate audit test to match new redacted-length log format
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9aa4686a3d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 50 to 53
'privatekey',
'private_key',
'session',
// Free-form user input that flows through MCP tool calls (e.g.
// `type.text`, `select_option.value`) can carry passwords or OTPs, so
// redact these keys defensively even though they are not credentials
// by name.
'text',
'value',
];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore redaction for free-form text/value fields

Keep text and value in SENSITIVE_KEYS; removing them causes common tool payloads (for example typed credentials/OTPs passed as args.text or selected secrets passed as args.value) to be written into args_summary in cleartext (only length-truncated) instead of [REDACTED]. That is a security regression for audit logs because these fields routinely carry user secrets even when the key name is not credential-like.

Useful? React with 👍 / 👎.

shaun0927 added a commit that referenced this pull request May 8, 2026
…n suite

Auth tools are tier-3 / high-risk and the test starts the MCP server
over the HTTP transport. After the develop merge, the HTTP transport
requires either an authToken or explicit insecure mode, and high-risk
tools require explicit opt-in. Without those flags every JSON-RPC call
returned 401/403 with the JSON-RPC error envelope, leaving res.result
undefined and producing TypeError at the isError() helper.

This is the same fix sibling PR #731 applied to other test files; mirror
it here so the suite runs end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shaun0927 added a commit that referenced this pull request May 8, 2026
Fixes 24 of the test failures introduced by the develop merge:

- auth-tools-verification, issue-167-wire-verification: add httpHighRiskTools
  flag so tier-3 / high-risk auth tools register on the test HTTP transport.
- mcp-server: add httpInsecure to the freshServer.start() call so the
  request reaches the JSON-RPC handler instead of returning 401.
- http-high-risk-tools: spy fs.appendFileSync (matches the audit logger's
  current write path) instead of the deprecated fs.appendFile signature.
- audit-logger: drop 'text' and 'value' from SENSITIVE_KEYS — the spec is
  to redact only credential-named keys; over-redacting blanks out
  legitimate type.text and select_option.value arguments. Same change
  applied in sibling PR #731.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shaun0927 added a commit that referenced this pull request May 8, 2026
The audit logger now records expression length only (len=N) instead of
the raw expression text. Update the assertion to match — same change
already applied in sibling PR #731 (input refactor) and #732 (webkit
refactor).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shaun0927 added 2 commits May 12, 2026 15:29
Fixes the remaining merge blockers found during PR #731 review: free-form audit payload fields are secret-bearing surfaces again, and the simctl probe now runs only after Tier 1 backends have failed. Tests were updated to lock both contracts.\n\nConstraint: PR #731 is intended as a behavior-preserving input-backend refactor, so fixes stay narrow and keep public APIs unchanged.\nRejected: Leaving text/value visible in audit summaries | Codex review correctly identified this as a security regression for typed or selected secrets.\nRejected: Keeping the simctl probe ahead of Tier 1 | It preserves test convenience but violates the documented fallback order and adds avoidable probe cost/noise.\nConfidence: high\nScope-risk: narrow\nDirective: Future audit-log tests should treat generic user-input fields as sensitive unless a field-specific safe summary is designed.\nTested: npm test -- --runInBand; npm run lint -- --quiet; npx tsc --noEmit --pretty false; npm run build; git diff --check\nNot-tested: Live simulator input dispatch against physical Xcode runtimes
Bring PR #731 onto the current develop tip after GitHub marked the branch dirty. Conflict resolution keeps develop's audit status typing and redaction wording while preserving the PR fix that delays simctl probing until after Tier 1 input backends.\n\nConstraint: GitHub mergeStateStatus was DIRTY after the review-fix commit, so the PR branch had to be updated before merge.\nRejected: Merging while GitHub reported DIRTY | That would bypass the requested merge-readiness gate.\nConfidence: high\nScope-risk: moderate\nDirective: Re-check full CI after this merge commit because develop added simulator module splits.\nTested: npx tsc --noEmit --pretty false; npm test -- --runInBand tests/unit/audit-logger.test.ts tests/unit/http-high-risk-tools.test.ts tests/unit/native-input-backend.test.ts tests/unit/input-backend-resolver.test.ts\nNot-tested: Full suite after merge commit yet
@shaun0927
Copy link
Copy Markdown
Owner Author

Review completed for PR #731.

This PR is valid and merge-ready. Its core intent is a behavior-preserving native input backend split: the former src/tools/native-input-backend.ts monolith is reduced to a compatibility shim, while the resolver, backend interface, concrete input backends, and Flutter VM resolver now live in focused src/input/* modules. That direction is sound because it improves module boundaries and test isolation without changing the public tool API or the documented fallback tiers.

During review I found and fixed two merge blockers:

  1. Restored defensive audit redaction for free-form text and value fields. Codex correctly flagged this as a security regression because those fields can contain typed credentials, OTPs, or selected secrets.
  2. Deferred the simctl help io probe until the resolver actually reaches the Tier 2 simctl branch, so Tier 1 PointerService/SimHID paths keep their intended priority and avoid unnecessary probe cost/noise.

I also updated the relevant regression tests and merged the latest develop into the PR branch after GitHub reported the branch as dirty.

Verification evidence:

  • Local: npm test -- --runInBand → 166 suites / 2528 tests passed.
  • Local: npm run lint -- --quiet → passed.
  • Local: npx tsc --noEmit --pretty false → passed.
  • Local: npm run build → passed, with only the existing optional ws native-package warnings (bufferutil, utf-8-validate).
  • Local: git diff --check → passed.
  • GitHub CI on the final head be8e9092: dependency-audit, lint, test, and build all passed.

Given the narrow corrective commits, clean local verification, green GitHub checks, and preserved public compatibility shim, this PR is safe to merge.

@shaun0927 shaun0927 merged commit 5ff7327 into develop May 12, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant