Chatbot: the PIF and holding-tank answers, and no more dead ends on a provider hiccup
Marty hit "I hit a snag answering that one" on "Should I pif someone from the holding tank". Two separate problems behind it. The real bug: OpenRouter pads a slow upstream with keepalive lines before the JSON, and when the provider gives up it can close having sent ONLY that padding. HTTP 200, no body. JSON.parse threw and EVERY non-canned question died the same way, not just this one. Now we find the actual JSON object in the stream, treat an empty answer as a failure rather than sending a blank reply, retry once (the usual cause is one provider dropping the request), and if both attempts fail we say plainly that it was our side, not their question, and point at a person. The content gap: PIF and the holding tank were in the system prompt but had no canned answers, so they depended on the AI being up. Both are now canned and instant. The PIF answer leads with the thing the question gets wrong, which is that you cannot PIF somebody still in the tank: adopt first, they link a wallet, then the button appears. It is honest that nothing obliges them to buy, that crypto transfers are irreversible, and that the 50 percent coming back only happens if they actually purchase. Also fixes a pre-existing routing bug found while testing: "how much do I earn" returned the PACKAGE PRICE ladder, because the price pattern matches "how much do" and the commission pattern only caught "how much earn". qa/chatbot-parse.mjs (9 assertions) drives the real parse branch against a stub server for every response shape: keepalive-only, keepalive-then-JSON, plain JSON, SSE comments, empty content, non-JSON. qa/run.sh member: 0 bugs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
// 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 = '<html>502 bad gateway</html>';
|
||||
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));
|
||||
t('and it does not promise they will buy', /no guarantee they buy at all/.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);
|
||||
Reference in New Issue
Block a user