From 138f70f53fb5a3607aef86e3e4998062abc99fdd Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 4 Sep 2026 02:33:37 -0400 Subject: [PATCH] test(mobile): census native-only APIs over the whole hosted graph The binding test banned Clipboard.setStringAsync, Linking.openURL and router.push('/terminal-settings') over a 42-file list, so injecting all three into MobileTerminalInputActions.tsx passed. The census walks the 1052 modules the hosted session route actually reaches and catches it. AsyncStorage is deliberately excluded: it is genuinely reachable from the hosted bundle today through the shared storage and transport modules. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb --- .../hosted-route-native-api-census.test.ts | 53 +++++++++++++++++++ ...-web-session-screen-binding-source.test.ts | 7 +-- 2 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 mobile/src/mobile-web/hosted-route-native-api-census.test.ts diff --git a/mobile/src/mobile-web/hosted-route-native-api-census.test.ts b/mobile/src/mobile-web/hosted-route-native-api-census.test.ts new file mode 100644 index 00000000000..16dd1cd4742 --- /dev/null +++ b/mobile/src/mobile-web/hosted-route-native-api-census.test.ts @@ -0,0 +1,53 @@ +import { readFileSync } from 'node:fs' +import { join, relative } from 'node:path' +import { describe, expect, it } from 'vitest' +import { hostedModuleGraph, hostedRoot, mobileRoot } from './hosted-module-graph.test-support' + +const HOSTED_SESSION_ROUTE = join(hostedRoot, 'h', '[hostId]', 'session', '[worktreeId].tsx') + +// Native-only entry points: on the hosted page these either throw or silently do nothing. +const BANNED_IN_HOSTED_BUNDLE = ['Clipboard.setStringAsync', "router.push('/terminal-settings')"] +const BANNED_IN_SESSION_ROUTE = [ + ...BANNED_IN_HOSTED_BUNDLE, + 'Linking.openURL', + "/transport/host-store'" +] + +describe('hosted route native API census', () => { + it('keeps native-only APIs out of every module the hosted session route reaches', () => { + const graph = hostedModuleGraph([HOSTED_SESSION_ROUTE]) + + // Presence precondition: the walk must actually reach the session screen's own modules. + expect(graph.length).toBeGreaterThan(500) + expect(graph.map((path) => relative(mobileRoot, path))).toEqual( + expect.arrayContaining([ + join('app', 'h', '[hostId]', 'session', '[worktreeId].tsx'), + join('src', 'session', 'MobileTerminalInputActions.tsx') + ]) + ) + expect(offenders(graph, BANNED_IN_SESSION_ROUTE)).toEqual([]) + }) + + it('keeps native-only APIs out of every module any hosted route reaches', () => { + const graph = hostedModuleGraph() + + expect(graph.length).toBeGreaterThan(500) + // AsyncStorage is deliberately not censused: it is reachable today through the shared + // storage and transport modules, and on web it resolves to localStorage. + expect(offenders(graph, BANNED_IN_HOSTED_BUNDLE)).toEqual([]) + }) +}) + +function offenders(graph: string[], banned: string[]): string[] { + const found: string[] = [] + for (const modulePath of graph) { + readFileSync(modulePath, 'utf8') + .split('\n') + .forEach((line, index) => { + if (banned.some((pattern) => line.includes(pattern))) { + found.push(`${relative(mobileRoot, modulePath)}:${index + 1}`) + } + }) + } + return found +} diff --git a/mobile/src/session/mobile-web-session-screen-binding-source.test.ts b/mobile/src/session/mobile-web-session-screen-binding-source.test.ts index e0beb768573..89311211f95 100644 --- a/mobile/src/session/mobile-web-session-screen-binding-source.test.ts +++ b/mobile/src/session/mobile-web-session-screen-binding-source.test.ts @@ -43,11 +43,8 @@ describe('mobile web session screen binding', () => { expect(hostedSessionRoute).toContain('lastConnectedAt={shell.lastConnectedAt}') expect(hostedSessionRoute).toContain('