Initial curated chimera source from a8ae5b1db07fb49926c94585e2718e5130330974
This commit is contained in:
commit
c737a37f71
531 files changed
+119270
No files matched your search
@@ -0,0 +1,81 @@
|
||||
'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);
|
||||
});
|
||||
Reference in new issue
Block a user