mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-27 04:49:49 -07:00
fix(anki): harden AnkiConnect requests and yomitan runtime fallback
- add request timeout and HTTP status check to verify-known-word-highlights AnkiConnect client - let yomitan-script-runtime search on its own when the resolved manifest path isn't usable - dedupe repeated tier-query setup in known-word-cache-maturity tests via setTierQueries helper
This commit is contained in:
@@ -224,13 +224,19 @@ function readPersistedCacheScope(cachePath: string): string | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ANKI_REQUEST_TIMEOUT_MS = 30_000;
|
||||||
|
|
||||||
function createAnkiClient(url: string) {
|
function createAnkiClient(url: string) {
|
||||||
const request = async (action: string, params: unknown): Promise<unknown> => {
|
const request = async (action: string, params: unknown): Promise<unknown> => {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ action, version: 6, params }),
|
body: JSON.stringify({ action, version: 6, params }),
|
||||||
|
signal: AbortSignal.timeout(ANKI_REQUEST_TIMEOUT_MS),
|
||||||
});
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`AnkiConnect ${action}: HTTP ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
const payload = (await response.json()) as { result: unknown; error: string | null };
|
const payload = (await response.json()) as { result: unknown; error: string | null };
|
||||||
if (payload.error) {
|
if (payload.error) {
|
||||||
throw new Error(`AnkiConnect ${action}: ${payload.error}`);
|
throw new Error(`AnkiConnect ${action}: ${payload.error}`);
|
||||||
|
|||||||
@@ -152,5 +152,6 @@ export async function createYomitanRuntimeStateWithSearch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return createYomitanRuntimeState(userDataPath, resolvedExtensionPath ?? undefined);
|
// No usable manifest at the resolved path, so let the loader search on its own.
|
||||||
|
return createYomitanRuntimeState(userDataPath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,18 @@ function createMaturityHarness(config: AnkiConnectConfig): {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The four queries a maturity refresh issues, in one place so a query-string
|
||||||
|
// change lands in a single spot.
|
||||||
|
function setTierQueries(
|
||||||
|
clientState: { findNotesByQuery: Map<string, number[]> },
|
||||||
|
tiers: { all: number[]; mature: number[]; young: number[]; learning: number[] },
|
||||||
|
): void {
|
||||||
|
clientState.findNotesByQuery.set('deck:"Mining"', tiers.all);
|
||||||
|
clientState.findNotesByQuery.set('deck:"Mining" prop:ivl>=21 -is:learn', tiers.mature);
|
||||||
|
clientState.findNotesByQuery.set('deck:"Mining" prop:ivl>=1 prop:ivl<21 -is:learn', tiers.young);
|
||||||
|
clientState.findNotesByQuery.set('deck:"Mining" is:learn', tiers.learning);
|
||||||
|
}
|
||||||
|
|
||||||
function maturityConfig(overrides: Partial<AnkiConnectConfig> = {}): AnkiConnectConfig {
|
function maturityConfig(overrides: Partial<AnkiConnectConfig> = {}): AnkiConnectConfig {
|
||||||
return {
|
return {
|
||||||
deck: 'Mining',
|
deck: 'Mining',
|
||||||
@@ -131,10 +143,7 @@ test('refresh fetches tier sets and getKnownWordTier classifies notes', async ()
|
|||||||
const { manager, calls, clientState, cleanup } = createMaturityHarness(maturityConfig());
|
const { manager, calls, clientState, cleanup } = createMaturityHarness(maturityConfig());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
clientState.findNotesByQuery.set('deck:"Mining"', [1, 2, 3, 4]);
|
setTierQueries(clientState, { all: [1, 2, 3, 4], mature: [1], young: [2], learning: [3] });
|
||||||
clientState.findNotesByQuery.set('deck:"Mining" prop:ivl>=21 -is:learn', [1]);
|
|
||||||
clientState.findNotesByQuery.set('deck:"Mining" prop:ivl>=1 prop:ivl<21 -is:learn', [2]);
|
|
||||||
clientState.findNotesByQuery.set('deck:"Mining" is:learn', [3]);
|
|
||||||
clientState.notesInfoResult = [
|
clientState.notesInfoResult = [
|
||||||
{ noteId: 1, fields: { Word: { value: '猫' } } },
|
{ noteId: 1, fields: { Word: { value: '猫' } } },
|
||||||
{ noteId: 2, fields: { Word: { value: '犬' } } },
|
{ noteId: 2, fields: { Word: { value: '犬' } } },
|
||||||
@@ -162,10 +171,7 @@ test('a note with cards in several tiers counts as its most mature card', async
|
|||||||
const { manager, clientState, cleanup } = createMaturityHarness(maturityConfig());
|
const { manager, clientState, cleanup } = createMaturityHarness(maturityConfig());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
clientState.findNotesByQuery.set('deck:"Mining"', [1]);
|
setTierQueries(clientState, { all: [1], mature: [1], young: [1], learning: [1] });
|
||||||
clientState.findNotesByQuery.set('deck:"Mining" prop:ivl>=21 -is:learn', [1]);
|
|
||||||
clientState.findNotesByQuery.set('deck:"Mining" prop:ivl>=1 prop:ivl<21 -is:learn', [1]);
|
|
||||||
clientState.findNotesByQuery.set('deck:"Mining" is:learn', [1]);
|
|
||||||
clientState.notesInfoResult = [{ noteId: 1, fields: { Word: { value: '猫' } } }];
|
clientState.notesInfoResult = [{ noteId: 1, fields: { Word: { value: '猫' } } }];
|
||||||
|
|
||||||
await manager.refresh(true);
|
await manager.refresh(true);
|
||||||
@@ -180,10 +186,7 @@ test('a word matched by several notes takes the most mature note tier', async ()
|
|||||||
const { manager, clientState, cleanup } = createMaturityHarness(maturityConfig());
|
const { manager, clientState, cleanup } = createMaturityHarness(maturityConfig());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
clientState.findNotesByQuery.set('deck:"Mining"', [1, 2]);
|
setTierQueries(clientState, { all: [1, 2], mature: [], young: [2], learning: [1] });
|
||||||
clientState.findNotesByQuery.set('deck:"Mining" prop:ivl>=21 -is:learn', []);
|
|
||||||
clientState.findNotesByQuery.set('deck:"Mining" prop:ivl>=1 prop:ivl<21 -is:learn', [2]);
|
|
||||||
clientState.findNotesByQuery.set('deck:"Mining" is:learn', [1]);
|
|
||||||
clientState.notesInfoResult = [
|
clientState.notesInfoResult = [
|
||||||
{ noteId: 1, fields: { Word: { value: '猫' } } },
|
{ noteId: 1, fields: { Word: { value: '猫' } } },
|
||||||
{ noteId: 2, fields: { Word: { value: '猫' } } },
|
{ noteId: 2, fields: { Word: { value: '猫' } } },
|
||||||
@@ -201,10 +204,7 @@ test('tiers are reading-aware for words with several readings', async () => {
|
|||||||
const { manager, clientState, cleanup } = createMaturityHarness(maturityConfig());
|
const { manager, clientState, cleanup } = createMaturityHarness(maturityConfig());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
clientState.findNotesByQuery.set('deck:"Mining"', [1, 2]);
|
setTierQueries(clientState, { all: [1, 2], mature: [1], young: [], learning: [2] });
|
||||||
clientState.findNotesByQuery.set('deck:"Mining" prop:ivl>=21 -is:learn', [1]);
|
|
||||||
clientState.findNotesByQuery.set('deck:"Mining" prop:ivl>=1 prop:ivl<21 -is:learn', []);
|
|
||||||
clientState.findNotesByQuery.set('deck:"Mining" is:learn', [2]);
|
|
||||||
clientState.notesInfoResult = [
|
clientState.notesInfoResult = [
|
||||||
{ noteId: 1, fields: { Word: { value: '床' }, Reading: { value: 'とこ' } } },
|
{ noteId: 1, fields: { Word: { value: '床' }, Reading: { value: 'とこ' } } },
|
||||||
{ noteId: 2, fields: { Word: { value: '床' }, Reading: { value: 'ゆか' } } },
|
{ noteId: 2, fields: { Word: { value: '床' }, Reading: { value: 'ゆか' } } },
|
||||||
@@ -227,10 +227,7 @@ test('reading-only fallback resolves tiers unless opted out', async () => {
|
|||||||
const { manager, clientState, cleanup } = createMaturityHarness(maturityConfig());
|
const { manager, clientState, cleanup } = createMaturityHarness(maturityConfig());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
clientState.findNotesByQuery.set('deck:"Mining"', [1]);
|
setTierQueries(clientState, { all: [1], mature: [1], young: [], learning: [] });
|
||||||
clientState.findNotesByQuery.set('deck:"Mining" prop:ivl>=21 -is:learn', [1]);
|
|
||||||
clientState.findNotesByQuery.set('deck:"Mining" prop:ivl>=1 prop:ivl<21 -is:learn', []);
|
|
||||||
clientState.findNotesByQuery.set('deck:"Mining" is:learn', []);
|
|
||||||
clientState.notesInfoResult = [
|
clientState.notesInfoResult = [
|
||||||
{ noteId: 1, fields: { Word: { value: '警告' }, Reading: { value: 'けいこく' } } },
|
{ noteId: 1, fields: { Word: { value: '警告' }, Reading: { value: 'けいこく' } } },
|
||||||
];
|
];
|
||||||
@@ -305,10 +302,7 @@ test('tiers persist to v4 state and reload without refetching', async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
Date.now = () => 120_000;
|
Date.now = () => 120_000;
|
||||||
clientState.findNotesByQuery.set('deck:"Mining"', [1, 2]);
|
setTierQueries(clientState, { all: [1, 2], mature: [1], young: [], learning: [2] });
|
||||||
clientState.findNotesByQuery.set('deck:"Mining" prop:ivl>=21 -is:learn', [1]);
|
|
||||||
clientState.findNotesByQuery.set('deck:"Mining" prop:ivl>=1 prop:ivl<21 -is:learn', []);
|
|
||||||
clientState.findNotesByQuery.set('deck:"Mining" is:learn', [2]);
|
|
||||||
clientState.notesInfoResult = [
|
clientState.notesInfoResult = [
|
||||||
{ noteId: 1, fields: { Word: { value: '猫' } } },
|
{ noteId: 1, fields: { Word: { value: '猫' } } },
|
||||||
{ noteId: 2, fields: { Word: { value: '犬' } } },
|
{ noteId: 2, fields: { Word: { value: '犬' } } },
|
||||||
@@ -359,10 +353,7 @@ test('appendFromNoteInfo preserves an existing maturity tier', async () => {
|
|||||||
const { manager, clientState, cleanup } = createMaturityHarness(maturityConfig());
|
const { manager, clientState, cleanup } = createMaturityHarness(maturityConfig());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
clientState.findNotesByQuery.set('deck:"Mining"', [7, 8]);
|
setTierQueries(clientState, { all: [7, 8], mature: [7], young: [], learning: [8] });
|
||||||
clientState.findNotesByQuery.set('deck:"Mining" prop:ivl>=21 -is:learn', [7]);
|
|
||||||
clientState.findNotesByQuery.set('deck:"Mining" prop:ivl>=1 prop:ivl<21 -is:learn', []);
|
|
||||||
clientState.findNotesByQuery.set('deck:"Mining" is:learn', [8]);
|
|
||||||
clientState.notesInfoResult = [
|
clientState.notesInfoResult = [
|
||||||
{ noteId: 7, fields: { Word: { value: '猫' } } },
|
{ noteId: 7, fields: { Word: { value: '猫' } } },
|
||||||
{ noteId: 8, fields: { Word: { value: '犬' } } },
|
{ noteId: 8, fields: { Word: { value: '犬' } } },
|
||||||
@@ -389,10 +380,7 @@ test('getKnownWordMatchNoteIds reports the notes behind a tier', async () => {
|
|||||||
const { manager, clientState, cleanup } = createMaturityHarness(maturityConfig());
|
const { manager, clientState, cleanup } = createMaturityHarness(maturityConfig());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
clientState.findNotesByQuery.set('deck:"Mining"', [1, 2, 3]);
|
setTierQueries(clientState, { all: [1, 2, 3], mature: [1], young: [2], learning: [3] });
|
||||||
clientState.findNotesByQuery.set('deck:"Mining" prop:ivl>=21 -is:learn', [1]);
|
|
||||||
clientState.findNotesByQuery.set('deck:"Mining" prop:ivl>=1 prop:ivl<21 -is:learn', [2]);
|
|
||||||
clientState.findNotesByQuery.set('deck:"Mining" is:learn', [3]);
|
|
||||||
clientState.notesInfoResult = [
|
clientState.notesInfoResult = [
|
||||||
{ noteId: 1, fields: { Word: { value: '床' }, Reading: { value: 'とこ' } } },
|
{ noteId: 1, fields: { Word: { value: '床' }, Reading: { value: 'とこ' } } },
|
||||||
{ noteId: 2, fields: { Word: { value: '床' }, Reading: { value: 'ゆか' } } },
|
{ noteId: 2, fields: { Word: { value: '床' }, Reading: { value: 'ゆか' } } },
|
||||||
|
|||||||
Reference in New Issue
Block a user