#!/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('')); // --- a wider label --------------------------------------------------------- // More initials, not a longer prefix: the whole point of initials is keeping // "dev-skills" and "dev-conventions" apart, and a prefix at any width does not. check('a wider label takes more initials', 'ccge', abbreviate('claude-code-gnome-extension', 4)); check('and stops at the segments it has', 'ds', abbreviate('dev-skills', 6)); check('a single word gets more of itself', 'jellyb', abbreviate('jellybit', 6)); check('the default is still three', 'ccg', abbreviate('claude-code-gnome-extension')); check('a nonsense width falls back to the default', 'ccg', abbreviate('claude-code-gnome-extension', 'wide')); // --- 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); // A digit still eats into the label instead of extending past the width, which // is what keeps a row of chips from rippling when one of them gains a digit. const wider = assignChips(many, new Map(), 5); check('a wider run holds its own width', [5], [...new Set([...wider.values()].map(l => l.length))]); check('and stays distinct', 12, new Set(wider.values()).size); check('the oldest keeps the clean label', 'umbar', wider.get('s0')); check('the next one gives up a character', 'umba2', wider.get('s1')); // Stickiness outranks the width: labels already handed out are kept as they // are. The indicator drops the map when the setting changes, which is the only // way a label is allowed to move. const kept = assignChips(many, wide, 5); check('an existing label is not re-cut', 'umb', kept.get('s0')); out(failures ? `\n${failures} failure(s)` : '\nall passed'); if (typeof imports !== 'undefined') imports.system.exit(failures ? 1 : 0); else if (failures) process.exit(1);