oxedyne/daimond/dev/verify_settings.mjs
4.3 KiB, 1 run
created by r2519314175:673, 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 key the provider rejects must not be reported "Saved."/connected: the BYOK |
| 2 | // form fetches /models with the key it was given, and a 401 there has to reach |
| 3 | // the user as a refusal rather than as a saved setting. |
| 4 | // |
| 5 | // The file used to say it also proved "a no-account stranger must have a way to |
| 6 | // create one from the credits pitch". It never did -- nothing below opens the |
| 7 | // pitch -- and the sentence is gone rather than left standing as a claim the |
| 8 | // run does not make. |
| 9 | // |
| 10 | // PROVED AGAINST A KEY THE MOCK ACCEPTS FIRST. `--break accept` sends a key the |
| 11 | // mock does NOT 401, so the note reads as a save and the check below must go |
| 12 | // red. A check that has only ever seen the failing input cannot tell the two |
| 13 | // apart. |
| 14 | // |
| 15 | // node dev/verify_settings.mjs --break accept # expected to FAIL |
| 16 | // node dev/verify_settings.mjs # and then, clean |
| 17 | import { open, errors, MOCK } from './harness.mjs'; |
| 18 | |
| 19 | const ok = [], bad = []; |
| 20 | const check = (name, pass, detail) => { |
| 21 | (pass ? ok : bad).push(name + (detail ? ' — ' + detail : '')); |
| 22 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 23 | }; |
| 24 | |
| 25 | const BREAK = (() => { |
| 26 | const i = process.argv.indexOf('--break'); |
| 27 | return i > 0 ? String(process.argv[i + 1] || '') : ''; |
| 28 | })(); |
| 29 | // "reject" is the mock's sentinel: /v1/models 401s any key containing it (see |
| 30 | // dev/mockllm.mjs). Anything else is accepted. |
| 31 | const KEY = BREAK === 'accept' ? 'accept-me' : 'reject'; |
| 32 | |
| 33 | const s = await open({ name: 'settings', connect: false }); |
| 34 | |
| 35 | // Open settings and save a key the model-fetch will reject. |
| 36 | const result = await s.page.evaluate(async (arg) => { |
| 37 | const cog = document.getElementById('settings-btn'); |
| 38 | if (cog) cog.click(); |
| 39 | await new Promise(r => setTimeout(r, 300)); |
| 40 | const prov = document.getElementById('cfg-provider'); |
| 41 | if (!prov) return { missing: '#cfg-provider' }; |
| 42 | prov.value = 'custom'; prov.dispatchEvent(new Event('change', { bubbles: true })); |
| 43 | await new Promise(r => setTimeout(r, 200)); |
| 44 | const url = document.getElementById('cfg-base-url'); |
| 45 | const key = document.getElementById('cfg-api-key'); |
| 46 | const save = document.getElementById('byok-save'); |
| 47 | const note = document.getElementById('byok-note'); |
| 48 | if (!url || !key || !save || !note) { |
| 49 | return { missing: [['#cfg-base-url', url], ['#cfg-api-key', key], ['#byok-save', save], |
| 50 | ['#byok-note', note]].filter(p => !p[1]).map(p => p[0]).join(', ') }; |
| 51 | } |
| 52 | url.value = arg.mock; |
| 53 | url.dispatchEvent(new Event('input', { bubbles: true })); |
| 54 | url.dispatchEvent(new Event('change', { bubbles: true })); |
| 55 | key.value = arg.key; |
| 56 | key.dispatchEvent(new Event('input', { bubbles: true })); |
| 57 | key.dispatchEvent(new Event('change', { bubbles: true })); |
| 58 | await new Promise(r => setTimeout(r, 1200)); // let the model fetch run and fail |
| 59 | // Type a model by hand so validation passes and the key-rejection check is reached. |
| 60 | const cus = document.getElementById('cfg-model-custom'); |
| 61 | if (cus) { cus.style.display = ''; cus.value = 'mock/fast'; cus.dispatchEvent(new Event('input', { bubbles: true })); } |
| 62 | save.click(); |
| 63 | await new Promise(r => setTimeout(r, 400)); |
| 64 | return { note: note.textContent }; |
| 65 | }, { mock: MOCK, key: KEY }); |
| 66 | |
| 67 | // A form that has moved is not a passing test: the checks below all read the |
| 68 | // note, and a note that is not there would read as "no rejection" forever. |
| 69 | check('the BYOK form is on the screen the settings cog opens', |
| 70 | !result.missing, result.missing ? `missing ${result.missing}` : 'provider, base URL, key, save and note all present'); |
| 71 | |
| 72 | const note = String(result.note || ''); |
| 73 | check('A KEY THE PROVIDER REJECTS IS REPORTED AS REJECTED', |
| 74 | /rejected/i.test(note), `the note reads ${JSON.stringify(note)}`); |
| 75 | // The other half of the same claim: it must not ALSO say the thing was saved or |
| 76 | // connected, which is what the user acts on. |
| 77 | check('and is not reported saved or connected in the same breath', |
| 78 | !/\bsaved\b|\bconnected\b/i.test(note), `the note reads ${JSON.stringify(note)}`); |
| 79 | |
| 80 | // 502 is this world with no gateway behind it; 401 is the rejection this file |
| 81 | // exists to drive. |
| 82 | const errs = errors(s).filter(e => !/502|Bad Gateway|401|Unauthorized/.test(e)); |
| 83 | check('nothing threw', errs.length === 0, errs.slice(0, 2).join(' | ')); |
| 84 | |
| 85 | await s.close(); |
| 86 | console.log(`\n${ok.length} passed, ${bad.length} failed`); |
| 87 | if (bad.length) { bad.forEach(b => console.log(' FAILED: ' + b)); process.exit(1); } |