fix(library): keep the resolved env fallback on failure statuses

The backend has no built-in library default — _get_dlc_dir() returns None
when DLC_DIR is unset and config.json is unusable — so deleting DLC_DIR on
invalid-config/write-failed stranded the user with an empty library and
only a console warn (corrupt config.json, or a saved dlc_dir pointing at
an unplugged drive, previously still produced a working library via the
env fallback).

invalid-config and write-failed now export the resolved fallback as
DLC_DIR, restoring pre-#117 behaviour exactly and only where config.json
cannot own the path. Config-owned dynamic refresh is unchanged for the
configured/bootstrapped paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
OmikronApex 2026-07-17 23:16:55 +02:00
parent aa388b87d5
commit 6eeec8e2a6
2 changed files with 54 additions and 0 deletions

View File

@ -56,6 +56,12 @@ export function applyLibraryPathToPythonEnvironment(
* Normal desktop launches instead keep the selected path in config.json so
* the backend can re-read a Settings change on the next manual scan without
* requiring a process restart.
*
* Failure statuses (invalid-config, write-failed) still export the resolved
* fallback as DLC_DIR: the backend has NO built-in default (_get_dlc_dir
* returns None), so removing the env var when config.json cannot own the
* path would strand the user with an empty library and only a console warn.
* Config-owned dynamic behaviour applies exactly when config.json is usable.
*/
export function prepareLibraryPathForPython(
configDir: string,
@ -79,6 +85,7 @@ export function prepareLibraryPathForPython(
if (!isPlainObject(parsed)) {
return {
status: 'invalid-config',
environmentDlcDir: resolvedDlcDir,
error: 'config.json is not a JSON object',
};
}
@ -86,6 +93,7 @@ export function prepareLibraryPathForPython(
} catch (err) {
return {
status: 'invalid-config',
environmentDlcDir: resolvedDlcDir,
error: err instanceof Error ? err.message : String(err),
};
}
@ -97,12 +105,14 @@ export function prepareLibraryPathForPython(
}
return {
status: 'invalid-config',
environmentDlcDir: resolvedDlcDir,
error: 'config.json dlc_dir is not an existing directory',
};
}
if (configured !== undefined && configured !== null && configured !== '') {
return {
status: 'invalid-config',
environmentDlcDir: resolvedDlcDir,
error: 'config.json dlc_dir is not a string',
};
}
@ -120,6 +130,7 @@ export function prepareLibraryPathForPython(
} catch { /* best-effort temporary-file cleanup */ }
return {
status: 'write-failed',
environmentDlcDir: resolvedDlcDir,
error: err instanceof Error ? err.message : String(err),
};
}

View File

@ -134,6 +134,49 @@ test('a corrupt config is never overwritten during bootstrap', () => {
assert.equal(fs.readFileSync(configFile, 'utf8'), '{broken');
});
test('invalid-config exports the resolved fallback so the backend keeps a library', () => {
// The backend has no built-in default (_get_dlc_dir returns None): with
// the env var deleted AND config.json unusable, scans and starter
// seeding would silently skip. Failure statuses must keep the pre-#117
// env fallback.
const configDir = tmpConfigDir();
const configFile = path.join(configDir, 'config.json');
fs.writeFileSync(configFile, JSON.stringify({ dlc_dir: path.join(configDir, 'gone') }));
const result = prepareLibraryPathForPython(configDir, 'C:\\Default Songs');
assert.equal(result.status, 'invalid-config');
assert.equal(result.environmentDlcDir, 'C:\\Default Songs');
const environment = {};
applyLibraryPathToPythonEnvironment(environment, result);
assert.deepEqual(environment, { DLC_DIR: 'C:\\Default Songs' });
});
test('corrupt config exports the resolved fallback without being overwritten', () => {
const configDir = tmpConfigDir();
const configFile = path.join(configDir, 'config.json');
fs.writeFileSync(configFile, '{broken');
const result = prepareLibraryPathForPython(configDir, 'C:\\Default Songs');
assert.equal(result.status, 'invalid-config');
assert.equal(result.environmentDlcDir, 'C:\\Default Songs');
assert.equal(fs.readFileSync(configFile, 'utf8'), '{broken');
});
test('write-failed exports the resolved fallback so the backend keeps a library', () => {
// Force the config write to fail by making config.json's parent a file.
const root = tmpConfigDir();
const configDir = path.join(root, 'not-a-dir');
fs.writeFileSync(configDir, 'x');
const result = prepareLibraryPathForPython(configDir, 'C:\\Default Songs');
assert.equal(result.status, 'write-failed');
assert.equal(result.environmentDlcDir, 'C:\\Default Songs');
});
test('Python environment omits DLC_DIR when config owns the library path', () => {
const environment = { DLC_DIR: 'C:\\Stale Parent Value', KEEP: 'yes' };