test: harden decision storage round-trip evidence
This commit is contained in:
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 }));
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user