Files
SubMiner/src/main/character-dictionary-runtime.test.ts

2044 lines
65 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { createCharacterDictionaryRuntimeService } from './character-dictionary-runtime';
const GRAPHQL_URL = 'https://graphql.anilist.co';
const LOCAL_FILE_HEADER_SIGNATURE = 0x04034b50;
const CENTRAL_DIRECTORY_SIGNATURE = 0x02014b50;
const END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50;
const PNG_1X1 = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+nmX8AAAAASUVORK5CYII=',
'base64',
);
function makeTempDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-character-dictionary-'));
}
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}`);
}
test('generateForCurrentMedia emits structured-content glossary so image stays with text', async () => {
const userDataPath = makeTempDir();
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;
if (url === GRAPHQL_URL) {
const body = JSON.parse(String(init?.body ?? '{}')) as {
query?: string;
variables?: Record<string, unknown>;
};
if (body.query?.includes('Page(perPage: 10)')) {
return new Response(
JSON.stringify({
data: {
Page: {
media: [
{
id: 130298,
episodes: 20,
title: {
romaji: 'Kage no Jitsuryokusha ni Naritakute!',
english: 'The Eminence in Shadow',
native: '陰の実力者になりたくて!',
},
},
],
},
},
}),
{
status: 200,
headers: { 'content-type': 'application/json' },
},
);
}
if (body.query?.includes('characters(page: $page')) {
return new Response(
JSON.stringify({
data: {
Media: {
title: {
romaji: 'Kage no Jitsuryokusha ni Naritakute!',
english: 'The Eminence in Shadow',
native: '陰の実力者になりたくて!',
},
characters: {
pageInfo: { hasNextPage: false },
edges: [
{
role: 'SUPPORTING',
node: {
id: 123,
description:
'__Race:__ Human\nAlexia Midgar is the second princess of the Kingdom of Midgar.',
image: {
large: 'https://example.com/alexia.png',
medium: null,
},
name: {
full: 'Alexia Midgar',
native: 'アレクシア・ミドガル',
},
},
},
],
},
},
},
}),
{
status: 200,
headers: { 'content-type': 'application/json' },
},
);
}
}
if (url === 'https://example.com/alexia.png') {
return new Response(PNG_1X1, {
status: 200,
headers: { 'content-type': 'image/png' },
});
}
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',
episode: 5,
source: 'fallback',
}),
now: () => 1_700_000_000_000,
});
const result = await runtime.generateForCurrentMedia();
const termBank = JSON.parse(
readStoredZipEntry(result.zipPath, 'term_bank_1.json').toString('utf8'),
) as Array<
[
string,
string,
string,
string,
number,
Array<string | Record<string, unknown>>,
number,
string,
]
>;
const alexia = termBank.find(([term]) => term === 'アレクシア');
assert.ok(alexia, 'expected compact native-name variant for character');
const glossary = alexia[5];
assert.equal(glossary.length, 1);
const entry = glossary[0] as {
type: string;
content: { tag: string; content: Array<Record<string, unknown>> };
};
assert.equal(entry.type, 'structured-content');
const wrapper = entry.content;
assert.equal(wrapper.tag, 'div');
const children = wrapper.content;
const nameDiv = children[0] as { tag: string; content: string };
assert.equal(nameDiv.tag, 'div');
assert.equal(nameDiv.content, 'アレクシア・ミドガル');
const secondaryNameDiv = children[1] as { tag: string; content: string };
assert.equal(secondaryNameDiv.tag, 'div');
assert.equal(secondaryNameDiv.content, 'Alexia Midgar');
const imageWrap = children[2] as { tag: string; content: Record<string, unknown> };
assert.equal(imageWrap.tag, 'div');
const image = imageWrap.content as Record<string, unknown>;
assert.equal(image.tag, 'img');
assert.equal(image.path, 'img/m130298-c123.png');
assert.equal(image.sizeUnits, 'em');
const sourceDiv = children[3] as { tag: string; content: string };
assert.equal(sourceDiv.tag, 'div');
assert.ok(sourceDiv.content.includes('The Eminence in Shadow'));
const roleBadgeDiv = children[4] as { tag: string; content: Record<string, unknown> };
assert.equal(roleBadgeDiv.tag, 'div');
const badge = roleBadgeDiv.content as { tag: string; content: string };
assert.equal(badge.tag, 'span');
assert.equal(badge.content, 'Side Character');
const descSection = children.find(
(c) =>
(c as { tag?: string }).tag === 'details' &&
Array.isArray((c as { content?: unknown[] }).content) &&
(c as { content: Array<{ content?: string }> }).content[0]?.content === 'Description',
) as { tag: string; open?: boolean; content: Array<Record<string, unknown>> } | undefined;
assert.ok(descSection, 'expected Description collapsible section');
assert.equal(descSection.open, false);
const descBody = descSection.content[1] as { content: string };
assert.ok(
descBody.content.includes('Alexia Midgar is the second princess of the Kingdom of Midgar.'),
);
const infoSection = children.find(
(c) =>
(c as { tag?: string }).tag === 'details' &&
Array.isArray((c as { content?: unknown[] }).content) &&
(c as { content: Array<{ content?: string }> }).content[0]?.content ===
'Character Information',
) as { tag: string; open?: boolean; content: Array<Record<string, unknown>> } | undefined;
assert.ok(
infoSection,
'expected Character Information collapsible section with parsed __Race:__ field',
);
assert.equal(infoSection.open, false);
const topLevelImageGlossaryEntry = glossary.find(
(item) =>
typeof item === 'object' && item !== null && (item as { type?: string }).type === 'image',
);
assert.equal(topLevelImageGlossaryEntry, undefined);
} finally {
globalThis.fetch = originalFetch;
}
});
test('generateForCurrentMedia applies configured open states to character dictionary sections', async () => {
const userDataPath = makeTempDir();
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;
if (url === GRAPHQL_URL) {
const body = JSON.parse(String(init?.body ?? '{}')) as {
query?: string;
};
if (body.query?.includes('Page(perPage: 10)')) {
return new Response(
JSON.stringify({
data: {
Page: {
media: [
{
id: 130298,
episodes: 20,
title: {
romaji: 'The Eminence in Shadow',
english: 'The Eminence in Shadow',
native: '陰の実力者になりたくて!',
},
},
],
},
},
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
if (body.query?.includes('characters(page: $page')) {
return new Response(
JSON.stringify({
data: {
Media: {
title: {
romaji: 'The Eminence in Shadow',
english: 'The Eminence in Shadow',
native: '陰の実力者になりたくて!',
},
characters: {
pageInfo: { hasNextPage: false },
edges: [
{
role: 'SUPPORTING',
voiceActors: [
{
id: 456,
name: {
full: 'Rina Hidaka',
native: '日高里菜',
},
image: {
medium: 'https://cdn.example.com/va-456.jpg',
},
},
],
node: {
id: 123,
description:
'Alexia Midgar is the second princess of the Kingdom of Midgar.\n\n__Race:__ Human',
image: {
large: 'https://cdn.example.com/character-123.png',
medium: 'https://cdn.example.com/character-123-small.png',
},
name: {
full: 'Alexia Midgar',
native: 'アレクシア・ミドガル',
},
},
},
],
},
},
},
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
}
if (url === 'https://cdn.example.com/character-123.png') {
return new Response(Buffer.from([0x89, 0x50, 0x4e, 0x47]), {
status: 200,
headers: { 'content-type': 'image/png' },
});
}
if (url === 'https://cdn.example.com/va-456.jpg') {
return new Response(Buffer.from([0xff, 0xd8, 0xff, 0xd9]), {
status: 200,
headers: { 'content-type': 'image/jpeg' },
});
}
throw new Error(`Unexpected fetch: ${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',
episode: 5,
source: 'fallback',
}),
getCollapsibleSectionOpenState: (section) =>
section === 'description' || section === 'voicedBy',
now: () => 1_700_000_000_000,
});
const result = await runtime.generateForCurrentMedia();
const termBank = JSON.parse(
readStoredZipEntry(result.zipPath, 'term_bank_1.json').toString('utf8'),
) as Array<
[
string,
string,
string,
string,
number,
Array<string | Record<string, unknown>>,
number,
string,
]
>;
const alexia = termBank.find(([term]) => term === 'アレクシア');
assert.ok(alexia);
const glossary = alexia[5];
const entry = glossary[0] as {
type: string;
content: { tag: string; content: Array<Record<string, unknown>> };
};
const children = entry.content.content;
const getSection = (title: string) =>
children.find(
(c) =>
(c as { tag?: string }).tag === 'details' &&
Array.isArray((c as { content?: unknown[] }).content) &&
(c as { content: Array<{ content?: string }> }).content[0]?.content === title,
) as { open?: boolean } | undefined;
assert.equal(getSection('Description')?.open, true);
assert.equal(getSection('Character Information')?.open, false);
assert.equal(getSection('Voiced by')?.open, true);
} finally {
globalThis.fetch = originalFetch;
}
});
test('generateForCurrentMedia reapplies collapsible open states when using cached snapshot data', async () => {
const userDataPath = makeTempDir();
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;
if (url === GRAPHQL_URL) {
const body = JSON.parse(String(init?.body ?? '{}')) as {
query?: string;
};
if (body.query?.includes('Page(perPage: 10)')) {
return new Response(
JSON.stringify({
data: {
Page: {
media: [
{
id: 130298,
episodes: 20,
title: {
romaji: 'The Eminence in Shadow',
english: 'The Eminence in Shadow',
native: '陰の実力者になりたくて!',
},
},
],
},
},
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
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',
voiceActors: [
{
id: 456,
name: {
full: 'Rina Hidaka',
native: '日高里菜',
},
image: {
medium: 'https://cdn.example.com/va-456.jpg',
},
},
],
node: {
id: 123,
description:
'Alexia Midgar is the second princess of the Kingdom of Midgar.\n\n__Race:__ Human',
image: {
large: 'https://cdn.example.com/character-123.png',
medium: null,
},
name: {
full: 'Alexia Midgar',
native: 'アレクシア・ミドガル',
},
},
},
],
},
},
},
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
}
if (url === 'https://cdn.example.com/character-123.png') {
return new Response(PNG_1X1, {
status: 200,
headers: { 'content-type': 'image/png' },
});
}
if (url === 'https://cdn.example.com/va-456.jpg') {
return new Response(PNG_1X1, {
status: 200,
headers: { 'content-type': 'image/png' },
});
}
throw new Error(`Unexpected fetch: ${url}`);
}) as typeof globalThis.fetch;
try {
const runtimeOpen = createCharacterDictionaryRuntimeService({
userDataPath,
getCurrentMediaPath: () => '/tmp/eminence-s01e05.mkv',
getCurrentMediaTitle: () => 'The Eminence in Shadow - S01E05',
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
guessAnilistMediaInfo: async () => ({
title: 'The Eminence in Shadow',
episode: 5,
source: 'fallback',
}),
getCollapsibleSectionOpenState: () => true,
now: () => 1_700_000_000_000,
});
await runtimeOpen.generateForCurrentMedia();
const runtimeClosed = createCharacterDictionaryRuntimeService({
userDataPath,
getCurrentMediaPath: () => '/tmp/eminence-s01e05.mkv',
getCurrentMediaTitle: () => 'The Eminence in Shadow - S01E05',
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
guessAnilistMediaInfo: async () => ({
title: 'The Eminence in Shadow',
episode: 5,
source: 'fallback',
}),
getCollapsibleSectionOpenState: () => false,
now: () => 1_700_000_000_500,
});
const result = await runtimeClosed.generateForCurrentMedia();
const termBank = JSON.parse(
readStoredZipEntry(result.zipPath, 'term_bank_1.json').toString('utf8'),
) as Array<
[
string,
string,
string,
string,
number,
Array<string | Record<string, unknown>>,
number,
string,
]
>;
const alexia = termBank.find(([term]) => term === 'アレクシア');
assert.ok(alexia);
const children = (
alexia[5][0] as {
content: { content: Array<Record<string, unknown>> };
}
).content.content;
const sections = children.filter((item) => (item as { tag?: string }).tag === 'details') as Array<{
open?: boolean;
}>;
assert.ok(sections.length >= 2);
assert.ok(sections.every((section) => section.open === false));
} finally {
globalThis.fetch = originalFetch;
}
});
test('generateForCurrentMedia adds kana aliases for romanized names when native name is kanji', async () => {
const userDataPath = makeTempDir();
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;
if (url === GRAPHQL_URL) {
const body = JSON.parse(String(init?.body ?? '{}')) as {
query?: string;
};
if (body.query?.includes('Page(perPage: 10)')) {
return new Response(
JSON.stringify({
data: {
Page: {
media: [
{
id: 20594,
episodes: 10,
title: {
romaji: 'Kono Subarashii Sekai ni Shukufuku wo!',
english: 'KONOSUBA -Gods blessing on this wonderful world!',
native: 'この素晴らしい世界に祝福を!',
},
},
],
},
},
}),
{
status: 200,
headers: { 'content-type': 'application/json' },
},
);
}
if (body.query?.includes('characters(page: $page')) {
return new Response(
JSON.stringify({
data: {
Media: {
title: {
romaji: 'Kono Subarashii Sekai ni Shukufuku wo!',
english: 'KONOSUBA -Gods blessing on this wonderful world!',
native: 'この素晴らしい世界に祝福を!',
},
characters: {
pageInfo: { hasNextPage: false },
edges: [
{
role: 'MAIN',
node: {
id: 1,
description: 'The protagonist.',
image: null,
name: {
full: 'Satou Kazuma',
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/konosuba-s02e05.mkv',
getCurrentMediaTitle: () => 'Konosuba S02E05',
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
guessAnilistMediaInfo: async () => ({
title: 'Konosuba',
episode: 5,
source: 'fallback',
}),
now: () => 1_700_000_000_000,
});
const result = await runtime.generateForCurrentMedia();
const termBank = JSON.parse(
readStoredZipEntry(result.zipPath, 'term_bank_1.json').toString('utf8'),
) as Array<
[
string,
string,
string,
string,
number,
Array<string | Record<string, unknown>>,
number,
string,
]
>;
const kazuma = termBank.find(([term]) => term === 'カズマ');
assert.ok(kazuma, 'expected katakana alias for romanized name');
assert.equal(kazuma[1], 'かずま');
const fullName = termBank.find(([term]) => term === 'サトウカズマ');
assert.ok(fullName, 'expected compact full-name katakana alias for romanized name');
assert.equal(fullName[1], 'さとうかずま');
} finally {
globalThis.fetch = originalFetch;
}
});
test('generateForCurrentMedia indexes AniList alternative character names for alias lookups', async () => {
const userDataPath = makeTempDir();
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;
if (url === GRAPHQL_URL) {
const body = JSON.parse(String(init?.body ?? '{}')) as {
query?: string;
};
if (body.query?.includes('Page(perPage: 10)')) {
return new Response(
JSON.stringify({
data: {
Page: {
media: [
{
id: 130298,
episodes: 20,
title: {
romaji: 'Kage no Jitsuryokusha ni Naritakute!',
english: 'The Eminence in Shadow',
native: '陰の実力者になりたくて!',
},
},
],
},
},
}),
{
status: 200,
headers: { 'content-type': 'application/json' },
},
);
}
if (body.query?.includes('characters(page: $page')) {
return new Response(
JSON.stringify({
data: {
Media: {
title: {
romaji: 'Kage no Jitsuryokusha ni Naritakute!',
english: 'The Eminence in Shadow',
native: '陰の実力者になりたくて!',
},
characters: {
pageInfo: { hasNextPage: false },
edges: [
{
role: 'MAIN',
node: {
id: 321,
description: 'Leader of Shadow Garden.',
image: null,
name: {
full: 'Cid Kagenou',
native: 'シド・カゲノー',
alternative: ['Shadow', 'Minoru Kagenou'],
},
},
},
],
},
},
},
}),
{
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',
episode: 5,
source: 'fallback',
}),
now: () => 1_700_000_000_000,
});
const result = await runtime.generateForCurrentMedia();
const termBank = JSON.parse(
readStoredZipEntry(result.zipPath, 'term_bank_1.json').toString('utf8'),
) as Array<
[
string,
string,
string,
string,
number,
Array<string | Record<string, unknown>>,
number,
string,
]
>;
const shadowKana = termBank.find(([term]) => term === 'シャドウ');
assert.ok(shadowKana, 'expected katakana alias from AniList alternative name');
assert.equal(shadowKana[1], 'しゃどう');
} finally {
globalThis.fetch = originalFetch;
}
});
test('getOrCreateCurrentSnapshot persists and reuses normalized snapshot data', async () => {
const userDataPath = makeTempDir();
const originalFetch = globalThis.fetch;
let searchQueryCount = 0;
let characterQueryCount = 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('Page(perPage: 10)')) {
searchQueryCount += 1;
return new Response(
JSON.stringify({
data: {
Page: {
media: [
{
id: 130298,
episodes: 20,
title: {
romaji: 'Kage no Jitsuryokusha ni Naritakute!',
english: 'The Eminence in Shadow',
native: '陰の実力者になりたくて!',
},
},
],
},
},
}),
{
status: 200,
headers: { 'content-type': 'application/json' },
},
);
}
if (body.query?.includes('characters(page: $page')) {
characterQueryCount += 1;
return new Response(
JSON.stringify({
data: {
Media: {
title: {
romaji: 'Kage no Jitsuryokusha ni Naritakute!',
english: 'The Eminence in Shadow',
native: '陰の実力者になりたくて!',
},
characters: {
pageInfo: { hasNextPage: false },
edges: [
{
role: 'MAIN',
node: {
id: 321,
description: 'Alpha is the second-in-command of Shadow Garden.',
image: {
large: 'https://example.com/alpha.png',
medium: null,
},
name: {
full: 'Alpha',
native: 'アルファ',
},
},
},
],
},
},
},
}),
{
status: 200,
headers: { 'content-type': 'application/json' },
},
);
}
}
if (url === 'https://example.com/alpha.png') {
return new Response(PNG_1X1, {
status: 200,
headers: { 'content-type': 'image/png' },
});
}
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',
episode: 5,
source: 'fallback',
}),
now: () => 1_700_000_000_100,
});
const first = await runtime.getOrCreateCurrentSnapshot();
const second = await runtime.getOrCreateCurrentSnapshot();
assert.equal(first.fromCache, false);
assert.equal(second.fromCache, true);
assert.equal(searchQueryCount, 2);
assert.equal(characterQueryCount, 1);
assert.equal(
fs.existsSync(path.join(userDataPath, 'character-dictionaries', 'cache.json')),
false,
);
const snapshotPath = path.join(
userDataPath,
'character-dictionaries',
'snapshots',
'anilist-130298.json',
);
const snapshot = JSON.parse(fs.readFileSync(snapshotPath, 'utf8')) as {
mediaId: number;
entryCount: number;
termEntries: Array<
[
string,
string,
string,
string,
number,
Array<string | Record<string, unknown>>,
number,
string,
]
>;
};
assert.equal(snapshot.mediaId, 130298);
assert.equal(snapshot.entryCount > 0, true);
const alpha = snapshot.termEntries.find(([term]) => term === 'アルファ');
assert.ok(alpha);
} finally {
globalThis.fetch = originalFetch;
}
});
test('getOrCreateCurrentSnapshot rebuilds snapshots written with an older format version', async () => {
const userDataPath = makeTempDir();
const originalFetch = globalThis.fetch;
let searchQueryCount = 0;
let characterQueryCount = 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('Page(perPage: 10)')) {
searchQueryCount += 1;
return new Response(
JSON.stringify({
data: {
Page: {
media: [
{
id: 130298,
episodes: 20,
title: {
romaji: 'Kage no Jitsuryokusha ni Naritakute!',
english: 'The Eminence in Shadow',
native: '陰の実力者になりたくて!',
},
},
],
},
},
}),
{
status: 200,
headers: { 'content-type': 'application/json' },
},
);
}
if (body.query?.includes('characters(page: $page')) {
characterQueryCount += 1;
return new Response(
JSON.stringify({
data: {
Media: {
title: {
romaji: 'Kage no Jitsuryokusha ni Naritakute!',
english: 'The Eminence in Shadow',
native: '陰の実力者になりたくて!',
},
characters: {
pageInfo: { hasNextPage: false },
edges: [
{
role: 'MAIN',
node: {
id: 321,
description: 'Alpha is the second-in-command of Shadow Garden.',
image: null,
name: {
full: 'Alpha',
native: 'アルファ',
},
},
},
],
},
},
},
}),
{
status: 200,
headers: { 'content-type': 'application/json' },
},
);
}
}
throw new Error(`Unexpected fetch URL: ${url}`);
}) as typeof globalThis.fetch;
try {
const snapshotsDir = path.join(userDataPath, 'character-dictionaries', 'snapshots');
fs.mkdirSync(snapshotsDir, { recursive: true });
fs.writeFileSync(
path.join(snapshotsDir, 'anilist-130298.json'),
JSON.stringify({
formatVersion: 9,
mediaId: 130298,
mediaTitle: 'The Eminence in Shadow',
entryCount: 1,
updatedAt: 1_700_000_000_000,
termEntries: [['stale', '', 'name side', '', 1, ['stale'], 0, '']],
images: [],
}),
'utf8',
);
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',
episode: 5,
source: 'fallback',
}),
now: () => 1_700_000_000_100,
});
const result = await runtime.getOrCreateCurrentSnapshot();
assert.equal(result.fromCache, false);
assert.equal(searchQueryCount, 1);
assert.equal(characterQueryCount, 1);
const snapshotPath = path.join(snapshotsDir, 'anilist-130298.json');
const snapshot = JSON.parse(fs.readFileSync(snapshotPath, 'utf8')) as {
formatVersion: number;
termEntries: Array<
[
string,
string,
string,
string,
number,
Array<string | Record<string, unknown>>,
number,
string,
]
>;
};
assert.equal(snapshot.formatVersion > 9, true);
assert.equal(
snapshot.termEntries.some(([term]) => term === 'アルファ'),
true,
);
assert.equal(
snapshot.termEntries.some(([term]) => term === 'stale'),
false,
);
} finally {
globalThis.fetch = originalFetch;
}
});
test('generateForCurrentMedia logs progress while resolving and rebuilding snapshot data', async () => {
const userDataPath = makeTempDir();
const originalFetch = globalThis.fetch;
const logs: string[] = [];
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('Page(perPage: 10)')) {
return new Response(
JSON.stringify({
data: {
Page: {
media: [
{
id: 130298,
episodes: 20,
title: {
romaji: 'Kage no Jitsuryokusha ni Naritakute!',
english: 'The Eminence in Shadow',
native: '陰の実力者になりたくて!',
},
},
],
},
},
}),
{
status: 200,
headers: { 'content-type': 'application/json' },
},
);
}
if (body.query?.includes('characters(page: $page')) {
return new Response(
JSON.stringify({
data: {
Media: {
title: {
romaji: 'Kage no Jitsuryokusha ni Naritakute!',
english: 'The Eminence in Shadow',
native: '陰の実力者になりたくて!',
},
characters: {
pageInfo: { hasNextPage: false },
edges: [
{
role: 'MAIN',
node: {
id: 321,
description: 'Alpha is the second-in-command of Shadow Garden.',
image: {
large: 'https://example.com/alpha.png',
medium: null,
},
name: {
full: 'Alpha',
native: 'アルファ',
},
},
},
],
},
},
},
}),
{
status: 200,
headers: { 'content-type': 'application/json' },
},
);
}
}
if (url === 'https://example.com/alpha.png') {
return new Response(PNG_1X1, {
status: 200,
headers: { 'content-type': 'image/png' },
});
}
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',
episode: 5,
source: 'fallback',
}),
now: () => 1_700_000_000_100,
sleep: async () => undefined,
logInfo: (message) => {
logs.push(message);
},
});
await runtime.generateForCurrentMedia();
assert.deepEqual(logs, [
'[dictionary] resolving current anime for character dictionary generation',
'[dictionary] current anime guess: The Eminence in Shadow (episode 5)',
'[dictionary] AniList match: The Eminence in Shadow -> AniList 130298',
'[dictionary] snapshot miss for AniList 130298, fetching characters',
'[dictionary] downloaded AniList character page 1 for AniList 130298',
'[dictionary] downloading 1 images for AniList 130298',
'[dictionary] stored snapshot for AniList 130298: 32 terms',
'[dictionary] building ZIP for AniList 130298',
'[dictionary] generated AniList 130298: 32 terms -> ' +
path.join(userDataPath, 'character-dictionaries', 'anilist-130298.zip'),
]);
} finally {
globalThis.fetch = originalFetch;
}
});
test('generateForCurrentMedia downloads shared voice actor images once per AniList person id', async () => {
const userDataPath = makeTempDir();
const originalFetch = globalThis.fetch;
const fetchedImageUrls: string[] = [];
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('Page(perPage: 10)')) {
return new Response(
JSON.stringify({
data: {
Page: {
media: [
{
id: 130298,
episodes: 20,
title: {
romaji: 'Kage no Jitsuryokusha ni Naritakute!',
english: 'The Eminence in Shadow',
native: '陰の実力者になりたくて!',
},
},
],
},
},
}),
{
status: 200,
headers: { 'content-type': 'application/json' },
},
);
}
if (body.query?.includes('characters(page: $page')) {
return new Response(
JSON.stringify({
data: {
Media: {
title: {
romaji: 'Kage no Jitsuryokusha ni Naritakute!',
english: 'The Eminence in Shadow',
native: '陰の実力者になりたくて!',
},
characters: {
pageInfo: { hasNextPage: false },
edges: [
{
role: 'MAIN',
voiceActors: [
{
id: 9001,
name: {
full: 'Kana Hanazawa',
native: '花澤香菜',
},
image: {
large: null,
medium: 'https://example.com/kana.png',
},
},
],
node: {
id: 321,
description: 'Alpha is the second-in-command of Shadow Garden.',
image: {
large: 'https://example.com/alpha.png',
medium: null,
},
name: {
full: 'Alpha',
native: 'アルファ',
},
},
},
{
role: 'SUPPORTING',
voiceActors: [
{
id: 9001,
name: {
full: 'Kana Hanazawa',
native: '花澤香菜',
},
image: {
large: null,
medium: 'https://example.com/kana.png',
},
},
],
node: {
id: 654,
description: 'Beta documents Shadow Garden operations.',
image: {
large: 'https://example.com/beta.png',
medium: null,
},
name: {
full: 'Beta',
native: 'ベータ',
},
},
},
],
},
},
},
}),
{
status: 200,
headers: { 'content-type': 'application/json' },
},
);
}
}
if (
url === 'https://example.com/alpha.png' ||
url === 'https://example.com/beta.png' ||
url === 'https://example.com/kana.png'
) {
fetchedImageUrls.push(url);
return new Response(PNG_1X1, {
status: 200,
headers: { 'content-type': 'image/png' },
});
}
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',
episode: 5,
source: 'fallback',
}),
now: () => 1_700_000_000_100,
sleep: async () => undefined,
});
await runtime.generateForCurrentMedia();
assert.deepEqual(fetchedImageUrls, [
'https://example.com/alpha.png',
'https://example.com/kana.png',
'https://example.com/beta.png',
]);
} finally {
globalThis.fetch = originalFetch;
}
});
test('buildMergedDictionary combines stored snapshots into one stable dictionary', async () => {
const userDataPath = makeTempDir();
const originalFetch = globalThis.fetch;
const current = { title: 'The Eminence in Shadow', episode: 5 };
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;
variables?: Record<string, unknown>;
};
if (body.query?.includes('Page(perPage: 10)')) {
if (body.variables?.search === 'The Eminence in Shadow') {
return new Response(
JSON.stringify({
data: {
Page: {
media: [
{
id: 130298,
episodes: 20,
title: {
romaji: 'Kage no Jitsuryokusha ni Naritakute!',
english: 'The Eminence in Shadow',
native: '陰の実力者になりたくて!',
},
},
],
},
},
}),
{
status: 200,
headers: { 'content-type': 'application/json' },
},
);
}
return new Response(
JSON.stringify({
data: {
Page: {
media: [
{
id: 21,
episodes: 28,
title: {
romaji: 'Sousou no Frieren',
english: 'Frieren: Beyond Journeys End',
native: '葬送のフリーレン',
},
},
],
},
},
}),
{
status: 200,
headers: { 'content-type': 'application/json' },
},
);
}
if (body.query?.includes('characters(page: $page')) {
const mediaId = Number(body.variables?.id);
if (mediaId === 130298) {
return new Response(
JSON.stringify({
data: {
Media: {
title: {
english: 'The Eminence in Shadow',
},
characters: {
pageInfo: { hasNextPage: false },
edges: [
{
role: 'MAIN',
node: {
id: 111,
description: 'Leader of Shadow Garden.',
image: {
large: 'https://example.com/alpha.png',
medium: null,
},
name: {
full: 'Alpha',
native: 'アルファ',
},
},
},
],
},
},
},
}),
{
status: 200,
headers: { 'content-type': 'application/json' },
},
);
}
return new Response(
JSON.stringify({
data: {
Media: {
title: {
english: 'Frieren: Beyond Journeys End',
},
characters: {
pageInfo: { hasNextPage: false },
edges: [
{
role: 'MAIN',
node: {
id: 222,
description: 'Elven mage.',
image: {
large: 'https://example.com/frieren.png',
medium: null,
},
name: {
full: 'Frieren',
native: 'フリーレン',
},
},
},
],
},
},
},
}),
{
status: 200,
headers: { 'content-type': 'application/json' },
},
);
}
}
if (url === 'https://example.com/alpha.png' || url === 'https://example.com/frieren.png') {
return new Response(PNG_1X1, {
status: 200,
headers: { 'content-type': 'image/png' },
});
}
throw new Error(`Unexpected fetch URL: ${url}`);
}) as typeof globalThis.fetch;
try {
const runtime = createCharacterDictionaryRuntimeService({
userDataPath,
getCurrentMediaPath: () => '/tmp/current.mkv',
getCurrentMediaTitle: () => current.title,
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
guessAnilistMediaInfo: async () => ({
title: current.title,
episode: current.episode,
source: 'fallback',
}),
now: () => 1_700_000_000_100,
});
await runtime.getOrCreateCurrentSnapshot();
current.title = 'Frieren: Beyond Journeys End';
current.episode = 1;
await runtime.getOrCreateCurrentSnapshot();
const merged = await runtime.buildMergedDictionary([21, 130298]);
const index = JSON.parse(readStoredZipEntry(merged.zipPath, 'index.json').toString('utf8')) as {
title: string;
};
const termBank = JSON.parse(
readStoredZipEntry(merged.zipPath, 'term_bank_1.json').toString('utf8'),
) as Array<
[
string,
string,
string,
string,
number,
Array<string | Record<string, unknown>>,
number,
string,
]
>;
const frieren = termBank.find(([term]) => term === 'フリーレン');
const alpha = termBank.find(([term]) => term === 'アルファ');
assert.equal(index.title, 'SubMiner Character Dictionary');
assert.equal(merged.entryCount >= 2, true);
assert.ok(frieren);
assert.ok(alpha);
assert.equal((frieren[5][0] as { type?: string }).type, 'structured-content');
assert.equal((alpha[5][0] as { type?: string }).type, 'structured-content');
} finally {
globalThis.fetch = originalFetch;
}
});
test('buildMergedDictionary rebuilds snapshots written with an older format version', async () => {
const userDataPath = makeTempDir();
const originalFetch = globalThis.fetch;
let characterQueryCount = 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;
variables?: Record<string, unknown>;
};
if (body.query?.includes('characters(page: $page')) {
characterQueryCount += 1;
assert.equal(body.variables?.id, 130298);
return new Response(
JSON.stringify({
data: {
Media: {
title: {
english: 'The Eminence in Shadow',
},
characters: {
pageInfo: { hasNextPage: false },
edges: [
{
role: 'MAIN',
node: {
id: 111,
description: 'Leader of Shadow Garden.',
image: null,
name: {
full: 'Cid Kagenou',
native: 'シド・カゲノー',
alternative: ['Shadow'],
},
},
},
],
},
},
},
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
}
throw new Error(`Unexpected fetch URL: ${url}`);
}) as typeof globalThis.fetch;
try {
const snapshotsDir = path.join(userDataPath, 'character-dictionaries', 'snapshots');
fs.mkdirSync(snapshotsDir, { recursive: true });
fs.writeFileSync(
path.join(snapshotsDir, 'anilist-130298.json'),
JSON.stringify({
formatVersion: 12,
mediaId: 130298,
mediaTitle: 'The Eminence in Shadow',
entryCount: 1,
updatedAt: 1_700_000_000_000,
termEntries: [['stale', '', 'name main', '', 100, ['stale'], 0, '']],
images: [],
}),
'utf8',
);
const runtime = createCharacterDictionaryRuntimeService({
userDataPath,
getCurrentMediaPath: () => null,
getCurrentMediaTitle: () => null,
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
guessAnilistMediaInfo: async () => null,
now: () => 1_700_000_000_100,
});
const merged = await runtime.buildMergedDictionary([130298]);
const termBank = JSON.parse(
readStoredZipEntry(merged.zipPath, 'term_bank_1.json').toString('utf8'),
) as Array<
[
string,
string,
string,
string,
number,
Array<string | Record<string, unknown>>,
number,
string,
]
>;
assert.equal(characterQueryCount, 1);
assert.ok(termBank.find(([term]) => term === 'シャドウ'));
} finally {
globalThis.fetch = originalFetch;
}
});
test('buildMergedDictionary reapplies collapsible open states from current config', async () => {
const userDataPath = makeTempDir();
const originalFetch = globalThis.fetch;
const current = { title: 'The Eminence in Shadow', episode: 5 };
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;
variables?: Record<string, unknown>;
};
if (body.query?.includes('Page(perPage: 10)')) {
if (body.variables?.search === 'The Eminence in Shadow') {
return new Response(
JSON.stringify({
data: {
Page: {
media: [
{
id: 130298,
episodes: 20,
title: {
english: 'The Eminence in Shadow',
},
},
],
},
},
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
return new Response(
JSON.stringify({
data: {
Page: {
media: [
{
id: 21,
episodes: 28,
title: {
english: 'Frieren: Beyond Journeys End',
},
},
],
},
},
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
if (body.query?.includes('characters(page: $page')) {
const mediaId = Number(body.variables?.id);
if (mediaId === 130298) {
return new Response(
JSON.stringify({
data: {
Media: {
title: { english: 'The Eminence in Shadow' },
characters: {
pageInfo: { hasNextPage: false },
edges: [
{
role: 'MAIN',
node: {
id: 111,
description: 'Leader of Shadow Garden.',
image: {
large: 'https://example.com/alpha.png',
medium: null,
},
name: {
full: 'Alpha',
native: 'アルファ',
},
},
},
],
},
},
},
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
return new Response(
JSON.stringify({
data: {
Media: {
title: { english: 'Frieren: Beyond Journeys End' },
characters: {
pageInfo: { hasNextPage: false },
edges: [
{
role: 'MAIN',
node: {
id: 222,
description: 'Elven mage.',
image: {
large: 'https://example.com/frieren.png',
medium: null,
},
name: {
full: 'Frieren',
native: 'フリーレン',
},
},
},
],
},
},
},
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
}
if (url === 'https://example.com/alpha.png' || url === 'https://example.com/frieren.png') {
return new Response(PNG_1X1, {
status: 200,
headers: { 'content-type': 'image/png' },
});
}
throw new Error(`Unexpected fetch URL: ${url}`);
}) as typeof globalThis.fetch;
try {
const runtimeOpen = createCharacterDictionaryRuntimeService({
userDataPath,
getCurrentMediaPath: () => '/tmp/current.mkv',
getCurrentMediaTitle: () => current.title,
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
guessAnilistMediaInfo: async () => ({
title: current.title,
episode: current.episode,
source: 'fallback',
}),
getCollapsibleSectionOpenState: () => true,
now: () => 1_700_000_000_100,
});
await runtimeOpen.getOrCreateCurrentSnapshot();
current.title = 'Frieren: Beyond Journeys End';
current.episode = 1;
await runtimeOpen.getOrCreateCurrentSnapshot();
const runtimeClosed = createCharacterDictionaryRuntimeService({
userDataPath,
getCurrentMediaPath: () => '/tmp/current.mkv',
getCurrentMediaTitle: () => current.title,
resolveMediaPathForJimaku: (mediaPath) => mediaPath,
guessAnilistMediaInfo: async () => ({
title: current.title,
episode: current.episode,
source: 'fallback',
}),
getCollapsibleSectionOpenState: () => false,
now: () => 1_700_000_000_200,
});
const merged = await runtimeClosed.buildMergedDictionary([21, 130298]);
const termBank = JSON.parse(
readStoredZipEntry(merged.zipPath, 'term_bank_1.json').toString('utf8'),
) as Array<
[
string,
string,
string,
string,
number,
Array<string | Record<string, unknown>>,
number,
string,
]
>;
const alpha = termBank.find(([term]) => term === 'アルファ');
assert.ok(alpha);
const children = (
alpha[5][0] as {
content: { content: Array<Record<string, unknown>> };
}
).content.content;
const sections = children.filter((item) => (item as { tag?: string }).tag === 'details') as Array<{
open?: boolean;
}>;
assert.ok(sections.length >= 1);
assert.ok(sections.every((section) => section.open === false));
} finally {
globalThis.fetch = originalFetch;
}
});
test('generateForCurrentMedia paces AniList requests and character image downloads', async () => {
const userDataPath = makeTempDir();
const originalFetch = globalThis.fetch;
const sleepCalls: number[] = [];
const imageRequests: string[] = [];
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('Page(perPage: 10)')) {
return new Response(
JSON.stringify({
data: {
Page: {
media: [
{
id: 130298,
episodes: 20,
title: {
romaji: 'Kage no Jitsuryokusha ni Naritakute!',
english: 'The Eminence in Shadow',
native: '陰の実力者になりたくて!',
},
},
],
},
},
}),
{
status: 200,
headers: { 'content-type': 'application/json' },
},
);
}
if (body.query?.includes('characters(page: $page')) {
return new Response(
JSON.stringify({
data: {
Media: {
title: {
romaji: 'Kage no Jitsuryokusha ni Naritakute!',
english: 'The Eminence in Shadow',
native: '陰の実力者になりたくて!',
},
characters: {
pageInfo: { hasNextPage: false },
edges: [
{
role: 'MAIN',
node: {
id: 111,
description: 'First character.',
image: {
large: 'https://example.com/alpha.png',
medium: null,
},
name: {
full: 'Alpha',
native: 'アルファ',
},
},
},
{
role: 'SUPPORTING',
node: {
id: 222,
description: 'Second character.',
image: {
large: 'https://example.com/beta.png',
medium: null,
},
name: {
full: 'Beta',
native: 'ベータ',
},
},
},
],
},
},
},
}),
{
status: 200,
headers: { 'content-type': 'application/json' },
},
);
}
}
if (url === 'https://example.com/alpha.png') {
imageRequests.push(url);
return new Response('missing', {
status: 404,
headers: { 'content-type': 'text/plain' },
});
}
if (url === 'https://example.com/beta.png') {
imageRequests.push(url);
return new Response(PNG_1X1, {
status: 200,
headers: { 'content-type': 'image/png' },
});
}
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',
episode: 5,
source: 'fallback',
}),
now: () => 1_700_000_000_000,
sleep: async (ms) => {
sleepCalls.push(ms);
},
});
await runtime.generateForCurrentMedia();
assert.deepEqual(sleepCalls, [2000, 250]);
assert.deepEqual(imageRequests, [
'https://example.com/alpha.png',
'https://example.com/beta.png',
]);
} finally {
globalThis.fetch = originalFetch;
}
});