From 04caa1248d0d894fa26115c4819e6ede683ce967 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Mon, 24 Aug 2026 11:39:34 +0800 Subject: [PATCH] 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). --- apps/web/tests/assembled-boot.ts | 7 +++++++ packages/client/ui-primitives/src/user-text.tsx | 10 +++++----- .../ui-primitives/tests/user-text.client.spec.tsx | 13 +++++++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/apps/web/tests/assembled-boot.ts b/apps/web/tests/assembled-boot.ts index c0199f2cba..e594e51659 100644 --- a/apps/web/tests/assembled-boot.ts +++ b/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 diff --git a/packages/client/ui-primitives/src/user-text.tsx b/packages/client/ui-primitives/src/user-text.tsx index def636a30c..494f49a868 100644 --- a/packages/client/ui-primitives/src/user-text.tsx +++ b/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 => { diff --git a/packages/client/ui-primitives/tests/user-text.client.spec.tsx b/packages/client/ui-primitives/tests/user-text.client.spec.tsx index a9654299b2..6e1c2e309f 100644 --- a/packages/client/ui-primitives/tests/user-text.client.spec.tsx +++ b/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)