oxedyne/daimond/dev/verify_egressconvo.mjs
22.0 KiB, 1 run
created by r2519314175:395, 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_egressconvo.mjs — one ask per conversation, and it covers every website. |
| 2 | // |
| 3 | // THE DEFECT, reported by a tester on 2026-08-26 and ruled on by the owner the |
| 4 | // next day. The tester wrote: *"Permission sought for every new web_fetch, yet |
| 5 | // the 'THE NETWORK' in the Permission mode dialog says 'Always allow', so I'm |
| 6 | // confused."* A lane read that as a LABEL fault — the setting governs whether a |
| 7 | // command keeps its network after the turn has read a stranger's words, and a |
| 8 | // page fetch goes through a different door — renamed the head to "Commands and |
| 9 | // the network", and shipped a line saying what it is not. That reading of the |
| 10 | // two mechanisms was right and is still right. |
| 11 | // |
| 12 | // THE OWNER THEN RULED THAT THE BEHAVIOUR WAS ALSO THE BUG: *"I should only be |
| 13 | // asked once in a session (i.e. new ordinary chat or fresh daimon) for |
| 14 | // permission to access ANY (not a specific website) ... At the moment, every |
| 15 | // website is triggering a new permission request for that url."* |
| 16 | // |
| 17 | // WHAT IT ACTUALLY DID. `_egressOk` in www/js/daimond.js was an object keyed by |
| 18 | // HOST, living as long as the page. So the scope was wrong twice over and in |
| 19 | // opposite directions: every new site asked again, and a yes given in one chat |
| 20 | // silently answered for the next chat, and for every daimon in the tab. The |
| 21 | // answer now lives on the engine's `TurnState`, where a conversation is a thing |
| 22 | // that exists, and `web_step` in src/tools.rs decides from it. |
| 23 | // |
| 24 | // NINE PROPERTIES. The first four are the owner's sentence, in order. |
| 25 | // |
| 26 | // 1. A CLEAN TURN IS NEVER ASKED. Held first, because every check after it is |
| 27 | // only meaningful about a turn where the question is genuinely live — and |
| 28 | // because a gate that asked on every fetch would pass 2 and 3 by asking |
| 29 | // nobody anything. |
| 30 | // 2. TWO SITES, ONE ASK. The reported defect exactly: two `web_fetch` calls to |
| 31 | // two different hosts in one turn used to be two dialogs. |
| 32 | // 3. AND WHAT THE ASK SAYS. Half the deliverable, and the half a count cannot |
| 33 | // see. The user is agreeing that this conversation may reach ANY website, |
| 34 | // which is materially bigger than "may reach example.com" — so the dialog |
| 35 | // has to say so. A wide grant collected in the words of a narrow one is the |
| 36 | // failure the label change was fixing, arriving from the other direction. |
| 37 | // 4. A LATER TURN IN THE SAME CHAT IS NOT ASKED. Separate from 2 because the |
| 38 | // grant surviving one turn and the grant surviving the conversation are |
| 39 | // different claims, and the old code satisfied neither. |
| 40 | // 5. A NEW CHAT ASKS AGAIN. The narrowing half of the ruling, and the one a |
| 41 | // wider fix would have quietly lost: `_egressOk` outlived every chat. |
| 42 | // 6. A DAIMON IS ITS OWN CONVERSATION. The chat's yes does not reach it. This |
| 43 | // is the sharp one, because a Diamond's client is CACHED BY PROVIDER AND |
| 44 | // MODEL (`diamondApp`), so the daimon shares one engine cache with every |
| 45 | // other Diamond on that model; an answer held in a plain `Option` there |
| 46 | // would be every Diamond's answer. Held in Rust as well, by |
| 47 | // `test_one_diamonds_yes_is_not_another_diamonds_on_a_shared_client`. |
| 48 | // 7. AND A LONG ADDRESS IS STILL ASKED ABOUT. The limit on the grant, and the |
| 49 | // reason the grant is safe to give: approving the web is not approving |
| 50 | // `somewhere.test/?everything-I-know=…`, which is the exfiltration this |
| 51 | // whole gate exists to catch. Measured AFTER the conversation has granted |
| 52 | // the web, which is the only state in which it can fail. |
| 53 | // 9. AND SO IS THE DIAMOND NEXT TO IT. Added 2026-08-28. Check 6 says a chat's |
| 54 | // yes does not answer for a daimon; this says a Diamond's READING does not |
| 55 | // cross to the Diamond beside it on the same client. `TurnState::tainted` was |
| 56 | // a bare `bool` on a cache every Diamond on one model shares, so a Research |
| 57 | // Diamond that fetched a page cut an Accounts Diamond's network and stamped |
| 58 | // its dispatched workers as carrying a stranger's words. The sharing is |
| 59 | // asserted before the property is, which is the lesson the Rust test next door |
| 60 | // taught by not doing it. |
| 61 | // 8. AND OUR OWN PAGES GRANT NOTHING. The same-origin shortcut answers with |
| 62 | // nobody asked, and the word it answers in now decides whether a standing |
| 63 | // grant is written. A fetch of Daimond's own address — which the model can |
| 64 | // write for itself — must not hand over every site in silence. Held as a |
| 65 | // pair: the shortcut still passing is not the property, the next site being |
| 66 | // asked about is. |
| 67 | // |
| 68 | // PROVED AGAINST BROKEN CODE FIRST, each break chosen to survive every check but |
| 69 | // the ones under test: |
| 70 | // |
| 71 | // node dev/verify_egressconvo.mjs --break perhost # 2, 4, 6: ask about every site again |
| 72 | // node dev/verify_egressconvo.mjs --break narrowask # 3: the wide grant, narrow words |
| 73 | // node dev/verify_egressconvo.mjs --break heavyfree # 7: a long address rides the grant |
| 74 | // node dev/verify_egressconvo.mjs --break sameorigin # 8: our own pages record a grant |
| 75 | // node dev/verify_egressconvo.mjs # and then, clean |
| 76 | // |
| 77 | // `perhost` is the sharp one: it restores the reported defect exactly, by making |
| 78 | // the page answer a granted conversation as though it had never been asked. |
| 79 | // |
| 80 | // `sameorigin` is the subtle one. The same-origin shortcut answers `allow` for |
| 81 | // Daimond's own pages, with nobody asked — and `allow` is now the word that |
| 82 | // RECORDS a standing grant. Restoring it grants the whole conversation on the |
| 83 | // strength of a request nobody saw. |
| 84 | // |
| 85 | // IT REDDENED NOTHING AT FIRST, and that was a finding about the checks rather |
| 86 | // than about the break: nothing in the run fetched our own origin, so the |
| 87 | // shortcut was never reached. Check 8 exists because of it. |
| 88 | // |
| 89 | // THE MARK IS SET DIRECTLY, through `DaimondCore.markRead`, which is the same |
| 90 | // one-way flag every real path ends at. Which reads produce it is a Rust |
| 91 | // question and is answered there. |
| 92 | // |
| 93 | // eval "$(bash dev/world.sh 4 --up)" |
| 94 | // node dev/verify_egressconvo.mjs |
| 95 | // |
| 96 | // Needs dev/serve.mjs and the mock. No gateway. Needs the wasm to have been |
| 97 | // rebuilt since src/ last changed. |
| 98 | import fs from 'node:fs'; |
| 99 | import path from 'node:path'; |
| 100 | import { fileURLToPath } from 'node:url'; |
| 101 | import { open, newChat, scratch, shot } from './harness.mjs'; |
| 102 | |
| 103 | const HERE = path.dirname(fileURLToPath(import.meta.url)); |
| 104 | const WWW = path.join(HERE, '..', 'www'); |
| 105 | |
| 106 | const BREAK = (() => { |
| 107 | const i = process.argv.indexOf('--break'); |
| 108 | return i > 0 ? String(process.argv[i + 1] || '') : ''; |
| 109 | })(); |
| 110 | |
| 111 | // Each break is one real edit to one real file, served in its place. All four |
| 112 | // live in the page, because that is the half a served file can reach; the engine |
| 113 | // half is broken in Rust, by dev/../src/tools.rs's own tests. |
| 114 | const BREAKS = { |
| 115 | // The conversation's grant never taken into account: every fetch draws the |
| 116 | // dialog again, which is the defect as reported. |
| 117 | perhost: { |
| 118 | file: 'js/daimond.js', |
| 119 | find: "\t\tif (granted) return 'allow-once';", |
| 120 | with: "\t\tif (false) return 'allow-once';", |
| 121 | }, |
| 122 | // The wide grant asked for in the words of a narrow one — the old per-site |
| 123 | // question, in front of a yes that now covers everything. |
| 124 | narrowask: { |
| 125 | file: 'js/daimond.js', |
| 126 | find: "\t\t\tt('egress.any_body', { host: host }),\n\t\t\tt('egress.any_ok'),\n\t\t\t{ title: t('egress.any_title'), danger: true });", |
| 127 | with: "\t\t\tt('egress.reach_body', { host: host }),\n\t\t\tt('egress.reach_ok', { host: host }),\n\t\t\t{ title: t('egress.reach_title', { host: host }), danger: true });", |
| 128 | }, |
| 129 | // The payload test moved below the grant, where it can no longer fire — which |
| 130 | // is the one edit that would make this widening genuinely dangerous. |
| 131 | heavyfree: { |
| 132 | file: 'js/daimond.js', |
| 133 | find: "\t\tif (load.heavy) {", |
| 134 | with: "\t\tif (load.heavy && !granted) {", |
| 135 | }, |
| 136 | // The daimon's mark put on the shared client WITHOUT naming the Diamond, which |
| 137 | // is where it landed before the taint was keyed by conversation: on the client's |
| 138 | // own key, which is the empty string and belongs to no Diamond at all. Reddens |
| 139 | // check 9's precondition -- and check 6 with it, honestly: a mark that lands on |
| 140 | // nobody's conversation is not the daimon's either. It is the only half of that |
| 141 | // fault a served file can reach; the bleed itself is a Rust field, broken and |
| 142 | // held there by |
| 143 | // `test_one_diamonds_reading_does_not_taint_another_on_a_shared_client`. |
| 144 | unnamedmark: { |
| 145 | file: 'js/daimond.js', |
| 146 | find: "\t\t\t\tif (da && da.set_tainted) { da.set_tainted(current.diamondId); return true; }", |
| 147 | with: "\t\t\t\tif (da && da.set_tainted) { da.set_tainted(); return true; }", |
| 148 | }, |
| 149 | // Our own origin answering in the word that records a standing grant. |
| 150 | sameorigin: { |
| 151 | file: 'js/daimond.js', |
| 152 | find: "\t\tif (!strict && host === location.host) return reading ? 'allow-once' : 'allow';", |
| 153 | with: "\t\tif (!strict && host === location.host) return 'allow';", |
| 154 | }, |
| 155 | }; |
| 156 | if (BREAK && !BREAKS[BREAK]) { |
| 157 | console.error(`unknown break '${BREAK}'; one of: ${Object.keys(BREAKS).join(', ')}`); |
| 158 | process.exit(2); |
| 159 | } |
| 160 | |
| 161 | let bad = 0; |
| 162 | const check = (pass, name, detail) => { |
| 163 | if (!pass) bad++; |
| 164 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 165 | }; |
| 166 | |
| 167 | const stub = async (page) => { |
| 168 | if (!BREAK) return; |
| 169 | const spec = BREAKS[BREAK]; |
| 170 | const src = fs.readFileSync(path.join(WWW, spec.file), 'utf8'); |
| 171 | // An anchor that is not there exactly once patches nothing and the run would |
| 172 | // pass quietly, which is worse than a red. |
| 173 | if (src.split(spec.find).length !== 2) { |
| 174 | console.error(`break '${BREAK}': its anchor is not in ${spec.file} exactly once`); |
| 175 | process.exit(2); |
| 176 | } |
| 177 | const body = src.replace(spec.find, spec.with); |
| 178 | await page.route('**/' + spec.file, (r) => r.fulfill({ |
| 179 | status: 200, contentType: 'application/javascript', body, |
| 180 | })); |
| 181 | }; |
| 182 | |
| 183 | const s = await open({ |
| 184 | name: 'egressconvo', |
| 185 | profile: scratch('pw', 'egressconvo' + (BREAK ? '-' + BREAK : '')), |
| 186 | route: stub, |
| 187 | }); |
| 188 | const { page: p } = s; |
| 189 | if (BREAK) console.log(`\n*** RUNNING UNDER --break ${BREAK}: failures below are the point ***\n`); |
| 190 | |
| 191 | /// Every dialog raised while `body` runs, answered the way `yes` says, with the |
| 192 | /// text of each one kept. |
| 193 | /// |
| 194 | /// COUNTED BY ANSWERING THEM, not by watching for one and stopping. A gate that |
| 195 | /// raised two dialogs would otherwise look exactly like a gate that raised one: |
| 196 | /// the second sits unanswered behind the first, the turn never finishes, and the |
| 197 | /// check reads "a dialog appeared" and passes. |
| 198 | const asks = async (body, { yes = true, snap = '' } = {}) => { |
| 199 | const seen = []; |
| 200 | let stop = false; |
| 201 | const pump = (async () => { |
| 202 | while (!stop) { |
| 203 | // READ, then photograph, then answer -- three steps and not one, because a |
| 204 | // dialog that has already been clicked is not on screen to be photographed, |
| 205 | // and the wording is half of what this file is for. |
| 206 | const got = await p.evaluate(() => { |
| 207 | const card = [...document.querySelectorAll('.dlg-card')] |
| 208 | .filter(c => c.getClientRects().length).pop(); |
| 209 | if (!card) return null; |
| 210 | return { |
| 211 | msg: (card.querySelector('.dlg-msg') || card).textContent || '', |
| 212 | title: (card.querySelector('h2') || {}).textContent || '', |
| 213 | ok: (card.querySelector('.dlg-ok') || {}).textContent || '', |
| 214 | }; |
| 215 | }).catch(() => null); |
| 216 | if (got) { |
| 217 | if (snap && !seen.length) await shot(s, snap); |
| 218 | await p.evaluate((ok) => { |
| 219 | const card = [...document.querySelectorAll('.dlg-card')] |
| 220 | .filter(c => c.getClientRects().length).pop(); |
| 221 | if (!card) return; |
| 222 | const b = card.querySelector(ok ? '.dlg-ok' : '.dlg-cancel') |
| 223 | || card.querySelector('.dlg-ok'); |
| 224 | if (b) b.click(); |
| 225 | }, yes).catch(() => {}); |
| 226 | seen.push(got); |
| 227 | } |
| 228 | await p.waitForTimeout(250); |
| 229 | } |
| 230 | })(); |
| 231 | await body(); |
| 232 | stop = true; |
| 233 | await pump; |
| 234 | return seen; |
| 235 | }; |
| 236 | |
| 237 | /// One turn in whatever conversation is in focus, and the dialogs it raised. |
| 238 | const turn = (text, { yes = true, wait = 6000, snap = '' } = {}) => asks(async () => { |
| 239 | await p.fill('#chat-input', text); |
| 240 | await p.click('#chat-send', { force: true }); |
| 241 | await p.waitForTimeout(wait); |
| 242 | }, { yes: yes, snap: snap }); |
| 243 | |
| 244 | const mark = () => p.evaluate(() => !!(window.DaimondCore && DaimondCore.markRead())); |
| 245 | |
| 246 | const FETCH = (u) => `@tool web_fetch {"url":"${u}"}`; |
| 247 | |
| 248 | try { |
| 249 | // ── 1. A clean turn is never asked ─────────────────────────── |
| 250 | await newChat(s); |
| 251 | const clean = await turn(FETCH('https://alpha.test/one')); |
| 252 | check(clean.length === 0, |
| 253 | 'A TURN THAT HAS READ NOTHING IS NOT ASKED AT ALL', |
| 254 | `${clean.length} dialog(s)`); |
| 255 | |
| 256 | // ── 2 and 3. Two sites, one ask, and what it says ──────────── |
| 257 | const marked = await mark(); |
| 258 | check(marked, 'the conversation can be marked as having read outside content', |
| 259 | marked ? '' : 'DaimondCore.markRead did not take'); |
| 260 | // TWO CALLS IN ONE TURN, to two different hosts. Every one of these was its |
| 261 | // own dialog before, and the second is the one the tester was reporting. |
| 262 | const two = await asks(async () => { |
| 263 | await p.fill('#chat-input', |
| 264 | '@tools web_fetch {"url":"https://alpha.test/one"} ;; web_fetch {"url":"https://beta.test/two"}'); |
| 265 | await p.click('#chat-send', { force: true }); |
| 266 | await p.waitForTimeout(9000); |
| 267 | }, { snap: 'egressconvo-ask' }); |
| 268 | check(two.length === 1, |
| 269 | 'TWO DIFFERENT SITES IN ONE TURN RAISE EXACTLY ONE ASK', |
| 270 | `${two.length} dialog(s): ${JSON.stringify(two.map(x => x.title))}`); |
| 271 | const said = two[0] || { msg: '', title: '', ok: '' }; |
| 272 | const whole = (said.title + ' ' + said.msg + ' ' + said.ok).toLowerCase(); |
| 273 | // AGAINST THE APP'S OWN STRING, not against words this file chose: the copy |
| 274 | // will be reworded, and a literal from today's draft would leave the check |
| 275 | // unable to fail for the right reason later. |
| 276 | const wide = await p.evaluate(() => ({ |
| 277 | title: DaimondI18n.t('egress.any_title'), |
| 278 | ok: DaimondI18n.t('egress.any_ok'), |
| 279 | body: DaimondI18n.t('egress.any_body', { host: 'alpha.test' }), |
| 280 | narrow: DaimondI18n.t('egress.reach_title', { host: 'alpha.test' }), |
| 281 | })); |
| 282 | check(said.title.trim() === wide.title.trim() && said.ok.trim() === wide.ok.trim(), |
| 283 | 'and it is the WIDE question, not the old one about a single site', |
| 284 | `title=${JSON.stringify(said.title)} ok=${JSON.stringify(said.ok)}`); |
| 285 | // The pair, because either half alone is satisfied by a reworded narrow |
| 286 | // dialog: it must say the grant covers ANY site, and it must say where the |
| 287 | // grant stops. |
| 288 | check(/any website/.test(whole) && /this conversation/.test(whole), |
| 289 | 'AND IT SAYS PLAINLY WHAT IS BEING GRANTED — any website, this conversation', |
| 290 | JSON.stringify(said.msg.slice(0, 180))); |
| 291 | |
| 292 | // ── 4. A later turn in the same chat ───────────────────────── |
| 293 | const third = await turn(FETCH('https://gamma.test/three')); |
| 294 | check(third.length === 0, |
| 295 | 'A THIRD SITE, IN A LATER TURN OF THE SAME CHAT, IS NOT ASKED', |
| 296 | `${third.length} dialog(s)`); |
| 297 | |
| 298 | // ── 7. And a long address is still its own question ────────── |
| 299 | // |
| 300 | // Here, while the conversation HAS granted the web, because that is the only |
| 301 | // state in which this can fail. |
| 302 | const heavy = await turn(FETCH( |
| 303 | 'https://gamma.test/x?carry=' + 'A'.repeat(200)), { wait: 8000 }); |
| 304 | check(heavy.length === 1, |
| 305 | 'AN ADDRESS CARRYING A PAYLOAD IS STILL ASKED ABOUT, grant or no grant', |
| 306 | `${heavy.length} dialog(s): ${JSON.stringify(heavy.map(x => x.title))}`); |
| 307 | |
| 308 | // ── 5. A new chat asks again ───────────────────────────────── |
| 309 | // |
| 310 | // A TURN FIRST, and it is not a flourish: `ensureApp` builds a chat's engine on |
| 311 | // its first send, so a chat that has said nothing has no engine to mark and |
| 312 | // `markRead` silently answers false. Written the other way round first, and the |
| 313 | // check duly read "not asked" about a chat that was never tainted — which is a |
| 314 | // check that cannot fail rather than a check that passed. |
| 315 | await newChat(s); |
| 316 | await turn('@text a brand new chat'); |
| 317 | const remarked = await mark(); |
| 318 | check(remarked, 'the new chat can be marked too', |
| 319 | remarked ? '' : 'markRead did not take on the new chat'); |
| 320 | const fresh = await turn(FETCH('https://alpha.test/one')); |
| 321 | check(fresh.length === 1, |
| 322 | 'A NEW CHAT ASKS AGAIN — the grant does not cross a conversation', |
| 323 | `${fresh.length} dialog(s)`); |
| 324 | |
| 325 | // ── 6. A daimon is its own conversation ────────────────────── |
| 326 | // |
| 327 | // Made the way a person makes one, and steered through the same composer, so |
| 328 | // what is measured is the daimon's own engine and not a second path. |
| 329 | await p.evaluate(() => document.getElementById('new-diamond-btn').click()); |
| 330 | await p.waitForSelector('.dlg-card', { timeout: 8000 }); |
| 331 | await p.evaluate(() => { |
| 332 | const card = [...document.querySelectorAll('.dlg-card')].filter(c => c.getClientRects().length).pop(); |
| 333 | const inp = card.querySelector('input.dlg-input'); |
| 334 | inp.value = 'Reaching'; |
| 335 | inp.dispatchEvent(new Event('input', { bubbles: true })); |
| 336 | card.querySelector('.dlg-ok').click(); |
| 337 | }); |
| 338 | await p.waitForTimeout(1800); |
| 339 | await p.$$eval('.diamond-box', els => els[0] && els[0].click()); |
| 340 | await p.waitForTimeout(1200); |
| 341 | const dmark = await mark(); |
| 342 | check(dmark, 'the daimon can be marked as having read outside content', |
| 343 | dmark ? '' : 'markRead did not reach the daimon'); |
| 344 | const daimon = await turn(FETCH('https://alpha.test/one'), { wait: 9000 }); |
| 345 | check(daimon.length === 1, |
| 346 | 'A DAIMON IS ITS OWN CONVERSATION — a chat’s yes does not answer for it', |
| 347 | `${daimon.length} dialog(s)`); |
| 348 | |
| 349 | // ── 9. AND SO IS THE DIAMOND NEXT TO IT ────────────────────── |
| 350 | // |
| 351 | // TWO DIAMONDS ON ONE MODEL. `diamondApp` caches one client per provider and |
| 352 | // model, a daimon turn clones that client's `read_seen` cache, and until |
| 353 | // 2026-08-28 the taint on it was a bare `bool`. So a Research Diamond that |
| 354 | // fetched a page took the network away from an Accounts Diamond that had |
| 355 | // touched nothing external, and stamped its dispatched workers as carrying a |
| 356 | // stranger's words. Held in Rust by |
| 357 | // `test_one_diamonds_reading_does_not_taint_another_on_a_shared_client`; held |
| 358 | // HERE because the Rust half cannot see whether the app names the Diamond when |
| 359 | // it marks and reads the flag, and unnamed the mark lands on the shared |
| 360 | // client's own conversation, which is nobody's. |
| 361 | // |
| 362 | // THE SHARING IS ASSERTED, not assumed. Two Diamonds that happened to sit on |
| 363 | // two clients would pass every check below while proving nothing at all -- which |
| 364 | // is the fault the Rust test next door was carrying, and the reason this line |
| 365 | // exists. |
| 366 | await p.evaluate(() => document.getElementById('new-diamond-btn').click()); |
| 367 | await p.waitForSelector('.dlg-card', { timeout: 8000 }); |
| 368 | await p.evaluate(() => { |
| 369 | const card = [...document.querySelectorAll('.dlg-card')].filter(c => c.getClientRects().length).pop(); |
| 370 | const inp = card.querySelector('input.dlg-input'); |
| 371 | inp.value = 'Accounts'; |
| 372 | inp.dispatchEvent(new Event('input', { bubbles: true })); |
| 373 | card.querySelector('.dlg-ok').click(); |
| 374 | }); |
| 375 | await p.waitForTimeout(1800); |
| 376 | const twoD = await p.evaluate(() => { |
| 377 | const rows = [...document.querySelectorAll('.diamond-box[data-id]')] |
| 378 | .map(e => ({ id: e.dataset.id, name: (e.textContent || '').trim() })); |
| 379 | const read = rows.find(r => /Reaching/.test(r.name)) || null; |
| 380 | const clean = rows.find(r => /Accounts/.test(r.name)) || null; |
| 381 | // ONE CLIENT, ASKED ABOUT BOTH. `diamondApp()` is the client for the starred |
| 382 | // model, which is the model a Diamond made with no pin runs on -- so this is |
| 383 | // the object both Diamonds' daimon turns clone their cache from. |
| 384 | const app = window.DaimondCore.diamondApp(); |
| 385 | const ask = (r) => (r && app && app.is_tainted) ? !!app.is_tainted(r.id) : null; |
| 386 | return { read, clean, dirtyMark: ask(read), cleanMark: ask(clean) }; |
| 387 | }); |
| 388 | check(!!twoD.read && !!twoD.clean, |
| 389 | 'a second Diamond exists beside the one that read a page', |
| 390 | JSON.stringify([twoD.read, twoD.clean])); |
| 391 | // THE PRECONDITION, STATED. This client knowing about the FIRST Diamond's |
| 392 | // reading is what proves the two share a cache at all; without it the check |
| 393 | // below would be two Diamonds on two clients, which never had the fault and |
| 394 | // would pass for no reason. |
| 395 | check(twoD.dirtyMark === true, |
| 396 | 'and the shared client carries the first Diamond’s reading, or nothing below is about sharing', |
| 397 | `mark=${String(twoD.dirtyMark)}`); |
| 398 | check(twoD.cleanMark === false, |
| 399 | 'A DIAMOND THAT READ NOTHING IS NOT MARKED BY THE ONE THAT DID', |
| 400 | `mark=${String(twoD.cleanMark)}`); |
| 401 | // AND WHAT THE USER MEETS: the clean Diamond keeps its network and is not |
| 402 | // interrupted. Measured by driving it, because the flag reading clean and the |
| 403 | // gate acting on it are two claims. |
| 404 | await p.evaluate((id) => { |
| 405 | const esc = (window.CSS && CSS.escape) ? CSS.escape(id) : id; |
| 406 | const box = document.querySelector('.diamond-box[data-id="' + esc + '"]'); |
| 407 | if (box) box.click(); |
| 408 | }, twoD.clean ? twoD.clean.id : ''); |
| 409 | await p.waitForTimeout(1200); |
| 410 | const untouchedDaimon = await turn(FETCH('https://delta.test/four'), { wait: 9000 }); |
| 411 | check(untouchedDaimon.length === 0, |
| 412 | 'and it is never asked — a stranger’s words stop at the Diamond that read them', |
| 413 | `${untouchedDaimon.length} dialog(s)`); |
| 414 | |
| 415 | // ── 8. Our own pages grant nothing ─────────────────────────── |
| 416 | // |
| 417 | // The same-origin shortcut answers without asking anybody, and the word it |
| 418 | // answers in is now the difference between "this one page" and "the whole |
| 419 | // conversation". If it says the wide word, a fetch of Daimond's own address — |
| 420 | // which the model can write for itself — hands over every site, with no |
| 421 | // dialog ever drawn. Measured as a PAIR, because the shortcut passing is not |
| 422 | // the property: what matters is that the NEXT site is still asked about. |
| 423 | await newChat(s); |
| 424 | await turn('@text a chat for our own pages'); |
| 425 | await mark(); |
| 426 | const ownUrl = await p.evaluate(() => location.origin + '/index.html'); |
| 427 | const own = await turn(FETCH(ownUrl)); |
| 428 | check(own.length === 0, |
| 429 | 'Daimond’s own pages are not put to the user', |
| 430 | `${own.length} dialog(s)`); |
| 431 | const after = await turn(FETCH('https://alpha.test/one')); |
| 432 | check(after.length === 1, |
| 433 | 'AND THEY GRANT NOTHING — the next site is still asked about', |
| 434 | `${after.length} dialog(s)`); |
| 435 | } finally { |
| 436 | await s.close(); |
| 437 | } |
| 438 | |
| 439 | console.log(bad ? `\n${bad} check(s) FAILED` : '\nall checks passed'); |
| 440 | process.exit(bad ? 1 : 0); |