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 db = new Database(dbPath);
ensureSchema(db);
@@ -103,9 +103,15 @@ test('fetchIfMissing backfills a missing blob from an existing cover URL', async
db.close();
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 db = new Database(dbPath);
ensureSchema(db);
@@ -179,7 +185,13 @@ test('fetchIfMissing reuses cached cover art from another video in the same anim
db.close();
cleanupDbPath(dbPath);
}
});
}
test(
'fetchIfMissing reuses cached cover art from another video in the same anime',
{ timeout: 15_000 },
reuseCachedAnimeCoverArt,
);
function createJsonResponse(payload: unknown): Response {
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']);
});
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', () => {
const paths: string[] = [];
+4 -1
View File
@@ -267,8 +267,11 @@ export function configureEarlyAppPaths(app: EarlyAppLike, options?: EarlyAppPath
existsSync: options?.existsSync ?? fs.existsSync,
});
const argv = options?.argv ?? process.argv;
const launchMpvIndex = argv.indexOf('--launch-mpv');
const appArgv = launchMpvIndex === -1 ? argv : argv.slice(0, launchMpvIndex);
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 userDataPath = useDevelopmentProfile
? platformPath.join(platformPath.dirname(configDir), DEVELOPMENT_APP_NAME)
+8 -3
View File
@@ -22,7 +22,10 @@ import {
shouldHandleStatsDaemonCommandAtEntry,
spawnDetachedApp,
} 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 { isAppControlServerAvailable, sendAppControlCommand } from './shared/app-control-client';
import {
@@ -193,8 +196,10 @@ registerFatalErrorHandlers({
});
function startMainProcess(): void {
// This profile-scoped lock serializes the runtime guard's read-check-write sequence.
const gotSingleInstanceLock = requestSingleInstanceLockEarly(app);
// Normal launches serialize the runtime guard with the profile-scoped lock. Stats daemon
// commands keep their existing lock bypass when Electron runs in Node mode.
const gotSingleInstanceLock =
shouldBypassSingleInstanceLockForArgv(process.argv) || requestSingleInstanceLockEarly(app);
if (!gotSingleInstanceLock) {
app.exit(0);
return;
+2
View File
@@ -89,6 +89,7 @@ test('runtime guard blocks a profile downgrade before rewriting its safety recor
if (result.ok) return;
assert.equal(result.title, 'Electron downgrade blocked');
assert.match(result.details, /destroy Yomitan dictionaries/);
assert.equal(result.details.includes(statePath), true);
assert.deepEqual(JSON.parse(fs.readFileSync(statePath, 'utf8')), {
highestElectronMajor: 44,
lastElectronVersion: '44.1.0',
@@ -114,6 +115,7 @@ test('runtime guard blocks a downgrade within the supported Electron major', ()
assert.equal(result.ok, false);
if (result.ok) return;
assert.equal(result.title, 'Electron downgrade blocked');
assert.equal(result.details.includes(statePath), true);
assert.deepEqual(JSON.parse(fs.readFileSync(statePath, 'utf8')), {
highestElectronMajor: 43,
lastElectronVersion: '43.4.1',
+1
View File
@@ -137,6 +137,7 @@ export function enforceElectronRuntimeGuard(options: {
details: [
`This profile was previously opened with Electron ${previousState.state.lastElectronVersion}.`,
`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.',
].join('\n'),
@@ -27,12 +27,15 @@ test('dictionary integrity observation establishes and updates a non-empty basel
safe: true,
previousCount: 5,
});
assert.deepEqual(
JSON.parse(
fs.readFileSync(path.join(userDataPath, 'yomitan-dictionary-integrity.json'), 'utf8'),
),
{ lastKnownNonEmptyCount: 3 },
);
const statePath = path.join(userDataPath, 'yomitan-dictionary-integrity.json');
const unchangedTimestamp = new Date('2000-01-01T00:00:00.000Z');
fs.utimesSync(statePath, unchangedTimestamp, unchangedTimestamp);
assert.deepEqual(observeYomitanDictionaryCount(userDataPath, 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']);
});
});
@@ -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', () => {
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(
() => 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 {
safe: false,
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 {
writeState(statePath, { lastKnownNonEmptyCount: normalizedCount });
} catch (error) {