oxedyne/daimond/dev/verify_crystalcap.mjs
24.2 KiB, 1 run
created by r2519314175:325, 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_crystalcap.mjs — a crystal is a summary and its page is a page, and |
| 2 | // each has a ceiling that says so. |
| 3 | // |
| 4 | // The crystal carries the reduced state of a Diamond; the scope attached to it |
| 5 | // carries the weight. Nothing enforced that, so a daimon that started recording |
| 6 | // rather than reducing simply kept going — and the bill arrived somewhere else, |
| 7 | // because every fold copies the whole crystal into `versions/` and all of it |
| 8 | // rides in the sync parcel. |
| 9 | // |
| 10 | // WHAT CHANGED ON 2026-08-09, and why this file was wrong until now: the crystal |
| 11 | // became TWO files. `crystal.json` is the memory and `crystal.html` is the page |
| 12 | // that renders it, and they have SEPARATE ceilings — `CRYSTAL_CAP_DEFAULT` and |
| 13 | // `CRYSTAL_PAGE_CAP_DEFAULT` in `src/tools.rs`, read from there at the top of this |
| 14 | // file rather than restated — because a page is bigger than a summary and NEITHER |
| 15 | // IS EXEMPT. This file pinned one ceiling on one file, so a page could be any size |
| 16 | // at all and nothing here would have noticed: the page rides in every `versions/` snapshot where it changed and |
| 17 | // shares SYNC_DIAMONDS_MAX (4 MB) with the memory, so exempting presentation |
| 18 | // voids the cap's stated purpose. Worse, the page is written by a MODEL, which |
| 19 | // is the one author in this app with no sense of how big a file is. |
| 20 | // |
| 21 | // The rule is one function per file — `tools::crystal_write_refused` and |
| 22 | // `tools::crystal_page_write_refused` — and each is checked at all THREE doors, |
| 23 | // which is the only reason either holds: |
| 24 | // |
| 25 | // * `Tool::FileWrite`, which is how a DAIMON rewrites the file. The store |
| 26 | // sees that write only afterwards, when `record_steer` snapshots what is on |
| 27 | // disk, so a check there would be refusing a write that already happened. |
| 28 | // * `Tool::FileEdit`, which is how a daimon edits it IN PLACE. This door was |
| 29 | // missing until 2026-08-09, and the file said "BOTH doors" while a daimon |
| 30 | // that edited rather than rewrote walked past the ceiling entirely. The |
| 31 | // store's door did then fire, but too late to help: it reads the old length |
| 32 | // from disk, and by then the edit had landed, so `old == new`, the refusal |
| 33 | // arrived after the fact, `record_steer` errored, the turn failed, and an |
| 34 | // OVERSIZED CRYSTAL WAS LEFT ON DISK WITH NO VERSION SNAPSHOT AND NO LOG |
| 35 | // RECORD. Every assertion below used to go through the other two doors, |
| 36 | // which is exactly why nothing went red. |
| 37 | // * `diamond::snapshot`, reached by `write_crystal_data` / `write_crystal_page`, |
| 38 | // which is how a HAND EDIT and a FOLD write. They never touch the file tool, |
| 39 | // so the first two doors do not see them. |
| 40 | // |
| 41 | // And three things the ceilings must NOT do, each of which would be worse than |
| 42 | // having no ceiling at all: |
| 43 | // |
| 44 | // * refuse a write that makes an oversized file SMALLER, which would leave |
| 45 | // every Diamond that predates the rule unable to be edited down to it; |
| 46 | // * apply to anything but the two live files — a `versions/NNNN.json` or |
| 47 | // `versions/NNNN.html` snapshot of an oversized crystal has to keep being |
| 48 | // written, or the Diamond at the ceiling cannot be recorded at all; |
| 49 | // * be ONE ceiling wearing two names. They are different numbers for different |
| 50 | // jobs, and a file sized between them is over one and under the other. |
| 51 | // |
| 52 | // How each family goes red (this lane could not run a browser, so the lead's |
| 53 | // batched pass is the first time they are exercised): |
| 54 | // |
| 55 | // * make `is_crystal_page_path` return false → every page check goes red and |
| 56 | // every data check stays green, which is the shape the old file could not see; |
| 57 | // * point `crystal_page_cap()` at `crystal_cap()` → the three DEFAULTS checks |
| 58 | // and the two independence checks go red; |
| 59 | // * drop the `is_crystal_*_path` guard from `Tool::FileEdit` → the two |
| 60 | // "refused bytes never reached the file" checks go red and nothing else does; |
| 61 | // * drop the `&& new_len >= old_len` half of either rule → the four shrink |
| 62 | // checks go red. |
| 63 | // |
| 64 | // node dev/verify_crystalcap.mjs |
| 65 | // |
| 66 | // Needs dev/serve.mjs (DAIMOND_PORT, default 8777). No gateway, no mock LLM: nothing |
| 67 | // here runs a turn. |
| 68 | import fs from 'node:fs'; |
| 69 | import path from 'node:path'; |
| 70 | import { fileURLToPath } from 'node:url'; |
| 71 | import { open, scratch } from './harness.mjs'; |
| 72 | |
| 73 | // The two shipped ceilings, READ FROM THE ENGINE rather than restated here. |
| 74 | // |
| 75 | // They were `16 * 1024` and `64 * 1024` written out as literals in this file, and on |
| 76 | // 2026-08-13 the page ceiling was raised to 128 KiB — so the "80 KiB is over it" check |
| 77 | // went red against a build that was working exactly as intended, and the number this |
| 78 | // file believed in was one nothing enforced. A restated constant can only ever be |
| 79 | // right until somebody changes the real one. `dev/verify_lifelog.mjs` reads the same |
| 80 | // constant the same way. |
| 81 | const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); |
| 82 | const capOf = (name) => { |
| 83 | const src = fs.readFileSync(path.join(ROOT, 'src', 'tools.rs'), 'utf8'); |
| 84 | const m = new RegExp(name + ':\\s*usize\\s*=\\s*(\\d+)\\s*\\*\\s*(\\d+)').exec(src); |
| 85 | if (!m) throw new Error('verify_crystalcap: ' + name + ' not found in src/tools.rs'); |
| 86 | return Number(m[1]) * Number(m[2]); |
| 87 | }; |
| 88 | const DATA_CAP = capOf('CRYSTAL_CAP_DEFAULT'); |
| 89 | const PAGE_CAP = capOf('CRYSTAL_PAGE_CAP_DEFAULT'); |
| 90 | |
| 91 | // The whole point of the defaults block below is that these are two DIFFERENT numbers |
| 92 | // with room between them. If they ever meet, there is no size that is over one and under |
| 93 | // the other, and the block would pass by having nothing left to ask. |
| 94 | if (!(DATA_CAP < PAGE_CAP)) { |
| 95 | console.error('verify_crystalcap: the page ceiling (' + PAGE_CAP + ') is not above the ' |
| 96 | + 'memory ceiling (' + DATA_CAP + '), so no file can be over one and under the other. ' |
| 97 | + 'These are meant to be different numbers for different jobs; see src/tools.rs.'); |
| 98 | process.exit(2); |
| 99 | } |
| 100 | // AND THE FIGURE THE SETTINGS PANE SHOWS, read the same way and for the same reason. |
| 101 | // |
| 102 | // `DEFAULT_CRYSTAL_KB` and `DEFAULT_CRYSTAL_PAGE_KB` in `daimond.js` are a hand-kept copy of the |
| 103 | // two constants above, used to label the "Default" row of each pulldown. Nothing set them from the |
| 104 | // engine and nothing checked them, so when the page ceiling was raised to 128 KiB on 2026-08-13 the |
| 105 | // label went on saying 64 KB and kept saying it for a fortnight. That is worse than having no label: |
| 106 | // the one place in the product that names the ceiling named half of it, and the author of a capp |
| 107 | // that met the real ceiling had to establish it by experiment. This runs before the browser does, |
| 108 | // because a number that disagrees with the engine is wrong whatever the app then does with it. |
| 109 | const labelOf = (name) => { |
| 110 | const src = fs.readFileSync(path.join(ROOT, 'www', 'js', 'daimond.js'), 'utf8'); |
| 111 | const m = new RegExp('var\\s+' + name + '\\s*=\\s*(\\d+)\\s*;').exec(src); |
| 112 | if (!m) throw new Error('verify_crystalcap: ' + name + ' not found in www/js/daimond.js'); |
| 113 | return Number(m[1]) * 1024; |
| 114 | }; |
| 115 | for (const [label, cap, engine] of [ |
| 116 | ['DEFAULT_CRYSTAL_KB', labelOf('DEFAULT_CRYSTAL_KB'), DATA_CAP], |
| 117 | ['DEFAULT_CRYSTAL_PAGE_KB', labelOf('DEFAULT_CRYSTAL_PAGE_KB'), PAGE_CAP], |
| 118 | ]) { |
| 119 | if (cap !== engine) { |
| 120 | console.error('verify_crystalcap: ' + label + ' in www/js/daimond.js says ' + cap |
| 121 | + ' bytes, and the engine enforces ' + engine + '. The settings pane would tell the ' |
| 122 | + 'user a ceiling that is not the one refusing their writes; see src/tools.rs.'); |
| 123 | process.exit(2); |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | // AND THE FIGURE THE GUIDE SHOWS, which is the copy a non-technical reader meets. |
| 128 | // |
| 129 | // `www/guide/capps.html` gives the page ceiling a card of its own, headed with the number, and |
| 130 | // `dev/guide-i18n/_source.json` carries that heading as a translatable run. The block above was |
| 131 | // written after the settings label had spent a fortnight naming half the real ceiling; the guide |
| 132 | // was a THIRD copy of the same number, it was left at 128 KB when the engine went to 512 KiB, and |
| 133 | // nothing here read it. A reader who trusts the guide over the refusal has no way to find out. |
| 134 | // Both are checked, because a corrected page with a stale bank entry ships the old figure to |
| 135 | // seven other languages. |
| 136 | const guideCap = (label, file, rel, text) => { |
| 137 | const m = /(\d+)\s*(K|M)i?B/i.exec(text); |
| 138 | if (!m) { |
| 139 | console.error('verify_crystalcap: ' + label + ' names no size in ' + rel + '. The guide\'s ' |
| 140 | + 'page-ceiling card is how a reader learns the number; see www/guide/capps.html.'); |
| 141 | process.exit(2); |
| 142 | } |
| 143 | return Number(m[1]) * (m[2].toUpperCase() === 'M' ? 1024 * 1024 : 1024); |
| 144 | }; |
| 145 | { |
| 146 | // The card is found by the control it names rather than by its heading id, so renumbering the |
| 147 | // page's anchors does not silently take the check with it. |
| 148 | const rel = path.join('www', 'guide', 'capps.html'); |
| 149 | const html = fs.readFileSync(path.join(ROOT, rel), 'utf8'); |
| 150 | const card = /<div class="card">\s*<h3[^>]*>([^<]*)<\/h3>\s*<p>(?:(?!<\/div>)[\s\S])*?Page size limit[\s\S]*?<\/div>/i.exec(html); |
| 151 | if (!card) { |
| 152 | console.error('verify_crystalcap: no card in ' + rel + ' names "Page size limit", so the ' |
| 153 | + 'guide either stopped naming the page ceiling or renamed the setting it points at.'); |
| 154 | process.exit(2); |
| 155 | } |
| 156 | const heading = card[1]; |
| 157 | const shown = guideCap('the guide\'s page-ceiling card', rel, rel, heading); |
| 158 | if (shown !== PAGE_CAP) { |
| 159 | console.error('verify_crystalcap: ' + rel + ' heads its page-ceiling card "' + heading.trim() |
| 160 | + '" (' + shown + ' bytes), and the engine enforces ' + PAGE_CAP + '. This is the one ' |
| 161 | + 'place a non-technical reader is told the ceiling; see src/tools.rs.'); |
| 162 | process.exit(2); |
| 163 | } |
| 164 | // And the translatable run behind it, which is what the seven locale pages are built from. |
| 165 | const bankRel = path.join('dev', 'guide-i18n', '_source.json'); |
| 166 | const bank = JSON.parse(fs.readFileSync(path.join(ROOT, bankRel), 'utf8')); |
| 167 | const runs = (bank['capps.html'] || []).filter((s) => /^\s*\d+\s*(K|M)i?B\s*$/i.test(s)); |
| 168 | if (runs.length !== 1 || guideCap('the bank\'s copy', bankRel, bankRel, runs[0]) !== PAGE_CAP) { |
| 169 | console.error('verify_crystalcap: ' + bankRel + ' holds ' + JSON.stringify(runs) + ' where ' |
| 170 | + 'the guide\'s page-ceiling heading should be, and the engine enforces ' + PAGE_CAP |
| 171 | + '. Run `node dev/guide_i18n.mjs extract` after editing the English page, or the ' |
| 172 | + 'translations keep shipping the old figure.'); |
| 173 | process.exit(2); |
| 174 | } |
| 175 | } |
| 176 | |
| 177 | const KIB = (n) => (n / 1024) + ' KiB'; |
| 178 | const MID_LEN = DATA_CAP + Math.floor((PAGE_CAP - DATA_CAP) / 2); // over memory, under page |
| 179 | const BIG_LEN = PAGE_CAP + 16 * 1024; // over page as well |
| 180 | |
| 181 | const PROFILE = scratch('pw', 'crystalcap'); |
| 182 | fs.rmSync(PROFILE, { recursive: true, force: true }); |
| 183 | |
| 184 | let bad = 0; |
| 185 | const check = (pass, name, detail) => { |
| 186 | if (!pass) bad++; |
| 187 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 188 | }; |
| 189 | |
| 190 | /// One recorded outcome, or a stand-in that FAILS and says why it is missing. |
| 191 | /// |
| 192 | /// A result the run never reached must not read as a pass, and it must not |
| 193 | /// throw here either: a build with no page ceiling should produce a column of |
| 194 | /// named reds pointing at the missing function, not one stack trace. |
| 195 | const R = (f, k) => (f && f[k]) || { ok: false, msg: (f && f.missing) || 'no result recorded' }; |
| 196 | |
| 197 | const s = await open({ name: 'crystalcap', profile: PROFILE, connect: false }); |
| 198 | const { page } = s; |
| 199 | |
| 200 | try { |
| 201 | await page.waitForTimeout(1500); |
| 202 | |
| 203 | const out = await page.evaluate(async ({ MID_LEN, BIG_LEN }) => { |
| 204 | const mod = await import('../pkg/oxedyne_daimond.js'); |
| 205 | const app = new mod.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 256, '', true); |
| 206 | const r = { absent: [] }; |
| 207 | |
| 208 | // Named up front, so a red is attributable to the engine rather than to the |
| 209 | // checks below discovering the same absence twenty times over. |
| 210 | for (const fn of ['set_crystal_cap', 'set_crystal_page_cap', |
| 211 | 'write_crystal_data', 'write_crystal_page']) { |
| 212 | if (typeof app[fn] !== 'function') r.absent.push(fn); |
| 213 | } |
| 214 | |
| 215 | const strip = (m) => String(m).replace(/\[[0-9;]*m/g, ''); |
| 216 | |
| 217 | // `run_tool` RESOLVES with the error text rather than rejecting, so a |
| 218 | // refusal and a write look identical to a `try`/`catch`. Success is the |
| 219 | // "Wrote N bytes" shape and nothing else -- reading it the other way round |
| 220 | // made this file report two passes it had not earned. |
| 221 | const write = async (path, content) => { |
| 222 | let msg; |
| 223 | try { msg = String(await app.run_tool('file_write', JSON.stringify({ path, content }))); } |
| 224 | catch (e) { msg = String(e && e.message ? e.message : e); } |
| 225 | return { ok: /^Wrote \d+ bytes/.test(msg), msg: strip(msg) }; |
| 226 | }; |
| 227 | const edit = async (path, oldS, newS) => { |
| 228 | let msg; |
| 229 | try { |
| 230 | msg = String(await app.run_tool('file_edit', |
| 231 | JSON.stringify({ path, old_string: oldS, new_string: newS }))); |
| 232 | } catch (e) { msg = String(e && e.message ? e.message : e); } |
| 233 | return { ok: /^Edited /.test(msg), msg: strip(msg) }; |
| 234 | }; |
| 235 | // The bytes as they are on disk, through the store's own reader. |
| 236 | // |
| 237 | // NOT `run_tool('file_read')`: that is a MODEL-FACING rendering which |
| 238 | // numbers every line and wraps an untrusted path in an envelope, and two |
| 239 | // verifiers in this tree have already compared it to plain bytes and been |
| 240 | // red for weeks over nothing. `store_read` pins the OPFS root, which is |
| 241 | // where a Diamond lives whatever folder the user has open. |
| 242 | const onDisk = async (path) => { |
| 243 | try { return await mod.store_read(path); } catch (e) { return 'ERR ' + e; } |
| 244 | }; |
| 245 | |
| 246 | // ── The two ceilings are different numbers ─────────────────── |
| 247 | // Measured BEFORE any setter runs, so what is under test is the shipped |
| 248 | // defaults. `MID_LEN` sits between the two, so it is over the memory's |
| 249 | // ceiling and under the page's: a single ceiling serving both files fails |
| 250 | // here whichever of the two numbers it happens to hold. `BIG_LEN` is over |
| 251 | // the page's as well, which is what says the page has a ceiling at all. |
| 252 | // Both are derived from the engine's own constants, so raising either |
| 253 | // ceiling moves the fixture with it instead of turning this red. |
| 254 | { |
| 255 | const id = await app.create_diamond('Ceilings'); |
| 256 | const dir = 'diamonds/' + id; |
| 257 | // Legal JSON and legal HTML at exactly the sizes named, so a refusal |
| 258 | // can only ever be the weight and never the shape. |
| 259 | const dataMid = '{"t":"' + 'm'.repeat(MID_LEN - 8) + '"}'; |
| 260 | const pageMid = '<!--' + 'm'.repeat(MID_LEN - 7) + '-->'; |
| 261 | const pageBig = '<!--' + 'p'.repeat(BIG_LEN - 7) + '-->'; |
| 262 | r.defDataMid = await write(dir + '/crystal.json', dataMid); |
| 263 | r.defPageMid = await write(dir + '/crystal.html', pageMid); |
| 264 | r.defPageBig = await write(dir + '/crystal.html', pageBig); |
| 265 | } |
| 266 | |
| 267 | // ── Each file, at each of its three doors ──────────────────── |
| 268 | const CAP = 1000; // small, so the test is about the rule and not about writing 16 KB. |
| 269 | const specs = [ |
| 270 | { |
| 271 | key: 'data', |
| 272 | file: 'crystal.json', |
| 273 | snap: 'versions/0007.json', |
| 274 | other: 'notes.json', |
| 275 | // Legal JSON at every size, so a refusal can only be the ceiling. |
| 276 | // A store door that parses what it is handed would otherwise refuse |
| 277 | // a wall of `x` for a reason that has nothing to do with weight, and |
| 278 | // the check would pass having proved nothing. |
| 279 | body: (s) => '{"t":"' + s + '"}', |
| 280 | capFn: 'set_crystal_cap', |
| 281 | hand: (id, text) => app.write_crystal_data(id, text), |
| 282 | }, |
| 283 | { |
| 284 | key: 'page', |
| 285 | file: 'crystal.html', |
| 286 | snap: 'versions/0007.html', |
| 287 | other: 'notes.html', |
| 288 | body: (s) => '<!--' + s + '-->', |
| 289 | capFn: 'set_crystal_page_cap', |
| 290 | hand: (id, text) => app.write_crystal_page(id, text), |
| 291 | }, |
| 292 | ]; |
| 293 | |
| 294 | for (const spec of specs) { |
| 295 | const f = {}; |
| 296 | r[spec.key] = f; |
| 297 | if (typeof app[spec.capFn] !== 'function') { |
| 298 | f.missing = 'this build of the engine has no ' + spec.capFn; |
| 299 | continue; |
| 300 | } |
| 301 | const setCap = (n) => app[spec.capFn](n); |
| 302 | const over = spec.body('').length; // what the wrapper itself costs |
| 303 | const fill = (n, ch) => spec.body((ch || 'x').repeat(Math.max(0, n - over))); |
| 304 | |
| 305 | setCap(CAP); |
| 306 | const id = await app.create_diamond('Capped ' + spec.key); |
| 307 | const dir = 'diamonds/' + id; |
| 308 | const path = dir + '/' + spec.file; |
| 309 | const big = fill(CAP + 500, 'x'); |
| 310 | const small = fill(200, 'y'); |
| 311 | |
| 312 | // ── The daimon's door ──────────────────────────────── |
| 313 | f.underCap = await write(path, small); |
| 314 | f.overCap = await write(path, big); |
| 315 | // And the file must still hold the small one: a refusal that wrote anyway |
| 316 | // is not a refusal. |
| 317 | f.afterRefusal = { ok: (await onDisk(path)) === small, msg: 'on disk after the refusal' }; |
| 318 | |
| 319 | // ── Not one of the two live files, not capped ──────── |
| 320 | f.versionBig = await write(dir + '/' + spec.snap, big); |
| 321 | f.ordinaryBig = await write(dir + '/' + spec.other, big); |
| 322 | // Exactly three path segments is what keeps `is_crystal_*_path` a small |
| 323 | // change rather than a redesign, and a four-segment `crystal/data.json` |
| 324 | // layout was declined for precisely this reason. A ceiling written as a |
| 325 | // filename match would silently start capping a user's own file. |
| 326 | f.nestedBig = await write(dir + '/nested/' + spec.file, big); |
| 327 | |
| 328 | // ── The store's door ───────────────────────────────── |
| 329 | // `write_crystal_*` DOES reject, so here a throw is the refusal. |
| 330 | const hand = async (text) => { |
| 331 | try { await spec.hand(id, text); return { ok: true, msg: 'accepted' }; } |
| 332 | catch (e) { return { ok: false, msg: strip(String(e && e.message ? e.message : e)) }; } |
| 333 | }; |
| 334 | f.storeOver = await hand(big); |
| 335 | f.storeUnder = await hand(small); |
| 336 | |
| 337 | // ── The daimon's OTHER door: file_edit ─────────────── |
| 338 | // `file_edit` writes the file just as `file_write` does, so it needs the |
| 339 | // same ceiling. Anchored on a unique token, because the tool refuses an |
| 340 | // `old_string` that appears more than once and a run of identical letters |
| 341 | // matches itself many times over. |
| 342 | const seed = spec.body('HEAD' + 'y'.repeat(200)); |
| 343 | f.editSeed = await write(path, seed); |
| 344 | f.editUnder = await edit(path, 'HEAD', 'HEADER'); |
| 345 | f.editOver = await edit(path, 'HEADER', 'z'.repeat(CAP + 500)); |
| 346 | // The specific harm this door caused: not that the write was allowed, but |
| 347 | // that the turn then died at the store's door leaving the oversized bytes |
| 348 | // on disk, unsnapshotted and unlogged. So the file itself is the assertion. |
| 349 | f.afterEditRefusal = { |
| 350 | ok: (await onDisk(path)) === seed.replace('HEAD', 'HEADER'), |
| 351 | msg: 'on disk after the refused edit', |
| 352 | }; |
| 353 | |
| 354 | // ── An already-oversized file can still be edited DOWN ─ |
| 355 | // Seeded past the ceiling with the ceiling RAISED -- not with zero, which |
| 356 | // means the default rather than "no ceiling", and which quietly refused |
| 357 | // the seed the first time this was written. |
| 358 | setCap(256 * 1024); |
| 359 | f.seeded = await write(path, fill(20 * 1024, 'z')); |
| 360 | setCap(CAP); |
| 361 | // The asymmetry has to hold at the edit door too, or a Diamond that |
| 362 | // predates the rule could be rewritten down to size but never edited down. |
| 363 | // A hair over half the run, so it matches once rather than twice. |
| 364 | f.editShrink = await edit(path, 'z'.repeat(10 * 1024 + 1), ''); |
| 365 | // 20 KB less 10241 leaves 10239 -- still over, so the writes below are |
| 366 | // still shrinking and the chain that follows is unchanged. |
| 367 | f.shrinkToward = await write(path, fill(5 * 1024, 'z')); // still over, but smaller |
| 368 | f.shrinkUnder = await write(path, small); // and all the way down |
| 369 | f.growAgain = await write(path, fill(6 * 1024, 'z')); // over again: refused |
| 370 | } |
| 371 | |
| 372 | // ── Two ceilings, two settings, no shared static ───────────── |
| 373 | // The likeliest way to build this wrong is a copy-pasted setter that moves |
| 374 | // the other file's number. Nothing in the run above would show it: each |
| 375 | // half only ever writes its own file. |
| 376 | if (r.absent.length === 0) { |
| 377 | const id = await app.create_diamond('Two settings'); |
| 378 | const dir = 'diamonds/' + id; |
| 379 | const five = 5 * 1024; |
| 380 | app.set_crystal_cap(64 * 1024); |
| 381 | app.set_crystal_page_cap(1000); |
| 382 | r.pageSetterLeftData = await write(dir + '/crystal.json', |
| 383 | '{"t":"' + 'd'.repeat(five) + '"}'); |
| 384 | app.set_crystal_cap(1000); |
| 385 | app.set_crystal_page_cap(64 * 1024); |
| 386 | r.dataSetterLeftPage = await write(dir + '/crystal.html', |
| 387 | '<!--' + 'h'.repeat(five) + '-->'); |
| 388 | } |
| 389 | |
| 390 | return r; |
| 391 | }, { MID_LEN, BIG_LEN }); |
| 392 | |
| 393 | if (out.absent.length) { |
| 394 | check(false, 'the engine offers both ceilings and both store doors', |
| 395 | 'missing: ' + out.absent.join(', ')); |
| 396 | } else { |
| 397 | check(true, 'the engine offers both ceilings and both store doors'); |
| 398 | } |
| 399 | |
| 400 | // ── The shipped defaults are two different numbers ─────────── |
| 401 | check(!out.defDataMid.ok, |
| 402 | KIB(MID_LEN) + ' of MEMORY is over the default ceiling (' + KIB(DATA_CAP) + ') and is refused', |
| 403 | out.defDataMid.msg); |
| 404 | check(out.defPageMid.ok, |
| 405 | 'the same ' + KIB(MID_LEN) + ' as a PAGE is under its own, larger ceiling (' |
| 406 | + KIB(PAGE_CAP) + ') and is written', |
| 407 | out.defPageMid.msg); |
| 408 | check(!out.defPageBig.ok, |
| 409 | 'but ' + KIB(BIG_LEN) + ' of page is over that one and is refused too', |
| 410 | out.defPageBig.msg); |
| 411 | |
| 412 | // The wording of a refusal, per file. The user has TWO pulldowns in settings, |
| 413 | // so a message that does not say which file it is about leaves them guessing |
| 414 | // which one to move -- and telling somebody to put their HTML "in the |
| 415 | // Diamond's scope" is advice for the memory, aimed at the wrong file. |
| 416 | const says = { |
| 417 | data: (m) => /scope/i.test(m || ''), |
| 418 | page: (m) => /page/i.test(m || ''), |
| 419 | }; |
| 420 | const said = { data: 'names the scope as the place for the detail', page: 'names the page' }; |
| 421 | |
| 422 | for (const key of ['data', 'page']) { |
| 423 | const f = out[key]; |
| 424 | const w = key === 'data' ? 'the memory' : 'the page'; |
| 425 | check(R(f, 'underCap').ok, w + ' under its ceiling is written', R(f, 'underCap').msg); |
| 426 | check(!R(f, 'overCap').ok, w + ' over it is refused at the daimon\'s door', |
| 427 | R(f, 'overCap').msg); |
| 428 | check(says[key](R(f, 'overCap').msg), 'and the refusal ' + said[key], |
| 429 | R(f, 'overCap').msg); |
| 430 | check(R(f, 'afterRefusal').ok, 'and the refused bytes did not reach the file'); |
| 431 | |
| 432 | check(R(f, 'versionBig').ok, 'a version snapshot of ' + w + ' is not measured against it', |
| 433 | R(f, 'versionBig').msg); |
| 434 | check(R(f, 'ordinaryBig').ok, 'nor is an ordinary file of the same kind beside it', |
| 435 | R(f, 'ordinaryBig').msg); |
| 436 | check(R(f, 'nestedBig').ok, 'nor is one a folder deeper, which is not the crystal at all', |
| 437 | R(f, 'nestedBig').msg); |
| 438 | |
| 439 | check(!R(f, 'storeOver').ok, 'a hand edit of ' + w + ' over the ceiling is refused at the store\'s door', |
| 440 | R(f, 'storeOver').msg); |
| 441 | check(says[key](R(f, 'storeOver').msg), 'and that refusal ' + said[key], |
| 442 | R(f, 'storeOver').msg); |
| 443 | check(R(f, 'storeUnder').ok, 'a hand edit under it is written', R(f, 'storeUnder').msg); |
| 444 | |
| 445 | check(R(f, 'editSeed').ok, w + ' small enough to edit is in place', R(f, 'editSeed').msg); |
| 446 | check(R(f, 'editUnder').ok, 'an edit that keeps it under the ceiling is written', |
| 447 | R(f, 'editUnder').msg); |
| 448 | check(!R(f, 'editOver').ok, 'an edit that would push it over is refused at the edit door', |
| 449 | R(f, 'editOver').msg); |
| 450 | check(says[key](R(f, 'editOver').msg), 'and that refusal ' + said[key], |
| 451 | R(f, 'editOver').msg); |
| 452 | // The one that matters most: the old failure was not a permitted write, it was |
| 453 | // a write that landed and then killed the turn, leaving bytes nothing recorded. |
| 454 | check(R(f, 'afterEditRefusal').ok, |
| 455 | 'and the refused bytes never reached the file, so nothing oversized is left unsnapshotted'); |
| 456 | check(R(f, 'editShrink').ok, 'an edit may still make an oversized ' + w + ' SMALLER', |
| 457 | R(f, 'editShrink').msg); |
| 458 | |
| 459 | check(R(f, 'seeded').ok, w + ' can be seeded past the ceiling with the ceiling lifted', |
| 460 | R(f, 'seeded').msg); |
| 461 | check(R(f, 'shrinkToward').ok, 'and edited SMALLER while still over', R(f, 'shrinkToward').msg); |
| 462 | check(R(f, 'shrinkUnder').ok, 'and all the way under', R(f, 'shrinkUnder').msg); |
| 463 | check(!R(f, 'growAgain').ok, 'but not grown again once it is under', R(f, 'growAgain').msg); |
| 464 | } |
| 465 | |
| 466 | // ── The two settings are two settings ──────────────────────── |
| 467 | check(R(out, 'pageSetterLeftData').ok, |
| 468 | 'lowering the PAGE ceiling leaves the memory\'s where it was', |
| 469 | R(out, 'pageSetterLeftData').msg); |
| 470 | check(R(out, 'dataSetterLeftPage').ok, |
| 471 | 'and lowering the memory\'s leaves the PAGE\'s where it was', |
| 472 | R(out, 'dataSetterLeftPage').msg); |
| 473 | |
| 474 | } finally { |
| 475 | await s.close(); |
| 476 | } |
| 477 | |
| 478 | console.log(bad === 0 ? '\nall checks passed' : `\n${bad} check(s) FAILED`); |
| 479 | process.exit(bad === 0 ? 0 : 1); |