263 lines
8.4 KiB
JavaScript
263 lines
8.4 KiB
JavaScript
'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 database = require('../../src/database');
|
|
const {
|
|
createV3Repositories,
|
|
sanitizePublicLocalCopy,
|
|
sanitizePublicConsoleInstall,
|
|
} = require('../../src/services/v3-repositories');
|
|
|
|
function withTempDatabase(fn) {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-v3-repositories-'));
|
|
database.configure({
|
|
dataDir: root,
|
|
logger: { info() {}, warn() {}, error() {},
|
|
},
|
|
});
|
|
return database.initialize()
|
|
.then(() => fn())
|
|
.finally(() => {
|
|
database.close();
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
});
|
|
}
|
|
|
|
function seedCoreRows() {
|
|
database.runMutatingSqlWithParams(`
|
|
INSERT INTO titles (platform, title_id, content_id, name, sort_name, region, metadata_source)
|
|
VALUES ('ps5', 'PPSA00001', 'content-01', 'Repo Test', 'Repo Test', 'US', 'tests')
|
|
`);
|
|
const title = Number(database.runQuery('SELECT last_insert_rowid() AS id')[0]?.id || 0);
|
|
|
|
database.runMutatingSqlWithParams(`
|
|
INSERT INTO storage_devices (scope, stable_fingerprint, display_name, device_type, availability_state)
|
|
VALUES ('server', 'test-device', 'Test Device', 'server', 'available')
|
|
`);
|
|
const device = Number(database.runQuery('SELECT last_insert_rowid() AS id')[0]?.id || 0);
|
|
|
|
database.runMutatingSqlWithParams(`
|
|
INSERT INTO storage_volumes (device_id, stable_volume_key, mount_path, can_copy, can_play, can_install)
|
|
VALUES (@deviceId, 'test-volume', '/test-volume', 1, 1, 1)
|
|
`, { deviceId: device });
|
|
const volume = Number(database.runQuery('SELECT last_insert_rowid() AS id')[0]?.id || 0);
|
|
|
|
return { title, volume };
|
|
}
|
|
|
|
function rowCount(table) {
|
|
return Number(database.runQuery(`SELECT COUNT(*) AS c FROM ${table}`)[0]?.c || 0);
|
|
}
|
|
|
|
test('local-copy repository writes only local copies and returns sanitized public model', () => {
|
|
return withTempDatabase(() => {
|
|
const { title, volume } = seedCoreRows();
|
|
const { localCopies } = createV3Repositories(database);
|
|
|
|
const result = localCopies.upsertLocalCopy({
|
|
titleId: title,
|
|
volumeId: volume,
|
|
relativePath: 'games/TestFolder',
|
|
copyType: 'folder',
|
|
baseVersion: '1.00',
|
|
versionSource: 'legacy',
|
|
generation: 10,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(result.reason, 'inserted');
|
|
assert.equal(sanitizePublicLocalCopy(result.copy).copyType, 'folder');
|
|
assert.equal(rowCount('console_installs'), 0);
|
|
assert.equal(rowCount('local_copies'), 1);
|
|
});
|
|
});
|
|
|
|
test('console-install repository writes only console installs and returns sanitized public model', () => {
|
|
return withTempDatabase(() => {
|
|
const { title, volume } = seedCoreRows();
|
|
const { consoleInstalls } = createV3Repositories(database);
|
|
|
|
const result = consoleInstalls.upsertConsoleInstall({
|
|
titleId: title,
|
|
volumeId: volume,
|
|
consoleProfileId: 'profile-A',
|
|
remoteLocationKey: 'ux0:/app0/PPSA00001',
|
|
installedVersion: '1.00',
|
|
installState: 'observed',
|
|
versionSource: 'scan',
|
|
evidenceConfidence: 'medium',
|
|
generation: 5,
|
|
});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(result.reason, 'inserted');
|
|
assert.equal(sanitizePublicConsoleInstall(result.install).installState, 'observed');
|
|
assert.equal(rowCount('local_copies'), 0);
|
|
assert.equal(rowCount('console_installs'), 1);
|
|
});
|
|
});
|
|
|
|
test('generation-aware upserts prevent stale local copy evidence overwrite', () => {
|
|
return withTempDatabase(() => {
|
|
const { title, volume } = seedCoreRows();
|
|
const { localCopies } = createV3Repositories(database);
|
|
|
|
localCopies.upsertLocalCopy({
|
|
titleId: title,
|
|
volumeId: volume,
|
|
relativePath: 'games/TestFolder',
|
|
copyType: 'folder',
|
|
baseVersion: '1.00',
|
|
generation: 10,
|
|
});
|
|
|
|
const stale = localCopies.upsertLocalCopy({
|
|
titleId: title,
|
|
volumeId: volume,
|
|
relativePath: 'games/TestFolder',
|
|
copyType: 'folder',
|
|
baseVersion: '2.00',
|
|
generation: 9,
|
|
});
|
|
|
|
assert.equal(stale.ok, false);
|
|
assert.equal(stale.reason, 'outdated-evidence');
|
|
const current = localCopies.upsertLocalCopy({
|
|
titleId: title,
|
|
volumeId: volume,
|
|
relativePath: 'games/TestFolder',
|
|
copyType: 'folder',
|
|
baseVersion: '2.00',
|
|
generation: 20,
|
|
});
|
|
assert.equal(current.ok, true);
|
|
assert.equal(current.reason, 'updated');
|
|
assert.equal(current.copy.baseVersion, '2.00');
|
|
});
|
|
});
|
|
|
|
test('generation-aware upserts prevent stale console install overwrite', () => {
|
|
return withTempDatabase(() => {
|
|
const { title, volume } = seedCoreRows();
|
|
const { consoleInstalls } = createV3Repositories(database);
|
|
|
|
consoleInstalls.upsertConsoleInstall({
|
|
titleId: title,
|
|
volumeId: volume,
|
|
consoleProfileId: 'profile-A',
|
|
remoteLocationKey: 'ux0:/app0/PPSA00001',
|
|
installedVersion: '1.00',
|
|
installState: 'observed',
|
|
generation: 10,
|
|
});
|
|
|
|
const stale = consoleInstalls.upsertConsoleInstall({
|
|
titleId: title,
|
|
volumeId: volume,
|
|
consoleProfileId: 'profile-A',
|
|
remoteLocationKey: 'ux0:/app0/PPSA00001',
|
|
installedVersion: '0.99',
|
|
installState: 'absent',
|
|
generation: 9,
|
|
});
|
|
|
|
assert.equal(stale.ok, false);
|
|
assert.equal(stale.reason, 'outdated-evidence');
|
|
|
|
const fresh = consoleInstalls.upsertConsoleInstall({
|
|
titleId: title,
|
|
volumeId: volume,
|
|
consoleProfileId: 'profile-A',
|
|
remoteLocationKey: 'ux0:/app0/PPSA00001',
|
|
installedVersion: '1.01',
|
|
installState: 'observed',
|
|
generation: 20,
|
|
});
|
|
assert.equal(fresh.ok, true);
|
|
assert.equal(fresh.reason, 'updated');
|
|
assert.equal(fresh.install.installedVersion, '1.01');
|
|
});
|
|
});
|
|
|
|
test('evidence writes are persisted with source/time/confidence metadata', () => {
|
|
return withTempDatabase(() => {
|
|
const { title, volume } = seedCoreRows();
|
|
const { consoleInstalls } = createV3Repositories(database);
|
|
const generation = Date.now();
|
|
|
|
const result = consoleInstalls.upsertConsoleInstall({
|
|
titleId: title,
|
|
volumeId: volume,
|
|
consoleProfileId: 'profile-A',
|
|
remoteLocationKey: 'ux0:/app0/PPSA00001',
|
|
installedVersion: '1.00',
|
|
installState: 'observed',
|
|
versionSource: 'legacy-console',
|
|
evidenceConfidence: 'high',
|
|
generation,
|
|
});
|
|
|
|
const install = database.runQuery(`
|
|
SELECT version_evidence_id, installed_version
|
|
FROM console_installs
|
|
WHERE id = ${result.install.id}
|
|
`)[0];
|
|
assert.equal(install.installed_version, '1.00');
|
|
assert.ok(install.version_evidence_id > 0);
|
|
|
|
const evidence = database.runQuery(`
|
|
SELECT subject_type, subject_id, semantic_version, version_source, confidence, observed_at
|
|
FROM version_evidence WHERE id = ${install.version_evidence_id}
|
|
`)[0];
|
|
assert.equal(evidence.subject_type, 'console_install');
|
|
assert.equal(evidence.subject_id, title);
|
|
assert.equal(evidence.semantic_version, '1.00');
|
|
assert.equal(evidence.version_source, 'legacy-console');
|
|
assert.equal(evidence.confidence, 'high');
|
|
assert.equal(Number(evidence.observed_at), generation);
|
|
});
|
|
});
|
|
|
|
test('public models are sanitized for invalid external fields', () => {
|
|
const sanitizedCopy = sanitizePublicLocalCopy({
|
|
id: 'bad-id',
|
|
title_id: '42',
|
|
volume_id: '100',
|
|
relative_path: ' games/Test ',
|
|
copy_type: 'not-a-type',
|
|
base_version: 1.23,
|
|
version_source: null,
|
|
size_bytes: 'abc',
|
|
scan_run_id: null,
|
|
last_seen_at: 'bad-ts',
|
|
availability_state: 'nonsense',
|
|
});
|
|
|
|
const sanitizedInstall = sanitizePublicConsoleInstall({
|
|
id: 'bad-id',
|
|
title_id: '42',
|
|
console_profile_id: ' profile-legacy',
|
|
volume_id: 3,
|
|
remote_location_key: 'ux0:/app0',
|
|
installed_version: 4,
|
|
install_state: 'invalid-state',
|
|
first_seen_at: 'bad',
|
|
last_seen_at: 'bad',
|
|
});
|
|
|
|
assert.equal(sanitizedCopy.copyType, 'unknown');
|
|
assert.equal(sanitizedCopy.availabilityState, 'unknown');
|
|
assert.equal(sanitizedCopy.relativePath, 'games/Test');
|
|
assert.equal(typeof sanitizedCopy.sizeBytes, 'number');
|
|
assert.equal(sanitizedCopy.availabilityState, 'unknown');
|
|
|
|
assert.equal(sanitizedInstall.installState, 'unknown');
|
|
assert.equal(typeof sanitizedInstall.firstSeenAt, 'number');
|
|
assert.equal(typeof sanitizedInstall.lastSeenAt, 'number');
|
|
});
|