Initial curated chimera source from a8ae5b1db07fb49926c94585e2718e5130330974

This commit is contained in:
Public Source Publisher committed 2026-10-01 01:38:08 +02:00
commit c737a37f71
531 files changed
+119270

No files matched your search

+496
View File
@@ -0,0 +1,496 @@
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const os = require('os');
const path = require('path');
const initSqlJs = require('sql.js');
const database = require('../../src/database');
const EXPECTED_MIGRATION_VERSIONS = Array.from({ length: 20 }, (_, index) => index + 1);
function withTempDatabase(fn, { root = null } = {}) {
const base = root || fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-db-migrations-'));
const logger = {
info: () => {},
warn: () => {},
error: () => {},
};
database.configure({ dataDir: base, logger });
return Promise.resolve()
.then(fn)
.finally(() => {
database.setTestMigrationFailureVersion(null);
database.close();
fs.rmSync(base, { recursive: true, force: true });
});
}
test('migrations create schema_migrations records for a new database', async () => {
await withTempDatabase(async () => {
await database.initialize();
const applied = database.runQuery('SELECT version, status FROM schema_migrations ORDER BY version');
assert.deepEqual(applied.map((row) => row.version), EXPECTED_MIGRATION_VERSIONS);
assert.deepEqual(applied.map((row) => row.status), Array(EXPECTED_MIGRATION_VERSIONS.length).fill('applied'));
assert.equal(database.runQuery("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'compatibility_findings'")[0]?.name, 'compatibility_findings');
assert.equal(database.getGame('missing.game'), undefined);
});
});
test('legacy schema is upgraded with legacy columns and backfills', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-db-migrations-legacy-'));
const dbPath = path.join(root, 'chimera.db');
const SQL = await initSqlJs();
const legacy = new SQL.Database();
legacy.run(`
CREATE TABLE games (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL DEFAULT '',
game_id TEXT UNIQUE NOT NULL,
content_id TEXT DEFAULT '',
size INTEGER DEFAULT 0,
version TEXT DEFAULT '',
installed INTEGER DEFAULT 0,
backed_up INTEGER DEFAULT 0,
ftp_path TEXT DEFAULT '',
last_scanned TEXT DEFAULT NULL,
created_at TEXT DEFAULT (datetime('now'))
)
`);
legacy.run(`
CREATE TABLE backups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
game_id TEXT NOT NULL,
backup_path TEXT NOT NULL UNIQUE
)
`);
legacy.run(`
CREATE TABLE backpork_folders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
path TEXT NOT NULL UNIQUE
)
`);
legacy.run(`
CREATE TABLE game_firmware (
game_id TEXT NOT NULL,
folder_name TEXT NOT NULL,
PRIMARY KEY (game_id, folder_name)
)
`);
legacy.run(`
CREATE TABLE game_hashes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
game_id TEXT NOT NULL,
file_hash TEXT NOT NULL,
file_size INTEGER NOT NULL,
backup_path TEXT NOT NULL UNIQUE,
verified INTEGER DEFAULT 1,
hashed_at TEXT DEFAULT (datetime('now'))
)
`);
legacy.run(`
CREATE TABLE payload_sources (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
github_url TEXT NOT NULL UNIQUE
)
`);
legacy.run(`
CREATE TABLE payload_files (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_id INTEGER NOT NULL,
asset_name TEXT NOT NULL,
asset_url TEXT DEFAULT NULL,
remote_hash TEXT DEFAULT NULL,
version TEXT DEFAULT NULL,
UNIQUE(source_id, asset_name)
)
`);
legacy.run(`
CREATE TABLE cheat_files (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT UNIQUE NOT NULL,
cusa_id TEXT NOT NULL,
version TEXT NOT NULL DEFAULT '',
title TEXT NOT NULL DEFAULT '',
data TEXT NOT NULL DEFAULT '{}',
cached_at TEXT DEFAULT (datetime('now'))
)
`);
legacy.run(`
CREATE TABLE automation_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id TEXT NOT NULL,
trigger TEXT NOT NULL DEFAULT 'manual',
status TEXT NOT NULL DEFAULT 'running',
started_at INTEGER NOT NULL
)
`);
legacy.run(`
INSERT INTO games (game_id, title, content_id, size, version, installed, backed_up, ftp_path, last_scanned)
VALUES ('CUSA12345', 'Legacy Example', 'CUSA12345X', 1234, '1.00', 1, 0, '/tmp/example', 1)
`);
legacy.run(`
INSERT INTO backpork_folders (name, path) VALUES ('USB1', '/mnt/usb1')
`);
legacy.run(`
INSERT INTO backups (game_id, backup_path)
VALUES ('CUSA12345', '/mnt/usb1/games/example')
`);
legacy.run(`
INSERT INTO game_hashes (game_id, file_hash, file_size, backup_path)
VALUES ('CUSA12345', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 100, '/mnt/usb1/games/example/hash.bin')
`);
const buffer = legacy.export();
fs.writeFileSync(dbPath, Buffer.from(buffer));
legacy.close();
await withTempDatabase(async () => {
await database.initialize();
const columns = database.runQuery("PRAGMA table_info(games)").map((row) => row.name);
assert.ok(columns.includes('prospero_name'));
assert.ok(columns.includes('metadata_failure_count'));
const applied = database.runQuery('SELECT version, status FROM schema_migrations ORDER BY version');
assert.deepEqual(applied.map((row) => row.version), EXPECTED_MIGRATION_VERSIONS);
assert.deepEqual(applied.map((row) => row.status), Array(EXPECTED_MIGRATION_VERSIONS.length).fill('applied'));
assert.equal(database.getGame('CUSA12345').installed, 1);
}, { root });
});
test('rerunning initialize is idempotent and keeps migration rows stable', async () => {
await withTempDatabase(async () => {
await database.initialize();
const first = database.runQuery('SELECT version, status FROM schema_migrations ORDER BY version');
assert.equal(first.length, EXPECTED_MIGRATION_VERSIONS.length);
await database.initialize();
const second = database.runQuery('SELECT version, status FROM schema_migrations ORDER BY version');
assert.deepEqual(second.map((row) => row.version), first.map((row) => row.version));
assert.deepEqual(second.map((row) => row.status), first.map((row) => row.status));
});
});
test('forced migration failure restores from pre-migration snapshot', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-fail-migration-'));
await withTempDatabase(async () => {
const dbPath = path.join(root, 'chimera.db');
const SQL = await initSqlJs();
const db = new SQL.Database();
db.run(`CREATE TABLE games (id INTEGER PRIMARY KEY AUTOINCREMENT, game_id TEXT UNIQUE NOT NULL, title TEXT NOT NULL DEFAULT '', content_id TEXT DEFAULT '', size INTEGER DEFAULT 0, version TEXT DEFAULT '', installed INTEGER DEFAULT 0, backed_up INTEGER DEFAULT 0, ftp_path TEXT DEFAULT '', last_scanned TEXT DEFAULT NULL, created_at TEXT DEFAULT (datetime('now')))`);
db.run(`INSERT INTO games (game_id, title, content_id, size, version) VALUES ('CUSA99999', 'Snapshot', 'CUSA99999', 5, '1.0')`);
const seed = db.export();
fs.writeFileSync(dbPath, Buffer.from(seed));
db.close();
database.configure({ dataDir: root, logger: { info: () => {}, warn: () => {}, error: () => {} } });
database.setTestMigrationFailureVersion(2);
await assert.rejects(() => database.initialize(), /Test-gedwongen migratiefout voor versie 2/);
const rowsAfterFailure = database.runQuery('SELECT version, status, recovery_instructions FROM schema_migrations ORDER BY version');
assert.equal(rowsAfterFailure.length, 2);
const failedRow = rowsAfterFailure.find((row) => row.version === 2);
assert.equal(failedRow.status, 'failed');
assert.ok(typeof failedRow.recovery_instructions === 'string');
assert.ok(database.getGame('CUSA99999'));
database.setTestMigrationFailureVersion(null);
await database.initialize();
const rowsAfterRetry = database.runQuery('SELECT version, status FROM schema_migrations ORDER BY version');
assert.equal(rowsAfterRetry[1].status, 'applied');
assert.equal(rowsAfterRetry[1].version, 2);
assert.equal(rowsAfterRetry[2].status, 'applied');
assert.equal(rowsAfterRetry[2].version, 3);
assert.equal(rowsAfterRetry[3].status, 'applied');
assert.equal(rowsAfterRetry[3].version, 4);
assert.equal(database.getGame('CUSA99999').title, 'Snapshot');
}, { root });
});
test('v3 schema shape includes title, copy, install, device, volume, and scan tables', async () => {
await withTempDatabase(async () => {
await database.initialize();
const required = [
'titles',
'title_aliases',
'storage_devices',
'storage_volumes',
'scan_runs',
'scan_root_coverage',
'local_copies',
'console_installs',
'version_evidence',
];
const existing = database.runQuery(`
SELECT name FROM sqlite_master
WHERE type = 'table'
AND name IN (${required.map((table) => `'${table}'`).join(',')})
`).map((row) => row.name);
assert.deepEqual(existing.sort(), required.sort());
});
});
test('v3 schema constraints use foreign keys and location/version indexes', async () => {
await withTempDatabase(async () => {
await database.initialize();
const SQL = await initSqlJs();
const migrated = new SQL.Database(database.exportDatabaseBuffer());
const fkChecks = {
local_copies: ['titles', 'storage_volumes', 'scan_runs'],
console_installs: ['titles', 'storage_volumes', 'scan_runs', 'version_evidence'],
storage_volumes: ['storage_devices'],
title_aliases: ['titles'],
scan_root_coverage: ['scan_runs', 'storage_volumes'],
};
for (const [table, expected] of Object.entries(fkChecks)) {
const rows = migrated.exec(`PRAGMA foreign_key_list('${table}')`);
const refs = new Set(rows[0]?.values?.map((row) => row[2]) ?? []);
for (const ref of expected) {
assert.ok(refs.has(ref), `Expected ${table} to reference ${ref}`);
}
}
const indexes = migrated.exec("SELECT name FROM sqlite_master WHERE type='index' AND name LIKE 'idx_local_copies_%' OR name LIKE 'idx_console_installs_%'");
const indexNames = new Set(indexes[0]?.values?.map((row) => row[0]) ?? []);
assert.ok(indexNames.has('idx_local_copies_title_last_seen'));
assert.ok(indexNames.has('idx_console_installs_title_profile'));
});
});
test('multi-location inserts are supported for local copies and console installs', async () => {
await withTempDatabase(async () => {
await database.initialize();
const SQL = await initSqlJs();
const migrated = new SQL.Database(database.exportDatabaseBuffer());
migrated.run(`
INSERT INTO titles (platform, title_id, name, sort_name, region)
VALUES ('ps5', 'CUSA12345', 'Legacy Example', 'Legacy Example', 'US')
`);
const titleId = migrated.exec("SELECT last_insert_rowid() AS id")[0]?.values?.[0]?.[0];
assert.ok(typeof titleId === 'number');
migrated.run(`
INSERT INTO storage_devices (scope, stable_fingerprint, display_name, device_type)
VALUES ('server', 'srv-volume-internal', 'Server Internal', 'internal'),
('server', 'srv-volume-external', 'Server USB', 'usb')
`);
const deviceRows = migrated.exec('SELECT id, stable_fingerprint FROM storage_devices ORDER BY stable_fingerprint')[0]?.values || [];
assert.equal(deviceRows.length, 2);
const internalId = deviceRows[0][0];
const usbId = deviceRows[1][0];
migrated.run(`
INSERT INTO storage_volumes (device_id, stable_volume_key, mount_path, can_copy, can_play, can_install)
VALUES (${internalId}, 'int-vol-1', '/games/ps5/internal', 1, 1, 1),
(${usbId}, 'ext-vol-1', '/games/ps5/usb', 1, 1, 1)
`);
const volumes = migrated.exec('SELECT id, stable_volume_key FROM storage_volumes ORDER BY stable_volume_key')[0]?.values || [];
assert.equal(volumes.length, 2);
const internalVolumeId = volumes[0][0];
const externalVolumeId = volumes[1][0];
migrated.run(`
INSERT INTO local_copies (title_id, volume_id, relative_path, copy_type, base_version, scan_run_id)
VALUES (${titleId}, ${internalVolumeId}, 'game-folder', 'folder', '1.00', NULL),
(${titleId}, ${externalVolumeId}, 'game-archive/game.pkg', 'archive', '1.00', NULL)
`);
migrated.run(`
INSERT INTO console_installs (title_id, console_profile_id, volume_id, remote_location_key, install_state, installed_version)
VALUES (${titleId}, 'console-A', ${internalVolumeId}, 'ux0:/app0/CUSA12345', 'observed', '1.00'),
(${titleId}, 'console-A', ${externalVolumeId}, 'ext0:/app0/CUSA12345', 'observed', '1.01')
`);
const copiesCount = migrated.exec(`SELECT COUNT(*) FROM local_copies WHERE title_id = ${titleId}`)[0]?.values?.[0]?.[0];
const installsCount = migrated.exec(`SELECT COUNT(*) FROM console_installs WHERE title_id = ${titleId}`)[0]?.values?.[0]?.[0];
assert.equal(copiesCount, 2);
assert.equal(installsCount, 2);
});
});
test('device disconnect state is persisted as unavailable', async () => {
await withTempDatabase(async () => {
await database.initialize();
const SQL = await initSqlJs();
const migrated = new SQL.Database(database.exportDatabaseBuffer());
migrated.run(`
INSERT INTO storage_devices (scope, stable_fingerprint, display_name, device_type)
VALUES ('server', 'device-disconnect', 'USB Drive', 'usb')
`);
migrated.run(`UPDATE storage_devices SET availability_state = 'unavailable' WHERE stable_fingerprint = 'device-disconnect'`);
const rows = migrated.exec(`
SELECT availability_state
FROM storage_devices
WHERE stable_fingerprint = 'device-disconnect'
`);
assert.equal(rows[0].values[0][0], 'unavailable');
});
});
test('legacy games and backups migrate into v3 titles, copies and install records', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-legacy-v3-migrate-'));
const dbPath = path.join(root, 'chimera.db');
const SQL = await initSqlJs();
const legacy = new SQL.Database();
legacy.run(`
CREATE TABLE games (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL DEFAULT '',
game_id TEXT UNIQUE NOT NULL,
content_id TEXT DEFAULT '',
size INTEGER DEFAULT 0,
version TEXT DEFAULT '',
installed INTEGER DEFAULT 0,
backed_up INTEGER DEFAULT 0,
ftp_path TEXT DEFAULT '',
last_scanned TEXT DEFAULT NULL,
created_at TEXT DEFAULT (datetime('now'))
)
`);
legacy.run(`
CREATE TABLE backups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
game_id TEXT NOT NULL,
backup_path TEXT NOT NULL UNIQUE
)
`);
legacy.run(`
INSERT INTO games (game_id, title, content_id, size, version, installed, ftp_path)
VALUES ('CUSA12345', 'Migrated Example', 'CUSA12345X', 1234, '1.00', 1, '/srv/internal/PS5/CUSA12345')
`);
legacy.run(`
INSERT INTO backups (game_id, backup_path)
VALUES ('CUSA12345', '/srv/internal/PS5/CUSA12345/folder')
`);
const buffer = legacy.export();
fs.writeFileSync(dbPath, Buffer.from(buffer));
legacy.close();
await withTempDatabase(async () => {
await database.initialize();
const titleRows = database.runQuery("SELECT * FROM titles WHERE title_id = 'CUSA12345'");
assert.equal(titleRows.length, 1);
const copies = database.runQuery("SELECT COUNT(*) AS c FROM local_copies");
const installs = database.runQuery("SELECT COUNT(*) AS c FROM console_installs");
const report = database.runQuery(`SELECT report_json FROM migration_reports WHERE migration_name = 'legacy-to-v3'`)[0];
assert.ok(report);
const payload = JSON.parse(report.report_json || '{}');
assert.equal(copies[0].c, 1);
assert.equal(installs[0].c, 1);
assert.equal(payload.legacyGames, 1);
assert.equal(payload.legacyBackups, 1);
assert.equal(payload.ambiguousVersions.length, 0);
const installState = database.runQuery(`
SELECT install_state
FROM console_installs ci
JOIN titles t ON t.id = ci.title_id
WHERE t.title_id = 'CUSA12345'
`);
assert.equal(installState[0].install_state, 'legacy-unverified');
}, { root });
});
test('legacy migration reports ambiguous version evidence without asserting certainty', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-legacy-v3-ambig-'));
const dbPath = path.join(root, 'chimera.db');
const SQL = await initSqlJs();
const legacy = new SQL.Database();
legacy.run(`
CREATE TABLE games (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL DEFAULT '',
game_id TEXT NOT NULL,
content_id TEXT DEFAULT '',
size INTEGER DEFAULT 0,
version TEXT DEFAULT '',
installed INTEGER DEFAULT 0,
backed_up INTEGER DEFAULT 0,
ftp_path TEXT DEFAULT '',
last_scanned TEXT DEFAULT NULL,
created_at TEXT DEFAULT (datetime('now'))
)
`);
legacy.run(`CREATE TABLE backups (id INTEGER PRIMARY KEY AUTOINCREMENT, game_id TEXT NOT NULL, backup_path TEXT NOT NULL UNIQUE)`);
legacy.run(`
INSERT INTO games (game_id, title, content_id, size, version, installed)
VALUES ('CUSA99999', 'Ambiguous Game', 'CUSA99999', 1, '1.00', 0),
('CUSA99999', 'Ambiguous Game', 'CUSA99999', 1, '1.01', 0)
`);
const buffer = legacy.export();
fs.writeFileSync(dbPath, Buffer.from(buffer));
legacy.close();
await withTempDatabase(async () => {
await database.initialize();
const report = database.runQuery(`SELECT report_json FROM migration_reports WHERE migration_name = 'legacy-to-v3'`)[0];
assert.ok(report);
const payload = JSON.parse(report.report_json || '{}');
assert.equal(payload.ambiguousVersions.length, 1);
assert.equal(payload.ambiguousVersions[0].game_id, 'CUSA99999');
const titles = database.runQuery("SELECT COUNT(*) AS c FROM titles WHERE title_id = 'CUSA99999'");
assert.equal(titles[0].c, 1);
}, { root });
});
test('legacy migration reports row counts and supports rollback', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-legacy-v3-report-'));
const dbPath = path.join(root, 'chimera.db');
const SQL = await initSqlJs();
const legacy = new SQL.Database();
legacy.run(`
CREATE TABLE games (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL DEFAULT '',
game_id TEXT UNIQUE NOT NULL,
content_id TEXT DEFAULT '',
size INTEGER DEFAULT 0,
version TEXT DEFAULT '',
installed INTEGER DEFAULT 0,
backed_up INTEGER DEFAULT 0,
ftp_path TEXT DEFAULT '',
last_scanned TEXT DEFAULT NULL,
created_at TEXT DEFAULT (datetime('now'))
)
`);
legacy.run(`CREATE TABLE backups (id INTEGER PRIMARY KEY AUTOINCREMENT, game_id TEXT NOT NULL, backup_path TEXT NOT NULL UNIQUE)`);
legacy.run(`INSERT INTO games (game_id, title, content_id, version, installed) VALUES ('CUSA12345', 'Report Game', 'CUSA12345', '1.0', 1)`);
const buffer = legacy.export();
fs.writeFileSync(dbPath, Buffer.from(buffer));
legacy.close();
await withTempDatabase(async () => {
database.configure({ dataDir: root, logger: { info: () => {}, warn: () => {}, error: () => {} } });
await database.initialize();
const report = database.runQuery(`SELECT report_json FROM migration_reports WHERE migration_name = 'legacy-to-v3'`)[0];
assert.ok(report);
const payload = JSON.parse(report.report_json || '{}');
assert.equal(payload.migration, '0004-migrate-legacy-games-to-v3');
assert.equal(payload.legacyGames, 1);
database.setTestMigrationFailureVersion(4);
await assert.rejects(() => database.initialize(), /Test-gedwongen migratiefout voor versie 4/);
const rowsAfterFailure = database.runQuery('SELECT version, status FROM schema_migrations ORDER BY version');
assert.equal(rowsAfterFailure.length, 4);
assert.equal(rowsAfterFailure[3].status, 'failed');
assert.ok(database.getGame('CUSA12345'));
database.setTestMigrationFailureVersion(null);
await database.initialize();
const rowsAfterRetry = database.runQuery('SELECT version, status FROM schema_migrations ORDER BY version');
assert.equal(rowsAfterRetry[3].status, 'applied');
assert.equal(rowsAfterRetry[3].version, 4);
assert.equal(database.getGame('CUSA12345').title, 'Report Game');
}, { root });
});