import test from 'node:test' import assert from 'node:assert/strict' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import { fileURLToPath } from 'node:url' import nativePlugin, { copyNativeProgram, listFiles, sha256 } from './tang-detective-native-plugin.mjs' import { validateCosMediaManifest } from './tang-detective-cos-media.mjs' import { validateNativeOutput } from './validate-tang-detective-output.mjs' import { readSourceManifest, validateSourceSnapshot } from './tang-detective-source-validation.mjs' import { createSyntheticSource, fakeVerifiedManifest, fakePruneReceipt, writeFixture } from './tang-detective-test-fixtures.mjs' const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') function fixture(t, { prune = false } = {}) { const temporary = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'tang-source-test-'))) t.after(() => fs.rmSync(temporary, { recursive: true, force: true })) const source = createSyntheticSource(temporary) const mediaManifest = fakeVerifiedManifest(source) const pruneReceipt = fakePruneReceipt(mediaManifest) const pruneReceiptPath = path.join(temporary, 'test-only-prune-receipt.json') writeFixture(temporary, 'test-only-prune-receipt.json', JSON.stringify(pruneReceipt)) if (prune) for (const file of source.sourceMedia) fs.unlinkSync(path.join(source.sourceDirectory, file.path)) const outputDirectory = path.join(temporary, 'output') writeFixture(outputDirectory, 'app.json', JSON.stringify({ pages: ['pages/index/index'] })) writeFixture(outputDirectory, 'app.js', '/* host unchanged */') const options = { sourceDirectory: source.sourceDirectory, sourceManifestPath: source.sourceManifestPath, overlayDirectory: path.join(projectRoot, 'native-adapter/tang-detective'), outputDirectory, mediaManifest, pruneReceiptPath } return { temporary, ...source, options, mediaManifest, pruneReceipt, pruneReceiptPath, outputDirectory } } const snapshot = directory => listFiles(directory).map(file => [file, sha256(fs.readFileSync(path.join(directory, file)))]) test('the pinned original manifest has 269 nonmedia and exactly 160 JPG / 44 MP3 files', () => { const source = readSourceManifest() assert.equal(source.files.size, 473) assert.equal([...source.files.keys()].filter(file => file.endsWith('.jpg')).length, 160) assert.equal([...source.files.keys()].filter(file => file.endsWith('.mp3')).length, 44) }) test('a fully pruned clone builds and validates from 269 intact files with no external backup access', t => { const context = fixture(t, { prune: true }) // A portable receipt records the archive hash. It never requires opening a // prior machine's absolute path, and no archive exists in this offline test. context.pruneReceipt.backup.originalPath = '/unavailable-on-another-machine/test-only.tar.gz' writeFixture(context.temporary, 'test-only-prune-receipt.json', JSON.stringify(context.pruneReceipt)) const media = validateCosMediaManifest(context.mediaManifest, context.options) assert.equal(media.sourceSnapshot.actual.size, 269) assert.equal(media.sourceSnapshot.missingMedia.length, 204) assert.equal(media.sourceSnapshot.prune.backupSha256, context.pruneReceipt.backup.sha256) const report = copyNativeProgram(context.options) assert.equal(report.source.omittedMediaFiles, 204) assert.equal(report.source.presentFiles, 269) assert.equal(report.sizes.mediaFileCount, 0) const validation = validateNativeOutput(context.outputDirectory, context.options) assert.deepEqual(validation.errors, []) assert.equal(validation.passed, true) const before = snapshot(context.outputDirectory) copyNativeProgram(context.options) assert.deepEqual(snapshot(context.outputDirectory), before) }) test('missing local media and missing remote prune receipt fail before any output changes', t => { const context = fixture(t) copyNativeProgram({ ...context.options, mediaManifest: null }) const before = snapshot(context.outputDirectory) fs.unlinkSync(path.join(context.sourceDirectory, context.sourceMedia[0].path)) assert.throws(() => copyNativeProgram({ ...context.options, mediaManifest: null }), /local mode requires all media/) assert.deepEqual(snapshot(context.outputDirectory), before) fs.unlinkSync(context.pruneReceiptPath) assert.throws(() => copyNativeProgram(context.options), /completed project-local media prune receipt is required/) assert.deepEqual(snapshot(context.outputDirectory), before) const report = validateNativeOutput(context.outputDirectory, { ...context.options, mediaManifest: null }) assert.equal(report.passed, false) assert.ok(report.errors.some(error => error.includes('local mode requires all media'))) }) test('the Vite buildStart preflight rejects missing local media before bundle output can begin', t => { const context = fixture(t, { prune: true }) const plugin = nativePlugin({ ...context.options, mediaManifest: null }) plugin.configResolved({ root: context.temporary, build: { outDir: 'output' } }) const before = snapshot(context.outputDirectory) assert.throws(() => plugin.buildStart.call({ addWatchFile() {} }), /local mode requires all media/) assert.deepEqual(snapshot(context.outputDirectory), before) }) test('pruning needs an exact complete receipt bound to the manifest, upload run, file records and archive hash', t => { const context = fixture(t, { prune: true }) const mutations = [ receipt => { receipt.schemaVersion = 2 }, receipt => { receipt.status = 'planned' }, receipt => { receipt.sourceManifestSha256 = '0'.repeat(64) }, receipt => { receipt.mediaManifestSha256 = '0'.repeat(64) }, receipt => { receipt.uploadRunId = 'different-run' }, receipt => { receipt.backup.sha256 = 'not-a-hash' }, receipt => { receipt.backup.bytes = 0 }, receipt => { receipt.backup.format = 'zip' }, receipt => { receipt.entries.pop() }, receipt => { receipt.entries[1] = { ...receipt.entries[0] } }, receipt => { receipt.entries[0].sourcePath = '../escape.jpg' }, receipt => { receipt.entries[0].sourcePath = 'app.json' }, receipt => { receipt.entries[0].sha256 = '0'.repeat(64) }, receipt => { receipt.entries[0].bytes++ }, receipt => { receipt.entries[0].url += '?tampered' }, receipt => { receipt.entries[0].objectKey = 'wrong.jpg' }, receipt => { receipt.entries[0].backupSha256 = '0'.repeat(64) }, ] const before = snapshot(context.outputDirectory) for (const mutate of mutations) { const receipt = structuredClone(context.pruneReceipt) mutate(receipt) assert.throws(() => copyNativeProgram({ ...context.options, pruneReceipt: receipt }), /Invalid Tang Detective source snapshot/) assert.deepEqual(snapshot(context.outputDirectory), before) } // Even a single omitted file needs evidence for the entire upload inventory. const partial = fixture(t) fs.unlinkSync(path.join(partial.sourceDirectory, partial.sourceMedia[0].path)) assert.equal(validateCosMediaManifest(partial.mediaManifest, partial.options).sourceSnapshot.missingMedia.length, 1) const invalidManifest = { ...context.mediaManifest, uploadRunId: undefined } assert.throws(() => validateCosMediaManifest(invalidManifest, { ...context.options, pruneReceipt: fakePruneReceipt(invalidManifest), }), /upload run mismatch/) }) test('remote pruning never allows missing nonmedia, changed surviving bytes or extra source files', t => { const context = fixture(t, { prune: true }) const nonmedia = 'utils/storage.js' const original = fs.readFileSync(path.join(context.sourceDirectory, nonmedia)) fs.unlinkSync(path.join(context.sourceDirectory, nonmedia)) assert.throws(() => copyNativeProgram(context.options), /missing non-media source: utils\/storage.js/) writeFixture(context.sourceDirectory, nonmedia, 'changed code') assert.throws(() => copyNativeProgram(context.options), /source bytes changed: utils\/storage.js/) writeFixture(context.sourceDirectory, nonmedia, original) for (const relative of ['assets/unexpected.jpg', 'package-game/data/unexpected.js']) { writeFixture(context.sourceDirectory, relative, 'unexpected') assert.throws(() => copyNativeProgram(context.options), /unexpected source file/) fs.unlinkSync(path.join(context.sourceDirectory, relative)) } fs.mkdirSync(path.join(context.sourceDirectory, 'unexpected-empty-directory')) assert.throws(() => copyNativeProgram(context.options), /unexpected source directory/) fs.rmdirSync(path.join(context.sourceDirectory, 'unexpected-empty-directory')) const sourceMedia = context.sourceMedia[0] writeFixture(context.sourceDirectory, sourceMedia.path, 'changed surviving media') assert.throws(() => copyNativeProgram(context.options), /source bytes changed/) assert.equal(listFiles(context.outputDirectory).length, 2) }) test('source and evidence symlinks, including dangling media links, cannot authorize or hide omission', t => { const context = fixture(t, { prune: true }) const target = path.join(context.sourceDirectory, context.sourceMedia[0].path) fs.symlinkSync(path.join(context.temporary, 'missing-media.jpg'), target) assert.throws(() => copyNativeProgram(context.options), /source symbolic links are forbidden/) fs.unlinkSync(target) const outside = path.join(context.temporary, 'original-source-directory') fs.renameSync(context.sourceDirectory, outside) fs.symlinkSync(outside, context.sourceDirectory, 'dir') try { assert.throws(() => copyNativeProgram(context.options), /source root must be a real directory/) } finally { fs.unlinkSync(context.sourceDirectory); fs.renameSync(outside, context.sourceDirectory) } const savedReceipt = path.join(context.temporary, 'original-prune-receipt.json') fs.renameSync(context.pruneReceiptPath, savedReceipt) fs.symlinkSync(savedReceipt, context.pruneReceiptPath) assert.throws(() => copyNativeProgram(context.options), /completed project-local media prune receipt is required/) fs.unlinkSync(context.pruneReceiptPath) fs.renameSync(savedReceipt, context.pruneReceiptPath) const savedSource = path.join(context.temporary, 'original-source-manifest.json') fs.renameSync(context.sourceManifestPath, savedSource) fs.symlinkSync(savedSource, context.sourceManifestPath) assert.throws(() => validateSourceSnapshot(context.options), /source manifest must be a regular file/) }) test('remote output validation rejects later receipt drift or removal', t => { const context = fixture(t, { prune: true }) copyNativeProgram(context.options) const invalid = { ...context.pruneReceipt, uploadRunId: 'changed-run' } const failed = validateNativeOutput(context.outputDirectory, { ...context.options, pruneReceipt: invalid }) assert.equal(failed.passed, false) assert.ok(failed.errors.some(error => error.includes('upload run mismatch'))) const rebound = structuredClone(context.pruneReceipt) rebound.backup.sha256 = '1'.repeat(64) for (const entry of rebound.entries) entry.backupSha256 = rebound.backup.sha256 const changedReceipt = validateNativeOutput(context.outputDirectory, { ...context.options, pruneReceipt: rebound }) assert.ok(changedReceipt.errors.includes('Media prune receipt changed after import')) fs.unlinkSync(context.pruneReceiptPath) assert.equal(validateNativeOutput(context.outputDirectory, context.options).passed, false) })