mirror of
https://github.com/stablyai/orca.git
synced 2026-11-05 19:47:27 +00:00
* fix(mobile): wall hosts that cannot serve the page instead of opening native screens * refactor(mobile): drop the native-route state and the shell fallback prop; unknown routes say unavailable * test(mobile): read the nullable page route refusal in its policy test * refactor(mobile): every build is the OTA shell; drop the native/OTA build switch The owner retired the native-screen fallback with no emergency native build, so EXPO_PUBLIC_MOBILE_SHELL, mobileShellBuildKind, shellSwitchDecision, the Metro cache-key suffix and both release workflows' shell input go. Every host route mounts the shell; a route naming no screen the shell could open gets the existing refusal screen instead of a native screen. Claude-Session: https://claude.ai/code/session_6cea92f6 * refactor(mobile): delete native screens the OTA shell no longer reaches With every build the OTA shell, these native siblings run nowhere: the native-executed closure (index.js plus every non-web route) does not reach them, and the page resolves their .web siblings. Each .web sibling takes the plain name, its web-overrides entry goes, and the native-only rich Markdown editor and Mermaid WebView generators and their outputs go with them. Kept: native siblings the node test suite still renders shared screens through (terminal WebView, browser frame path, clipboard, dictation, media picker, live input, snapshot budget, input styles). Claude-Session: https://claude.ai/code/session_6cea92f6 * test(mobile): drop the phone-preview comparison the deleted native sibling held Claude-Session: https://claude.ai/code/session_6cea92f6 * refactor(mobile): drop the dead web route, flag-era comments and the one-value shell kind The host route files no longer switch, so their comments say what they do now: a route naming no screen shows the unavailable screen. app/h/[hostId]/web.tsx duplicated index.tsx and nothing linked to it, so it and its stack entry go (the bundle sweep is re-measured: 77 scripts at 15 routes). Diagnostics print the bridge protocol on its own instead of a shell kind that has one value. Claude-Session: https://claude.ai/code/session_6cea92f6 * refactor(mobile): one host for the rich editor, plain Text, and drop two unused native deps MobileSelectableText was react-native's Text, so callers use Text. The rich Markdown editor's host-facing seams (messages, URL prompt, keyboard inset) are required now that the page is its only host; the WebView defaults go. Comments that described deleted native components are rewritten, and expo-image-manipulator and react-native-uitextview leave package.json and the lockfile. Claude-Session: https://claude.ai/code/session_6cea92f6
125 lines
6.2 KiB
JavaScript
125 lines
6.2 KiB
JavaScript
/**
|
|
* Which page routes render an HTML artifact, and the grant the preview needs from each of them.
|
|
*
|
|
* Natively the preview is its own WebView and `onShouldStartLoadWithRequest` hands every request to
|
|
* `openExternalLink`, so there is nothing to negotiate. In the page a tap inside the sealed frame
|
|
* becomes a top-frame navigation, and only the shell around the document can cancel it and open the
|
|
* URL. A shell built before that event drops the navigation in silence, so
|
|
* `use-html-preview-link-grant.web.ts` asks first (`init.grants.native`) and a route that declared
|
|
* nothing renders the artifact's links as text.
|
|
*
|
|
* `externalNavigation` has no call site of the shape `mobile-web-app-page-grant-call-sites.mjs`
|
|
* parses -- nothing is requested and nothing is answered, so there is no seam function to find --
|
|
* which is why it belongs here beside `haptics` and `screencastBinary` rather than as a row there.
|
|
* The screencast census's shape, against the third token that is neither a verb nor a notify.
|
|
*
|
|
* The lane is optional, so what this census pins is different from the required-lane ones: a route
|
|
* missing the grant is not a native screen, it is a preview with inert links. That makes the
|
|
* declaration easy to forget, and this is the only thing that would notice.
|
|
*/
|
|
import { readFileSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { describe, expect, it } from 'vitest'
|
|
import { mobileWebAppRouteClosure } from './build-mobile-web-app-bundle.mjs'
|
|
import { mobileWebAppDependenciesPresent } from './mobile-web-app-bundle-dependencies.mjs'
|
|
import {
|
|
PAGE_ROUTE_MODULES,
|
|
pageRouteModulesCoverTheManifest
|
|
} from './mobile-web-app-page-route-modules.mjs'
|
|
import { MOBILE_WEB_PAGE_ROUTES } from './mobile-web-page-routes.mjs'
|
|
|
|
const mobileDir = fileURLToPath(new URL('../../mobile/', import.meta.url))
|
|
const describeClosure = mobileWebAppDependenciesPresent() ? describe : describe.skip
|
|
|
|
/** The seam as the web build resolves it, and the native sibling the page must never reach. */
|
|
const SEAM = 'src/components/use-html-preview-link-grant.web.ts'
|
|
const NATIVE = 'src/components/use-html-preview-link-grant.ts'
|
|
/** Where the grant token is declared, so this file reads it rather than spelling it again. */
|
|
const GRANT_MODULE = 'src/mobile-web-shell/cancelled-navigation-target.ts'
|
|
|
|
/** The token, parsed off its own declaration: a second spelling is one that can drift. */
|
|
function externalNavigationGrantToken() {
|
|
const source = readFileSync(join(mobileDir, GRANT_MODULE), 'utf8')
|
|
const declared = /BRIDGE_EXTERNAL_NAVIGATION_GRANT = '([^']+)'/.exec(source)
|
|
if (declared === null) {
|
|
throw new Error(`${GRANT_MODULE} no longer declares the grant this census reads`)
|
|
}
|
|
return declared[1]
|
|
}
|
|
|
|
/** The modules that call the hook, which is the preview and whatever else grows one. */
|
|
function linkGrantCallers(closure) {
|
|
return closure.local.filter((file) => {
|
|
if (!/\.tsx?$/.test(file) || file === SEAM || file === NATIVE) {
|
|
return false
|
|
}
|
|
return /\buseHtmlPreviewLinkGrant\s*\(/.test(readFileSync(join(mobileDir, file), 'utf8'))
|
|
})
|
|
}
|
|
|
|
/** Both lanes, because the token may be declared on either and the census is about the route
|
|
* holding it at all. Which lane is the product call ruling 37.3 gave the desktop. */
|
|
function routesDeclaring(token) {
|
|
return MOBILE_WEB_PAGE_ROUTES.filter(
|
|
(route) => route.grants.includes(token) || (route.optionalGrants ?? []).includes(token)
|
|
).map((route) => route.pathname)
|
|
}
|
|
|
|
describeClosure(
|
|
'the routes that render an HTML artifact',
|
|
() => {
|
|
it('declares external navigation on exactly the routes whose closure asks for it', async () => {
|
|
const asking = []
|
|
for (const [route, mod] of PAGE_ROUTE_MODULES) {
|
|
const closure = await mobileWebAppRouteClosure(mod)
|
|
if (linkGrantCallers(closure).length > 0) {
|
|
asking.push(route)
|
|
}
|
|
}
|
|
// One route today, and the precondition an assertion about a derived set needs: an empty
|
|
// list is also what a walk that read nothing produces. The file preview route is deliberately
|
|
// not here -- it renders `MobileFilePreviewScreen`, which has no HTML preview in its closure.
|
|
expect(asking).toEqual(['/h/[hostId]/session/[worktreeId]'])
|
|
expect([...routesDeclaring(externalNavigationGrantToken())].sort()).toEqual(
|
|
[...asking].sort()
|
|
)
|
|
})
|
|
|
|
it('declares it on the optional lane, so a shell without it keeps the screen on the page', () => {
|
|
const session = MOBILE_WEB_PAGE_ROUTES.find(
|
|
(route) => route.pathname === '/h/[hostId]/session/[worktreeId]'
|
|
)
|
|
if (!session) {
|
|
throw new Error('the manifest lost the session route this census is written against')
|
|
}
|
|
const token = externalNavigationGrantToken()
|
|
expect(session.optionalGrants).toContain(token)
|
|
// The half that matters: on the required lane this one name would take the whole session
|
|
// screen native on every shell built before the cancelled-navigation event.
|
|
expect(session.grants).not.toContain(token)
|
|
})
|
|
|
|
it('reaches the seam through its web sibling, and the caller is the preview', async () => {
|
|
const closure = await mobileWebAppRouteClosure(
|
|
PAGE_ROUTE_MODULES.get('/h/[hostId]/session/[worktreeId]')
|
|
)
|
|
expect(closure.local).toContain(SEAM)
|
|
expect(closure.local).not.toContain(NATIVE)
|
|
expect(linkGrantCallers(closure)).toEqual(['src/components/MobileHtmlPreview.tsx'])
|
|
// The preview is mounted by the session's file reader rather than by a route of its own,
|
|
// which is why the grant has no route to be pinned against except this one.
|
|
expect(closure.local).toContain('src/session/MobileSessionFileReader.tsx')
|
|
// And the inerting pass is in the closure too: without it the hook's answer would change a
|
|
// sandbox token and leave the dead anchor ruling 37.2 forbids.
|
|
expect(closure.local).toContain('src/components/html-preview-inert-links.ts')
|
|
})
|
|
|
|
it('covers every declared page route, so a new one cannot be missed by this file', () => {
|
|
const { mapped, declared } = pageRouteModulesCoverTheManifest(MOBILE_WEB_PAGE_ROUTES)
|
|
expect(mapped).toEqual(declared)
|
|
})
|
|
},
|
|
240_000
|
|
)
|