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
This commit is contained in:
Jinwoo-H
2026-09-04 02:33:37 -04:00
parent 59798d6858
commit 138f70f53f
2 changed files with 55 additions and 5 deletions
@@ -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
}
@@ -43,11 +43,8 @@ describe('mobile web session screen binding', () => {
expect(hostedSessionRoute).toContain('lastConnectedAt={shell.lastConnectedAt}')
expect(hostedSessionRoute).toContain('<SessionScreen')
expect(nativeSessionRoute).toContain('export function SessionScreen(')
expect(nativeSessionRoute).not.toContain('Clipboard.setStringAsync')
expect(nativeSessionRoute).not.toContain('Linking.openURL')
expect(nativeSessionRoute).not.toContain("router.push('/terminal-settings')")
expect(nativeSessionRoute).not.toContain('@react-native-async-storage/async-storage')
expect(nativeSessionRoute).not.toContain("from '../../../../src/transport/host-store'")
// The native-only API bans moved to hosted-route-native-api-census.test.ts, which walks
// every module the hosted session route reaches rather than this fixed file list.
expect(nativeSessionRoute).toContain('sessionDeviceOperations?.openTerminalSettings()')
expect(nativeSessionRoute).toContain(
'sessionDeviceOperations?.loadTerminalAccessoryPreferences()'