fix(storage): address CI and review feedback

This commit is contained in:
2026-08-18 22:42:26 -07:00
parent 06b21a68fa
commit 80ffa26400
8 changed files with 75 additions and 18 deletions
@@ -47,7 +47,7 @@ test('stripFilenameTags normalizes common media-title formats', () => {
); );
}); });
test('fetchIfMissing backfills a missing blob from an existing cover URL', async () => { async function backfillMissingCoverBlob(): Promise<void> {
const dbPath = makeDbPath(); const dbPath = makeDbPath();
const db = new Database(dbPath); const db = new Database(dbPath);
ensureSchema(db); ensureSchema(db);
@@ -103,9 +103,15 @@ test('fetchIfMissing backfills a missing blob from an existing cover URL', async
db.close(); db.close();
cleanupDbPath(dbPath); cleanupDbPath(dbPath);
} }
}); }
test('fetchIfMissing reuses cached cover art from another video in the same anime', async () => { test(
'fetchIfMissing backfills a missing blob from an existing cover URL',
{ timeout: 15_000 },
backfillMissingCoverBlob,
);
async function reuseCachedAnimeCoverArt(): Promise<void> {
const dbPath = makeDbPath(); const dbPath = makeDbPath();
const db = new Database(dbPath); const db = new Database(dbPath);
ensureSchema(db); ensureSchema(db);
@@ -179,7 +185,13 @@ test('fetchIfMissing reuses cached cover art from another video in the same anim
db.close(); db.close();
cleanupDbPath(dbPath); cleanupDbPath(dbPath);
} }
}); }
test(
'fetchIfMissing reuses cached cover art from another video in the same anime',
{ timeout: 15_000 },
reuseCachedAnimeCoverArt,
);
function createJsonResponse(payload: unknown): Response { function createJsonResponse(payload: unknown): Response {
return new Response(JSON.stringify(payload), { return new Response(JSON.stringify(payload), {
+25
View File
@@ -629,6 +629,31 @@ test('configureEarlyAppPaths isolates development runs from the production profi
assert.deepEqual(calls, ['name:SubMiner', 'path:userData:/tmp/xdg/SubMiner-dev']); assert.deepEqual(calls, ['name:SubMiner', 'path:userData:/tmp/xdg/SubMiner-dev']);
}); });
test('configureEarlyAppPaths ignores development flags forwarded to mpv', () => {
for (const forwardedFlag of ['--dev', '--debug']) {
let selectedPath = '';
const userDataPath = configureEarlyAppPaths(
{
setName: () => {},
setPath: (_key, value) => {
selectedPath = value;
},
},
{
platform: 'linux',
homeDir: '/home/tester',
xdgConfigHome: '/tmp/xdg',
existsSync: () => false,
argv: ['electron', '.', '--launch-mpv', forwardedFlag],
env: {},
},
);
assert.equal(userDataPath, '/tmp/xdg/SubMiner');
assert.equal(selectedPath, '/tmp/xdg/SubMiner');
}
});
test('configureEarlyAppPaths uses the supplied environment for config discovery', () => { test('configureEarlyAppPaths uses the supplied environment for config discovery', () => {
const paths: string[] = []; const paths: string[] = [];
+4 -1
View File
@@ -267,8 +267,11 @@ export function configureEarlyAppPaths(app: EarlyAppLike, options?: EarlyAppPath
existsSync: options?.existsSync ?? fs.existsSync, existsSync: options?.existsSync ?? fs.existsSync,
}); });
const argv = options?.argv ?? process.argv; const argv = options?.argv ?? process.argv;
const launchMpvIndex = argv.indexOf('--launch-mpv');
const appArgv = launchMpvIndex === -1 ? argv : argv.slice(0, launchMpvIndex);
const useDevelopmentProfile = const useDevelopmentProfile =
(argv.includes('--dev') || argv.includes('--debug')) && env[USE_PRODUCTION_PROFILE_ENV] !== '1'; (appArgv.includes('--dev') || appArgv.includes('--debug')) &&
env[USE_PRODUCTION_PROFILE_ENV] !== '1';
const platformPath = platform === 'win32' ? path.win32 : path.posix; const platformPath = platform === 'win32' ? path.win32 : path.posix;
const userDataPath = useDevelopmentProfile const userDataPath = useDevelopmentProfile
? platformPath.join(platformPath.dirname(configDir), DEVELOPMENT_APP_NAME) ? platformPath.join(platformPath.dirname(configDir), DEVELOPMENT_APP_NAME)
+8 -3
View File
@@ -22,7 +22,10 @@ import {
shouldHandleStatsDaemonCommandAtEntry, shouldHandleStatsDaemonCommandAtEntry,
spawnDetachedApp, spawnDetachedApp,
} from './main-entry-runtime'; } from './main-entry-runtime';
import { requestSingleInstanceLockEarly } from './main/early-single-instance'; import {
requestSingleInstanceLockEarly,
shouldBypassSingleInstanceLockForArgv,
} from './main/early-single-instance';
import { readConfiguredWindowsMpvLaunch } from './main-entry-launch-config'; import { readConfiguredWindowsMpvLaunch } from './main-entry-launch-config';
import { isAppControlServerAvailable, sendAppControlCommand } from './shared/app-control-client'; import { isAppControlServerAvailable, sendAppControlCommand } from './shared/app-control-client';
import { import {
@@ -193,8 +196,10 @@ registerFatalErrorHandlers({
}); });
function startMainProcess(): void { function startMainProcess(): void {
// This profile-scoped lock serializes the runtime guard's read-check-write sequence. // Normal launches serialize the runtime guard with the profile-scoped lock. Stats daemon
const gotSingleInstanceLock = requestSingleInstanceLockEarly(app); // commands keep their existing lock bypass when Electron runs in Node mode.
const gotSingleInstanceLock =
shouldBypassSingleInstanceLockForArgv(process.argv) || requestSingleInstanceLockEarly(app);
if (!gotSingleInstanceLock) { if (!gotSingleInstanceLock) {
app.exit(0); app.exit(0);
return; return;
+2
View File
@@ -89,6 +89,7 @@ test('runtime guard blocks a profile downgrade before rewriting its safety recor
if (result.ok) return; if (result.ok) return;
assert.equal(result.title, 'Electron downgrade blocked'); assert.equal(result.title, 'Electron downgrade blocked');
assert.match(result.details, /destroy Yomitan dictionaries/); assert.match(result.details, /destroy Yomitan dictionaries/);
assert.equal(result.details.includes(statePath), true);
assert.deepEqual(JSON.parse(fs.readFileSync(statePath, 'utf8')), { assert.deepEqual(JSON.parse(fs.readFileSync(statePath, 'utf8')), {
highestElectronMajor: 44, highestElectronMajor: 44,
lastElectronVersion: '44.1.0', lastElectronVersion: '44.1.0',
@@ -114,6 +115,7 @@ test('runtime guard blocks a downgrade within the supported Electron major', ()
assert.equal(result.ok, false); assert.equal(result.ok, false);
if (result.ok) return; if (result.ok) return;
assert.equal(result.title, 'Electron downgrade blocked'); assert.equal(result.title, 'Electron downgrade blocked');
assert.equal(result.details.includes(statePath), true);
assert.deepEqual(JSON.parse(fs.readFileSync(statePath, 'utf8')), { assert.deepEqual(JSON.parse(fs.readFileSync(statePath, 'utf8')), {
highestElectronMajor: 43, highestElectronMajor: 43,
lastElectronVersion: '43.4.1', lastElectronVersion: '43.4.1',
+1
View File
@@ -137,6 +137,7 @@ export function enforceElectronRuntimeGuard(options: {
details: [ details: [
`This profile was previously opened with Electron ${previousState.state.lastElectronVersion}.`, `This profile was previously opened with Electron ${previousState.state.lastElectronVersion}.`,
`The current runtime is Electron ${options.electronVersion}.`, `The current runtime is Electron ${options.electronVersion}.`,
`Runtime safety record: ${statePath}.`,
'', '',
'Opening Chromium storage with an older Electron version can destroy Yomitan dictionaries. Upgrade SubMiner before using this profile.', 'Opening Chromium storage with an older Electron version can destroy Yomitan dictionaries. Upgrade SubMiner before using this profile.',
].join('\n'), ].join('\n'),
@@ -27,12 +27,15 @@ test('dictionary integrity observation establishes and updates a non-empty basel
safe: true, safe: true,
previousCount: 5, previousCount: 5,
}); });
assert.deepEqual( const statePath = path.join(userDataPath, 'yomitan-dictionary-integrity.json');
JSON.parse( const unchangedTimestamp = new Date('2000-01-01T00:00:00.000Z');
fs.readFileSync(path.join(userDataPath, 'yomitan-dictionary-integrity.json'), 'utf8'), fs.utimesSync(statePath, unchangedTimestamp, unchangedTimestamp);
), assert.deepEqual(observeYomitanDictionaryCount(userDataPath, 3), {
{ lastKnownNonEmptyCount: 3 }, safe: true,
); previousCount: 3,
});
assert.equal(fs.statSync(statePath).mtimeMs, unchangedTimestamp.getTime());
assert.deepEqual(JSON.parse(fs.readFileSync(statePath, 'utf8')), { lastKnownNonEmptyCount: 3 });
assert.deepEqual(fs.readdirSync(userDataPath), ['yomitan-dictionary-integrity.json']); assert.deepEqual(fs.readdirSync(userDataPath), ['yomitan-dictionary-integrity.json']);
}); });
}); });
@@ -114,11 +117,17 @@ test('dictionary integrity blocks automatic mutation after a non-empty profile b
test('dictionary integrity fails closed when its state is malformed', () => { test('dictionary integrity fails closed when its state is malformed', () => {
withTempDir((userDataPath) => { withTempDir((userDataPath) => {
fs.writeFileSync(path.join(userDataPath, 'yomitan-dictionary-integrity.json'), '{}', 'utf8'); const statePath = path.join(userDataPath, 'yomitan-dictionary-integrity.json');
fs.writeFileSync(statePath, '{}', 'utf8');
assert.throws( assert.throws(
() => assertYomitanDictionaryMutationSafe(userDataPath, 2), () => assertYomitanDictionaryMutationSafe(userDataPath, 2),
/could not verify Yomitan dictionary storage/, (error: unknown) => {
assert.ok(error instanceof Error);
assert.match(error.message, /could not verify Yomitan dictionary storage/);
assert.equal(error.message.includes(statePath), true);
return true;
},
); );
}); });
}); });
@@ -70,7 +70,7 @@ export function observeYomitanDictionaryCount(
return { return {
safe: false, safe: false,
previousCount: null, previousCount: null,
message: `SubMiner could not verify Yomitan dictionary storage: ${(error as Error).message}`, message: `SubMiner could not verify Yomitan dictionary storage at ${statePath}: ${(error as Error).message}`,
}; };
} }
@@ -86,7 +86,7 @@ export function observeYomitanDictionaryCount(
}; };
} }
if (normalizedCount > 0) { if (normalizedCount > 0 && normalizedCount !== state?.lastKnownNonEmptyCount) {
try { try {
writeState(statePath, { lastKnownNonEmptyCount: normalizedCount }); writeState(statePath, { lastKnownNonEmptyCount: normalizedCount });
} catch (error) { } catch (error) {