diff --git a/backlog/tasks/task-152 - Fix-early-Electron-userData-path-casing-to-stay-under-SubMiner-config-dir.md b/backlog/tasks/task-152 - Fix-early-Electron-userData-path-casing-to-stay-under-SubMiner-config-dir.md new file mode 100644 index 0000000..171540e --- /dev/null +++ b/backlog/tasks/task-152 - Fix-early-Electron-userData-path-casing-to-stay-under-SubMiner-config-dir.md @@ -0,0 +1,65 @@ +--- +id: TASK-152 +title: Fix early Electron userData path casing to stay under SubMiner config dir +status: Done +assignee: + - codex +created_date: '2026-03-10 06:46' +updated_date: '2026-03-10 06:51' +labels: + - bug + - config + - electron +dependencies: [] +references: + - /home/sudacode/projects/japanese/SubMiner/src/main-entry.ts + - /home/sudacode/projects/japanese/SubMiner/src/main.ts + - /home/sudacode/projects/japanese/SubMiner/src/config/path-resolution.ts +documentation: + - /home/sudacode/projects/japanese/subminer-docs/development.md + - /home/sudacode/projects/japanese/subminer-docs/architecture.md +priority: high +--- + +## Description + + +Electron startup touches the app object before the main-process bootstrap overrides userData, which can create/write the default lowercase ~/.config/subminer directory on Linux/macOS. Ensure early startup pins the app identity and userData path to the canonical SubMiner config directory before any Electron APIs can materialize the default path, and keep regression coverage around the bootstrap path behavior. + + +## Acceptance Criteria + +- [x] #1 Electron startup uses the canonical SubMiner config directory as userData before other Electron app calls can create the default lowercase directory. +- [x] #2 Regression tests cover the early bootstrap path setup and fail if startup falls back to a lowercase subminer config path. +- [x] #3 Existing config path resolution behavior for SubMiner casing remains intact. + + +## Implementation Plan + + +1. Add regression coverage for early Electron bootstrap path setup, including a case that would otherwise fall back to lowercase subminer. +2. Extract a pure helper that computes and applies the canonical app name/userData path from config resolution. +3. Call the helper from main-entry before any Electron app interactions that could materialize the default userData directory. +4. Run focused tests for startup/config path behavior, then the relevant fast gate if green. + + +## Implementation Notes + + +Added early Electron bootstrap path setup in src/main-entry so app name and userData are pinned to the canonical SubMiner config dir before single-instance/whenReady handling. + +Added a regression test in src/main-entry-runtime.test.ts covering the lowercase subminer fallback case. + +Validation: bun test src/main-entry-runtime.test.ts src/config/path-resolution.test.ts; bun run typecheck. bun run test:fast still fails on an existing unrelated renderer JLPT CSS test in src/renderer/subtitle-render.test.ts. + + +## Final Summary + + +Pinned Electron's app identity and userData path during entry bootstrap so startup uses the canonical SubMiner config directory before any other Electron app calls can materialize the default lowercase path. Added a regression test covering the lowercase subminer fallback case and kept existing config-path resolution coverage green. + +Validation: +- bun test src/main-entry-runtime.test.ts src/config/path-resolution.test.ts +- bun run typecheck +- bun run test:fast (fails on existing unrelated JLPT CSS renderer test in src/renderer/subtitle-render.test.ts) + diff --git a/changes/config-userdata-casing.md b/changes/config-userdata-casing.md new file mode 100644 index 0000000..b7b7951 --- /dev/null +++ b/changes/config-userdata-casing.md @@ -0,0 +1,4 @@ +type: fixed +area: startup + +- Prevented early Electron startup from writing config and user data under a lowercase `~/.config/subminer` path instead of the canonical `~/.config/SubMiner` directory. diff --git a/src/main-entry-runtime.test.ts b/src/main-entry-runtime.test.ts index 3235077..90a7027 100644 --- a/src/main-entry-runtime.test.ts +++ b/src/main-entry-runtime.test.ts @@ -1,6 +1,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { + configureEarlyAppPaths, normalizeStartupArgv, normalizeLaunchMpvTargets, sanitizeHelpEnv, @@ -115,3 +116,30 @@ test('shouldDetachBackgroundLaunch only for first background invocation', () => ); assert.equal(shouldDetachBackgroundLaunch(['--start'], {}), false); }); + +test('configureEarlyAppPaths pins userData to canonical SubMiner config dir', () => { + const calls: string[] = []; + + const userDataPath = configureEarlyAppPaths( + { + setName: (name) => { + calls.push(`name:${name}`); + }, + setPath: (key, value) => { + calls.push(`path:${key}:${value}`); + }, + }, + { + platform: 'linux', + homeDir: '/home/tester', + xdgConfigHome: '/tmp/xdg', + existsSync: (candidate) => candidate === '/tmp/xdg/subminer/config.jsonc', + }, + ); + + assert.equal(userDataPath, '/tmp/xdg/SubMiner'); + assert.deepEqual(calls, [ + 'name:SubMiner', + 'path:userData:/tmp/xdg/SubMiner', + ]); +}); diff --git a/src/main-entry-runtime.ts b/src/main-entry-runtime.ts index a58f831..6666134 100644 --- a/src/main-entry-runtime.ts +++ b/src/main-entry-runtime.ts @@ -1,9 +1,26 @@ +import fs from 'node:fs'; +import os from 'node:os'; import { CliArgs, parseArgs, shouldStartApp } from './cli/args'; +import { resolveConfigDir } from './config/path-resolution'; const BACKGROUND_ARG = '--background'; const START_ARG = '--start'; const PASSWORD_STORE_ARG = '--password-store'; const BACKGROUND_CHILD_ENV = 'SUBMINER_BACKGROUND_CHILD'; +const APP_NAME = 'SubMiner'; + +type EarlyAppLike = { + setName: (name: string) => void; + setPath: (name: 'userData', value: string) => void; +}; + +type EarlyAppPathOptions = { + platform?: NodeJS.Platform; + appDataDir?: string; + xdgConfigHome?: string; + homeDir?: string; + existsSync?: (candidate: string) => boolean; +}; function removeLsfgLayer(env: NodeJS.ProcessEnv): void { if (typeof env.VK_INSTANCE_LAYERS === 'string' && /lsfg/i.test(env.VK_INSTANCE_LAYERS)) { @@ -62,6 +79,24 @@ export function normalizeStartupArgv(argv: string[], env: NodeJS.ProcessEnv): st return argv; } +export function configureEarlyAppPaths( + app: EarlyAppLike, + options?: EarlyAppPathOptions, +): string { + const userDataPath = resolveConfigDir({ + platform: options?.platform ?? process.platform, + appDataDir: options?.appDataDir ?? process.env.APPDATA, + xdgConfigHome: options?.xdgConfigHome ?? process.env.XDG_CONFIG_HOME, + homeDir: options?.homeDir ?? os.homedir(), + existsSync: options?.existsSync ?? fs.existsSync, + }); + + app.setName(APP_NAME); + app.setPath('userData', userDataPath); + + return userDataPath; +} + export function shouldDetachBackgroundLaunch(argv: string[], env: NodeJS.ProcessEnv): boolean { if (env.ELECTRON_RUN_AS_NODE === '1') return false; if (!argv.includes(BACKGROUND_ARG)) return false; diff --git a/src/main-entry.ts b/src/main-entry.ts index c99e6de..eb337f0 100644 --- a/src/main-entry.ts +++ b/src/main-entry.ts @@ -2,6 +2,7 @@ import { spawn } from 'node:child_process'; import { app, dialog } from 'electron'; import { printHelp } from './cli/help'; import { + configureEarlyAppPaths, normalizeLaunchMpvTargets, normalizeStartupArgv, sanitizeStartupEnv, @@ -31,6 +32,7 @@ function applySanitizedEnv(sanitizedEnv: NodeJS.ProcessEnv): void { process.argv = normalizeStartupArgv(process.argv, process.env); applySanitizedEnv(sanitizeStartupEnv(process.env)); +configureEarlyAppPaths(app); if (shouldDetachBackgroundLaunch(process.argv, process.env)) { const child = spawn(process.execPath, process.argv.slice(1), {