Files
builder/scripts/test-friendly-errors.mjs
jelveh 6dafe11822 Retry a build turn when puter.js reports a dropped connection
puter.js fails a stream that loses its connection mid-response with
'Network request failed.' (code network_error), and a request that gets no
response with 'Network request to <url> failed'. Neither matched the
transient-error list, so the turn ended on a red error card showing the raw
text, with no automatic retry and no Resume banner.

Treat both as transient, so the turn retries with backoff and falls back to
the Resume banner, and show them as the friendly network message.
2026-10-31 10:50:08 -07:00

58 lines
4.0 KiB
JavaScript

import fs from 'node:fs';
// ---- Regression guard: error text classification ----------------------------
// friendlyErrorMessage rewrites raw provider errors into one actionable
// sentence. Two defects: it matched '529'/'503'/'429' as bare substrings, so
// the context-window error — whose text carries a six-digit token count — was
// mislabelled "the service is busy, try again" (a retry fails identically,
// forever), and a JSON envelope whose inner message was not a string made the
// display path throw on .trim(), so no error card rendered at all.
// Evaluates the real functions from helpers.js.
let failures = 0;
function check(name, cond, detail) {
if (cond) console.log('ok - ' + name);
else { console.error('FAIL - ' + name + (detail ? '\n ' + detail : '')); failures++; }
}
const src = fs.readFileSync(new URL('../src/js/helpers.js', import.meta.url), 'utf8');
const a = src.indexOf('function extractErrorText(');
const b = src.indexOf('function nl_to_p(');
if (a < 0 || b <= a) throw new Error('could not extract the error helpers');
const { extractErrorText, friendlyErrorMessage } = new Function(src.slice(a, b) + '\nreturn { extractErrorText, friendlyErrorMessage };')();
const busy = "The AI service is busy right now. Please wait a moment and try again.";
const quick = "You're sending messages too quickly. Please wait a few seconds and try again.";
// The token count must not read as a status code.
for (const n of ['215290', '205030', '204290', '529000', '150300']) {
const msg = friendlyErrorMessage(`prompt is too long: ${n} tokens > 200000 maximum`);
check(`prompt too long (${n} tokens) is explained as a too-long conversation`, /too long/.test(msg) && msg !== busy && msg !== quick, msg);
}
check('a request id containing 529 is not "busy"',
friendlyErrorMessage('Request req_01HZ5293K failed: invalid_request_error: model not found') !== busy);
// Real status codes still classify.
check('a real 529 is "busy"', friendlyErrorMessage('Error 529: overloaded') === busy);
check('a real 503 is "busy"', friendlyErrorMessage('upstream returned 503') === busy);
check('a real 429 is "too quickly"', friendlyErrorMessage('HTTP 429') === quick);
check('"rate limit" still classifies', friendlyErrorMessage('rate limit exceeded') === quick);
const offline = "Couldn't reach the AI service. Check your internet connection and try again.";
check('a dropped puter.js stream is explained, not shown raw', friendlyErrorMessage(extractErrorText({ message: 'Network request failed.', code: 'network_error' })) === offline);
check('a failed puter.js request is explained, not shown raw', friendlyErrorMessage('Network request to https://api.puter.com/drivers/call failed') === offline);
check('other context-window phrasings are caught too',
/too long/.test(friendlyErrorMessage('context_length_exceeded')) && /too long/.test(friendlyErrorMessage('This model\'s maximum context length is 200000 tokens')));
// Non-string envelopes must not throw.
let threw = false, out = '';
try { out = friendlyErrorMessage(extractErrorText('400 {"error":{"message":{"code":1}}}')); } catch (e) { threw = true; }
check('a non-string inner message never throws', !threw);
check('… and still yields a readable sentence', typeof out === 'string' && out.length > 0, out);
check('a bare object error is stringified, not thrown',
(() => { try { return typeof friendlyErrorMessage(extractErrorText({ error: { message: 42 } })) === 'string'; } catch (e) { return false; } })());
check('null/undefined give the generic fallback',
/Something went wrong/.test(friendlyErrorMessage(extractErrorText(null))) && /Something went wrong/.test(friendlyErrorMessage(undefined)));
check('a normal envelope still unwraps',
friendlyErrorMessage(extractErrorText('400 {"type":"error","error":{"message":"image exceeds 5 MB maximum"}}')).includes('too large'));
if (failures) { console.error(`\n${failures} friendly-error check(s) failed.`); process.exit(1); }
console.log('\nAll friendly-error checks passed.');