merge main and harden canonical media recovery

This commit is contained in:
Timo
2026-08-25 16:58:15 +02:00
81 changed files with 3422 additions and 1133 deletions
+1 -1
View File
@@ -65,7 +65,7 @@ For the complete event list, read the `EVENTS` object in [`constants.js`](consta
- `HEARTBEAT_INTERVAL`: content heartbeat interval in milliseconds.
- `FORCE_SYNC_TIMEOUT`: max wait for force-sync ACKs.
- `EPISODE_LOBBY_TIMEOUT`: max wait for episode-lobby readiness.
- `MAX_MEDIA_TIME`: shared relay/extension upper bound for synchronized media positions.
- `MAX_MEDIA_TIME`: shared relay/extension upper bound, in seconds, for synchronized media positions.
## Do Not Break
+115
View File
@@ -0,0 +1,115 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import {
BLACKLIST_DOMAINS,
BLACKLIST_OVERRIDES_STORAGE_KEY,
BLACKLIST_SOURCE_DEFAULT,
BLACKLIST_SOURCE_USER,
CUSTOM_BLACKLIST_STORAGE_KEY,
createEmptyBlacklistOverrides,
deriveBlacklistOverrides,
getBlacklistEntries,
getEffectiveBlacklistDomains,
isUrlBlacklisted,
normalizeBlacklistDomain,
normalizeBlacklistOverrides,
parseBlacklistDomains
} from './blacklist.js';
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
describe('blacklist behavior', () => {
it('normalizes, deduplicates, and rejects unsafe entries', () => {
expect(CUSTOM_BLACKLIST_STORAGE_KEY).toBe('customBlacklistDomains');
expect(BLACKLIST_OVERRIDES_STORAGE_KEY).toBe('blacklistOverrides');
expect(normalizeBlacklistDomain(' Example.COM. ')).toBe('example.com');
expect(normalizeBlacklistDomain('https://Video.Example.com/watch/123')).toBe('video.example.com');
expect(normalizeBlacklistDomain('*.example.com')).toBeNull();
expect(normalizeBlacklistDomain('not a domain')).toBeNull();
expect(parseBlacklistDomains('Example.com\nhttps://sub.example.com/path\nexample.com\n')).toEqual({
domains: ['example.com', 'sub.example.com'],
invalid: []
});
expect(parseBlacklistDomains('example.com\nnot a domain').invalid).toEqual(['not a domain']);
expect(parseBlacklistDomains('# note\nvideos.example\n\n# defaults\ngoogle.com')).toEqual({
domains: ['videos.example', 'google.com'],
invalid: []
});
});
it('matches only exact hosts and their subdomains', () => {
expect(isUrlBlacklisted('https://mail.google.com/inbox', ['google.com'])).toBe(true);
expect(isUrlBlacklisted('https://notgoogle.com/', ['google.com'])).toBe(false);
expect(isUrlBlacklisted('not a url', ['example.com'])).toBe(false);
expect(isUrlBlacklisted('https://drive.google.com/file/d/x/view', BLACKLIST_DOMAINS)).toBe(false);
expect(isUrlBlacklisted('https://drive.google.com/file/d/x/view', ['drive.google.com'])).toBe(true);
expect(isUrlBlacklisted('https://docs.google.com/document/d/x', BLACKLIST_DOMAINS)).toBe(true);
});
it('stores user edits as a delta so future defaults continue to flow in', () => {
expect(createEmptyBlacklistOverrides()).toEqual({ removedDefaults: [], addedDomains: [] });
const edited = BLACKLIST_DOMAINS
.filter(domain => domain !== 'reddit.com' && domain !== 'imgur.com')
.concat(['videos.example']);
const overrides = deriveBlacklistOverrides(edited);
expect(overrides).toEqual({
removedDefaults: ['reddit.com', 'imgur.com'],
addedDomains: ['videos.example']
});
const effective = new Set(getEffectiveBlacklistDomains(overrides));
expect(effective.has('reddit.com')).toBe(false);
expect(effective.has('videos.example')).toBe(true);
const removed = new Set(overrides.removedDefaults);
for (const domain of BLACKLIST_DOMAINS) {
expect(effective.has(domain) || removed.has(domain)).toBe(true);
}
const readded = deriveBlacklistOverrides([...effective, 'reddit.com'], overrides);
expect(new Set(readded.removedDefaults).has('reddit.com')).toBe(false);
expect(deriveBlacklistOverrides(['google.com'], {
removedDefaults: [],
addedDomains: ['google.com']
}).addedDomains).toEqual(['google.com']);
});
it('normalizes legacy and contradictory storage without losing intent', () => {
expect(getEffectiveBlacklistDomains(undefined)).toEqual(BLACKLIST_DOMAINS);
expect(getEffectiveBlacklistDomains([])).toEqual([]);
expect(normalizeBlacklistOverrides({
removedDefaults: ['example.com'],
addedDomains: ['example.com']
})).toEqual({ removedDefaults: [], addedDomains: ['example.com'] });
expect(normalizeBlacklistOverrides('nonsense')).toEqual(createEmptyBlacklistOverrides());
const overrides = { removedDefaults: ['reddit.com'], addedDomains: ['videos.example'] };
const entries = getBlacklistEntries(overrides);
expect(entries.find(entry => entry.domain === 'videos.example')?.source).toBe(BLACKLIST_SOURCE_USER);
expect(entries.find(entry => entry.domain === 'google.com')?.source).toBe(BLACKLIST_SOURCE_DEFAULT);
const rendered = [
'# Your entries',
...entries.filter(entry => entry.source === BLACKLIST_SOURCE_USER).map(entry => entry.domain),
'',
'# Shipped defaults',
...entries.filter(entry => entry.source === BLACKLIST_SOURCE_DEFAULT).map(entry => entry.domain)
].join('\n');
expect(deriveBlacklistOverrides(parseBlacklistDomains(rendered).domains, overrides)).toEqual(
normalizeBlacklistOverrides(overrides)
);
});
});
describe('blacklist integration contracts', () => {
it('keeps storage local and the editor present', () => {
const popupSource = fs.readFileSync(path.join(repoRoot, 'extension/popup.js'), 'utf8');
const popupHtml = fs.readFileSync(path.join(repoRoot, 'extension/popup.html'), 'utf8');
expect(popupSource).toMatch(/chrome\.storage\.local\.set\(\{ \[BLACKLIST_OVERRIDES_STORAGE_KEY\]: overrides \}\)/);
expect(popupSource).not.toMatch(/chrome\.storage\.sync\.set\(\{ \[(?:BLACKLIST_OVERRIDES|CUSTOM_BLACKLIST)_STORAGE_KEY\]/);
expect(popupSource).toMatch(/chrome\.storage\.local\.remove\(CUSTOM_BLACKLIST_STORAGE_KEY\)/);
expect(popupSource).toMatch(/isUrlBlacklisted\(tab\.url, blacklistDomains\)/);
expect(popupHtml).toMatch(/id="blacklistDomains"/);
expect(popupHtml).toMatch(/id="blacklistReset"/);
});
});
+9 -1
View File
@@ -7,7 +7,7 @@
*/
export const PROTOCOL_VERSION = "1.0.0";
export const APP_VERSION = "3.1.3";
export const APP_VERSION = "3.1.5";
export const OFFICIAL_SERVER_URL = 'wss://syncserver.koalastuff.net';
export const OFFICIAL_LANDING_PAGE_URL = 'https://sync.koalastuff.net';
@@ -65,6 +65,14 @@ export const EVENTS = {
PONG: "pong" // server responds with same { t } for client RTT calculation
};
// Stable server error identifiers. Clients must branch on these codes instead
// of localized or user-facing message text whenever the error changes session
// state.
export const ERROR_CODES = {
ROOM_CLOSED: 'room_closed',
PEER_TIMED_OUT: 'peer_timed_out'
};
// Room control modes (Host Control Mode feature).
// NOTE: content.js does not import this module — it uses the string literals
// 'everyone' / 'host-only' directly. Keep these values in sync there.
+41
View File
@@ -0,0 +1,41 @@
import { describe, expect, it } from 'vitest';
import { generateUsername, getAvatarForName, USERNAME_ADJECTIVES, USERNAME_NOUNS } from './names.js';
describe('generated peer names', () => {
it.each([
['Koala', '🐨'],
['koala', '🐨'],
['MyKoalaUser', '🐨'],
['Tiger', '🐯'],
['Panda', '🐼'],
['Fox', '🦊'],
['CaterpillarCat', '🐛'],
['Cat', '🐱'],
['Polar', '🐻\u200D❄️'],
['Crow', '🐦\u200D⬛'],
['Ninja', '🥷'],
['Wizard', '🧙'],
['Pirate', '🏴'],
['Alien', '👾'],
['Robot', '🤖']
])('maps %s to %s', (name, avatar) => {
expect(getAvatarForName(name)).toBe(avatar);
});
it.each(['', 'Xyzzy123', null, undefined])('uses the fallback for %j', name => {
expect(getAvatarForName(name)).toBe('👤');
});
it('generates only adjective-noun combinations', () => {
for (let sample = 0; sample < 100; sample++) {
const name = generateUsername();
expect(name).toMatch(/^[A-Z][a-z]+[A-Z][a-z]+$/);
expect(USERNAME_ADJECTIVES.some(adjective => name.startsWith(adjective))).toBe(true);
expect(USERNAME_NOUNS.some(noun => name.endsWith(noun))).toBe(true);
}
});
it('defines an avatar for every generated noun', () => {
for (const noun of USERNAME_NOUNS) expect(getAvatarForName(noun)).not.toBe('👤');
});
});