mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-29 19:21:33 -07:00
fix(stats): fall back to Default deck when deck config is blank
- Trim deck name before checking falsy so empty string uses Default - Add fake AnkiConnect server helper and regression test for blank deck
This commit is contained in:
@@ -4,6 +4,7 @@ import fs from 'node:fs';
|
||||
import http from 'node:http';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import type { AddressInfo } from 'node:net';
|
||||
import { createStatsApp, startStatsServer } from '../stats-server.js';
|
||||
import type { ImmersionTrackerService } from '../immersion-tracker-service.js';
|
||||
|
||||
@@ -334,6 +335,57 @@ function withTempDir<T>(fn: (dir: string) => Promise<T> | T): Promise<T> | T {
|
||||
return result;
|
||||
}
|
||||
|
||||
type CapturedAnkiRequest = {
|
||||
action?: string;
|
||||
params?: {
|
||||
note?: {
|
||||
deckName?: string;
|
||||
modelName?: string;
|
||||
fields?: Record<string, string>;
|
||||
tags?: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
async function withFakeAnkiConnect<T>(
|
||||
fn: (requests: CapturedAnkiRequest[], url: string) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const requests: CapturedAnkiRequest[] = [];
|
||||
const server = http.createServer((req, res) => {
|
||||
const chunks: Buffer[] = [];
|
||||
req.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
||||
req.on('end', () => {
|
||||
const payload = JSON.parse(Buffer.concat(chunks).toString('utf8')) as CapturedAnkiRequest;
|
||||
requests.push(payload);
|
||||
|
||||
let body: unknown = { result: null, error: null };
|
||||
if (payload.action === 'addNote') {
|
||||
if (!payload.params?.note?.deckName) {
|
||||
body = { result: null, error: 'deck was not found: ' };
|
||||
} else {
|
||||
body = { result: 12345, error: null };
|
||||
}
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(body));
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, '127.0.0.1', resolve);
|
||||
});
|
||||
|
||||
try {
|
||||
const address = server.address() as AddressInfo;
|
||||
return await fn(requests, `http://127.0.0.1:${address.port}`);
|
||||
} finally {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.close((err) => (err ? reject(err) : resolve()));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
describe('stats server API routes', () => {
|
||||
it('GET /api/stats/overview returns overview data', async () => {
|
||||
const app = createStatsApp(createMockTracker());
|
||||
@@ -1035,6 +1087,68 @@ describe('stats server API routes', () => {
|
||||
assert.ok(Array.isArray(body));
|
||||
});
|
||||
|
||||
it('POST /api/stats/mine-card falls back to Default deck for empty deck config', async () => {
|
||||
await withTempDir(async (dir) => {
|
||||
const sourcePath = path.join(dir, 'episode.mkv');
|
||||
fs.writeFileSync(sourcePath, 'fake media');
|
||||
|
||||
await withFakeAnkiConnect(async (requests, url) => {
|
||||
const app = createStatsApp(createMockTracker(), {
|
||||
ankiConnectConfig: {
|
||||
url,
|
||||
deck: '',
|
||||
tags: ['SubMiner'],
|
||||
fields: {
|
||||
word: 'Expression',
|
||||
audio: 'ExpressionAudio',
|
||||
image: 'Picture',
|
||||
sentence: 'Sentence',
|
||||
miscInfo: 'MiscInfo',
|
||||
translation: 'SelectionText',
|
||||
},
|
||||
media: {
|
||||
generateAudio: false,
|
||||
generateImage: false,
|
||||
},
|
||||
isLapis: {
|
||||
enabled: true,
|
||||
sentenceCardModel: 'Lapis Morph',
|
||||
},
|
||||
isKiku: {
|
||||
enabled: true,
|
||||
fieldGrouping: 'manual',
|
||||
deleteDuplicateInAuto: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const res = await app.request('/api/stats/mine-card?mode=sentence', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
sourcePath,
|
||||
startMs: 1_000,
|
||||
endMs: 2_000,
|
||||
sentence: '猫を見た',
|
||||
word: '猫',
|
||||
secondaryText: 'I saw a cat',
|
||||
videoTitle: 'Episode 1',
|
||||
}),
|
||||
});
|
||||
|
||||
const body = await res.json();
|
||||
assert.equal(res.status, 200, JSON.stringify(body));
|
||||
assert.equal(body.noteId, 12345);
|
||||
|
||||
const addNoteRequest = requests.find((request) => request.action === 'addNote');
|
||||
assert.equal(addNoteRequest?.params?.note?.deckName, 'Default');
|
||||
assert.equal(addNoteRequest?.params?.note?.modelName, 'Lapis Morph');
|
||||
assert.equal(addNoteRequest?.params?.note?.fields?.Sentence, '<b>猫</b>を見た');
|
||||
assert.equal(addNoteRequest?.params?.note?.fields?.IsSentenceCard, 'x');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('GET /api/stats/episode/:videoId/detail returns episode detail', async () => {
|
||||
const app = createStatsApp(createMockTracker());
|
||||
const res = await app.request('/api/stats/episode/1/detail');
|
||||
|
||||
@@ -1101,7 +1101,7 @@ export function createStatsApp(
|
||||
}
|
||||
|
||||
const model = ankiConfig.isLapis?.sentenceCardModel || 'Basic';
|
||||
const deck = ankiConfig.deck ?? 'Default';
|
||||
const deck = ankiConfig.deck?.trim() || 'Default';
|
||||
const tags = ankiConfig.tags ?? ['SubMiner'];
|
||||
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user