From d87f18233406048bc62374c783ad35d10f08b00b Mon Sep 17 00:00:00 2001 From: Shevchik Igor Date: Thu, 13 Aug 2026 12:57:10 +0000 Subject: [PATCH 1/3] test(CommandPalette): cover the gaps an independent review pass found MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six mutations of `src/runtime/utils/search.ts` survived the whole suite. Each is now killed by a named test. **`forceKey`/`omitKeys` had no coverage at all.** Deleting both guards outright left all 218 tests in `test/utils/search.spec.ts` and `test/components/CommandPalette.spec.ts` passing. The mechanism runs three times per item per keystroke — `CommandPalette` calls `highlight()` once per rendered field against one shared `matches` array, and these guards are what make each call pick its own entry. Nothing anywhere built a `matches` array with more than one entry, so both `continue` branches were dead code as far as the suite was concerned, and a regression would have rendered the label's text in the suffix slot with nothing failing. Covered twice: a unit block with the wanted key placed last, and an end-to-end case where fuse really returns three matches. **The surrogate `_START` fixtures sat 0x100 from their bounds.** An off-by-one widen of `HIGH_SURROGATE_START` or `LOW_SURROGATE_START` reached neither `휀` nor `\uDB00` and survived — while the comment beside them claimed these strings were what caught widening. True of the `_END` pair only. Moved to `퟿` and `\uDBFF`, one code point outside the bound each. **`CLUSTER_CONTINUATION_FLOOR` had no fixture at the floor.** The only combining mark in the suite is U+0301, one above it, and `0x301 < 0x300` and `0x301 < 0x301` are both false — so raising the floor changed nothing and the mutation survived. Added U+0300, where it flips. **`value = value || ''` was untested.** Fuse declares `value?: string`; without the guard a match carrying none throws inside `substring()`, in `CommandPalette`'s render path. **The cluster truncation cases were vacuous about quantity.** Forcing the truncation branch unconditionally — collapsing every result to a bare ellipsis — left all five passing, because an empty string satisfies "no fragments" for free. They now count what survived, in code points rather than clusters: the budget buys six two-code-point flags but only two five-code-point ZWJ families. Two comments stated things the code does not do. The retained prefix is not "always 13 characters" — the budget spans every mark from the first one on, so it is 13 per mark, now pinned across one to four of them. And `Infinity` is not "clamped to the end of the value": `Number.isInteger` rejects it exactly as it rejects `NaN` and `6.5`, so it never reaches the arithmetic. Also escaped the combining accent fixture rather than leaving it a literal glyph — NFC collapses a literal to one precomposed code point, which would silently turn it into a single-code-point fixture that passes whatever the floor is set to — and parametrised the ceiling-escaping case over `"` as well as `&`, since the six-fold expansion is the one `createClusterSnapper` documents as worst. Tests only; `src/` untouched. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LWWrBHgfqGSbeU3V6UuMF8 --- test/components/CommandPalette.spec.ts | 33 ++++++ test/utils/search.spec.ts | 152 ++++++++++++++++++++++--- 2 files changed, 171 insertions(+), 14 deletions(-) diff --git a/test/components/CommandPalette.spec.ts b/test/components/CommandPalette.spec.ts index d9f78ea9..2dcad4b0 100644 --- a/test/components/CommandPalette.spec.ts +++ b/test/components/CommandPalette.spec.ts @@ -278,6 +278,39 @@ describe('CommandPalette', () => { expect(wrapper.html()).not.toContain('�') expect(wrapper.find('mark').text()).not.toMatch(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/) }) + + it('marks each field from its own match, not from whichever fuse returned first', async () => { + // The component calls `highlight()` three times per item against one shared + // `matches` array, and `forceKey`/`omitKeys` are what make each call pick + // its own field. Every other item in this file has a `label` and nothing + // else, so fuse returns a single match and the selection is trivially + // right whatever those arguments do — deleting both guards used to leave + // this whole file green. + // + // Here the term hits all three fields, so fuse returns three matches and a + // mis-selection renders one field's text inside another's element. + const wrapper = await mountSuspended(CommandPalette, { + props: { + groups: [{ id: 'g', items: [{ label: 'orbit alpha', suffix: 'orbit bravo', description: 'orbit charlie' }] }], + searchTerm: 'orbit', + fuse: { fuseOptions: { includeMatches: true, threshold: 0.1, ignoreLocation: true, keys: ['label', 'suffix', 'description'] } } + } as any + }) + + const marks = wrapper.findAll('mark') + + expect(marks.length).toBeGreaterThan(1) + + // Each mark must sit inside the field it came from: the surrounding text + // is what betrays a swap, since the marked word itself is the same in all + // three. + for (const field of ['alpha', 'bravo', 'charlie']) { + const owner = marks.find(mark => mark.element.parentElement?.textContent?.includes(field)) + + expect(owner, `no mark rendered beside "${field}"`).toBeDefined() + expect(owner!.text()).toBe('orbit') + } + }) }) it('renders an `item-description` slot for an item that has no description', async () => { diff --git a/test/utils/search.spec.ts b/test/utils/search.spec.ts index 585880bc..0fe180c1 100644 --- a/test/utils/search.spec.ts +++ b/test/utils/search.spec.ts @@ -123,6 +123,64 @@ describe('highlight', () => { }) }) + describe('key selection', () => { + // `CommandPalette` calls `highlight()` three times per item against one + // shared `matches` array — once per rendered field — and depends on + // `forceKey`/`omitKeys` to make each call pick its own entry. Deleting both + // guards outright left every test in this file and in + // `CommandPalette.spec.ts` passing: no other fixture anywhere carries more + // than one match, so both `continue` branches were dead code as far as the + // suite was concerned, and a regression would have rendered the label's + // text in the suffix slot with nothing failing. + const label = 'alpha team' + const suffix = 'alpha squad' + const description = 'alpha unit' + + // Deliberately not in field order, and `label` deliberately last: a + // `forceKey` that stopped being honoured would return the *suffix* entry, + // which reads as a plausible result rather than an obvious break. + const item = { + label, + suffix, + description, + matches: [ + { key: 'suffix', value: suffix, indices: [[0, 4]] as [number, number][] }, + { key: 'description', value: description, indices: [[0, 4]] as [number, number][] }, + { key: 'label', value: label, indices: [[0, 4]] as [number, number][] } + ] + } + + it('marks the forced key rather than the first match', () => { + expect(highlight(item, 'alpha', 'label')).toBe('alpha team') + }) + + it('skips the omitted keys — the three calls `CommandPalette` makes per item', () => { + expect(highlight(item, 'alpha', 'label', undefined)).toBe('alpha team') + expect(highlight(item, 'alpha', 'suffix', ['label'])).toBe('alpha squad') + expect(highlight(item, 'alpha', 'description', ['label', 'suffix'])).toBe('alpha unit') + }) + + it('takes the first match when nothing is forced or omitted', () => { + expect(highlight(item, 'alpha')).toBe('alpha squad') + }) + + it('returns undefined when every match is omitted', () => { + expect(highlight(item, 'alpha', undefined, ['label', 'suffix', 'description'])).toBeUndefined() + }) + + it('returns undefined when the forced key is not among the matches', () => { + expect(highlight({ label, matches: [{ key: 'suffix', value: suffix, indices: [[0, 4]] }] }, 'alpha', 'label')) + .toBeUndefined() + }) + + it('falls back to an empty value rather than throwing when a match carries none', () => { + // Fuse's own type declares `value?: string`. Without the `value || ''` + // guard this reaches `substring()` on `undefined` and throws inside + // `CommandPalette`'s render path — a harder failure than an empty mark. + expect(highlight({ label, matches: [{ key: 'label', indices: [[0, 3]] }] }, 'alpha', 'label')).toBe('') + }) + }) + describe('mark insertion', () => { function highlightRegions(value: string, indices: [number, number][]) { // A one-character term keeps `minTokenLength` at 1, so every region here is @@ -233,8 +291,13 @@ describe('highlight', () => { // obligation. `NaN` compares false against everything — `end > start` // included, so no mark is emitted and nothing looks wrong — and then lands // in the cursor, where `substring(NaN)` reads as `substring(0)` and repeats - // the entire value after the part already written. `Infinity` clamps to the - // end of the value and marks all of it. + // the entire value after the part already written. + // + // All three cases below take the same exit, and it is the filter rather + // than any clamp: `Number.isInteger` is false for `NaN`, for `Infinity` + // and for `6.5` alike, so none of them reaches the snapping arithmetic at + // all. Worth stating because the `Infinity` case reads like a clamp and is + // not one. const value = 'alpha beta gamma' function withExtra(extra: [number, number]) { @@ -276,11 +339,28 @@ describe('highlight', () => { ['a ZWJ family', '\u{1F468}‍\u{1F469}‍\u{1F467}'], ['an emoji with a skin-tone modifier', '\u{1F44D}\u{1F3FF}'], ['a Devanagari consonant with a vowel sign', 'कि'], - ['a combining accent', 'é'] + // Escaped rather than written as a glyph: a literal combining mark is + // exactly what NFC collapses into one precomposed code point, and an IDE + // reformat doing that silently turns this into a single-code-point fixture + // that passes whatever `CLUSTER_CONTINUATION_FLOOR` is set to. Every other + // multi-code-point fixture here already uses escapes. + ['a combining accent', 'e\u0301'], + // U+0300 sits *at* the floor, U+0301 one above it, and only the former + // pins it: `0x301 < 0x300` and `0x301 < 0x301` are both false, so raising + // the floor by one leaves the accent above unaffected and the mutation + // survives. `0x300 < 0x301` is true, which screens this cluster out and + // fails loudly. + ['a combining grave, at the screening floor', 'e\u0300'] ] const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' }) + // The truncation budget, in *code points* — the same figure `truncation from + // the start` pins as `RETAINED`. How many clusters that buys depends on the + // cluster: six two-code-point flags, but only two five-code-point ZWJ + // families, and the remainder is snapped away rather than kept as a fragment. + const BUDGET_CODE_POINTS = 13 + function highlightAfter(cluster: string, count: number) { const value = cluster.repeat(count) + 'match' const index = value.indexOf('match') @@ -312,6 +392,13 @@ describe('highlight', () => { // Re-segmenting what survived must yield whole copies of the fixture and // nothing else — a fragment would segment into something different. expect([...segmenter.segment(kept)].map(entry => entry.segment).filter(entry => entry !== cluster)).toEqual([]) + + // And count them. The filter above is vacuously satisfied by an empty + // string, so on its own it passes against an implementation that + // truncates the match away entirely — verified by forcing the truncation + // branch unconditionally, which left all of these green. + expect([...segmenter.segment(kept)]) + .toHaveLength(Math.min(count, Math.floor(BUDGET_CODE_POINTS / [...cluster].length))) } }) @@ -443,7 +530,12 @@ describe('highlight', () => { // `LOW_SURROGATE_START`, a BMP character before a lone low surrogate // for `HIGH_SURROGATE_START`, and a lone high surrogate before a // private-use character for `LOW_SURROGATE_END`. - const unpaired = ['\uDC00\uDC01', '\uD800\uDB00', '\uD700\uDC00', '\uD800\uE000'] + // + // Each probe sits one code point outside the bound it pins, so a widen + // of one is caught. Two of them used to sit 0x100 away \u2014 far enough that + // an off-by-one widen of either `_START` constant reached nothing and + // survived the suite, while the comment above claimed otherwise. + const unpaired = ['\uDC00\uDC01', '\uD800\uDBFF', '\uD7FF\uDC00', '\uD800\uE000'] expect(unpaired.map((prefix) => { const value = `${prefix}zz` @@ -486,17 +578,22 @@ describe('highlight', () => { expect(firstCluster(result)).toBe(FLAG) }) - it('measures the ceiling against the value, not its escaped copy', () => { - // Escaping expands: `&` to five characters, `"` to six. Weighing the - // escaped copy therefore retired the snap for values a fraction of the - // ceiling's length — this one is at 22% of it — and did so silently, for - // exactly the multi-code-point clusters the snap exists to keep whole - // (#387). Held against `value.length`, it snaps. - const value = `${'&'.repeat(1600)}${FLAG.repeat(50)}match` + // Both escapes the ceiling arithmetic can be defeated by, at their own + // expansion factors. `"` is the six-fold one the `createClusterSnapper` + // docs cite as the worst case, and until now only `&` had a fixture. + it.each([ + ['&', '&', 1600], + ['"', '"', 1350] + ])('measures the ceiling against the value, not its %s-escaped copy', (raw, escaped, count) => { + // Weighing the escaped copy retired the snap for values a fraction of the + // ceiling's length — both of these sit around a fifth of it — and did so + // silently, for exactly the multi-code-point clusters the snap exists to + // keep whole (#387). Held against `value.length`, they snap. + const value = `${raw.repeat(count)}${FLAG.repeat(50)}match` const result = highlight(matchAfter(value), 'match', 'label') ?? '' expect(value.length).toBeLessThan(GRAPHEME_SNAP_MAX_LENGTH) - expect(value.replace(/&/g, '&').length + MARKUP).toBeGreaterThan(GRAPHEME_SNAP_MAX_LENGTH) + expect(value.replaceAll(raw, escaped).length + MARKUP).toBeGreaterThan(GRAPHEME_SNAP_MAX_LENGTH) expect(result).not.toMatch(LONE_SURROGATE) expect(firstCluster(result)).toBe(FLAG) @@ -506,8 +603,13 @@ describe('highlight', () => { describe('truncation from the start', () => { // `maxLength` counts the tag characters that the counter inside // `truncateHTMLFromStart` skips, so the two cancel and the surviving prefix is - // always `''.length + ''.length` characters — whatever the match - // is, and whether the content is BMP or astral. + // `''.length + ''.length` characters — whether the content is BMP + // or astral. + // + // Per *mark*, though, not per call: the budget is measured over everything + // from the first `` onward, so it grows by another 13 for every further + // mark in that span. `scales the budget with the number of marks` pins that; + // every other case here has exactly one. const RETAINED = ''.length + ''.length function highlightAfterFiller(filler: string, count: number) { @@ -545,6 +647,28 @@ describe('highlight', () => { expect(highlightAfterFiller('a', 50)).toBe(`...${'a'.repeat(RETAINED)}match`) }) + it('scales the budget with the number of marks', () => { + // Fuse routinely returns several regions for one field on a multi-word + // query, and the budget counts the tag characters of *all* of them, so the + // retained prefix grows by 13 per mark rather than staying at 13. Nothing + // asserted this, and the comment above used to claim the figure was fixed. + const marked = (count: number) => { + const words = Array.from({ length: count }, () => 'match').join(' ') + const value = 'a'.repeat(60) + words + const indices: [number, number][] = [] + + for (let index = 0, at = 60; index < count; index++, at += 6) { + indices.push([at, at + 4]) + } + + const result = highlight({ label: value, matches: [{ key: 'label', value, indices }] }, 'match', 'label') ?? '' + + return result.replace(/^\.\.\./, '').split('')[0]!.length + } + + expect([1, 2, 3, 4].map(marked)).toEqual([1, 2, 3, 4].map(count => RETAINED * count)) + }) + it('measures the budget in code points when astral content follows the match', () => { // Guards the caller's half of the fix. Sizing the budget in UTF-16 units // while the counter inside `truncateHTMLFromStart` counts code points would From 74bea9f036ed2b379ca5bd07050b158b22c469c1 Mon Sep 17 00:00:00 2001 From: Shevchik Igor Date: Thu, 13 Aug 2026 13:43:33 +0000 Subject: [PATCH 2/3] test(CommandPalette): pin `omitKeys` on its own, not behind `forceKey` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three of the review passes over this branch landed on the same thing, from different directions: `skips the omitted keys` did not pin `omitKeys`. Every one of its calls set `forceKey` as well, and that narrows the loop to a single candidate before the omit check can matter — deleting the `omitKeys` guard alone left all three of its assertions green. Only `returns undefined when every match is omitted` caught it, because that one leaves `forceKey` unset. A test that passes for the wrong reason is exactly the failure mode this branch exists to remove, so: renamed to say what it does pin — that the three real call shapes resolve to three different fields — and added two cases that isolate the guard. One omits a key with nothing forced, so the omit is the only thing between the cursor and the first entry, and the loop has to carry on past it rather than give up. The other omits the key it forces. Deleting the guard now fails three tests instead of one. Also from the same passes: - The prefix in `scales the budget with the number of marks` was a literal 60 characters. At five marks the budget reaches 65, truncation stops happening, and the test silently measures the whole prefix instead — a flat line that still passes for the counts that fit. Derived from `RETAINED` and the counts under test, with an assertion that truncation actually occurred. - One assertion was a duplicate: an omitted fourth argument and an explicit `undefined` bind identically. - A comment carried the literal text `—` rather than an em dash. Inside a `//` comment nothing interprets it, so it would have stayed visible as an escape sequence. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LWWrBHgfqGSbeU3V6UuMF8 --- test/utils/search.spec.ts | 40 ++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/test/utils/search.spec.ts b/test/utils/search.spec.ts index 0fe180c1..72ff28c0 100644 --- a/test/utils/search.spec.ts +++ b/test/utils/search.spec.ts @@ -154,12 +154,30 @@ describe('highlight', () => { expect(highlight(item, 'alpha', 'label')).toBe('alpha team') }) - it('skips the omitted keys — the three calls `CommandPalette` makes per item', () => { + it('resolves the three calls `CommandPalette` makes per item to three different fields', () => { expect(highlight(item, 'alpha', 'label', undefined)).toBe('alpha team') expect(highlight(item, 'alpha', 'suffix', ['label'])).toBe('alpha squad') expect(highlight(item, 'alpha', 'description', ['label', 'suffix'])).toBe('alpha unit') }) + it('skips an omitted key and keeps looking', () => { + // The case above pins the real call shapes, but it cannot pin `omitKeys`: + // every one of its calls sets `forceKey` too, and that narrows the loop to + // a single candidate before the omit check can matter. Deleting the + // `omitKeys` guard alone leaves all three of its assertions green. + // + // Here nothing is forced, so the omit is the only thing standing between + // the cursor and the first entry — and the loop has to carry on past it + // rather than give up, which `returns undefined when every match is + // omitted` cannot show either. + expect(highlight(item, 'alpha', undefined, ['suffix'])).toBe('alpha unit') + expect(highlight(item, 'alpha', undefined, ['suffix', 'description'])).toBe('alpha team') + }) + + it('honours an omitted key that is also the forced one', () => { + expect(highlight(item, 'alpha', 'label', ['label'])).toBeUndefined() + }) + it('takes the first match when nothing is forced or omitted', () => { expect(highlight(item, 'alpha')).toBe('alpha squad') }) @@ -532,7 +550,7 @@ describe('highlight', () => { // private-use character for `LOW_SURROGATE_END`. // // Each probe sits one code point outside the bound it pins, so a widen - // of one is caught. Two of them used to sit 0x100 away \u2014 far enough that + // of one is caught. Two of them used to sit 0x100 away — far enough that // an off-by-one widen of either `_START` constant reached nothing and // survived the suite, while the comment above claimed otherwise. const unpaired = ['\uDC00\uDC01', '\uD800\uDBFF', '\uD7FF\uDC00', '\uD800\uE000'] @@ -652,21 +670,33 @@ describe('highlight', () => { // query, and the budget counts the tag characters of *all* of them, so the // retained prefix grows by 13 per mark rather than staying at 13. Nothing // asserted this, and the comment above used to claim the figure was fixed. + const COUNTS = [1, 2, 3, 4] + + // The prefix has to outlast the largest budget, or truncation stops + // happening and the test quietly measures the whole prefix instead of the + // budget — passing for counts that fit and reporting a flat line for the + // rest. Derived rather than written as a literal for that reason. + const prefixLength = RETAINED * Math.max(...COUNTS) + 1 + const marked = (count: number) => { const words = Array.from({ length: count }, () => 'match').join(' ') - const value = 'a'.repeat(60) + words + const value = 'a'.repeat(prefixLength) + words const indices: [number, number][] = [] - for (let index = 0, at = 60; index < count; index++, at += 6) { + for (let index = 0, at = prefixLength; index < count; index++, at += 6) { indices.push([at, at + 4]) } const result = highlight({ label: value, matches: [{ key: 'label', value, indices }] }, 'match', 'label') ?? '' + // An untruncated result would have no ellipsis and would return the full + // prefix, which is what the guard above exists to prevent. + expect(result.startsWith('...')).toBe(true) + return result.replace(/^\.\.\./, '').split('')[0]!.length } - expect([1, 2, 3, 4].map(marked)).toEqual([1, 2, 3, 4].map(count => RETAINED * count)) + expect(COUNTS.map(marked)).toEqual(COUNTS.map(count => RETAINED * count)) }) it('measures the budget in code points when astral content follows the match', () => { From 010f0bf48224127e67e62e74cb85f2a03e08c631 Mon Sep 17 00:00:00 2001 From: Shevchik Igor Date: Thu, 13 Aug 2026 14:02:18 +0000 Subject: [PATCH 3/3] test(CommandPalette): correct three claims the documentation review checked MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All three were mine, and all three were checkable — which is how they were caught. **"218 tests" was 220.** The figure came from an early planning note and was carried into the commit message and the PR body without being re-measured against the final `origin/main` content. Counted: 110 per project, two projects. **"Every other multi-code-point fixture here already uses escapes" was false.** The Devanagari pair is written as literal glyphs, and the ZWJ family's joiners are literal U+200D bytes sitting between escaped emoji — only the flag and the skin-tone modifier were fully escaped. Rather than restate the claim more narrowly, the ZWJ joiners are now escaped too, for a reason the original comment did not name: they are invisible in an editor, so one could be deleted without anyone seeing. That is a different hazard from NFC collapse and it applies to that fixture specifically. The Devanagari pair keeps its glyphs — it is visible, and `normalize('NFC')` round-trips it unchanged, so neither hazard applies. The comment now says which reason applies where instead of claiming a consistency that was not there. The escaped joiners are byte-identical to the literals they replace. **"752 tests green" had no stated scope**, and none of the obvious readings reproduce it — not the repository, not `test/utils/` plus `test/components/`, not the two files this branch changes. It counted `test/utils/` plus `CommandPalette.spec.ts`, which is a scope nobody could infer. The PR body now names the command. The same convention was used on #388's "732" and was equally unverifiable there. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LWWrBHgfqGSbeU3V6UuMF8 --- test/utils/search.spec.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/test/utils/search.spec.ts b/test/utils/search.spec.ts index 72ff28c0..9e7c1738 100644 --- a/test/utils/search.spec.ts +++ b/test/utils/search.spec.ts @@ -354,14 +354,17 @@ describe('highlight', () => { // that anything was lost. const CLUSTERS: [string, string][] = [ ['a flag', '\u{1F1FA}\u{1F1F8}'], - ['a ZWJ family', '\u{1F468}‍\u{1F469}‍\u{1F467}'], + ['a ZWJ family', '\u{1F468}\u200D\u{1F469}\u200D\u{1F467}'], ['an emoji with a skin-tone modifier', '\u{1F44D}\u{1F3FF}'], ['a Devanagari consonant with a vowel sign', 'कि'], - // Escaped rather than written as a glyph: a literal combining mark is - // exactly what NFC collapses into one precomposed code point, and an IDE - // reformat doing that silently turns this into a single-code-point fixture - // that passes whatever `CLUSTER_CONTINUATION_FLOOR` is set to. Every other - // multi-code-point fixture here already uses escapes. + // Escaped rather than written as a glyph. Two hazards, and only the + // first applies here: NFC collapses a literal combining mark into one + // precomposed code point, so an IDE reformat would silently turn this + // into a single-code-point fixture that passes whatever the floor is set + // to. The ZWJ family above is escaped for the second reason instead — + // its joiners are invisible, so one could be deleted without anyone + // seeing. The Devanagari pair needs neither: it is visible, and + // `'कि'.normalize('NFC')` round-trips unchanged. ['a combining accent', 'e\u0301'], // U+0300 sits *at* the floor, U+0301 one above it, and only the former // pins it: `0x301 < 0x300` and `0x301 < 0x301` are both false, so raising