Files
orca/mobile/src/components/markdown-href-routing.test.ts
T
Brennan Benson 9ee359550b fix(mobile): make native-chat file links and path citations tappable (STA-3331) (#12364)
* fix(mobile): make native-chat file links and path citations tappable (STA-3331)

- Linkify POSIX absolute paths in chat prose (leading-/ regex alternative;
  URL guard now keys off the char before the matched slash)
- Parse agent-style path:line(:col) citations in prose, code spans, and the
  open flow; line/column ride into the mobile file preview route
- Route non-web markdown hrefs (file: URIs, relative/absolute paths) to the
  file opener instead of silently dropping them; unknown schemes stay dead
- Resolve chat paths against the worktree root, not the terminal's live cwd
- Reuse the terminal tap-to-open flow for chat taps (haptic, preview route,
  tab activation with retries) via a shared identity-stable hook, and toast
  on misses instead of silent no-ops
- Keep snake_case paths whole (intraword underscores are literal text),
  scan bold/italic/strike spans for paths, split trailing punctuation off
  autolinks, and let taps land while the composer keyboard is up

* fix(mobile): harden chat file tap handling

* refactor(chat): share native chat href routing

* fix(mobile): detect files directly under path roots

* fix(mobile): keep inline tokens and dunder paths intact around emphasis

Review follow-ups on the chat file-link work:

- A rejected intraword `_` token left the scan index past its closing
  underscore, so every inline token between two snake_case words was
  swallowed and rendered as literal source — including markdown links,
  which became untappable. Rescan from just past the opening delimiter.
- Treat a path separator as an intraword flank so `src/__init__.py` and
  `a/__tests__/x.ts` stay whole; previously they rendered as bold plus a
  remnant that the new absolute-root pattern turned into a tap on `/x.ts`.
- Bound the `:line(:col)` tail so `src/app.ts:1e3` and `:80%` no longer
  parse a line number, while a cited range still opens its first line.
- Route chat tap failures through the composer banner (toast fallback):
  chat taps happen with the keyboard up, which covers the toast.
- Drop the tap-handler mirror's dep list; the call site rebuilds its
  accessors every render, so it could never skip on a route that
  rerenders per keystroke.

* Revert "fix(mobile): keep inline tokens and dunder paths intact around emphasis"

This reverts commit 308bfaf22b.

* fix(mobile): preserve chat file-link parsing and feedback
2026-08-04 15:46:08 -07:00

80 lines
2.6 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { routeMarkdownHref } from './markdown-href-routing'
describe('routeMarkdownHref', () => {
it('routes web and mail links to the system handler', () => {
expect(routeMarkdownHref('https://example.com/docs')).toEqual({
kind: 'web',
url: 'https://example.com/docs'
})
expect(routeMarkdownHref('http://localhost:3000/')).toEqual({
kind: 'web',
url: 'http://localhost:3000/'
})
expect(routeMarkdownHref(' mailto:dev@example.com ')).toEqual({
kind: 'web',
url: 'mailto:dev@example.com'
})
})
it('routes relative hrefs to the file opener', () => {
expect(routeMarkdownHref('src/foo.ts')).toEqual({ kind: 'file', pathText: 'src/foo.ts' })
expect(routeMarkdownHref('./docs/plan.md')).toEqual({
kind: 'file',
pathText: './docs/plan.md'
})
})
it('carries a #L fragment as a :line suffix', () => {
expect(routeMarkdownHref('docs/plan.md#L42')).toEqual({
kind: 'file',
pathText: 'docs/plan.md:42'
})
expect(routeMarkdownHref('docs/plan.md?plain=1#line-7')).toEqual({
kind: 'file',
pathText: 'docs/plan.md:7'
})
expect(routeMarkdownHref('docs/plan.md#usage')).toEqual({
kind: 'file',
pathText: 'docs/plan.md'
})
})
it('decodes percent-encoded href paths', () => {
expect(routeMarkdownHref('docs/release%20notes.md')).toEqual({
kind: 'file',
pathText: 'docs/release notes.md'
})
})
it('routes file: URIs to the file opener', () => {
expect(routeMarkdownHref('file:///Users/me/wt/src/app.tsx')).toEqual({
kind: 'file',
pathText: '/Users/me/wt/src/app.tsx'
})
expect(routeMarkdownHref('file:///Users/me/wt/src/app.tsx#L12')).toEqual({
kind: 'file',
pathText: '/Users/me/wt/src/app.tsx:12'
})
expect(routeMarkdownHref('file:///C:/repo/src/index.ts')).toEqual({
kind: 'file',
pathText: 'C:/repo/src/index.ts'
})
})
it('keeps Windows drive paths out of the scheme filter', () => {
expect(routeMarkdownHref(String.raw`C:\repo\src\index.ts`)).toEqual({
kind: 'file',
pathText: String.raw`C:\repo\src\index.ts`
})
})
it('drops anchors, unknown schemes, and empty hrefs', () => {
expect(routeMarkdownHref('#section')).toEqual({ kind: 'none' })
expect(routeMarkdownHref('')).toEqual({ kind: 'none' })
expect(routeMarkdownHref('editor://file/x.ts')).toEqual({ kind: 'none' })
expect(routeMarkdownHref('javascript:alert(1)')).toEqual({ kind: 'none' })
expect(routeMarkdownHref('data:text/plain,hi')).toEqual({ kind: 'none' })
})
})