import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { loadDeploymentProfileArtifact, createDeploymentBindingContract, validateDeploymentProfileArtifact, assertUseTimePathWithinRepoRoot, } from '../src/storage/profile-artifact.mjs'; import { createRuntimeBinding } from '../src/adapters/index.mjs'; const packageRoot = path.resolve(import.meta.dirname, '..'); const repoRoot = path.resolve(packageRoot, '..', '..'); function createArtifact(overrides = {}) { return { kind: 'DeploymentProfileArtifact', apiVersion: 'reporting-governance/v1alpha1', spec: { package: { pluginVersion: '0.1.0-mainline' }, bindings: { runtime: 'openclaw', entrypoint: 'scripts/watchdog_auto_notify_orchestrator.mjs', scripts: { watchdog: 'scripts/long_task_watchdog.mjs' }, artifact_roots: { queueItems: 'state/operator-notify-queue' }, }, }, ...overrides, }; } test('deployment profile artifact loads from package profiles and preserves compatibility envelope metadata', () => { const { artifactPath, artifact } = loadDeploymentProfileArtifact({ profileId: 'strict-manager-mode' }); assert.equal(path.relative(packageRoot, artifactPath), path.join('profiles', 'strict-manager-mode.profile.json')); assert.equal(artifact.kind, 'DeploymentProfileArtifact'); assert.equal(artifact.metadata.id, 'strict-manager-mode'); assert.equal(artifact.metadata.compatibility_mode, 'strict_envelope'); assert.equal(artifact.spec.package.pluginVersion, '0.1.0-mainline'); }); test('deployment binding contract resolves package artifact into real repo script and artifact paths', () => { const { artifact } = loadDeploymentProfileArtifact({ profileId: 'strict-manager-mode' }); const binding = createDeploymentBindingContract({ artifact }); assert.equal(binding.runtime, 'openclaw'); assert.equal(binding.pluginVersion, '0.1.0-mainline'); assert.equal(binding.compatibilityMode, 'strict_envelope'); assert.equal(binding.entrypoint, path.resolve(repoRoot, 'scripts/watchdog_auto_notify_orchestrator.mjs')); assert.equal(binding.scripts.watchdog, path.resolve(repoRoot, 'scripts/long_task_watchdog.mjs')); assert.equal(binding.artifactRoots.queueItems, path.resolve(repoRoot, 'state/operator-notify-queue')); assert.equal(fs.existsSync(binding.scripts.orchestrator), true); }); test('runtime binding can be instantiated from profile artifact binding contract', () => { const { artifact } = loadDeploymentProfileArtifact({ profileId: 'strict-manager-mode' }); const contract = createDeploymentBindingContract({ artifact }); const runtimeBinding = createRuntimeBinding({ cwd: repoRoot, scripts: contract.scripts, }); assert.equal(runtimeBinding.cwd, repoRoot); assert.equal(runtimeBinding.scripts.dispatcher, contract.scripts.dispatcher); assert.equal(runtimeBinding.scripts.bridgeSupervisor, contract.scripts.bridgeSupervisor); assert.equal(runtimeBinding.scripts.senderBinding, contract.scripts.senderBinding); }); test('deployment profile artifact validation fails closed on boundary drift', () => { assert.throws( () => validateDeploymentProfileArtifact({}), /kind must be DeploymentProfileArtifact/ ); assert.throws( () => validateDeploymentProfileArtifact(createArtifact({ spec: { package: { pluginVersion: '0.1.0-mainline' }, bindings: { entrypoint: '', scripts: { watchdog: 'scripts/long_task_watchdog.mjs' }, artifact_roots: { queueItems: 'state/operator-notify-queue' }, }, }, })), /spec\.bindings\.entrypoint must be a non-empty string/ ); assert.throws( () => validateDeploymentProfileArtifact(createArtifact({ spec: { package: { pluginVersion: '' }, bindings: { entrypoint: 'scripts/watchdog_auto_notify_orchestrator.mjs', scripts: { watchdog: '' }, artifact_roots: { queueItems: 'state/operator-notify-queue' }, }, }, })), /spec\.package\.pluginVersion must be a non-empty string/ ); assert.throws( () => validateDeploymentProfileArtifact(createArtifact({ spec: { package: { pluginVersion: '0.1.0-mainline' }, bindings: { entrypoint: 'scripts/watchdog_auto_notify_orchestrator.mjs', scripts: [], artifact_roots: { queueItems: 'state/operator-notify-queue' }, }, }, })), /spec\.bindings\.scripts must be an object record/ ); }); test('deployment profile artifact validation rejects absolute binding paths', () => { assert.throws( () => validateDeploymentProfileArtifact(createArtifact({ spec: { package: { pluginVersion: '0.1.0-mainline' }, bindings: { entrypoint: '/abs/path', scripts: { watchdog: 'scripts/long_task_watchdog.mjs' }, artifact_roots: { queueItems: 'state/operator-notify-queue' }, }, }, })), /spec\.bindings\.entrypoint must stay within repo root: absolute paths are not allowed/ ); assert.throws( () => validateDeploymentProfileArtifact(createArtifact({ spec: { package: { pluginVersion: '0.1.0-mainline' }, bindings: { entrypoint: 'scripts/watchdog_auto_notify_orchestrator.mjs', scripts: { watchdog: '/abs/path' }, artifact_roots: { queueItems: 'state/operator-notify-queue' }, }, }, })), /spec\.bindings\.scripts\.watchdog must stay within repo root: absolute paths are not allowed/ ); assert.throws( () => validateDeploymentProfileArtifact(createArtifact({ spec: { package: { pluginVersion: '0.1.0-mainline' }, bindings: { entrypoint: 'scripts/watchdog_auto_notify_orchestrator.mjs', scripts: { watchdog: 'scripts/long_task_watchdog.mjs' }, artifact_roots: { queueItems: '/abs/path' }, }, }, })), /spec\.bindings\.artifact_roots\.queueItems must stay within repo root: absolute paths are not allowed/ ); }); test('deployment profile artifact validation rejects escape paths after resolution', () => { assert.throws( () => validateDeploymentProfileArtifact(createArtifact({ spec: { package: { pluginVersion: '0.1.0-mainline' }, bindings: { entrypoint: '../../escape', scripts: { watchdog: 'scripts/long_task_watchdog.mjs' }, artifact_roots: { queueItems: 'state/operator-notify-queue' }, }, }, })), /spec\.bindings\.entrypoint must stay within repo root: path escapes root boundary/ ); assert.throws( () => createDeploymentBindingContract({ artifact: createArtifact({ spec: { package: { pluginVersion: '0.1.0-mainline' }, bindings: { entrypoint: 'scripts/watchdog_auto_notify_orchestrator.mjs', scripts: { watchdog: '../../escape' }, artifact_roots: { queueItems: 'state/operator-notify-queue' }, }, }, }), repoRootOverride: repoRoot, }), /spec\.bindings\.scripts\.watchdog must stay within repo root: path escapes root boundary/ ); assert.throws( () => validateDeploymentProfileArtifact(createArtifact({ spec: { package: { pluginVersion: '0.1.0-mainline' }, bindings: { entrypoint: 'scripts/watchdog_auto_notify_orchestrator.mjs', scripts: { watchdog: 'scripts/long_task_watchdog.mjs' }, artifact_roots: { queueItems: '../escape' }, }, }, })), /spec\.bindings\.artifact_roots\.queueItems must stay within repo root: path escapes root boundary/ ); }); test('deployment binding contract allows normalized in-root paths that contain dot segments', () => { const binding = createDeploymentBindingContract({ artifact: createArtifact({ spec: { package: { pluginVersion: '0.1.0-mainline' }, bindings: { runtime: 'openclaw', entrypoint: 'scripts/../scripts/watchdog_auto_notify_orchestrator.mjs', scripts: { watchdog: 'scripts/./long_task_watchdog.mjs', }, artifact_roots: { queueItems: 'state/../state/operator-notify-queue', }, }, }, }), repoRootOverride: repoRoot, }); assert.equal(binding.entrypoint, path.resolve(repoRoot, 'scripts/watchdog_auto_notify_orchestrator.mjs')); assert.equal(binding.scripts.watchdog, path.resolve(repoRoot, 'scripts/long_task_watchdog.mjs')); assert.equal(binding.artifactRoots.queueItems, path.resolve(repoRoot, 'state/operator-notify-queue')); }); test('deployment profile artifact validation rejects artifact_roots symlink escape after realpath resolution', async (t) => { const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'reporting-governance-profile-artifact-')); t.after(() => fs.rmSync(sandbox, { recursive: true, force: true })); const fakeRepoRoot = path.join(sandbox, 'repo'); const outsideRoot = path.join(sandbox, 'outside'); fs.mkdirSync(fakeRepoRoot, { recursive: true }); fs.mkdirSync(outsideRoot, { recursive: true }); fs.symlinkSync(outsideRoot, path.join(fakeRepoRoot, 'state-link'), 'dir'); fs.writeFileSync(path.join(fakeRepoRoot, 'entry.mjs'), 'export default true;\n'); fs.writeFileSync(path.join(fakeRepoRoot, 'watchdog.mjs'), 'export default true;\n'); assert.throws( () => validateDeploymentProfileArtifact(createArtifact({ spec: { package: { pluginVersion: '0.1.0-mainline' }, bindings: { entrypoint: 'entry.mjs', scripts: { watchdog: 'watchdog.mjs' }, artifact_roots: { queueItems: 'state-link/queue' }, }, }, }), { repoRootOverride: fakeRepoRoot }), /spec\.bindings\.artifact_roots\.queueItems must stay within repo root: symlink resolution escapes realpath boundary/ ); }); test('deployment profile artifact validation rejects entrypoint and scripts symlink escapes after realpath resolution', async (t) => { const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'reporting-governance-profile-artifact-scripts-')); t.after(() => fs.rmSync(sandbox, { recursive: true, force: true })); const fakeRepoRoot = path.join(sandbox, 'repo'); const outsideRoot = path.join(sandbox, 'outside'); fs.mkdirSync(fakeRepoRoot, { recursive: true }); fs.mkdirSync(outsideRoot, { recursive: true }); fs.symlinkSync(outsideRoot, path.join(fakeRepoRoot, 'scripts-link'), 'dir'); fs.writeFileSync(path.join(fakeRepoRoot, 'safe-watchdog.mjs'), `export default true;\n`); fs.writeFileSync(path.join(outsideRoot, 'entry.mjs'), `export default true;\n`); fs.writeFileSync(path.join(fakeRepoRoot, 'entry.mjs'), `export default true;\n`); fs.writeFileSync(path.join(outsideRoot, 'watchdog.mjs'), `export default true;\n`); assert.throws( () => validateDeploymentProfileArtifact(createArtifact({ spec: { package: { pluginVersion: '0.1.0-mainline' }, bindings: { entrypoint: 'scripts-link/entry.mjs', scripts: { watchdog: 'safe-watchdog.mjs' }, artifact_roots: { queueItems: 'state/operator-notify-queue' }, }, }, }), { repoRootOverride: fakeRepoRoot }), /spec\.bindings\.entrypoint must stay within repo root: symlink resolution escapes realpath boundary/ ); assert.throws( () => validateDeploymentProfileArtifact(createArtifact({ spec: { package: { pluginVersion: '0.1.0-mainline' }, bindings: { entrypoint: 'entry.mjs', scripts: { watchdog: 'scripts-link/watchdog.mjs' }, artifact_roots: { queueItems: 'state/operator-notify-queue' }, }, }, }), { repoRootOverride: fakeRepoRoot }), /spec\.bindings\.scripts\.watchdog must stay within repo root: symlink resolution escapes realpath boundary/ ); }); test('use-time repo-root boundary check rejects symlink escapes for missing artifact leaf', async (t) => { const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'reporting-governance-use-time-boundary-')); t.after(() => fs.rmSync(sandbox, { recursive: true, force: true })); const fakeRepoRoot = path.join(sandbox, 'repo'); const outsideRoot = path.join(sandbox, 'outside'); fs.mkdirSync(fakeRepoRoot, { recursive: true }); fs.mkdirSync(outsideRoot, { recursive: true }); fs.symlinkSync(outsideRoot, path.join(fakeRepoRoot, 'queue-link'), 'dir'); assert.throws( () => assertUseTimePathWithinRepoRoot(path.join(fakeRepoRoot, 'queue-link', 'pending'), 'orchestrator adapter queueDir', { repoRootOverride: fakeRepoRoot, allowMissingLeaf: true, }), /orchestrator adapter queueDir must stay within repo root: symlink resolution escapes realpath boundary/ ); });