From 37410c0be55f2fc587af4ff8b1303a3470f1b4f6 Mon Sep 17 00:00:00 2001 From: Eve Date: Fri, 8 May 2026 13:38:42 +0800 Subject: [PATCH] test: harden decision storage round-trip evidence --- .../src/storage/decision-store.mjs | 7 + .../test/decision-store.test.mjs | 124 +++++++++ .../decision-record-artifact.schema.json | 260 ++++++++++++++++++ 3 files changed, 391 insertions(+) create mode 100644 schemas/reporting-governance/decision-record-artifact.schema.json diff --git a/plugins/reporting-governance/src/storage/decision-store.mjs b/plugins/reporting-governance/src/storage/decision-store.mjs index e319cc8..1d60866 100644 --- a/plugins/reporting-governance/src/storage/decision-store.mjs +++ b/plugins/reporting-governance/src/storage/decision-store.mjs @@ -39,5 +39,12 @@ export function createFileDecisionStore({ decisionsDir, repoRootOverride } = {}) artifactPath: resolvedPath, }; }, + consume(artifactPath, consumer) { + if (typeof consumer !== 'function') { + throw new Error('decision store consumer must be a function'); + } + const loaded = this.load(artifactPath); + return consumer(loaded); + }, }; } diff --git a/plugins/reporting-governance/test/decision-store.test.mjs b/plugins/reporting-governance/test/decision-store.test.mjs index f28616b..d68ba86 100644 --- a/plugins/reporting-governance/test/decision-store.test.mjs +++ b/plugins/reporting-governance/test/decision-store.test.mjs @@ -205,6 +205,10 @@ test('decision artifact validator enforces receipt status enum and required fiel const partialMissingBlocked = structuredClone(partialArtifact); partialMissingBlocked.spec.receipt.blocked_actions = []; assert.throws(() => validateDecisionRecordArtifact(partialMissingBlocked), /blocked_actions must be non-empty when delivery_state=partial/); + + const partialMissingIntent = structuredClone(partialArtifact); + partialMissingIntent.spec.receipt.enforcement_intent = []; + assert.throws(() => validateDecisionRecordArtifact(partialMissingIntent), /enforcement_intent must be non-empty when delivery_state=partial/); }); test('decision artifact validator rejects unsupported receipt state combinations', () => { @@ -304,6 +308,103 @@ test('file decision store writes and reloads a validated decision artifact insid assert.equal(loaded.artifact.spec.receipt.delivery_state, 'pending_external_send'); }); +test('file decision store round-trips failed receipt through write load and consume', async (t) => { + const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'reporting-governance-decision-store-')); + t.after(() => fs.rmSync(sandbox, { recursive: true, force: true })); + + const fakeRepoRoot = path.join(sandbox, 'repo'); + fs.mkdirSync(fakeRepoRoot, { recursive: true }); + + const planned = createPlannedDecision(); + const store = createFileDecisionStore({ + decisionsDir: path.join(fakeRepoRoot, 'state', 'decisions'), + repoRootOverride: fakeRepoRoot, + }); + + const written = store.write({ + decision: planned.decision, + receipt: { + ...planned.receipt, + status: 'failed', + delivery_state: 'failed', + notes: [...planned.receipt.notes, 'bridge send failed'], + failure_reason: 'bridge_send_failed' + }, + recordedAt: '2026-05-08T04:00:00.000Z', + source: { + event_id: 'evt_watchdog_failed_001', + task_id: 'task-reporting-governance', + correlation_id: 'corr-failed-001', + }, + }); + + const consumed = store.consume(written.artifactPath, ({ artifact, artifactPath }) => ({ + artifactPath, + receiptStatus: artifact.spec.receipt.status, + deliveryState: artifact.spec.receipt.delivery_state, + failureReason: artifact.spec.receipt.failure_reason, + eventId: artifact.spec.source.event_id, + })); + + assert.equal(consumed.receiptStatus, 'failed'); + assert.equal(consumed.deliveryState, 'failed'); + assert.equal(consumed.failureReason, 'bridge_send_failed'); + assert.equal(consumed.eventId, 'evt_watchdog_failed_001'); + assert.equal(consumed.artifactPath, written.artifactPath); +}); + +test('file decision store round-trips partial receipt with source linkage through write load and consume', async (t) => { + const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'reporting-governance-decision-store-')); + t.after(() => fs.rmSync(sandbox, { recursive: true, force: true })); + + const fakeRepoRoot = path.join(sandbox, 'repo'); + fs.mkdirSync(fakeRepoRoot, { recursive: true }); + + const planned = createPlannedDecision(); + const store = createFileDecisionStore({ + decisionsDir: path.join(fakeRepoRoot, 'state', 'decisions'), + repoRootOverride: fakeRepoRoot, + }); + + const partialReceipt = { + ...planned.receipt, + status: 'degraded', + delivery_state: 'partial', + enforcement_intent: [{ action: 'emit_event', mandatory: true }], + blocked_actions: [{ action: 'notify_operator', mandatory: true, reason: 'sender binding unavailable' }], + notes: [...planned.receipt.notes, 'event emitted but operator notice still blocked'] + }; + + const written = store.write({ + decision: planned.decision, + receipt: partialReceipt, + recordedAt: '2026-05-08T04:00:00.000Z', + source: { + event_id: 'state/operator-notify-bridge-receipts/notify_abc-partial.json', + task_id: 'task-reporting-governance', + correlation_id: 'corr-partial-001', + }, + }); + + const consumed = store.consume(written.artifactPath, ({ artifact }) => ({ + taskId: artifact.spec.source.task_id, + correlationId: artifact.spec.source.correlation_id, + eventId: artifact.spec.source.event_id, + receiptStatus: artifact.spec.receipt.status, + deliveryState: artifact.spec.receipt.delivery_state, + blockedActions: artifact.spec.receipt.blocked_actions, + enforcementIntent: artifact.spec.receipt.enforcement_intent, + })); + + assert.equal(consumed.taskId, 'task-reporting-governance'); + assert.equal(consumed.correlationId, 'corr-partial-001'); + assert.equal(consumed.eventId, 'state/operator-notify-bridge-receipts/notify_abc-partial.json'); + assert.equal(consumed.receiptStatus, 'degraded'); + assert.equal(consumed.deliveryState, 'partial'); + assert.equal(consumed.blockedActions.length, 1); + assert.equal(consumed.enforcementIntent.length, 1); +}); + test('file decision store load rejects corrupted or malformed artifacts', async (t) => { const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'reporting-governance-decision-store-')); t.after(() => fs.rmSync(sandbox, { recursive: true, force: true })); @@ -356,6 +457,29 @@ test('file decision store load rejects corrupted or malformed artifacts', async assert.throws(() => store.load(corruptedArtifactPath), /metadata\.recorded_at must be a non-empty string/); }); +test('file decision store consume rejects non-function consumer', async (t) => { + const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'reporting-governance-decision-store-')); + t.after(() => fs.rmSync(sandbox, { recursive: true, force: true })); + + const fakeRepoRoot = path.join(sandbox, 'repo'); + fs.mkdirSync(fakeRepoRoot, { recursive: true }); + + const planned = createPlannedDecision(); + const store = createFileDecisionStore({ + decisionsDir: path.join(fakeRepoRoot, 'state', 'decisions'), + repoRootOverride: fakeRepoRoot, + }); + + const written = store.write({ + decision: planned.decision, + receipt: planned.receipt, + recordedAt: '2026-05-08T04:00:00.000Z', + source: { task_id: 'task-reporting-governance' } + }); + + assert.throws(() => store.consume(written.artifactPath, null), /decision store consumer must be a function/); +}); + test('file decision store produces distinct filenames for same decision written twice', async (t) => { const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'reporting-governance-decision-store-')); t.after(() => fs.rmSync(sandbox, { recursive: true, force: true })); diff --git a/schemas/reporting-governance/decision-record-artifact.schema.json b/schemas/reporting-governance/decision-record-artifact.schema.json new file mode 100644 index 0000000..8b40c61 --- /dev/null +++ b/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 + } + ] +}