Files
kfj001 3e02a6ae74
CI / test (pull_request) Successful in 5m41s
F1 BLOCKING — walk() cycle detection (src/viewer/index.js:161)
Added a per-tree WeakSet visited-guard so the probe terminates. The depth cap alone didn't stop the infinite recursion: window/_parent/_top/frames/_globalObject/_globalProxy are mutually-reachable circular Proxies, so Strategy H's window scan re-walked the same identities along exponentially many paths. The set is declared inside walk (not the outer function) so the separate player trees stay independent. This is what was hanging npm test (exit 124); it now completes in ~15s.
F2 SHOULD-FIX — doc/behavior mismatch (src/background/background.js:299-322)
Reworded the referer-capture comment block. It claimed the referer was read "via onBeforeSendHeaders, which sees the request headers," but the code uses onBeforeRequest + chrome.tabs.get (deriving the referer from the source tab's URL). Now states the actual mechanism and documents why onBeforeSendHeaders+extraHeaders was avoided (extraInfoSpec is rejected by some Chromium builds and fails the whole SW).
F3 SHOULD-FIX — leaked settings file
git rm --cached .claude/settings.local.json and added it to .gitignore. It carried machine-specific absolute paths (/Users/kyle/.claude/...).
F4 FYI — package-lock churn (package-lock.json)
Removed the spurious root "dependencies": { @rollup/rollup-darwin-arm64 } and restored the dev: true / optional: true flags on that platform package so it matches the rest of the @rollup/rollup-* siblings.
F5 FYI — real provider hostnames in test fixtures
Swapped phncdn.com→neutral example.net (registrable-domain sibling test) and thisvid.com→example.com in data_flow/hls_qa/simple_video/referer_proxy test files, rewording comments that named the real sites. src/ and __tests__/ now have zero phncdn/thisvid/pornhub references.
2026-08-30 21:56:05 +00:00

220 lines
8.3 KiB
JavaScript

import { describe, it } from './runner.mjs';
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const schema = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '../src/viewer/schema.json'), 'utf8'));
const ajv = new Ajv({ strict: false });
addFormats(ajv);
const validate = ajv.compile(schema);
describe('Data flow: popup -> storage -> viewer', () => {
it('should pass a scraped HLS entry through schema validation', () => {
// Simulate popup scraping an HLS stream
const scrapedEntry = {
url: 'https://stream.example.com/watch',
src: 'https://cdn.example.com/live/playlist.m3u8',
poster: 'https://cdn.example.com/live/thumb.jpg',
tab: 'Live Broadcast',
hlsManifest: 'https://cdn.example.com/live/playlist.m3u8',
mediaType: 'hls',
};
// Validate against schema (as storage layer would)
const valid = validate(scrapedEntry);
if (!valid) {
throw new Error(`Scraped HLS entry fails schema: ${JSON.stringify(validate.errors)}`);
}
// Verify the viewer would detect it as HLS
const isHls = (entry) => typeof entry === 'object' && entry.mediaType === 'hls';
if (!isHls(scrapedEntry)) {
throw new Error('Scraped HLS entry not detected as HLS by viewer');
}
// Verify the viewer would resolve the manifest
const resolveHlsManifest = (entry) => {
if (typeof entry === 'object' && entry.hlsManifest) return entry.hlsManifest;
return null;
};
const manifest = resolveHlsManifest(scrapedEntry);
if (manifest !== 'https://cdn.example.com/live/playlist.m3u8') {
throw new Error(`Manifest URL wrong: ${manifest}`);
}
});
it('should pass a scraped direct entry through schema validation', () => {
const scrapedEntry = {
url: 'https://video.example.com/watch',
src: 'https://cdn.example.com/video.mp4',
poster: 'https://cdn.example.com/thumb.jpg',
tab: 'Video Page',
};
const valid = validate(scrapedEntry);
if (!valid) {
throw new Error(`Scraped direct entry fails schema: ${JSON.stringify(validate.errors)}`);
}
const isHls = (entry) => typeof entry === 'object' && entry.mediaType === 'hls';
if (isHls(scrapedEntry)) {
throw new Error('Direct entry falsely detected as HLS');
}
const resolveSrc = (entry) => {
if (typeof entry === 'string') return entry;
return entry.src || entry.url || '';
};
const src = resolveSrc(scrapedEntry);
if (src !== 'https://cdn.example.com/video.mp4') {
throw new Error(`Src URL wrong: ${src}`);
}
});
it('should survive a round-trip: storage serialization -> deserialization', () => {
const originalEntry = {
url: 'https://example.com/stream',
src: 'https://cdn.example.com/stream.m3u8',
poster: 'https://example.com/thumb.jpg',
tab: 'HLS Stream',
hlsManifest: 'https://cdn.example.com/stream.m3u8',
mediaType: 'hls',
};
// Simulate storage round-trip (JSON serialization)
const serialized = JSON.stringify(originalEntry);
const deserialized = JSON.parse(serialized);
// Validate after deserialization
const valid = validate(deserialized);
if (!valid) {
throw new Error(`Round-tripped entry fails schema: ${JSON.stringify(validate.errors)}`);
}
// Verify fields are intact
if (deserialized.mediaType !== 'hls') {
throw new Error('mediaType lost during round-trip');
}
if (deserialized.hlsManifest !== 'https://cdn.example.com/stream.m3u8') {
throw new Error('hlsManifest lost during round-trip');
}
// Verify viewer still detects it correctly
const isHls = (entry) => typeof entry === 'object' && entry.mediaType === 'hls';
if (!isHls(deserialized)) {
throw new Error('Round-tripped entry not detected as HLS');
}
});
it('should handle batch entries from popup -> storage -> viewer pipeline', () => {
// Simulate popup storing multiple entries
const popupEntries = [
{ url: 'https://example.com/1', src: 'https://cdn.example.com/1.mp4', tab: 'Tab 1' },
{
url: 'https://example.com/2',
src: 'https://cdn.example.com/2.m3u8',
hlsManifest: 'https://cdn.example.com/2.m3u8',
mediaType: 'hls',
tab: 'Tab 2',
},
{ url: 'https://example.com/3', src: 'https://cdn.example.com/3.webm', mediaType: 'direct', tab: 'Tab 3' },
];
// Validate all entries
for (const [i, entry] of popupEntries.entries()) {
const valid = validate(entry);
if (!valid) {
throw new Error(`Entry ${i} fails schema: ${JSON.stringify(validate.errors)}`);
}
}
// Simulate storage round-trip
const serialized = JSON.stringify(popupEntries);
const deserialized = JSON.parse(serialized);
// Verify viewer can process all entries
const isHls = (entry) => typeof entry === 'object' && entry.mediaType === 'hls';
const resolveSrc = (entry) => {
if (typeof entry === 'string') return entry;
return entry.src || entry.url || '';
};
const resolveHlsManifest = (entry) => {
if (typeof entry === 'object' && entry.hlsManifest) return entry.hlsManifest;
return null;
};
// Entry 0: legacy (no mediaType)
if (isHls(deserialized[0])) throw new Error('Entry 0 should not be HLS');
if (resolveSrc(deserialized[0]) !== 'https://cdn.example.com/1.mp4') throw new Error('Entry 0 src wrong');
// Entry 1: HLS
if (!isHls(deserialized[1])) throw new Error('Entry 1 should be HLS');
if (resolveHlsManifest(deserialized[1]) !== 'https://cdn.example.com/2.m3u8') throw new Error('Entry 1 manifest wrong');
// Entry 2: explicit direct
if (isHls(deserialized[2])) throw new Error('Entry 2 should not be HLS');
if (resolveSrc(deserialized[2]) !== 'https://cdn.example.com/3.webm') throw new Error('Entry 2 src wrong');
});
it('should reject malformed entries that could crash the viewer', () => {
const malformedEntries = [
{ hlsManifest: 'https://example.com/stream.m3u8' }, // missing url
{ url: 'https://example.com/video.mp4', mediaType: 'invalid' }, // invalid mediaType
{ url: 'not-a-url' }, // technically valid per schema (format: uri not enforced by Ajv without custom formats)
];
for (const [i, entry] of malformedEntries.entries()) {
const valid = validate(entry);
if (!valid && i < 2) {
// Expected failures for entries 0 and 1
continue;
}
}
// Entry 0 (missing url) should be rejected
const entry0Valid = validate(malformedEntries[0]);
if (entry0Valid) {
throw new Error('Entry without url should be rejected');
}
// Entry 1 (invalid mediaType) should be rejected
const entry1Valid = validate(malformedEntries[1]);
if (entry1Valid) {
throw new Error('Entry with invalid mediaType should be rejected');
}
});
it('an HLS entry carrying a scan-time referer passes schema + round-trips', () => {
// The referer is bound to the entry at scan time so it survives the
// watch-later flow (the source tab may be long closed). Because the schema
// sets additionalProperties:false, a `referer` the store carries must be an
// explicit property or storage-layer validation would reject the entry.
const scrapedEntry = {
url: 'https://pageA.example.com/watch',
src: 'https://cdn.example.com/hls/live/master.m3u8',
hlsManifest: 'https://cdn.example.com/hls/live/master.m3u8',
mediaType: 'hls',
poster: 'https://pageA.example.com/thumb.jpg',
tab: 'Referer-protected stream',
referer: 'https://pageA.example.com/watch',
};
const valid = validate(scrapedEntry);
if (!valid) {
throw new Error(`Entry with referer fails schema: ${JSON.stringify(validate.errors)}`);
}
// The referer must survive the storage round-trip intact so the viewer can
// send it to the proxy when the user opens a previously-scanned entry.
const roundTripped = JSON.parse(JSON.stringify(scrapedEntry));
if (roundTripped.referer !== 'https://pageA.example.com/watch') {
throw new Error('referer lost during round-trip');
}
if (!validate(roundTripped)) {
throw new Error('Round-tripped entry with referer fails schema');
}
});
});