mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-20 12:15:26 -07:00
fix(storage): address CI and review feedback
This commit is contained in:
@@ -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), {
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user