oxedyne/daimond/dev/verify_draftkeep.mjs
5.5 KiB, 1 run
created by r2519314175:379, 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 | // A DRAFT BELONGS TO THE DIAMOND, NOT TO THE FACE IT WAS TYPED ON. |
| 2 | // |
| 3 | // Reported by the owner, 2026-08-21, using the app: he began typing a reply to a |
| 4 | // daimon on the chat face, switched to the crystal face, and the text was gone. |
| 5 | // |
| 6 | // `moveComposerTo` guards the SAVE on the conversation actually changing -- |
| 7 | // "re-entering the conversation already on screen must not read the box back into |
| 8 | // itself" -- and then clobbers the box unconditionally on the next line: |
| 9 | // |
| 10 | // if (current && current !== next) current._draft = chatInput.value; |
| 11 | // chatInput.value = (next && next._draft) || ''; |
| 12 | // |
| 13 | // A Diamond's two faces share ONE conversation record, so a face switch has |
| 14 | // `current === next`: the save is skipped and the overwrite is not. The box is |
| 15 | // replaced by a stale `_draft`, empty for anyone who had not switched away before. |
| 16 | // The comment above the guard describes the intended behaviour correctly; the code |
| 17 | // implements half of it. |
| 18 | // |
| 19 | // The last two checks exist so the fix cannot be "never touch the box": moving |
| 20 | // between two DIFFERENT conversations must still put each draft back where it |
| 21 | // belongs, which is the feature the guard was protecting. |
| 22 | import { open, shot, errors } from './harness.mjs'; |
| 23 | |
| 24 | const BREAK = (() => { |
| 25 | const i = process.argv.indexOf('--break'); |
| 26 | return i > 0 ? process.argv[i + 1] : ''; |
| 27 | })(); |
| 28 | |
| 29 | let failed = 0; |
| 30 | const check = (ok, what, detail) => { |
| 31 | console.log(` ${ok ? 'ok ' : 'FAIL'} ${what}${detail ? ' — ' + detail : ''}`); |
| 32 | if (!ok) failed++; |
| 33 | }; |
| 34 | |
| 35 | const s = await open({ name: 'draftkeep' }); |
| 36 | try { |
| 37 | const p = s.page; |
| 38 | |
| 39 | // The break restores the shipped behaviour, so the checks below can be shown to |
| 40 | // redden on it. It patches the live function rather than a copy of the rule. |
| 41 | if (BREAK === 'wipe') { |
| 42 | await p.evaluate(() => { |
| 43 | const el = document.getElementById('chat-input'); |
| 44 | window.__wipe = () => { el.value = ''; }; |
| 45 | }); |
| 46 | } |
| 47 | |
| 48 | const type = async (text) => { |
| 49 | await p.fill('#chat-input', text); |
| 50 | await p.waitForTimeout(80); |
| 51 | }; |
| 52 | const composer = () => p.$eval('#chat-input', (el) => el.value); |
| 53 | const face = async (which) => { |
| 54 | await p.$eval('#dview-' + which, (el) => el.click()); |
| 55 | await p.waitForTimeout(400); |
| 56 | if (BREAK === 'wipe') await p.evaluate(() => window.__wipe && window.__wipe()); |
| 57 | }; |
| 58 | |
| 59 | // The rail's add button opens a naming dialog; the Diamond does not exist until |
| 60 | // it is confirmed, and `#chat-input` is not on screen before that. |
| 61 | await p.$eval('#new-diamond-btn', (el) => el.click()); |
| 62 | await p.waitForTimeout(800); |
| 63 | await p.fill('.dlg-card input', 'DraftKeep'); |
| 64 | await p.$eval('.dlg-card .dlg-ok', (el) => el.click()); |
| 65 | await p.waitForTimeout(1200); |
| 66 | |
| 67 | // ── 1. chat face → crystal face ────────────────────────────── |
| 68 | await face('chat'); |
| 69 | await type('half a sentence I have not finished'); |
| 70 | await face('crystal'); |
| 71 | check((await composer()) === 'half a sentence I have not finished', |
| 72 | 'TEXT TYPED ON THE CHAT FACE SURVIVES A SWITCH TO THE CRYSTAL FACE', |
| 73 | JSON.stringify(await composer())); |
| 74 | |
| 75 | // ── 2. and back again ──────────────────────────────────────── |
| 76 | await face('chat'); |
| 77 | check((await composer()) === 'half a sentence I have not finished', |
| 78 | 'and it is still there on the way back', |
| 79 | JSON.stringify(await composer())); |
| 80 | |
| 81 | // ── 3. crystal face → chat face ────────────────────────────── |
| 82 | await face('crystal'); |
| 83 | await type('a different unfinished thought'); |
| 84 | await face('chat'); |
| 85 | check((await composer()) === 'a different unfinished thought', |
| 86 | 'TEXT TYPED ON THE CRYSTAL FACE SURVIVES A SWITCH TO THE CHAT FACE', |
| 87 | JSON.stringify(await composer())); |
| 88 | |
| 89 | // ── 4. a real conversation change still swaps the draft ────── |
| 90 | // |
| 91 | // The guard exists for this. A fix that simply stopped touching the box would |
| 92 | // carry one conversation's draft into another, which is a worse bug than the |
| 93 | // one being fixed: it would put words the user wrote to a daimon into the box |
| 94 | // aimed at something else. |
| 95 | await p.$eval('#new-session-btn', (el) => el.click()); |
| 96 | await p.waitForTimeout(700); |
| 97 | check((await composer()) === '', |
| 98 | 'A DIFFERENT CONVERSATION DOES NOT INHERIT THE DIAMOND\'S DRAFT', |
| 99 | JSON.stringify(await composer())); |
| 100 | |
| 101 | const rail = await p.$$('#diamond-list > *'); |
| 102 | if (rail.length) { |
| 103 | await rail[0].evaluate((el) => el.click()); |
| 104 | await p.waitForTimeout(700); |
| 105 | check((await composer()) === 'a different unfinished thought', |
| 106 | 'and going back to the Diamond finds ITS draft, not the chat\'s', |
| 107 | JSON.stringify(await composer())); |
| 108 | } else { |
| 109 | check(false, 'and going back to the Diamond finds ITS draft', 'no tile to click'); |
| 110 | } |
| 111 | |
| 112 | await shot(s, 'draftkeep'); |
| 113 | // A 502 on /api/ means there is no gateway behind this world -- that is what a |
| 114 | // bad-gateway status IS -- and drafts are a page concern that needs none, so this |
| 115 | // verifier runs either way. While a gate holds :9002 there is no gateway to have. |
| 116 | // The exemption is narrow on purpose: only 502, only /api/. A 4xx or a 500 from |
| 117 | // the same path is a gateway answering badly rather than a gateway absent, and |
| 118 | // still fails, as does any other console error. |
| 119 | const errs = errors(s).filter((e) => |
| 120 | !(/\b502\b/.test(e) && /\/api\//.test(e))); |
| 121 | check(errs.length === 0, 'nothing threw, gateway-absent 502s aside', |
| 122 | errs.slice(0, 2).join(' | ')); |
| 123 | } finally { |
| 124 | await s.close(); |
| 125 | } |
| 126 | console.log(failed ? `\ndraftkeep: ${failed} check(s) FAILED` : '\ndraftkeep: all checks passed'); |
| 127 | process.exit(failed ? 1 : 0); |