Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_tagkill.mjs

8.1 KiB, 1 run

created by r2519314175:723, 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_tagkill.mjs — a tag can be got rid of, and says who it will affect.
2//
3// The tag editor's upper box takes a tag OFF this Diamond. Nothing anywhere could
4// delete the tag itself, so a mistyped or abandoned tag stayed in the pool for
5// ever, offered to every Diamond, for the rest of the account's life.
6//
7// The pool box can now delete one — which is a different act from detaching it,
8// because the pool is the USER'S vocabulary and other Diamonds are filed under
9// it. So it asks first, and it says how many Diamonds it will change. The oracle
10// for the removal is the store, read back through the wasm, not the chips on
11// screen: a paint that merely stopped drawing the tag would look identical.
12//
13// Kept separate from verify_tags rather than bolted onto it: that file's 49
14// checks include the exact sequence of the starter suggestions and the exact
15// textContent of a pool chip, and this feature must not disturb either. The last
16// check here asserts both, so the two files cannot drift apart silently.
17//
18// node dev/verify_tagkill.mjs
19//
20// Needs dev/serve.mjs (DAIMOND_PORT, default 8777). No gateway; the model is never
21// called.
22
23import { open, shot, errors } from './harness.mjs';
24
25const ok = [], bad = [];
26const check = (name, pass, detail) => {
27 (pass ? ok : bad).push(name + (detail ? ' — ' + detail : ''));
28 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
29};
30const sleep = ms => new Promise(r => setTimeout(r, ms));
31
32// The mock model is connected but never called: nothing here reaches a provider.
33// It is connected because the rail's "+" refuses to create a Diamond without one.
34const s = await open({ name: 'tagkill', defaults: false });
35const p = s.page;
36
37/// The store's own answer: every Diamond and the tags actually written down.
38const stored = () => p.evaluate(async () => {
39 const m = await import('/pkg/oxedyne_daimond.js');
40 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
41 return JSON.parse(await app.list_diamonds()).map(r => ({ name: r.name, tags: r.tags }));
42});
43const pool = () => p.$$eval('.tag-sug .tag-chip', els => els.map(e => ({
44 text: e.textContent, kill: !!e.querySelector('.tag-kill'),
45})));
46const dlg = () => p.evaluate(() => {
47 const c = document.querySelector('.dlg-card');
48 return c ? c.textContent : '';
49});
50async function clickIf(sel) {
51 const el = await p.$(sel);
52 if (!el) return false;
53 await el.click({ force: true });
54 await sleep(500);
55 return true;
56}
57/// The pool closer for one named tag.
58async function killChip(tag) {
59 return p.evaluateHandle((t) => {
60 const chip = [...document.querySelectorAll('.tag-sug .tag-chip')]
61 .find(e => e.textContent === t);
62 return chip ? chip.querySelector('.tag-kill') : null;
63 }, tag);
64}
65async function clickKill(tag) {
66 const h = await killChip(tag);
67 const el = h.asElement();
68 if (!el) return false;
69 await el.click({ force: true });
70 await sleep(600);
71 return true;
72}
73
74// ── Seed: three Diamonds, two of them sharing a tag ──────────────────
75async function newDiamond(name) {
76 await p.click('#new-diamond-btn', { force: true });
77 await p.waitForSelector('.dlg-input', { timeout: 10000 });
78 await p.fill('.dlg-input', name);
79 await p.click('.dlg-ok', { force: true });
80 await sleep(800);
81}
82for (const n of ['Alpha', 'Beta', 'Gamma']) await newDiamond(n);
83
84async function openEditor(name) {
85 await p.evaluate((n) => {
86 const box = [...document.querySelectorAll('.diamond-box')]
87 .find(b => (b.querySelector('.session-box-name') || {}).textContent === n);
88 if (box) box.click();
89 }, name);
90 await sleep(700);
91 for (const b of await p.$$('.crystal-act')) {
92 if ((await b.textContent()).includes('Tags')) { await b.click({ force: true }); break; }
93 }
94 await p.waitForSelector('.tag-editor', { timeout: 8000 });
95}
96async function addTags(name, tags) {
97 await openEditor(name);
98 for (const t of tags) {
99 await p.fill('.tag-input', t);
100 await p.keyboard.press('Enter');
101 await sleep(500);
102 }
103}
104await addTags('Alpha', ['shared', 'alpha-only']);
105await addTags('Beta', ['shared']);
106// Gamma carries neither, so both are in ITS pool — which is where a tag is
107// deleted from.
108await openEditor('Gamma');
109
110// ── 1. The pool offers a closer, except on the starter suggestions ───
111const pool1 = await pool();
112const shared = pool1.find(x => x.text === 'shared');
113const starter = pool1.find(x => x.text === 'person');
114check('a tag in the pool carries a closer', !!shared && shared.kill,
115 JSON.stringify(pool1));
116check('a starter suggestion does not (nothing could remove it)',
117 !!starter && starter.kill === false, JSON.stringify(starter));
118check('and a pool chip still reads as the bare tag, which is what the rail and the search read',
119 !!shared && shared.text === 'shared', JSON.stringify(pool1.map(x => x.text)));
120await shot(s, 'tagkill-pool');
121
122// ── 2. The closer asks first, and says who it affects ────────────────
123check('clicking the closer opens the app\'s own confirmation', await clickKill('shared'));
124const ask = await dlg();
125check('the dialog names the tag and counts the Diamonds it is filed on',
126 /shared/.test(ask) && /\b2\b/.test(ask), ask ? ask.slice(0, 140) : '(no dialog)');
127await shot(s, 'tagkill-confirm');
128
129// ── 3. Cancelling changes nothing at all ─────────────────────────────
130await clickIf('.dlg-cancel');
131const afterCancel = await stored();
132check('cancelling leaves every Diamond exactly as it was',
133 afterCancel.filter(d => (d.tags || []).includes('shared')).length === 2,
134 JSON.stringify(afterCancel));
135
136// ── 4. Confirming takes it off every Diamond that carried it ─────────
137await clickKill('shared');
138await clickIf('.dlg-ok');
139await sleep(1200);
140const afterKill = await stored();
141check('confirming deletes the tag from every Diamond, in the STORE',
142 afterKill.every(d => !(d.tags || []).includes('shared')), JSON.stringify(afterKill));
143check('and leaves their other tags alone',
144 (afterKill.find(d => d.name === 'Alpha') || {}).tags.includes('alpha-only'),
145 JSON.stringify(afterKill.find(d => d.name === 'Alpha')));
146const pool2 = await pool();
147check('the deleted tag is gone from the pool',
148 !pool2.some(x => x.text === 'shared'), JSON.stringify(pool2.map(x => x.text)));
149const railChips = await p.$$eval('.session-box-tags .tag-chip', els => els.map(e => e.textContent));
150check('and gone from the rail',
151 !railChips.includes('shared'), JSON.stringify(railChips));
152
153// ── 5. A filter on a deleted tag is cleared, not left hiding everything ──
154await p.evaluate(() => {
155 const chip = [...document.querySelectorAll('.session-box-tags .tag-chip')]
156 .find(e => e.textContent === 'alpha-only');
157 if (chip) chip.click();
158});
159await sleep(500);
160const filtered = await p.$$eval('.diamond-box', els => els.length);
161check('a tag filter narrows the rail (so the next check means something)',
162 filtered === 1, `${filtered} Diamonds shown`);
163await clickKill('alpha-only');
164await clickIf('.dlg-ok');
165await sleep(1200);
166const shown = await p.$$eval('.diamond-box', els => els.length);
167const filterUp = await p.evaluate(() => {
168 const f = document.getElementById('diamond-filter');
169 return !!f && f.style.display !== 'none' && getComputedStyle(f).display !== 'none';
170});
171check('deleting the tag that was filtering clears the filter rather than emptying the rail',
172 shown === 3 && !filterUp, `${shown} shown, filter chip up=${filterUp}`);
173
174// ── 6. The starter set is untouched, in its own order ────────────────
175const starters = (await pool()).map(x => x.text).slice(0, 4);
176check('the starter suggestions keep their order (verify_tags asserts this exact sequence)',
177 JSON.stringify(starters) === JSON.stringify(['person', 'project', 'topic', 'org']),
178 JSON.stringify(starters));
179
180const errs = errors(s).filter(e => !/favicon|404|401|402|502|Bad Gateway|net::ERR/.test(e));
181console.log('\nconsole errors:', errs.slice(0, 5));
182check('nothing throws while all this happens', errs.length === 0, errs[0] || '');
183
184await s.close();
185console.log(`\n${ok.length} passed, ${bad.length} failed`);
186if (bad.length) console.log('FAILED:\n ' + bad.join('\n '));
187process.exit(bad.length ? 1 : 0);