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 | |
| 23 | import { open, shot, errors } from './harness.mjs'; |
| 24 | |
| 25 | const ok = [], bad = []; |
| 26 | const check = (name, pass, detail) => { |
| 27 | (pass ? ok : bad).push(name + (detail ? ' — ' + detail : '')); |
| 28 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 29 | }; |
| 30 | const 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. |
| 34 | const s = await open({ name: 'tagkill', defaults: false }); |
| 35 | const p = s.page; |
| 36 | |
| 37 | /// The store's own answer: every Diamond and the tags actually written down. |
| 38 | const 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 | }); |
| 43 | const pool = () => p.$$eval('.tag-sug .tag-chip', els => els.map(e => ({ |
| 44 | text: e.textContent, kill: !!e.querySelector('.tag-kill'), |
| 45 | }))); |
| 46 | const dlg = () => p.evaluate(() => { |
| 47 | const c = document.querySelector('.dlg-card'); |
| 48 | return c ? c.textContent : ''; |
| 49 | }); |
| 50 | async 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. |
| 58 | async 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 | } |
| 65 | async 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 ────────────────── |
| 75 | async 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 | } |
| 82 | for (const n of ['Alpha', 'Beta', 'Gamma']) await newDiamond(n); |
| 83 | |
| 84 | async 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 | } |
| 96 | async 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 | } |
| 104 | await addTags('Alpha', ['shared', 'alpha-only']); |
| 105 | await addTags('Beta', ['shared']); |
| 106 | // Gamma carries neither, so both are in ITS pool — which is where a tag is |
| 107 | // deleted from. |
| 108 | await openEditor('Gamma'); |
| 109 | |
| 110 | // ── 1. The pool offers a closer, except on the starter suggestions ─── |
| 111 | const pool1 = await pool(); |
| 112 | const shared = pool1.find(x => x.text === 'shared'); |
| 113 | const starter = pool1.find(x => x.text === 'person'); |
| 114 | check('a tag in the pool carries a closer', !!shared && shared.kill, |
| 115 | JSON.stringify(pool1)); |
| 116 | check('a starter suggestion does not (nothing could remove it)', |
| 117 | !!starter && starter.kill === false, JSON.stringify(starter)); |
| 118 | check('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))); |
| 120 | await shot(s, 'tagkill-pool'); |
| 121 | |
| 122 | // ── 2. The closer asks first, and says who it affects ──────────────── |
| 123 | check('clicking the closer opens the app\'s own confirmation', await clickKill('shared')); |
| 124 | const ask = await dlg(); |
| 125 | check('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)'); |
| 127 | await shot(s, 'tagkill-confirm'); |
| 128 | |
| 129 | // ── 3. Cancelling changes nothing at all ───────────────────────────── |
| 130 | await clickIf('.dlg-cancel'); |
| 131 | const afterCancel = await stored(); |
| 132 | check('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 ───────── |
| 137 | await clickKill('shared'); |
| 138 | await clickIf('.dlg-ok'); |
| 139 | await sleep(1200); |
| 140 | const afterKill = await stored(); |
| 141 | check('confirming deletes the tag from every Diamond, in the STORE', |
| 142 | afterKill.every(d => !(d.tags || []).includes('shared')), JSON.stringify(afterKill)); |
| 143 | check('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'))); |
| 146 | const pool2 = await pool(); |
| 147 | check('the deleted tag is gone from the pool', |
| 148 | !pool2.some(x => x.text === 'shared'), JSON.stringify(pool2.map(x => x.text))); |
| 149 | const railChips = await p.$$eval('.session-box-tags .tag-chip', els => els.map(e => e.textContent)); |
| 150 | check('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 ── |
| 154 | await 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 | }); |
| 159 | await sleep(500); |
| 160 | const filtered = await p.$$eval('.diamond-box', els => els.length); |
| 161 | check('a tag filter narrows the rail (so the next check means something)', |
| 162 | filtered === 1, `${filtered} Diamonds shown`); |
| 163 | await clickKill('alpha-only'); |
| 164 | await clickIf('.dlg-ok'); |
| 165 | await sleep(1200); |
| 166 | const shown = await p.$$eval('.diamond-box', els => els.length); |
| 167 | const filterUp = await p.evaluate(() => { |
| 168 | const f = document.getElementById('diamond-filter'); |
| 169 | return !!f && f.style.display !== 'none' && getComputedStyle(f).display !== 'none'; |
| 170 | }); |
| 171 | check('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 ──────────────── |
| 175 | const starters = (await pool()).map(x => x.text).slice(0, 4); |
| 176 | check('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 | |
| 180 | const errs = errors(s).filter(e => !/favicon|404|401|402|502|Bad Gateway|net::ERR/.test(e)); |
| 181 | console.log('\nconsole errors:', errs.slice(0, 5)); |
| 182 | check('nothing throws while all this happens', errs.length === 0, errs[0] || ''); |
| 183 | |
| 184 | await s.close(); |
| 185 | console.log(`\n${ok.length} passed, ${bad.length} failed`); |
| 186 | if (bad.length) console.log('FAILED:\n ' + bad.join('\n ')); |
| 187 | process.exit(bad.length ? 1 : 0); |