#!/usr/bin/env node
/**
* Context Overflow UI Tests
*
* Tests for the context overflow analytics page and related functionality.
*
* Run: node test_context_overflow_ci.js
*/
const { setupTest, teardownTest, TestResults, log, navigateTo, withTimeout } = require('./test_lib');
// ============================================================================
// Context Overflow Page Tests
// ============================================================================
const ContextOverflowTests = {
async contextOverflowPageLoads(page, baseUrl) {
await navigateTo(page, `${baseUrl}/metrics/context-overflow`);
// The page-specific scaffold is server-rendered (pages/context_overflow.html):
//
...
Token Usage & Context Analytics
// Gate the pass on that container AND the exact header text so the test
// fails if a login/error/wrong page loaded instead. The old check passed
// on a bare `h1, .page-title` or a body-wide "overflow"/"truncat" substring
// (which also matches the static help-panel chrome), so it could not tell
// the real page apart from any other page that has a heading.
const result = await page.evaluate(() => {
const container = document.getElementById('context-overflow');
const header = container?.querySelector('.ldr-page-header h1');
return {
hasContainer: !!container,
headerText: header?.textContent?.trim() || '',
is404: document.body.textContent?.includes('404') || document.body.textContent?.includes('Not Found')
};
});
if (result.is404) {
return { passed: null, skipped: true, message: 'Context overflow page not found (feature may not be enabled)' };
}
const passed = result.hasContainer && result.headerText === 'Token Usage & Context Analytics';
return {
passed,
message: passed
? `Context overflow page loaded (#context-overflow header: "${result.headerText}")`
: `Context overflow page failed to load (container=${result.hasContainer}, header="${result.headerText}")`
};
},
async truncationRateDisplay(page, baseUrl) {
await navigateTo(page, `${baseUrl}/metrics/context-overflow`);
// Poll for the controller to resolve one of two end states:
// (1) #truncation-rate injected (data path), or
// (2) #empty-no-data shown (no-LLM / fresh-DB path).
// Replaces a fixed delay(1000) — the controller resolves quickly when idle,
// but polling avoids flakiness if the fetch is slow under CI load.
try {
await page.waitForFunction(
() => {
if (document.getElementById('truncation-rate')) return true;
const empty = document.getElementById('empty-no-data');
return !!empty && getComputedStyle(empty).display !== 'none';
},
{ timeout: 5000 }
);
} catch (_) {
// Fall through — the assertion below reports what's actually on the page.
}
// The truncation rate is rendered by context-overflow.js into the
// overflow-specific element `#truncation-rate` (inside #context-overflow-section),
// and ONLY when the API returns requests_with_context_data > 0. With no LLM /
// fresh DB (CI), there is no token data, so the controller shows the
// #empty-no-data state and never injects #truncation-rate.
//
// The old test read a body-wide percentage regex, which matched the static
// help-panel text ("Green (<10%) / Orange (10-20%) / Red (>20%)") and so
// reported a bogus "10%" truncation rate on a page with zero data. We now
// read the rate ONLY from #truncation-rate, and when that element is absent
// we assert the empty-state rendered on the correct page instead of skipping.
const result = await page.evaluate(() => {
const onPage = !!document.getElementById('context-overflow');
const rateElement = document.getElementById('truncation-rate');
const rateText = rateElement?.textContent?.trim() || '';
const ratePct = rateText.match(/(\d+(?:\.\d*)?)\s*%/);
const emptyNoData = document.getElementById('empty-no-data');
const emptyVisible = emptyNoData
? getComputedStyle(emptyNoData).display !== 'none'
: false;
return {
onPage,
hasRateElement: !!rateElement,
rateText,
rateIsPercent: !!ratePct,
emptyVisible
};
});
if (result.hasRateElement) {
// Data present: the dedicated rate element must show a percentage.
return {
passed: result.rateIsPercent,
message: result.rateIsPercent
? `Truncation rate displayed in #truncation-rate: "${result.rateText}"`
: `#truncation-rate present but not a percentage: "${result.rateText}"`
};
}
// No data (CI no-LLM path): assert we are on the overflow page and the
// zero-data empty state rendered, rather than skipping or matching help text.
return {
passed: result.onPage && result.emptyVisible,
message: result.onPage && result.emptyVisible
? 'No token data: #truncation-rate absent and #empty-no-data shown (expected with no LLM)'
: `Truncation-rate state unexpected (onPage=${result.onPage}, emptyVisible=${result.emptyVisible})`
};
},
async averageTruncatedTokens(page, baseUrl) {
await navigateTo(page, `${baseUrl}/metrics/context-overflow`);
const result = await page.evaluate(() => {
const avgElement = document.querySelector(
'[class*="average-tokens"], ' +
'[class*="avg-truncated"], ' +
'.token-average'
);
// Look for token count patterns
const tokenPattern = /(\d[\d,]*)\s*(?:tokens?|truncated)/i;
const bodyText = document.body.textContent || '';
const tokenMatch = bodyText.match(tokenPattern);
const hasAvgText = bodyText.toLowerCase().includes('average') &&
bodyText.toLowerCase().includes('token');
return {
hasAvgElement: !!avgElement,
avgText: avgElement?.textContent?.trim(),
hasTokenMatch: !!tokenMatch,
tokenValue: tokenMatch ? tokenMatch[1] : null,
hasAvgText
};
});
if (!result.hasAvgElement && !result.hasTokenMatch && !result.hasAvgText) {
return { passed: null, skipped: true, message: 'No average truncated tokens display found' };
}
return {
passed: true,
message: result.hasAvgElement
? `Average tokens: "${result.avgText}"`
: (result.tokenValue ? `Token count found: ${result.tokenValue}` : 'Average token info found')
};
},
async contextOverflowChart(page, baseUrl) {
await navigateTo(page, `${baseUrl}/metrics/context-overflow`);
// Poll for the controller to resolve one of two end states:
// (1) #context-chart canvas injected (data path), or
// (2) #empty-no-data shown (no-LLM / fresh-DB path).
try {
await page.waitForFunction(
() => {
if (document.getElementById('context-chart')) return true;
const empty = document.getElementById('empty-no-data');
return !!empty && getComputedStyle(empty).display !== 'none';
},
{ timeout: 5000 }
);
} catch (_) {
// Fall through — the assertion below reports what's actually on the page.
}
// The overflow scatter chart is a single page-specific