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 initSqlJs = require('sql.js'); const migration = require('../../src/database/migrations/0016-create-trophy-reconstruction-journals'); const { ACTION_CAPABILITIES, createSqliteTrophyActionRepository, createTrophyBackupService, createTrophyReconstructionActionService, createTrophyReconstructionSimulator, createUnsupportedLiveTrophyAdapter, } = require('../../src/services/trophy-reconstruction-action-service'); const FUTURE = '2099-01-01T00:00:00.000Z'; function item(trophyId, earnedDateTime = `2026-01-${String(trophyId).padStart(2, '0')}T03:04:05.000Z`) { return { npCommunicationId: 'NPWR12345_00', platform: 'PS5', service: 'trophy2', groupId: 'default', trophyId, sourceEarned: true, sourceEarnedDateTime: earnedDateTime, expectedLocalPreState: { earned: false, earnedDateTime: null }, }; } function plan(items = [item(2)]) { return { id: `trophy-plan-${'a'.repeat(64)}`, planSha256: 'a'.repeat(64), idempotencyKey: 'b'.repeat(64), immutable: true, createdAt: '2026-07-22T00:00:00.000Z', expiresAt: FUTURE, adapter: { id: 'simulated-local-adapter', version: '1.0.0', timestampPolicy: 'preserve_exact', capabilities: [...ACTION_CAPABILITIES], firmwares: ['9.60'], services: ['trophy2'], }, target: { firmware: '9.60', service: 'trophy2' }, items, requiredBackup: { status: 'required', artifactRequired: true }, }; } function initialState(items) { return items.map((entry) => ({ npCommunicationId: entry.npCommunicationId, platform: entry.platform, service: entry.service, groupId: entry.groupId, trophyId: entry.trophyId, earned: false, earnedDateTime: null, })); } async function harness(t, options = {}) { const SQL = await initSqlJs(); const db = new SQL.Database(); migration.up(db); const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-ch0905-')); t.after(() => fs.rmSync(root, { recursive: true, force: true })); let sequence = 0; const now = () => 1784721600000; const repository = createSqliteTrophyActionRepository({ db, now, idFactory: () => `run-${++sequence}`, }); const backupService = createTrophyBackupService({ backupRoot: root, repository, now, idFactory: () => `backup-${++sequence}`, }); const selectedPlan = options.plan || plan(); const adapter = createTrophyReconstructionSimulator({ initialItems: initialState(selectedPlan.items), ...options.simulator, }); const service = createTrophyReconstructionActionService({ repository, backupService, now, planValidator: async () => ({ valid: true, reasons: [] }), }); return { adapter, backupService, db, repository, selectedPlan, service }; } function confirmation(selectedPlan) { return `RECONSTRUCT LOCAL TROPHIES ${selectedPlan.items.length}`; } test('verified backup is mandatory and backup failure prevents all mutation', async (t) => { const context = await harness(t, { simulator: { backupFailure: true } }); await assert.rejects( context.service.execute({ plan: context.selectedPlan, adapter: context.adapter, confirmation: confirmation(context.selectedPlan) }), (error) => error.code === 'TROPHY_BACKUP_FAILED', ); assert.equal(context.adapter.metrics().applyCalls, 0); assert.deepEqual(context.repository.listRuns(context.selectedPlan.id), []); }); test('successful apply uses frozen items and rerun is idempotent', async (t) => { const context = await harness(t); const first = await context.service.execute({ plan: context.selectedPlan, adapter: context.adapter, confirmation: confirmation(context.selectedPlan), }); assert.equal(first.status, 'completed'); assert.equal(first.postVerification.verified, true); assert.equal(context.adapter.metrics().applyCalls, context.selectedPlan.items.length); const storedBackup = await context.backupService.load(first.backupId); assert.equal(storedBackup.verified, true); assert.throws( () => context.db.run("UPDATE trophy_reconstruction_backups SET manifest_json = '{}'"), /immutable/i, ); const second = await context.service.execute({ plan: context.selectedPlan, adapter: context.adapter, confirmation: confirmation(context.selectedPlan), }); assert.equal(second.idempotent, true); assert.equal(second.runId, first.runId); assert.equal(context.adapter.metrics().applyCalls, context.selectedPlan.items.length); }); test('mid-action failure journals items and restores the verified backup', async (t) => { const selectedPlan = plan([item(2), item(3)]); const context = await harness(t, { plan: selectedPlan, simulator: { failAtItem: 'NPWR12345_00:PS5:trophy2:default:3' }, }); await assert.rejects( context.service.execute({ plan: selectedPlan, adapter: context.adapter, confirmation: confirmation(selectedPlan) }), (error) => error.code === 'TROPHY_ACTION_FAILED' && error.rollbackStatus === 'rolled_back', ); assert.ok(context.adapter.state().every(([, entry]) => entry.earned === false)); const [run] = context.repository.listRuns(selectedPlan.id); assert.equal(run.status, 'rolled_back'); const journal = run.items; assert.equal(journal[0].status, 'verified'); assert.equal(journal[1].status, 'failed'); }); test('post-scan mismatch triggers rollback instead of reporting success', async (t) => { const context = await harness(t, { simulator: { postScanMismatch: true } }); await assert.rejects( context.service.execute({ plan: context.selectedPlan, adapter: context.adapter, confirmation: confirmation(context.selectedPlan) }), (error) => error.code === 'TROPHY_ACTION_FAILED' && error.rollbackStatus === 'rolled_back', ); assert.ok(context.adapter.state().every(([, entry]) => entry.earned === false)); }); test('unsupported exact timestamp capability is rejected before backup or mutation', async (t) => { const context = await harness(t, { simulator: { timestampPolicy: 'current_time_only' } }); await assert.rejects( context.service.execute({ plan: context.selectedPlan, adapter: context.adapter, confirmation: confirmation(context.selectedPlan) }), (error) => error.code === 'TROPHY_TIMESTAMP_UNSUPPORTED', ); assert.equal(context.adapter.metrics().applyCalls, 0); assert.deepEqual(context.repository.listRuns(context.selectedPlan.id), []); }); test('action service has no network or PSN provider dependency and live binding is explicitly unavailable', () => { const source = fs.readFileSync( path.join(__dirname, '../../src/services/trophy-reconstruction-action-service.js'), 'utf8', ); assert.doesNotMatch(source, /node:https?|node-fetch|axios|\bfetch\s*\(/); assert.doesNotMatch(source, /psn-provider|psn-client/i); const live = createUnsupportedLiveTrophyAdapter(); assert.equal(live.status().available, false); assert.match(live.status().reason, /unsupported|version-pinned/i); const status = fs.readFileSync( path.join(__dirname, '../../docs/autonomous/CH-0905_TROPHY_ADAPTER_STATUS.md'), 'utf8', ); assert.match(status, /unsupported/i); assert.match(status, /version-pinned/i); });