From ece663d1dfee6d61237cceb82a6f19653de0c687 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Tue, 9 Jun 2026 14:19:27 -0400 Subject: [PATCH] test(04-07): add failing collation regression test (LIST-03) - Seed items with ranks 'a0' and 'a1', drag second to top via rank 'Zz' - Assert 'Zz' < 'a0' is true in JS (documents uppercase-before-lowercase intent) - GET /api/lists/:id/items must return Zz-ranked item at index 0 - Fails now because MariaDB utf8mb4_uca1400_ai_ci sorts 'Zz' after 'a0' - Will pass once rank column gets COLLATE utf8mb4_bin via migration --- apps/api/tests/routes/lists.test.ts | 34 +++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/apps/api/tests/routes/lists.test.ts b/apps/api/tests/routes/lists.test.ts index 92b308f..17c1097 100644 --- a/apps/api/tests/routes/lists.test.ts +++ b/apps/api/tests/routes/lists.test.ts @@ -1037,4 +1037,38 @@ describe('PATCH /api/list-items/:id { position } — reorder ordering (LIST-03, const res = await app.request(jsonRequest('PATCH', `/api/list-items/${itemId}`, { position: 'a1', checked: true })) expect(res.status).toBe(400) }) + + it('drag-to-top: uppercase-prefixed rank sorts above lowercase ranks (LIST-03 collation regression)', async () => { + const ownerId = await seedUser('reorder-collation') + currentDevUserId = ownerId + const listId = await seedList(ownerId, 'Collation Test', false) + + // Seed two active items: item1 has lowercase rank 'a0', item2 has rank 'a1' + const id1 = await seedItem(listId, 'first', 'a0') + const id2 = await seedItem(listId, 'second', 'a1') + + // JS string comparison: 'Zz' < 'a0' is true (uppercase sorts before lowercase in JS/Unicode) + // This is what fractional-indexing produces for generateKeyBetween(null, 'a0') — a drag-to-top + expect('Zz' < 'a0').toBe(true) + + // Drag item2 to top by assigning it the uppercase-prefixed rank 'Zz' + const app = await getApp() + const patchRes = await app.request(jsonRequest('PATCH', `/api/list-items/${id2}`, { position: 'Zz' })) + expect(patchRes.status).toBe(200) + const patchBody = await patchRes.json() as { rank: string } + expect(patchBody.rank).toBe('Zz') + + // GET /api/lists/:listId/items — the dragged item (rank 'Zz') MUST be returned first (index 0) + // This exercises the real DB ORDER BY rank. Without utf8mb4_bin collation, MariaDB's + // case-insensitive default collation sorts 'Zz' AFTER 'a0', placing the item last. + const getRes = await app.request(`/api/lists/${listId}/items`) + expect(getRes.status).toBe(200) + const getBody = await getRes.json() as { items: Array<{ id: number; rank: string }> } + expect(getBody.items).toHaveLength(2) + // item2 (rank 'Zz') must be first — drag-to-top persists + expect(getBody.items[0].id).toBe(id2) + expect(getBody.items[0].rank).toBe('Zz') + // item1 (rank 'a0') must be second + expect(getBody.items[1].id).toBe(id1) + }) })