#!/usr/bin/gjs -m // Chip labelling: shortening, collisions, and stickiness. // // Run: gjs -m tests/test-abbrev.js (or: node tests/test-abbrev.js) import { abbreviate, assignChips } from '../lib/abbrev.js'; let failures = 0; const out = typeof print === 'function' ? print : console.log; function check(name, expected, actual) { const ok = JSON.stringify(expected) === JSON.stringify(actual); if (!ok) failures++; out(`${ok ? 'ok ' : 'FAIL'} ${name}${ok ? '' : ` (expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)})`}`); } // --- shortening ------------------------------------------------------------ check('initials for multi-segment names', 'ds', abbreviate('dev-skills')); check('a plain prefix would have collided here', 'dc', abbreviate('dev-conventions')); check('three segments', 'pps', abbreviate('pet-project-server')); check('four segments truncate to three', 'ccg', abbreviate('claude-code-gnome-extension')); check('single word takes first letters', 'jel', abbreviate('jellybit')); check('short single word stays short', 'um', abbreviate('um')); check('camelCase counts as segments', 'om', abbreviate('outlineMcp')); check('digits survive', 'p2', abbreviate('proj_2')); check('empty name does not crash', '?', abbreviate('')); // --- collisions ------------------------------------------------------------ const two = [ { sessionId: 'a', since: 100, base: 'dev-skills' }, { sessionId: 'b', since: 200, base: 'dev-skills' }, ]; let map = assignChips(two); check('oldest keeps the clean label', 'ds', map.get('a')); check('newer takes the digit', 'ds2', map.get('b')); const three = [...two, { sessionId: 'c', since: 300, base: 'dev-skills' }]; map = assignChips(three, map); check('third in the same project', 'ds3', map.get('c')); check('adding one does not disturb the first', 'ds', map.get('a')); check('adding one does not disturb the second', 'ds2', map.get('b')); // Order of the input array must not matter -- only start time does. const shuffled = [three[2], three[0], three[1]]; const fromScratch = assignChips(shuffled); check('assignment follows start time, not array order', ['ds', 'ds2', 'ds3'], [fromScratch.get('a'), fromScratch.get('b'), fromScratch.get('c')]); // --- stickiness ------------------------------------------------------------ // The point of the whole module: closing the session that forced the digit // must not renumber the one still on screen. const afterClose = assignChips([three[1], three[2]], map); check('surviving session keeps its digit', 'ds2', afterClose.get('b')); check('and so does the one after it', 'ds3', afterClose.get('c')); // A label freed by a closed session may be handed to a genuinely new one. const reused = assignChips( [three[1], { sessionId: 'd', since: 400, base: 'dev-skills' }], afterClose); check('freed label is reused', 'ds', reused.get('d')); check('existing session still untouched', 'ds2', reused.get('b')); // --- widths ---------------------------------------------------------------- const many = Array.from({ length: 12 }, (_, i) => ({ sessionId: `s${i}`, since: i, base: 'umbar' })); const wide = assignChips(many); const lengths = [...new Set([...wide.values()].map(l => l.length))]; check('every label fits in three characters', [3], lengths); check('twelve sessions in one project are all distinct', 12, new Set(wide.values()).size); out(failures ? `\n${failures} failure(s)` : '\nall passed'); if (typeof imports !== 'undefined') imports.system.exit(failures ? 1 : 0); else if (failures) process.exit(1);