oxedyne/daimond/dev/verify_droots.mjs
21.8 KiB, 1 run
created by r2519314175:385, 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_droots.mjs — a Diamond is the same Diamond in either workspace mode. |
| 2 | // |
| 3 | // The Workspace panel switches the agent's root between the browser sandbox |
| 4 | // (OPFS, private, syncs) and a real folder on the machine (does not sync). That |
| 5 | // switch is meant to move the WORK. It was moving Daimond's own state with it: |
| 6 | // a Diamond lives at `diamonds/<id>`, the store pinned that to the sandbox, and |
| 7 | // every other reader of the same path — the Workspace panel, a dispatched |
| 8 | // worker, the "view this delta" button — resolved it against whichever root was |
| 9 | // active. So with a folder open the Diamond's own directory could not be listed |
| 10 | // or read at all, and a worker's files went into the user's repository, outside |
| 11 | // the sync parcel, where nothing would ever see them again. |
| 12 | // |
| 13 | // The rule now is one line: `diamonds/` is app state and does not follow the |
| 14 | // root (`is_store_path`, src/tools.rs; `resolve_root`, src/wasm/opfs.rs). The |
| 15 | // user's work still does, which is half of what is checked here — a fix that |
| 16 | // pinned everything to the sandbox would break real-folder mode outright and |
| 17 | // several checks below would still pass. |
| 18 | // |
| 19 | // What is pinned: |
| 20 | // * a Diamond's directory lists and reads the same in both modes; |
| 21 | // * a write into it lands in the store and travels in the export; |
| 22 | // * the user's own files still follow the folder, and are out of view in the |
| 23 | // sandbox — the switch still does its job; |
| 24 | // * `diamonds-old/` and `src/diamonds/` are the user's, not the store; |
| 25 | // * Diamonds made in either mode survive a reload and appear in the parcel; |
| 26 | // * Diamond files an earlier build left in a folder are adopted into the |
| 27 | // store on activation, both copies are kept where they differ, a folder |
| 28 | // that is not a Diamond is left alone, nothing is deleted, and running it |
| 29 | // again adopts nothing. |
| 30 | // |
| 31 | // A real folder needs `showDirectoryPicker()`, a native dialog no harness can |
| 32 | // answer, so an OPFS subdirectory stands in for one — the same stand-in |
| 33 | // dev/verify_fsa.mjs uses, and for the same reason: what the picker returns is |
| 34 | // a FileSystemDirectoryHandle, and OPFS hands out that very type. |
| 35 | // |
| 36 | // Run with dev/serve.mjs (DAIMOND_PORT, default 8777) up. No gateway, no hand, no mock |
| 37 | // model. |
| 38 | // |
| 39 | // node dev/verify_droots.mjs |
| 40 | // |
| 41 | // Every check here has been proved against BROKEN code: see |
| 42 | // dev/breakproof_droots.sh, which reverts the fix four different ways and |
| 43 | // records which checks go red for each. |
| 44 | import { open, signInAs } from './harness.mjs'; |
| 45 | |
| 46 | const ok = [], bad = []; |
| 47 | const check = (name, pass, detail) => { |
| 48 | (pass ? ok : bad).push(name); |
| 49 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 50 | }; |
| 51 | |
| 52 | const FOLDER = 'standin-folder'; // an OPFS subdirectory standing in for a picked folder |
| 53 | |
| 54 | /// A crystal as `crystal.json` now holds one. Nothing here is about the crystal's |
| 55 | /// shape -- these fixtures need a Diamond to be carrying something they can tell |
| 56 | /// apart -- but it has to be the real format, because the store parses it. |
| 57 | const crystalOf = (title) => JSON.stringify({ title: title }); |
| 58 | |
| 59 | const s = await open({ name: 'droots', connect: false }); |
| 60 | const p = s.page; |
| 61 | await p.waitForTimeout(1500); |
| 62 | |
| 63 | // Helpers installed in the page, so every step below drives the real engine. |
| 64 | // Re-installed after the reload, which takes the page's globals with it. |
| 65 | const install = () => p.evaluate(async (folder) => { |
| 66 | const mod = await import('../pkg/oxedyne_daimond.js'); |
| 67 | window.__d = { |
| 68 | mod, |
| 69 | app: new mod.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 256, '', true), |
| 70 | root: await navigator.storage.getDirectory(), |
| 71 | folder: null, |
| 72 | }; |
| 73 | __d.folder = await __d.root.getDirectoryHandle(folder, { create: true }); |
| 74 | // Read a file straight out of a directory handle, bypassing every tool: the |
| 75 | // question "where did the bytes land" must not be answered by the thing under test. |
| 76 | window.__at = async (which, path) => { |
| 77 | let cur = which === 'folder' ? __d.folder : __d.root; |
| 78 | const parts = path.split('/'); |
| 79 | try { |
| 80 | for (let i = 0; i < parts.length - 1; i++) cur = await cur.getDirectoryHandle(parts[i]); |
| 81 | const fh = await cur.getFileHandle(parts[parts.length - 1]); |
| 82 | return await (await fh.getFile()).text(); |
| 83 | } catch (e) { return null; } |
| 84 | }; |
| 85 | window.__put = async (which, path, body) => { |
| 86 | let cur = which === 'folder' ? __d.folder : __d.root; |
| 87 | const parts = path.split('/'); |
| 88 | for (let i = 0; i < parts.length - 1; i++) { |
| 89 | cur = await cur.getDirectoryHandle(parts[i], { create: true }); |
| 90 | } |
| 91 | const fh = await cur.getFileHandle(parts[parts.length - 1], { create: true }); |
| 92 | const w = await fh.createWritable(); |
| 93 | await w.write(body); |
| 94 | await w.close(); |
| 95 | }; |
| 96 | window.__tool = (name, args) => __d.app.run_tool(name, JSON.stringify(args)); |
| 97 | window.__err = (v) => typeof v !== 'string' || /^\s*Error\b/i.test(v); |
| 98 | }, FOLDER); |
| 99 | |
| 100 | await install(); |
| 101 | |
| 102 | const tool = (name, args) => p.evaluate(([n, a]) => __tool(n, a), [name, args]); |
| 103 | const at = (which, path) => p.evaluate(([w, x]) => __at(w, x), [which, path]); |
| 104 | const put = (which, path, body) => p.evaluate(([w, x, b]) => __put(w, x, b), [which, path, body]); |
| 105 | const toFolder = () => p.evaluate(() => __d.mod.set_workspace_dir(__d.folder)); |
| 106 | const toBrowser = () => p.evaluate(() => __d.mod.use_opfs_workspace()); |
| 107 | |
| 108 | // ── A Diamond made in the sandbox ─────────────────────────────────────── |
| 109 | |
| 110 | const A = await p.evaluate(async (crystal) => { |
| 111 | const id = await __d.app.create_diamond('Made in the browser'); |
| 112 | await __d.app.write_crystal_data(id, crystal); |
| 113 | await __tool('file_write', { path: 'diamonds/' + id + '/note.md', content: 'note of A' }); |
| 114 | return id; |
| 115 | }, crystalOf('crystal of A')); |
| 116 | check('a Diamond can be made in the browser sandbox', !!A, A); |
| 117 | |
| 118 | const readsIn = async (id, what) => ({ |
| 119 | list: await tool('file_list', { path: 'diamonds/' + id }), |
| 120 | crystal: await tool('file_read', { path: 'diamonds/' + id + '/crystal.json' }), |
| 121 | note: await tool('file_read', { path: 'diamonds/' + id + '/note.md' }), |
| 122 | what, |
| 123 | }); |
| 124 | |
| 125 | const browserA = await readsIn(A, 'browser'); |
| 126 | check('its directory lists in the browser sandbox', |
| 127 | /crystal\.json/.test(browserA.list) && /note\.md/.test(browserA.list), browserA.list); |
| 128 | check('and its crystal reads there', /crystal of A/.test(browserA.crystal), browserA.crystal); |
| 129 | |
| 130 | // ── The switch to the machine folder ──────────────────────────────────── |
| 131 | |
| 132 | await toFolder(); |
| 133 | const machineA = await readsIn(A, 'machine'); |
| 134 | check('the SAME directory still lists once a folder is open', |
| 135 | /crystal\.json/.test(machineA.list) && /note\.md/.test(machineA.list), machineA.list); |
| 136 | check('and the crystal reads back byte for byte', |
| 137 | /crystal of A/.test(machineA.crystal), machineA.crystal); |
| 138 | check('and so does a file written into it before the switch', |
| 139 | /note of A/.test(machineA.note), machineA.note); |
| 140 | |
| 141 | // A write made WHILE the folder is open belongs to the Diamond, not to the repo. |
| 142 | await tool('file_write', { path: 'diamonds/' + A + '/worker.md', content: 'a worker wrote this' }); |
| 143 | check('a write into the Diamond lands in the store, not the folder', |
| 144 | (await at('opfs', 'diamonds/' + A + '/worker.md')) === 'a worker wrote this' |
| 145 | && (await at('folder', 'diamonds/' + A + '/worker.md')) === null, |
| 146 | 'store: ' + JSON.stringify(await at('opfs', 'diamonds/' + A + '/worker.md')) |
| 147 | + ' folder: ' + JSON.stringify(await at('folder', 'diamonds/' + A + '/worker.md'))); |
| 148 | |
| 149 | const exportA = await p.evaluate((id) => __d.app.export_diamond(id), A); |
| 150 | check('and it travels with the Diamond, so it reaches the other device', |
| 151 | exportA.includes('worker.md') && exportA.includes('a worker wrote this'), |
| 152 | exportA.slice(0, 160)); |
| 153 | |
| 154 | // ── The user's work still follows the folder ──────────────────────────── |
| 155 | // |
| 156 | // Without this the fix could be "pin everything to the sandbox", which passes |
| 157 | // every check above and destroys the feature. |
| 158 | |
| 159 | await tool('file_write', { path: 'src/main.rs', content: 'fn main() {}' }); |
| 160 | check("the user's own file goes into the folder, as real-folder mode promises", |
| 161 | (await at('folder', 'src/main.rs')) === 'fn main() {}' |
| 162 | && (await at('opfs', 'src/main.rs')) === null, |
| 163 | 'folder: ' + JSON.stringify(await at('folder', 'src/main.rs'))); |
| 164 | |
| 165 | // Paths that merely look like the store are the user's. |
| 166 | await tool('file_write', { path: 'diamonds-old/keep.md', content: 'theirs' }); |
| 167 | await tool('file_write', { path: 'src/diamonds/keep.md', content: 'also theirs' }); |
| 168 | check('a path that only resembles the store is still the user\'s work', |
| 169 | (await at('folder', 'diamonds-old/keep.md')) === 'theirs' |
| 170 | && (await at('folder', 'src/diamonds/keep.md')) === 'also theirs' |
| 171 | && (await at('opfs', 'diamonds-old/keep.md')) === null |
| 172 | && (await at('opfs', 'src/diamonds/keep.md')) === null, |
| 173 | 'diamonds-old in folder: ' + JSON.stringify(await at('folder', 'diamonds-old/keep.md')) |
| 174 | + ', in store: ' + JSON.stringify(await at('opfs', 'diamonds-old/keep.md'))); |
| 175 | |
| 176 | // ── A Diamond made while the folder is open ───────────────────────────── |
| 177 | |
| 178 | const B = await p.evaluate(async (crystal) => { |
| 179 | const id = await __d.app.create_diamond('Made on the machine'); |
| 180 | await __d.app.write_crystal_data(id, crystal); |
| 181 | await __tool('file_write', { path: 'diamonds/' + id + '/note.md', content: 'note of B' }); |
| 182 | return id; |
| 183 | }, crystalOf('crystal of B')); |
| 184 | check('a Diamond can be made while a folder is open', !!B, B); |
| 185 | check('and none of it lands in the folder', |
| 186 | (await at('folder', 'diamonds/' + B + '/crystal.json')) === null, 'folder holds nothing for B'); |
| 187 | |
| 188 | await toBrowser(); |
| 189 | const browserB = await readsIn(B, 'browser'); |
| 190 | check('switching back to the browser, it is still there', |
| 191 | /crystal\.json/.test(browserB.list) && /note\.md/.test(browserB.list), browserB.list); |
| 192 | check('with its crystal and its files intact', |
| 193 | /crystal of B/.test(browserB.crystal) && /note of B/.test(browserB.note), |
| 194 | browserB.crystal + ' | ' + browserB.note); |
| 195 | check("and the folder's own files are out of view again", |
| 196 | (await tool('file_read', { path: 'src/main.rs' })).startsWith('Error'), |
| 197 | (await tool('file_read', { path: 'src/main.rs' })).slice(0, 40)); |
| 198 | |
| 199 | // ── Both are in the sync parcel ───────────────────────────────────────── |
| 200 | |
| 201 | const inParcel = async () => p.evaluate(async () => { |
| 202 | const parcel = await window.DaimondCore.collectSync(); |
| 203 | return (parcel.diamonds || []).map((d) => ({ id: d.id, data: d.data || '' })); |
| 204 | }); |
| 205 | const parcelBrowser = await inParcel(); |
| 206 | const carries = (rows, id, body) => { |
| 207 | const row = rows.find((r) => r.id === id); |
| 208 | return !!row && row.data.includes(body); |
| 209 | }; |
| 210 | check('both Diamonds are in the parcel from the browser sandbox', |
| 211 | carries(parcelBrowser, A, 'crystal of A') && carries(parcelBrowser, B, 'crystal of B'), |
| 212 | parcelBrowser.map((r) => r.id).join(' ')); |
| 213 | |
| 214 | await toFolder(); |
| 215 | const parcelMachine = await inParcel(); |
| 216 | check('and both are still in the parcel with a folder open', |
| 217 | carries(parcelMachine, A, 'crystal of A') && carries(parcelMachine, B, 'crystal of B'), |
| 218 | parcelMachine.map((r) => r.id).join(' ')); |
| 219 | check('including the file the worker wrote while the folder was open', |
| 220 | carries(parcelMachine, A, 'a worker wrote this')); |
| 221 | |
| 222 | // ── Across a reload ───────────────────────────────────────────────────── |
| 223 | |
| 224 | await p.reload({ waitUntil: 'domcontentloaded' }); |
| 225 | await signInAs(s, 'droots'); |
| 226 | await p.waitForTimeout(2500); |
| 227 | await install(); |
| 228 | |
| 229 | const afterReload = await p.evaluate(async ({ a, b }) => { |
| 230 | const listed = JSON.parse(await __d.app.list_diamonds()); |
| 231 | const browser = { |
| 232 | a: await __tool('file_read', { path: 'diamonds/' + a + '/crystal.json' }), |
| 233 | b: await __tool('file_read', { path: 'diamonds/' + b + '/crystal.json' }), |
| 234 | }; |
| 235 | __d.mod.set_workspace_dir(__d.folder); |
| 236 | const machine = { |
| 237 | a: await __tool('file_read', { path: 'diamonds/' + a + '/crystal.json' }), |
| 238 | b: await __tool('file_read', { path: 'diamonds/' + b + '/crystal.json' }), |
| 239 | }; |
| 240 | __d.mod.use_opfs_workspace(); |
| 241 | return { ids: listed.map((d) => d.id), browser, machine }; |
| 242 | }, { a: A, b: B }); |
| 243 | |
| 244 | check('after a reload the rail still holds both Diamonds', |
| 245 | afterReload.ids.includes(A) && afterReload.ids.includes(B), afterReload.ids.join(' ')); |
| 246 | check('and both crystals read in the browser sandbox', |
| 247 | /crystal of A/.test(afterReload.browser.a) && /crystal of B/.test(afterReload.browser.b), |
| 248 | afterReload.browser.a + ' | ' + afterReload.browser.b); |
| 249 | check('and both read again with the folder open', |
| 250 | /crystal of A/.test(afterReload.machine.a) && /crystal of B/.test(afterReload.machine.b), |
| 251 | afterReload.machine.a + ' | ' + afterReload.machine.b); |
| 252 | |
| 253 | // ── Bringing home what an earlier build left in a folder ──────────────── |
| 254 | // |
| 255 | // The state a user of yesterday's build is actually in: Diamond files sitting |
| 256 | // in their project, invisible to the app and outside the parcel. |
| 257 | |
| 258 | const C = 'c0ffee5eaded'; // a whole Diamond, in the folder and nowhere else |
| 259 | await p.evaluate(async ({ a, c, crystalC }) => { |
| 260 | // (1) A worker's file for a Diamond the store already holds. |
| 261 | await __put('folder', 'diamonds/' + a + '/stranded.md', 'left in the folder'); |
| 262 | // (2) A file that CLASHES with one the store holds, with different bytes. |
| 263 | await __put('folder', 'diamonds/' + a + '/note.md', 'the folder version of the note'); |
| 264 | // (3) A whole Diamond that only exists in the folder. |
| 265 | await __put('folder', 'diamonds/' + c + '/crystal.json', crystalC); |
| 266 | await __put('folder', 'diamonds/' + c + '/.daimond/meta.json', |
| 267 | JSON.stringify({ name: 'Found in the folder', crystal_version: 0, updated: 1, touched: 1 })); |
| 268 | // (4) A directory of the user's that happens to live under diamonds/. |
| 269 | await __put('folder', 'diamonds/notes/todo.md', 'not a Diamond at all'); |
| 270 | }, { a: A, c: C, crystalC: crystalOf('crystal of C') }); |
| 271 | |
| 272 | // Adoption runs where the app runs it: on folder activation. |
| 273 | const report = await p.evaluate(async () => { |
| 274 | __d.mod.set_workspace_dir(__d.folder); |
| 275 | return JSON.parse(await __d.mod.adopt_folder_diamonds()); |
| 276 | }); |
| 277 | |
| 278 | check('adoption reports the Diamonds it brought home', |
| 279 | report.adopted.length === 2 |
| 280 | && report.adopted.some((d) => d.id === A) |
| 281 | && report.adopted.some((d) => d.id === C), |
| 282 | JSON.stringify(report.adopted)); |
| 283 | check('a stranded worker file is now in the store', |
| 284 | (await at('opfs', 'diamonds/' + A + '/stranded.md')) === 'left in the folder', |
| 285 | JSON.stringify(await at('opfs', 'diamonds/' + A + '/stranded.md'))); |
| 286 | check('a clash keeps what the store already had', |
| 287 | (await at('opfs', 'diamonds/' + A + '/note.md')) === 'note of A', |
| 288 | JSON.stringify(await at('opfs', 'diamonds/' + A + '/note.md'))); |
| 289 | check('and keeps the folder\'s copy beside it rather than dropping it', |
| 290 | (await at('opfs', 'diamonds/' + A + '/note.from-machine.md')) === 'the folder version of the note', |
| 291 | JSON.stringify(await at('opfs', 'diamonds/' + A + '/note.from-machine.md'))); |
| 292 | check('and says so, naming the file it kept', |
| 293 | (report.adopted.find((d) => d.id === A) || {}).kept |
| 294 | && report.adopted.find((d) => d.id === A).kept |
| 295 | .includes('diamonds/' + A + '/note.from-machine.md'), |
| 296 | JSON.stringify((report.adopted.find((d) => d.id === A) || {}).kept)); |
| 297 | |
| 298 | const adoptedC = await p.evaluate(async (c) => { |
| 299 | const listed = JSON.parse(await __d.app.list_diamonds()); |
| 300 | const row = listed.find((d) => d.id === c) || null; |
| 301 | return { row, crystal: await __d.app.read_crystal_data(c).catch(() => null) }; |
| 302 | }, C); |
| 303 | check('a whole Diamond found only in the folder joins the rail', |
| 304 | !!adoptedC.row && adoptedC.row.name === 'Found in the folder', JSON.stringify(adoptedC.row)); |
| 305 | check('with its crystal', adoptedC.crystal === crystalOf('crystal of C'), |
| 306 | JSON.stringify(adoptedC.crystal)); |
| 307 | |
| 308 | check('a folder of the user\'s under diamonds/ is left alone', |
| 309 | report.left.includes('diamonds/notes') |
| 310 | && (await at('opfs', 'diamonds/notes/todo.md')) === null, |
| 311 | JSON.stringify(report.left)); |
| 312 | check('and nothing at all was deleted from the folder', |
| 313 | (await at('folder', 'diamonds/' + A + '/stranded.md')) === 'left in the folder' |
| 314 | && (await at('folder', 'diamonds/' + C + '/crystal.json')) === crystalOf('crystal of C') |
| 315 | && (await at('folder', 'diamonds/notes/todo.md')) === 'not a Diamond at all'); |
| 316 | |
| 317 | const second = await p.evaluate(() => __d.mod.adopt_folder_diamonds().then(JSON.parse)); |
| 318 | check('running adoption again brings nothing home, and makes no second copy', |
| 319 | second.adopted.length === 0 |
| 320 | && (await at('opfs', 'diamonds/' + A + '/note.from-machine.from-machine.md')) === null, |
| 321 | JSON.stringify(second.adopted)); |
| 322 | |
| 323 | // The adopted Diamond has to sync like any other, which is the point of moving it. |
| 324 | await toBrowser(); |
| 325 | const parcelAfter = await inParcel(); |
| 326 | check('the adopted Diamond is in the sync parcel', |
| 327 | carries(parcelAfter, C, 'crystal of C') && carries(parcelAfter, A, 'left in the folder'), |
| 328 | parcelAfter.map((r) => r.id).join(' ')); |
| 329 | |
| 330 | // With no folder open there is nothing to adopt, and saying so is not an error. |
| 331 | const none = await p.evaluate(() => __d.mod.adopt_folder_diamonds().then(JSON.parse)); |
| 332 | check('with no folder open, adoption looks at nothing and says so', |
| 333 | none.folder === false && none.adopted.length === 0, JSON.stringify(none)); |
| 334 | |
| 335 | // ── And through the app's own hands, not the engine's ─────────────────── |
| 336 | // |
| 337 | // Everything above called the adoption edge directly. What a user of yesterday's |
| 338 | // build actually does is open the app with a folder already granted, so the run |
| 339 | // that matters is the silent reconnect at boot — `Files.tryReconnect` → |
| 340 | // `activateFolder` → adoption — and the user has to be TOLD, because files moved. |
| 341 | |
| 342 | const D = 'dead1eaf0000'; |
| 343 | await p.evaluate(async ({ folder, d, crystalD }) => { |
| 344 | // A whole Diamond, in the folder and nowhere else. |
| 345 | await __put('folder', 'diamonds/' + d + '/crystal.json', crystalD); |
| 346 | await __put('folder', 'diamonds/' + d + '/.daimond/meta.json', |
| 347 | JSON.stringify({ name: 'Stranded in the project', crystal_version: 0, updated: 2, touched: 2 })); |
| 348 | // Where the panel looks for a folder it may reconnect without a picker. |
| 349 | const db = await new Promise((res, rej) => { |
| 350 | const q = indexedDB.open('daimond-fsa', 1); |
| 351 | q.onupgradeneeded = () => q.result.createObjectStore('handles'); |
| 352 | q.onsuccess = () => res(q.result); |
| 353 | q.onerror = () => rej(q.error); |
| 354 | }); |
| 355 | await new Promise((res, rej) => { |
| 356 | const tx = db.transaction('handles', 'readwrite'); |
| 357 | tx.objectStore('handles').put(__d.folder, 'workspace'); |
| 358 | tx.oncomplete = res; |
| 359 | tx.onerror = () => rej(tx.error); |
| 360 | }); |
| 361 | void folder; |
| 362 | }, { folder: FOLDER, d: D, crystalD: crystalOf('crystal of D') }); |
| 363 | |
| 364 | await p.reload({ waitUntil: 'domcontentloaded' }); |
| 365 | await signInAs(s, 'droots'); |
| 366 | await p.waitForTimeout(4000); |
| 367 | await install(); |
| 368 | |
| 369 | const booted = await p.evaluate(async (d) => { |
| 370 | const mod = await import('../pkg/oxedyne_daimond.js'); |
| 371 | const app = new mod.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 256, '', true); |
| 372 | const card = document.querySelector('.modal.dlg .dlg-card'); |
| 373 | return { |
| 374 | mode: mod.workspace_mode(), |
| 375 | ids: JSON.parse(await app.list_diamonds()).map((x) => x.id), |
| 376 | crystal: await app.read_crystal_data(d).catch(() => null), |
| 377 | title: card ? (card.querySelector('h2') || {}).textContent : null, |
| 378 | body: card ? (card.querySelector('.dlg-msg, .dlg-pre') || {}).textContent : null, |
| 379 | }; |
| 380 | }, D); |
| 381 | |
| 382 | check('the app reconnected the folder on its own at boot', booted.mode === 'folder', booted.mode); |
| 383 | check('and brought the stranded Diamond into the store on the way', |
| 384 | booted.ids.includes(D) && booted.crystal === crystalOf('crystal of D'), |
| 385 | booted.ids.join(' ') + ' | ' + JSON.stringify(booted.crystal)); |
| 386 | check('and told the user, naming the Diamond it moved', |
| 387 | !!booted.title && /brought back/i.test(booted.title) |
| 388 | && !!booted.body && booted.body.includes('Stranded in the project'), |
| 389 | JSON.stringify(booted.title) + ' | ' + JSON.stringify(booted.body)); |
| 390 | // THE PROPERTY. Adoption COPIES; the user's own files stay in their folder, |
| 391 | // and the dialog invites them to delete those copies once satisfied. So the |
| 392 | // message must say the folder copies are still there and where to find them, |
| 393 | // and must never claim Daimond moved or removed anything -- and the file must |
| 394 | // really still be on disk. A user who reads "moved" does not go looking, and a |
| 395 | // user who reads "copied" about files that were not copied deletes the only |
| 396 | // remaining set. |
| 397 | // |
| 398 | // This was `/left exactly where they are/`, which the rewrite broke by dropping |
| 399 | // one adverb. The wording is not the property; the file still being there is. |
| 400 | const stillInTheFolder = (b) => { |
| 401 | const bits = b.split(/(?<=[.!?;])\s+/); |
| 402 | const neg = /\b(not|no|never|nothing|none|nor)\b|n[’']t\b/i; |
| 403 | const gone = /\b(moved|removed|deleted|erased|taken away)\b/i; |
| 404 | return { |
| 405 | // It says the user's copies stayed put. |
| 406 | kept: bits.some(x => /\b(left|still|remains?|remained|untouched|unchanged|kept|in place|where they (are|were))\b/i.test(x) |
| 407 | && /\b(cop(y|ies|ied)|files?|originals?|them|these)\b/i.test(x)), |
| 408 | // It says where they are. |
| 409 | where: /diamonds\/|\bfolder\b/i.test(b), |
| 410 | // It never claims Daimond took them away. |
| 411 | notMoved: !bits.some(x => gone.test(x) && !neg.test(x)), |
| 412 | }; |
| 413 | }; |
| 414 | const told = stillInTheFolder(booted.body || ''); |
| 415 | const onDisk = (await at('folder', 'diamonds/' + D + '/crystal.json')) === crystalOf('crystal of D'); |
| 416 | check('and said the copies in their folder are still there, untaken — and they are', |
| 417 | !!booted.body && told.kept && told.where && told.notMoved && onDisk, |
| 418 | Object.entries({ ...told, onDisk }).filter(([, v]) => !v).map(([k]) => 'no ' + k).join(', ') |
| 419 | || JSON.stringify(booted.body)); |
| 420 | |
| 421 | // A resource the browser could not load is the dev stack, not the page: no |
| 422 | // gateway runs here, so its probes answer 401 or 502 and neither is a throw. |
| 423 | const noise = s.errs.filter((e) => |
| 424 | !/favicon|ERR_ABORTED|net::ERR|Failed to load resource/i.test(e)); |
| 425 | check('the page threw nothing along the way', noise.length === 0, noise.slice(0, 3).join(' | ')); |
| 426 | |
| 427 | await s.close(); |
| 428 | console.log('\n' + ok.length + ' ok, ' + bad.length + ' failed'); |
| 429 | process.exit(bad.length ? 1 : 0); |