import test from 'node:test'; import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { initializeControl, transition } from '../src/state.mjs'; import { sandboxProblem } from '../../seat/src/sandbox.mjs'; const skip = sandboxProblem() ?? false; test('the actual nested read-only control mount refuses unlink, replacement and parent removal', { skip }, t => { const root = mkdtempSync(join(tmpdir(), 'processing-mount-')); t.after(() => rmSync(root, { recursive: true, force: true })); initializeControl(root); transition(root, { business: 'demo', human: 'jason', halted: true }); const file = join(root, 'control', 'processing.json'), bytes = readFileSync(file), inode = statSync(file).ino; writeFileSync(join(root, 'replacement'), 'off replacement'); const r = spawnSync('bwrap', ['--unshare-user', '--unshare-pid', '--die-with-parent', '--ro-bind', '/usr', '/usr', '--symlink', 'usr/bin', '/bin', '--symlink', 'usr/lib', '/lib', '--symlink', 'usr/lib64', '/lib64', '--proc', '/proc', '--dev', '/dev', '--bind', root, '/fixture', '--ro-bind', join(root, 'control'), '/fixture/control', '/bin/sh', '-c', 'rm /fixture/control/processing.json && exit 10; echo unlink-refused; mv /fixture/replacement /fixture/control/processing.json && exit 11; echo replace-refused; rm -rf /fixture/control && exit 12; echo parent-refused'], { encoding: 'utf8', timeout: 5000 }); assert.equal(r.status, 0, r.stderr); assert.match(r.stdout, /unlink-refused/); assert.match(r.stdout, /replace-refused/); assert.match(r.stdout, /parent-refused/); assert.deepEqual(readFileSync(file), bytes); assert.equal(statSync(file).ino, inode); }); test('the image entrypoint refuses halted and stale generations using the shared reader', t => { const root = mkdtempSync(join(tmpdir(), 'processing-entry-')); t.after(() => rmSync(root, { recursive: true, force: true })); const initial = initializeControl(root), entry = new URL('../src/entrypoint.mjs', import.meta.url).pathname; const run = generation => spawnSync(process.execPath, [entry, root], { env: { ...process.env, MOSAIC_PROCESSING_GENERATION: generation }, encoding: 'utf8' }); assert.equal(run(initial.generation).status, 0); const halt = transition(root, { business: 'demo', human: 'jason', halted: true }); assert.equal(run(initial.generation).status, 3); transition(root, { business: 'demo', human: 'jason', halted: false, generation: halt.generation }); const stale = run(initial.generation); assert.equal(stale.status, 3); assert.match(stale.stderr, /processing-generation-changed/); });