mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-20 12:11:28 -07:00
Enforce config example drift checks in CI and release
- add `verify:config-example` script with tests to fail on missing/stale generated config artifacts - run the verification in CI and release workflows, and document it in release/docs guidance - fix docs-site Cloudflare Pages watch path to `docs-site/*` with regression coverage
This commit is contained in:
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -46,6 +46,9 @@ jobs:
|
|||||||
# Keep explicit typecheck for fast fail before full build/bundle.
|
# Keep explicit typecheck for fast fail before full build/bundle.
|
||||||
run: bun run typecheck
|
run: bun run typecheck
|
||||||
|
|
||||||
|
- name: Verify generated config examples
|
||||||
|
run: bun run verify:config-example
|
||||||
|
|
||||||
- name: Test suite (source)
|
- name: Test suite (source)
|
||||||
run: bun run test:fast
|
run: bun run test:fast
|
||||||
|
|
||||||
|
|||||||
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
@@ -295,6 +295,9 @@ jobs:
|
|||||||
- name: Enforce generated launcher workflow
|
- name: Enforce generated launcher workflow
|
||||||
run: bash scripts/verify-generated-launcher.sh
|
run: bash scripts/verify-generated-launcher.sh
|
||||||
|
|
||||||
|
- name: Verify generated config examples
|
||||||
|
run: bun run verify:config-example
|
||||||
|
|
||||||
- name: Package optional assets bundle
|
- name: Package optional assets bundle
|
||||||
run: |
|
run: |
|
||||||
tar -czf "release/subminer-assets.tar.gz" \
|
tar -czf "release/subminer-assets.tar.gz" \
|
||||||
|
|||||||
1
Makefile
1
Makefile
@@ -165,7 +165,6 @@ generate-config: ensure-bun
|
|||||||
@bun run electron . --generate-config
|
@bun run electron . --generate-config
|
||||||
|
|
||||||
generate-example-config: ensure-bun
|
generate-example-config: ensure-bun
|
||||||
@bun run build
|
|
||||||
@bun run generate:config-example
|
@bun run generate:config-example
|
||||||
|
|
||||||
dev-start: ensure-bun
|
dev-start: ensure-bun
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
id: TASK-157
|
||||||
|
title: Fix Cloudflare Pages watch path for docs-site
|
||||||
|
status: In Progress
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-03-10 20:15'
|
||||||
|
updated_date: '2026-03-10 20:15'
|
||||||
|
labels:
|
||||||
|
- docs-site
|
||||||
|
- cloudflare
|
||||||
|
dependencies: []
|
||||||
|
priority: medium
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Cloudflare Pages skipped a docs-site deployment after the docs repo moved into the main `SubMiner` repository. The documented/configured watch path uses `docs-site/**`, but Pages monorepo watch paths use a single `*` wildcard pattern. Correct the documented setting and leave a regression test so future repo moves or docs rewrites do not reintroduce the bad pattern.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [ ] #1 Docs contributor guidance points Cloudflare Pages watch paths at `docs-site/*`, not `docs-site/**`.
|
||||||
|
- [ ] #2 Regression coverage fails if the docs revert to the incorrect watch-path string.
|
||||||
|
- [ ] #3 Implementation notes record that the Cloudflare dashboard setting must be updated manually and the docs deploy retriggered.
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
<!-- SECTION:NOTES:BEGIN -->
|
||||||
|
Added docs regression coverage in `docs-site/docs-sync.test.ts` for the Pages watch-path string, then corrected the Cloudflare Pages instructions in `docs-site/README.md` and `docs-site/development.md`.
|
||||||
|
|
||||||
|
Manual follow-up still required outside git: update the Cloudflare Pages project include path from `docs-site/**` to `docs-site/*`, then trigger a fresh deployment against `main`.
|
||||||
|
<!-- SECTION:NOTES:END -->
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
id: TASK-158
|
||||||
|
title: Enforce generated config example drift checks
|
||||||
|
status: Done
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-03-10 20:35'
|
||||||
|
updated_date: '2026-03-10 20:35'
|
||||||
|
labels:
|
||||||
|
- config
|
||||||
|
- docs-site
|
||||||
|
dependencies: []
|
||||||
|
priority: medium
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
The generated `config.example.jsonc` artifact is covered by generation-path tests, but there is no hard gate that fails when the checked-in example drifts from the canonical template. The in-repo docs-site copy can also drift silently.
|
||||||
|
|
||||||
|
Scope:
|
||||||
|
|
||||||
|
- add a first-party verification path that compares generated config-example content against committed artifacts
|
||||||
|
- fail fast when repo-root `config.example.jsonc` is stale or missing
|
||||||
|
- fail fast when `docs-site/public/config.example.jsonc` is stale or missing, when the docs site exists
|
||||||
|
- wire the verification into the normal gate and release flow
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [x] #1 Automated verification fails when repo-root `config.example.jsonc` is missing or stale.
|
||||||
|
- [x] #2 Automated verification fails when in-repo docs-site `public/config.example.jsonc` is missing or stale, when docs-site exists.
|
||||||
|
- [x] #3 CI/release or equivalent project gates run the verification automatically.
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
Added `src/verify-config-example.ts`, which renders the canonical config template and compares it against the checked-in repo-root `config.example.jsonc` plus `docs-site/public/config.example.jsonc` when the docs site exists.
|
||||||
|
|
||||||
|
Wired the new verification into `package.json` as `bun run verify:config-example`, added regression coverage for missing and stale artifacts, and enforced the new check in both CI and release workflows.
|
||||||
|
|
||||||
|
Regenerated the checked-in config example artifacts so the new gate passes in the repo-local docs-site layout, and documented the release-step expectation in `docs/RELEASING.md`.
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
4
changes/config-example-drift-check.md
Normal file
4
changes/config-example-drift-check.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
type: internal
|
||||||
|
area: config
|
||||||
|
|
||||||
|
- add an enforced `verify:config-example` gate so checked-in example config artifacts cannot drift silently
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
* Copy to %APPDATA%/SubMiner/config.jsonc on Windows, or $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) on Linux/macOS.
|
* Copy to %APPDATA%/SubMiner/config.jsonc on Windows, or $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) on Linux/macOS.
|
||||||
*/
|
*/
|
||||||
{
|
{
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Overlay Auto-Start
|
// Overlay Auto-Start
|
||||||
// When overlay connects to mpv, automatically show overlay and hide mpv subtitles.
|
// When overlay connects to mpv, automatically show overlay and hide mpv subtitles.
|
||||||
@@ -17,7 +18,7 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
"texthooker": {
|
"texthooker": {
|
||||||
"launchAtStartup": true, // Launch texthooker server automatically when SubMiner starts. Values: true | false
|
"launchAtStartup": true, // Launch texthooker server automatically when SubMiner starts. Values: true | false
|
||||||
"openBrowser": true, // Open browser setting. Values: true | false
|
"openBrowser": true // Open browser setting. Values: true | false
|
||||||
}, // Configure texthooker startup launch and browser opening behavior.
|
}, // Configure texthooker startup launch and browser opening behavior.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -27,7 +28,7 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
"websocket": {
|
"websocket": {
|
||||||
"enabled": "auto", // Built-in subtitle websocket server mode. Values: auto | true | false
|
"enabled": "auto", // Built-in subtitle websocket server mode. Values: auto | true | false
|
||||||
"port": 6677, // Built-in subtitle websocket server port.
|
"port": 6677 // Built-in subtitle websocket server port.
|
||||||
}, // Built-in WebSocket server broadcasts subtitle text to connected clients.
|
}, // Built-in WebSocket server broadcasts subtitle text to connected clients.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -37,7 +38,7 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
"annotationWebsocket": {
|
"annotationWebsocket": {
|
||||||
"enabled": true, // Annotated subtitle websocket server enabled state. Values: true | false
|
"enabled": true, // Annotated subtitle websocket server enabled state. Values: true | false
|
||||||
"port": 6678, // Annotated subtitle websocket server port.
|
"port": 6678 // Annotated subtitle websocket server port.
|
||||||
}, // Dedicated annotated subtitle websocket for bundled texthooker and token-aware clients.
|
}, // Dedicated annotated subtitle websocket for bundled texthooker and token-aware clients.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -46,7 +47,7 @@
|
|||||||
// Set to debug for full runtime diagnostics.
|
// Set to debug for full runtime diagnostics.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"logging": {
|
"logging": {
|
||||||
"level": "info", // Minimum log level for runtime logging. Values: debug | info | warn | error
|
"level": "info" // Minimum log level for runtime logging. Values: debug | info | warn | error
|
||||||
}, // Controls logging verbosity.
|
}, // Controls logging verbosity.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -60,7 +61,7 @@
|
|||||||
"mecab": true, // Warm up MeCab tokenizer at startup. Values: true | false
|
"mecab": true, // Warm up MeCab tokenizer at startup. Values: true | false
|
||||||
"yomitanExtension": true, // Warm up Yomitan extension at startup. Values: true | false
|
"yomitanExtension": true, // Warm up Yomitan extension at startup. Values: true | false
|
||||||
"subtitleDictionaries": true, // Warm up subtitle dictionaries at startup. Values: true | false
|
"subtitleDictionaries": true, // Warm up subtitle dictionaries at startup. Values: true | false
|
||||||
"jellyfinRemoteSession": true, // Warm up Jellyfin remote session at startup. Values: true | false
|
"jellyfinRemoteSession": true // Warm up Jellyfin remote session at startup. Values: true | false
|
||||||
}, // Background warmup controls for MeCab, Yomitan, dictionaries, and Jellyfin session.
|
}, // Background warmup controls for MeCab, Yomitan, dictionaries, and Jellyfin session.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -81,7 +82,7 @@
|
|||||||
"toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting.
|
"toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting.
|
||||||
"markAudioCard": "CommandOrControl+Shift+A", // Mark audio card setting.
|
"markAudioCard": "CommandOrControl+Shift+A", // Mark audio card setting.
|
||||||
"openRuntimeOptions": "CommandOrControl+Shift+O", // Open runtime options setting.
|
"openRuntimeOptions": "CommandOrControl+Shift+O", // Open runtime options setting.
|
||||||
"openJimaku": "Ctrl+Shift+J", // Open jimaku setting.
|
"openJimaku": "Ctrl+Shift+J" // Open jimaku setting.
|
||||||
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -101,7 +102,7 @@
|
|||||||
"secondarySub": {
|
"secondarySub": {
|
||||||
"secondarySubLanguages": [], // Secondary sub languages setting.
|
"secondarySubLanguages": [], // Secondary sub languages setting.
|
||||||
"autoLoadSecondarySub": false, // Auto load secondary sub setting. Values: true | false
|
"autoLoadSecondarySub": false, // Auto load secondary sub setting. Values: true | false
|
||||||
"defaultMode": "hover", // Default mode setting.
|
"defaultMode": "hover" // Default mode setting.
|
||||||
}, // Dual subtitle track options.
|
}, // Dual subtitle track options.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -113,7 +114,7 @@
|
|||||||
"alass_path": "", // Alass path setting.
|
"alass_path": "", // Alass path setting.
|
||||||
"ffsubsync_path": "", // Ffsubsync path setting.
|
"ffsubsync_path": "", // Ffsubsync path setting.
|
||||||
"ffmpeg_path": "", // Ffmpeg path setting.
|
"ffmpeg_path": "", // Ffmpeg path setting.
|
||||||
"replace": true, // Replace the active subtitle file when sync completes. Values: true | false
|
"replace": true // Replace the active subtitle file when sync completes. Values: true | false
|
||||||
}, // Subsync engine and executable paths.
|
}, // Subsync engine and executable paths.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -121,7 +122,7 @@
|
|||||||
// Initial vertical subtitle position from the bottom.
|
// Initial vertical subtitle position from the bottom.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"subtitlePosition": {
|
"subtitlePosition": {
|
||||||
"yPercent": 10, // Y percent setting.
|
"yPercent": 10 // Y percent setting.
|
||||||
}, // Initial vertical subtitle position from the bottom.
|
}, // Initial vertical subtitle position from the bottom.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -158,7 +159,7 @@
|
|||||||
"N2": "#f5a97f", // N2 setting.
|
"N2": "#f5a97f", // N2 setting.
|
||||||
"N3": "#f9e2af", // N3 setting.
|
"N3": "#f9e2af", // N3 setting.
|
||||||
"N4": "#a6e3a1", // N4 setting.
|
"N4": "#a6e3a1", // N4 setting.
|
||||||
"N5": "#8aadf4", // N5 setting.
|
"N5": "#8aadf4" // N5 setting.
|
||||||
}, // Jlpt colors setting.
|
}, // Jlpt colors setting.
|
||||||
"frequencyDictionary": {
|
"frequencyDictionary": {
|
||||||
"enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false
|
"enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false
|
||||||
@@ -167,7 +168,13 @@
|
|||||||
"mode": "single", // single: use one color for all matching tokens. banded: use color ramp by frequency band. Values: single | banded
|
"mode": "single", // single: use one color for all matching tokens. banded: use color ramp by frequency band. Values: single | banded
|
||||||
"matchMode": "headword", // headword: frequency lookup uses dictionary form. surface: lookup uses subtitle-visible token text. Values: headword | surface
|
"matchMode": "headword", // headword: frequency lookup uses dictionary form. surface: lookup uses subtitle-visible token text. Values: headword | surface
|
||||||
"singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`.
|
"singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`.
|
||||||
"bandedColors": ["#ed8796", "#f5a97f", "#f9e2af", "#8bd5ca", "#8aadf4"], // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
|
"bandedColors": [
|
||||||
|
"#ed8796",
|
||||||
|
"#f5a97f",
|
||||||
|
"#f9e2af",
|
||||||
|
"#8bd5ca",
|
||||||
|
"#8aadf4"
|
||||||
|
] // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
|
||||||
}, // Frequency dictionary setting.
|
}, // Frequency dictionary setting.
|
||||||
"secondary": {
|
"secondary": {
|
||||||
"fontFamily": "Inter, Noto Sans, Helvetica Neue, sans-serif", // Font family setting.
|
"fontFamily": "Inter, Noto Sans, Helvetica Neue, sans-serif", // Font family setting.
|
||||||
@@ -182,8 +189,8 @@
|
|||||||
"backgroundColor": "rgba(20, 22, 34, 0.78)", // Background color setting.
|
"backgroundColor": "rgba(20, 22, 34, 0.78)", // Background color setting.
|
||||||
"backdropFilter": "blur(6px)", // Backdrop filter setting.
|
"backdropFilter": "blur(6px)", // Backdrop filter setting.
|
||||||
"fontWeight": "600", // Font weight setting.
|
"fontWeight": "600", // Font weight setting.
|
||||||
"fontStyle": "normal", // Font style setting.
|
"fontStyle": "normal" // Font style setting.
|
||||||
}, // Secondary setting.
|
} // Secondary setting.
|
||||||
}, // Primary and secondary subtitle styling.
|
}, // Primary and secondary subtitle styling.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -197,7 +204,7 @@
|
|||||||
"model": "openai/gpt-4o-mini", // Model setting.
|
"model": "openai/gpt-4o-mini", // Model setting.
|
||||||
"baseUrl": "https://openrouter.ai/api", // Base URL for the shared OpenAI-compatible AI provider.
|
"baseUrl": "https://openrouter.ai/api", // Base URL for the shared OpenAI-compatible AI provider.
|
||||||
"systemPrompt": "You are a translation engine. Return only the translated text with no explanations.", // System prompt setting.
|
"systemPrompt": "You are a translation engine. Return only the translated text with no explanations.", // System prompt setting.
|
||||||
"requestTimeoutMs": 15000, // Timeout in milliseconds for shared AI provider requests.
|
"requestTimeoutMs": 15000 // Timeout in milliseconds for shared AI provider requests.
|
||||||
}, // Canonical OpenAI-compatible provider transport settings shared by Anki and YouTube subtitle fixing.
|
}, // Canonical OpenAI-compatible provider transport settings shared by Anki and YouTube subtitle fixing.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -215,20 +222,22 @@
|
|||||||
"enabled": true, // Enable local AnkiConnect-compatible proxy for push-based auto-enrichment. Values: true | false
|
"enabled": true, // Enable local AnkiConnect-compatible proxy for push-based auto-enrichment. Values: true | false
|
||||||
"host": "127.0.0.1", // Bind host for local AnkiConnect proxy.
|
"host": "127.0.0.1", // Bind host for local AnkiConnect proxy.
|
||||||
"port": 8766, // Bind port for local AnkiConnect proxy.
|
"port": 8766, // Bind port for local AnkiConnect proxy.
|
||||||
"upstreamUrl": "http://127.0.0.1:8765", // Upstream AnkiConnect URL proxied by local AnkiConnect proxy.
|
"upstreamUrl": "http://127.0.0.1:8765" // Upstream AnkiConnect URL proxied by local AnkiConnect proxy.
|
||||||
}, // Proxy setting.
|
}, // Proxy setting.
|
||||||
"tags": ["SubMiner"], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
|
"tags": [
|
||||||
|
"SubMiner"
|
||||||
|
], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
|
||||||
"fields": {
|
"fields": {
|
||||||
"audio": "ExpressionAudio", // Audio setting.
|
"audio": "ExpressionAudio", // Audio setting.
|
||||||
"image": "Picture", // Image setting.
|
"image": "Picture", // Image setting.
|
||||||
"sentence": "Sentence", // Sentence setting.
|
"sentence": "Sentence", // Sentence setting.
|
||||||
"miscInfo": "MiscInfo", // Misc info setting.
|
"miscInfo": "MiscInfo", // Misc info setting.
|
||||||
"translation": "SelectionText", // Translation setting.
|
"translation": "SelectionText" // Translation setting.
|
||||||
}, // Fields setting.
|
}, // Fields setting.
|
||||||
"ai": {
|
"ai": {
|
||||||
"enabled": false, // Enable AI provider usage for Anki translation/enrichment flows. Values: true | false
|
"enabled": false, // Enable AI provider usage for Anki translation/enrichment flows. Values: true | false
|
||||||
"model": "", // Optional model override for Anki AI translation/enrichment flows.
|
"model": "", // Optional model override for Anki AI translation/enrichment flows.
|
||||||
"systemPrompt": "", // Optional system prompt override for Anki AI translation/enrichment flows.
|
"systemPrompt": "" // Optional system prompt override for Anki AI translation/enrichment flows.
|
||||||
}, // Ai setting.
|
}, // Ai setting.
|
||||||
"media": {
|
"media": {
|
||||||
"generateAudio": true, // Generate audio setting. Values: true | false
|
"generateAudio": true, // Generate audio setting. Values: true | false
|
||||||
@@ -241,7 +250,7 @@
|
|||||||
"animatedCrf": 35, // Animated crf setting.
|
"animatedCrf": 35, // Animated crf setting.
|
||||||
"audioPadding": 0.5, // Audio padding setting.
|
"audioPadding": 0.5, // Audio padding setting.
|
||||||
"fallbackDuration": 3, // Fallback duration setting.
|
"fallbackDuration": 3, // Fallback duration setting.
|
||||||
"maxMediaDuration": 30, // Max media duration setting.
|
"maxMediaDuration": 30 // Max media duration setting.
|
||||||
}, // Media setting.
|
}, // Media setting.
|
||||||
"behavior": {
|
"behavior": {
|
||||||
"overwriteAudio": true, // Overwrite audio setting. Values: true | false
|
"overwriteAudio": true, // Overwrite audio setting. Values: true | false
|
||||||
@@ -249,7 +258,7 @@
|
|||||||
"mediaInsertMode": "append", // Media insert mode setting.
|
"mediaInsertMode": "append", // Media insert mode setting.
|
||||||
"highlightWord": true, // Highlight word setting. Values: true | false
|
"highlightWord": true, // Highlight word setting. Values: true | false
|
||||||
"notificationType": "osd", // Notification type setting.
|
"notificationType": "osd", // Notification type setting.
|
||||||
"autoUpdateNewCards": true, // Automatically update newly added cards. Values: true | false
|
"autoUpdateNewCards": true // Automatically update newly added cards. Values: true | false
|
||||||
}, // Behavior setting.
|
}, // Behavior setting.
|
||||||
"nPlusOne": {
|
"nPlusOne": {
|
||||||
"highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false
|
"highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false
|
||||||
@@ -258,20 +267,20 @@
|
|||||||
"decks": [], // Decks used for N+1 known-word cache scope. Supports one or more deck names.
|
"decks": [], // Decks used for N+1 known-word cache scope. Supports one or more deck names.
|
||||||
"minSentenceWords": 3, // Minimum sentence word count required for N+1 targeting (default: 3).
|
"minSentenceWords": 3, // Minimum sentence word count required for N+1 targeting (default: 3).
|
||||||
"nPlusOne": "#c6a0f6", // Color used for the single N+1 target token highlight.
|
"nPlusOne": "#c6a0f6", // Color used for the single N+1 target token highlight.
|
||||||
"knownWord": "#a6da95", // Color used for legacy known-word highlights.
|
"knownWord": "#a6da95" // Color used for legacy known-word highlights.
|
||||||
}, // N plus one setting.
|
}, // N plus one setting.
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"pattern": "[SubMiner] %f (%t)", // Pattern setting.
|
"pattern": "[SubMiner] %f (%t)" // Pattern setting.
|
||||||
}, // Metadata setting.
|
}, // Metadata setting.
|
||||||
"isLapis": {
|
"isLapis": {
|
||||||
"enabled": false, // Enabled setting. Values: true | false
|
"enabled": false, // Enabled setting. Values: true | false
|
||||||
"sentenceCardModel": "Japanese sentences", // Sentence card model setting.
|
"sentenceCardModel": "Japanese sentences" // Sentence card model setting.
|
||||||
}, // Is lapis setting.
|
}, // Is lapis setting.
|
||||||
"isKiku": {
|
"isKiku": {
|
||||||
"enabled": false, // Enabled setting. Values: true | false
|
"enabled": false, // Enabled setting. Values: true | false
|
||||||
"fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled
|
"fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled
|
||||||
"deleteDuplicateInAuto": true, // Delete duplicate in auto setting. Values: true | false
|
"deleteDuplicateInAuto": true // Delete duplicate in auto setting. Values: true | false
|
||||||
}, // Is kiku setting.
|
} // Is kiku setting.
|
||||||
}, // Automatic Anki updates and media generation options.
|
}, // Automatic Anki updates and media generation options.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -281,7 +290,7 @@
|
|||||||
"jimaku": {
|
"jimaku": {
|
||||||
"apiBaseUrl": "https://jimaku.cc", // Api base url setting.
|
"apiBaseUrl": "https://jimaku.cc", // Api base url setting.
|
||||||
"languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none
|
"languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none
|
||||||
"maxEntryResults": 10, // Maximum Jimaku search results returned.
|
"maxEntryResults": 10 // Maximum Jimaku search results returned.
|
||||||
}, // Jimaku API configuration and defaults.
|
}, // Jimaku API configuration and defaults.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -296,9 +305,12 @@
|
|||||||
"fixWithAi": false, // Use shared AI provider to post-process whisper-generated YouTube subtitles. Values: true | false
|
"fixWithAi": false, // Use shared AI provider to post-process whisper-generated YouTube subtitles. Values: true | false
|
||||||
"ai": {
|
"ai": {
|
||||||
"model": "", // Optional model override for YouTube subtitle AI post-processing.
|
"model": "", // Optional model override for YouTube subtitle AI post-processing.
|
||||||
"systemPrompt": "", // Optional system prompt override for YouTube subtitle AI post-processing.
|
"systemPrompt": "" // Optional system prompt override for YouTube subtitle AI post-processing.
|
||||||
}, // Ai setting.
|
}, // Ai setting.
|
||||||
"primarySubLanguages": ["ja", "jpn"], // Comma-separated primary subtitle language priority used by the launcher.
|
"primarySubLanguages": [
|
||||||
|
"ja",
|
||||||
|
"jpn"
|
||||||
|
] // Comma-separated primary subtitle language priority used by the launcher.
|
||||||
}, // Defaults for SubMiner YouTube subtitle generation.
|
}, // Defaults for SubMiner YouTube subtitle generation.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -319,9 +331,9 @@
|
|||||||
"collapsibleSections": {
|
"collapsibleSections": {
|
||||||
"description": false, // Open the Description section by default in character dictionary glossary entries. Values: true | false
|
"description": false, // Open the Description section by default in character dictionary glossary entries. Values: true | false
|
||||||
"characterInformation": false, // Open the Character Information section by default in character dictionary glossary entries. Values: true | false
|
"characterInformation": false, // Open the Character Information section by default in character dictionary glossary entries. Values: true | false
|
||||||
"voicedBy": false, // Open the Voiced by section by default in character dictionary glossary entries. Values: true | false
|
"voicedBy": false // Open the Voiced by section by default in character dictionary glossary entries. Values: true | false
|
||||||
}, // Collapsible sections setting.
|
} // Collapsible sections setting.
|
||||||
}, // Character dictionary setting.
|
} // Character dictionary setting.
|
||||||
}, // Anilist API credentials and update behavior.
|
}, // Anilist API credentials and update behavior.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -345,8 +357,16 @@
|
|||||||
"pullPictures": false, // Enable Jellyfin poster/icon fetching for launcher menus. Values: true | false
|
"pullPictures": false, // Enable Jellyfin poster/icon fetching for launcher menus. Values: true | false
|
||||||
"iconCacheDir": "/tmp/subminer-jellyfin-icons", // Directory used by launcher for cached Jellyfin poster icons.
|
"iconCacheDir": "/tmp/subminer-jellyfin-icons", // Directory used by launcher for cached Jellyfin poster icons.
|
||||||
"directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false
|
"directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false
|
||||||
"directPlayContainers": ["mkv", "mp4", "webm", "mov", "flac", "mp3", "aac"], // Container allowlist for direct play decisions.
|
"directPlayContainers": [
|
||||||
"transcodeVideoCodec": "h264", // Preferred transcode video codec when direct play is unavailable.
|
"mkv",
|
||||||
|
"mp4",
|
||||||
|
"webm",
|
||||||
|
"mov",
|
||||||
|
"flac",
|
||||||
|
"mp3",
|
||||||
|
"aac"
|
||||||
|
], // Container allowlist for direct play decisions.
|
||||||
|
"transcodeVideoCodec": "h264" // Preferred transcode video codec when direct play is unavailable.
|
||||||
}, // Optional Jellyfin integration for auth, browsing, and playback launch.
|
}, // Optional Jellyfin integration for auth, browsing, and playback launch.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -357,7 +377,7 @@
|
|||||||
"discordPresence": {
|
"discordPresence": {
|
||||||
"enabled": false, // Enable optional Discord Rich Presence updates. Values: true | false
|
"enabled": false, // Enable optional Discord Rich Presence updates. Values: true | false
|
||||||
"updateIntervalMs": 3000, // Minimum interval between presence payload updates.
|
"updateIntervalMs": 3000, // Minimum interval between presence payload updates.
|
||||||
"debounceMs": 750, // Debounce delay used to collapse bursty presence updates.
|
"debounceMs": 750 // Debounce delay used to collapse bursty presence updates.
|
||||||
}, // Optional Discord Rich Presence activity card updates for current playback/study session.
|
}, // Optional Discord Rich Presence activity card updates for current playback/study session.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -379,7 +399,7 @@
|
|||||||
"telemetryDays": 30, // Telemetry retention window in days.
|
"telemetryDays": 30, // Telemetry retention window in days.
|
||||||
"dailyRollupsDays": 365, // Daily rollup retention window in days.
|
"dailyRollupsDays": 365, // Daily rollup retention window in days.
|
||||||
"monthlyRollupsDays": 1825, // Monthly rollup retention window in days.
|
"monthlyRollupsDays": 1825, // Monthly rollup retention window in days.
|
||||||
"vacuumIntervalDays": 7, // Minimum days between VACUUM runs.
|
"vacuumIntervalDays": 7 // Minimum days between VACUUM runs.
|
||||||
}, // Retention setting.
|
} // Retention setting.
|
||||||
}, // Enable/disable immersion tracking.
|
} // Enable/disable immersion tracking.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,4 +31,6 @@ bun run docs:dev
|
|||||||
- Root directory: `docs-site`
|
- Root directory: `docs-site`
|
||||||
- Build command: `bun run docs:build`
|
- Build command: `bun run docs:build`
|
||||||
- Build output directory: `.vitepress/dist`
|
- Build output directory: `.vitepress/dist`
|
||||||
- Build watch paths: `docs-site/**`
|
- Build watch paths: `docs-site/*`
|
||||||
|
|
||||||
|
Cloudflare Pages watch paths use a single `*` wildcard for monorepo subdirectories. `docs-site/*` matches nested files under the docs site; `docs-site/**` can cause docs-only pushes to be skipped.
|
||||||
|
|||||||
@@ -186,7 +186,9 @@ Cloudflare Pages deploy settings:
|
|||||||
- Root directory: `docs-site`
|
- Root directory: `docs-site`
|
||||||
- Build command: `bun run docs:build`
|
- Build command: `bun run docs:build`
|
||||||
- Build output directory: `.vitepress/dist`
|
- Build output directory: `.vitepress/dist`
|
||||||
- Build watch paths: `docs-site/**`
|
- Build watch paths: `docs-site/*`
|
||||||
|
|
||||||
|
Use Cloudflare's single `*` wildcard syntax for watch paths. `docs-site/*` covers nested docs-site changes in the repo; `docs-site/**` is not the correct Pages pattern and may skip docs-only pushes.
|
||||||
|
|
||||||
## Makefile Reference
|
## Makefile Reference
|
||||||
|
|
||||||
|
|||||||
@@ -22,9 +22,11 @@ test('docs reflect current launcher and release surfaces', () => {
|
|||||||
|
|
||||||
expect(readmeContents).toContain('Root directory: `docs-site`');
|
expect(readmeContents).toContain('Root directory: `docs-site`');
|
||||||
expect(readmeContents).toContain('Build output directory: `.vitepress/dist`');
|
expect(readmeContents).toContain('Build output directory: `.vitepress/dist`');
|
||||||
|
expect(readmeContents).toContain('Build watch paths: `docs-site/*`');
|
||||||
expect(developmentContents).not.toContain('../subminer-docs');
|
expect(developmentContents).not.toContain('../subminer-docs');
|
||||||
expect(developmentContents).toContain('bun run docs:build');
|
expect(developmentContents).toContain('bun run docs:build');
|
||||||
expect(developmentContents).toContain('bun run docs:test');
|
expect(developmentContents).toContain('bun run docs:test');
|
||||||
|
expect(developmentContents).toContain('Build watch paths: `docs-site/*`');
|
||||||
expect(developmentContents).not.toContain('test:subtitle:dist');
|
expect(developmentContents).not.toContain('test:subtitle:dist');
|
||||||
expect(developmentContents).toContain('bun run build:win');
|
expect(developmentContents).toContain('bun run build:win');
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* SubMiner Example Configuration File
|
* SubMiner Example Configuration File
|
||||||
*
|
*
|
||||||
* This file is auto-generated from src/config/definitions.ts.
|
* This file is auto-generated from src/config/definitions.ts.
|
||||||
* Copy to $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) and edit as needed.
|
* Copy to %APPDATA%/SubMiner/config.jsonc on Windows, or $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) on Linux/macOS.
|
||||||
*/
|
*/
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -201,7 +201,9 @@
|
|||||||
"enabled": false, // Enable shared OpenAI-compatible AI provider features. Values: true | false
|
"enabled": false, // Enable shared OpenAI-compatible AI provider features. Values: true | false
|
||||||
"apiKey": "", // Static API key for the shared OpenAI-compatible AI provider.
|
"apiKey": "", // Static API key for the shared OpenAI-compatible AI provider.
|
||||||
"apiKeyCommand": "", // Shell command used to resolve the shared AI provider API key.
|
"apiKeyCommand": "", // Shell command used to resolve the shared AI provider API key.
|
||||||
|
"model": "openai/gpt-4o-mini", // Model setting.
|
||||||
"baseUrl": "https://openrouter.ai/api", // Base URL for the shared OpenAI-compatible AI provider.
|
"baseUrl": "https://openrouter.ai/api", // Base URL for the shared OpenAI-compatible AI provider.
|
||||||
|
"systemPrompt": "You are a translation engine. Return only the translated text with no explanations.", // System prompt setting.
|
||||||
"requestTimeoutMs": 15000 // Timeout in milliseconds for shared AI provider requests.
|
"requestTimeoutMs": 15000 // Timeout in milliseconds for shared AI provider requests.
|
||||||
}, // Canonical OpenAI-compatible provider transport settings shared by Anki and YouTube subtitle fixing.
|
}, // Canonical OpenAI-compatible provider transport settings shared by Anki and YouTube subtitle fixing.
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
4. Review `CHANGELOG.md`.
|
4. Review `CHANGELOG.md`.
|
||||||
5. Run release gate locally:
|
5. Run release gate locally:
|
||||||
`bun run changelog:check --version <version>`
|
`bun run changelog:check --version <version>`
|
||||||
|
`bun run verify:config-example`
|
||||||
`bun run test:fast`
|
`bun run test:fast`
|
||||||
`bun run typecheck`
|
`bun run typecheck`
|
||||||
6. Commit release prep.
|
6. Commit release prep.
|
||||||
|
|||||||
@@ -28,8 +28,8 @@
|
|||||||
"docs:build": "bun run --cwd docs-site docs:build",
|
"docs:build": "bun run --cwd docs-site docs:build",
|
||||||
"docs:preview": "bun run --cwd docs-site docs:preview",
|
"docs:preview": "bun run --cwd docs-site docs:preview",
|
||||||
"docs:test": "bun run --cwd docs-site test",
|
"docs:test": "bun run --cwd docs-site test",
|
||||||
"test:config:src": "bun test src/config/config.test.ts src/config/path-resolution.test.ts src/config/resolve/anki-connect.test.ts src/config/resolve/subtitle-style.test.ts src/config/resolve/jellyfin.test.ts src/config/definitions/domain-registry.test.ts src/generate-config-example.test.ts",
|
"test:config:src": "bun test src/config/config.test.ts src/config/path-resolution.test.ts src/config/resolve/anki-connect.test.ts src/config/resolve/subtitle-style.test.ts src/config/resolve/jellyfin.test.ts src/config/definitions/domain-registry.test.ts src/generate-config-example.test.ts src/verify-config-example.test.ts",
|
||||||
"test:config:dist": "bun test dist/config/config.test.js dist/config/path-resolution.test.js dist/config/resolve/anki-connect.test.js dist/config/resolve/subtitle-style.test.js dist/config/resolve/jellyfin.test.js dist/config/definitions/domain-registry.test.js dist/generate-config-example.test.js",
|
"test:config:dist": "bun test dist/config/config.test.js dist/config/path-resolution.test.js dist/config/resolve/anki-connect.test.js dist/config/resolve/subtitle-style.test.js dist/config/resolve/jellyfin.test.js dist/config/definitions/domain-registry.test.js dist/generate-config-example.test.js dist/verify-config-example.test.js",
|
||||||
"test:config:smoke:dist": "bun test dist/config/path-resolution.test.js",
|
"test:config:smoke:dist": "bun test dist/config/path-resolution.test.js",
|
||||||
"test:plugin:src": "lua scripts/test-plugin-start-gate.lua && lua scripts/test-plugin-binary-windows.lua",
|
"test:plugin:src": "lua scripts/test-plugin-start-gate.lua && lua scripts/test-plugin-binary-windows.lua",
|
||||||
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",
|
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",
|
||||||
@@ -55,7 +55,8 @@
|
|||||||
"test:core": "bun run test:core:src",
|
"test:core": "bun run test:core:src",
|
||||||
"test:subtitle": "bun run test:subtitle:src",
|
"test:subtitle": "bun run test:subtitle:src",
|
||||||
"test:fast": "bun run test:config:src && bun run test:core:src && bun test src/main-entry-runtime.test.ts src/anki-integration/anki-connect-proxy.test.ts src/release-workflow.test.ts src/ci-workflow.test.ts scripts/build-changelog.test.ts scripts/mkv-to-readme-video.test.ts && bun run tsc && bun test dist/main/runtime/registry.test.js",
|
"test:fast": "bun run test:config:src && bun run test:core:src && bun test src/main-entry-runtime.test.ts src/anki-integration/anki-connect-proxy.test.ts src/release-workflow.test.ts src/ci-workflow.test.ts scripts/build-changelog.test.ts scripts/mkv-to-readme-video.test.ts && bun run tsc && bun test dist/main/runtime/registry.test.js",
|
||||||
"generate:config-example": "bun run build && bun dist/generate-config-example.js",
|
"generate:config-example": "bun run src/generate-config-example.ts",
|
||||||
|
"verify:config-example": "bun run src/verify-config-example.ts",
|
||||||
"start": "bun run build && electron . --start",
|
"start": "bun run build && electron . --start",
|
||||||
"dev": "bun run build && electron . --start --dev",
|
"dev": "bun run build && electron . --start --dev",
|
||||||
"stop": "electron . --stop",
|
"stop": "electron . --stop",
|
||||||
|
|||||||
@@ -14,3 +14,7 @@ test('ci workflow checks pull requests for required changelog fragments', () =>
|
|||||||
assert.match(ciWorkflow, /bun run changelog:pr-check/);
|
assert.match(ciWorkflow, /bun run changelog:pr-check/);
|
||||||
assert.match(ciWorkflow, /skip-changelog/);
|
assert.match(ciWorkflow, /skip-changelog/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('ci workflow verifies generated config examples stay in sync', () => {
|
||||||
|
assert.match(ciWorkflow, /bun run verify:config-example/);
|
||||||
|
});
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ test('release workflow verifies a committed changelog section before publish', (
|
|||||||
assert.match(releaseWorkflow, /bun run changelog:check/);
|
assert.match(releaseWorkflow, /bun run changelog:check/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('release workflow verifies generated config examples before packaging artifacts', () => {
|
||||||
|
assert.match(releaseWorkflow, /bun run verify:config-example/);
|
||||||
|
});
|
||||||
|
|
||||||
test('release workflow generates release notes from committed changelog output', () => {
|
test('release workflow generates release notes from committed changelog output', () => {
|
||||||
assert.match(releaseWorkflow, /bun run changelog:release-notes/);
|
assert.match(releaseWorkflow, /bun run changelog:release-notes/);
|
||||||
assert.ok(!releaseWorkflow.includes('git log --pretty=format:"- %s"'));
|
assert.ok(!releaseWorkflow.includes('git log --pretty=format:"- %s"'));
|
||||||
@@ -47,6 +51,10 @@ test('release package scripts disable implicit electron-builder publishing', ()
|
|||||||
assert.match(packageJson.scripts['build:win:unsigned'] ?? '', /build-win-unsigned\.mjs/);
|
assert.match(packageJson.scripts['build:win:unsigned'] ?? '', /build-win-unsigned\.mjs/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('config example generation runs directly from source without unrelated bundle prerequisites', () => {
|
||||||
|
assert.equal(packageJson.scripts['generate:config-example'], 'bun run src/generate-config-example.ts');
|
||||||
|
});
|
||||||
|
|
||||||
test('windows release workflow publishes unsigned artifacts directly without SignPath', () => {
|
test('windows release workflow publishes unsigned artifacts directly without SignPath', () => {
|
||||||
assert.match(releaseWorkflow, /Build unsigned Windows artifacts/);
|
assert.match(releaseWorkflow, /Build unsigned Windows artifacts/);
|
||||||
assert.match(releaseWorkflow, /run: bun run build:win:unsigned/);
|
assert.match(releaseWorkflow, /run: bun run build:win:unsigned/);
|
||||||
|
|||||||
93
src/verify-config-example.test.ts
Normal file
93
src/verify-config-example.test.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import test from 'node:test';
|
||||||
|
import {
|
||||||
|
verifyConfigExampleArtifacts,
|
||||||
|
type ConfigExampleVerificationResult,
|
||||||
|
} from './verify-config-example';
|
||||||
|
|
||||||
|
function createWorkspace(name: string): string {
|
||||||
|
const baseDir = path.join(process.cwd(), '.tmp', 'verify-config-example-test');
|
||||||
|
fs.mkdirSync(baseDir, { recursive: true });
|
||||||
|
return fs.mkdtempSync(path.join(baseDir, `${name}-`));
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertResult(
|
||||||
|
result: ConfigExampleVerificationResult,
|
||||||
|
expected: {
|
||||||
|
missingPaths?: string[];
|
||||||
|
stalePaths?: string[];
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
assert.deepEqual(result.missingPaths, expected.missingPaths ?? []);
|
||||||
|
assert.deepEqual(result.stalePaths, expected.stalePaths ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('verifyConfigExampleArtifacts reports repo config example when missing', () => {
|
||||||
|
const workspace = createWorkspace('missing-root');
|
||||||
|
const projectRoot = path.join(workspace, 'SubMiner');
|
||||||
|
|
||||||
|
fs.mkdirSync(projectRoot, { recursive: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = verifyConfigExampleArtifacts({ cwd: projectRoot });
|
||||||
|
|
||||||
|
assertResult(result, {
|
||||||
|
missingPaths: [path.join(projectRoot, 'config.example.jsonc')],
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(workspace, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('verifyConfigExampleArtifacts reports stale docs-site artifact when docs site exists', () => {
|
||||||
|
const workspace = createWorkspace('stale-docs-site');
|
||||||
|
const projectRoot = path.join(workspace, 'SubMiner');
|
||||||
|
const docsSiteRoot = path.join(projectRoot, 'docs-site');
|
||||||
|
|
||||||
|
fs.mkdirSync(projectRoot, { recursive: true });
|
||||||
|
fs.mkdirSync(path.join(docsSiteRoot, 'public'), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(projectRoot, 'config.example.jsonc'), 'fresh\n');
|
||||||
|
fs.writeFileSync(path.join(docsSiteRoot, 'public', 'config.example.jsonc'), 'stale\n');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = verifyConfigExampleArtifacts({
|
||||||
|
cwd: projectRoot,
|
||||||
|
template: 'fresh\n',
|
||||||
|
});
|
||||||
|
|
||||||
|
assertResult(result, {
|
||||||
|
stalePaths: [path.join(docsSiteRoot, 'public', 'config.example.jsonc')],
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(workspace, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('verifyConfigExampleArtifacts passes when repo and docs-site artifacts match', () => {
|
||||||
|
const workspace = createWorkspace('matching-artifacts');
|
||||||
|
const projectRoot = path.join(workspace, 'SubMiner');
|
||||||
|
const docsSiteRoot = path.join(projectRoot, 'docs-site');
|
||||||
|
const template = '{\n "ok": true\n}\n';
|
||||||
|
|
||||||
|
fs.mkdirSync(projectRoot, { recursive: true });
|
||||||
|
fs.mkdirSync(path.join(docsSiteRoot, 'public'), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(projectRoot, 'config.example.jsonc'), template);
|
||||||
|
fs.writeFileSync(path.join(docsSiteRoot, 'public', 'config.example.jsonc'), template);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = verifyConfigExampleArtifacts({
|
||||||
|
cwd: projectRoot,
|
||||||
|
template,
|
||||||
|
});
|
||||||
|
|
||||||
|
assertResult(result, {});
|
||||||
|
assert.deepEqual(result.outputPaths, [
|
||||||
|
path.join(projectRoot, 'config.example.jsonc'),
|
||||||
|
path.join(docsSiteRoot, 'public', 'config.example.jsonc'),
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(workspace, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
80
src/verify-config-example.ts
Normal file
80
src/verify-config-example.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import { DEFAULT_CONFIG, generateConfigTemplate } from './config';
|
||||||
|
import { resolveConfigExampleOutputPaths } from './generate-config-example';
|
||||||
|
|
||||||
|
export type ConfigExampleVerificationResult = {
|
||||||
|
docsSiteDetected: boolean;
|
||||||
|
missingPaths: string[];
|
||||||
|
outputPaths: string[];
|
||||||
|
stalePaths: string[];
|
||||||
|
template: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function verifyConfigExampleArtifacts(options?: {
|
||||||
|
cwd?: string;
|
||||||
|
docsSiteDirName?: string;
|
||||||
|
template?: string;
|
||||||
|
deps?: {
|
||||||
|
existsSync?: (candidate: string) => boolean;
|
||||||
|
readFileSync?: (candidate: string, encoding: BufferEncoding) => string;
|
||||||
|
};
|
||||||
|
}): ConfigExampleVerificationResult {
|
||||||
|
const existsSync = options?.deps?.existsSync ?? fs.existsSync;
|
||||||
|
const readFileSync = options?.deps?.readFileSync ?? fs.readFileSync;
|
||||||
|
const template = options?.template ?? generateConfigTemplate(DEFAULT_CONFIG);
|
||||||
|
const outputPaths = resolveConfigExampleOutputPaths({
|
||||||
|
cwd: options?.cwd,
|
||||||
|
docsSiteDirName: options?.docsSiteDirName,
|
||||||
|
existsSync,
|
||||||
|
});
|
||||||
|
const missingPaths: string[] = [];
|
||||||
|
const stalePaths: string[] = [];
|
||||||
|
|
||||||
|
for (const outputPath of outputPaths) {
|
||||||
|
if (!existsSync(outputPath)) {
|
||||||
|
missingPaths.push(outputPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (readFileSync(outputPath, 'utf-8') !== template) {
|
||||||
|
stalePaths.push(outputPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
docsSiteDetected: outputPaths.length > 1,
|
||||||
|
missingPaths,
|
||||||
|
outputPaths,
|
||||||
|
stalePaths,
|
||||||
|
template,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function main(): void {
|
||||||
|
const result = verifyConfigExampleArtifacts();
|
||||||
|
|
||||||
|
if (result.missingPaths.length === 0 && result.stalePaths.length === 0) {
|
||||||
|
console.log('[OK] config example artifacts verified');
|
||||||
|
for (const outputPath of result.outputPaths) {
|
||||||
|
console.log(` ${outputPath}`);
|
||||||
|
}
|
||||||
|
if (!result.docsSiteDetected) {
|
||||||
|
console.log(' docs-site not present; skipped docs artifact check');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('[FAIL] config example artifacts are out of sync');
|
||||||
|
for (const missingPath of result.missingPaths) {
|
||||||
|
console.error(` missing: ${missingPath}`);
|
||||||
|
}
|
||||||
|
for (const stalePath of result.stalePaths) {
|
||||||
|
console.error(` stale: ${stalePath}`);
|
||||||
|
}
|
||||||
|
console.error(' run: bun run generate:config-example');
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user