// Owner-run, local test support. No product capability or processing-state writes. import { constants, closeSync, fsyncSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'; import { dirname, isAbsolute, join, resolve, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; import { spawnSync } from 'node:child_process'; import { homedir } from 'node:os'; export const ROOT = fileURLToPath(new URL('../../../', import.meta.url)); export const fail = message => { throw new Error(message); }; export const inside = (parent, path) => { const r = relative(parent, path); return !r || (r !== '..' && !r.startsWith('../') && !isAbsolute(r)); }; export function plainPath(path) { if (!isAbsolute(path ?? '') || resolve(path) !== path) fail('canonical absolute path required'); let cursor = path; while (cursor !== dirname(cursor)) { try { if (lstatSync(cursor).isSymbolicLink()) fail('symbolic link refused'); } catch (e) { if (e.code !== 'ENOENT') throw e; } cursor = dirname(cursor); } return path; } export function privateDir(path, create = false) { plainPath(path); if (create) mkdirSync(path, { mode: 0o700 }); const s = lstatSync(path); if (!s.isDirectory() || s.uid !== process.getuid() || (s.mode & 0o777) !== 0o700) fail('private owner directory required'); return path; } export function readPrivate(file) { plainPath(file); const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK); try { const s = fstatSync(fd); if (!s.isFile() || s.uid !== process.getuid() || (s.mode & 0o777) !== 0o600 || s.size > 16 * 1024 * 1024) fail('private bounded owner file required'); return readFileSync(fd, 'utf8'); } finally { closeSync(fd); } } export function exclusive(file, value) { plainPath(file); const fd = openSync(file, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600); try { writeFileSync(fd, value); fsyncSync(fd); } finally { closeSync(fd); } } export function options(argv, allowed) { const out = {}; while (argv.length) { const key = argv.shift(); if (!allowed.includes(key) || Object.hasOwn(out, key) || !argv.length) fail('unknown, duplicate or missing option'); out[key] = argv.shift(); } return out; } export function testInstance(instance, config) { if (instance !== 'ui797') fail('literal ui797 test instance required'); plainPath(config); const copies = join(process.env.XDG_DATA_HOME || join(homedir(), '.local/share'), 'mosaic-stack/copies'); if (inside(ROOT, config) || inside(copies, config)) fail('test config and secret slots must be outside source and stack copies'); if (config.split('/').includes('mosaic-dev') || config.split('/').includes('.mosaic-dev') || dirname(config).split('/').at(-1) !== `mosaic-${instance}`) fail('matching named test config required; mosaic-dev refused'); if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') fail('TLS verification must remain enabled'); const run = spawnSync(process.execPath, [join(ROOT, 'scripts/mosaic-config.mjs'), 'validate'], { env: { ...process.env, MOSAIC_CONFIG: config }, encoding: 'utf8' }); if (run.status !== 0) fail('test system configuration validation refused'); const system = JSON.parse(run.stdout); plainPath(system.dataRoot); if (inside(ROOT, system.dataRoot) || inside(copies, system.dataRoot)) fail('test data root must be outside source and stack copies'); if (system.environment !== 'development' || system.dataRoot.split('/').some(p => ['.mosaic-dev','mosaic-dev'].includes(p))) fail('isolated development data root required'); const marker = JSON.parse(readFileSync(join(system.dataRoot, 'instance.json'), 'utf8')); if (marker.instanceVersion !== 1 || marker.name !== instance) fail('test instance marker mismatch'); return { system, config: realpathSync(config) }; } export function outsideRuntime(path, system, config) { plainPath(path); if (inside(ROOT, path) || inside(system.dataRoot, path) || inside(dirname(config), path)) fail('runtime must be outside repository, config and model data'); } export function isMain(meta) { return process.argv[1] && fileURLToPath(meta) === resolve(process.argv[1]); }