项目文件夹

文件
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

429 行
19 KiB
JavaScript

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
#!/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):
// <div class="ldr-page active" id="context-overflow"> ... <h1>Token Usage & Context Analytics</h1>
// 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 <canvas id="context-chart">
// that context-overflow.js injects into #context-overflow-section, and ONLY when
// the API reports requests_with_context_data > 0. With no LLM / fresh DB (CI) there
// is no token data, so no canvas is created.
//
// The old test matched `canvas, svg, .chart, [class*="chart"]` — the substring
// selector matched unrelated sidebar/header FontAwesome icons (<i class="fas fa-chart-bar">),
// so it reported "Found 4 charts (types: i, i, i, i)" with zero real charts. We now
// assert on the overflow-specific chart scaffold: the #context-overflow-section
// injection point must exist on the correct page, and if a real #context-chart
// canvas is present (data path) it must be a <canvas>; otherwise the empty state
// must be shown — never counting icon chrome.
const result = await page.evaluate(() => {
const onPage = !!document.getElementById('context-overflow');
const section = document.getElementById('context-overflow-section');
const contextChart = document.getElementById('context-chart');
const isCanvas = contextChart?.tagName?.toLowerCase() === 'canvas';
const emptyNoData = document.getElementById('empty-no-data');
const emptyVisible = emptyNoData
? getComputedStyle(emptyNoData).display !== 'none'
: false;
return {
onPage,
hasSection: !!section,
hasContextChart: !!contextChart,
isCanvas,
emptyVisible
};
});
if (result.hasContextChart) {
// Data path: the overflow-specific chart element must be a real canvas.
return {
passed: result.onPage && result.hasSection && result.isCanvas,
message: result.isCanvas
? 'Overflow scatter chart present (#context-chart is a <canvas>)'
: `#context-chart present but not a <canvas>`
};
}
// No data (CI no-LLM path): the chart injection point exists on the correct
// page and the empty state rendered. This fails (not skips) if we are on the
// wrong page or the overflow scaffold did not load.
return {
passed: result.onPage && result.hasSection && result.emptyVisible,
message: result.onPage && result.hasSection && result.emptyVisible
? 'No token data: #context-overflow-section scaffold present and #empty-no-data shown (no #context-chart canvas without data)'
: `Overflow chart scaffold unexpected (onPage=${result.onPage}, section=${result.hasSection}, emptyVisible=${result.emptyVisible})`
};
},
async periodFilterWorks(page, baseUrl) {
await navigateTo(page, `${baseUrl}/metrics/context-overflow`);
const result = await page.evaluate(() => {
const periodFilter = document.querySelector(
'select[name*="period"], ' +
'#period-filter, ' +
'.period-filter, ' +
'select[id*="period"]'
);
if (periodFilter) {
const options = Array.from(periodFilter.options);
return {
exists: true,
type: 'select',
optionCount: options.length,
options: options.map(o => o.text).slice(0, 6)
};
}
// Check for button-based filter
const buttons = document.querySelectorAll('.period-btn, .time-filter button, [data-period]');
if (buttons.length > 0) {
return {
exists: true,
type: 'buttons',
buttonCount: buttons.length,
options: Array.from(buttons).map(b => b.textContent?.trim()).slice(0, 6)
};
}
return { exists: false };
});
if (!result.exists) {
return { passed: null, skipped: true, message: 'No period filter found' };
}
return {
passed: true,
message: result.type === 'select'
? `Period filter (${result.optionCount} options): ${result.options.join(', ')}`
: `Period buttons (${result.buttonCount}): ${result.options.join(', ')}`
};
},
async overflowDetailsTable(page, baseUrl) {
await navigateTo(page, `${baseUrl}/metrics/context-overflow`);
const result = await page.evaluate(() => {
const table = document.querySelector('table, .overflow-details, .data-table');
if (!table) return { hasTable: false };
const headers = Array.from(table.querySelectorAll('th')).map(th => th.textContent?.toLowerCase().trim());
const rows = table.querySelectorAll('tbody tr');
return {
hasTable: true,
headerCount: headers.length,
rowCount: rows.length,
headers: headers.slice(0, 6),
hasResearchColumn: headers.some(h => h.includes('research') || h.includes('query')),
hasTokenColumn: headers.some(h => h.includes('token'))
};
});
if (!result.hasTable) {
return { passed: null, skipped: true, message: 'No overflow details table found' };
}
return {
passed: true,
message: `Details table: ${result.rowCount} rows, columns: ${result.headers.join(', ')}`
};
}
};
// ============================================================================
// Context Overflow API Tests
// ============================================================================
const ContextOverflowApiTests = {
async contextOverflowApiResponds(page, baseUrl) {
await navigateTo(page, `${baseUrl}/metrics/context-overflow`);
const result = await page.evaluate(async (url) => {
try {
const response = await fetch(`${url}/metrics/api/context-overflow`);
if (!response.ok) return { ok: false, status: response.status };
const data = await response.json();
return {
ok: true,
status: response.status,
hasData: Object.keys(data).length > 0,
keys: Object.keys(data).slice(0, 5)
};
} catch (e) {
return { ok: false, error: e.message };
}
}, baseUrl);
if (!result.ok && result.status === 404) {
return { passed: null, skipped: true, message: 'Context overflow API not found' };
}
return {
passed: result.ok,
message: result.ok
? `Context overflow API responds (keys: ${result.keys.join(', ')})`
: `API failed: ${result.error || 'status ' + result.status}`
};
}
};
// ============================================================================
// Main Test Runner
// ============================================================================
async function main() {
log.section('Context Overflow Tests');
const ctx = await setupTest({ authenticate: true });
const results = new TestResults('Context Overflow Tests');
const { page } = ctx;
const { baseUrl } = ctx.config;
const subTestTimeout = ctx.config.isCI ? 60000 : 30000;
async function run(category, name, testFn) {
try {
const result = await withTimeout(
testFn(page, baseUrl),
subTestTimeout,
`${category}/${name}`
);
if (result.skipped) {
results.skip(category, name, result.message);
} else {
results.add(category, name, result.passed, result.message);
}
} catch (error) {
results.add(category, name, false, `Error: ${error.message}`);
}
}
try {
// Pre-navigate to context overflow page once to check if it loads
// (avoids 7 × 30s timeouts in CI when page is slow)
log.section('Context Overflow Page');
let pageAccessible = false;
try {
await navigateTo(page, `${baseUrl}/metrics/context-overflow`);
const is404 = await page.evaluate(() =>
document.body.textContent?.includes('404') || document.body.textContent?.includes('Not Found')
);
pageAccessible = !is404;
} catch (navError) {
log.warning(`Context overflow page not accessible: ${navError.message}`);
}
if (!pageAccessible) {
const skipMsg = 'Context overflow page not accessible in CI';
results.skip('Page', 'Context Overflow Page Loads', skipMsg);
results.skip('Page', 'Truncation Rate Display', skipMsg);
results.skip('Page', 'Average Truncated Tokens', skipMsg);
results.skip('Page', 'Context Overflow Chart', skipMsg);
results.skip('Page', 'Period Filter Works', skipMsg);
results.skip('Page', 'Overflow Details Table', skipMsg);
results.skip('API', 'Context Overflow API Responds', skipMsg);
} else {
await run('Page', 'Context Overflow Page Loads', (p, u) => ContextOverflowTests.contextOverflowPageLoads(p, u));
await run('Page', 'Truncation Rate Display', (p, u) => ContextOverflowTests.truncationRateDisplay(p, u));
await run('Page', 'Average Truncated Tokens', (p, u) => ContextOverflowTests.averageTruncatedTokens(p, u));
await run('Page', 'Context Overflow Chart', (p, u) => ContextOverflowTests.contextOverflowChart(p, u));
await run('Page', 'Period Filter Works', (p, u) => ContextOverflowTests.periodFilterWorks(p, u));
await run('Page', 'Overflow Details Table', (p, u) => ContextOverflowTests.overflowDetailsTable(p, u));
// API Tests
log.section('Context Overflow API');
await run('API', 'Context Overflow API Responds', (p, u) => ContextOverflowApiTests.contextOverflowApiResponds(p, u));
}
} catch (error) {
log.error(`Fatal error: ${error.message}`);
console.error(error.stack);
} finally {
results.print();
results.save();
await teardownTest(ctx);
process.exit(results.exitCode());
}
}
// Run if executed directly
if (require.main === module) {
main().catch(error => {
console.error('Test runner failed:', error);
process.exit(1);
});
}
module.exports = { ContextOverflowTests, ContextOverflowApiTests };