Преглед изворни кода

test(client): close the merged-architecture coverage and jsdom gaps

- user-text.tsx enters ui-primitives' per-file 100% gate: replace three
  regex-guaranteed impossible ?? fallbacks with asserted captures, make
  the precedence sort a branch-free rank comparator, and pin the nested
  recall-label ordering and the no-basename quoted-path fallback with
  tests (100% statements/branches/functions locally).
- assembled-boot stubs Range.prototype.getBoundingClientRect: jsdom has
  no Range geometry, and Lexical's selection reveal now reaches it in the
  built-graph lane after the architecture merge (the unhandled TypeError
  behind command-image-envelope and preview-boot).
Yichen Jiang пре 1 месец
родитељ
комит
04caa1248d

+ 7 - 0
apps/web/tests/assembled-boot.ts

@@ -159,6 +159,13 @@ export function installAssembledBootEnv(): void {
   if (typeof Element.prototype.scrollIntoView !== 'function') {
     Element.prototype.scrollIntoView = () => {}
   }
+  // jsdom implements no Range geometry either: Lexical's selection reveal
+  // measures the caret with one after a programmatic edit settles focus.
+  if (typeof Range.prototype.getBoundingClientRect !== 'function') {
+    Range.prototype.getBoundingClientRect = () => ({
+      top: 0, bottom: 0, left: 0, right: 0, width: 0, height: 0, x: 0, y: 0, toJSON: () => ({}),
+    }) as DOMRect
+  }
   beforeEach(() => {
     localStorage.clear()
     // The locale service derives its provisional locale from the browser and

+ 5 - 5
packages/client/ui-primitives/src/user-text.tsx

@@ -41,7 +41,7 @@ export function projectUserText(text: string, sessionLabels: readonly string[]):
       end: wire.index + wire[0].length,
       label: wire[0],
       kind: 'session',
-      display: wire[1] ?? '',
+      display: wire[1] as string, // non-optional capture in SESSION_WIRE_RE
     })
   }
   for (const rawLabel of [...new Set(sessionLabels)].sort((a, b) => b.length - a.length)) {
@@ -55,16 +55,16 @@ export function projectUserText(text: string, sessionLabels: readonly string[]):
   const re = /(^|\s)(\/[\w-]+|@"[^"\n]+"|@[^\s]+)/gu
   let m: RegExpExecArray | null
   while ((m = re.exec(text)) !== null) {
-    const tokenStart = m.index + (m[1]?.length ?? 0)
-    const rawLabel = m[2] ?? ''
+    const tokenStart = m.index + (m[1] as string).length // (^|\s) captures '' at line start
+    const rawLabel = m[2] as string // non-optional alternation capture
     const label = rawLabel.startsWith('@"')
       ? rawLabel
       : rawLabel.replace(/[.,;:!?,。;:!?]+$/gu, '')
     if (label.length <= 1) continue
     ranges.push({ start: tokenStart, end: tokenStart + label.length, label, kind: 'plain' })
   }
-  ranges.sort((a, b) => a.start - b.start
-    || (a.kind === b.kind ? b.end - a.end : a.kind === 'session' ? -1 : 1))
+  const rankOf = (range: DecorationRange): number => range.kind === 'session' ? 0 : 1
+  ranges.sort((a, b) => a.start - b.start || rankOf(a) - rankOf(b) || b.end - a.end)
   const parts: ReactNode[] = []
   let cursor = 0
   const pushPlain = (from: number, to: number): void => {

+ 13 - 0
packages/client/ui-primitives/tests/user-text.client.spec.tsx

@@ -62,6 +62,19 @@ describe('projectUserText', () => {
     expect(host.textContent).toBe('用 /plan。 试试 @。')
   })
 
+  it('prefers the longer recall label when one nests inside another', () => {
+    const host = project('@会话一 收尾', ['会话', '会话一'])
+    const chips = [...host.querySelectorAll('[data-ref-chip="session"]')]
+    expect(chips.map(c => c.textContent)).toEqual(['会话一'])
+    expect(host.textContent).toBe('会话一 收尾')
+  })
+
+  it('falls back to the raw quoted label when the path has no basename', () => {
+    const host = project('看 @"/" 下面')
+    const chip = host.querySelector('[data-ref-chip="file"]')!
+    expect(chip.textContent).toBe('"/"')
+  })
+
   it('renders undecorated text as one inline run', () => {
     const host = project('纯文本,无引用')
     expect(host.querySelectorAll('div').length).toBe(0)