mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-27 16:49:51 -07:00
refactor(anki): parse the known-word cache in one shared module
The cache manager and the stats server each carried a hand-written parser for the same file. When the format went to v4 for maturity tiers only the manager was updated, so loadKnownWordsSet fell through to null and every session reported 0 known words from an intact cache. Move the state union, its parser, and a knownWordsFromState accessor into known-word-cache-format and have both readers go through it. The stats server no longer mentions a version at all, and both version switches close with assertNever, so adding a V5 is a compile error at every consumer instead of a silently empty result. An unparseable cache now warns rather than reading as "no known words". Two stats fixtures omitted refreshedAtMs/scope and only passed because the stats reader was laxer than the manager; they now carry the fields every cache the app writes has always had.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import {
|
||||
KnownWordCacheState,
|
||||
knownWordsFromState,
|
||||
parseKnownWordCacheState,
|
||||
} from './known-word-cache-format';
|
||||
|
||||
function parseOrThrow(value: unknown): KnownWordCacheState {
|
||||
const parsed = parseKnownWordCacheState(value);
|
||||
assert.ok(parsed, 'expected the payload to parse');
|
||||
return parsed;
|
||||
}
|
||||
|
||||
const BASE = { refreshedAtMs: 1, scope: 'deck:test' };
|
||||
|
||||
test('known-word cache format reads words from every version the union covers', () => {
|
||||
assert.deepEqual(
|
||||
knownWordsFromState(parseOrThrow({ ...BASE, version: 1, words: ['する'] })),
|
||||
new Set(['する']),
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
knownWordsFromState(
|
||||
parseOrThrow({ ...BASE, version: 2, words: ['する'], notes: { '1': ['する'] } }),
|
||||
),
|
||||
new Set(['する']),
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
knownWordsFromState(
|
||||
parseOrThrow({
|
||||
...BASE,
|
||||
version: 3,
|
||||
notes: { '1': [{ word: 'する', reading: 'する' }], '2': [{ word: '猫', reading: null }] },
|
||||
}),
|
||||
),
|
||||
new Set(['する', '猫']),
|
||||
);
|
||||
|
||||
// v4 only adds `tiers`; the words a reader sees must not change.
|
||||
assert.deepEqual(
|
||||
knownWordsFromState(
|
||||
parseOrThrow({
|
||||
...BASE,
|
||||
version: 4,
|
||||
notes: { '1': [{ word: 'する', reading: 'する' }], '2': [{ word: '猫', reading: null }] },
|
||||
tiers: { '1': 'mature', '2': 'young' },
|
||||
}),
|
||||
),
|
||||
new Set(['する', '猫']),
|
||||
);
|
||||
});
|
||||
|
||||
test('known-word cache format rejects payloads that are not a known cache state', () => {
|
||||
const notes = { '1': [{ word: 'する', reading: null }] };
|
||||
|
||||
assert.equal(parseKnownWordCacheState(null), null);
|
||||
assert.equal(parseKnownWordCacheState('{}'), null);
|
||||
|
||||
// An unknown version must not be read as an empty cache.
|
||||
assert.equal(parseKnownWordCacheState({ ...BASE, version: 99, notes }), null);
|
||||
|
||||
assert.equal(
|
||||
parseKnownWordCacheState({ version: 4, scope: 'deck:test', notes, tiers: {} }),
|
||||
null,
|
||||
);
|
||||
assert.equal(parseKnownWordCacheState({ version: 4, refreshedAtMs: 1, notes, tiers: {} }), null);
|
||||
|
||||
// v4 without its tiers map is a v3 payload mislabelled as v4.
|
||||
assert.equal(parseKnownWordCacheState({ ...BASE, version: 4, notes }), null);
|
||||
|
||||
// v3 carries entry objects, not the bare strings v2 used.
|
||||
assert.equal(parseKnownWordCacheState({ ...BASE, version: 3, notes: { '1': ['する'] } }), null);
|
||||
});
|
||||
Reference in New Issue
Block a user