mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-20 00:15:27 -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 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), {
|
||||||
|
|||||||
@@ -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[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user