From 8a91206e076cd803c90e59ec27126df131cdce57 Mon Sep 17 00:00:00 2001 From: Eve Date: Fri, 8 May 2026 15:47:18 +0800 Subject: [PATCH] test: add packed consumer install smoke --- plugins/reporting-governance/README.md | 16 +- .../reporting-governance/package-lock.json | 3 + plugins/reporting-governance/package.json | 2 +- .../decision-record-artifact.schema.json | 260 ++++++++++++ .../reporting-governance/decision.schema.json | 376 ++++++++++++++++++ .../src/storage/decision-artifact.mjs | 2 +- .../test/package-structure.test.mjs | 2 + .../packed-consumer-install.smoke.test.mjs | 80 ++++ 8 files changed, 738 insertions(+), 3 deletions(-) create mode 100644 plugins/reporting-governance/schemas/reporting-governance/decision-record-artifact.schema.json create mode 100644 plugins/reporting-governance/schemas/reporting-governance/decision.schema.json create mode 100644 plugins/reporting-governance/test/packed-consumer-install.smoke.test.mjs diff --git a/plugins/reporting-governance/README.md b/plugins/reporting-governance/README.md index 6b51c9b..bf3cd19 100644 --- a/plugins/reporting-governance/README.md +++ b/plugins/reporting-governance/README.md @@ -269,5 +269,19 @@ npm run smoke reporting-governance-package-smoke --compact ``` -This smoke path uses package-local `profiles-src/`, `schemas/`, and `scripts/` only. +Repeatable packed-consumer install smoke is now also covered by package test: + +```bash +cd plugins/reporting-governance +npm pack +node --test test/packed-consumer-install.smoke.test.mjs +``` + +That path verifies a clean temp consumer can `npm install ` and then use only declared public surfaces: + +- package root export: `@openclaw/plugin-reporting-governance` +- exported subpath: `@openclaw/plugin-reporting-governance/adapters/orchestrator` +- package bin: `reporting-governance-package-smoke` + +This smoke path uses package-local `profiles-src/`, `schemas/`, and `scripts` only. It writes temp runtime artifacts under a caller-provided or temp workspace and verifies the dry-run orchestrator path end to end. diff --git a/plugins/reporting-governance/package-lock.json b/plugins/reporting-governance/package-lock.json index 1b4abbd..d035ba4 100644 --- a/plugins/reporting-governance/package-lock.json +++ b/plugins/reporting-governance/package-lock.json @@ -11,6 +11,9 @@ "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "yaml": "^2.8.0" + }, + "bin": { + "reporting-governance-package-smoke": "scripts/package-smoke.mjs" } }, "node_modules/ajv": { diff --git a/plugins/reporting-governance/package.json b/plugins/reporting-governance/package.json index e9fd589..b721441 100644 --- a/plugins/reporting-governance/package.json +++ b/plugins/reporting-governance/package.json @@ -17,7 +17,7 @@ "reporting-governance-package-smoke": "./scripts/package-smoke.mjs" }, "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/decision-store.test.mjs test/decision-store-runtime.integration.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/decision-store.test.mjs test/decision-store-runtime.integration.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/packed-consumer-install.smoke.test.mjs", "smoke": "node ./scripts/package-smoke.mjs --compact" }, "dependencies": { diff --git a/plugins/reporting-governance/schemas/reporting-governance/decision-record-artifact.schema.json b/plugins/reporting-governance/schemas/reporting-governance/decision-record-artifact.schema.json new file mode 100644 index 0000000..8b40c61 --- /dev/null +++ b/plugins/reporting-governance/schemas/reporting-governance/decision-record-artifact.schema.json @@ -0,0 +1,260 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cowbay.org/schemas/reporting-governance/decision-record-artifact.schema.json", + "title": "Reporting Governance Decision Record Artifact", + "description": "Portable decision storage artifact combining canonical decision output, truthful receipt state, and source linkage.", + "type": "object", + "additionalProperties": false, + "required": ["kind", "apiVersion", "metadata", "spec"], + "properties": { + "kind": { "const": "DecisionRecordArtifact" }, + "apiVersion": { "const": "reporting-governance/v1alpha1" }, + "metadata": { "$ref": "#/$defs/metadata" }, + "spec": { "$ref": "#/$defs/spec" } + }, + "$defs": { + "canonicalTimestamp": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$" + }, + "sourceEventId": { + "type": "string", + "minLength": 1, + "pattern": "^(?:evt_[A-Za-z0-9._:-]+|/?[A-Za-z0-9._-]+(?:/[A-Za-z0-9._-]+)+|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$" + }, + "metadata": { + "type": "object", + "additionalProperties": false, + "required": ["record_id", "recorded_at", "policy_id", "decision", "correlation_id", "task_id", "event_id"], + "properties": { + "record_id": { "type": "string", "minLength": 1 }, + "recorded_at": { "$ref": "#/$defs/canonicalTimestamp" }, + "policy_id": { "type": "string", "minLength": 1 }, + "decision": { "type": "string", "minLength": 1 }, + "correlation_id": { "type": ["string", "null"] }, + "task_id": { "type": ["string", "null"] }, + "event_id": { + "oneOf": [ + { "$ref": "#/$defs/sourceEventId" }, + { "type": "null" } + ] + } + } + }, + "receiptAction": { + "type": "object", + "additionalProperties": true, + "required": ["action", "mandatory"], + "properties": { + "action": { "type": "string", "minLength": 1 }, + "mandatory": { "type": "boolean" } + } + }, + "blockedAction": { + "type": "object", + "additionalProperties": true, + "required": ["action", "mandatory"], + "properties": { + "action": { "type": "string", "minLength": 1 }, + "mandatory": { "type": "boolean" }, + "reason": { "type": "string" } + } + }, + "receipt": { + "type": "object", + "additionalProperties": true, + "required": [ + "policy_id", + "decision", + "status", + "delivery_state", + "enforcement_intent", + "blocked_actions", + "notes" + ], + "properties": { + "policy_id": { "type": "string", "minLength": 1 }, + "decision": { "type": "string", "minLength": 1 }, + "status": { "enum": ["planned", "acked", "blocked", "degraded", "failed"] }, + "delivery_state": { "enum": ["prepared", "queued", "dispatched", "pending_external_send", "acked", "blocked", "failed", "partial"] }, + "operator_notice_required": { "type": "boolean" }, + "enforcement_intent": { + "type": "array", + "items": { "$ref": "#/$defs/receiptAction" } + }, + "blocked_actions": { + "type": "array", + "items": { "$ref": "#/$defs/blockedAction" } + }, + "notes": { + "type": "array", + "items": { "type": "string" } + }, + "failure_reason": { "type": "string", "minLength": 1 } + }, + "allOf": [ + { + "if": { + "properties": { "status": { "const": "failed" } }, + "required": ["status"] + }, + "then": { "required": ["failure_reason"] } + }, + { + "if": { + "properties": { "delivery_state": { "const": "partial" } }, + "required": ["delivery_state"] + }, + "then": { + "properties": { + "blocked_actions": { "minItems": 1 }, + "enforcement_intent": { "minItems": 1 } + } + } + }, + { + "if": { + "properties": { "status": { "const": "planned" } }, + "required": ["status"] + }, + "then": { + "properties": { + "delivery_state": { "enum": ["prepared", "queued", "dispatched", "pending_external_send", "partial"] } + } + } + }, + { + "if": { + "properties": { "status": { "const": "acked" } }, + "required": ["status"] + }, + "then": { + "properties": { + "delivery_state": { "const": "acked" } + } + } + }, + { + "if": { + "properties": { "status": { "const": "blocked" } }, + "required": ["status"] + }, + "then": { + "properties": { + "delivery_state": { "const": "blocked" } + } + } + }, + { + "if": { + "properties": { "status": { "const": "degraded" } }, + "required": ["status"] + }, + "then": { + "properties": { + "delivery_state": { "enum": ["partial", "failed", "blocked"] } + } + } + }, + { + "if": { + "properties": { "status": { "const": "failed" } }, + "required": ["status"] + }, + "then": { + "properties": { + "delivery_state": { "const": "failed" } + } + } + } + ] + }, + "source": { + "type": "object", + "additionalProperties": false, + "required": ["event_id", "task_id", "correlation_id"], + "properties": { + "event_id": { + "oneOf": [ + { "$ref": "#/$defs/sourceEventId" }, + { "type": "null" } + ] + }, + "task_id": { "type": ["string", "null"] }, + "correlation_id": { "type": ["string", "null"] } + } + }, + "spec": { + "type": "object", + "additionalProperties": false, + "required": ["decision", "receipt", "source"], + "properties": { + "decision": { "$ref": "decision.schema.json" }, + "receipt": { "$ref": "#/$defs/receipt" }, + "source": { "$ref": "#/$defs/source" } + } + } + }, + "allOf": [ + { + "if": { + "properties": { + "metadata": { + "properties": { + "event_id": { "type": "null" } + }, + "required": ["event_id"] + } + } + }, + "then": { + "properties": { + "spec": { + "properties": { + "source": { + "properties": { + "event_id": { "type": "null" } + }, + "required": ["event_id"] + } + } + } + } + }, + "else": { + "properties": { + "spec": { + "properties": { + "source": { + "properties": { + "event_id": { "$ref": "#/$defs/sourceEventId" } + }, + "required": ["event_id"] + } + } + } + } + } + }, + { + "if": { + "properties": { + "spec": { + "properties": { + "source": { + "properties": { + "task_id": { "type": "null" }, + "correlation_id": { "type": "null" }, + "event_id": { "type": "null" } + }, + "required": ["task_id", "correlation_id", "event_id"] + } + }, + "required": ["source"] + } + } + }, + "then": false + } + ] +} diff --git a/plugins/reporting-governance/schemas/reporting-governance/decision.schema.json b/plugins/reporting-governance/schemas/reporting-governance/decision.schema.json new file mode 100644 index 0000000..8a96f17 --- /dev/null +++ b/plugins/reporting-governance/schemas/reporting-governance/decision.schema.json @@ -0,0 +1,376 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cowbay.org/schemas/reporting-governance/decision.schema.json", + "title": "Reporting Governance Decision", + "description": "Canonical policy decision schema for the reporting-governance plugin.", + "type": "object", + "additionalProperties": false, + "required": [ + "decision", + "policy_id", + "severity", + "reason", + "rewritten_message", + "suggested_status", + "required_actions", + "operator_notice" + ], + "properties": { + "decision": { + "$ref": "#/$defs/decision", + "description": "Canonical enforcement disposition." + }, + "policy_id": { + "type": "string", + "minLength": 1, + "description": "Stable policy rule identifier that produced the decision." + }, + "severity": { + "$ref": "#/$defs/severity", + "description": "Risk or urgency classification for the decision." + }, + "reason": { + "type": "string", + "minLength": 1, + "description": "Human-readable rationale for the decision." + }, + "rewritten_message": { + "type": ["string", "null"], + "description": "Replacement or annotated outgoing message text when governance rewrites or forces placeholder disclosure." + }, + "suggested_status": { + "$ref": "#/$defs/suggestedStatusOrNull", + "description": "Recommended workflow status after enforcement, if any." + }, + "required_actions": { + "type": "array", + "items": { + "$ref": "#/$defs/requiredAction" + }, + "description": "Ordered list of required enforcement actions." + }, + "operator_notice": { + "$ref": "#/$defs/operatorNoticeOrNull", + "description": "Operator-visible notice requirement produced by the policy." + } + }, + "$defs": { + "decision": { + "type": "string", + "enum": [ + "allow", + "rewrite", + "block", + "require_review", + "force_checkpoint", + "escalate", + "downgrade_status", + "annotate_placeholder" + ] + }, + "severity": { + "type": "string", + "enum": [ + "info", + "low", + "medium", + "high", + "critical" + ] + }, + "suggestedStatus": { + "type": "string", + "enum": [ + "in_progress", + "pending_verification", + "blocked", + "failed", + "awaiting_review", + "completed" + ] + }, + "suggestedStatusOrNull": { + "oneOf": [ + { + "$ref": "#/$defs/suggestedStatus" + }, + { + "type": "null" + } + ] + }, + "actionVerb": { + "type": "string", + "enum": [ + "dispatch_message", + "rewrite_message", + "append_audit_note", + "block_transition", + "set_status", + "request_review", + "emit_event", + "notify_operator", + "start_watchdog", + "raise_escalation", + "record_placeholder" + ] + }, + "actionTarget": { + "type": "string", + "enum": [ + "outgoing_report", + "status_transition", + "operator_channel", + "task_record", + "event_stream", + "watchdog", + "review_queue" + ] + }, + "requiredAction": { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "target", + "mandatory" + ], + "properties": { + "action": { + "$ref": "#/$defs/actionVerb" + }, + "target": { + "$ref": "#/$defs/actionTarget" + }, + "mandatory": { + "type": "boolean" + }, + "details": { + "type": "object", + "additionalProperties": true + } + } + }, + "urgency": { + "type": "string", + "enum": [ + "info", + "low", + "medium", + "high", + "critical" + ] + }, + "operatorNotice": { + "type": "object", + "additionalProperties": false, + "required": [ + "required", + "channel", + "urgency", + "message", + "deadline" + ], + "properties": { + "required": { + "type": "boolean" + }, + "channel": { + "type": ["string", "null"] + }, + "urgency": { + "oneOf": [ + { + "$ref": "#/$defs/urgency" + }, + { + "type": "null" + } + ] + }, + "message": { + "type": ["string", "null"] + }, + "must_reference": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "deadline": { + "type": ["string", "null"], + "format": "date-time" + } + } + }, + "operatorNoticeOrNull": { + "oneOf": [ + { + "$ref": "#/$defs/operatorNotice" + }, + { + "type": "null" + } + ] + } + }, + "allOf": [ + { + "if": { + "properties": { + "decision": { + "const": "rewrite" + } + }, + "required": ["decision"] + }, + "then": { + "properties": { + "rewritten_message": { + "type": "string", + "minLength": 1 + } + } + } + }, + { + "if": { + "properties": { + "decision": { + "const": "annotate_placeholder" + } + }, + "required": ["decision"] + }, + "then": { + "properties": { + "rewritten_message": { + "type": "string", + "minLength": 1 + }, + "operator_notice": { + "$ref": "#/$defs/operatorNotice" + } + } + } + }, + { + "if": { + "properties": { + "decision": { + "const": "force_checkpoint" + } + }, + "required": ["decision"] + }, + "then": { + "properties": { + "operator_notice": { + "$ref": "#/$defs/operatorNotice" + } + }, + "allOf": [ + { + "properties": { + "operator_notice": { + "properties": { + "required": { + "const": true + } + }, + "required": ["required"] + } + } + }, + { + "properties": { + "required_actions": { + "contains": { + "type": "object", + "properties": { + "action": { + "enum": ["notify_operator", "dispatch_message"] + }, + "mandatory": { + "const": true + } + }, + "required": ["action", "mandatory"] + } + } + } + } + ] + } + }, + { + "if": { + "properties": { + "decision": { + "const": "downgrade_status" + } + }, + "required": ["decision"] + }, + "then": { + "properties": { + "suggested_status": { + "const": "pending_verification" + }, + "required_actions": { + "contains": { + "type": "object", + "properties": { + "action": { + "const": "set_status" + }, + "mandatory": { + "const": true + }, + "details": { + "type": "object", + "properties": { + "to": { + "const": "pending_verification" + } + }, + "required": ["to"] + } + }, + "required": ["action", "mandatory", "details"] + } + } + } + } + }, + { + "if": { + "properties": { + "decision": { + "const": "block" + } + }, + "required": ["decision"] + }, + "then": { + "properties": { + "required_actions": { + "contains": { + "type": "object", + "properties": { + "action": { + "const": "block_transition" + }, + "mandatory": { + "const": true + } + }, + "required": ["action", "mandatory"] + } + } + } + } + } + ] +} diff --git a/plugins/reporting-governance/src/storage/decision-artifact.mjs b/plugins/reporting-governance/src/storage/decision-artifact.mjs index 05e7f8f..90fdd18 100644 --- a/plugins/reporting-governance/src/storage/decision-artifact.mjs +++ b/plugins/reporting-governance/src/storage/decision-artifact.mjs @@ -20,7 +20,7 @@ const RECEIPT_STATUS_RULES = { }; const packageRoot = path.resolve(import.meta.dirname, '..', '..'); -const repoRoot = path.resolve(packageRoot, '..', '..'); +const repoRoot = packageRoot; const decisionSchemaPath = path.resolve(repoRoot, 'schemas', 'reporting-governance', 'decision.schema.json'); const decisionRecordArtifactSchemaPath = path.resolve(repoRoot, 'schemas', 'reporting-governance', 'decision-record-artifact.schema.json'); diff --git a/plugins/reporting-governance/test/package-structure.test.mjs b/plugins/reporting-governance/test/package-structure.test.mjs index 981cbb4..8a5ee1e 100644 --- a/plugins/reporting-governance/test/package-structure.test.mjs +++ b/plugins/reporting-governance/test/package-structure.test.mjs @@ -29,6 +29,8 @@ const requiredPaths = [ 'profiles-src/strict-manager-mode.yaml', 'schemas/reporting-governance/deployment-profile.schema.json', 'schemas/reporting-governance/capability-descriptor.schema.json', + 'schemas/reporting-governance/decision.schema.json', + 'schemas/reporting-governance/decision-record-artifact.schema.json', 'scripts/package-smoke.mjs', 'scripts/watchdog_auto_notify_orchestrator.mjs', 'scripts/long_task_watchdog.mjs', diff --git a/plugins/reporting-governance/test/packed-consumer-install.smoke.test.mjs b/plugins/reporting-governance/test/packed-consumer-install.smoke.test.mjs new file mode 100644 index 0000000..3cc139e --- /dev/null +++ b/plugins/reporting-governance/test/packed-consumer-install.smoke.test.mjs @@ -0,0 +1,80 @@ +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 { spawnSync } from 'node:child_process'; + +const packageRoot = path.resolve(import.meta.dirname, '..'); + +function run(command, args, { cwd, env = {} } = {}) { + const result = spawnSync(command, args, { + cwd, + encoding: 'utf8', + env: { + ...process.env, + ...env, + }, + }); + + assert.equal( + result.status, + 0, + [ + `command failed: ${command} ${args.join(' ')}`, + `cwd: ${cwd}`, + result.stdout && `stdout:\n${result.stdout.trim()}`, + result.stderr && `stderr:\n${result.stderr.trim()}`, + ].filter(Boolean).join('\n\n') + ); + + return result; +} + +test('packed tarball installs into clean consumer and works via public exports/bin only', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'reporting-governance-packed-consumer-')); + + try { + const packResult = run('npm', ['pack', '--json'], { cwd: packageRoot }); + const packPayload = JSON.parse(packResult.stdout.trim()); + const tarballName = packPayload.at(-1)?.filename; + assert.ok(tarballName, 'npm pack should return tarball filename'); + + const tarballPath = path.join(packageRoot, tarballName); + assert.equal(fs.existsSync(tarballPath), true, 'tarball should exist after npm pack'); + + const consumerRoot = path.join(root, 'consumer'); + fs.mkdirSync(consumerRoot, { recursive: true }); + run('npm', ['init', '-y'], { cwd: consumerRoot }); + run('npm', ['install', tarballPath], { cwd: consumerRoot }); + + const exportProbe = run(process.execPath, ['--input-type=module', '--eval', ` + import * as plugin from '@openclaw/plugin-reporting-governance'; + import { runOrchestratorAdapter } from '@openclaw/plugin-reporting-governance/adapters/orchestrator'; + process.stdout.write(JSON.stringify({ + packageName: plugin.packageName, + hasRunWatchdogChain: typeof plugin.runWatchdogChain, + hasRunOrchestratorAdapter: typeof runOrchestratorAdapter, + })); + `], { cwd: consumerRoot }); + const exportPayload = JSON.parse(exportProbe.stdout.trim()); + assert.equal(exportPayload.packageName, '@openclaw/plugin-reporting-governance'); + assert.equal(exportPayload.hasRunWatchdogChain, 'function'); + assert.equal(exportPayload.hasRunOrchestratorAdapter, 'function'); + + const smokeResult = run( + path.join(consumerRoot, 'node_modules', '.bin', 'reporting-governance-package-smoke'), + ['--compact'], + { cwd: consumerRoot } + ); + const smokePayload = JSON.parse(smokeResult.stdout.trim()); + assert.equal(smokePayload.ok, true); + assert.equal(smokePayload.tool, 'reporting-governance-package-smoke'); + assert.equal(smokePayload.orchestrator.ok, true); + assert.equal(smokePayload.orchestrator.dispatchedCount, 1); + assert.equal(smokePayload.orchestrator.pendingCount, 1); + assert.equal(smokePayload.orchestrator.notificationCount, 1); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +});