import assert from 'node:assert/strict'; import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import test from 'node:test'; import { hashArtifact, inspectPortalRuntime, assertPortalRuntimePath, removeForbiddenPortalRuntimePaths, REQUIRED_PORTAL_RUNTIME_PATHS, } from './artifact.mjs'; async function withTempDir(fn) { const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-release-gate-')); try { return await fn(dir); } finally { await fs.rm(dir, { recursive: true, force: true }); } } test('directory artifact hash is stable across mtime changes', async () => { await withTempDir(async (dir) => { await fs.mkdir(path.join(dir, 'nested')); const file = path.join(dir, 'nested', 'value.txt'); await fs.writeFile(file, 'stable\n'); const first = await hashArtifact(dir); await fs.utimes(file, new Date(), new Date(Date.now() + 10_000)); const second = await hashArtifact(dir); assert.equal(first.sha256, second.sha256); assert.equal(first.files, 1); }); }); test('artifact hashing refuses symlinks that escape the artifact root', async () => { await withTempDir(async (dir) => { await fs.symlink('/tmp', path.join(dir, 'escape')); await assert.rejects(() => hashArtifact(dir), /symlink escapes root/); }); }); test('Portal runtime inspection requires dependency closure and no persisted state', async () => { await withTempDir(async (dir) => { for (const relative of REQUIRED_PORTAL_RUNTIME_PATHS) { const absolute = path.join(dir, relative); if (relative === 'dist') { await fs.mkdir(absolute, { recursive: true }); } else { await fs.mkdir(path.dirname(absolute), { recursive: true }); await fs.writeFile(absolute, 'fixture\n'); } } assert.deepEqual(await inspectPortalRuntime(dir), { missing: [], forbidden: [], passed: true, }); await fs.writeFile(path.join(dir, '.env'), 'SECRET=not-real\n'); const unsafe = await inspectPortalRuntime(dir); assert.deepEqual(unsafe.forbidden, ['.env']); assert.equal(unsafe.passed, false); }); }); test('runtime sanitizer removes persisted paths before artifact hashing', async () => { await withTempDir(async (dir) => { const runtime = path.join(dir, '.runtime', 'portal'); await fs.mkdir(path.join(runtime, 'public', 'plaza-covers'), { recursive: true }); await fs.mkdir(path.join(runtime, 'dist', 'dev'), { recursive: true }); await fs.mkdir(path.join(runtime, 'users'), { recursive: true }); await fs.writeFile(path.join(runtime, '.env'), 'SECRET=fixture\n'); await fs.writeFile(path.join(runtime, 'public', 'plaza-covers', 'cover.jpg'), 'fixture'); await fs.writeFile(path.join(runtime, 'dist', 'dev', 'local-demo.html'), 'http://127.0.0.1'); await fs.writeFile(path.join(runtime, 'dist', 'hello-john.html'), 'development fixture'); await fs.writeFile(path.join(runtime, 'users', 'profile.json'), '{}'); await fs.writeFile(path.join(runtime, 'server.mjs'), 'export {};\n'); await removeForbiddenPortalRuntimePaths(runtime); await assert.rejects(() => fs.access(path.join(runtime, '.env'))); await assert.rejects(() => fs.access(path.join(runtime, 'dist', 'dev'))); await assert.rejects(() => fs.access(path.join(runtime, 'dist', 'hello-john.html'))); await assert.rejects(() => fs.access(path.join(runtime, 'users'))); await assert.rejects(() => fs.access(path.join(runtime, 'public', 'plaza-covers'))); await fs.access(path.join(runtime, 'server.mjs')); }); }); test('runtime sanitizer and gate artifact path refuse broad targets', async () => { await withTempDir(async (dir) => { await assert.rejects( () => removeForbiddenPortalRuntimePaths(dir), /Refusing to sanitize/, ); assert.throws( () => assertPortalRuntimePath(dir, { repoRoot: dir }), /must be/, ); assert.equal( assertPortalRuntimePath(path.join(dir, '.runtime', 'portal'), { repoRoot: dir }), path.join(dir, '.runtime', 'portal'), ); }); });