feat(reporting-governance): schema-validate profile generation
This commit is contained in:
84
plugins/reporting-governance/package-lock.json
generated
Normal file
84
plugins/reporting-governance/package-lock.json
generated
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
{
|
||||||
|
"name": "@openclaw/plugin-reporting-governance",
|
||||||
|
"version": "0.1.0-mainline",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@openclaw/plugin-reporting-governance",
|
||||||
|
"version": "0.1.0-mainline",
|
||||||
|
"dependencies": {
|
||||||
|
"ajv": "^8.20.0",
|
||||||
|
"yaml": "^2.8.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ajv": {
|
||||||
|
"version": "8.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
|
||||||
|
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"fast-uri": "^3.0.1",
|
||||||
|
"json-schema-traverse": "^1.0.0",
|
||||||
|
"require-from-string": "^2.0.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fast-deep-equal": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/fast-uri": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fastify"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fastify"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/json-schema-traverse": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/require-from-string": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yaml": {
|
||||||
|
"version": "2.8.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz",
|
||||||
|
"integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"yaml": "bin.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/eemeli"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,5 +15,9 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node --test test/package-structure.test.mjs test/policy-evaluator.test.mjs test/compatibility-preflight.test.mjs test/profile-artifact.test.mjs test/profile-generator.test.mjs test/decision-runner.test.mjs test/governance-contract.integration.test.mjs test/watchdog-chain.integration.test.mjs test/runtime-integrated.integration.test.mjs test/exports-boundary.integration.test.mjs"
|
"test": "node --test test/package-structure.test.mjs test/policy-evaluator.test.mjs test/compatibility-preflight.test.mjs test/profile-artifact.test.mjs test/profile-generator.test.mjs test/decision-runner.test.mjs test/governance-contract.integration.test.mjs test/watchdog-chain.integration.test.mjs test/runtime-integrated.integration.test.mjs test/exports-boundary.integration.test.mjs"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ajv": "^8.20.0",
|
||||||
|
"yaml": "^2.8.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,170 +1,65 @@
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import Ajv2020 from 'ajv/dist/2020.js';
|
||||||
|
import YAML from 'yaml';
|
||||||
|
|
||||||
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 schemaPath = '../../../schemas/reporting-governance/deployment-profile.schema.json';
|
const schemaPath = path.resolve(repoRoot, 'schemas', 'reporting-governance', 'deployment-profile.schema.json');
|
||||||
|
const artifactSchemaRefPath = '../../../schemas/reporting-governance/deployment-profile.schema.json';
|
||||||
|
|
||||||
function readText(filePath) {
|
function readText(filePath) {
|
||||||
return fs.readFileSync(filePath, 'utf8');
|
return fs.readFileSync(filePath, 'utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
function countIndent(line) {
|
function loadDeploymentProfileSchema() {
|
||||||
let count = 0;
|
return JSON.parse(readText(schemaPath));
|
||||||
while (count < line.length && line[count] === ' ') count += 1;
|
|
||||||
return count;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseScalar(raw) {
|
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
||||||
const value = raw.trim();
|
const validateDeploymentProfile = ajv.compile(loadDeploymentProfileSchema());
|
||||||
if (value === 'true') return true;
|
|
||||||
if (value === 'false') return false;
|
function formatAjvErrors(errors = []) {
|
||||||
if (/^-?\d+$/.test(value)) return Number.parseInt(value, 10);
|
if (!Array.isArray(errors) || errors.length === 0) {
|
||||||
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
return 'schema validation failed';
|
||||||
return value.slice(1, -1);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseBlock(lines, startIndex = 0, baseIndent = 0) {
|
return errors.map((error) => {
|
||||||
const firstMeaningful = lines.slice(startIndex).find((line) => line.trim() !== '');
|
const keyword = error.keyword;
|
||||||
const container = firstMeaningful?.trim().startsWith('- ') ? [] : {};
|
const instancePath = error.instancePath || '/';
|
||||||
let index = startIndex;
|
|
||||||
|
|
||||||
while (index < lines.length) {
|
if (keyword === 'required') {
|
||||||
const line = lines[index];
|
const missing = error.params?.missingProperty ?? 'unknown';
|
||||||
const trimmed = line.trim();
|
return `${instancePath} missing required property ${missing}`;
|
||||||
if (trimmed === '' || trimmed.startsWith('#')) {
|
|
||||||
index += 1;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const indent = countIndent(line);
|
if (keyword === 'additionalProperties') {
|
||||||
if (indent < baseIndent) break;
|
const extra = error.params?.additionalProperty ?? 'unknown';
|
||||||
if (indent > baseIndent) {
|
return `${instancePath} must not include additional property ${extra}`;
|
||||||
throw new Error(`unexpected indentation at line ${index + 1}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(container)) {
|
if (keyword === 'const') {
|
||||||
if (!trimmed.startsWith('- ')) {
|
return `${instancePath} must equal ${JSON.stringify(error.params?.allowedValue)}`;
|
||||||
throw new Error(`expected array item at line ${index + 1}`);
|
|
||||||
}
|
|
||||||
const itemBody = trimmed.slice(2);
|
|
||||||
if (itemBody === '') {
|
|
||||||
const nested = parseBlock(lines, index + 1, baseIndent + 2);
|
|
||||||
container.push(nested.value);
|
|
||||||
index = nested.nextIndex;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (itemBody.includes(':')) {
|
|
||||||
const separator = itemBody.indexOf(':');
|
|
||||||
const key = itemBody.slice(0, separator).trim();
|
|
||||||
const remainder = itemBody.slice(separator + 1).trim();
|
|
||||||
const objectItem = {};
|
|
||||||
if (remainder === '') {
|
|
||||||
const nested = parseBlock(lines, index + 1, baseIndent + 4);
|
|
||||||
objectItem[key] = nested.value;
|
|
||||||
container.push(objectItem);
|
|
||||||
index = nested.nextIndex;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
objectItem[key] = parseScalar(remainder);
|
|
||||||
container.push(objectItem);
|
|
||||||
index += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
container.push(parseScalar(itemBody));
|
|
||||||
index += 1;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const separator = trimmed.indexOf(':');
|
return `${instancePath} ${error.message}`.trim();
|
||||||
if (separator === -1) {
|
}).join('; ');
|
||||||
throw new Error(`expected key/value pair at line ${index + 1}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = trimmed.slice(0, separator).trim();
|
|
||||||
let remainder = trimmed.slice(separator + 1).trim();
|
|
||||||
|
|
||||||
if (remainder === '>-') {
|
|
||||||
const blockLines = [];
|
|
||||||
index += 1;
|
|
||||||
while (index < lines.length) {
|
|
||||||
const nestedLine = lines[index];
|
|
||||||
const nestedTrimmed = nestedLine.trim();
|
|
||||||
if (nestedTrimmed === '') {
|
|
||||||
blockLines.push('');
|
|
||||||
index += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const nestedIndent = countIndent(nestedLine);
|
|
||||||
if (nestedIndent <= baseIndent) break;
|
|
||||||
blockLines.push(nestedLine.slice(baseIndent + 2).trim());
|
|
||||||
index += 1;
|
|
||||||
}
|
|
||||||
container[key] = blockLines.join(' ').replace(/\s+/g, ' ').trim();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (remainder === '') {
|
|
||||||
const nested = parseBlock(lines, index + 1, baseIndent + 2);
|
|
||||||
container[key] = nested.value;
|
|
||||||
index = nested.nextIndex;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
container[key] = parseScalar(remainder);
|
|
||||||
index += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { value: container, nextIndex: index };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseDeploymentProfileYaml(yamlText) {
|
export function parseDeploymentProfileYaml(yamlText) {
|
||||||
const lines = yamlText.replace(/\r\n/g, '\n').split('\n');
|
return YAML.parse(yamlText);
|
||||||
return parseBlock(lines, 0, 0).value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateDeploymentProfileSchema(profile) {
|
export function validateDeploymentProfileSchema(profile) {
|
||||||
if (!profile || typeof profile !== 'object' || Array.isArray(profile)) {
|
if (!profile || typeof profile !== 'object' || Array.isArray(profile)) {
|
||||||
throw new Error('deployment profile must be an object');
|
throw new Error('deployment profile must be an object');
|
||||||
}
|
}
|
||||||
if (profile.apiVersion !== 'reporting-governance/v1alpha1') {
|
|
||||||
throw new Error('deployment profile apiVersion must be reporting-governance/v1alpha1');
|
if (!validateDeploymentProfile(profile)) {
|
||||||
}
|
throw new Error(`deployment profile schema validation failed: ${formatAjvErrors(validateDeploymentProfile.errors)}`);
|
||||||
if (profile.kind !== 'DeploymentProfile') {
|
|
||||||
throw new Error('deployment profile kind must be DeploymentProfile');
|
|
||||||
}
|
|
||||||
if (typeof profile?.metadata?.id !== 'string' || profile.metadata.id.trim() === '') {
|
|
||||||
throw new Error('deployment profile metadata.id must be a non-empty string');
|
|
||||||
}
|
|
||||||
if (typeof profile?.metadata?.version !== 'string' || profile.metadata.version.trim() === '') {
|
|
||||||
throw new Error('deployment profile metadata.version must be a non-empty string');
|
|
||||||
}
|
|
||||||
if (typeof profile?.metadata?.runtime !== 'string' || profile.metadata.runtime.trim() === '') {
|
|
||||||
throw new Error('deployment profile metadata.runtime must be a non-empty string');
|
|
||||||
}
|
|
||||||
if (typeof profile?.spec?.package?.pluginVersion !== 'string' || profile.spec.package.pluginVersion.trim() === '') {
|
|
||||||
throw new Error('deployment profile spec.package.pluginVersion must be a non-empty string');
|
|
||||||
}
|
|
||||||
if (!profile?.spec?.adapters || typeof profile.spec.adapters !== 'object' || Array.isArray(profile.spec.adapters)) {
|
|
||||||
throw new Error('deployment profile spec.adapters must be an object');
|
|
||||||
}
|
|
||||||
if (!profile?.spec?.notifications || typeof profile.spec.notifications !== 'object' || Array.isArray(profile.spec.notifications)) {
|
|
||||||
throw new Error('deployment profile spec.notifications must be an object');
|
|
||||||
}
|
|
||||||
if (!profile?.spec?.audit || typeof profile.spec.audit !== 'object' || Array.isArray(profile.spec.audit)) {
|
|
||||||
throw new Error('deployment profile spec.audit must be an object');
|
|
||||||
}
|
|
||||||
if (!Array.isArray(profile?.spec?.audit?.requiredArtifacts) || profile.spec.audit.requiredArtifacts.length === 0) {
|
|
||||||
throw new Error('deployment profile spec.audit.requiredArtifacts must be a non-empty array');
|
|
||||||
}
|
|
||||||
if (!profile?.spec?.capability_expectations || typeof profile.spec.capability_expectations !== 'object' || Array.isArray(profile.spec.capability_expectations)) {
|
|
||||||
throw new Error('deployment profile spec.capability_expectations must be an object');
|
|
||||||
}
|
|
||||||
if (!Array.isArray(profile.spec.capability_expectations.required)) {
|
|
||||||
throw new Error('deployment profile spec.capability_expectations.required must be an array');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return profile;
|
return profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +69,7 @@ export function generateDeploymentProfileArtifact(profile, { sourceProfile } = {
|
|||||||
const sourceProfilePath = sourceProfile ?? path.join('profiles', `${id}.yaml`);
|
const sourceProfilePath = sourceProfile ?? path.join('profiles', `${id}.yaml`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
$schema: schemaPath,
|
$schema: artifactSchemaRefPath,
|
||||||
apiVersion: validated.apiVersion,
|
apiVersion: validated.apiVersion,
|
||||||
kind: 'DeploymentProfileArtifact',
|
kind: 'DeploymentProfileArtifact',
|
||||||
metadata: {
|
metadata: {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
parseDeploymentProfileYaml,
|
parseDeploymentProfileYaml,
|
||||||
@@ -14,6 +16,66 @@ import { validateDeploymentProfileArtifact } from '../src/storage/profile-artifa
|
|||||||
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 complexProfileYaml = `apiVersion: reporting-governance/v1alpha1
|
||||||
|
kind: DeploymentProfile
|
||||||
|
metadata:
|
||||||
|
id: demo
|
||||||
|
version: 1.0.0
|
||||||
|
runtime: openclaw
|
||||||
|
title: Demo Profile
|
||||||
|
summary: >-
|
||||||
|
Complex profile for YAML parser coverage with nested objects,
|
||||||
|
arrays, and folded scalars.
|
||||||
|
spec:
|
||||||
|
package:
|
||||||
|
pluginVersion: 0.1.0-mainline
|
||||||
|
compatibility:
|
||||||
|
adapterInterface: reporting-governance-adapter-interface
|
||||||
|
deploymentModel: reporting-governance-deployment-model
|
||||||
|
policies:
|
||||||
|
enabledPacks:
|
||||||
|
- no-silence
|
||||||
|
- verified-completion-only
|
||||||
|
overrides:
|
||||||
|
completion:
|
||||||
|
minQuality: moderate
|
||||||
|
checkpoints:
|
||||||
|
overdueAction: force_checkpoint
|
||||||
|
escalationAfterMisses: 2
|
||||||
|
adapters:
|
||||||
|
watchdog:
|
||||||
|
enabled: true
|
||||||
|
scheduleMode: cron
|
||||||
|
queue:
|
||||||
|
enabled: true
|
||||||
|
dispatcher:
|
||||||
|
enabled: true
|
||||||
|
bridge:
|
||||||
|
enabled: true
|
||||||
|
truthfulDeliveryStates:
|
||||||
|
- dispatched
|
||||||
|
- acked
|
||||||
|
sender:
|
||||||
|
mode: openclaw-cli
|
||||||
|
orchestrator:
|
||||||
|
enabled: true
|
||||||
|
notifications:
|
||||||
|
operatorVisibleRecoveryRequired: true
|
||||||
|
allowedTerminalStates:
|
||||||
|
- acked
|
||||||
|
- blocked
|
||||||
|
audit:
|
||||||
|
portableArtifactsRequired: true
|
||||||
|
requiredArtifacts:
|
||||||
|
- queue_items
|
||||||
|
- bridge_receipts
|
||||||
|
capability_expectations:
|
||||||
|
required:
|
||||||
|
- create_queue_items
|
||||||
|
preferred:
|
||||||
|
- direct_sender_binding
|
||||||
|
`;
|
||||||
|
|
||||||
test('deployment profile yaml parser reads strict-manager-mode profile shape', () => {
|
test('deployment profile yaml parser reads strict-manager-mode profile shape', () => {
|
||||||
const artifact = generateDeploymentProfileArtifactFromFile(path.join(repoRoot, 'profiles', 'strict-manager-mode.yaml'));
|
const artifact = generateDeploymentProfileArtifactFromFile(path.join(repoRoot, 'profiles', 'strict-manager-mode.yaml'));
|
||||||
|
|
||||||
@@ -26,7 +88,7 @@ test('deployment profile yaml parser reads strict-manager-mode profile shape', (
|
|||||||
test('deployment profile schema validator rejects malformed profile', () => {
|
test('deployment profile schema validator rejects malformed profile', () => {
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => validateDeploymentProfileSchema({ kind: 'DeploymentProfile' }),
|
() => validateDeploymentProfileSchema({ kind: 'DeploymentProfile' }),
|
||||||
/deployment profile apiVersion must be reporting-governance\/v1alpha1/
|
/deployment profile schema validation failed: .*missing required property apiVersion/
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.throws(
|
assert.throws(
|
||||||
@@ -35,19 +97,32 @@ test('deployment profile schema validator rejects malformed profile', () => {
|
|||||||
kind: 'DeploymentProfile',
|
kind: 'DeploymentProfile',
|
||||||
metadata: { id: 'x', version: '1.0.0', runtime: 'openclaw' },
|
metadata: { id: 'x', version: '1.0.0', runtime: 'openclaw' },
|
||||||
spec: {
|
spec: {
|
||||||
package: { pluginVersion: '0.1.0-mainline' },
|
package: { pluginVersion: 7 },
|
||||||
adapters: {},
|
adapters: {},
|
||||||
notifications: {},
|
notifications: {
|
||||||
audit: { requiredArtifacts: [] },
|
operatorVisibleRecoveryRequired: true,
|
||||||
|
allowedTerminalStates: ['acked'],
|
||||||
|
},
|
||||||
|
audit: { portableArtifactsRequired: true, requiredArtifacts: ['queue_items'] },
|
||||||
capability_expectations: { required: [] },
|
capability_expectations: { required: [] },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
/deployment profile spec\.audit\.requiredArtifacts must be a non-empty array/
|
/deployment profile schema validation failed: .*\/spec\/package\/pluginVersion must be string/
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('deployment profile yaml parser covers composite nested yaml structure', () => {
|
||||||
|
const profile = parseDeploymentProfileYaml(complexProfileYaml);
|
||||||
|
|
||||||
|
assert.equal(profile.metadata.summary, 'Complex profile for YAML parser coverage with nested objects, arrays, and folded scalars.');
|
||||||
|
assert.deepEqual(profile.spec.policies.enabledPacks, ['no-silence', 'verified-completion-only']);
|
||||||
|
assert.equal(profile.spec.policies.overrides.checkpoints.overdueAction, 'force_checkpoint');
|
||||||
|
assert.deepEqual(profile.spec.adapters.bridge.truthfulDeliveryStates, ['dispatched', 'acked']);
|
||||||
|
assert.deepEqual(profile.spec.notifications.allowedTerminalStates, ['acked', 'blocked']);
|
||||||
|
});
|
||||||
|
|
||||||
test('yaml to artifact generation produces validator-compatible package artifact', () => {
|
test('yaml to artifact generation produces validator-compatible package artifact', () => {
|
||||||
const profile = parseDeploymentProfileYaml(`apiVersion: reporting-governance/v1alpha1\nkind: DeploymentProfile\nmetadata:\n id: demo\n version: 1.0.0\n runtime: openclaw\nspec:\n package:\n pluginVersion: 0.1.0-mainline\n adapters:\n watchdog:\n enabled: true\n queue:\n enabled: true\n dispatcher:\n enabled: true\n bridge:\n enabled: true\n sender:\n mode: openclaw-cli\n orchestrator:\n enabled: true\n notifications:\n operatorVisibleRecoveryRequired: true\n allowedTerminalStates:\n - acked\n audit:\n portableArtifactsRequired: true\n requiredArtifacts:\n - queue_items\n capability_expectations:\n required:\n - create_queue_items\n`);
|
const profile = parseDeploymentProfileYaml(complexProfileYaml);
|
||||||
const artifact = generateDeploymentProfileArtifact(profile, { sourceProfile: 'profiles/demo.yaml' });
|
const artifact = generateDeploymentProfileArtifact(profile, { sourceProfile: 'profiles/demo.yaml' });
|
||||||
|
|
||||||
assert.equal(artifact.kind, 'DeploymentProfileArtifact');
|
assert.equal(artifact.kind, 'DeploymentProfileArtifact');
|
||||||
@@ -69,3 +144,43 @@ test('strict-manager YAML generation matches checked-in package artifact snapsho
|
|||||||
|
|
||||||
assert.deepEqual(generated, checkedIn);
|
assert.deepEqual(generated, checkedIn);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('profile artifact generator CLI exits 1 with actionable error on malformed profile', () => {
|
||||||
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'reporting-governance-profile-cli-'));
|
||||||
|
const malformedProfilePath = path.join(tempDir, 'malformed.yaml');
|
||||||
|
const outputArtifactPath = path.join(tempDir, 'artifact.json');
|
||||||
|
|
||||||
|
fs.writeFileSync(malformedProfilePath, `apiVersion: reporting-governance/v1alpha1
|
||||||
|
kind: DeploymentProfile
|
||||||
|
metadata:
|
||||||
|
id: malformed
|
||||||
|
version: 1.0.0
|
||||||
|
runtime: openclaw
|
||||||
|
spec:
|
||||||
|
package:
|
||||||
|
pluginVersion: 123
|
||||||
|
adapters: {}
|
||||||
|
notifications:
|
||||||
|
operatorVisibleRecoveryRequired: true
|
||||||
|
allowedTerminalStates:
|
||||||
|
- acked
|
||||||
|
audit:
|
||||||
|
portableArtifactsRequired: true
|
||||||
|
requiredArtifacts:
|
||||||
|
- queue_items
|
||||||
|
capability_expectations:
|
||||||
|
required: []
|
||||||
|
`, 'utf8');
|
||||||
|
|
||||||
|
const result = spawnSync(
|
||||||
|
process.execPath,
|
||||||
|
[path.join(repoRoot, 'scripts', 'generate_reporting_governance_profile_artifact.mjs'), malformedProfilePath, outputArtifactPath],
|
||||||
|
{ encoding: 'utf8' }
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(result.status, 1);
|
||||||
|
assert.match(result.stderr, /profile artifact generation failed:/);
|
||||||
|
assert.match(result.stderr, /deployment profile schema validation failed:/);
|
||||||
|
assert.match(result.stderr, /\/spec\/package\/pluginVersion must be string/);
|
||||||
|
assert.equal(fs.existsSync(outputArtifactPath), false);
|
||||||
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ function main() {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const sourcePath = path.resolve(sourcePathArg);
|
const sourcePath = path.resolve(sourcePathArg);
|
||||||
const outputPath = path.resolve(outputPathArg);
|
const outputPath = path.resolve(outputPathArg);
|
||||||
const artifact = generateDeploymentProfileArtifactFromFile(sourcePath);
|
const artifact = generateDeploymentProfileArtifactFromFile(sourcePath);
|
||||||
@@ -20,6 +21,10 @@ function main() {
|
|||||||
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||||
fs.writeFileSync(outputPath, JSON.stringify(artifact, null, 2) + '\n', 'utf8');
|
fs.writeFileSync(outputPath, JSON.stringify(artifact, null, 2) + '\n', 'utf8');
|
||||||
console.log(JSON.stringify({ ok: true, sourcePath, outputPath, profileId: artifact.metadata.id }, null, 2));
|
console.log(JSON.stringify({ ok: true, sourcePath, outputPath, profileId: artifact.metadata.id }, null, 2));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`profile artifact generation failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|||||||
Reference in New Issue
Block a user