diff --git a/src/anki-integration/known-word-cache-maturity.test.ts b/src/anki-integration/known-word-cache-maturity.test.ts index f3af5b40..6128af49 100644 --- a/src/anki-integration/known-word-cache-maturity.test.ts +++ b/src/anki-integration/known-word-cache-maturity.test.ts @@ -20,6 +20,7 @@ function createMaturityHarness(config: AnkiConnectConfig): { findNotesResult: number[]; notesInfoResult: HarnessNoteInfo[]; findNotesByQuery: Map; + failedQueries: Set; }; createSiblingManager: () => KnownWordCacheManager; cleanup: () => void; @@ -31,12 +32,16 @@ function createMaturityHarness(config: AnkiConnectConfig): { findNotesResult: [] as number[], notesInfoResult: [] as HarnessNoteInfo[], findNotesByQuery: new Map(), + failedQueries: new Set(), }; const deps = { client: { findNotes: async (query: string) => { calls.findNotes += 1; calls.queries.push(query); + if (clientState.failedQueries.has(query)) { + throw new Error(`Anki unavailable for query: ${query}`); + } if (clientState.findNotesByQuery.has(query)) { return clientState.findNotesByQuery.get(query) ?? []; } @@ -220,7 +225,10 @@ test('reading-only fallback resolves tiers unless opted out', async () => { await manager.refresh(true); assert.equal(manager.getKnownWordTier('けいこく'), 'mature'); - assert.equal(manager.getKnownWordTier('けいこく', undefined, { allowReadingOnlyMatch: false }), null); + assert.equal( + manager.getKnownWordTier('けいこく', undefined, { allowReadingOnlyMatch: false }), + null, + ); } finally { cleanup(); } @@ -245,6 +253,29 @@ test('getKnownWordTier returns null and skips tier queries when maturity is disa } }); +test('refresh preserves known-word cache when maturity lookup fails', async () => { + const { manager, statePath, clientState, cleanup } = createMaturityHarness(maturityConfig()); + + try { + clientState.findNotesByQuery.set('deck:"Mining"', [1]); + clientState.failedQueries.add('deck:"Mining" prop:ivl>=21'); + clientState.notesInfoResult = [{ noteId: 1, fields: { Word: { value: '猫' } } }]; + + await manager.refresh(true); + + assert.equal(manager.isKnownWord('猫'), true); + assert.equal(manager.getKnownWordTier('猫'), null); + const persisted = JSON.parse(fs.readFileSync(statePath, 'utf-8')) as { + version: number; + tiers: Record; + }; + assert.equal(persisted.version, 4); + assert.deepEqual(persisted.tiers, {}); + } finally { + cleanup(); + } +}); + test('tiers persist to v4 state and reload without refetching', async () => { const { manager, calls, statePath, clientState, createSiblingManager, cleanup } = createMaturityHarness(maturityConfig()); diff --git a/src/anki-integration/known-word-cache.ts b/src/anki-integration/known-word-cache.ts index 15db5a21..6c130850 100644 --- a/src/anki-integration/known-word-cache.ts +++ b/src/anki-integration/known-word-cache.ts @@ -389,13 +389,18 @@ export class KnownWordCacheManager { this.isRefreshingKnownWords = true; try { const noteFieldsById = await this.fetchKnownWordNoteFieldsById(); - const tierSets = this.isMaturityTrackingEnabled() - ? await fetchKnownWordMaturityTierSets( + let tierSets = null; + if (this.isMaturityTrackingEnabled()) { + try { + tierSets = await fetchKnownWordMaturityTierSets( (query, options) => this.deps.client.findNotes(query, options), this.getKnownWordQueryScopes().map((scope) => scope.query), getMatureIntervalThresholdDays(this.deps.getConfig()), - ) - : null; + ); + } catch (error) { + log.warn('Failed to fetch known-word maturity tiers:', (error as Error).message); + } + } const currentNoteIds = Array.from(noteFieldsById.keys()).sort((a, b) => a - b); if (this.noteEntriesById.size === 0) {