oxedyne/daimond/dev/verify_sharedbaseline.mjs
7.4 KiB, 1 run
created by r2519314175:677, 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_sharedbaseline.mjs — the sync fork point records only what was SHARED. |
| 2 | // |
| 3 | // The baseline is the state both devices agreed on, and `applyFiles` reads it as |
| 4 | // exactly that: a path in the baseline that a COMPLETE remote census does not |
| 5 | // carry has been deleted there, so it is deleted here. |
| 6 | // |
| 7 | // It was written on the way out of a PULL, from everything this device happened |
| 8 | // to be holding. A file created locally and never yet sent anywhere was |
| 9 | // therefore entered as "agreed" by a pull that carried no news of it — and the |
| 10 | // other device's next complete census, which had never heard of the file, |
| 11 | // deleted it. Nothing was wrong on either side; the record of agreement was a |
| 12 | // record of a conversation that never happened. |
| 13 | // |
| 14 | // A file cannot be agreed until it has been sent, and the moment it has is a |
| 15 | // successful push. That is what `commitFileBaseline` is for and where sync.js |
| 16 | // already calls it. |
| 17 | // |
| 18 | // The second check is the cloud index's fork point, which had the same shape of |
| 19 | // fault for a different reason: `applyFiles` runs before `applyChunked` in |
| 20 | // `applySync` and rewrites the cloud baseline on its way out, so the chunk merge |
| 21 | // compared local against ITSELF. `localChanged` was false for every path, the |
| 22 | // remote won unconditionally, and cloud.js's both-sides-diverged branch could |
| 23 | // never run. |
| 24 | // |
| 25 | // node dev/verify_sharedbaseline.mjs |
| 26 | // |
| 27 | // Needs dev/serve.mjs (DAIMOND_PORT, default 8777). No gateway: applySync is driven |
| 28 | // directly, which is what lets a "remote" parcel be fabricated with a known census. |
| 29 | import fs from 'node:fs'; |
| 30 | import { open, scratch } from './harness.mjs'; |
| 31 | |
| 32 | const PROFILE = scratch('pw', 'sharedbaseline'); |
| 33 | fs.rmSync(PROFILE, { recursive: true, force: true }); |
| 34 | |
| 35 | let bad = 0; |
| 36 | const check = (pass, name, detail) => { |
| 37 | if (!pass) bad++; |
| 38 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 39 | }; |
| 40 | |
| 41 | const s = await open({ name: 'sharedbaseline', profile: PROFILE, connect: false }); |
| 42 | const { page } = s; |
| 43 | |
| 44 | try { |
| 45 | await page.waitForFunction(() => !!(window.DaimondCore && DaimondCore.collectSync), |
| 46 | null, { timeout: 15000 }); |
| 47 | await page.waitForTimeout(900); |
| 48 | |
| 49 | const write = (path, content) => page.evaluate(async (a) => { |
| 50 | const mod = await import('../pkg/oxedyne_daimond.js'); |
| 51 | const app = new mod.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 256, '', true); |
| 52 | await app.run_tool('file_write', JSON.stringify({ path: a.path, content: a.content })); |
| 53 | }, { path, content }); |
| 54 | |
| 55 | const exists = (path) => page.evaluate(async (p) => { |
| 56 | try { |
| 57 | const dir = await DaimondCloud.opfsRoot(); |
| 58 | await dir.getFileHandle(p); |
| 59 | return true; |
| 60 | } catch (e) { return false; } |
| 61 | }, path); |
| 62 | |
| 63 | const baseline = () => page.evaluate(() => { |
| 64 | try { return JSON.parse(localStorage.getItem('daimond-sync-filebase') || '{}'); } |
| 65 | catch (e) { return {}; } |
| 66 | }); |
| 67 | |
| 68 | // ── The setup: one file both devices know, one only this device has ── |
| 69 | await write('shared.md', 'both devices have this\n'); |
| 70 | await write('mine-only.md', 'made here, never sent anywhere\n'); |
| 71 | await page.waitForTimeout(400); |
| 72 | |
| 73 | // A parcel from the other device that carries the shared file and, truthfully, |
| 74 | // has never heard of the local one. `filesComplete` is what entitles the |
| 75 | // receiver to delete by absence, so it is the whole point of the fixture. |
| 76 | const report = await page.evaluate(async () => { |
| 77 | const mine = await DaimondCore.collectSync(); |
| 78 | const remote = { |
| 79 | v: 2, chats: [], tombs: {}, msgTombs: {}, |
| 80 | files: { 'shared.md': mine.files['shared.md'] }, |
| 81 | filesComplete: true, |
| 82 | diamonds: [], diamondTombs: {}, chunked: {}, |
| 83 | }; |
| 84 | const r = await DaimondCore.applySync(remote); |
| 85 | return r && r.failed ? r.failed : []; |
| 86 | }); |
| 87 | check(report.length === 0, 'the fabricated parcel merges cleanly', report.join(',')); |
| 88 | |
| 89 | // ── 1. A file that never left must survive ────────────────────── |
| 90 | check(await exists('mine-only.md'), |
| 91 | 'a file this device has never sent survives a complete census that lacks it'); |
| 92 | check(await exists('shared.md'), 'and the file both devices hold is still here'); |
| 93 | |
| 94 | const base1 = await baseline(); |
| 95 | check(!Object.prototype.hasOwnProperty.call(base1, 'mine-only.md'), |
| 96 | 'and it is NOT recorded as agreed, having never been sent', |
| 97 | JSON.stringify(Object.keys(base1))); |
| 98 | check(Object.prototype.hasOwnProperty.call(base1, 'shared.md'), |
| 99 | 'while the shared file IS', JSON.stringify(Object.keys(base1))); |
| 100 | |
| 101 | // ── 2. A second round must not change its mind ────────────────── |
| 102 | // The bug deleted on the round AFTER the one that recorded the agreement, so |
| 103 | // one pull is not enough to prove anything. |
| 104 | await page.evaluate(async () => { |
| 105 | const mine = await DaimondCore.collectSync(); |
| 106 | await DaimondCore.applySync({ |
| 107 | v: 2, chats: [], tombs: {}, msgTombs: {}, |
| 108 | files: { 'shared.md': mine.files['shared.md'] }, |
| 109 | filesComplete: true, diamonds: [], diamondTombs: {}, chunked: {}, |
| 110 | }); |
| 111 | }); |
| 112 | await page.waitForTimeout(400); |
| 113 | check(await exists('mine-only.md'), |
| 114 | 'and it survives a SECOND complete census, which is when it used to go'); |
| 115 | |
| 116 | // ── 3. A real deletion is still honoured ──────────────────────── |
| 117 | // The fix must not turn the delete-by-absence rule off: a file that WAS |
| 118 | // shared, and that a complete census no longer carries, is still gone. |
| 119 | await page.evaluate(async () => { |
| 120 | await DaimondCore.applySync({ |
| 121 | v: 2, chats: [], tombs: {}, msgTombs: {}, |
| 122 | files: {}, filesComplete: true, diamonds: [], diamondTombs: {}, chunked: {}, |
| 123 | }); |
| 124 | }); |
| 125 | await page.waitForTimeout(400); |
| 126 | check(!(await exists('shared.md')), |
| 127 | 'a file that WAS shared and is now absent from a complete census is deleted'); |
| 128 | check(await exists('mine-only.md'), |
| 129 | 'and the never-sent file is still not collateral'); |
| 130 | const base2 = await baseline(); |
| 131 | check(!Object.prototype.hasOwnProperty.call(base2, 'shared.md'), |
| 132 | 'the deleted path leaves the fork point with it', JSON.stringify(Object.keys(base2))); |
| 133 | |
| 134 | // ── 4. The cloud fork point is not moved by a pull ────────────── |
| 135 | // `applyFiles` used to commit it on its way out, and `applyChunked` runs |
| 136 | // after it in `applySync` -- so the chunk merge compared this device's index |
| 137 | // against a copy of itself taken moments earlier. Every path read as |
| 138 | // "unchanged locally", the remote won unconditionally, and the branch that |
| 139 | // preserves a doubly-diverged file as `.synced` was unreachable. A pull must |
| 140 | // leave the fork point where the last successful PUSH put it. |
| 141 | // |
| 142 | // The key is `daimond-cloud-base`. This check first named it |
| 143 | // `daimond-sync-cloudbase`, which nothing in the app writes -- so it wrote a |
| 144 | // sentinel nobody could disturb and passed without testing anything. |
| 145 | await page.evaluate(() => { |
| 146 | localStorage.setItem('daimond-cloud-base', JSON.stringify({ 'sentinel.bin': 'from-the-last-push' })); |
| 147 | }); |
| 148 | await page.evaluate(async () => { |
| 149 | await DaimondCore.applySync({ |
| 150 | v: 2, chats: [], tombs: {}, msgTombs: {}, |
| 151 | files: {}, filesComplete: false, diamonds: [], diamondTombs: {}, chunked: {}, |
| 152 | }); |
| 153 | }); |
| 154 | await page.waitForTimeout(400); |
| 155 | const cloudBase = await page.evaluate(() => { |
| 156 | try { return JSON.parse(localStorage.getItem('daimond-cloud-base') || '{}'); } |
| 157 | catch (e) { return {}; } |
| 158 | }); |
| 159 | check(cloudBase['sentinel.bin'] === 'from-the-last-push', |
| 160 | 'a pull leaves the cloud fork point where the last push put it', |
| 161 | JSON.stringify(cloudBase)); |
| 162 | |
| 163 | } finally { |
| 164 | await s.close(); |
| 165 | } |
| 166 | |
| 167 | console.log(bad === 0 ? '\nall checks passed' : `\n${bad} check(s) FAILED`); |
| 168 | process.exit(bad === 0 ? 0 : 1); |