'use strict'; const assert = require('node:assert/strict'); const test = require('node:test'); const { createGameMoveAdapterService, createMemoryJournalStore, unavailableLiveProvider } = require('../../src/services/game-move-adapter-service'); function provider(overrides = {}) { const calls = { copies: [], deletes: [] }; const base = { probe: () => ({ provider: 'simulator', version: '1.0.0', live: false, available: true, capabilities: { copyFirst: true, resume: true, verifyHash: true, gameActiveGuard: true, freeSpaceGuard: true, deleteSource: true } }), isGameActive: async () => false, freeBytes: async () => 10_000, copy: async input => { calls.copies.push(input.offset); input.onProgress(input.totalBytes); return { bytesCopied: input.totalBytes }; }, hashSource: async () => 'abc', hashDestination: async () => 'abc', deleteSource: async input => { calls.deletes.push(input); }, ...overrides, }; return { adapter: base, calls }; } test('successful copy verifies before a separately confirmed source delete', async () => { const fake = provider(); const service = createGameMoveAdapterService({ provider: fake.adapter, uuid: () => 'journal-1', now: () => 100 }); const verified = await service.start({ titleId: 'PPSA00001', sourceId: 'internal', destinationId: 'm2', totalBytes: 1000, expectedHash: 'abc' }); assert.equal(verified.status, 'verified'); assert.equal(verified.deleteEligible, true); assert.equal(fake.calls.deletes.length, 0); await assert.rejects(service.deleteSource('journal-1', 'wrong'), /Exact confirmation/); const completed = await service.deleteSource('journal-1', 'DELETE SOURCE journal-1'); assert.equal(completed.status, 'completed'); assert.equal(completed.sourceDeleted, true); assert.equal(fake.calls.deletes.length, 1); }); test('interrupted copy resumes from journal offset', async () => { let attempts = 0; const fake = provider({ copy: async input => { fake.calls.copies.push(input.offset); attempts += 1; if (attempts === 1) { const error = new Error('simulated interruption'); error.bytesCopied = 400; throw error; } input.onProgress(input.totalBytes); return { bytesCopied: input.totalBytes }; } }); const service = createGameMoveAdapterService({ provider: fake.adapter, journals: createMemoryJournalStore(), uuid: () => 'journal-2' }); const interrupted = await service.start({ titleId: 'PPSA00001', sourceId: 'internal', destinationId: 'usb', totalBytes: 1000 }); assert.equal(interrupted.status, 'interrupted'); assert.equal(interrupted.bytesCopied, 400); const resumed = await service.resume('journal-2'); assert.equal(resumed.status, 'verified'); assert.deepEqual(fake.calls.copies, [0, 400]); }); test('hash mismatch never becomes delete eligible', async () => { const fake = provider({ hashDestination: async () => 'different' }); const service = createGameMoveAdapterService({ provider: fake.adapter, uuid: () => 'journal-3' }); const result = await service.start({ titleId: 'PPSA00001', sourceId: 'internal', destinationId: 'm2', totalBytes: 1000 }); assert.equal(result.status, 'needs_attention'); assert.equal(result.error, 'hash-mismatch'); assert.equal(result.deleteEligible, false); await assert.rejects(service.deleteSource('journal-3', 'DELETE SOURCE journal-3'), /unavailable until copy verification/); }); test('guards stop active games and insufficient free space before copying', async () => { const active = provider({ isGameActive: async () => true }); const activeResult = await createGameMoveAdapterService({ provider: active.adapter }).start({ titleId: 'PPSA00001', sourceId: 'a', destinationId: 'b', totalBytes: 100 }); assert.equal(activeResult.error, 'game-active'); assert.equal(active.calls.copies.length, 0); const full = provider({ freeBytes: async () => 10 }); const fullResult = await createGameMoveAdapterService({ provider: full.adapter }).start({ titleId: 'PPSA00001', sourceId: 'a', destinationId: 'b', totalBytes: 100 }); assert.equal(fullResult.error, 'insufficient-free-space'); assert.equal(full.calls.copies.length, 0); }); test('capability mismatch and default live provider are visibly unavailable', async () => { const unavailable = createGameMoveAdapterService({ provider: unavailableLiveProvider() }); assert.equal(unavailable.probe().available, false); assert.equal(unavailable.probe().compatible, false); await assert.rejects(unavailable.start({ titleId: 'PPSA00001', sourceId: 'a', destinationId: 'b', totalBytes: 100 }), /unavailable or capability mismatch/i); const mismatch = provider({ probe: () => ({ provider: 'old-adapter', version: '0.1', available: true, capabilities: { copyFirst: true } }) }); await assert.rejects(createGameMoveAdapterService({ provider: mismatch.adapter }).start({ titleId: 'PPSA00001', sourceId: 'a', destinationId: 'b', totalBytes: 100 }), /capability mismatch/i); });