Files
chimera-Public/test/server/ch-0305-portable-manifest.test.js

70 lines
5.2 KiB
JavaScript

'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 }); }
});