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
211 lines
7.7 KiB
JavaScript
211 lines
7.7 KiB
JavaScript
/**
|
|
* Tests for utils/log-helpers.js
|
|
*
|
|
* The log panel filters and dedups thousands of log entries during a
|
|
* research run. These helpers power that — an off-by-one in
|
|
* checkLogVisibility means warnings disappear; a collision in
|
|
* hashString means dedupe drops real entries.
|
|
*/
|
|
|
|
import '@js/utils/log-helpers.js';
|
|
|
|
const {
|
|
checkLogVisibility,
|
|
emptyCounts,
|
|
hashString,
|
|
normalizeMessage,
|
|
normalizeTimestamps,
|
|
} = window.LdrLogHelpers;
|
|
|
|
describe('checkLogVisibility', () => {
|
|
it("filter 'all' shows every type", () => {
|
|
for (const type of ['info', 'milestone', 'warning', 'error']) {
|
|
expect(checkLogVisibility(type, 'all')).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("filter 'info' only shows info entries (regression: was a no-op)", () => {
|
|
// The pre-fix implementation returned true for every type under
|
|
// the 'info' filter, which made clicking the Info filter button
|
|
// effectively a no-op. This test pins down the correct, narrow
|
|
// behaviour: only entries whose type is 'info' are visible.
|
|
expect(checkLogVisibility('info', 'info')).toBe(true);
|
|
expect(checkLogVisibility('milestone', 'info')).toBe(false);
|
|
expect(checkLogVisibility('warning', 'info')).toBe(false);
|
|
expect(checkLogVisibility('error', 'info')).toBe(false);
|
|
});
|
|
|
|
it("filter 'milestone' only shows milestones", () => {
|
|
expect(checkLogVisibility('milestone', 'milestone')).toBe(true);
|
|
expect(checkLogVisibility('info', 'milestone')).toBe(false);
|
|
expect(checkLogVisibility('warning', 'milestone')).toBe(false);
|
|
expect(checkLogVisibility('error', 'milestone')).toBe(false);
|
|
});
|
|
|
|
it("accepts plural 'milestones' as an alias", () => {
|
|
expect(checkLogVisibility('milestone', 'milestones')).toBe(true);
|
|
expect(checkLogVisibility('info', 'milestones')).toBe(false);
|
|
});
|
|
|
|
it("filter 'warning' shows warnings and errors", () => {
|
|
expect(checkLogVisibility('warning', 'warning')).toBe(true);
|
|
expect(checkLogVisibility('error', 'warning')).toBe(true);
|
|
expect(checkLogVisibility('info', 'warning')).toBe(false);
|
|
expect(checkLogVisibility('milestone', 'warning')).toBe(false);
|
|
});
|
|
|
|
it("accepts plural 'warnings' as an alias", () => {
|
|
expect(checkLogVisibility('warning', 'warnings')).toBe(true);
|
|
expect(checkLogVisibility('error', 'warnings')).toBe(true);
|
|
expect(checkLogVisibility('info', 'warnings')).toBe(false);
|
|
});
|
|
|
|
it("filter 'error' only shows errors", () => {
|
|
expect(checkLogVisibility('error', 'error')).toBe(true);
|
|
expect(checkLogVisibility('warning', 'error')).toBe(false);
|
|
expect(checkLogVisibility('info', 'error')).toBe(false);
|
|
expect(checkLogVisibility('milestone', 'error')).toBe(false);
|
|
});
|
|
|
|
it("accepts plural 'errors' as an alias", () => {
|
|
expect(checkLogVisibility('error', 'errors')).toBe(true);
|
|
expect(checkLogVisibility('warning', 'errors')).toBe(false);
|
|
});
|
|
|
|
it('unknown filter defaults to showing everything', () => {
|
|
expect(checkLogVisibility('info', 'bogus')).toBe(true);
|
|
expect(checkLogVisibility('error', '')).toBe(true);
|
|
expect(checkLogVisibility('milestone', undefined)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('emptyCounts', () => {
|
|
it('returns a fresh object on each call (mutations do not leak)', () => {
|
|
const a = emptyCounts();
|
|
const b = emptyCounts();
|
|
expect(a).not.toBe(b);
|
|
a.info = 99;
|
|
expect(b.info).toBe(0);
|
|
});
|
|
|
|
it('initialises every category to zero with the expected keys', () => {
|
|
const counts = emptyCounts();
|
|
expect(counts).toEqual({
|
|
info: 0,
|
|
milestone: 0,
|
|
warning: 0,
|
|
error: 0,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('hashString', () => {
|
|
it('returns "0" for empty/null/undefined', () => {
|
|
expect(hashString('')).toBe('0');
|
|
expect(hashString(null)).toBe('0');
|
|
expect(hashString(undefined)).toBe('0');
|
|
});
|
|
|
|
it('is deterministic: same input → same output', () => {
|
|
const a = hashString('log entry one');
|
|
const b = hashString('log entry one');
|
|
expect(a).toBe(b);
|
|
});
|
|
|
|
it('produces different hashes for different inputs', () => {
|
|
expect(hashString('log entry one')).not.toBe(hashString('log entry two'));
|
|
expect(hashString('a')).not.toBe(hashString('b'));
|
|
});
|
|
|
|
it('handles long strings', () => {
|
|
const long = 'x'.repeat(10000);
|
|
expect(hashString(long)).toMatch(/^-?\d+$/);
|
|
});
|
|
|
|
it('handles unicode characters', () => {
|
|
// Should not throw and should differ from its ASCII analogue
|
|
expect(hashString('café')).toMatch(/^-?\d+$/);
|
|
expect(hashString('café')).not.toBe(hashString('cafe'));
|
|
});
|
|
});
|
|
|
|
describe('normalizeMessage', () => {
|
|
it('returns empty string for falsy input', () => {
|
|
expect(normalizeMessage('')).toBe('');
|
|
expect(normalizeMessage(null)).toBe('');
|
|
expect(normalizeMessage(undefined)).toBe('');
|
|
});
|
|
|
|
it('trims whitespace', () => {
|
|
expect(normalizeMessage(' hello ')).toBe('hello');
|
|
expect(normalizeMessage('\t\nhello\r\n')).toBe('hello');
|
|
});
|
|
|
|
it('lowercases', () => {
|
|
expect(normalizeMessage('HELLO World')).toBe('hello world');
|
|
});
|
|
|
|
it('trims and lowercases in combination', () => {
|
|
expect(normalizeMessage(' HELLO ')).toBe('hello');
|
|
});
|
|
});
|
|
|
|
describe('normalizeTimestamps', () => {
|
|
it('is a no-op when all logs share the same date', () => {
|
|
const logs = [
|
|
{time: '2026-04-01T10:00:00Z', message: 'a'},
|
|
{time: '2026-04-01T11:00:00Z', message: 'b'},
|
|
{time: '2026-04-01T12:00:00Z', message: 'c'},
|
|
];
|
|
const before = JSON.parse(JSON.stringify(logs));
|
|
normalizeTimestamps(logs);
|
|
expect(logs).toEqual(before);
|
|
});
|
|
|
|
it('re-stamps an outlier date to the majority date', () => {
|
|
const logs = [
|
|
{time: '2026-04-01T10:00:00.000Z', message: 'a'},
|
|
{time: '2026-04-01T11:00:00.000Z', message: 'b'},
|
|
{time: '2026-04-01T12:00:00.000Z', message: 'c'},
|
|
{time: '2026-04-02T15:30:00.000Z', message: 'outlier'},
|
|
];
|
|
const originalTime = logs[3].time;
|
|
normalizeTimestamps(logs);
|
|
const outlier = logs[3];
|
|
// The outlier's date portion should now be 2026-04-01
|
|
expect(outlier.time.startsWith('2026-04-01')).toBe(true);
|
|
// The time should have changed from the original
|
|
expect(outlier.time).not.toBe(originalTime);
|
|
// ID should be regenerated from the new timestamp + hash
|
|
expect(outlier.id).toMatch(/^2026-04-01.*-/);
|
|
});
|
|
|
|
it('does nothing when logs is empty', () => {
|
|
const logs = [];
|
|
expect(() => normalizeTimestamps(logs)).not.toThrow();
|
|
expect(logs).toEqual([]);
|
|
});
|
|
|
|
it('does nothing when every log has an unparseable time', () => {
|
|
const logs = [
|
|
{time: 'not-a-date', message: 'a'},
|
|
{time: 'also-not-a-date', message: 'b'},
|
|
];
|
|
// Should not throw; mostCommonDate would remain null
|
|
expect(() => normalizeTimestamps(logs)).not.toThrow();
|
|
});
|
|
|
|
it('handles a tie by picking one date deterministically (first seen wins via iteration order)', () => {
|
|
// Two days with equal count — either is acceptable, just shouldn't crash
|
|
const logs = [
|
|
{time: '2026-04-01T10:00:00Z', message: 'a'},
|
|
{time: '2026-04-02T10:00:00Z', message: 'b'},
|
|
];
|
|
expect(() => normalizeTimestamps(logs)).not.toThrow();
|
|
// Both should end up on the same day
|
|
const d1 = logs[0].time.split('T')[0];
|
|
const d2 = logs[1].time.split('T')[0];
|
|
expect(d1).toBe(d2);
|
|
});
|
|
});
|