From df80ec2e1052a655d295e513c0d348a569fc6934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Ronzon?= Date: Fri, 4 Sep 2026 12:25:34 -0600 Subject: [PATCH 1/2] test(search): stop generating lone surrogates in authorsSort names The astral-plane samples in the non-Latin name arbitrary were split with `.split('')`, which splits on UTF-16 code units: U+2000B and U+2A600 each became two standalone surrogate halves, so generated names could hold lone surrogates. Those have no UTF-8 encoding and all serialize to U+FFFD, which ties names that compareByCodePoint ranks as distinct and made Property 4 (minimality under UTF-8 byte order) fail on ~9% of seeds. Spread the string instead so it iterates code points, keeping both astral characters as whole units. Property 4 now holds on 300/300 seeds. --- .../2_utils/computeDocumentAuthorsSort.property.test.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/integration/2_utils/computeDocumentAuthorsSort.property.test.js b/test/integration/2_utils/computeDocumentAuthorsSort.property.test.js index 7e42591f0..cecee21b5 100644 --- a/test/integration/2_utils/computeDocumentAuthorsSort.property.test.js +++ b/test/integration/2_utils/computeDocumentAuthorsSort.property.test.js @@ -24,8 +24,14 @@ const nameArb = fc.oneof( maxLength: 15, }), fc.string({ + // Iterate by code point, not by `.split('')`: the astral-plane samples + // below (U+2000B, U+2A600) are two UTF-16 code units each, so splitting by + // code unit would offer their surrogate halves as standalone units and + // generate names holding lone surrogates. Those have no UTF-8 encoding — + // every one of them serializes to U+FFFD — which collapses names this + // helper orders as distinct and makes Property 4 fail on ~9% of seeds. unit: fc.constantFrom( - ...'ЯрославльΩμέγα山田太郎홍길동محمدמשהสมชาย𠀋𪘀'.split(''), + ...'ЯрославльΩμέγα山田太郎홍길동محمدמשהสมชาย𠀋𪘀', '\u{10FFFD}', '�', '~' From c0cb9708cfd24c64a6292c377ec0486435004381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Ronzon?= Date: Fri, 4 Sep 2026 12:46:38 -0600 Subject: [PATCH 2/2] test(search): derive authorsSort survival from the helper, not trim() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nonEmptyNamesArb gated on `n.trim().length > 0`, but normalizeName strips combining marks before trimming, so a name made purely of diacritics is non-blank to trim yet normalizes to nothing. U+1FDD decomposes under NFKD to a space plus two combining marks and leaves nothing behind, so the filter handed it to the properties as "authored" while the helper correctly returned the authorless key — failing Property 2 on ~0.25% of seeds. Ask computeDocumentAuthorsSort itself whether a list survives instead of re-implementing its normalization in the precondition. The behaviour under test is unchanged and the filter still keeps ~63% of generated lists, spanning every script the arbitrary covers. --- .../computeDocumentAuthorsSort.property.test.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/test/integration/2_utils/computeDocumentAuthorsSort.property.test.js b/test/integration/2_utils/computeDocumentAuthorsSort.property.test.js index cecee21b5..839cd973f 100644 --- a/test/integration/2_utils/computeDocumentAuthorsSort.property.test.js +++ b/test/integration/2_utils/computeDocumentAuthorsSort.property.test.js @@ -47,9 +47,18 @@ const namesArb = fc.array(fc.oneof(nameArb, fc.constantFrom(null, undefined)), { maxLength: 5, }); -/** Arbitrary: a list holding at least one name that survives normalization. */ -const nonEmptyNamesArb = namesArb.filter((names) => - names.some((n) => typeof n === 'string' && n.trim().length > 0) +/** + * Arbitrary: a list holding at least one name that survives normalization. + * + * Survival is decided by asking the helper itself, not by `.trim()`: `trim` + * only strips whitespace, while normalizeName strips combining marks first, so + * a name made purely of diacritics is non-blank to `trim` yet normalizes away. + * U+1FDD, say, decomposes under NFKD to a space plus two combining marks and + * leaves nothing behind — a `trim`-based precondition would hand such a name to + * the properties below as "authored" and they would rightly disagree. + */ +const nonEmptyNamesArb = namesArb.filter( + (names) => computeDocumentAuthorsSort(names) !== EMPTY_AUTHORS_SORT_KEY ); /** Strip the ordering bucket prefix to recover the normalized name. */