Files
SubMiner/src/anki-integration/known-word-cache-format.test.ts
T
sudacode 48c36aa195 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.
2026-07-26 01:33:04 -07:00

77 lines
2.3 KiB
TypeScript

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);
});