mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 18:22:42 -08:00
pretty
This commit is contained in:
@@ -1,17 +1,17 @@
|
||||
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";
|
||||
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-"));
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-config-test-'));
|
||||
}
|
||||
|
||||
test("loads defaults when config is missing", () => {
|
||||
test('loads defaults when config is missing', () => {
|
||||
const dir = makeTempDir();
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
@@ -21,9 +21,9 @@ test("loads defaults when config is missing", () => {
|
||||
assert.equal(config.jellyfin.remoteControlEnabled, true);
|
||||
assert.equal(config.jellyfin.remoteControlAutoConnect, true);
|
||||
assert.equal(config.jellyfin.autoAnnounce, false);
|
||||
assert.equal(config.jellyfin.remoteControlDeviceName, "SubMiner");
|
||||
assert.equal(config.jellyfin.remoteControlDeviceName, 'SubMiner');
|
||||
assert.equal(config.immersionTracking.enabled, true);
|
||||
assert.equal(config.immersionTracking.dbPath, "");
|
||||
assert.equal(config.immersionTracking.dbPath, '');
|
||||
assert.equal(config.immersionTracking.batchSize, 25);
|
||||
assert.equal(config.immersionTracking.flushIntervalMs, 500);
|
||||
assert.equal(config.immersionTracking.queueCap, 1000);
|
||||
@@ -36,16 +36,16 @@ test("loads defaults when config is missing", () => {
|
||||
assert.equal(config.immersionTracking.retention.vacuumIntervalDays, 7);
|
||||
});
|
||||
|
||||
test("parses anilist.enabled and warns for invalid value", () => {
|
||||
test('parses anilist.enabled and warns for invalid value', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "config.jsonc"),
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"anilist": {
|
||||
"enabled": "yes"
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
@@ -53,16 +53,16 @@ test("parses anilist.enabled and warns for invalid value", () => {
|
||||
const warnings = service.getWarnings();
|
||||
|
||||
assert.equal(config.anilist.enabled, DEFAULT_CONFIG.anilist.enabled);
|
||||
assert.ok(warnings.some((warning) => warning.path === "anilist.enabled"));
|
||||
assert.ok(warnings.some((warning) => warning.path === 'anilist.enabled'));
|
||||
|
||||
service.patchRawConfig({ anilist: { enabled: true } });
|
||||
assert.equal(service.getConfig().anilist.enabled, true);
|
||||
});
|
||||
|
||||
test("parses jellyfin remote control fields", () => {
|
||||
test('parses jellyfin remote control fields', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "config.jsonc"),
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"jellyfin": {
|
||||
"enabled": true,
|
||||
@@ -73,31 +73,31 @@ test("parses jellyfin remote control fields", () => {
|
||||
"remoteControlDeviceName": "SubMiner"
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
|
||||
assert.equal(config.jellyfin.enabled, true);
|
||||
assert.equal(config.jellyfin.serverUrl, "http://127.0.0.1:8096");
|
||||
assert.equal(config.jellyfin.serverUrl, 'http://127.0.0.1:8096');
|
||||
assert.equal(config.jellyfin.remoteControlEnabled, true);
|
||||
assert.equal(config.jellyfin.remoteControlAutoConnect, true);
|
||||
assert.equal(config.jellyfin.autoAnnounce, true);
|
||||
assert.equal(config.jellyfin.remoteControlDeviceName, "SubMiner");
|
||||
assert.equal(config.jellyfin.remoteControlDeviceName, 'SubMiner');
|
||||
});
|
||||
|
||||
test("parses jellyfin.enabled and remoteControlEnabled disabled combinations", () => {
|
||||
test('parses jellyfin.enabled and remoteControlEnabled disabled combinations', () => {
|
||||
const disabledDir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(disabledDir, "config.jsonc"),
|
||||
path.join(disabledDir, 'config.jsonc'),
|
||||
`{
|
||||
"jellyfin": {
|
||||
"enabled": false,
|
||||
"remoteControlEnabled": false
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const disabledService = new ConfigService(disabledDir);
|
||||
@@ -109,22 +109,21 @@ test("parses jellyfin.enabled and remoteControlEnabled disabled combinations", (
|
||||
.getWarnings()
|
||||
.some(
|
||||
(warning) =>
|
||||
warning.path === "jellyfin.enabled" ||
|
||||
warning.path === "jellyfin.remoteControlEnabled",
|
||||
warning.path === 'jellyfin.enabled' || warning.path === 'jellyfin.remoteControlEnabled',
|
||||
),
|
||||
false,
|
||||
);
|
||||
|
||||
const mixedDir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(mixedDir, "config.jsonc"),
|
||||
path.join(mixedDir, 'config.jsonc'),
|
||||
`{
|
||||
"jellyfin": {
|
||||
"enabled": true,
|
||||
"remoteControlEnabled": false
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const mixedService = new ConfigService(mixedDir);
|
||||
@@ -136,17 +135,16 @@ test("parses jellyfin.enabled and remoteControlEnabled disabled combinations", (
|
||||
.getWarnings()
|
||||
.some(
|
||||
(warning) =>
|
||||
warning.path === "jellyfin.enabled" ||
|
||||
warning.path === "jellyfin.remoteControlEnabled",
|
||||
warning.path === 'jellyfin.enabled' || warning.path === 'jellyfin.remoteControlEnabled',
|
||||
),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("accepts immersion tracking config values", () => {
|
||||
test('accepts immersion tracking config values', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "config.jsonc"),
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"immersionTracking": {
|
||||
"enabled": false,
|
||||
@@ -165,17 +163,14 @@ test("accepts immersion tracking config values", () => {
|
||||
}
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
|
||||
assert.equal(config.immersionTracking.enabled, false);
|
||||
assert.equal(
|
||||
config.immersionTracking.dbPath,
|
||||
"/tmp/immersions/custom.sqlite",
|
||||
);
|
||||
assert.equal(config.immersionTracking.dbPath, '/tmp/immersions/custom.sqlite');
|
||||
assert.equal(config.immersionTracking.batchSize, 50);
|
||||
assert.equal(config.immersionTracking.flushIntervalMs, 750);
|
||||
assert.equal(config.immersionTracking.queueCap, 2000);
|
||||
@@ -188,10 +183,10 @@ test("accepts immersion tracking config values", () => {
|
||||
assert.equal(config.immersionTracking.retention.vacuumIntervalDays, 14);
|
||||
});
|
||||
|
||||
test("falls back for invalid immersion tracking tuning values", () => {
|
||||
test('falls back for invalid immersion tracking tuning values', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "config.jsonc"),
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"immersionTracking": {
|
||||
"batchSize": 0,
|
||||
@@ -208,7 +203,7 @@ test("falls back for invalid immersion tracking tuning values", () => {
|
||||
}
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
@@ -226,102 +221,71 @@ test("falls back for invalid immersion tracking tuning values", () => {
|
||||
assert.equal(config.immersionTracking.retention.monthlyRollupsDays, 1825);
|
||||
assert.equal(config.immersionTracking.retention.vacuumIntervalDays, 7);
|
||||
|
||||
assert.ok(warnings.some((warning) => warning.path === 'immersionTracking.batchSize'));
|
||||
assert.ok(warnings.some((warning) => warning.path === 'immersionTracking.flushIntervalMs'));
|
||||
assert.ok(warnings.some((warning) => warning.path === 'immersionTracking.queueCap'));
|
||||
assert.ok(warnings.some((warning) => warning.path === 'immersionTracking.payloadCapBytes'));
|
||||
assert.ok(warnings.some((warning) => warning.path === 'immersionTracking.maintenanceIntervalMs'));
|
||||
assert.ok(warnings.some((warning) => warning.path === 'immersionTracking.retention.eventsDays'));
|
||||
assert.ok(
|
||||
warnings.some((warning) => warning.path === "immersionTracking.batchSize"),
|
||||
warnings.some((warning) => warning.path === 'immersionTracking.retention.telemetryDays'),
|
||||
);
|
||||
assert.ok(
|
||||
warnings.some(
|
||||
(warning) => warning.path === "immersionTracking.flushIntervalMs",
|
||||
),
|
||||
warnings.some((warning) => warning.path === 'immersionTracking.retention.dailyRollupsDays'),
|
||||
);
|
||||
assert.ok(
|
||||
warnings.some((warning) => warning.path === "immersionTracking.queueCap"),
|
||||
warnings.some((warning) => warning.path === 'immersionTracking.retention.monthlyRollupsDays'),
|
||||
);
|
||||
assert.ok(
|
||||
warnings.some(
|
||||
(warning) => warning.path === "immersionTracking.payloadCapBytes",
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
warnings.some(
|
||||
(warning) => warning.path === "immersionTracking.maintenanceIntervalMs",
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
warnings.some(
|
||||
(warning) => warning.path === "immersionTracking.retention.eventsDays",
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
warnings.some(
|
||||
(warning) => warning.path === "immersionTracking.retention.telemetryDays",
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
warnings.some(
|
||||
(warning) =>
|
||||
warning.path === "immersionTracking.retention.dailyRollupsDays",
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
warnings.some(
|
||||
(warning) =>
|
||||
warning.path === "immersionTracking.retention.monthlyRollupsDays",
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
warnings.some(
|
||||
(warning) =>
|
||||
warning.path === "immersionTracking.retention.vacuumIntervalDays",
|
||||
),
|
||||
warnings.some((warning) => warning.path === 'immersionTracking.retention.vacuumIntervalDays'),
|
||||
);
|
||||
});
|
||||
|
||||
test("parses jsonc and warns/falls back on invalid value", () => {
|
||||
test('parses jsonc and warns/falls back on invalid value', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "config.jsonc"),
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
// invalid websocket port
|
||||
"websocket": { "port": "bad" }
|
||||
}`,
|
||||
"utf-8",
|
||||
'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"));
|
||||
assert.ok(service.getWarnings().some((w) => w.path === 'websocket.port'));
|
||||
});
|
||||
|
||||
test("accepts valid logging.level", () => {
|
||||
test('accepts valid logging.level', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "config.jsonc"),
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"logging": {
|
||||
"level": "warn"
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
|
||||
assert.equal(config.logging.level, "warn");
|
||||
assert.equal(config.logging.level, 'warn');
|
||||
});
|
||||
|
||||
test("falls back for invalid logging.level and reports warning", () => {
|
||||
test('falls back for invalid logging.level and reports warning', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "config.jsonc"),
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"logging": {
|
||||
"level": "trace"
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
@@ -329,13 +293,13 @@ test("falls back for invalid logging.level and reports warning", () => {
|
||||
const warnings = service.getWarnings();
|
||||
|
||||
assert.equal(config.logging.level, DEFAULT_CONFIG.logging.level);
|
||||
assert.ok(warnings.some((warning) => warning.path === "logging.level"));
|
||||
assert.ok(warnings.some((warning) => warning.path === 'logging.level'));
|
||||
});
|
||||
|
||||
test("parses invisible overlay config and new global shortcuts", () => {
|
||||
test('parses invisible overlay config and new global shortcuts', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "config.jsonc"),
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"shortcuts": {
|
||||
"toggleVisibleOverlayGlobal": "Alt+Shift+U",
|
||||
@@ -350,36 +314,32 @@ test("parses invisible overlay config and new global shortcuts", () => {
|
||||
"primarySubLanguages": ["ja", "jpn", "jp"]
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
'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.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",
|
||||
]);
|
||||
assert.deepEqual(config.youtubeSubgen.primarySubLanguages, ['ja', 'jpn', 'jp']);
|
||||
});
|
||||
|
||||
test("runtime options registry is centralized", () => {
|
||||
test('runtime options registry is centralized', () => {
|
||||
const ids = RUNTIME_OPTION_REGISTRY.map((entry) => entry.id);
|
||||
assert.deepEqual(ids, [
|
||||
"anki.autoUpdateNewCards",
|
||||
"anki.nPlusOneMatchMode",
|
||||
"anki.kikuFieldGrouping",
|
||||
'anki.autoUpdateNewCards',
|
||||
'anki.nPlusOneMatchMode',
|
||||
'anki.kikuFieldGrouping',
|
||||
]);
|
||||
});
|
||||
|
||||
test("validates ankiConnect n+1 behavior values", () => {
|
||||
test('validates ankiConnect n+1 behavior values', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "config.jsonc"),
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"ankiConnect": {
|
||||
"nPlusOne": {
|
||||
@@ -388,7 +348,7 @@ test("validates ankiConnect n+1 behavior values", () => {
|
||||
}
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
@@ -403,22 +363,14 @@ test("validates ankiConnect n+1 behavior values", () => {
|
||||
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",
|
||||
),
|
||||
);
|
||||
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", () => {
|
||||
test('accepts valid ankiConnect n+1 behavior values', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "config.jsonc"),
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"ankiConnect": {
|
||||
"nPlusOne": {
|
||||
@@ -427,7 +379,7 @@ test("accepts valid ankiConnect n+1 behavior values", () => {
|
||||
}
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
@@ -437,10 +389,10 @@ test("accepts valid ankiConnect n+1 behavior values", () => {
|
||||
assert.equal(config.ankiConnect.nPlusOne.refreshMinutes, 120);
|
||||
});
|
||||
|
||||
test("validates ankiConnect n+1 minimum sentence word count", () => {
|
||||
test('validates ankiConnect n+1 minimum sentence word count', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "config.jsonc"),
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"ankiConnect": {
|
||||
"nPlusOne": {
|
||||
@@ -448,7 +400,7 @@ test("validates ankiConnect n+1 minimum sentence word count", () => {
|
||||
}
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
@@ -459,17 +411,13 @@ test("validates ankiConnect n+1 minimum sentence word count", () => {
|
||||
config.ankiConnect.nPlusOne.minSentenceWords,
|
||||
DEFAULT_CONFIG.ankiConnect.nPlusOne.minSentenceWords,
|
||||
);
|
||||
assert.ok(
|
||||
warnings.some(
|
||||
(warning) => warning.path === "ankiConnect.nPlusOne.minSentenceWords",
|
||||
),
|
||||
);
|
||||
assert.ok(warnings.some((warning) => warning.path === 'ankiConnect.nPlusOne.minSentenceWords'));
|
||||
});
|
||||
|
||||
test("accepts valid ankiConnect n+1 minimum sentence word count", () => {
|
||||
test('accepts valid ankiConnect n+1 minimum sentence word count', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "config.jsonc"),
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"ankiConnect": {
|
||||
"nPlusOne": {
|
||||
@@ -477,7 +425,7 @@ test("accepts valid ankiConnect n+1 minimum sentence word count", () => {
|
||||
}
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
@@ -486,10 +434,10 @@ test("accepts valid ankiConnect n+1 minimum sentence word count", () => {
|
||||
assert.equal(config.ankiConnect.nPlusOne.minSentenceWords, 4);
|
||||
});
|
||||
|
||||
test("validates ankiConnect n+1 match mode values", () => {
|
||||
test('validates ankiConnect n+1 match mode values', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "config.jsonc"),
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"ankiConnect": {
|
||||
"nPlusOne": {
|
||||
@@ -497,7 +445,7 @@ test("validates ankiConnect n+1 match mode values", () => {
|
||||
}
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
@@ -508,17 +456,13 @@ test("validates ankiConnect n+1 match mode values", () => {
|
||||
config.ankiConnect.nPlusOne.matchMode,
|
||||
DEFAULT_CONFIG.ankiConnect.nPlusOne.matchMode,
|
||||
);
|
||||
assert.ok(
|
||||
warnings.some(
|
||||
(warning) => warning.path === "ankiConnect.nPlusOne.matchMode",
|
||||
),
|
||||
);
|
||||
assert.ok(warnings.some((warning) => warning.path === 'ankiConnect.nPlusOne.matchMode'));
|
||||
});
|
||||
|
||||
test("accepts valid ankiConnect n+1 match mode values", () => {
|
||||
test('accepts valid ankiConnect n+1 match mode values', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "config.jsonc"),
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"ankiConnect": {
|
||||
"nPlusOne": {
|
||||
@@ -526,19 +470,19 @@ test("accepts valid ankiConnect n+1 match mode values", () => {
|
||||
}
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
|
||||
assert.equal(config.ankiConnect.nPlusOne.matchMode, "surface");
|
||||
assert.equal(config.ankiConnect.nPlusOne.matchMode, 'surface');
|
||||
});
|
||||
|
||||
test("validates ankiConnect n+1 color values", () => {
|
||||
test('validates ankiConnect n+1 color values', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "config.jsonc"),
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"ankiConnect": {
|
||||
"nPlusOne": {
|
||||
@@ -547,37 +491,26 @@ test("validates ankiConnect n+1 color values", () => {
|
||||
}
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
'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.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",
|
||||
),
|
||||
);
|
||||
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", () => {
|
||||
test('accepts valid ankiConnect n+1 color values', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "config.jsonc"),
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"ankiConnect": {
|
||||
"nPlusOne": {
|
||||
@@ -586,20 +519,20 @@ test("accepts valid ankiConnect n+1 color values", () => {
|
||||
}
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
'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");
|
||||
assert.equal(config.ankiConnect.nPlusOne.nPlusOne, '#c6a0f6');
|
||||
assert.equal(config.ankiConnect.nPlusOne.knownWord, '#a6da95');
|
||||
});
|
||||
|
||||
test("supports legacy ankiConnect.behavior N+1 settings as fallback", () => {
|
||||
test('supports legacy ankiConnect.behavior N+1 settings as fallback', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "config.jsonc"),
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"ankiConnect": {
|
||||
"behavior": {
|
||||
@@ -609,7 +542,7 @@ test("supports legacy ankiConnect.behavior N+1 settings as fallback", () => {
|
||||
}
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
@@ -618,21 +551,21 @@ test("supports legacy ankiConnect.behavior N+1 settings as fallback", () => {
|
||||
|
||||
assert.equal(config.ankiConnect.nPlusOne.highlightEnabled, true);
|
||||
assert.equal(config.ankiConnect.nPlusOne.refreshMinutes, 90);
|
||||
assert.equal(config.ankiConnect.nPlusOne.matchMode, "surface");
|
||||
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",
|
||||
warning.path === 'ankiConnect.behavior.nPlusOneHighlightEnabled' ||
|
||||
warning.path === 'ankiConnect.behavior.nPlusOneRefreshMinutes' ||
|
||||
warning.path === 'ankiConnect.behavior.nPlusOneMatchMode',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("accepts valid ankiConnect n+1 deck list", () => {
|
||||
test('accepts valid ankiConnect n+1 deck list', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "config.jsonc"),
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"ankiConnect": {
|
||||
"nPlusOne": {
|
||||
@@ -640,19 +573,19 @@ test("accepts valid ankiConnect n+1 deck list", () => {
|
||||
}
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
const config = service.getConfig();
|
||||
|
||||
assert.deepEqual(config.ankiConnect.nPlusOne.decks, ["Deck One", "Deck Two"]);
|
||||
assert.deepEqual(config.ankiConnect.nPlusOne.decks, ['Deck One', 'Deck Two']);
|
||||
});
|
||||
|
||||
test("falls back to default when ankiConnect n+1 deck list is invalid", () => {
|
||||
test('falls back to default when ankiConnect n+1 deck list is invalid', () => {
|
||||
const dir = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "config.jsonc"),
|
||||
path.join(dir, 'config.jsonc'),
|
||||
`{
|
||||
"ankiConnect": {
|
||||
"nPlusOne": {
|
||||
@@ -660,7 +593,7 @@ test("falls back to default when ankiConnect n+1 deck list is invalid", () => {
|
||||
}
|
||||
}
|
||||
}`,
|
||||
"utf-8",
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const service = new ConfigService(dir);
|
||||
@@ -668,12 +601,10 @@ test("falls back to default when ankiConnect n+1 deck list is invalid", () => {
|
||||
const warnings = service.getWarnings();
|
||||
|
||||
assert.deepEqual(config.ankiConnect.nPlusOne.decks, []);
|
||||
assert.ok(
|
||||
warnings.some((warning) => warning.path === "ankiConnect.nPlusOne.decks"),
|
||||
);
|
||||
assert.ok(warnings.some((warning) => warning.path === 'ankiConnect.nPlusOne.decks'));
|
||||
});
|
||||
|
||||
test("template generator includes known keys", () => {
|
||||
test('template generator includes known keys', () => {
|
||||
const output = generateConfigTemplate(DEFAULT_CONFIG);
|
||||
assert.match(output, /"ankiConnect":/);
|
||||
assert.match(output, /"logging":/);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,3 @@
|
||||
export * from "./definitions";
|
||||
export * from "./service";
|
||||
export * from "./template";
|
||||
export * from './definitions';
|
||||
export * from './service';
|
||||
export * from './template';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,40 +1,32 @@
|
||||
import { ResolvedConfig } from "../types";
|
||||
import {
|
||||
CONFIG_TEMPLATE_SECTIONS,
|
||||
DEFAULT_CONFIG,
|
||||
deepCloneConfig,
|
||||
} from "./definitions";
|
||||
import { ResolvedConfig } from '../types';
|
||||
import { CONFIG_TEMPLATE_SECTIONS, DEFAULT_CONFIG, deepCloneConfig } from './definitions';
|
||||
|
||||
function renderValue(value: unknown, indent = 0): string {
|
||||
const pad = " ".repeat(indent);
|
||||
const nextPad = " ".repeat(indent + 2);
|
||||
const pad = ' '.repeat(indent);
|
||||
const nextPad = ' '.repeat(indent + 2);
|
||||
|
||||
if (value === null) return "null";
|
||||
if (typeof value === "string") return JSON.stringify(value);
|
||||
if (typeof value === "number" || typeof value === "boolean")
|
||||
return String(value);
|
||||
if (value === null) return 'null';
|
||||
if (typeof value === 'string') return JSON.stringify(value);
|
||||
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) return "[]";
|
||||
const items = value.map(
|
||||
(item) => `${nextPad}${renderValue(item, indent + 2)}`,
|
||||
);
|
||||
return `\n${items.join(",\n")}\n${pad}`.replace(/^/, "[").concat("]");
|
||||
if (value.length === 0) return '[]';
|
||||
const items = value.map((item) => `${nextPad}${renderValue(item, indent + 2)}`);
|
||||
return `\n${items.join(',\n')}\n${pad}`.replace(/^/, '[').concat(']');
|
||||
}
|
||||
|
||||
if (typeof value === "object") {
|
||||
if (typeof value === 'object') {
|
||||
const entries = Object.entries(value as Record<string, unknown>).filter(
|
||||
([, child]) => child !== undefined,
|
||||
);
|
||||
if (entries.length === 0) return "{}";
|
||||
if (entries.length === 0) return '{}';
|
||||
const lines = entries.map(
|
||||
([key, child]) =>
|
||||
`${nextPad}${JSON.stringify(key)}: ${renderValue(child, indent + 2)}`,
|
||||
([key, child]) => `${nextPad}${JSON.stringify(key)}: ${renderValue(child, indent + 2)}`,
|
||||
);
|
||||
return `\n${lines.join(",\n")}\n${pad}`.replace(/^/, "{").concat("}");
|
||||
return `\n${lines.join(',\n')}\n${pad}`.replace(/^/, '{').concat('}');
|
||||
}
|
||||
|
||||
return "null";
|
||||
return 'null';
|
||||
}
|
||||
|
||||
function renderSection(
|
||||
@@ -44,38 +36,32 @@ function renderSection(
|
||||
comments: string[],
|
||||
): string {
|
||||
const lines: string[] = [];
|
||||
lines.push(" // ==========================================");
|
||||
lines.push(' // ==========================================');
|
||||
for (const comment of comments) {
|
||||
lines.push(` // ${comment}`);
|
||||
}
|
||||
lines.push(" // ==========================================");
|
||||
lines.push(
|
||||
` ${JSON.stringify(key)}: ${renderValue(value, 2)}${isLast ? "" : ","}`,
|
||||
);
|
||||
return lines.join("\n");
|
||||
lines.push(' // ==========================================');
|
||||
lines.push(` ${JSON.stringify(key)}: ${renderValue(value, 2)}${isLast ? '' : ','}`);
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function generateConfigTemplate(
|
||||
config: ResolvedConfig = deepCloneConfig(DEFAULT_CONFIG),
|
||||
): string {
|
||||
const lines: string[] = [];
|
||||
lines.push("/**");
|
||||
lines.push(" * SubMiner Example Configuration File");
|
||||
lines.push(" *");
|
||||
lines.push(" * This file is auto-generated from src/config/definitions.ts.");
|
||||
lines.push('/**');
|
||||
lines.push(' * SubMiner Example Configuration File');
|
||||
lines.push(' *');
|
||||
lines.push(' * This file is auto-generated from src/config/definitions.ts.');
|
||||
lines.push(
|
||||
" * Copy to $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) and edit as needed.",
|
||||
' * Copy to $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) and edit as needed.',
|
||||
);
|
||||
lines.push(" */");
|
||||
lines.push("{");
|
||||
lines.push(' */');
|
||||
lines.push('{');
|
||||
|
||||
CONFIG_TEMPLATE_SECTIONS.forEach((section, index) => {
|
||||
lines.push("");
|
||||
const comments = [
|
||||
section.title,
|
||||
...section.description,
|
||||
...(section.notes ?? []),
|
||||
];
|
||||
lines.push('');
|
||||
const comments = [section.title, ...section.description, ...(section.notes ?? [])];
|
||||
lines.push(
|
||||
renderSection(
|
||||
section.key,
|
||||
@@ -86,7 +72,7 @@ export function generateConfigTemplate(
|
||||
);
|
||||
});
|
||||
|
||||
lines.push("}");
|
||||
lines.push("");
|
||||
return lines.join("\n");
|
||||
lines.push('}');
|
||||
lines.push('');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user