oxedyne/daimond/dev/verify_docroundtrip.mjs
5.7 KiB, 1 run
created by r2519314175:375, 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_docroundtrip.mjs — the Doc panel shows the FILE, and saving it changes nothing. |
| 2 | // |
| 3 | // `file_read` is a tool that renders a file FOR A MODEL. It numbers every line |
| 4 | // (`1\t`), it says so when it truncates, and it explains itself when the bytes |
| 5 | // are in cloud storage. All three are right for an agent and none of them is the |
| 6 | // file. The viewer read the file that way and showed the result as the document. |
| 7 | // |
| 8 | // Two consequences, and the second is data loss: |
| 9 | // |
| 10 | // 1. With the line-number toggle on, the reader saw TWO columns of numbers -- |
| 11 | // the tool's, baked into the text, and the viewer's own gutter beside it. |
| 12 | // 2. The editor was seeded with the same string, so OPENING A FILE AND PRESSING |
| 13 | // SAVE WITHOUT TYPING ANYTHING wrote the line numbers into it, and did it |
| 14 | // again over the top on every repeat. A truncated read would have written |
| 15 | // the truncation the same way. |
| 16 | // |
| 17 | // So the property is not "the viewer looks right". It is that **a round trip |
| 18 | // through the viewer is the identity function**, which is the thing a person |
| 19 | // betting their files on this app needs to be true. |
| 20 | // |
| 21 | // node dev/verify_docroundtrip.mjs |
| 22 | // |
| 23 | // Needs dev/serve.mjs (DAIMOND_PORT, default 8777). No gateway, no mock LLM: nothing |
| 24 | // here runs a turn. |
| 25 | import fs from 'node:fs'; |
| 26 | import { open, scratch } from './harness.mjs'; |
| 27 | |
| 28 | const PROFILE = scratch('pw', 'docroundtrip'); |
| 29 | fs.rmSync(PROFILE, { recursive: true, force: true }); |
| 30 | |
| 31 | let bad = 0; |
| 32 | const check = (pass, name, detail) => { |
| 33 | if (!pass) bad++; |
| 34 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 35 | }; |
| 36 | |
| 37 | const BODY = 'alpha\nbeta\ngamma\na line long enough that it wraps inside the panel and shows where a continuation goes\n'; |
| 38 | |
| 39 | const s = await open({ name: 'docroundtrip', profile: PROFILE, connect: false }); |
| 40 | const { page } = s; |
| 41 | |
| 42 | try { |
| 43 | await page.waitForTimeout(1500); |
| 44 | |
| 45 | /// The bytes as they actually sit in the account's OPFS, read past the app. |
| 46 | const raw = () => page.evaluate(async () => { |
| 47 | const dir = await DaimondCloud.opfsRoot(); |
| 48 | return await (await (await dir.getFileHandle('round.md')).getFile()).text(); |
| 49 | }); |
| 50 | |
| 51 | await page.evaluate(async (body) => { |
| 52 | const mod = await import('../pkg/oxedyne_daimond.js'); |
| 53 | const app = new mod.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 256, '', true); |
| 54 | await app.run_tool('file_write', JSON.stringify({ path: 'round.md', content: body })); |
| 55 | }, BODY); |
| 56 | check(await raw() === BODY, 'the fixture is on disk as written'); |
| 57 | |
| 58 | // Open it the way a person does, from the tree. `Files` is closure-scoped and |
| 59 | // not reachable from here, so the row is the door -- and the tree's listing |
| 60 | // races the write that made the file, so the panel is re-opened until the row |
| 61 | // is there rather than once with a hopeful pause. A verifier that flakes on |
| 62 | // its own fixture says nothing about the property it exists for. |
| 63 | let row = null; |
| 64 | for (let i = 0; i < 10 && !row; i++) { |
| 65 | await page.evaluate(() => { |
| 66 | try { DaimondPanels.hide('work'); DaimondPanels.show('work'); } catch (e) {} |
| 67 | }); |
| 68 | await page.waitForTimeout(600); |
| 69 | row = await page.$('text=round.md'); |
| 70 | } |
| 71 | check(!!row, 'the file is listed in the Workspace tree'); |
| 72 | if (!row) throw new Error('fixture never appeared in the tree'); |
| 73 | await row.click(); |
| 74 | await page.waitForTimeout(1400); |
| 75 | |
| 76 | // ── 1. What is on screen is the file ──────────────────────────── |
| 77 | const shown = await page.evaluate(() => { |
| 78 | const b = document.querySelector('.files-view-body'); |
| 79 | return b ? b.textContent : ''; |
| 80 | }); |
| 81 | check(shown.indexOf('alpha') !== -1, 'the document is on screen', JSON.stringify(shown.slice(0, 40))); |
| 82 | check(!/(^|\n)\s*\d+\t/.test(shown), |
| 83 | 'and carries none of file_read\'s line-number prefixes', |
| 84 | JSON.stringify(shown.slice(0, 60))); |
| 85 | |
| 86 | // ── 2. One gutter, not two ────────────────────────────────────── |
| 87 | const btn = await page.$('#doc-lineno'); |
| 88 | check(!!btn && await btn.isVisible(), 'the line-number toggle is offered over a text file'); |
| 89 | if (btn && await btn.isVisible()) { |
| 90 | if (await btn.getAttribute('aria-pressed') !== 'true') { await btn.click(); await page.waitForTimeout(600); } |
| 91 | const rows = await page.evaluate(() => [...document.querySelectorAll('.lnrow')].slice(0, 3).map(r => ({ |
| 92 | gutter: (r.querySelector('.ln') || {}).textContent || '', |
| 93 | text: [...r.childNodes].filter(n => n.nodeType === 3).map(n => n.textContent).join(''), |
| 94 | }))); |
| 95 | check(rows.length >= 3, 'the numbered view renders a row per line', JSON.stringify(rows.length)); |
| 96 | check(rows.every(r => !/^\d+\t/.test(r.text)), |
| 97 | 'and each row\'s text is the line, not a second number', |
| 98 | JSON.stringify(rows)); |
| 99 | check(rows[0] && rows[0].gutter === '1' && rows[1] && rows[1].gutter === '2', |
| 100 | 'with the gutter counting from one', JSON.stringify(rows.map(r => r.gutter))); |
| 101 | } |
| 102 | |
| 103 | // ── 3. The round trip is the identity ─────────────────────────── |
| 104 | // Edit, then save, typing nothing. This is the check the whole file exists for. |
| 105 | await page.getByRole('button', { name: /Edit/ }).first().click(); |
| 106 | await page.waitForTimeout(600); |
| 107 | const seeded = await page.evaluate(() => { |
| 108 | const ta = document.querySelector('.files-edit'); |
| 109 | return ta ? ta.value : null; |
| 110 | }); |
| 111 | check(seeded === BODY, 'the editor is seeded with the file, byte for byte', |
| 112 | JSON.stringify((seeded || '').slice(0, 40))); |
| 113 | await page.getByRole('button', { name: /Save/ }).first().click(); |
| 114 | await page.waitForTimeout(1500); |
| 115 | const after = await raw(); |
| 116 | check(after === BODY, 'and a save that typed nothing changed nothing', |
| 117 | JSON.stringify(after.slice(0, 60))); |
| 118 | |
| 119 | } finally { |
| 120 | await s.close(); |
| 121 | } |
| 122 | |
| 123 | console.log(bad === 0 ? '\nall checks passed' : `\n${bad} check(s) FAILED`); |
| 124 | process.exit(bad === 0 ? 0 : 1); |