mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-17 06:47:48 +00:00
feat: store the hide-clutter list as a delta, not a snapshot
Saving the editor used to persist the full effective list, which froze the
shipped defaults at that moment: every default added in a later version
never reached anyone who had touched the list once.
Storage now keeps only { removedDefaults, addedDomains }, and the effective
list is resolved against whatever ships in the running version. Entries are
tagged default or user, the editor groups them under comment headers, and
'#' lines are ignored on save. Lists saved by earlier versions are migrated
to the delta form on first read and the legacy key is removed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,10 +6,17 @@ import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
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 '../shared/blacklist.js';
|
||||
|
||||
@@ -34,9 +41,92 @@ assert.equal(isUrlBlacklisted('https://mail.google.com/inbox', ['google.com']),
|
||||
assert.equal(isUrlBlacklisted('https://notgoogle.com/', ['google.com']), false, 'lookalike domains do not match');
|
||||
assert.equal(isUrlBlacklisted('not a url', ['example.com']), false, 'invalid URLs are ignored');
|
||||
|
||||
// --- Delta storage: shipped defaults keep flowing in after the user edits ---
|
||||
|
||||
assert.equal(BLACKLIST_OVERRIDES_STORAGE_KEY, 'blacklistOverrides');
|
||||
assert.deepEqual(createEmptyBlacklistOverrides(), { removedDefaults: [], addedDomains: [] });
|
||||
|
||||
// A user who removes two defaults and adds one of their own.
|
||||
const edited = BLACKLIST_DOMAINS
|
||||
.filter(domain => domain !== 'reddit.com' && domain !== 'imgur.com')
|
||||
.concat(['videos.example']);
|
||||
const overrides = deriveBlacklistOverrides(edited);
|
||||
assert.deepEqual(overrides.removedDefaults, ['reddit.com', 'imgur.com'], 'only the removed defaults are stored');
|
||||
assert.deepEqual(overrides.addedDomains, ['videos.example'], 'only the added domains are stored');
|
||||
|
||||
const effective = getEffectiveBlacklistDomains(overrides);
|
||||
assert.equal(effective.includes('reddit.com'), false, 'a removed default stays removed');
|
||||
assert.equal(effective.includes('videos.example'), true, 'an added domain stays added');
|
||||
|
||||
// The property that makes newly shipped defaults reach existing users: every
|
||||
// shipped domain the user did not explicitly remove is part of the result, so a
|
||||
// default added in a later version cannot be missing from a stored delta.
|
||||
const removedSet = new Set(overrides.removedDefaults);
|
||||
for (const domain of BLACKLIST_DOMAINS) {
|
||||
assert.equal(
|
||||
effective.includes(domain) || removedSet.has(domain),
|
||||
true,
|
||||
`shipped default ${domain} must be present unless explicitly removed`
|
||||
);
|
||||
}
|
||||
|
||||
// Legacy full-list snapshots migrate to the delta form.
|
||||
assert.deepEqual(
|
||||
deriveBlacklistOverrides(edited),
|
||||
normalizeBlacklistOverrides(overrides),
|
||||
'a legacy snapshot produces the same delta'
|
||||
);
|
||||
assert.deepEqual(getEffectiveBlacklistDomains(undefined), BLACKLIST_DOMAINS, 'no stored delta uses shipped defaults');
|
||||
assert.deepEqual(getEffectiveBlacklistDomains([]), [], 'a legacy empty snapshot still means no filtering');
|
||||
|
||||
// Re-adding a removed default clears the removal instead of stacking state.
|
||||
const readded = deriveBlacklistOverrides(effective.concat(['reddit.com']), overrides);
|
||||
assert.equal(readded.removedDefaults.includes('reddit.com'), false, 're-adding a default clears its removal');
|
||||
|
||||
// A domain the user added explicitly stays tagged as theirs even once the same
|
||||
// domain ships as a default, so dropping the default does not drop their entry.
|
||||
const stillUser = deriveBlacklistOverrides(['google.com'], { removedDefaults: [], addedDomains: ['google.com'] });
|
||||
assert.deepEqual(stillUser.addedDomains, ['google.com'], 'an explicit addition survives becoming a default');
|
||||
|
||||
// Contradictory stored state resolves in favour of the addition.
|
||||
assert.deepEqual(
|
||||
normalizeBlacklistOverrides({ removedDefaults: ['example.com'], addedDomains: ['example.com'] }),
|
||||
{ removedDefaults: [], addedDomains: ['example.com'] },
|
||||
'a domain cannot be removed and added at once'
|
||||
);
|
||||
assert.deepEqual(normalizeBlacklistOverrides('nonsense'), createEmptyBlacklistOverrides(), 'garbage storage falls back to defaults');
|
||||
|
||||
// Entries are tagged so the editor can show what came from where.
|
||||
const entries = getBlacklistEntries(overrides);
|
||||
assert.equal(entries.find(e => e.domain === 'videos.example').source, BLACKLIST_SOURCE_USER);
|
||||
assert.equal(entries.find(e => e.domain === 'google.com').source, BLACKLIST_SOURCE_DEFAULT);
|
||||
|
||||
// Comment lines are editor notes, not domains, and never count as invalid.
|
||||
const withComments = parseBlacklistDomains('# your entries\nvideos.example\n\n#shipped defaults\ngoogle.com');
|
||||
assert.deepEqual(withComments.domains, ['videos.example', 'google.com'], 'comment lines are skipped');
|
||||
assert.deepEqual(withComments.invalid, [], 'comment lines are not reported as invalid');
|
||||
|
||||
// Round trip through the grouped editor body: rendering with comment headers
|
||||
// and saving it again must not change the stored delta.
|
||||
const rendered = [
|
||||
'# Your entries',
|
||||
...entries.filter(e => e.source === BLACKLIST_SOURCE_USER).map(e => e.domain),
|
||||
'',
|
||||
'# Shipped defaults',
|
||||
...entries.filter(e => e.source === BLACKLIST_SOURCE_DEFAULT).map(e => e.domain)
|
||||
].join('\n');
|
||||
const roundTripped = parseBlacklistDomains(rendered);
|
||||
assert.deepEqual(roundTripped.invalid, [], 'the rendered editor body contains no invalid entries');
|
||||
assert.deepEqual(
|
||||
deriveBlacklistOverrides(roundTripped.domains, overrides),
|
||||
normalizeBlacklistOverrides(overrides),
|
||||
'render then save leaves the delta unchanged'
|
||||
);
|
||||
|
||||
const popupSource = fs.readFileSync(path.join(repoRoot, 'extension/popup.js'), 'utf8');
|
||||
assert.match(popupSource, /chrome\.storage\.local\.set\(\{ \[CUSTOM_BLACKLIST_STORAGE_KEY\]: domains \}\)/, 'custom list is saved locally');
|
||||
assert.doesNotMatch(popupSource, /chrome\.storage\.sync\.set\(\{ \[CUSTOM_BLACKLIST_STORAGE_KEY\]/, 'custom list is never synced');
|
||||
assert.match(popupSource, /chrome\.storage\.local\.set\(\{ \[BLACKLIST_OVERRIDES_STORAGE_KEY\]: overrides \}\)/, 'the delta is saved locally');
|
||||
assert.doesNotMatch(popupSource, /chrome\.storage\.sync\.set\(\{ \[(?:BLACKLIST_OVERRIDES|CUSTOM_BLACKLIST)_STORAGE_KEY\]/, 'the list is never synced');
|
||||
assert.match(popupSource, /chrome\.storage\.local\.remove\(CUSTOM_BLACKLIST_STORAGE_KEY\)/, 'the legacy snapshot is cleaned up after migration');
|
||||
assert.match(popupSource, /isUrlBlacklisted\(tab\.url, blacklistDomains\)/, 'tab filtering uses the effective custom list');
|
||||
|
||||
// A broad parent domain must not hide a host with a dedicated player path,
|
||||
|
||||
Reference in New Issue
Block a user