257 lines
13 KiB
JavaScript
257 lines
13 KiB
JavaScript
'use strict';
|
|
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const crypto = require('crypto');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const { registerFunctionalModules } = require('../../src/server/register-functional-modules');
|
|
const github = require('../../src/github');
|
|
|
|
test('autoloader snapshots can be taken, exported, imported and restored', async () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-snapshots-'));
|
|
const paths = {
|
|
root,
|
|
backups: path.join(root, 'backups'),
|
|
cache: path.join(root, 'cache'),
|
|
tmp: path.join(root, 'tmp'),
|
|
};
|
|
for (const dir of Object.values(paths)) fs.mkdirSync(dir, { recursive: true });
|
|
const remote = new Map([
|
|
['/data/ps5_autoloader/etaHEN.elf', Buffer.from('payload')],
|
|
['/data/ps5_autoloader/autoload.txt', Buffer.from('etaHEN.elf\n')],
|
|
]);
|
|
const ftp = {
|
|
isConnected: () => true,
|
|
hasCredentials: () => true,
|
|
listDir: async () => [{ name: 'etaHEN.elf', isFile: true, isDir: false, size: 7 }, { name: 'autoload.txt', isFile: true, isDir: false, size: 11 }],
|
|
downloadFile: async (remotePath, localPath) => {
|
|
if (!remote.has(remotePath)) throw new Error('missing remote file');
|
|
fs.writeFileSync(localPath, remote.get(remotePath));
|
|
},
|
|
uploadFile: async (localPath, remotePath) => { remote.set(remotePath, fs.readFileSync(localPath)); },
|
|
};
|
|
const definitions = new Map();
|
|
registerFunctionalModules({
|
|
register: definition => definitions.set(definition.name, definition),
|
|
db: {}, ftp,
|
|
settings: { get: (key, fallback) => key === 'ftp.host' ? '192.168.1.105' : fallback },
|
|
secrets: { get: () => '', has: () => false },
|
|
paths,
|
|
logger: { warn() {}, info() {}, error() {} },
|
|
broadcast() {},
|
|
});
|
|
|
|
assert.equal(definitions.get('payload-manager:autoload-repair-plan').classification, 'sensitive');
|
|
assert.equal(definitions.get('payload-manager:autoload-repair-apply').classification, 'destructive');
|
|
assert.equal(definitions.get('payload-manager:autoload-repair-apply').confirmation, 'PERSIST PS5 PAYLOAD MANAGER 0.5.1');
|
|
assert.equal(definitions.get('payload-manager:autoload-repair-rollback-plan').classification, 'sensitive');
|
|
assert.equal(definitions.get('payload-manager:autoload-repair-rollback').classification, 'destructive');
|
|
assert.equal(definitions.get('payload-manager:autoload-repair-rollback').confirmation, 'HERSTEL ORIGINELE BOOTKETEN');
|
|
assert.equal(definitions.get('etahen:autoload-persistence-plan').classification, 'sensitive');
|
|
assert.equal(definitions.get('etahen:autoload-persistence-apply').confirmation, 'PERSIST DIRECT ETAHEN 2.6B');
|
|
assert.equal(definitions.get('etahen:direct-start').classification, 'destructive');
|
|
assert.equal(definitions.get('etahen:direct-start').confirmation, 'START DIRECT ETAHEN 2.6B');
|
|
assert.equal(definitions.get('payload-manager:verified-start').classification, 'destructive');
|
|
assert.equal(definitions.get('payload-manager:verified-start').confirmation, 'START VERIFIED PS5 PAYLOAD MANAGER 0.5.1');
|
|
assert.equal(definitions.get('etahen:autoload-persistence-rollback-plan').classification, 'sensitive');
|
|
assert.equal(definitions.get('etahen:autoload-persistence-rollback').confirmation, 'HERSTEL VOOR ETAHEN 2.6B');
|
|
|
|
const taken = await definitions.get('snapshot:autoloader:take').handler({ label: 'Werkend' });
|
|
assert.equal(taken.meta.label, 'Werkend');
|
|
assert.equal(taken.meta.files.length, 1);
|
|
assert.equal(taken.meta.hasAutoload, true);
|
|
|
|
const listed = await definitions.get('snapshot:autoloader:list').handler();
|
|
assert.equal(listed.length, 1);
|
|
assert.equal(listed[0].id, taken.meta.id);
|
|
|
|
const exported = await definitions.get('snapshot:autoloader:export').handler({ id: taken.meta.id });
|
|
const archivePath = path.join(paths.cache, decodeURIComponent(exported.downloadUrl.replace('/pork-cache/', '')));
|
|
assert.equal(fs.existsSync(archivePath), true);
|
|
|
|
const imported = await definitions.get('snapshot:autoloader:import').handler({ data: fs.readFileSync(archivePath), filename: exported.filename, label: 'Import' });
|
|
assert.equal(imported.meta.label, 'Import');
|
|
assert.equal(imported.meta.files[0].name, 'etaHEN.elf');
|
|
|
|
remote.clear();
|
|
const restored = await definitions.get('snapshot:autoloader:restore').handler({ id: imported.meta.id });
|
|
assert.equal(restored.ok, true);
|
|
assert.equal(restored.fileCount, 1);
|
|
assert.equal(remote.get('/data/ps5_autoloader/etaHEN.elf').toString(), 'payload');
|
|
assert.equal(remote.get('/data/ps5_autoloader/autoload.txt').toString(), 'etaHEN.elf\n');
|
|
});
|
|
|
|
test('Y2JB generator can use verified payloads from the Chimera catalog without a browser upload', async () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-y2jb-catalog-'));
|
|
const paths = { root, backups: path.join(root, 'backups'), cache: path.join(root, 'cache'), tmp: path.join(root, 'tmp') };
|
|
const payloadRoot = path.join(root, 'payloads');
|
|
for (const dir of [...Object.values(paths), payloadRoot]) fs.mkdirSync(dir, { recursive: true });
|
|
const payloadBytes = Buffer.from('verified catalog payload');
|
|
const payloadPath = path.join(payloadRoot, 'catalog-test.elf');
|
|
fs.writeFileSync(payloadPath, payloadBytes);
|
|
const payloadHash = crypto.createHash('sha256').update(payloadBytes).digest('hex');
|
|
const definitions = new Map();
|
|
registerFunctionalModules({
|
|
register: definition => definitions.set(definition.name, definition),
|
|
db: {
|
|
getPayloadCatalog: () => ({
|
|
files: [{
|
|
id: 42,
|
|
asset_name: 'catalog-test.elf',
|
|
local_path: payloadPath,
|
|
local_size: payloadBytes.length,
|
|
local_hash: payloadHash,
|
|
local_version: '1.2.3',
|
|
source_name: 'Fixture source',
|
|
}],
|
|
}),
|
|
},
|
|
ftp: { isConnected: () => false, hasCredentials: () => false },
|
|
settings: { get: (key, fallback) => key === 'payload.localPath' ? payloadRoot : fallback },
|
|
secrets: { get: () => '', has: () => false },
|
|
paths,
|
|
logger: { warn() {}, info() {}, error() {} },
|
|
broadcast() {},
|
|
});
|
|
try {
|
|
const catalog = definitions.get('y2jbgen:catalog').handler();
|
|
assert.deepEqual(catalog.items, [{
|
|
id: '42',
|
|
name: 'catalog-test.elf',
|
|
size: payloadBytes.length,
|
|
sha256: payloadHash,
|
|
version: '1.2.3',
|
|
source: 'Fixture source',
|
|
}]);
|
|
const result = await definitions.get('y2jbgen:build').handler({
|
|
catalogPayloadIds: ['42'],
|
|
entries: [{ type: 'payload', value: 'catalog-test.elf' }],
|
|
});
|
|
assert.equal(result.outputZip, 'y2jb_update.zip');
|
|
assert.match(result.updateInfo, /ps5_autoloader\/catalog-test\.elf\|24/);
|
|
|
|
fs.writeFileSync(payloadPath, 'tampered');
|
|
await assert.rejects(
|
|
() => definitions.get('y2jbgen:build').handler({
|
|
catalogPayloadIds: ['42'],
|
|
entries: [{ type: 'payload', value: 'catalog-test.elf' }],
|
|
}),
|
|
/Checksum/
|
|
);
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('media catalog persists deltas and reuses unchanged PS5 directories', async () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-media-catalog-'));
|
|
const paths = { root, backups: path.join(root, 'backups'), cache: path.join(root, 'cache'), tmp: path.join(root, 'tmp') };
|
|
for (const dir of Object.values(paths)) fs.mkdirSync(dir, { recursive: true });
|
|
let marker = new Date('2026-07-16T10:00:00Z');
|
|
let files = [{ name: '20260716_120000.jpg', isDir: false, size: 123, modifiedAt: marker }];
|
|
let failChild = false;
|
|
const calls = [];
|
|
const ftp = {
|
|
isConnected: () => true, hasCredentials: () => true,
|
|
listDir: async remotePath => {
|
|
calls.push(remotePath);
|
|
if (remotePath === '/user/av_contents/photo') return [{ name: 'CUSA12345', isDir: true, size: 0, modifiedAt: marker }];
|
|
if (remotePath === '/user/av_contents/photo/CUSA12345') { if (failChild) throw new Error('temporary FTP failure'); return files; }
|
|
if (remotePath === '/user/av_contents/video') return [];
|
|
return [];
|
|
},
|
|
};
|
|
const definitions = new Map();
|
|
registerFunctionalModules({
|
|
register: definition => definitions.set(definition.name, definition), db: {}, ftp,
|
|
settings: { get: (key, fallback) => key === 'ftp.host' ? '127.0.0.1' : fallback },
|
|
secrets: { get: () => '', has: () => false }, paths,
|
|
logger: { warn() {}, info() {}, error() {} }, broadcast() {},
|
|
});
|
|
try {
|
|
const scan = definitions.get('media:scan');
|
|
const first = await scan.handler({ force: true });
|
|
assert.equal(first.stats.total, 1);
|
|
assert.equal(first.delta.added, 1);
|
|
assert.equal(first.scan.mode, 'full');
|
|
const catalogFile = path.join(paths.cache, 'media', 'catalog.json');
|
|
const cached = JSON.parse(fs.readFileSync(catalogFile, 'utf8'));
|
|
cached.scannedAt = 1;
|
|
fs.writeFileSync(catalogFile, JSON.stringify(cached));
|
|
calls.length = 0;
|
|
const second = await scan.handler({ force: false });
|
|
assert.equal(second.scan.mode, 'incremental');
|
|
assert.equal(second.scan.reusedDirectories, 1);
|
|
assert.deepEqual(calls, ['/user/av_contents/photo', '/user/av_contents/video']);
|
|
marker = new Date('2026-07-16T10:05:00Z');
|
|
files = [...files, { name: '20260716_120500.png', isDir: false, size: 456, modifiedAt: marker }];
|
|
const stale = JSON.parse(fs.readFileSync(catalogFile, 'utf8'));
|
|
stale.scannedAt = 1;
|
|
fs.writeFileSync(catalogFile, JSON.stringify(stale));
|
|
const third = await scan.handler({ force: false });
|
|
assert.equal(third.stats.total, 2);
|
|
assert.equal(third.delta.added, 1);
|
|
assert.equal(third.scan.mode, 'incremental');
|
|
const loaded = definitions.get('media:catalog').handler();
|
|
assert.equal(loaded.items.length, 2);
|
|
assert.equal(Object.prototype.hasOwnProperty.call(loaded, 'directories'), true);
|
|
assert.equal(loaded.directories, undefined);
|
|
marker = new Date('2026-07-16T10:10:00Z');
|
|
failChild = true;
|
|
const beforeFailure = JSON.parse(fs.readFileSync(catalogFile, 'utf8'));
|
|
beforeFailure.scannedAt = 1;
|
|
fs.writeFileSync(catalogFile, JSON.stringify(beforeFailure));
|
|
const preserved = await scan.handler({ force: false });
|
|
assert.equal(preserved.stats.total, 2);
|
|
assert.equal(preserved.scan.errors.length, 1);
|
|
assert.equal(preserved.scan.errors[0].preservedItems, 2);
|
|
assert.equal(preserved.delta.removed, 0);
|
|
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('payload synchronization stops API calls after a GitHub rate limit and preserves cached rows', async () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-payload-rate-limit-'));
|
|
const paths = { root, backups: path.join(root, 'backups'), cache: path.join(root, 'cache'), tmp: path.join(root, 'tmp') };
|
|
for (const dir of Object.values(paths)) fs.mkdirSync(dir, { recursive: true });
|
|
const sources = [1, 2, 3].map(id => ({ id, name: `Source ${id}`, github_url: `https://github.com/example/repo-${id}`, enabled: 1, built_in: 0 }));
|
|
const updates = [];
|
|
const progress = [];
|
|
let calls = 0;
|
|
const originalFetchLatestRelease = github.fetchLatestRelease;
|
|
github.fetchLatestRelease = async () => {
|
|
calls += 1;
|
|
const error = new Error('GitHub API-limiet bereikt; probeer later opnieuw.');
|
|
error.code = 'GITHUB_RATE_LIMIT';
|
|
error.retryAt = Date.now() + 60_000;
|
|
throw error;
|
|
};
|
|
const definitions = new Map();
|
|
try {
|
|
registerFunctionalModules({
|
|
register: definition => definitions.set(definition.name, definition),
|
|
db: {
|
|
upsertPayloadSource() {}, listPayloadSources: () => sources, removePayloadSource() {},
|
|
getPayloadFiles: sourceId => [{ id: sourceId, asset_name: `cached-${sourceId}.elf` }],
|
|
updatePayloadSourceError: (sourceId, error) => updates.push({ sourceId, error }),
|
|
},
|
|
ftp: { isConnected: () => false, hasCredentials: () => false },
|
|
settings: { get: (key, fallback) => fallback }, secrets: { get: () => '', has: () => false }, paths,
|
|
logger: { warn() {}, info() {}, error() {} }, broadcast: (name, value) => progress.push({ name, value }),
|
|
});
|
|
const result = await definitions.get('payload:check-updates').handler({});
|
|
assert.equal(calls, 1);
|
|
assert.equal(result.length, 3);
|
|
assert.equal(result[0].deferred, undefined);
|
|
assert.equal(result[1].deferred, true);
|
|
assert.equal(result[2].deferred, true);
|
|
assert.equal(updates.length, 1);
|
|
assert.equal(result[1].files[0].asset_name, 'cached-2.elf');
|
|
assert.equal(progress.filter(item => item.value.status === 'deferred').length, 2);
|
|
} finally {
|
|
github.fetchLatestRelease = originalFetchLatestRelease;
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|