Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_naming.mjs

6.9 KiB, 1 run

created by r2519314175:545, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1// verify_naming.mjs — two Diamonds must never be offered the same name.
2//
3// THIS FILE USED TO BE ABOUT CHATS, and the change is worth recording rather
4// than losing in a diff. The bug the user reported was: with Chat-0001 and
5// Chat-0002 in the rail, the next new chat was called Chat-0002.
6//
7// The cause is a shape worth remembering, because this codebase has had it more
8// than once: ONE FACT STORED IN TWO PLACES THAT DO NOT TRAVEL TOGETHER. The
9// counter lives in localStorage, which is per device. The things it names live
10// in the synced store, shared across every device on the account. So one made on
11// the other machine arrives here having advanced ITS counter and not this one,
12// and the next one made here is handed a number already in use. A backup import
13// opens the same gap: it restores the records, not the counter.
14//
15// CHATS ARE NO LONGER NUMBERED AT ALL, which retires half of that. A chat is
16// throw-away — get in, get out, and spend the time curating Diamonds — and an
17// accession number is what a museum gives a thing it is keeping, so the number
18// was the interface arguing the opposite of what the app is for. It was also
19// never true: the counter was per device while the chats were shared, so the
20// numbers implied a chronology they did not have. A chat is now identified by
21// WHEN it was last touched, which is derived, needs no counter, and cannot
22// collide. `dev/verify_chatlife.mjs` holds that ground.
23//
24// A DIAMOND IS STILL NUMBERED, and should be: it is the thing being kept, it is
25// named on purpose at the moment it is made, and the number is only what the
26// dialog opens on. Which means the collision above is still live for Diamonds
27// and still needs guarding — so this file follows it there rather than being
28// deleted along with its old subject. The fix under test is the same one:
29// `nextDiamondNumber` treats the counter as a FLOOR and lets whatever number is
30// actually in use win over it.
31//
32// It drives the real path: it plants the higher-numbered Diamond the way sync
33// would (straight into the store, counter untouched), reloads, and asks what the
34// New Diamond dialog would offer.
35import { open, signInAs } from './harness.mjs';
36
37const ok = [], bad = [];
38const check = (name, pass, detail) => {
39 (pass ? ok : bad).push(name);
40 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
41 return pass;
42};
43
44// NOT a fixed profile, and no default Diamonds: this asserts what the FIRST
45// Diamond would be offered, so it needs a rail with nothing in it. On a fixed
46// profile the second run inherits the first run's Diamonds and counter and goes
47// red for a reason that has nothing to do with the product. That trap has bitten
48// verify_durability and verify_mailfolders already.
49const s = await open({ name: 'naming', defaults: false });
50const p = s.page;
51await p.waitForTimeout(1500);
52
53/// Every Diamond name in the rail, in rail order.
54const names = () => p.evaluate(() =>
55 [...document.querySelectorAll('#diamond-list .diamond-box')]
56 .map((e) => (e.querySelector('.session-box-name') || {}).textContent || '')
57 .map((t) => t.trim()));
58
59/// What the New Diamond dialog would put in its name field.
60const offered = () => p.evaluate(() => window.DaimondCore.nextDiamondLabel());
61
62/// Make one Diamond, straight through the engine, with the counter advanced the
63/// way the dialog advances it. Not through the dialog itself: this file is about
64/// the NUMBER, and driving three modals to get at it would make a naming test
65/// into a dialog test that fails for unrelated reasons.
66const makeDiamond = (name) => p.evaluate(async (n) => {
67 const id = await window.DaimondCore.diamondApp().create_diamond(n);
68 window.DaimondCore.takeDiamondLabel();
69 return id;
70}, name);
71
72try {
73 check('a fresh rail offers the first number', (await offered()) === 'Diamond-0001',
74 await offered());
75
76 await makeDiamond('Diamond-0001');
77 await p.evaluate(() => window.DaimondCore.loadDiamonds());
78 await p.waitForTimeout(600);
79 check('the first Diamond is in the rail', (await names()).includes('Diamond-0001'),
80 JSON.stringify(await names()));
81
82 const counterAfterFirst = await p.evaluate(() =>
83 localStorage.getItem('daimond-diamond-counter'));
84 check('the counter is at 1', counterAfterFirst === '1', String(counterAfterFirst));
85
86 // ── The arrival from the other device ───────────────────────────
87 //
88 // Created WITHOUT touching the counter, which is exactly what a sync pull
89 // does: the parcel carries the Diamonds and the counter is not in it.
90 await p.evaluate(async () => {
91 await window.DaimondCore.diamondApp().create_diamond('Diamond-0002');
92 });
93 await p.reload();
94 // A reload lands on the lock gate: boot stops there and nothing behind it
95 // runs, so a test that reloads and reads the rail reads an empty one. Sign in
96 // the way a person would.
97 await p.waitForTimeout(800);
98 if (await p.$('#id-primary')) await signInAs(s, 'naming');
99 await p.waitForTimeout(2000);
100
101 const both = await names();
102 check('both Diamonds are in the rail',
103 both.includes('Diamond-0001') && both.includes('Diamond-0002'), JSON.stringify(both));
104 const counterStill = await p.evaluate(() => localStorage.getItem('daimond-diamond-counter'));
105 check('and the counter did NOT move when the second one arrived', counterStill === '1',
106 String(counterStill));
107
108 // ── The next one must not collide ───────────────────────────────
109 const next = await offered();
110 check('the next Diamond is not offered a name already in use',
111 !both.includes(next), `offered ${next}, rail ${JSON.stringify(both)}`);
112 check('it is offered the next free number', next === 'Diamond-0003', next);
113
114 // ── Proved red ──────────────────────────────────────────────────
115 //
116 // The old behaviour, reconstructed: take the counter as the answer and ignore
117 // what is in use. It must produce the collision the user reported, or this
118 // file is testing something that was never broken.
119 console.log('');
120 const wouldCollide = await p.evaluate(() => {
121 const used = [...document.querySelectorAll('#diamond-list .diamond-box')]
122 .map((e) => ((e.querySelector('.session-box-name') || {}).textContent || '').trim());
123 // The counter as it stands, incremented — which is what the old rule did.
124 const n = parseInt(localStorage.getItem('daimond-diamond-counter') || '0', 10) || 0;
125 const oldWay = 'Diamond-' + ('000' + (n + 1)).slice(-4);
126 return { oldWay, collides: used.includes(oldWay) };
127 });
128 check(`self-test: counter-only naming would have offered ${wouldCollide.oldWay}, `
129 + 'which is already in the rail', wouldCollide.collides === true,
130 'the fixture never reached the state under test, so the checks above prove nothing');
131} finally {
132 await s.close();
133}
134
135console.log(`\n${ok.length} ok, ${bad.length} failed`);
136if (bad.length) { console.log('failed: ' + bad.join('; ')); process.exit(1); }
137process.exit(0);