W / test /cascade-timeout-invalidation.test.js
Ac66's picture
Upload folder using huggingface_hub
2b64d42 verified
// Issue #101 (nalayahfowlkest-ship-it): claude-opus-4.6-thinking,
// upstream "context deadline exceeded" mid-stream. The error surfaces
// to the client, but on the next turn the model only sees the latest
// tool_result with no earlier user prompts:
//
// "I can see the content from a previous tool call ... However, I
// don't have the earlier conversation context that explains what
// specific task you'd like me to work on."
//
// Root cause: when cascade upstream times out mid-stream, the cascade
// trajectory is left in an inconsistent state (the assistant never
// finished, but the prior tool_result chunk is in there). The proxy
// previously restored the cascade entry to the reuse pool unconditionally
// (because reuseEntryDead was only set on explicit "cascade not_found"
// errors). Next request reused the half-broken trajectory.
//
// Fix (chat.js stream + non-stream catch blocks): also mark the entry
// dead when the error message matches "context deadline exceeded" or
// "Client.Timeout or context cancellation while reading body". Static
// analysis below pins both code paths.
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const CHAT_JS = readFileSync(join(__dirname, '..', 'src/handlers/chat.js'), 'utf8');
describe('upstream-timeout cascade invalidation (#101)', () => {
test('stream catch block matches the timeout patterns and sets reuseEntryDead', () => {
// Find the streamResponse-side catch block. The whole region we care
// about sits between the `lastErr = err;` assignment and the rate-
// limit branch β€” pin it explicitly so a future refactor that splits
// the block doesn't silently regress.
const m = CHAT_JS.match(/lastErr = err;\s+reuseEntry = null;[\s\S]{0,1500}?const isAuthFail = /);
assert.ok(m, 'stream catch block region not found β€” refactor may have changed shape');
const region = m[0];
assert.match(region, /context deadline exceeded/i,
'stream timeout regex must mention "context deadline exceeded"');
assert.match(region, /context cancellation while reading body/i,
'stream timeout regex must mention "context cancellation while reading body"');
assert.match(region, /client\\?\.timeout/i,
'stream timeout regex must include Client.Timeout fallback');
assert.match(region, /reuseEntryDead = true/,
'stream timeout branch must set reuseEntryDead = true');
});
test('non-stream branch matches the same timeout patterns and sets reuseEntryDead', () => {
// The non-stream path in handleChatCompletions checks
// `result.body?.error?.message`. Verify the same invalidation logic
// exists there β€” without it, non-stream callers (some Cherry Studio
// configurations, Anthropic SDK with stream=false) would still
// restore the broken cascade.
const m = CHAT_JS.match(/if \(result\.reuseEntryInvalid\) reuseEntryDead = true;[\s\S]{0,800}?lastErr = result;/);
assert.ok(m, 'non-stream invalidation region not found β€” refactor may have changed shape');
const region = m[0];
assert.match(region, /context deadline exceeded/i);
assert.match(region, /context cancellation while reading body/i);
assert.match(region, /client\\?\.timeout/i);
assert.match(region, /reuseEntryDead = true/);
});
test('regex actually matches the user-reported error message verbatim', () => {
// Real error from #101:
// "Encountered retryable error from model provider: context
// deadline exceeded (Client.Timeout or context cancellation
// while reading body)"
// Reproduce the regex literally to make sure it triggers.
const re = /context deadline exceeded|context cancellation while reading body|client\.timeout/i;
const userError = 'Encountered retryable error from model provider: context deadline exceeded (Client.Timeout or context cancellation while reading body)';
assert.ok(re.test(userError),
'regex must match the literal error string from issue #101');
});
test('regex does NOT match unrelated rate-limit / panel-state errors', () => {
// Negative cases β€” make sure we don't over-match and accidentally
// invalidate the cascade for transient errors that should retry on
// the same trajectory.
const re = /context deadline exceeded|context cancellation while reading body|client\.timeout/i;
assert.equal(re.test('rate limit exceeded for model claude-opus-4-7'), false);
assert.equal(re.test('Panel state not found for sessionId xxx'), false);
assert.equal(re.test('cascade not_found upstream after 3 retries'), false);
assert.equal(re.test('internal error occurred (Error ID: abc)'), false);
});
});