Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

377 lines
15 KiB
JavaScript

/**
* Tests for components/context-overflow.js
*
* Specifically locks the AbortController race-fix: when a new
* loadContextData() supersedes an in-flight one, the older fetch's
* AbortSignal must fire so its (slower) response cannot overwrite
* the newer one's render.
*/
beforeAll(async () => {
// Mark as a test environment so the file's auto-init doesn't fire
// (it would query DOM elements we haven't built yet).
window.__VITEST_TEST__ = true;
// Stub Chart constructor — happy-dom can't run Chart.js, and we only
// care that the page wires it up correctly. Records last-call args
// so tests can assert on the chart options (annotations, etc.).
globalThis.Chart = class {
constructor(ctx, config) {
globalThis.Chart.lastCall = { ctx, config };
globalThis.Chart.allCalls ||= [];
globalThis.Chart.allCalls.push({ ctx, config });
}
destroy() {}
};
// Stub URLValidator — used by the scatter chart's onClick handler.
globalThis.URLValidator = { safeAssign: () => {} };
// Set up the minimum DOM the loader touches when called directly.
document.body.innerHTML = `
<div id="loading"></div>
<div id="content"></div>
`;
await import('@js/components/context-overflow.js');
});
afterAll(() => {
delete window.__VITEST_TEST__;
});
describe('loadContextData — abort-on-supersede', () => {
it('aborts the in-flight fetch when a newer call starts', async () => {
let firstAbortFired = false;
// First call: never-resolving fetch. We capture its AbortSignal.
// Second call should trigger abort on the first.
let firstSignal;
const fetchMock = vi.fn((url, opts) => {
if (!firstSignal) {
firstSignal = opts.signal;
firstSignal.addEventListener('abort', () => {
firstAbortFired = true;
});
return new Promise(() => {}); // never settles
}
// Second call: resolve with a minimal valid payload so
// the loader's success path completes without throwing.
return Promise.resolve({
ok: true,
json: () => Promise.resolve({
status: 'success',
overview: {
truncation_rate: 0,
truncated_requests: 0,
requests_with_context_data: 0,
total_requests: 0,
avg_tokens_truncated: 0,
},
token_summary: { total_requests: 0 },
model_stats: [],
recent_truncated: [],
chart_data: [],
context_limits: [],
all_requests: [],
pagination: { page: 1, total_pages: 1 },
}),
});
});
global.fetch = fetchMock;
const ctrl = window.contextOverflowController;
// Fire-and-forget the first; it never resolves on its own.
ctrl.loadContextData('30d');
// Newer call — should abort the first.
await ctrl.loadContextData('7d');
expect(firstAbortFired).toBe(true);
// Second fetch was actually issued (not just an abort).
expect(fetchMock).toHaveBeenCalledTimes(2);
// Both fetches were given AbortSignals (defensive contract check).
expect(fetchMock.mock.calls[0][1]).toHaveProperty('signal');
expect(fetchMock.mock.calls[1][1]).toHaveProperty('signal');
});
it('passes the period through to the request URL', async () => {
const fetchMock = vi.fn(() => Promise.resolve({
ok: true,
json: () => Promise.resolve({
status: 'success',
overview: {
truncation_rate: 0,
truncated_requests: 0,
requests_with_context_data: 0,
total_requests: 0,
avg_tokens_truncated: 0,
},
token_summary: { total_requests: 0 },
model_stats: [],
recent_truncated: [],
chart_data: [],
context_limits: [],
all_requests: [],
pagination: { page: 1, total_pages: 1 },
}),
}));
global.fetch = fetchMock;
await window.contextOverflowController.loadContextData('1y', 3);
const url = fetchMock.mock.calls[0][0];
expect(url).toContain('period=1y');
expect(url).toContain('page=3');
});
});
describe('displayContextOverflowSection — restored UI features', () => {
// Set up the static DOM elements that the page expects (these live in
// context_overflow.html outside the dynamic context-overflow-section).
function setupPageDom() {
document.body.innerHTML = `
<div id="loading"></div>
<div id="content"></div>
<div id="empty-no-data"></div>
<div id="empty-no-truncation"></div>
<div id="empty-no-context-data"></div>
<div id="warning-banner"></div>
<span id="warning-rate"></span>
<div id="context-overflow-section"></div>
<tbody id="requests-tbody"></tbody>
<span id="pagination-info"></span>
<button id="pagination-prev"></button>
<button id="pagination-next"></button>
`;
}
function richPayload() {
return {
status: 'success',
overview: {
truncation_rate: 5,
truncated_requests: 2,
requests_with_context_data: 10,
total_requests: 12,
avg_tokens_truncated: 150,
},
token_summary: { total_requests: 12 },
model_stats: [
{
model: 'gpt-4',
provider: 'openai',
total_requests: 10,
truncated_count: 2,
truncation_rate: 20,
avg_context_limit: 8192,
},
],
model_token_stats: [
{
model: 'gpt-4',
provider: 'openai',
min_prompt: 100,
avg_prompt: 4000,
max_prompt: 7500,
avg_response_time_ms: 1200,
},
],
recent_truncated: [],
chart_data: [
{
timestamp: new Date().toISOString(),
prompt_tokens: 4000,
actual_prompt_tokens: 4000,
context_limit: 8192,
research_phase: 'search',
research_id: 'r1',
model: 'gpt-4',
response_time_ms: 1200,
},
],
context_limits: [],
phase_breakdown: [
{ phase: 'search', count: 5, total_tokens: 20000, avg_tokens: 4000 },
],
current_context_window: 4096,
all_requests: [],
pagination: { page: 1, total_pages: 1 },
};
}
it('renders phase chart card and summary table when phase_breakdown is provided', async () => {
setupPageDom();
global.fetch = vi.fn(() => Promise.resolve({
ok: true,
json: () => Promise.resolve(richPayload()),
}));
await window.contextOverflowController.loadContextData('30d');
// Phase card was added to the dynamic section
expect(document.getElementById('phase-chart')).not.toBeNull();
expect(document.getElementById('phase-summary-table')).not.toBeNull();
// Summary table populated from phase_breakdown
expect(document.getElementById('phase-summary-table').innerHTML)
.toContain('search');
});
it('renders latency chart card', async () => {
setupPageDom();
global.fetch = vi.fn(() => Promise.resolve({
ok: true,
json: () => Promise.resolve(richPayload()),
}));
await window.contextOverflowController.loadContextData('30d');
expect(document.getElementById('latency-chart')).not.toBeNull();
});
it('renders per-model token stats grid (min/avg/max prompt) when model_token_stats provided', async () => {
setupPageDom();
global.fetch = vi.fn(() => Promise.resolve({
ok: true,
json: () => Promise.resolve(richPayload()),
}));
await window.contextOverflowController.loadContextData('30d');
const modelStatsHtml = document.getElementById('model-stats').innerHTML;
// Min/avg/max prompt labels appear, indicating the restored grid renders
expect(modelStatsHtml).toContain('Min Prompt');
expect(modelStatsHtml).toContain('Avg Prompt');
expect(modelStatsHtml).toContain('Max Prompt');
// Context utilization progress bar
expect(modelStatsHtml).toContain('Context utilization');
// Average response time line
expect(modelStatsHtml).toContain('1.2s');
});
it('passes context-limit annotations to the scatter chart so reference lines render', async () => {
setupPageDom();
globalThis.Chart.allCalls = [];
global.fetch = vi.fn(() => Promise.resolve({
ok: true,
json: () => Promise.resolve(richPayload()),
}));
await window.contextOverflowController.loadContextData('30d');
// Find the context chart call (looks for time-axis scatter chart with
// an annotation block — distinguishes it from latency / phase charts).
const contextChartCall = globalThis.Chart.allCalls.find(c =>
c.config?.type === 'scatter'
&& c.config?.options?.scales?.x?.type === 'time'
);
expect(contextChartCall).toBeDefined();
const annotations = contextChartCall.config.options.plugins.annotation.annotations;
// limit_8192 annotation exists AND has the right yMin/yMax values
// (presence-only check would pass even if the line was rendered at 0)
expect(annotations.limit_8192).toBeDefined();
expect(annotations.limit_8192.yMin).toBe(8192);
expect(annotations.limit_8192.yMax).toBe(8192);
expect(annotations.limit_8192.borderDash).toEqual([6, 4]);
// current_setting line at the configured num_ctx (4096), distinct
// styling (solid line, no dash) so users can tell it from historical
expect(annotations.current_setting).toBeDefined();
expect(annotations.current_setting.yMin).toBe(4096);
expect(annotations.current_setting.borderDash).toBeUndefined();
});
it('separates noLimitData into its own bucket instead of conflating with cautionData', async () => {
setupPageDom();
globalThis.Chart.allCalls = [];
const payload = richPayload();
// Two points: one with a limit (lands in safeData), one without (must
// land in noLimitData, NOT cautionData)
payload.chart_data = [
{ ...payload.chart_data[0], original_prompt_tokens: 1000, context_limit: 8192 },
{ ...payload.chart_data[0], original_prompt_tokens: 1000, context_limit: null },
];
global.fetch = vi.fn(() => Promise.resolve({
ok: true,
json: () => Promise.resolve(payload),
}));
await window.contextOverflowController.loadContextData('30d');
const contextChartCall = globalThis.Chart.allCalls.find(c =>
c.config?.type === 'scatter'
&& c.config?.options?.scales?.x?.type === 'time'
);
const datasets = contextChartCall.config.data.datasets;
const noLimitDataset = datasets.find(d => d.label === 'No context limit reported');
const cautionDataset = datasets.find(d => d.label?.startsWith('Caution'));
// Point with no limit goes to its own dataset, not caution
expect(noLimitDataset.data).toHaveLength(1);
expect(noLimitDataset.data[0].context_limit).toBeNull();
expect(cautionDataset.data).toHaveLength(0);
});
it('detects local providers using canonical names (lmstudio/llamacpp without underscores)', async () => {
// Regression guard against the LOCAL_PROVIDERS underscore mismatch.
// The TokenUsage.model_provider canonical values per default_settings.json
// are 'lmstudio' / 'llamacpp' — the chart must mark these as local.
setupPageDom();
globalThis.Chart.allCalls = [];
const payload = richPayload();
payload.chart_data = [
{ ...payload.chart_data[0], provider: 'lmstudio', original_prompt_tokens: 1000, context_limit: 8192 },
{ ...payload.chart_data[0], provider: 'llamacpp', original_prompt_tokens: 1000, context_limit: 8192 },
{ ...payload.chart_data[0], provider: 'openai', original_prompt_tokens: 1000, context_limit: 8192 },
];
global.fetch = vi.fn(() => Promise.resolve({
ok: true,
json: () => Promise.resolve(payload),
}));
await window.contextOverflowController.loadContextData('30d');
const contextChartCall = globalThis.Chart.allCalls.find(c =>
c.config?.type === 'scatter'
&& c.config?.options?.scales?.x?.type === 'time'
);
// All 3 points are <50% util → they all land in safeData
const safeDataset = contextChartCall.config.data.datasets.find(d => d.label?.startsWith('Safe'));
const points = safeDataset.data;
expect(points).toHaveLength(3);
const lmstudioPoint = points.find(p => p.provider === 'lmstudio');
const llamacppPoint = points.find(p => p.provider === 'llamacpp');
const openaiPoint = points.find(p => p.provider === 'openai');
expect(lmstudioPoint.isLocal).toBe(true);
expect(llamacppPoint.isLocal).toBe(true);
expect(openaiPoint.isLocal).toBe(false);
});
it('hides the no-truncation banner when chart_data has any >80% util request even if truncated_requests is 0', async () => {
// Empty-state guard: a request at 85% utilisation that the backend
// hasn't yet flagged as truncated is still a problem the user should
// see, so the "all clear" banner must not show.
setupPageDom();
const payload = richPayload();
payload.overview.truncated_requests = 0; // backend says no truncation events
payload.overview.requests_with_context_data = 1;
payload.chart_data = [
{
...payload.chart_data[0],
original_prompt_tokens: 7000, // 7000 / 8192 = 85.4% > 80%
context_limit: 8192,
},
];
global.fetch = vi.fn(() => Promise.resolve({
ok: true,
json: () => Promise.resolve(payload),
}));
await window.contextOverflowController.loadContextData('30d');
const banner = document.getElementById('empty-no-truncation');
// banner.style.display defaults to '' before any code touches it;
// after displayContextData runs the banner is set to 'none' for this case
expect(banner.style.display).toBe('none');
});
});