'use strict'; const assert = require('node:assert/strict'); const crypto = require('crypto'); const fs = require('fs'); const os = require('os'); const path = require('path'); const test = require('node:test'); const { buildManifest, createPortableManifestService, inventoryFromManifest, previewNormalizedLayout, validateManifest } = require('../../src/services/portable-manifest-service'); const manifestSchema = require('../../docs/schemas/chimera-manifest.schema.json'); const evidence = (value, source = 'pkg-param-sfo', confidence = 'high') => ({ value, source, confidence, observedAt: 1000 }); const component = (id, relativePath, type, version = '1.00') => ({ id, relativePath, size: 4, sha256: crypto.createHash('sha256').update(relativePath).digest('hex'), type: evidence(type), titleId: evidence('CUSA12345'), contentId: evidence('UP0001-CUSA12345_00-ABCDEFGHIJKLMNOP'), version: evidence(version), platform: 'ps4', validated: true, provenance: [{ source: 'staging-sha256', confidence: 'verified', observedAt: 1000 }] }); function classification(components) { return { schema: 1, jobId: 'job-id', status: 'classified', classifiedAt: 1000, title: { id: evidence('CUSA12345'), platform: 'ps4', observedIds: ['CUSA12345'] }, components, issues: [], notes: [{ relativePath: 'notes.txt', size: 4, sha256: crypto.createHash('sha256').update('notes').digest('hex'), provenance: 'uploaded', execution: 'never' }], recipeProposal: { installOrder: components.map((item, index) => ({ order: index + 1, componentId: item.id, componentType: item.type.value, action: 'proposal-only' })), arbitraryCommands: [], notesExecutable: false } }; } test('portable manifest schema exposes the versioned required contract', () => { assert.equal(manifestSchema.$schema, 'https://json-schema.org/draft/2020-12/schema'); assert.deepEqual(manifestSchema.required, ['schemaVersion', 'createdAt', 'import', 'title', 'destination', 'components', 'notes', 'recipe']); }); test('manifest samples validate and preserve original names, hashes and provenance', () => { const source = classification([component('a'.repeat(24), 'Original Folder/Base Game.pkg', 'base'), component('b'.repeat(24), 'Updates/Patch.pkg', 'update', '1.02')]); const manifest = buildManifest(source, { titleName: 'Test: Game?' }, 2000); assert.deepEqual(validateManifest(manifest), { valid: true, errors: [] }); assert.equal(manifest.title.name, 'Test Game'); assert.equal(manifest.components[0].originalRelativePath, 'Original Folder/Base Game.pkg'); assert.equal(manifest.components[0].sha256, source.components[0].sha256); assert.deepEqual(manifest.components[0].provenance, source.components[0].provenance); assert.equal(manifest.destination.relativePath, 'PS4-Games/Test Game [CUSA12345]'); }); test('manifest JSON round trip rebuilds essential inventory without physical paths', () => { const manifest = buildManifest(classification([component('c'.repeat(24), 'base.pkg', 'base')]), { titleName: 'Round Trip' }, 2000); const restored = JSON.parse(JSON.stringify(manifest)); const inventory = inventoryFromManifest(restored); assert.equal(inventory.title.titleId, 'CUSA12345'); assert.equal(inventory.title.metadataSource, 'portable-manifest'); assert.equal(inventory.localCopy.componentCount, 1); assert.equal(inventory.packages[0].sha256, manifest.components[0].sha256); assert.equal(JSON.stringify(inventory).includes('C:\\'), false); }); test('normalized preview is deterministic and resolves case collisions without path escapes', () => { const source = classification([component('d'.repeat(24), 'one/Game.pkg', 'base'), component('e'.repeat(24), 'two/game.pkg', 'base')]); const forward = previewNormalizedLayout(source, { titleName: '../Unsafe / Name' }); const reverse = previewNormalizedLayout({ ...source, components: [...source.components].reverse() }, { titleName: '../Unsafe / Name' }); assert.deepEqual(forward, reverse); assert.notEqual(forward.components[0].normalizedRelativePath.toLowerCase(), forward.components[1].normalizedRelativePath.toLowerCase()); assert.equal(forward.destination.includes('..'), false); const invalid = buildManifest(classification([component('f'.repeat(24), 'base.pkg', 'base')]), {}, 2000); invalid.components[0].normalizedRelativePath = '../escape.pkg'; assert.equal(validateManifest(invalid).valid, false); }); test('service writes .chimera manifest atomically and records its digest', () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-manifest-')); let summary = null; try { const value = classification([component('1'.repeat(24), 'base.pkg', 'base')]); const stagingStore = { getClassification: () => value, openForInspection: () => ({ payloadPath: root }), saveManifest: (_id, _owner, _manifest, sha256) => { summary = sha256; } }; const service = createPortableManifestService({ stagingStore, now: () => 2000 }); const result = service.create('job', 'alice', { titleName: 'Persisted' }); const content = fs.readFileSync(path.join(root, '.chimera', 'manifest.json'), 'utf8'); assert.equal(crypto.createHash('sha256').update(content).digest('hex'), result.sha256); assert.equal(summary, result.sha256); } finally { fs.rmSync(root, { recursive: true, force: true }); } });