feat(reporting-governance): wire profile artifacts into contract and orchestrator

This commit is contained in:
Eve
2026-05-08 10:16:29 +08:00
parent 6366f70491
commit 3223feba93
8 changed files with 247 additions and 35 deletions

View File

@@ -10,6 +10,7 @@ Current purpose:
- prepare the next implementation round for evaluator / decision-runner extraction - prepare the next implementation round for evaluator / decision-runner extraction
- provide a minimal package-level policy evaluator and decision runner skeleton that can be verified in isolation - provide a minimal package-level policy evaluator and decision runner skeleton that can be verified in isolation
- add one minimal package-owned deployment profile artifact / loader / binding contract slice that is executable in tests - add one minimal package-owned deployment profile artifact / loader / binding contract slice that is executable in tests
- let profile artifacts drive one real orchestrator adapter entrypoint instead of staying test-only
## Package skeleton ## Package skeleton
@@ -118,9 +119,6 @@ Practical migration rule:
- new integrations should always send a profile artifact or package version pin. - new integrations should always send a profile artifact or package version pin.
- old integrations may temporarily call without one, but should treat returned notes as migration debt. - old integrations may temporarily call without one, but should treat returned notes as migration debt.
Practical migration rule:
- depend on package root exports or declared adapter subpaths only - depend on package root exports or declared adapter subpaths only
- do not couple runtime integrations to repo-private file paths - do not couple runtime integrations to repo-private file paths
- treat capability descriptors and schemas as package artifacts, but not as guaranteed JS import entrypoints unless exported later - treat capability descriptors and schemas as package artifacts, but not as guaranteed JS import entrypoints unless exported later
@@ -151,14 +149,17 @@ This round adds one small but real package artifact path:
- package artifact: `profiles/strict-manager-mode.profile.json` - package artifact: `profiles/strict-manager-mode.profile.json`
- loader: `src/storage/profile-artifact.mjs#loadDeploymentProfileArtifact(...)` - loader: `src/storage/profile-artifact.mjs#loadDeploymentProfileArtifact(...)`
- validator: `src/storage/profile-artifact.mjs#validateDeploymentProfileArtifact(...)`
- binding contract: `src/storage/profile-artifact.mjs#createDeploymentBindingContract(...)` - binding contract: `src/storage/profile-artifact.mjs#createDeploymentBindingContract(...)`
What this slice does: What this slice does:
1. package ships a profile artifact snapshot under package boundary 1. package ships a profile artifact snapshot under package boundary
2. loader resolves that artifact from package-local path 2. loader resolves that artifact from package-local path
3. binding contract translates profile-declared script/artifact roots into concrete repo/runtime paths 3. validator fail-closes minimal boundary drift on `kind`, `apiVersion`, `spec.bindings.entrypoint`, `scripts`, `artifact_roots`, and `spec.package.pluginVersion`
4. adapter runtime binding can be instantiated from that contract in tests 4. binding contract translates profile-declared script/artifact roots into concrete repo/runtime paths
5. adapter runtime binding can be instantiated from that contract in tests
6. orchestrator adapter can now bootstrap from package profile artifact input directly
What this slice does **not** claim yet: What this slice does **not** claim yet:
@@ -169,14 +170,6 @@ What this slice does **not** claim yet:
It is intentionally the smallest verifiable step that proves package profile artifacts are executable inputs rather than documentation only. It is intentionally the smallest verifiable step that proves package profile artifacts are executable inputs rather than documentation only.
## Current reference composition
The current reference composition is the OpenClaw watchdog chain:
```text
watchdog -> queue -> dispatcher -> bridge -> sender binding -> acked|blocked|pending_external_send
```
## Minimal evaluator / decision runner now included ## Minimal evaluator / decision runner now included
The current package now includes a small but runnable `core/` implementation: The current package now includes a small but runnable `core/` implementation:
@@ -195,6 +188,7 @@ Current package-core responsibilities:
- convert a canonical decision into an execution plan, enforcement intent, and receipt skeleton - convert a canonical decision into an execution plan, enforcement intent, and receipt skeleton
- truthfully degrade unsupported enforcement paths based on the capability descriptor - truthfully degrade unsupported enforcement paths based on the capability descriptor
- provide one minimal contract path from `capability descriptor -> policy decision -> execution planning` - provide one minimal contract path from `capability descriptor -> policy decision -> execution planning`
- surface deployment binding metadata when caller passes a validated profile artifact
Still **runtime-adapter responsibility** at this stage: Still **runtime-adapter responsibility** at this stage:
@@ -213,13 +207,15 @@ This slice now has one small but testable contract path:
1. capability descriptor advertises real enforcement support 1. capability descriptor advertises real enforcement support
2. policy evaluator emits a canonical decision from event/evidence/context 2. policy evaluator emits a canonical decision from event/evidence/context
3. decision runner converts that decision into execution planning 3. decision runner converts that decision into execution planning
4. the result declares: 4. validated profile artifact can supply deployment binding metadata
5. orchestrator adapter can consume profile artifact bindings and run one real runtime layer
6. the result declares:
- adapter-dispatch actions required - adapter-dispatch actions required
- package-core actions possible locally - package-core actions possible locally
- blocked mandatory actions when capability support is missing - blocked mandatory actions when capability support is missing
- truthful delivery / receipt state - truthful delivery / receipt state
This is intentionally **planning-level end-to-end**, not full live inline interception. This is intentionally **planning-level end-to-end plus one adapter bootstrap layer**, not full live inline interception.
It proves contract alignment without pretending all runtime enforcement is already extracted. It proves contract alignment without pretending all runtime enforcement is already extracted.
## Not yet included ## Not yet included
@@ -232,4 +228,4 @@ This package still does **not** claim full implementation of:
- complete rewrite / placeholder / review / status-downgrade adapter execution - complete rewrite / placeholder / review / status-downgrade adapter execution
- non-watchdog full runtime governance interception - non-watchdog full runtime governance interception
It now provides the first package-mainline evaluator / decision-runner core, a compatibility-envelope boundary, and a minimal package profile artifact/binding slice, but the remaining enforcement surface is still intentionally honest about adapter gaps. It now provides the first package-mainline evaluator / decision-runner core, a compatibility-envelope boundary, a minimal package profile artifact/binding slice, and one profile-driven orchestrator path, but the remaining enforcement surface is still intentionally honest about adapter gaps.

View File

@@ -1,10 +1,15 @@
import path from 'node:path'; import path from 'node:path';
import { ensureSuccess, parseJsonStdout, runNodeScript } from './_script-runner.mjs'; import { ensureSuccess, parseJsonStdout, runNodeScript } from './_script-runner.mjs';
import { createRuntimeBinding, resolveScriptPath } from './runtime-binding.mjs'; import { createRuntimeBinding, resolveScriptPath } from './runtime-binding.mjs';
import { loadDeploymentProfileArtifact, createDeploymentBindingContract } from '../storage/profile-artifact.mjs';
export function runOrchestratorAdapter({ export function runOrchestratorAdapter({
scriptPath = null, scriptPath = null,
runtimeBinding = null, runtimeBinding = null,
profileArtifact = null,
profileArtifactPath = null,
profileId = null,
repoRootOverride = null,
state, state,
evidenceDir, evidenceDir,
eventDir, eventDir,
@@ -23,8 +28,18 @@ export function runOrchestratorAdapter({
claim = false, claim = false,
dryRun = false, dryRun = false,
} = {}) { } = {}) {
const binding = runtimeBinding ?? createRuntimeBinding(); const deploymentBinding = profileArtifact || profileArtifactPath || profileId
const resolvedScriptPath = path.resolve(scriptPath ?? resolveScriptPath('orchestrator', { runtimeBinding: binding })); ? createDeploymentBindingContract({
artifact: profileArtifact ?? loadDeploymentProfileArtifact({ artifactPath: profileArtifactPath, profileId }).artifact,
repoRootOverride,
})
: null;
const binding = runtimeBinding ?? createRuntimeBinding({
cwd: repoRootOverride,
scripts: deploymentBinding?.scripts,
});
const resolvedScriptPath = path.resolve(scriptPath ?? deploymentBinding?.entrypoint ?? resolveScriptPath('orchestrator', { runtimeBinding: binding }));
const resolvedWatchdogScript = path.resolve(watchdogScript ?? resolveScriptPath('watchdog', { runtimeBinding: binding })); const resolvedWatchdogScript = path.resolve(watchdogScript ?? resolveScriptPath('watchdog', { runtimeBinding: binding }));
const resolvedDispatcherScript = path.resolve(dispatcherScript ?? resolveScriptPath('dispatcher', { runtimeBinding: binding })); const resolvedDispatcherScript = path.resolve(dispatcherScript ?? resolveScriptPath('dispatcher', { runtimeBinding: binding }));
const resolvedSupervisorScript = path.resolve(supervisorScript ?? resolveScriptPath('bridgeSupervisor', { runtimeBinding: binding })); const resolvedSupervisorScript = path.resolve(supervisorScript ?? resolveScriptPath('bridgeSupervisor', { runtimeBinding: binding }));

View File

@@ -1,6 +1,7 @@
import { evaluatePolicies } from './policy-evaluator.mjs'; import { evaluatePolicies } from './policy-evaluator.mjs';
import { runCompatibilityPreflight } from './compatibility-preflight.mjs'; import { runCompatibilityPreflight } from './compatibility-preflight.mjs';
import { planDecisionExecution } from './decision-runner.mjs'; import { planDecisionExecution } from './decision-runner.mjs';
import { createDeploymentBindingContract } from '../storage/profile-artifact.mjs';
function createBlockedReceipt({ evaluation, preflight }) { function createBlockedReceipt({ evaluation, preflight }) {
return { return {
@@ -57,6 +58,7 @@ export function executeGovernanceContract({
context = {}, context = {},
profile = {}, profile = {},
packageVersion, packageVersion,
repoRootOverride,
} = {}) { } = {}) {
const evaluation = evaluatePolicies({ const evaluation = evaluatePolicies({
event, event,
@@ -81,10 +83,15 @@ export function executeGovernanceContract({
capabilityDescriptor, capabilityDescriptor,
}); });
const deploymentBinding = profile?.spec?.bindings
? createDeploymentBindingContract({ artifact: profile, repoRootOverride })
: null;
return { return {
evaluation, evaluation,
preflight, preflight,
planning, planning,
deploymentBinding,
contract: { contract: {
runtime: capabilityDescriptor?.metadata?.id ?? capabilityDescriptor?.runtime?.name ?? 'unknown-runtime', runtime: capabilityDescriptor?.metadata?.id ?? capabilityDescriptor?.runtime?.name ?? 'unknown-runtime',
policy_id: evaluation.decision.policy_id, policy_id: evaluation.decision.policy_id,

View File

@@ -29,11 +29,11 @@ scripts/long_task_watchdog.mjs
## Package mapping ## Package mapping
- `src/adapters/watchdog-adapter.mjs` → watchdog trigger + canonical event seeding - `src/adapters/watchdog.mjs` → watchdog trigger + canonical event seeding
- `src/adapters/dispatcher-adapter.mjs` → queue to spool handoff - `src/adapters/dispatcher.mjs` → queue to spool handoff
- `src/adapters/bridge-adapter.mjs` → spool consumption + receipt writeback - `src/adapters/bridge-supervisor.mjs` → spool consumption + receipt writeback
- `src/adapters/sender-binding-adapter.mjs` → sender contract boundary - `src/adapters/sender-binding.mjs` → sender contract boundary
- `src/adapters/orchestrator-adapter.mjs` → deterministic composition entrypoint - `src/adapters/orchestrator.mjs` → deterministic composition entrypoint
## Runtime artifact classes ## Runtime artifact classes

View File

@@ -3,11 +3,58 @@ import path from 'node:path';
const packageRoot = path.resolve(import.meta.dirname, '..', '..'); const packageRoot = path.resolve(import.meta.dirname, '..', '..');
const repoRoot = path.resolve(packageRoot, '..', '..'); const repoRoot = path.resolve(packageRoot, '..', '..');
const EXPECTED_KIND = 'DeploymentProfileArtifact';
const EXPECTED_API_VERSION = 'reporting-governance/v1alpha1';
function readJsonFile(filePath) { function readJsonFile(filePath) {
return JSON.parse(fs.readFileSync(filePath, 'utf8')); return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} }
function assertNonEmptyString(value, label) {
if (typeof value !== 'string' || value.trim() === '') {
throw new Error(`${label} must be a non-empty string`);
}
return value.trim();
}
function assertObjectRecord(value, label) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error(`${label} must be an object record`);
}
return value;
}
export function validateDeploymentProfileArtifact(artifact) {
if (!artifact || typeof artifact !== 'object' || Array.isArray(artifact)) {
throw new Error('deployment profile artifact must be an object');
}
if (artifact.kind !== EXPECTED_KIND) {
throw new Error(`deployment profile artifact kind must be ${EXPECTED_KIND}`);
}
if (artifact.apiVersion !== EXPECTED_API_VERSION) {
throw new Error(`deployment profile artifact apiVersion must be ${EXPECTED_API_VERSION}`);
}
const bindings = artifact?.spec?.bindings;
if (!bindings || typeof bindings !== 'object' || Array.isArray(bindings)) {
throw new Error('deployment profile artifact bindings are required');
}
assertNonEmptyString(bindings.entrypoint, 'deployment profile artifact spec.bindings.entrypoint');
const scripts = assertObjectRecord(bindings.scripts, 'deployment profile artifact spec.bindings.scripts');
const artifactRoots = assertObjectRecord(bindings.artifact_roots, 'deployment profile artifact spec.bindings.artifact_roots');
assertNonEmptyString(artifact?.spec?.package?.pluginVersion, 'deployment profile artifact spec.package.pluginVersion');
for (const [key, relativePath] of Object.entries(scripts)) {
assertNonEmptyString(relativePath, `deployment profile artifact spec.bindings.scripts.${key}`);
}
for (const [key, relativePath] of Object.entries(artifactRoots)) {
assertNonEmptyString(relativePath, `deployment profile artifact spec.bindings.artifact_roots.${key}`);
}
return artifact;
}
export function resolvePackageArtifactPath(...segments) { export function resolvePackageArtifactPath(...segments) {
return path.resolve(packageRoot, ...segments); return path.resolve(packageRoot, ...segments);
} }
@@ -17,7 +64,7 @@ export function loadDeploymentProfileArtifact({ artifactPath, profileId } = {})
artifactPath artifactPath
?? resolvePackageArtifactPath('profiles', `${profileId ?? 'strict-manager-mode'}.profile.json`) ?? resolvePackageArtifactPath('profiles', `${profileId ?? 'strict-manager-mode'}.profile.json`)
); );
const artifact = readJsonFile(resolvedPath); const artifact = validateDeploymentProfileArtifact(readJsonFile(resolvedPath));
return { return {
artifactPath: resolvedPath, artifactPath: resolvedPath,
artifact, artifact,
@@ -25,30 +72,30 @@ export function loadDeploymentProfileArtifact({ artifactPath, profileId } = {})
} }
export function createDeploymentBindingContract({ artifact, repoRootOverride } = {}) { export function createDeploymentBindingContract({ artifact, repoRootOverride } = {}) {
if (!artifact?.spec?.bindings) { const validatedArtifact = validateDeploymentProfileArtifact(artifact);
throw new Error('deployment profile artifact bindings are required');
}
const root = path.resolve(repoRootOverride ?? repoRoot); const root = path.resolve(repoRootOverride ?? repoRoot);
const scripts = Object.fromEntries( const scripts = Object.fromEntries(
Object.entries(artifact.spec.bindings.scripts ?? {}).map(([key, relativePath]) => [key, path.resolve(root, relativePath)]) Object.entries(validatedArtifact.spec.bindings.scripts).map(([key, relativePath]) => [key, path.resolve(root, relativePath)])
); );
const artifactRoots = Object.fromEntries( const artifactRoots = Object.fromEntries(
Object.entries(artifact.spec.bindings.artifact_roots ?? {}).map(([key, relativePath]) => [key, path.resolve(root, relativePath)]) Object.entries(validatedArtifact.spec.bindings.artifact_roots).map(([key, relativePath]) => [key, path.resolve(root, relativePath)])
); );
return { return {
runtime: artifact.spec.bindings.runtime ?? artifact.metadata?.runtime ?? 'unknown-runtime', runtime: validatedArtifact.spec.bindings.runtime ?? validatedArtifact.metadata?.runtime ?? 'unknown-runtime',
entrypoint: path.resolve(root, artifact.spec.bindings.entrypoint), entrypoint: path.resolve(root, validatedArtifact.spec.bindings.entrypoint),
pluginVersion: artifact.spec?.package?.pluginVersion ?? null, pluginVersion: validatedArtifact.spec.package.pluginVersion,
compatibilityMode: artifact.metadata?.compatibility_mode ?? 'strict_envelope', compatibilityMode: validatedArtifact.metadata?.compatibility_mode ?? 'strict_envelope',
scripts, scripts,
artifactRoots, artifactRoots,
}; };
} }
export const __testables = { export const __testables = {
EXPECTED_KIND,
EXPECTED_API_VERSION,
packageRoot, packageRoot,
repoRoot, repoRoot,
readJsonFile, readJsonFile,
validateDeploymentProfileArtifact,
}; };

View File

@@ -1,9 +1,13 @@
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import path from 'node:path';
import { executeGovernanceContract, runCompatibilityPreflight } from '../src/core/index.mjs'; import { executeGovernanceContract, runCompatibilityPreflight } from '../src/core/index.mjs';
import capabilityDescriptor from '../capabilities/openclaw-watchdog-reference.json' with { type: 'json' }; import capabilityDescriptor from '../capabilities/openclaw-watchdog-reference.json' with { type: 'json' };
const packageRoot = path.resolve(import.meta.dirname, '..');
const repoRoot = path.resolve(packageRoot, '..', '..');
const noSilencePack = { const noSilencePack = {
metadata: { id: 'no-silence', severity_default: 'high' }, metadata: { id: 'no-silence', severity_default: 'high' },
spec: { spec: {
@@ -294,3 +298,53 @@ test('schema/version mismatch blocks contract before any runnable plan is produc
assert.ok(result.planning.receipt.notes.some((note) => note.includes('schema mismatch: decision_schema'))); assert.ok(result.planning.receipt.notes.some((note) => note.includes('schema mismatch: decision_schema')));
assert.ok(result.planning.receipt.notes.some((note) => note.includes('plugin version 0.1.0-mainline is not declared compatible'))); assert.ok(result.planning.receipt.notes.some((note) => note.includes('plugin version 0.1.0-mainline is not declared compatible')));
}); });
test('executeGovernanceContract exposes deployment binding when profile artifact bindings are provided', () => {
const profileArtifact = {
...strictProfile,
kind: 'DeploymentProfileArtifact',
apiVersion: 'reporting-governance/v1alpha1',
metadata: {
...strictProfile.metadata,
runtime: 'openclaw',
compatibility_mode: 'strict_envelope',
},
spec: {
...strictProfile.spec,
bindings: {
runtime: 'openclaw',
entrypoint: 'scripts/watchdog_auto_notify_orchestrator.mjs',
scripts: {
watchdog: 'scripts/long_task_watchdog.mjs',
dispatcher: 'scripts/operator_notify_dispatcher.mjs',
bridgeSupervisor: 'scripts/operator_notify_bridge_supervisor.mjs',
senderBinding: 'scripts/operator_notify_sender_binding.mjs',
orchestrator: 'scripts/watchdog_auto_notify_orchestrator.mjs'
},
artifact_roots: {
queueItems: 'state/operator-notify-queue'
}
}
}
};
const result = executeGovernanceContract({
event: {
type: 'silence_timeout',
payload: { checkpoint_overdue: true }
},
capabilityDescriptor,
policyPacks: [noSilencePack],
context: {
signals: ['checkpoint_overdue']
},
profile: profileArtifact,
packageVersion: '0.1.0-mainline',
repoRootOverride: repoRoot,
});
assert.equal(result.preflight.status, 'pass');
assert.equal(result.deploymentBinding.entrypoint, path.resolve(repoRoot, 'scripts/watchdog_auto_notify_orchestrator.mjs'));
assert.equal(result.deploymentBinding.scripts.dispatcher, path.resolve(repoRoot, 'scripts/operator_notify_dispatcher.mjs'));
assert.equal(result.deploymentBinding.artifactRoots.queueItems, path.resolve(repoRoot, 'state/operator-notify-queue'));
});

View File

@@ -3,7 +3,11 @@ import assert from 'node:assert/strict';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { loadDeploymentProfileArtifact, createDeploymentBindingContract } from '../src/storage/profile-artifact.mjs'; import {
loadDeploymentProfileArtifact,
createDeploymentBindingContract,
validateDeploymentProfileArtifact,
} from '../src/storage/profile-artifact.mjs';
import { createRuntimeBinding } from '../src/adapters/index.mjs'; import { createRuntimeBinding } from '../src/adapters/index.mjs';
const packageRoot = path.resolve(import.meta.dirname, '..'); const packageRoot = path.resolve(import.meta.dirname, '..');
@@ -45,3 +49,58 @@ test('runtime binding can be instantiated from profile artifact binding contract
assert.equal(runtimeBinding.scripts.bridgeSupervisor, contract.scripts.bridgeSupervisor); assert.equal(runtimeBinding.scripts.bridgeSupervisor, contract.scripts.bridgeSupervisor);
assert.equal(runtimeBinding.scripts.senderBinding, contract.scripts.senderBinding); 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({
kind: 'DeploymentProfileArtifact',
apiVersion: 'reporting-governance/v1alpha1',
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({
kind: 'DeploymentProfileArtifact',
apiVersion: 'reporting-governance/v1alpha1',
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({
kind: 'DeploymentProfileArtifact',
apiVersion: 'reporting-governance/v1alpha1',
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/
);
});

View File

@@ -9,6 +9,8 @@ import {
runWatchdogChain, runWatchdogChain,
} from '../src/index.mjs'; } from '../src/index.mjs';
const packageRoot = path.resolve(import.meta.dirname, '..');
function createFixtureRoot() { function createFixtureRoot() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'reporting-governance-plugin-')); return fs.mkdtempSync(path.join(os.tmpdir(), 'reporting-governance-plugin-'));
} }
@@ -115,3 +117,35 @@ test('dry-run path produces verifiable pending receipt via package adapter', ()
fs.rmSync(root, { recursive: true, force: true }); fs.rmSync(root, { recursive: true, force: true });
} }
}); });
test('orchestrator adapter can bootstrap from profile artifact loader path', () => {
const root = createFixtureRoot();
try {
mkdirs(root, ['evidence', 'events', 'queue', 'spool', 'receipts']);
const statePath = writeState(root);
const result = runOrchestratorAdapter({
profileId: 'strict-manager-mode',
repoRootOverride: path.resolve(packageRoot, '..', '..'),
state: statePath,
evidenceDir: path.join(root, 'evidence'),
eventDir: path.join(root, 'events'),
queueDir: path.join(root, 'queue'),
spoolDir: path.join(root, 'spool'),
receiptDir: path.join(root, 'receipts'),
writeState: true,
dryRun: true,
now: '2026-05-07T08:20:00.000Z',
});
assert.equal(result.ok, true);
assert.equal(result.result.watchdog.notificationCount, 1);
assert.equal(result.result.dispatcher.dispatchedCount, 1);
assert.equal(result.result.supervisor.pendingCount, 1);
const receipt = readSingleJson(path.join(root, 'receipts')).payload;
assert.equal(receipt.state, 'pending_external_send');
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});