72 lines
4.2 KiB
JavaScript
72 lines
4.2 KiB
JavaScript
// 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]); }
|