Files
SubMiner/src/config/config.test.ts

424 lines
11 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { ConfigService } from "./service";
import { DEFAULT_CONFIG, RUNTIME_OPTION_REGISTRY } from "./definitions";
import { generateConfigTemplate } from "./template";
function makeTempDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), "subminer-config-test-"));
}
test("loads defaults when config is missing", () => {
const dir = makeTempDir();
const service = new ConfigService(dir);
const config = service.getConfig();
assert.equal(config.websocket.port, DEFAULT_CONFIG.websocket.port);
assert.equal(config.ankiConnect.behavior.autoUpdateNewCards, true);
});
test("parses jsonc and warns/falls back on invalid value", () => {
const dir = makeTempDir();
fs.writeFileSync(
path.join(dir, "config.jsonc"),
`{
// invalid websocket port
"websocket": { "port": "bad" }
}`,
"utf-8",
);
const service = new ConfigService(dir);
const config = service.getConfig();
assert.equal(config.websocket.port, DEFAULT_CONFIG.websocket.port);
assert.ok(service.getWarnings().some((w) => w.path === "websocket.port"));
});
test("accepts valid logging.level", () => {
const dir = makeTempDir();
fs.writeFileSync(
path.join(dir, "config.jsonc"),
`{
"logging": {
"level": "warn"
}
}`,
"utf-8",
);
const service = new ConfigService(dir);
const config = service.getConfig();
assert.equal(config.logging.level, "warn");
});
test("falls back for invalid logging.level and reports warning", () => {
const dir = makeTempDir();
fs.writeFileSync(
path.join(dir, "config.jsonc"),
`{
"logging": {
"level": "trace"
}
}`,
"utf-8",
);
const service = new ConfigService(dir);
const config = service.getConfig();
const warnings = service.getWarnings();
assert.equal(config.logging.level, DEFAULT_CONFIG.logging.level);
assert.ok(
warnings.some((warning) => warning.path === "logging.level"),
);
});
test("parses invisible overlay config and new global shortcuts", () => {
const dir = makeTempDir();
fs.writeFileSync(
path.join(dir, "config.jsonc"),
`{
"shortcuts": {
"toggleVisibleOverlayGlobal": "Alt+Shift+U",
"toggleInvisibleOverlayGlobal": "Alt+Shift+I",
"openJimaku": "Ctrl+Alt+J"
},
"invisibleOverlay": {
"startupVisibility": "hidden"
},
"bind_visible_overlay_to_mpv_sub_visibility": false,
"youtubeSubgen": {
"primarySubLanguages": ["ja", "jpn", "jp"]
}
}`,
"utf-8",
);
const service = new ConfigService(dir);
const config = service.getConfig();
assert.equal(config.shortcuts.toggleVisibleOverlayGlobal, "Alt+Shift+U");
assert.equal(config.shortcuts.toggleInvisibleOverlayGlobal, "Alt+Shift+I");
assert.equal(config.shortcuts.openJimaku, "Ctrl+Alt+J");
assert.equal(config.invisibleOverlay.startupVisibility, "hidden");
assert.equal(config.bind_visible_overlay_to_mpv_sub_visibility, false);
assert.deepEqual(config.youtubeSubgen.primarySubLanguages, ["ja", "jpn", "jp"]);
});
test("runtime options registry is centralized", () => {
const ids = RUNTIME_OPTION_REGISTRY.map((entry) => entry.id);
assert.deepEqual(ids, [
"anki.autoUpdateNewCards",
"anki.nPlusOneMatchMode",
"anki.kikuFieldGrouping",
]);
});
test("validates ankiConnect n+1 behavior values", () => {
const dir = makeTempDir();
fs.writeFileSync(
path.join(dir, "config.jsonc"),
`{
"ankiConnect": {
"nPlusOne": {
"highlightEnabled": "yes",
"refreshMinutes": -5
}
}
}`,
"utf-8",
);
const service = new ConfigService(dir);
const config = service.getConfig();
const warnings = service.getWarnings();
assert.equal(
config.ankiConnect.nPlusOne.highlightEnabled,
DEFAULT_CONFIG.ankiConnect.nPlusOne.highlightEnabled,
);
assert.equal(
config.ankiConnect.nPlusOne.refreshMinutes,
DEFAULT_CONFIG.ankiConnect.nPlusOne.refreshMinutes,
);
assert.ok(
warnings.some(
(warning) => warning.path === "ankiConnect.nPlusOne.highlightEnabled",
),
);
assert.ok(
warnings.some(
(warning) => warning.path === "ankiConnect.nPlusOne.refreshMinutes",
),
);
});
test("accepts valid ankiConnect n+1 behavior values", () => {
const dir = makeTempDir();
fs.writeFileSync(
path.join(dir, "config.jsonc"),
`{
"ankiConnect": {
"nPlusOne": {
"highlightEnabled": true,
"refreshMinutes": 120
}
}
}`,
"utf-8",
);
const service = new ConfigService(dir);
const config = service.getConfig();
assert.equal(config.ankiConnect.nPlusOne.highlightEnabled, true);
assert.equal(config.ankiConnect.nPlusOne.refreshMinutes, 120);
});
test("validates ankiConnect n+1 minimum sentence word count", () => {
const dir = makeTempDir();
fs.writeFileSync(
path.join(dir, "config.jsonc"),
`{
"ankiConnect": {
"nPlusOne": {
"minSentenceWords": 0
}
}
}`,
"utf-8",
);
const service = new ConfigService(dir);
const config = service.getConfig();
const warnings = service.getWarnings();
assert.equal(
config.ankiConnect.nPlusOne.minSentenceWords,
DEFAULT_CONFIG.ankiConnect.nPlusOne.minSentenceWords,
);
assert.ok(
warnings.some(
(warning) => warning.path === "ankiConnect.nPlusOne.minSentenceWords",
),
);
});
test("accepts valid ankiConnect n+1 minimum sentence word count", () => {
const dir = makeTempDir();
fs.writeFileSync(
path.join(dir, "config.jsonc"),
`{
"ankiConnect": {
"nPlusOne": {
"minSentenceWords": 4
}
}
}`,
"utf-8",
);
const service = new ConfigService(dir);
const config = service.getConfig();
assert.equal(config.ankiConnect.nPlusOne.minSentenceWords, 4);
});
test("validates ankiConnect n+1 match mode values", () => {
const dir = makeTempDir();
fs.writeFileSync(
path.join(dir, "config.jsonc"),
`{
"ankiConnect": {
"nPlusOne": {
"matchMode": "bad-mode"
}
}
}`,
"utf-8",
);
const service = new ConfigService(dir);
const config = service.getConfig();
const warnings = service.getWarnings();
assert.equal(
config.ankiConnect.nPlusOne.matchMode,
DEFAULT_CONFIG.ankiConnect.nPlusOne.matchMode,
);
assert.ok(
warnings.some((warning) =>
warning.path === "ankiConnect.nPlusOne.matchMode",
),
);
});
test("accepts valid ankiConnect n+1 match mode values", () => {
const dir = makeTempDir();
fs.writeFileSync(
path.join(dir, "config.jsonc"),
`{
"ankiConnect": {
"nPlusOne": {
"matchMode": "surface"
}
}
}`,
"utf-8",
);
const service = new ConfigService(dir);
const config = service.getConfig();
assert.equal(config.ankiConnect.nPlusOne.matchMode, "surface");
});
test("validates ankiConnect n+1 color values", () => {
const dir = makeTempDir();
fs.writeFileSync(
path.join(dir, "config.jsonc"),
`{
"ankiConnect": {
"nPlusOne": {
"nPlusOne": "not-a-color",
"knownWord": 123
}
}
}`,
"utf-8",
);
const service = new ConfigService(dir);
const config = service.getConfig();
const warnings = service.getWarnings();
assert.equal(
config.ankiConnect.nPlusOne.nPlusOne,
DEFAULT_CONFIG.ankiConnect.nPlusOne.nPlusOne,
);
assert.equal(
config.ankiConnect.nPlusOne.knownWord,
DEFAULT_CONFIG.ankiConnect.nPlusOne.knownWord,
);
assert.ok(
warnings.some((warning) => warning.path === "ankiConnect.nPlusOne.nPlusOne"),
);
assert.ok(
warnings.some((warning) => warning.path === "ankiConnect.nPlusOne.knownWord"),
);
});
test("accepts valid ankiConnect n+1 color values", () => {
const dir = makeTempDir();
fs.writeFileSync(
path.join(dir, "config.jsonc"),
`{
"ankiConnect": {
"nPlusOne": {
"nPlusOne": "#c6a0f6",
"knownWord": "#a6da95"
}
}
}`,
"utf-8",
);
const service = new ConfigService(dir);
const config = service.getConfig();
assert.equal(config.ankiConnect.nPlusOne.nPlusOne, "#c6a0f6");
assert.equal(config.ankiConnect.nPlusOne.knownWord, "#a6da95");
});
test("supports legacy ankiConnect.behavior N+1 settings as fallback", () => {
const dir = makeTempDir();
fs.writeFileSync(
path.join(dir, "config.jsonc"),
`{
"ankiConnect": {
"behavior": {
"nPlusOneHighlightEnabled": true,
"nPlusOneRefreshMinutes": 90,
"nPlusOneMatchMode": "surface"
}
}
}`,
"utf-8",
);
const service = new ConfigService(dir);
const config = service.getConfig();
const warnings = service.getWarnings();
assert.equal(config.ankiConnect.nPlusOne.highlightEnabled, true);
assert.equal(config.ankiConnect.nPlusOne.refreshMinutes, 90);
assert.equal(config.ankiConnect.nPlusOne.matchMode, "surface");
assert.ok(
warnings.some(
(warning) =>
warning.path === "ankiConnect.behavior.nPlusOneHighlightEnabled" ||
warning.path === "ankiConnect.behavior.nPlusOneRefreshMinutes" ||
warning.path === "ankiConnect.behavior.nPlusOneMatchMode",
),
);
});
test("accepts valid ankiConnect n+1 deck list", () => {
const dir = makeTempDir();
fs.writeFileSync(
path.join(dir, "config.jsonc"),
`{
"ankiConnect": {
"nPlusOne": {
"decks": ["Deck One", "Deck Two"]
}
}
}`,
"utf-8",
);
const service = new ConfigService(dir);
const config = service.getConfig();
assert.deepEqual(config.ankiConnect.nPlusOne.decks, ["Deck One", "Deck Two"]);
});
test("falls back to default when ankiConnect n+1 deck list is invalid", () => {
const dir = makeTempDir();
fs.writeFileSync(
path.join(dir, "config.jsonc"),
`{
"ankiConnect": {
"nPlusOne": {
"decks": "not-an-array"
}
}
}`,
"utf-8",
);
const service = new ConfigService(dir);
const config = service.getConfig();
const warnings = service.getWarnings();
assert.deepEqual(config.ankiConnect.nPlusOne.decks, []);
assert.ok(
warnings.some((warning) => warning.path === "ankiConnect.nPlusOne.decks"),
);
});
test("template generator includes known keys", () => {
const output = generateConfigTemplate(DEFAULT_CONFIG);
assert.match(output, /"ankiConnect":/);
assert.match(output, /"logging":/);
assert.match(output, /"websocket":/);
assert.match(output, /"youtubeSubgen":/);
assert.match(output, /"nPlusOne"\s*:\s*\{/);
assert.match(output, /"nPlusOne": "#c6a0f6"/);
assert.match(output, /"knownWord": "#a6da95"/);
assert.match(output, /"minSentenceWords": 3/);
assert.match(output, /auto-generated from src\/config\/definitions.ts/);
});