Files
chimera-Public/test/server/ch-0206-guided-move.test.js

63 lines
4.5 KiB
JavaScript

'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const test = require('node:test');
const database = require('../../src/database');
const { createStorageInventoryService } = require('../../src/services/storage-inventory-service');
const { createGuidedGameMoveService } = require('../../src/services/guided-game-move-service');
async function fixture(t) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-guided-move-'));
database.configure({ dataDir: root, logger: { info() {}, warn() {}, error() {} } });
await database.initialize();
t.after(() => { database.close(); fs.rmSync(root, { recursive: true, force: true }); });
const observedAt = Date.now();
const storage = createStorageInventoryService({ db: database, now: () => observedAt });
const inventory = storage.observe({ scope: 'console', adapterId: 'sentinel', source: 'sentinel-test', observedAt, completeCoverage: true, devices: [
{ stableFingerprint: 'internal', displayName: 'Internal', volumes: [{ stableVolumeKey: 'internal', mountPath: '/user/app', capacityBytes: 2000, freeBytes: 1000, capabilities: { canInstall: true } }] },
{ stableFingerprint: 'm2', displayName: 'M.2 SSD', volumes: [{ stableVolumeKey: 'm2', mountPath: '/mnt/m2', capacityBytes: 3000, freeBytes: 2000, capabilities: { canInstall: true } }] },
] });
const sourceVolumeId = inventory.devices.find(item => item.stableFingerprint === 'internal').volumes[0].id;
const destinationVolumeId = inventory.devices.find(item => item.stableFingerprint === 'm2').volumes[0].id;
database.runMutatingSqlWithParams("INSERT INTO titles (platform, title_id, name, sort_name) VALUES ('ps5', 'PPSA00001', 'Move Test', 'Move Test')", {});
const titleDbId = Number(database.runQuery('SELECT last_insert_rowid() AS id')[0].id);
database.runMutatingSqlWithParams(`INSERT INTO console_installs (title_id, console_profile_id, volume_id, remote_location_key, install_state, last_seen_at) VALUES (@titleId, 'ps5', @volumeId, 'source', 'observed', @seenAt)`, { titleId: titleDbId, volumeId: sourceVolumeId, seenAt: observedAt });
return { service: createGuidedGameMoveService({ db: database, now: () => observedAt, uuid: () => 'plan-1' }), titleDbId, sourceVolumeId, destinationVolumeId, observedAt };
}
test('guided plan validates source, destination, capacity and returns exact manual steps', async t => {
const f = await fixture(t);
const plan = f.service.create({ titleId: 'PPSA00001', sourceVolumeId: f.sourceVolumeId, destinationVolumeId: f.destinationVolumeId, requiredBytes: 500 });
assert.equal(plan.status, 'planned');
assert.equal(plan.beforeSnapshot.length, 1);
assert.ok(plan.steps.some(step => step.includes('Settings > Storage > Games and Apps')));
assert.ok(plan.steps.some(step => step.includes('Do not delete the source')));
assert.equal(plan.sourceDeletionInitiated, false);
assert.throws(() => f.service.create({ titleId: 'PPSA00001', sourceVolumeId: f.sourceVolumeId, destinationVolumeId: f.destinationVolumeId, requiredBytes: 2500 }), /no longer eligible.*insufficient-capacity/i);
});
test('after-scan reconciliation links snapshots and reports the location diff', async t => {
const f = await fixture(t);
f.service.create({ titleId: 'PPSA00001', sourceVolumeId: f.sourceVolumeId, destinationVolumeId: f.destinationVolumeId, requiredBytes: 500 });
database.runMutatingSqlWithParams(`INSERT INTO console_installs (title_id, console_profile_id, volume_id, remote_location_key, install_state, last_seen_at) VALUES (@titleId, 'ps5', @volumeId, 'destination', 'observed', @seenAt)`, { titleId: f.titleDbId, volumeId: f.destinationVolumeId, seenAt: f.observedAt + 1 });
const result = f.service.reconcile('plan-1');
assert.equal(result.status, 'completed');
assert.equal(result.diff.destinationObserved, true);
assert.equal(result.diff.sourceStillObserved, true);
assert.equal(result.diff.sourceDeletionInitiated, false);
assert.equal(result.afterSnapshot.length, 2);
});
test('cancel changes only plan state and exposes no delete operation', async t => {
const f = await fixture(t);
f.service.create({ titleId: 'PPSA00001', sourceVolumeId: f.sourceVolumeId, destinationVolumeId: f.destinationVolumeId, requiredBytes: 500 });
const cancelled = f.service.cancel('plan-1');
assert.equal(cancelled.status, 'cancelled');
assert.equal(cancelled.sourceDeletionInitiated, false);
assert.equal(typeof f.service.delete, 'undefined');
assert.throws(() => f.service.reconcile('plan-1'), /cancelled/i);
});