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

205 lines
7.5 KiB
JavaScript

/**
* Tests for components/semantic_search.js
*
* Focus on the pure data-shaping helpers exposed via window.SemanticSearch:
* - buildTieredResults: 3-tier merge with dedup + sort
* - isSafeExternalUrl: URL scheme validation (security-critical)
*
* createSemanticResultCard and renderSnippet do DOM/markdown work and depend
* on optional libraries (marked, DOMPurify) — exercised via integration in
* library-search tests already.
*/
import '@js/components/semantic_search.js';
const SS = window.SemanticSearch;
describe('SemanticSearch.buildTieredResults', () => {
it('returns empty tiers for empty inputs', () => {
const r = SS.buildTieredResults([], []);
expect(r.tier1).toEqual([]);
expect(r.tier2).toEqual([]);
expect(r.tier3).toEqual([]);
});
it('puts text-only matches in tier2 (preserving original order)', () => {
const text = [{ id: 'a' }, { id: 'b' }, { id: 'c' }];
const r = SS.buildTieredResults(text, []);
expect(r.tier1).toEqual([]);
expect(r.tier3).toEqual([]);
expect(r.tier2.map(x => x.historyItem.id)).toEqual(['a', 'b', 'c']);
});
it('puts semantic-only matches in tier3', () => {
const sem = [
{ research_id: 'x', similarity: 0.5 },
{ research_id: 'y', similarity: 0.8 },
];
const r = SS.buildTieredResults([], sem);
expect(r.tier1).toEqual([]);
expect(r.tier2).toEqual([]);
expect(r.tier3).toHaveLength(2);
});
it('places items appearing in both tiers in tier1 with semanticMatch populated', () => {
const text = [{ id: 'a' }, { id: 'b' }];
const sem = [
{ research_id: 'a', similarity: 0.7, snippet: 'hi' },
{ research_id: 'c', similarity: 0.9 },
];
const r = SS.buildTieredResults(text, sem);
expect(r.tier1).toHaveLength(1);
expect(r.tier1[0].historyItem.id).toBe('a');
expect(r.tier1[0].semanticMatch.similarity).toBe(0.7);
expect(r.tier1[0].semanticMatch.snippet).toBe('hi');
// 'b' was text-only → tier2
expect(r.tier2.map(x => x.historyItem.id)).toEqual(['b']);
// 'c' was semantic-only → tier3
expect(r.tier3.map(x => x.semanticResult.research_id)).toEqual(['c']);
});
it('sorts tier1 by similarity DESC', () => {
const text = [{ id: 'a' }, { id: 'b' }, { id: 'c' }];
const sem = [
{ research_id: 'a', similarity: 0.3 },
{ research_id: 'b', similarity: 0.9 },
{ research_id: 'c', similarity: 0.6 },
];
const r = SS.buildTieredResults(text, sem);
expect(r.tier1.map(x => x.historyItem.id)).toEqual(['b', 'c', 'a']);
});
it('sorts tier3 by similarity DESC', () => {
const sem = [
{ research_id: 'a', similarity: 0.2 },
{ research_id: 'b', similarity: 0.95 },
{ research_id: 'c', similarity: 0.5 },
];
const r = SS.buildTieredResults([], sem);
expect(r.tier3.map(x => x.semanticResult.research_id)).toEqual(['b', 'c', 'a']);
});
it('dedups semantic results by ID, keeping the highest similarity', () => {
const sem = [
{ research_id: 'dup', similarity: 0.3 },
{ research_id: 'dup', similarity: 0.8 },
{ research_id: 'dup', similarity: 0.5 },
];
const r = SS.buildTieredResults([], sem);
expect(r.tier3).toHaveLength(1);
expect(r.tier3[0].semanticResult.similarity).toBe(0.8);
});
it('skips semantic results missing the configured ID key', () => {
const sem = [
{ research_id: 'a', similarity: 0.5 },
{ similarity: 0.9 }, // missing research_id
{ research_id: '', similarity: 0.7 }, // empty falsy
];
const r = SS.buildTieredResults([], sem);
expect(r.tier3).toHaveLength(1);
expect(r.tier3[0].semanticResult.research_id).toBe('a');
});
it('honors custom textIdKey and semanticIdKey', () => {
const text = [{ doc_id: 'x' }];
const sem = [{ id: 'x', similarity: 0.5 }];
const r = SS.buildTieredResults(text, sem, {
textIdKey: 'doc_id',
semanticIdKey: 'id',
});
expect(r.tier1).toHaveLength(1);
expect(r.tier1[0].historyItem.doc_id).toBe('x');
});
it('uses default keys when options is undefined', () => {
const text = [{ id: 'k' }];
const sem = [{ research_id: 'k', similarity: 0.5 }];
const r = SS.buildTieredResults(text, sem);
expect(r.tier1).toHaveLength(1);
});
it('coerces IDs to strings for matching (number vs string)', () => {
const text = [{ id: 42 }];
const sem = [{ research_id: '42', similarity: 0.5 }];
const r = SS.buildTieredResults(text, sem);
expect(r.tier1).toHaveLength(1);
});
it('defaults snippet to empty string when missing', () => {
const text = [{ id: 'a' }];
const sem = [{ research_id: 'a', similarity: 0.5 }];
const r = SS.buildTieredResults(text, sem);
expect(r.tier1[0].semanticMatch.snippet).toBe('');
});
});
describe('SemanticSearch.isSafeExternalUrl', () => {
let savedValidator;
beforeEach(() => {
// Force the fallback path so we test the inline scheme list,
// not URLValidator's behavior (covered by url-validator tests).
savedValidator = window.URLValidator;
delete window.URLValidator;
});
afterEach(() => {
if (savedValidator !== undefined) window.URLValidator = savedValidator;
});
it('accepts http and https URLs', () => {
expect(SS.isSafeExternalUrl('http://example.com')).toBe(true);
expect(SS.isSafeExternalUrl('https://example.com/path?q=1')).toBe(true);
});
it('rejects javascript: URLs', () => {
expect(SS.isSafeExternalUrl('javascript:alert(1)')).toBe(false);
});
it('rejects data: URLs', () => {
expect(SS.isSafeExternalUrl('data:text/html,<script>alert(1)</script>')).toBe(false);
});
it('rejects vbscript: URLs', () => {
expect(SS.isSafeExternalUrl('vbscript:msgbox(1)')).toBe(false);
});
it('rejects about:, blob:, file: schemes', () => {
expect(SS.isSafeExternalUrl('about:blank')).toBe(false);
expect(SS.isSafeExternalUrl('blob:https://example.com/abc')).toBe(false);
expect(SS.isSafeExternalUrl('file:///etc/passwd')).toBe(false);
});
it('rejects non-string input', () => {
expect(SS.isSafeExternalUrl(null)).toBe(false);
expect(SS.isSafeExternalUrl(undefined)).toBe(false);
expect(SS.isSafeExternalUrl(42)).toBe(false);
expect(SS.isSafeExternalUrl({})).toBe(false);
});
it('rejects empty string', () => {
expect(SS.isSafeExternalUrl('')).toBe(false);
});
it('is case-insensitive against scheme obfuscation', () => {
expect(SS.isSafeExternalUrl('JaVaScRiPt:alert(1)')).toBe(false);
});
it('rejects relative URLs (no scheme)', () => {
expect(SS.isSafeExternalUrl('/relative/path')).toBe(false);
expect(SS.isSafeExternalUrl('example.com')).toBe(false);
});
it('delegates to URLValidator when present', () => {
window.URLValidator = {
isSafeUrl: vi.fn().mockReturnValue(true),
};
expect(SS.isSafeExternalUrl('https://example.com')).toBe(true);
expect(window.URLValidator.isSafeUrl).toHaveBeenCalledWith(
'https://example.com',
{ allowMailto: false }
);
});
});