test: cut low-value tests, rewrite the rest around behavior, parallelize lanes (#285)

This commit is contained in:
sudacode authored and GitHub committed 2026-10-05 00:07:44 -07:00
1 parent 4b6842d034
commit b48083ddd8
368 files changed
+18045 -60331

No files matched your search

@@ -121,18 +121,3 @@ test('readSnapshot ignores snapshots written with an older format version', asyn
assert.equal(await readSnapshot(snapshotPath), null);
});
test('readSnapshot ignores v15 snapshots with stale romanized character-name entries', async () => {
const outputDir = makeTempDir();
const snapshotPath = getSnapshotPath(outputDir, 130298);
const staleSnapshot = {
...createSnapshot(),
formatVersion: 15,
termEntries: [['Vanir', 'ばにる', 'name primary', '', 75, ['Vanir'], 0, '']],
};
fs.mkdirSync(path.dirname(snapshotPath), { recursive: true });
fs.writeFileSync(snapshotPath, JSON.stringify(staleSnapshot), 'utf8');
assert.equal(await readSnapshot(snapshotPath), null);
});
@@ -161,6 +161,36 @@ test('getManualSelectionSnapshot hydrates override episode count from searched c
}
});
test('getManualSelectionSnapshot falls back to mpv current video path when app media path is not ready', async () => {
const userDataPath = makeTempDir();
const mpvPath =
'C:\\Videos\\KonoSuba - God’s blessing on this wonderful world!! (2016) - S02E05.mkv';
const calls: Array<{ mediaPath: string | null; mediaTitle: string | null }> = [];
const runtime = createCharacterDictionaryRuntimeService({
userDataPath,
getCurrentMediaPath: () => null,
getCurrentVideoPath: () => mpvPath,
getCurrentMediaTitle: () => null,
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
guessAnilistMediaInfo: async (mediaPath, mediaTitle) => {
calls.push({ mediaPath, mediaTitle });
return {
title: 'KonoSuba - God’s blessing on this wonderful world!!',
season: 2,
episode: 5,
source: 'fallback',
};
},
now: () => 1_700_000_000_000,
});
const snapshot = await runtime.getManualSelectionSnapshot(undefined, '');
assert.deepEqual(calls, [{ mediaPath: mpvPath, mediaTitle: null }]);
assert.equal(snapshot.guessTitle, 'KonoSuba - God’s blessing on this wonderful world!!');
assert.equal(snapshot.candidates.length, 0);
});
test('resolvePinnedMediaId returns the manual override for the current season', async () => {
const userDataPath = makeTempDir();
const seasonThreePath = '/anime/Oregairu/My Teen Romantic Comedy SNAFU (2013) - S03E01.mkv';
@@ -106,12 +106,14 @@ test('returns null for a media with no cached snapshot', async () => {
});
});
test('key changes when the snapshot content changes', async () => {
test('invalidate picks up a snapshot change within the recheck interval and changes the key', async () => {
await withTempDir(async (dir) => {
writeSnapshot(dir, 1, [['ミナト', 'みなと']]);
// Frozen clock: only invalidate() can trigger the refresh below.
const lookup = createCharacterNameCandidateLookup({
outputDir: dir,
getCurrentMediaId: () => 1,
now: () => 1_000_000,
});
const first = await waitForRefresh(() => lookup.get());
@@ -162,31 +164,3 @@ test('does not re-read the snapshot directory on every lookup', async () => {
assert.equal(refreshed.forms.length, 4, 'expected a refresh past the interval');
});
});
test('invalidate picks up a snapshot change on the next refresh', async () => {
await withTempDir(async (dir) => {
writeSnapshot(dir, 1, [['ミナト', 'みなと']]);
let nowMs = 1_000_000;
const lookup = createCharacterNameCandidateLookup({
outputDir: dir,
getCurrentMediaId: () => 1,
now: () => nowMs,
});
await waitForRefresh(() => lookup.get());
assert.equal(lookup.get()?.forms.length, 2);
writeSnapshot(dir, 1, [
['ミナト', 'みなと'],
['アクア', 'あくあ'],
]);
nowMs += 1;
lookup.invalidate();
const refreshed = await waitForRefresh(() => {
const candidates = lookup.get();
return candidates && candidates.forms.length === 4 ? candidates : null;
});
assert.equal(refreshed.forms.length, 4);
});
});
@@ -0,0 +1,253 @@
import assert from 'node:assert/strict';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import type { TestContext } from 'node:test';
import { createCharacterDictionaryRuntimeService } from '../character-dictionary-runtime';
import { ANILIST_GRAPHQL_URL } from './constants';
import type { CharacterDictionaryRuntimeDeps, CharacterDictionaryTermEntry } from './types';
// Shared fixtures for the character dictionary runtime tests: a fake AniList GraphQL + image
// server, a runtime factory with Eminence in Shadow defaults, and readers for the built ZIP.
export const PNG_1X1 = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+nmX8AAAAASUVORK5CYII=',
'base64',
);
export function makeTempDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-character-dictionary-'));
}
type FetchHandler = (url: string, init?: RequestInit) => Promise<Response> | Response;
/** Replaces `globalThis.fetch` for the duration of the test. */
export function installFetch(t: TestContext, handler: FetchHandler): void {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
return handler(url, init);
}) as typeof globalThis.fetch;
t.after(() => {
globalThis.fetch = originalFetch;
});
}
export function jsonResponse(body: unknown): Response {
return new Response(JSON.stringify(body), {
status: 200,
headers: { 'content-type': 'application/json' },
});
}
/** AniList `characters.edges[]` item, as returned by the GraphQL API. */
export type FakeAniListCharacterEdge = {
role: string;
voiceActors?: Array<{
id: number;
name: { full?: string; native?: string };
image?: { large?: string | null; medium?: string | null };
}>;
node: {
id: number;
description?: string;
image?: { large?: string | null; medium?: string | null } | null;
name: {
full?: string;
native?: string;
first?: string | null;
last?: string | null;
alternative?: string[];
};
gender?: string;
age?: string;
dateOfBirth?: { month: number; day: number };
bloodType?: string;
};
};
export type FakeAniListMedia = {
id: number;
episodes?: number;
title: { romaji?: string; english?: string; native?: string };
/** Search text that resolves to this media. Unmatched searches resolve to the first media. */
search?: string;
characters?: FakeAniListCharacterEdge[];
};
export type FakeAniList = {
/** Every fetched URL, in request order. */
requests: string[];
searchCount: number;
characterPageCount: number;
imageRequests: string[];
};
/**
* Serves AniList media search and character page queries from `media`, and image downloads for
* `images` (PNG) and `missingImages` (404). Any other request throws.
*/
export function installFakeAniList(
t: TestContext,
options: { media?: FakeAniListMedia[]; images?: string[]; missingImages?: string[] } = {},
): FakeAniList {
const media = options.media ?? [];
const images = new Set(options.images ?? []);
const missingImages = new Set(options.missingImages ?? []);
const fake: FakeAniList = {
requests: [],
searchCount: 0,
characterPageCount: 0,
imageRequests: [],
};
installFetch(t, (url, init) => {
fake.requests.push(url);
if (url === ANILIST_GRAPHQL_URL) {
const body = JSON.parse(String(init?.body ?? '{}')) as {
query?: string;
variables?: { search?: string; id?: number };
};
if (body.query?.includes('Page(perPage: 10)') && media.length > 0) {
fake.searchCount += 1;
const match = media.find((entry) => entry.search === body.variables?.search) ?? media[0]!;
return jsonResponse({
data: {
Page: { media: [{ id: match.id, episodes: match.episodes ?? 12, title: match.title }] },
},
});
}
const pageMedia = media.find((entry) => entry.id === body.variables?.id);
if (body.query?.includes('characters(page: $page') && pageMedia) {
fake.characterPageCount += 1;
return jsonResponse({
data: {
Media: {
title: pageMedia.title,
characters: {
pageInfo: { hasNextPage: false },
edges: pageMedia.characters ?? [],
},
},
},
});
}
}
if (images.has(url) || missingImages.has(url)) {
fake.imageRequests.push(url);
return images.has(url)
? new Response(PNG_1X1, { status: 200, headers: { 'content-type': 'image/png' } })
: new Response('missing', { status: 404, headers: { 'content-type': 'text/plain' } });
}
throw new Error(`Unexpected fetch URL: ${url}`);
});
return fake;
}
export const EMINENCE_MEDIA_ID = 130298;
export const EMINENCE_TITLE = {
romaji: 'Kage no Jitsuryokusha ni Naritakute!',
english: 'The Eminence in Shadow',
native: '陰の実力者になりたくて!',
};
export type CharacterDictionaryRuntime = ReturnType<typeof createCharacterDictionaryRuntimeService>;
/** Creates a runtime watching an Eminence in Shadow episode. AniList pacing sleeps are skipped. */
export function createRuntime(overrides: Partial<CharacterDictionaryRuntimeDeps> = {}): {
runtime: CharacterDictionaryRuntime;
userDataPath: string;
outputDir: string;
} {
const userDataPath = overrides.userDataPath ?? makeTempDir();
const runtime = createCharacterDictionaryRuntimeService({
getCurrentMediaPath: () => '/tmp/eminence-s01e05.mkv',
getCurrentMediaTitle: () => 'The Eminence in Shadow - S01E05',
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
guessAnilistMediaInfo: async () => ({
title: 'The Eminence in Shadow',
season: null,
episode: 5,
source: 'fallback',
}),
now: () => 1_700_000_000_000,
sleep: async () => undefined,
...overrides,
userDataPath,
});
return { runtime, userDataPath, outputDir: path.join(userDataPath, 'character-dictionaries') };
}
const LOCAL_FILE_HEADER_SIGNATURE = 0x04034b50;
const CENTRAL_DIRECTORY_SIGNATURE = 0x02014b50;
const END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50;
export function readStoredZipEntry(zipPath: string, entryName: string): Buffer {
const archive = fs.readFileSync(zipPath);
let offset = 0;
while (offset + 4 <= archive.length) {
const signature = archive.readUInt32LE(offset);
if (
signature === CENTRAL_DIRECTORY_SIGNATURE ||
signature === END_OF_CENTRAL_DIRECTORY_SIGNATURE
) {
break;
}
if (signature !== LOCAL_FILE_HEADER_SIGNATURE) {
throw new Error(`Unexpected ZIP signature 0x${signature.toString(16)} at offset ${offset}`);
}
const compressionMethod = archive.readUInt16LE(offset + 8);
assert.equal(compressionMethod, 0, 'expected stored ZIP entry');
const compressedSize = archive.readUInt32LE(offset + 18);
const fileNameLength = archive.readUInt16LE(offset + 26);
const extraFieldLength = archive.readUInt16LE(offset + 28);
const fileNameStart = offset + 30;
const fileNameEnd = fileNameStart + fileNameLength;
const fileName = archive.subarray(fileNameStart, fileNameEnd).toString('utf8');
const dataStart = fileNameEnd + extraFieldLength;
const dataEnd = dataStart + compressedSize;
if (fileName === entryName) {
return archive.subarray(dataStart, dataEnd);
}
offset = dataEnd;
}
throw new Error(`ZIP entry not found: ${entryName}`);
}
export function readTermBank(zipPath: string): CharacterDictionaryTermEntry[] {
return JSON.parse(
readStoredZipEntry(zipPath, 'term_bank_1.json').toString('utf8'),
) as CharacterDictionaryTermEntry[];
}
type StructuredNode = { tag?: string; open?: boolean; content?: unknown };
/** Top-level structured-content children of a term entry's glossary. */
export function getGlossaryChildren(entry: CharacterDictionaryTermEntry): StructuredNode[] {
const glossary = entry[5][0] as { content: { content: StructuredNode[] } };
return glossary.content.content;
}
/** Finds a collapsible `details` section by its summary title. */
export function findSection(
children: StructuredNode[],
title: string,
): (StructuredNode & { content: StructuredNode[] }) | undefined {
return children.find(
(child): child is StructuredNode & { content: StructuredNode[] } =>
child.tag === 'details' &&
Array.isArray(child.content) &&
(child.content[0] as StructuredNode | undefined)?.content === title,
);
}
@@ -1,219 +1,135 @@
import assert from 'node:assert/strict';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import test from 'node:test';
import test, { type TestContext } from 'node:test';
import { createCharacterDictionaryRuntimeService } from '../character-dictionary-runtime';
import { getSnapshotPath, writeSnapshot } from './cache';
import { CHARACTER_DICTIONARY_FORMAT_VERSION } from './constants';
import type { CharacterDictionarySnapshot, NameSplitTokenizer } from './types';
import { ANILIST_GRAPHQL_URL, CHARACTER_DICTIONARY_FORMAT_VERSION } from './constants';
import {
EMINENCE_MEDIA_ID,
EMINENCE_TITLE,
PNG_1X1,
createRuntime,
installFakeAniList,
installFetch,
jsonResponse,
type FakeAniListCharacterEdge,
} from './runtime-test-harness';
import type {
CharacterDictionaryRuntimeDeps,
CharacterDictionarySnapshot,
NameSplitTokenizer,
} from './types';
const GRAPHQL_URL = 'https://graphql.anilist.co';
const PNG_1X1 = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+nmX8AAAAASUVORK5CYII=',
'base64',
);
function makeTempDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-character-dictionary-'));
}
function createSnapshotWithoutImages(): CharacterDictionarySnapshot {
function createSnapshotWithoutImages(
overrides: Partial<CharacterDictionarySnapshot> = {},
): CharacterDictionarySnapshot {
return {
formatVersion: CHARACTER_DICTIONARY_FORMAT_VERSION,
mediaId: 130298,
mediaId: EMINENCE_MEDIA_ID,
mediaTitle: 'The Eminence in Shadow',
entryCount: 1,
updatedAt: 1_700_000_000_000,
termEntries: [['アレクシア', 'あれくしあ', 'name primary', '', 75, ['Alexia'], 0, '']],
images: [],
...overrides,
};
}
test('generateForCurrentMedia refreshes same-version snapshots missing images when inline images are enabled', async () => {
const userDataPath = makeTempDir();
const outputDir = path.join(userDataPath, 'character-dictionaries');
await writeSnapshot(getSnapshotPath(outputDir, 130298), createSnapshotWithoutImages());
const originalFetch = globalThis.fetch;
const fetchUrls: string[] = [];
/** Seeds a cached snapshot, then returns a runtime that will find it for the current episode. */
async function createRuntimeWithSnapshot(
snapshot: CharacterDictionarySnapshot,
overrides: Partial<CharacterDictionaryRuntimeDeps> = {},
) {
const harness = createRuntime({ now: () => 1_700_000_000_500, ...overrides });
await writeSnapshot(getSnapshotPath(harness.outputDir, EMINENCE_MEDIA_ID), snapshot);
return harness;
}
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
fetchUrls.push(url);
function readStoredSnapshot(outputDir: string): CharacterDictionarySnapshot {
return JSON.parse(
fs.readFileSync(getSnapshotPath(outputDir, EMINENCE_MEDIA_ID), 'utf8'),
) as CharacterDictionarySnapshot;
}
if (url === GRAPHQL_URL) {
const body = JSON.parse(String(init?.body ?? '{}')) as {
query?: string;
};
if (body.query?.includes('characters(page: $page')) {
return new Response(
JSON.stringify({
data: {
Media: {
title: {
english: 'The Eminence in Shadow',
},
characters: {
pageInfo: { hasNextPage: false },
edges: [
{
role: 'SUPPORTING',
node: {
id: 123,
description: 'Alexia Midgar.',
image: {
large: 'https://cdn.example.com/character-123.png',
medium: null,
},
name: {
full: 'Alexia Midgar',
native: 'アレクシア・ミドガル',
},
},
},
],
},
},
},
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
}
function installCharacterPage(t: TestContext, edge: FakeAniListCharacterEdge, images?: string[]) {
return installFakeAniList(t, {
media: [
{ id: EMINENCE_MEDIA_ID, title: { english: EMINENCE_TITLE.english }, characters: [edge] },
],
images,
});
}
if (url === 'https://cdn.example.com/character-123.png') {
return new Response(PNG_1X1, {
status: 200,
headers: { 'content-type': 'image/png' },
});
}
test('generateForCurrentMedia refreshes same-version snapshots missing images when inline images are enabled', async (t) => {
const imageUrl = 'https://cdn.example.com/character-123.png';
const anilist = installCharacterPage(
t,
{
role: 'SUPPORTING',
node: {
id: 123,
description: 'Alexia Midgar.',
image: { large: imageUrl, medium: null },
name: { full: 'Alexia Midgar', native: 'アレクシア・ミドガル' },
},
},
[imageUrl],
);
const { runtime, outputDir } = await createRuntimeWithSnapshot(createSnapshotWithoutImages(), {
getNameMatchImagesEnabled: () => true,
});
throw new Error(`Unexpected fetch URL: ${url}`);
}) as typeof globalThis.fetch;
const result = await runtime.generateForCurrentMedia();
try {
const runtime = createCharacterDictionaryRuntimeService({
userDataPath,
getCurrentMediaPath: () => '/tmp/eminence-s01e05.mkv',
getCurrentMediaTitle: () => 'The Eminence in Shadow - S01E05',
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
guessAnilistMediaInfo: async () => ({
title: 'The Eminence in Shadow',
season: null,
episode: 5,
source: 'fallback',
}),
getNameMatchImagesEnabled: () => true,
now: () => 1_700_000_000_500,
});
const result = await runtime.generateForCurrentMedia();
const refreshedSnapshot = JSON.parse(
fs.readFileSync(getSnapshotPath(outputDir, 130298), 'utf8'),
) as CharacterDictionarySnapshot;
assert.equal(result.fromCache, false);
assert.ok(fetchUrls.includes(GRAPHQL_URL));
assert.ok(refreshedSnapshot.images.some((image) => image.path === 'img/m130298-c123.png'));
} finally {
globalThis.fetch = originalFetch;
}
assert.equal(result.fromCache, false);
assert.equal(anilist.characterPageCount, 1);
assert.ok(
readStoredSnapshot(outputDir).images.some((image) => image.path === 'img/m130298-c123.png'),
);
});
async function runNameSplitRefreshScenario(tokenizeJapaneseName: NameSplitTokenizer): Promise<{
async function runNameSplitRefreshScenario(
t: TestContext,
tokenizeJapaneseName: NameSplitTokenizer,
): Promise<{
characterPageRequests: number;
firstResultFromCache: boolean;
refreshedNameSplitSource: CharacterDictionarySnapshot['nameSplitSource'];
secondResultFromCache: boolean;
}> {
const userDataPath = makeTempDir();
const outputDir = path.join(userDataPath, 'character-dictionaries');
await writeSnapshot(getSnapshotPath(outputDir, 130298), {
...createSnapshotWithoutImages(),
nameSplitSource: 'heuristic',
const anilist = installCharacterPage(t, {
role: 'SUPPORTING',
node: {
id: 123,
description: 'Alexia Midgar.',
image: { large: null, medium: null },
name: { first: 'Taro', last: 'Yamada', full: 'Taro Yamada', native: '山田太郎' },
},
});
const originalFetch = globalThis.fetch;
let characterPageRequests = 0;
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
if (url === GRAPHQL_URL) {
const body = JSON.parse(String(init?.body ?? '{}')) as { query?: string };
if (body.query?.includes('characters(page: $page')) {
characterPageRequests += 1;
return new Response(
JSON.stringify({
data: {
Media: {
title: { english: 'The Eminence in Shadow' },
characters: {
pageInfo: { hasNextPage: false },
edges: [
{
role: 'SUPPORTING',
node: {
id: 123,
description: 'Alexia Midgar.',
image: { large: null, medium: null },
name: {
first: 'Taro',
last: 'Yamada',
full: 'Taro Yamada',
native: '山田太郎',
},
},
},
],
},
},
},
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
}
throw new Error(`Unexpected fetch URL: ${url}`);
}) as typeof globalThis.fetch;
try {
const runtime = createCharacterDictionaryRuntimeService({
userDataPath,
getCurrentMediaPath: () => '/tmp/eminence-s01e05.mkv',
getCurrentMediaTitle: () => 'The Eminence in Shadow - S01E05',
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
guessAnilistMediaInfo: async () => ({
title: 'The Eminence in Shadow',
season: null,
episode: 5,
source: 'fallback',
}),
const { runtime, outputDir } = await createRuntimeWithSnapshot(
createSnapshotWithoutImages({ nameSplitSource: 'heuristic' }),
{
getNameMatchImagesEnabled: () => false,
tokenizeJapaneseName,
getJapaneseNameTokenizerAvailable: () => true,
now: () => 1_700_000_000_500,
});
},
);
const firstResult = await runtime.generateForCurrentMedia();
const refreshedSnapshot = JSON.parse(
fs.readFileSync(getSnapshotPath(outputDir, 130298), 'utf8'),
) as CharacterDictionarySnapshot;
const secondResult = await runtime.generateForCurrentMedia();
const firstResult = await runtime.generateForCurrentMedia();
const refreshedSnapshot = readStoredSnapshot(outputDir);
const secondResult = await runtime.generateForCurrentMedia();
return {
characterPageRequests,
firstResultFromCache: firstResult.fromCache,
refreshedNameSplitSource: refreshedSnapshot.nameSplitSource,
secondResultFromCache: secondResult.fromCache,
};
} finally {
globalThis.fetch = originalFetch;
}
return {
characterPageRequests: anilist.characterPageCount,
firstResultFromCache: firstResult.fromCache,
refreshedNameSplitSource: refreshedSnapshot.nameSplitSource,
secondResultFromCache: secondResult.fromCache,
};
}
test('generateForCurrentMedia keeps failed MeCab name split refreshes retryable', async () => {
test('generateForCurrentMedia keeps failed MeCab name split refreshes retryable', async (t) => {
let tokenizerCalls = 0;
const result = await runNameSplitRefreshScenario(async () => {
const result = await runNameSplitRefreshScenario(t, async () => {
tokenizerCalls += 1;
return null;
});
@@ -225,9 +141,9 @@ test('generateForCurrentMedia keeps failed MeCab name split refreshes retryable'
assert.equal(tokenizerCalls, 2);
});
test('generateForCurrentMedia caches completed MeCab refreshes with no resolved splits', async () => {
test('generateForCurrentMedia caches completed MeCab refreshes with no resolved splits', async (t) => {
let tokenizerCalls = 0;
const result = await runNameSplitRefreshScenario(async () => {
const result = await runNameSplitRefreshScenario(t, async () => {
tokenizerCalls += 1;
return [];
});
@@ -239,129 +155,53 @@ test('generateForCurrentMedia caches completed MeCab refreshes with no resolved
assert.equal(tokenizerCalls, 1);
});
test('generateForCurrentMedia keeps mecab-split snapshots when MeCab is available', async () => {
const userDataPath = makeTempDir();
const outputDir = path.join(userDataPath, 'character-dictionaries');
await writeSnapshot(getSnapshotPath(outputDir, 130298), {
...createSnapshotWithoutImages(),
nameSplitSource: 'mecab',
});
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (input: string | URL | Request) => {
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
throw new Error(`Unexpected fetch URL: ${url}`);
}) as typeof globalThis.fetch;
try {
const runtime = createCharacterDictionaryRuntimeService({
userDataPath,
getCurrentMediaPath: () => '/tmp/eminence-s01e05.mkv',
getCurrentMediaTitle: () => 'The Eminence in Shadow - S01E05',
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
guessAnilistMediaInfo: async () => ({
title: 'The Eminence in Shadow',
season: null,
episode: 5,
source: 'fallback',
}),
const keptSnapshotCases: Array<{
name: string;
snapshot: CharacterDictionarySnapshot;
deps: Partial<CharacterDictionaryRuntimeDeps>;
}> = [
{
name: 'keeps mecab-split snapshots when MeCab is available',
snapshot: createSnapshotWithoutImages({ nameSplitSource: 'mecab' }),
deps: {
getNameMatchImagesEnabled: () => false,
tokenizeJapaneseName: async () => null,
getJapaneseNameTokenizerAvailable: () => true,
now: () => 1_700_000_000_500,
});
const result = await runtime.generateForCurrentMedia();
assert.equal(result.fromCache, true);
} finally {
globalThis.fetch = originalFetch;
}
});
test('generateForCurrentMedia keeps heuristic-split snapshots while MeCab is unavailable', async () => {
const userDataPath = makeTempDir();
const outputDir = path.join(userDataPath, 'character-dictionaries');
await writeSnapshot(getSnapshotPath(outputDir, 130298), {
...createSnapshotWithoutImages(),
nameSplitSource: 'heuristic',
});
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (input: string | URL | Request) => {
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
throw new Error(`Unexpected fetch URL: ${url}`);
}) as typeof globalThis.fetch;
try {
const runtime = createCharacterDictionaryRuntimeService({
userDataPath,
getCurrentMediaPath: () => '/tmp/eminence-s01e05.mkv',
getCurrentMediaTitle: () => 'The Eminence in Shadow - S01E05',
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
guessAnilistMediaInfo: async () => ({
title: 'The Eminence in Shadow',
season: null,
episode: 5,
source: 'fallback',
}),
},
},
{
name: 'keeps heuristic-split snapshots while MeCab is unavailable',
snapshot: createSnapshotWithoutImages({ nameSplitSource: 'heuristic' }),
deps: {
getNameMatchImagesEnabled: () => false,
tokenizeJapaneseName: async () => null,
getJapaneseNameTokenizerAvailable: () => false,
now: () => 1_700_000_000_500,
});
},
},
{
name: 'keeps same-version snapshots without images when inline images are disabled',
snapshot: createSnapshotWithoutImages(),
deps: { getNameMatchImagesEnabled: () => false },
},
];
for (const { name, snapshot, deps } of keptSnapshotCases) {
test(`generateForCurrentMedia ${name}`, async (t) => {
// No media or images: any AniList request throws.
installFakeAniList(t);
const { runtime } = await createRuntimeWithSnapshot(snapshot, deps);
const result = await runtime.generateForCurrentMedia();
assert.equal(result.fromCache, true);
} finally {
globalThis.fetch = originalFetch;
}
});
});
}
test('generateForCurrentMedia keeps same-version snapshots without images when inline images are disabled', async () => {
const userDataPath = makeTempDir();
const outputDir = path.join(userDataPath, 'character-dictionaries');
await writeSnapshot(getSnapshotPath(outputDir, 130298), createSnapshotWithoutImages());
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (input: string | URL | Request) => {
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
throw new Error(`Unexpected fetch URL: ${url}`);
}) as typeof globalThis.fetch;
try {
const runtime = createCharacterDictionaryRuntimeService({
userDataPath,
getCurrentMediaPath: () => '/tmp/eminence-s01e05.mkv',
getCurrentMediaTitle: () => 'The Eminence in Shadow - S01E05',
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
guessAnilistMediaInfo: async () => ({
title: 'The Eminence in Shadow',
season: null,
episode: 5,
source: 'fallback',
}),
getNameMatchImagesEnabled: () => false,
now: () => 1_700_000_000_500,
});
const result = await runtime.generateForCurrentMedia();
assert.equal(result.fromCache, true);
} finally {
globalThis.fetch = originalFetch;
}
});
test('an unresolvable season is not cached as a normal AniList match', async () => {
const userDataPath = makeTempDir();
const originalFetch = globalThis.fetch;
test('an unresolvable season is not cached as a normal AniList match', async (t) => {
let searchCalls = 0;
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
if (url !== GRAPHQL_URL) {
installFetch(t, (url, init) => {
if (url !== ANILIST_GRAPHQL_URL) {
return new Response(PNG_1X1, { status: 200, headers: { 'content-type': 'image/png' } });
}
@@ -371,84 +211,66 @@ test('an unresolvable season is not cached as a normal AniList match', async ()
};
if (body.query?.includes('characters(page: $page')) {
return new Response(
JSON.stringify({
data: {
Media: {
title: { english: 'My Teen Romantic Comedy SNAFU' },
characters: {
pageInfo: { hasNextPage: false },
edges: [
{
role: 'MAIN',
node: { id: 1, name: { full: 'Hachiman Hikigaya', native: '比企谷八幡' } },
},
],
},
return jsonResponse({
data: {
Media: {
title: { english: 'My Teen Romantic Comedy SNAFU' },
characters: {
pageInfo: { hasNextPage: false },
edges: [
{
role: 'MAIN',
node: { id: 1, name: { full: 'Hachiman Hikigaya', native: '比企谷八幡' } },
},
],
},
},
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
},
});
}
if (typeof body.variables?.id === 'number') {
// No sequel edges: season 3 is unreachable from the season 1 anchor.
return new Response(JSON.stringify({ data: { Media: { relations: { edges: [] } } } }), {
status: 200,
headers: { 'content-type': 'application/json' },
});
return jsonResponse({ data: { Media: { relations: { edges: [] } } } });
}
searchCalls += 1;
return new Response(
JSON.stringify({
data: {
Page: {
media: [
{
id: 14813,
episodes: 13,
format: 'TV',
seasonYear: 2013,
title: { romaji: null, english: 'My Teen Romantic Comedy SNAFU', native: null },
},
],
},
return jsonResponse({
data: {
Page: {
media: [
{
id: 14813,
episodes: 13,
format: 'TV',
seasonYear: 2013,
title: { romaji: null, english: 'My Teen Romantic Comedy SNAFU', native: null },
},
],
},
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}) as typeof globalThis.fetch;
},
});
});
const warnings: string[] = [];
try {
const runtime = createCharacterDictionaryRuntimeService({
userDataPath,
getCurrentMediaPath: () =>
'/anime/Oregairu/My Teen Romantic Comedy SNAFU (2013) - S03E01.mkv',
getCurrentMediaTitle: () => null,
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
guessAnilistMediaInfo: async () => ({
title: 'My Teen Romantic Comedy SNAFU',
year: 2013,
season: 3,
episode: 1,
source: 'guessit',
}),
now: () => 1_700_000_000_000,
sleep: async () => {},
logWarn: (message) => warnings.push(message),
});
const { runtime } = createRuntime({
getCurrentMediaPath: () => '/anime/Oregairu/My Teen Romantic Comedy SNAFU (2013) - S03E01.mkv',
getCurrentMediaTitle: () => null,
guessAnilistMediaInfo: async () => ({
title: 'My Teen Romantic Comedy SNAFU',
year: 2013,
season: 3,
episode: 1,
source: 'guessit',
}),
logWarn: (message) => warnings.push(message),
});
await runtime.getOrCreateCurrentSnapshot();
const searchesAfterFirst = searchCalls;
await runtime.getOrCreateCurrentSnapshot();
await runtime.getOrCreateCurrentSnapshot();
const searchesAfterFirst = searchCalls;
await runtime.getOrCreateCurrentSnapshot();
// Re-resolved rather than served from the resolution cache, and warned both times.
assert.ok(searchCalls > searchesAfterFirst);
assert.equal(warnings.filter((m) => /could not find season 3/i.test(m)).length, 2);
} finally {
globalThis.fetch = originalFetch;
}
// Re-resolved rather than served from the resolution cache, and warned both times.
assert.ok(searchCalls > searchesAfterFirst);
assert.equal(warnings.filter((m) => /could not find season 3/i.test(m)).length, 2);
});