'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); });