test: harden reporting-governance package boundaries

This commit is contained in:
Eve
2026-05-08 09:11:04 +08:00
parent 145371fd23
commit 87911d16e0
11 changed files with 272 additions and 48 deletions

View File

@@ -14,6 +14,6 @@
"./adapters/orchestrator": "./src/adapters/orchestrator.mjs" "./adapters/orchestrator": "./src/adapters/orchestrator.mjs"
}, },
"scripts": { "scripts": {
"test": "node --test test/package-structure.test.mjs test/policy-evaluator.test.mjs test/decision-runner.test.mjs test/watchdog-chain.integration.test.mjs" "test": "node --test test/package-structure.test.mjs test/policy-evaluator.test.mjs test/decision-runner.test.mjs test/watchdog-chain.integration.test.mjs test/exports-boundary.integration.test.mjs"
} }
} }

View File

@@ -1,17 +1,11 @@
import path from 'node:path';
import process from 'node:process'; import process from 'node:process';
import { spawnSync } from 'node:child_process'; import { spawnSync } from 'node:child_process';
import { createRuntimeBinding, packageRoot, repoRoot, resolveRepoPath } from './runtime-binding.mjs';
const packageRoot = path.resolve(import.meta.dirname, '..', '..');
const repoRoot = path.resolve(packageRoot, '..', '..');
export function resolveRepoPath(...segments) {
return path.join(repoRoot, ...segments);
}
export function runNodeScript(scriptPath, args = [], options = {}) { export function runNodeScript(scriptPath, args = [], options = {}) {
const runtimeBinding = options.runtimeBinding ?? createRuntimeBinding(options);
return spawnSync(process.execPath, [scriptPath, ...args], { return spawnSync(process.execPath, [scriptPath, ...args], {
cwd: repoRoot, cwd: runtimeBinding.cwd,
encoding: 'utf8', encoding: 'utf8',
...options, ...options,
}); });
@@ -32,4 +26,4 @@ export function parseJsonStdout(label, result) {
} }
} }
export { packageRoot, repoRoot }; export { packageRoot, repoRoot, resolveRepoPath };

View File

@@ -1,31 +1,34 @@
import path from 'node:path'; import path from 'node:path';
import { ensureSuccess, parseJsonStdout, resolveRepoPath, runNodeScript } from './_script-runner.mjs'; import { ensureSuccess, parseJsonStdout, runNodeScript } from './_script-runner.mjs';
import { createRuntimeBinding, resolveScriptPath } from './runtime-binding.mjs';
const DEFAULT_SCRIPT = resolveRepoPath('scripts', 'operator_notify_bridge_supervisor.mjs');
const DEFAULT_DISPATCHER_SCRIPT = resolveRepoPath('scripts', 'operator_notify_dispatcher.mjs');
export function runBridgeSupervisorAdapter({ export function runBridgeSupervisorAdapter({
scriptPath = DEFAULT_SCRIPT, scriptPath = null,
runtimeBinding = null,
spoolDir, spoolDir,
queueDir, queueDir,
receiptDir, receiptDir,
dispatcherScript = DEFAULT_DISPATCHER_SCRIPT, dispatcherScript = null,
senderCommand = null, senderCommand = null,
now = null, now = null,
compact = true, compact = true,
dryRun = false, dryRun = false,
} = {}) { } = {}) {
const binding = runtimeBinding ?? createRuntimeBinding();
const resolvedScriptPath = path.resolve(scriptPath ?? resolveScriptPath('bridgeSupervisor', { runtimeBinding: binding }));
const resolvedDispatcherScript = path.resolve(dispatcherScript ?? resolveScriptPath('dispatcher', { runtimeBinding: binding }));
const args = []; const args = [];
if (spoolDir) args.push('--spool-dir', path.resolve(spoolDir)); if (spoolDir) args.push('--spool-dir', path.resolve(spoolDir));
if (queueDir) args.push('--queue-dir', path.resolve(queueDir)); if (queueDir) args.push('--queue-dir', path.resolve(queueDir));
if (receiptDir) args.push('--receipt-dir', path.resolve(receiptDir)); if (receiptDir) args.push('--receipt-dir', path.resolve(receiptDir));
if (dispatcherScript) args.push('--dispatcher-script', path.resolve(dispatcherScript)); if (resolvedDispatcherScript) args.push('--dispatcher-script', resolvedDispatcherScript);
if (senderCommand) args.push('--sender-command', senderCommand); if (senderCommand) args.push('--sender-command', senderCommand);
if (now) args.push('--now', now); if (now) args.push('--now', now);
if (dryRun) args.push('--dry-run'); if (dryRun) args.push('--dry-run');
if (compact) args.push('--compact'); if (compact) args.push('--compact');
const result = runNodeScript(path.resolve(scriptPath), args); const result = runNodeScript(resolvedScriptPath, args, { runtimeBinding: binding });
ensureSuccess('bridge supervisor adapter', result); ensureSuccess('bridge supervisor adapter', result);
return parseJsonStdout('bridge supervisor adapter', result); return parseJsonStdout('bridge supervisor adapter', result);
} }

View File

@@ -1,10 +1,10 @@
import path from 'node:path'; import path from 'node:path';
import { ensureSuccess, parseJsonStdout, resolveRepoPath, runNodeScript } from './_script-runner.mjs'; import { ensureSuccess, parseJsonStdout, runNodeScript } from './_script-runner.mjs';
import { createRuntimeBinding, resolveScriptPath } from './runtime-binding.mjs';
const DEFAULT_SCRIPT = resolveRepoPath('scripts', 'operator_notify_dispatcher.mjs');
export function runDispatcherAdapter({ export function runDispatcherAdapter({
scriptPath = DEFAULT_SCRIPT, scriptPath = null,
runtimeBinding = null,
queueDir, queueDir,
spoolDir, spoolDir,
now = null, now = null,
@@ -14,6 +14,9 @@ export function runDispatcherAdapter({
block = null, block = null,
note = null, note = null,
} = {}) { } = {}) {
const binding = runtimeBinding ?? createRuntimeBinding();
const resolvedScriptPath = path.resolve(scriptPath ?? resolveScriptPath('dispatcher', { runtimeBinding: binding }));
const args = []; const args = [];
if (queueDir) args.push('--queue-dir', path.resolve(queueDir)); if (queueDir) args.push('--queue-dir', path.resolve(queueDir));
if (spoolDir) args.push('--spool-dir', path.resolve(spoolDir)); if (spoolDir) args.push('--spool-dir', path.resolve(spoolDir));
@@ -24,7 +27,7 @@ export function runDispatcherAdapter({
if (note) args.push('--note', note); if (note) args.push('--note', note);
if (compact) args.push('--compact'); if (compact) args.push('--compact');
const result = runNodeScript(path.resolve(scriptPath), args); const result = runNodeScript(resolvedScriptPath, args, { runtimeBinding: binding });
ensureSuccess('dispatcher adapter', result); ensureSuccess('dispatcher adapter', result);
return parseJsonStdout('dispatcher adapter', result); return parseJsonStdout('dispatcher adapter', result);
} }

View File

@@ -1,3 +1,4 @@
export { createRuntimeBinding, resolveScriptPath, SCRIPT_ENV_KEYS, SCRIPT_NAMES } from './runtime-binding.mjs';
export { runWatchdogAdapter } from './watchdog.mjs'; export { runWatchdogAdapter } from './watchdog.mjs';
export { runDispatcherAdapter } from './dispatcher.mjs'; export { runDispatcherAdapter } from './dispatcher.mjs';
export { runBridgeSupervisorAdapter } from './bridge-supervisor.mjs'; export { runBridgeSupervisorAdapter } from './bridge-supervisor.mjs';

View File

@@ -1,22 +1,19 @@
import path from 'node:path'; import path from 'node:path';
import { ensureSuccess, parseJsonStdout, resolveRepoPath, runNodeScript } from './_script-runner.mjs'; import { ensureSuccess, parseJsonStdout, runNodeScript } from './_script-runner.mjs';
import { createRuntimeBinding, resolveScriptPath } from './runtime-binding.mjs';
const DEFAULT_SCRIPT = resolveRepoPath('scripts', 'watchdog_auto_notify_orchestrator.mjs');
const DEFAULT_WATCHDOG_SCRIPT = resolveRepoPath('scripts', 'long_task_watchdog.mjs');
const DEFAULT_DISPATCHER_SCRIPT = resolveRepoPath('scripts', 'operator_notify_dispatcher.mjs');
const DEFAULT_SUPERVISOR_SCRIPT = resolveRepoPath('scripts', 'operator_notify_bridge_supervisor.mjs');
export function runOrchestratorAdapter({ export function runOrchestratorAdapter({
scriptPath = DEFAULT_SCRIPT, scriptPath = null,
runtimeBinding = null,
state, state,
evidenceDir, evidenceDir,
eventDir, eventDir,
queueDir, queueDir,
spoolDir, spoolDir,
receiptDir, receiptDir,
watchdogScript = DEFAULT_WATCHDOG_SCRIPT, watchdogScript = null,
dispatcherScript = DEFAULT_DISPATCHER_SCRIPT, dispatcherScript = null,
supervisorScript = DEFAULT_SUPERVISOR_SCRIPT, supervisorScript = null,
senderCommand = null, senderCommand = null,
senderMode = null, senderMode = null,
openclawBin = 'openclaw', openclawBin = 'openclaw',
@@ -26,6 +23,12 @@ export function runOrchestratorAdapter({
claim = false, claim = false,
dryRun = false, dryRun = false,
} = {}) { } = {}) {
const binding = runtimeBinding ?? createRuntimeBinding();
const resolvedScriptPath = path.resolve(scriptPath ?? resolveScriptPath('orchestrator', { runtimeBinding: binding }));
const resolvedWatchdogScript = path.resolve(watchdogScript ?? resolveScriptPath('watchdog', { runtimeBinding: binding }));
const resolvedDispatcherScript = path.resolve(dispatcherScript ?? resolveScriptPath('dispatcher', { runtimeBinding: binding }));
const resolvedSupervisorScript = path.resolve(supervisorScript ?? resolveScriptPath('bridgeSupervisor', { runtimeBinding: binding }));
const args = []; const args = [];
if (state) args.push('--state', path.resolve(state)); if (state) args.push('--state', path.resolve(state));
if (evidenceDir) args.push('--evidence-dir', path.resolve(evidenceDir)); if (evidenceDir) args.push('--evidence-dir', path.resolve(evidenceDir));
@@ -33,9 +36,9 @@ export function runOrchestratorAdapter({
if (queueDir) args.push('--queue-dir', path.resolve(queueDir)); if (queueDir) args.push('--queue-dir', path.resolve(queueDir));
if (spoolDir) args.push('--spool-dir', path.resolve(spoolDir)); if (spoolDir) args.push('--spool-dir', path.resolve(spoolDir));
if (receiptDir) args.push('--receipt-dir', path.resolve(receiptDir)); if (receiptDir) args.push('--receipt-dir', path.resolve(receiptDir));
if (watchdogScript) args.push('--watchdog-script', path.resolve(watchdogScript)); if (resolvedWatchdogScript) args.push('--watchdog-script', resolvedWatchdogScript);
if (dispatcherScript) args.push('--dispatcher-script', path.resolve(dispatcherScript)); if (resolvedDispatcherScript) args.push('--dispatcher-script', resolvedDispatcherScript);
if (supervisorScript) args.push('--supervisor-script', path.resolve(supervisorScript)); if (resolvedSupervisorScript) args.push('--supervisor-script', resolvedSupervisorScript);
if (senderCommand) args.push('--sender-command', senderCommand); if (senderCommand) args.push('--sender-command', senderCommand);
if (senderMode) args.push('--sender-mode', senderMode); if (senderMode) args.push('--sender-mode', senderMode);
if (openclawBin) args.push('--openclaw-bin', openclawBin); if (openclawBin) args.push('--openclaw-bin', openclawBin);
@@ -45,7 +48,7 @@ export function runOrchestratorAdapter({
if (dryRun) args.push('--dry-run'); if (dryRun) args.push('--dry-run');
if (compact) args.push('--compact'); if (compact) args.push('--compact');
const result = runNodeScript(path.resolve(scriptPath), args); const result = runNodeScript(resolvedScriptPath, args, { runtimeBinding: binding });
ensureSuccess('orchestrator adapter', result); ensureSuccess('orchestrator adapter', result);
return parseJsonStdout('orchestrator adapter', result); return parseJsonStdout('orchestrator adapter', result);
} }

View File

@@ -0,0 +1,59 @@
import path from 'node:path';
import process from 'node:process';
const ENV_PREFIX = 'OPENCLAW_REPORTING_GOVERNANCE_';
const packageRoot = path.resolve(import.meta.dirname, '..', '..');
const repoRoot = path.resolve(packageRoot, '..', '..');
const SCRIPT_NAMES = {
watchdog: 'long_task_watchdog.mjs',
dispatcher: 'operator_notify_dispatcher.mjs',
bridgeSupervisor: 'operator_notify_bridge_supervisor.mjs',
senderBinding: 'operator_notify_sender_binding.mjs',
orchestrator: 'watchdog_auto_notify_orchestrator.mjs',
};
const SCRIPT_ENV_KEYS = {
watchdog: `${ENV_PREFIX}WATCHDOG_SCRIPT`,
dispatcher: `${ENV_PREFIX}DISPATCHER_SCRIPT`,
bridgeSupervisor: `${ENV_PREFIX}BRIDGE_SUPERVISOR_SCRIPT`,
senderBinding: `${ENV_PREFIX}SENDER_BINDING_SCRIPT`,
orchestrator: `${ENV_PREFIX}ORCHESTRATOR_SCRIPT`,
};
function normalizeString(value) {
return typeof value === 'string' && value.trim() ? value.trim() : null;
}
export function resolveRepoPath(...segments) {
return path.join(repoRoot, ...segments);
}
export function createRuntimeBinding(overrides = {}) {
const scripts = {};
for (const [key, fileName] of Object.entries(SCRIPT_NAMES)) {
const envValue = normalizeString(process.env[SCRIPT_ENV_KEYS[key]]);
const overrideValue = normalizeString(overrides?.scripts?.[key]);
scripts[key] = path.resolve(overrideValue ?? envValue ?? resolveRepoPath('scripts', fileName));
}
return {
packageRoot,
repoRoot,
cwd: path.resolve(normalizeString(overrides.cwd) ?? repoRoot),
scripts,
};
}
export function resolveScriptPath(name, options = {}) {
const runtimeBinding = options.runtimeBinding ?? createRuntimeBinding(options);
const scriptPath = runtimeBinding?.scripts?.[name];
if (!scriptPath) {
throw new Error(`unknown runtime binding script: ${name}`);
}
return path.resolve(scriptPath);
}
export { packageRoot, repoRoot, SCRIPT_ENV_KEYS, SCRIPT_NAMES };

View File

@@ -1,10 +1,11 @@
import path from 'node:path'; import path from 'node:path';
import { ensureSuccess, parseJsonStdout, resolveRepoPath, runNodeScript } from './_script-runner.mjs'; import process from 'node:process';
import { ensureSuccess, parseJsonStdout, runNodeScript } from './_script-runner.mjs';
const DEFAULT_SCRIPT = resolveRepoPath('scripts', 'operator_notify_sender_binding.mjs'); import { createRuntimeBinding, resolveScriptPath } from './runtime-binding.mjs';
export function runSenderBindingAdapter({ export function runSenderBindingAdapter({
scriptPath = DEFAULT_SCRIPT, scriptPath = null,
runtimeBinding = null,
mode = 'shim', mode = 'shim',
attemptDir, attemptDir,
openclawBin = 'openclaw', openclawBin = 'openclaw',
@@ -12,16 +13,20 @@ export function runSenderBindingAdapter({
compact = true, compact = true,
env = {}, env = {},
} = {}) { } = {}) {
const binding = runtimeBinding ?? createRuntimeBinding();
const resolvedScriptPath = path.resolve(scriptPath ?? resolveScriptPath('senderBinding', { runtimeBinding: binding }));
const args = ['--mode', mode, '--openclaw-bin', openclawBin]; const args = ['--mode', mode, '--openclaw-bin', openclawBin];
if (attemptDir) args.push('--attempt-dir', path.resolve(attemptDir)); if (attemptDir) args.push('--attempt-dir', path.resolve(attemptDir));
if (now) args.push('--now', now); if (now) args.push('--now', now);
if (compact) args.push('--compact'); if (compact) args.push('--compact');
const result = runNodeScript(path.resolve(scriptPath), args, { const result = runNodeScript(resolvedScriptPath, args, {
env: { env: {
...process.env, ...process.env,
...env, ...env,
}, },
runtimeBinding: binding,
}); });
ensureSuccess('sender binding adapter', result); ensureSuccess('sender binding adapter', result);
return parseJsonStdout('sender binding adapter', result); return parseJsonStdout('sender binding adapter', result);

View File

@@ -1,10 +1,10 @@
import path from 'node:path'; import path from 'node:path';
import { ensureSuccess, parseJsonStdout, resolveRepoPath, runNodeScript } from './_script-runner.mjs'; import { ensureSuccess, parseJsonStdout, runNodeScript } from './_script-runner.mjs';
import { createRuntimeBinding, resolveScriptPath } from './runtime-binding.mjs';
const DEFAULT_SCRIPT = resolveRepoPath('scripts', 'long_task_watchdog.mjs');
export function runWatchdogAdapter({ export function runWatchdogAdapter({
scriptPath = DEFAULT_SCRIPT, scriptPath = null,
runtimeBinding = null,
state, state,
evidenceDir, evidenceDir,
eventDir, eventDir,
@@ -13,6 +13,9 @@ export function runWatchdogAdapter({
compact = true, compact = true,
writeState = false, writeState = false,
} = {}) { } = {}) {
const binding = runtimeBinding ?? createRuntimeBinding();
const resolvedScriptPath = path.resolve(scriptPath ?? resolveScriptPath('watchdog', { runtimeBinding: binding }));
const args = []; const args = [];
if (state) args.push('--state', path.resolve(state)); if (state) args.push('--state', path.resolve(state));
if (evidenceDir) args.push('--evidence-dir', path.resolve(evidenceDir)); if (evidenceDir) args.push('--evidence-dir', path.resolve(evidenceDir));
@@ -22,7 +25,7 @@ export function runWatchdogAdapter({
if (writeState) args.push('--write-state'); if (writeState) args.push('--write-state');
if (compact) args.push('--compact'); if (compact) args.push('--compact');
const result = runNodeScript(path.resolve(scriptPath), args); const result = runNodeScript(resolvedScriptPath, args, { runtimeBinding: binding });
ensureSuccess('watchdog adapter', result); ensureSuccess('watchdog adapter', result);
return parseJsonStdout('watchdog adapter', result); return parseJsonStdout('watchdog adapter', result);
} }

View File

@@ -31,6 +31,10 @@ export const packageBoundaries = {
export { evaluatePolicyPack, evaluatePolicies, planDecisionExecution } from './core/index.mjs'; export { evaluatePolicyPack, evaluatePolicies, planDecisionExecution } from './core/index.mjs';
export { export {
createRuntimeBinding,
resolveScriptPath,
SCRIPT_ENV_KEYS,
SCRIPT_NAMES,
runWatchdogAdapter, runWatchdogAdapter,
runDispatcherAdapter, runDispatcherAdapter,
runBridgeSupervisorAdapter, runBridgeSupervisorAdapter,

View File

@@ -0,0 +1,149 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { spawnSync } from 'node:child_process';
const packageRoot = path.resolve(import.meta.dirname, '..');
function createFixtureRoot() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'reporting-governance-exports-'));
}
function installPackageAlias(root) {
const packageDir = path.join(root, 'node_modules', '@openclaw', 'plugin-reporting-governance');
fs.mkdirSync(path.dirname(packageDir), { recursive: true });
fs.symlinkSync(packageRoot, packageDir, 'dir');
return packageDir;
}
function writeState(root) {
const statePath = path.join(root, 'watchdog-state.json');
fs.writeFileSync(statePath, `${JSON.stringify({
version: 1,
watchdogs: [
{
id: 'reporting-governance-plugin-watchdog',
task: 'reporting-governance plugin spec development',
status: 'active',
ownerSessionKey: 'agent:coder:main',
reportChannel: 'telegram',
reportTarget: '864811879',
intervalMinutes: 10,
lastMilestoneAt: '2026-05-07T08:00:00.000Z',
lastAlertAt: null
}
]
}, null, 2)}\n`, 'utf8');
return statePath;
}
function runNodeEval(root, source, env = {}) {
const result = spawnSync(process.execPath, ['--input-type=module', '--eval', source], {
cwd: root,
encoding: 'utf8',
env: {
...process.env,
...env,
},
});
if (result.status !== 0) {
throw new Error(`node eval failed: ${(result.stderr ?? '').trim() || '(no stderr)'}`);
}
return JSON.parse((result.stdout ?? '').trim());
}
test('package root export resolves public package surface only', () => {
const root = createFixtureRoot();
try {
installPackageAlias(root);
const result = runNodeEval(root, `
import * as plugin from '@openclaw/plugin-reporting-governance';
process.stdout.write(JSON.stringify({
packageName: plugin.packageName,
hasRunWatchdogChain: typeof plugin.runWatchdogChain,
hasPlanDecisionExecution: typeof plugin.planDecisionExecution,
}));
`);
assert.equal(result.packageName, '@openclaw/plugin-reporting-governance');
assert.equal(result.hasRunWatchdogChain, 'function');
assert.equal(result.hasPlanDecisionExecution, 'function');
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('adapters subpath export resolves package-owned adapter index', () => {
const root = createFixtureRoot();
try {
installPackageAlias(root);
const result = runNodeEval(root, `
import * as adapters from '@openclaw/plugin-reporting-governance/adapters';
process.stdout.write(JSON.stringify({
adapterKeys: Object.keys(adapters).sort(),
}));
`);
assert.deepEqual(result.adapterKeys, [
'SCRIPT_ENV_KEYS',
'SCRIPT_NAMES',
'createRuntimeBinding',
'resolveScriptPath',
'runBridgeSupervisorAdapter',
'runDispatcherAdapter',
'runOrchestratorAdapter',
'runSenderBindingAdapter',
'runWatchdogAdapter',
]);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('leaf subpath export resolves and can execute through injected runtime binding', () => {
const root = createFixtureRoot();
try {
installPackageAlias(root);
fs.mkdirSync(path.join(root, 'scripts'), { recursive: true });
fs.mkdirSync(path.join(root, 'events'), { recursive: true });
fs.mkdirSync(path.join(root, 'evidence'), { recursive: true });
fs.mkdirSync(path.join(root, 'queue'), { recursive: true });
const statePath = writeState(root);
const stubScriptPath = path.join(root, 'scripts', 'custom-watchdog.mjs');
fs.writeFileSync(stubScriptPath, `
process.stdout.write(JSON.stringify({
ok: true,
source: 'stub-watchdog',
argv: process.argv.slice(2),
}));
`, 'utf8');
const result = runNodeEval(root, `
import { runWatchdogAdapter } from '@openclaw/plugin-reporting-governance/adapters/watchdog';
const out = runWatchdogAdapter({
state: ${JSON.stringify(statePath)},
evidenceDir: ${JSON.stringify(path.join(root, 'evidence'))},
eventDir: ${JSON.stringify(path.join(root, 'events'))},
notificationDir: ${JSON.stringify(path.join(root, 'queue'))},
runtimeBinding: {
cwd: ${JSON.stringify(root)},
scripts: {
watchdog: ${JSON.stringify(stubScriptPath)},
},
},
now: '2026-05-07T08:20:00.000Z',
});
process.stdout.write(JSON.stringify(out));
`);
assert.equal(result.ok, true);
assert.equal(result.source, 'stub-watchdog');
assert.ok(result.argv.includes('--state'));
assert.ok(result.argv.includes(path.resolve(statePath)));
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});