Files
stack/packages/cli/live/support-common.mjs

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