// The OpenRouter response shapes that broke the chatbot on 2026-09-17, and the ones // that must keep working. Runs the real parse branch from chatbot.js with a fake https // server, so this is the actual code path, not a copy of it. // // Run: node qa/chatbot-parse.mjs import http from 'node:http'; import https from 'node:https'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); const ok = [], bad = []; const t = (n, c, extra) => { (c ? ok : bad).push(n + (c || !extra ? '' : ' -> ' + extra)); }; let BODY = ''; const srv = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(BODY); }); await new Promise(r => srv.listen(0, '127.0.0.1', r)); const port = srv.address().port; // point chatbot.js's https.request at the local plain-http stub const realRequest = https.request; https.request = (opts, cb) => http.request({ ...opts, hostname: '127.0.0.1', port, protocol: 'http:', rejectUnauthorized: false }, cb); process.env.OPENROUTER_API_KEY = 'test-key'; const cb = require('../chatbot.js'); cb.init({ dataDir: '.', chain: null }); const good = JSON.stringify({ choices: [{ message: { content: 'Here is a straight answer.' } }] }); const ask = async q => (await cb.answer(q)).reply; const FAILMSG = /did not come back to me just now/; // 1. the exact production failure: 200 with only keepalive padding, no JSON at all BODY = '\n \n \n'; t('keepalive-only body no longer crashes into a dead end', FAILMSG.test(await ask('explain something obscure'))); // 2. the shape that ALSO arrives normally: keepalive padding, THEN the real JSON BODY = '\n \n' + good; t('keepalive padding before real JSON still parses', /straight answer/.test(await ask('explain something obscure'))); // 3. plain JSON, the happy path BODY = good; t('plain JSON still parses', /straight answer/.test(await ask('explain something obscure'))); // 4. SSE-style comment lines before the JSON BODY = ': OPENROUTER PROCESSING\n: OPENROUTER PROCESSING\n' + good; t('SSE comment lines before the JSON still parse', /straight answer/.test(await ask('explain something obscure'))); // 5. 200 with an empty content string is a failure, not a blank reply to the member BODY = JSON.stringify({ choices: [{ message: { content: ' ' } }] }); t('empty content is treated as a failure, never sent as a blank answer', FAILMSG.test(await ask('explain something obscure'))); // 6. garbage that is not JSON at all BODY = '502 bad gateway'; t('non-JSON body is handled', FAILMSG.test(await ask('explain something obscure'))); // 7. canned answers never touch the network, so they survive any outage BODY = ''; const pif = await ask('Should I pif someone from the holding tank'); t('the PIF question answers with no AI at all', /PIF is a gift/.test(pif), pif.slice(0, 80)); t('and it says you cannot PIF someone still in the tank', /still sitting in the holding tank/.test(pif)); // Marty, 2026-09-17: the risk is the answer, not a footnote. Sending POL to someone // who has never replied is the easiest money on the site to lose, so every one of // these must survive any future edit to the wording. t('it says talk to them and get a commitment FIRST', /do not send anything until you have actually talked to that person/i.test(pif)); t('it says the transfer is irreversible with no refund', /no refund, no chargeback/i.test(pif)); t('it says support cannot pull it back', /nothing we can do to pull it back/i.test(pif)); t('it says they may go quiet or spend it elsewhere', /go quiet, spend it on something else, or never sign in again/i.test(pif)); t('it flags the tank member as the highest risk case', /easiest money on this site to lose/i.test(pif)); t('it gives the honest arithmetic both ways', /costs you about half/.test(pif) && /costs you all of it/.test(pif)); t('it says never gift money you need and never borrow', /never gift money you need, never borrow/i.test(pif)); t('it never sells PIF as a tactic', !/you should (definitely |really )?pif/i.test(pif)); https.request = realRequest; srv.close(); console.log('PASS ' + ok.length); for (const b of bad) console.log('FAIL ' + b); process.exit(bad.length ? 1 : 0);