'use strict'; const assert = require('node:assert/strict'); const test = require('node:test'); const initSqlJs = require('sql.js'); const migration = require('../../src/database/migrations/0015-create-trophy-reconstruction-plans'); const { REQUIRED_CAPABILITIES, createSqliteTrophyPlanRepository, createTrophyReconstructionPlanService } = require('../../src/services/trophy-reconstruction-plan-service'); const PSN_HASH = 'a'.repeat(64); const LOCAL_HASH = 'b'.repeat(64); const MAP_HASH = 'c'.repeat(64); const CONTEXT_HASH = 'd'.repeat(64); const EARNED_AT = '2026-01-02T03:04:05.000Z'; function evidence() { const psnSnapshot = { id: 'psn-proof', canonicalSha256: PSN_HASH, immutable: true, snapshot: { account: { accountId: '1234567890123456' } } }; const localSnapshot = { id: 'local-proof', canonicalSha256: LOCAL_HASH, immutable: true, snapshot: { provider: { firmware: '9.60' }, coverage: { status: 'complete', canConcludeAbsence: true } } }; const item = { state: 'psn_only', npCommunicationId: 'NPWR12345_00', platform: 'PS5', service: 'trophy2', groupId: 'default', trophyId: 2, sourceEarned: true, localEarned: false, sourceEarnedDateTime: EARNED_AT, exactAccountMapping: true, exactTitleMapping: true, exactTrophyMapping: true }; const diff = { readOnly: true, psnSnapshotId: psnSnapshot.id, localSnapshotId: localSnapshot.id, mappingId: 'mapping-1', mappingRevision: 1, mappingExact: true, mappingStateHash: MAP_HASH, contextHash: CONTEXT_HASH, items: [item] }; const adapter = { id: 'simulated-local-adapter', version: '1.0.0', timestampPolicy: 'preserve_exact', capabilities: [...REQUIRED_CAPABILITIES], firmwares: ['9.60'], services: ['trophy2'] }; const selections = [{ npCommunicationId: item.npCommunicationId, platform: item.platform, service: item.service, groupId: item.groupId, trophyId: item.trophyId }]; return { psnSnapshot, localSnapshot, diff, adapter, selections }; } async function setup(nowRef = { value: 1800000000000 }) { const SQL = await initSqlJs(); const db = new SQL.Database(); migration.up(db); const repository = createSqliteTrophyPlanRepository({ db }); return { db, repository, service: createTrophyReconstructionPlanService({ repository, now: () => nowRef.value }), nowRef }; } test('eligible PSN-earned exact evidence creates one immutable idempotent plan', async t => { const env = await setup(); t.after(() => env.db.close()); const input = evidence(); const first = env.service.create(input); env.nowRef.value += 1000; const second = env.service.create(input); assert.equal(first.id, second.id); assert.equal(first.idempotencyKey, second.idempotencyKey); assert.equal(env.repository.list().length, 1); assert.deepEqual(first.psnSnapshot, { id: 'psn-proof', sha256: PSN_HASH }); assert.deepEqual(first.localSnapshot, { id: 'local-proof', sha256: LOCAL_HASH }); assert.equal(first.items[0].sourceEarned, true); assert.equal(first.items[0].sourceEarnedDateTime, EARNED_AT); assert.deepEqual(first.requiredBackup, { status: 'required', artifactRequired: true }); assert.throws(() => env.db.run('UPDATE trophy_reconstruction_plans SET adapter_version = ?', ['2']), /immutable/); }); test('eligibility matrix blocks unearned, wrong-state, incomplete and incapable inputs', async t => { const env = await setup(); t.after(() => env.db.close()); const cases = [ ['STATE_NOT_PSN_ONLY', input => { input.diff.items[0].state = 'both'; }], ['SOURCE_NOT_EARNED', input => { input.diff.items[0].sourceEarned = false; }], ['ACCOUNT_MAPPING_NOT_EXACT', input => { input.diff.mappingExact = false; }], ['LOCAL_COVERAGE_INCOMPLETE', input => { input.localSnapshot.snapshot.coverage = { status: 'partial', canConcludeAbsence: false }; }], ['ADAPTER_CAPABILITY_MISSING:verified-backup', input => { input.adapter.capabilities = input.adapter.capabilities.filter(item => item !== 'verified-backup'); }], ['ADAPTER_FIRMWARE_UNSUPPORTED', input => { input.adapter.firmwares = ['10.00']; }], ['ADAPTER_SERVICE_UNSUPPORTED:trophy2', input => { input.adapter.services = ['trophy']; }], ]; for (const [reason, mutate] of cases) { const input = evidence(); mutate(input); const assessment = env.service.assess(input); assert.equal(assessment.eligible, false, reason); assert.ok(assessment.reasons.some(item => item === reason || item.startsWith(`${reason}:`)), `${reason}: ${assessment.reasons.join(',')}`); } }); test('arbitrary, duplicate and unearned trophy selections are rejected', async t => { const env = await setup(); t.after(() => env.db.close()); const arbitrary = evidence(); arbitrary.selections[0].trophyId = 999; assert.throws(() => env.service.create(arbitrary), error => error.code === 'TROPHY_PLAN_INELIGIBLE' && error.reasons.some(reason => reason.startsWith('ARBITRARY_TROPHY_ID:'))); const duplicate = evidence(); duplicate.selections.push({ ...duplicate.selections[0] }); assert.throws(() => env.service.create(duplicate), error => error.code === 'TROPHY_PLAN_INELIGIBLE' && error.reasons.includes('DUPLICATE_SELECTION')); const unearned = evidence(); unearned.diff.items[0].sourceEarned = false; assert.throws(() => env.service.create(unearned), error => error.code === 'TROPHY_PLAN_INELIGIBLE' && error.reasons.some(reason => reason.startsWith('SOURCE_NOT_EARNED:'))); }); test('snapshot, mapping, adapter and expiry changes invalidate frozen plans', async t => { const env = await setup(); t.after(() => env.db.close()); const input = evidence(); const plan = env.service.create({ ...input, ttlMs: 60000 }); assert.equal(env.service.validate(plan, input).valid, true); const sourceChanged = evidence(); sourceChanged.psnSnapshot = { ...sourceChanged.psnSnapshot, canonicalSha256: 'e'.repeat(64) }; assert.ok(env.service.validate(plan, sourceChanged).reasons.includes('SOURCE_SNAPSHOT_CHANGED')); const localChanged = evidence(); localChanged.localSnapshot = { ...localChanged.localSnapshot, canonicalSha256: 'f'.repeat(64) }; assert.ok(env.service.validate(plan, localChanged).reasons.includes('LOCAL_SNAPSHOT_CHANGED')); const mappingChanged = evidence(); mappingChanged.diff = { ...mappingChanged.diff, mappingStateHash: 'e'.repeat(64), contextHash: 'f'.repeat(64), mappingRevision: 2 }; assert.ok(env.service.validate(plan, mappingChanged).reasons.includes('MAPPING_OR_DIFF_CHANGED')); const adapterChanged = evidence(); adapterChanged.adapter = { ...adapterChanged.adapter, version: '2.0.0' }; assert.ok(env.service.validate(plan, adapterChanged).reasons.includes('ADAPTER_CHANGED')); env.nowRef.value = plan.expiresAt; assert.ok(env.service.validate(plan, evidence()).reasons.includes('PLAN_EXPIRED')); }); test('only preserve_exact timestamp policy can create a plan', async t => { const env = await setup(); t.after(() => env.db.close()); for (const policy of ['preserve_date_only', 'current_time_only', 'unsupported']) { const input = evidence(); input.adapter.timestampPolicy = policy; const assessment = env.service.assess(input); assert.equal(assessment.eligible, false); assert.ok(assessment.reasons.includes('TIMESTAMP_POLICY_UNSUPPORTED')); } const exact = env.service.assess(evidence()); assert.equal(exact.eligible, true); });