From 1586c14aa88286b92cad94f6b39702983592acb5 Mon Sep 17 00:00:00 2001 From: claude-bot-go Date: Tue, 28 Jul 2026 18:29:19 +0800 Subject: [PATCH] =?UTF-8?q?Fix=20#149:=20[milestone=20Milestone=205=20]=20?= =?UTF-8?q?Pipeline=20health=20and=20SLA=20monitoring=20=E2=80=94=20add=20?= =?UTF-8?q?end-to-end=20latency=20tracking,=20per-stag...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 16 ++ schemas/index.json | 50 +++++- schemas/pipeline/backlog-status.schema.json | 81 +++++++++ .../pipeline-health-record.schema.json | 165 ++++++++++++++++++ schemas/pipeline/sla-metrics.schema.json | 136 +++++++++++++++ .../invalid/backlog-status/example.json | 10 ++ .../pipeline-health-record/example.json | 9 + .../fixtures/invalid/sla-metrics/example.json | 4 + .../valid/backlog-status/example.json | 15 ++ .../valid/pipeline-health-record/example.json | 62 +++++++ tests/fixtures/valid/sla-metrics/example.json | 38 ++++ tests/pipeline-health.test.js | 135 ++++++++++++++ 12 files changed, 720 insertions(+), 1 deletion(-) create mode 100644 package-lock.json create mode 100644 schemas/pipeline/backlog-status.schema.json create mode 100644 schemas/pipeline/pipeline-health-record.schema.json create mode 100644 schemas/pipeline/sla-metrics.schema.json create mode 100644 tests/fixtures/invalid/backlog-status/example.json create mode 100644 tests/fixtures/invalid/pipeline-health-record/example.json create mode 100644 tests/fixtures/invalid/sla-metrics/example.json create mode 100644 tests/fixtures/valid/backlog-status/example.json create mode 100644 tests/fixtures/valid/pipeline-health-record/example.json create mode 100644 tests/fixtures/valid/sla-metrics/example.json create mode 100644 tests/pipeline-health.test.js diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..68e1b18 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,16 @@ +{ + "name": "@wasmagent/protocol", + "version": "0.1.7", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@wasmagent/protocol", + "version": "0.1.7", + "license": "Apache-2.0", + "bin": { + "wasmagent-protocol": "bin/cli.js" + } + } + } +} diff --git a/schemas/index.json b/schemas/index.json index 2bc43b1..48ff804 100644 --- a/schemas/index.json +++ b/schemas/index.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "protocol": "AEP", - "description": "Registry of canonical WasmAgent protocol schemas \u2014 the single source of truth for every cross-repository contract. Downstream repos consume via @wasmagent/protocol (npm) and wasmagent-protocol (PyPI). Covers: AEP, compliance, AgentBOM, MCP Posture, Trust Passport.", + "description": "Registry of canonical WasmAgent protocol schemas \u2014 the single source of truth for every cross-repository contract. Downstream repos consume via @wasmagent/protocol (npm) and wasmagent-protocol (PyPI). Covers: AEP, compliance, AgentBOM, MCP Posture, Trust Passport, Pipeline health.", "canonical_host": "https://wasmagent.dev/schemas/", "schemas": [ { @@ -184,6 +184,54 @@ "open-agent-audit" ], "summary": "Trust Passport \u2014 signed, expiring, verifiable trust-state artifact for AI agents." + }, + { + "id": "pipeline-health-record", + "title": "PipelineHealthRecord", + "path": "schemas/pipeline/pipeline-health-record.schema.json", + "canonical_id": "https://wasmagent.dev/schemas/pipeline/pipeline-health-record.schema.json", + "version": "pipeline-health-record/v1", + "stability": "evolving", + "owners": [ + "trace-pipeline" + ], + "consumers": [ + "trace-pipeline", + "wasmagent-ops" + ], + "summary": "Composite pipeline health snapshot \u2014 rolls up end-to-end latency, per-stage success rates, backlog/alert status, and the schema-evolution canary result shared across the production trace pipeline." + }, + { + "id": "sla-metrics", + "title": "SlaMetrics", + "path": "schemas/pipeline/sla-metrics.schema.json", + "canonical_id": "https://wasmagent.dev/schemas/pipeline/sla-metrics.schema.json", + "version": "sla-metrics/v1", + "stability": "evolving", + "owners": [ + "trace-pipeline" + ], + "consumers": [ + "trace-pipeline", + "wasmagent-ops" + ], + "summary": "Latency and per-stage success-rate measurements for a pipeline window \u2014 end-to-end latency tracking and per-stage success rates." + }, + { + "id": "backlog-status", + "title": "BacklogStatus", + "path": "schemas/pipeline/backlog-status.schema.json", + "canonical_id": "https://wasmagent.dev/schemas/pipeline/backlog-status.schema.json", + "version": "backlog-status/v1", + "stability": "evolving", + "owners": [ + "trace-pipeline" + ], + "consumers": [ + "trace-pipeline", + "wasmagent-ops" + ], + "summary": "Point-in-time backlog state for a single pipeline queue \u2014 drives alerting on backlog buildup (depth/age threshold breaches)." } ] } diff --git a/schemas/pipeline/backlog-status.schema.json b/schemas/pipeline/backlog-status.schema.json new file mode 100644 index 0000000..5f74313 --- /dev/null +++ b/schemas/pipeline/backlog-status.schema.json @@ -0,0 +1,81 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://wasmagent.dev/schemas/pipeline/backlog-status.schema.json", + "title": "BacklogStatus", + "description": "Point-in-time backlog state for a single pipeline queue (backlog-status/v1). Drives alerting on backlog buildup: when depth or oldest-item age breaches a threshold, or arrival exceeds drain, the queue is accumulating work faster than it can drain. Emitted by trace-pipeline and consumed by wasmagent-ops; rolled up into pipeline-health-record alerts as backlog_buildup.", + "type": "object", + "required": [ + "schema_version", + "pipeline_id", + "queue", + "measured_at_ms", + "depth", + "arrival_rate_per_second", + "drain_rate_per_second", + "breaches_threshold" + ], + "additionalProperties": false, + "properties": { + "schema_version": { + "type": "string", + "const": "backlog-status/v1", + "description": "Schema version identifier. Always 'backlog-status/v1'." + }, + "pipeline_id": { + "type": "string", + "minLength": 1 + }, + "queue": { + "type": "string", + "minLength": 1, + "description": "Name of the queue/stage this backlog describes (e.g. 'validation', 'ingest')." + }, + "measured_at_ms": { + "type": "integer", + "minimum": 0 + }, + "depth": { + "type": "integer", + "minimum": 0, + "description": "Current number of unprocessed items in the queue." + }, + "oldest_item_age_ms": { + "type": "number", + "minimum": 0, + "description": "Age of the oldest unprocessed item in milliseconds." + }, + "arrival_rate_per_second": { + "type": "number", + "minimum": 0, + "description": "Mean rate at which items are entering the queue." + }, + "drain_rate_per_second": { + "type": "number", + "minimum": 0, + "description": "Mean rate at which items are leaving the queue (processed or dropped)." + }, + "growth_rate_per_second": { + "type": "number", + "description": "arrival_rate_per_second - drain_rate_per_second. Positive values indicate buildup." + }, + "high_watermark": { + "type": "integer", + "minimum": 0, + "description": "Maximum observed depth over the measurement window." + }, + "threshold_depth": { + "type": "integer", + "minimum": 0, + "description": "Alert threshold for depth; depth above this triggers backlog_buildup alerting." + }, + "threshold_age_ms": { + "type": "number", + "minimum": 0, + "description": "Alert threshold for oldest_item_age_ms." + }, + "breaches_threshold": { + "type": "boolean", + "description": "True iff depth > threshold_depth OR oldest_item_age_ms > threshold_age_ms (for whichever thresholds are set). Rolled up into pipeline-health-record alerts." + } + } +} diff --git a/schemas/pipeline/pipeline-health-record.schema.json b/schemas/pipeline/pipeline-health-record.schema.json new file mode 100644 index 0000000..dc0b277 --- /dev/null +++ b/schemas/pipeline/pipeline-health-record.schema.json @@ -0,0 +1,165 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://wasmagent.dev/schemas/pipeline/pipeline-health-record.schema.json", + "title": "PipelineHealthRecord", + "description": "Composite pipeline health snapshot (pipeline-health-record/v1). Rolls up SLA metrics (end-to-end latency + per-stage success rates), per-queue backlog status, active alerts (validation failures, backlog buildup, latency SLO breaches, canary failures), and the result of automated schema-evolution canary testing against wasmagent-protocol updates. Single source of truth shared by trace-pipeline (emitter) and wasmagent-ops (consumer).", + "type": "object", + "required": [ + "schema_version", + "pipeline_id", + "recorded_at_ms", + "overall_status", + "sla", + "alerts", + "canary" + ], + "additionalProperties": false, + "properties": { + "schema_version": { + "type": "string", + "const": "pipeline-health-record/v1", + "description": "Schema version identifier. Always 'pipeline-health-record/v1'." + }, + "pipeline_id": { + "type": "string", + "minLength": 1 + }, + "recorded_at_ms": { + "type": "integer", + "minimum": 0 + }, + "overall_status": { + "type": "string", + "enum": [ + "healthy", + "degraded", + "unhealthy" + ], + "description": "Derived rollup: healthy = no alerts and SLOs met; degraded = warning alerts or near-breach; unhealthy = critical alert or SLO breach." + }, + "sla": { + "$ref": "sla-metrics.schema.json", + "description": "Latency and per-stage success-rate measurements for this snapshot's window." + }, + "backlogs": { + "type": "array", + "description": "Per-queue backlog status. Queues whose breaches_threshold is true SHOULD also appear as backlog_buildup alerts.", + "items": { + "$ref": "backlog-status.schema.json" + } + }, + "alerts": { + "type": "array", + "description": "Active alerts fired in this snapshot's window. validation_failure covers schema/validation rejections; backlog_buildup covers queue depth/age breaches.", + "items": { + "type": "object", + "required": [ + "kind", + "severity", + "message", + "first_observed_at_ms" + ], + "additionalProperties": false, + "properties": { + "kind": { + "type": "string", + "enum": [ + "validation_failure", + "backlog_buildup", + "latency_slo_breach", + "canary_failure" + ] + }, + "severity": { + "type": "string", + "enum": [ + "info", + "warning", + "critical" + ] + }, + "stage": { + "type": "string", + "description": "Optional pipeline stage/queue the alert is scoped to." + }, + "message": { + "type": "string", + "minLength": 1 + }, + "first_observed_at_ms": { + "type": "integer", + "minimum": 0 + }, + "count": { + "type": "integer", + "minimum": 1, + "description": "Number of occurrences in the window." + } + } + } + }, + "canary": { + "type": "object", + "description": "Result of automated canary testing of schema evolution: the pipeline re-runs its validation fixtures against a candidate wasmagent-protocol update before adoption, so a breaking schema change fails here instead of in production.", + "required": [ + "protocol_version", + "status", + "checked_at_ms" + ], + "additionalProperties": false, + "properties": { + "protocol_version": { + "type": "string", + "minLength": 1, + "description": "The wasmagent-protocol package version the canary validated against (e.g. '0.1.7')." + }, + "status": { + "type": "string", + "enum": [ + "pass", + "fail", + "skipped" + ], + "description": "pass = all sampled fixtures still validate under the candidate protocol; fail = at least one broke (also surfaces a canary_failure alert); skipped = canary not run." + }, + "schemas_checked": { + "type": "integer", + "minimum": 0, + "description": "Number of canonical schemas exercised by the canary." + }, + "failures": { + "type": "array", + "description": "Per-fixture canary failures. SHOULD be non-empty iff status is 'fail'.", + "items": { + "type": "object", + "required": [ + "schema_id", + "fixture", + "reason" + ], + "additionalProperties": false, + "properties": { + "schema_id": { + "type": "string", + "minLength": 1 + }, + "fixture": { + "type": "string", + "minLength": 1, + "description": "Path/id of the fixture that regressed." + }, + "reason": { + "type": "string", + "minLength": 1 + } + } + } + }, + "checked_at_ms": { + "type": "integer", + "minimum": 0 + } + } + } + } +} diff --git a/schemas/pipeline/sla-metrics.schema.json b/schemas/pipeline/sla-metrics.schema.json new file mode 100644 index 0000000..7bdaf77 --- /dev/null +++ b/schemas/pipeline/sla-metrics.schema.json @@ -0,0 +1,136 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://wasmagent.dev/schemas/pipeline/sla-metrics.schema.json", + "title": "SlaMetrics", + "description": "Latency and per-stage success-rate measurements for one pipeline window (sla-metrics/v1). Emitted by trace-pipeline and consumed by wasmagent-ops for SLA dashboards and SLO-breach alerting. This is the canonical home for end-to-end latency tracking and per-stage success rates shared across the production trace pipeline.", + "type": "object", + "required": [ + "schema_version", + "pipeline_id", + "window", + "end_to_end_latency_ms", + "per_stage" + ], + "additionalProperties": false, + "properties": { + "schema_version": { + "type": "string", + "const": "sla-metrics/v1", + "description": "Schema version identifier. Always 'sla-metrics/v1'." + }, + "pipeline_id": { + "type": "string", + "minLength": 1, + "description": "Identifier of the pipeline these metrics describe (e.g. 'trace-pipeline')." + }, + "window": { + "type": "object", + "description": "Half-open measurement window [start_ms, end_ms).", + "required": [ + "start_ms", + "end_ms" + ], + "additionalProperties": false, + "properties": { + "start_ms": { + "type": "integer", + "minimum": 0 + }, + "end_ms": { + "type": "integer", + "minimum": 0 + } + } + }, + "end_to_end_latency_ms": { + "$ref": "#/$defs/LatencyHistogram", + "description": "End-to-end pipeline latency distribution across the window — the full path from ingest to emitted output." + }, + "per_stage": { + "type": "array", + "description": "Per-stage breakdown: success/failure counts and success rate for each named pipeline stage.", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "stage", + "success_count", + "failure_count", + "success_rate" + ], + "additionalProperties": false, + "properties": { + "stage": { + "type": "string", + "minLength": 1 + }, + "success_count": { + "type": "integer", + "minimum": 0 + }, + "failure_count": { + "type": "integer", + "minimum": 0 + }, + "success_rate": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "success_count / (success_count + failure_count), clamped to [0, 1]." + }, + "latency_ms": { + "$ref": "#/$defs/LatencyHistogram" + } + } + } + }, + "throughput_per_second": { + "type": "number", + "minimum": 0, + "description": "Optional mean throughput over the window (records completed per second)." + } + }, + "$defs": { + "LatencyHistogram": { + "type": "object", + "description": "Latency distribution summary in milliseconds. p50 <= p95 <= p99 by convention.", + "required": [ + "p50", + "p95", + "p99", + "count" + ], + "additionalProperties": false, + "properties": { + "p50": { + "type": "number", + "minimum": 0 + }, + "p95": { + "type": "number", + "minimum": 0 + }, + "p99": { + "type": "number", + "minimum": 0 + }, + "min": { + "type": "number", + "minimum": 0 + }, + "max": { + "type": "number", + "minimum": 0 + }, + "mean": { + "type": "number", + "minimum": 0 + }, + "count": { + "type": "integer", + "minimum": 0 + } + } + } + } +} diff --git a/tests/fixtures/invalid/backlog-status/example.json b/tests/fixtures/invalid/backlog-status/example.json new file mode 100644 index 0000000..09eb06b --- /dev/null +++ b/tests/fixtures/invalid/backlog-status/example.json @@ -0,0 +1,10 @@ +{ + "schema_version": "backlog-status/v1", + "pipeline_id": "trace-pipeline", + "queue": "validation", + "measured_at_ms": 1722000060000, + "depth": -5, + "arrival_rate_per_second": 50.0, + "drain_rate_per_second": 48.0, + "breaches_threshold": false +} diff --git a/tests/fixtures/invalid/pipeline-health-record/example.json b/tests/fixtures/invalid/pipeline-health-record/example.json new file mode 100644 index 0000000..8b06847 --- /dev/null +++ b/tests/fixtures/invalid/pipeline-health-record/example.json @@ -0,0 +1,9 @@ +{ + "schema_version": "pipeline-health-record/v9", + "pipeline_id": "trace-pipeline", + "recorded_at_ms": 1722000060000, + "overall_status": "broken", + "sla": {}, + "alerts": [], + "canary": {} +} diff --git a/tests/fixtures/invalid/sla-metrics/example.json b/tests/fixtures/invalid/sla-metrics/example.json new file mode 100644 index 0000000..b3db3c1 --- /dev/null +++ b/tests/fixtures/invalid/sla-metrics/example.json @@ -0,0 +1,4 @@ +{ + "schema_version": "sla-metrics/v2", + "pipeline_id": "trace-pipeline" +} diff --git a/tests/fixtures/valid/backlog-status/example.json b/tests/fixtures/valid/backlog-status/example.json new file mode 100644 index 0000000..c652e4d --- /dev/null +++ b/tests/fixtures/valid/backlog-status/example.json @@ -0,0 +1,15 @@ +{ + "schema_version": "backlog-status/v1", + "pipeline_id": "trace-pipeline", + "queue": "validation", + "measured_at_ms": 1722000060000, + "depth": 128, + "oldest_item_age_ms": 4200.0, + "arrival_rate_per_second": 50.0, + "drain_rate_per_second": 48.0, + "growth_rate_per_second": 2.0, + "high_watermark": 250, + "threshold_depth": 1000, + "threshold_age_ms": 30000.0, + "breaches_threshold": false +} diff --git a/tests/fixtures/valid/pipeline-health-record/example.json b/tests/fixtures/valid/pipeline-health-record/example.json new file mode 100644 index 0000000..45c82b6 --- /dev/null +++ b/tests/fixtures/valid/pipeline-health-record/example.json @@ -0,0 +1,62 @@ +{ + "schema_version": "pipeline-health-record/v1", + "pipeline_id": "trace-pipeline", + "recorded_at_ms": 1722000060000, + "overall_status": "degraded", + "sla": { + "schema_version": "sla-metrics/v1", + "pipeline_id": "trace-pipeline", + "window": { + "start_ms": 1722000000000, + "end_ms": 1722000060000 + }, + "end_to_end_latency_ms": { + "p50": 120.0, + "p95": 480.0, + "p99": 950.0, + "count": 10000 + }, + "per_stage": [ + { + "stage": "ingest", + "success_count": 10000, + "failure_count": 0, + "success_rate": 1.0 + }, + { + "stage": "validation", + "success_count": 9960, + "failure_count": 40, + "success_rate": 0.996 + } + ] + }, + "backlogs": [ + { + "schema_version": "backlog-status/v1", + "pipeline_id": "trace-pipeline", + "queue": "validation", + "measured_at_ms": 1722000060000, + "depth": 128, + "arrival_rate_per_second": 50.0, + "drain_rate_per_second": 48.0, + "breaches_threshold": false + } + ], + "alerts": [ + { + "kind": "validation_failure", + "severity": "warning", + "stage": "validation", + "message": "40 records failed schema validation in the window", + "first_observed_at_ms": 1722000010000, + "count": 40 + } + ], + "canary": { + "protocol_version": "0.1.7", + "status": "pass", + "schemas_checked": 3, + "checked_at_ms": 1722000050000 + } +} diff --git a/tests/fixtures/valid/sla-metrics/example.json b/tests/fixtures/valid/sla-metrics/example.json new file mode 100644 index 0000000..ea2accc --- /dev/null +++ b/tests/fixtures/valid/sla-metrics/example.json @@ -0,0 +1,38 @@ +{ + "schema_version": "sla-metrics/v1", + "pipeline_id": "trace-pipeline", + "window": { + "start_ms": 1722000000000, + "end_ms": 1722000060000 + }, + "end_to_end_latency_ms": { + "p50": 120.0, + "p95": 480.0, + "p99": 950.0, + "min": 40.0, + "max": 1200.0, + "mean": 180.0, + "count": 10000 + }, + "per_stage": [ + { + "stage": "ingest", + "success_count": 10000, + "failure_count": 0, + "success_rate": 1.0, + "latency_ms": { + "p50": 10.0, + "p95": 25.0, + "p99": 60.0, + "count": 10000 + } + }, + { + "stage": "validation", + "success_count": 9960, + "failure_count": 40, + "success_rate": 0.996 + } + ], + "throughput_per_second": 1666.7 +} diff --git a/tests/pipeline-health.test.js b/tests/pipeline-health.test.js new file mode 100644 index 0000000..0111802 --- /dev/null +++ b/tests/pipeline-health.test.js @@ -0,0 +1,135 @@ +// Acceptance test for the pipeline-health schema family (issue #149). +// +// The milestone bullet asks for four things: end-to-end latency tracking, +// per-stage success rates, alerting on validation failures / backlog buildup, +// and automated canary testing of schema evolution from wasmagent-protocol +// updates. This test asserts the registered schemas jointly cover all four +// concerns and that the composed health fixture wires them together. Fixture +// validity against the JSON Schemas is already enforced by +// tests/conformance.py; this is the JS-side structural acceptance gate. +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +import { getSchema, index, schemas, canonicalIds } from '../index.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const IDS = ['pipeline-health-record', 'sla-metrics', 'backlog-status']; + +function fixture(...parts) { + return JSON.parse(readFileSync(join(here, 'fixtures', ...parts), 'utf8')); +} + +/** Every $ref string appearing anywhere in a schema document. */ +function collectRefs(node, out = []) { + if (Array.isArray(node)) { + for (const v of node) collectRefs(v, out); + } else if (node && typeof node === 'object') { + for (const [k, v] of Object.entries(node)) { + if (k === '$ref' && typeof v === 'string') out.push(v); + else collectRefs(v, out); + } + } + return out; +} + +// --- registration --------------------------------------------------------- + +test('all three pipeline schemas are registered and loadable', () => { + const registered = new Set(index.schemas.map((s) => s.id)); + for (const id of IDS) { + assert.ok(registered.has(id), `${id} missing from schemas/index.json`); + assert.ok(id in schemas, `${id} not loadable via the schemas map`); + } +}); + +test('each pipeline schema has a canonical $id under the pipeline family', () => { + const canonical = canonicalIds(); + const expected = { + 'pipeline-health-record': 'https://wasmagent.dev/schemas/pipeline/pipeline-health-record.schema.json', + 'sla-metrics': 'https://wasmagent.dev/schemas/pipeline/sla-metrics.schema.json', + 'backlog-status': 'https://wasmagent.dev/schemas/pipeline/backlog-status.schema.json', + }; + for (const [id, cid] of Object.entries(expected)) { + assert.equal(getSchema(id).$id, cid, `${id} $id mismatch`); + assert.ok(canonical.has(cid), `${cid} not in canonicalIds()`); + } +}); + +// --- concern 1 + 2: latency + per-stage success rates -------------------- + +test('sla-metrics captures end-to-end latency and per-stage success rates', () => { + const sla = getSchema('sla-metrics'); + const props = sla.properties; + assert.ok(props.end_to_end_latency_ms, 'sla-metrics must track end-to-end latency'); + assert.equal( + props.end_to_end_latency_ms.$ref, + '#/$defs/LatencyHistogram', + 'end-to-end latency must be a histogram with percentiles', + ); + assert.ok(props.per_stage, 'sla-metrics must carry a per_stage breakdown'); + const stageProps = props.per_stage.items.properties; + assert.ok(stageProps.success_rate, 'per_stage items must carry success_rate'); + assert.equal(stageProps.success_rate.minimum, 0); + assert.equal(stageProps.success_rate.maximum, 1); +}); + +// --- concern 3: alerting on validation failures + backlog buildup -------- + +test('pipeline-health-record alerts on validation failures and backlog buildup', () => { + const health = getSchema('pipeline-health-record'); + const alertKinds = health.properties.alerts.items.properties.kind.enum; + assert.ok(alertKinds.includes('validation_failure'), 'alerts must include validation_failure'); + assert.ok(alertKinds.includes('backlog_buildup'), 'alerts must include backlog_buildup'); + + // backlog-status feeds the backlog_buildup alert via a breach flag. + const backlog = getSchema('backlog-status'); + assert.ok( + 'breaches_threshold' in backlog.properties, + 'backlog-status must expose breaches_threshold', + ); + + // And the health record composes both sub-schemas by reference. + const refs = collectRefs(health); + assert.ok( + refs.includes('sla-metrics.schema.json'), + 'health record must $ref sla-metrics', + ); + assert.ok( + refs.includes('backlog-status.schema.json'), + 'health record must $ref backlog-status', + ); +}); + +// --- concern 4: automated canary testing of schema evolution ------------- + +test('pipeline-health-record carries a schema-evolution canary result', () => { + const health = getSchema('pipeline-health-record'); + assert.ok(health.properties.canary, 'health record must carry a canary block'); + const canaryProps = health.properties.canary.properties; + assert.ok( + canaryProps.protocol_version, + 'canary must name the protocol version under test', + ); + assert.deepEqual([...canaryProps.status.enum].sort(), ['fail', 'pass', 'skipped']); + assert.ok(canaryProps.failures, 'canary must be able to report per-fixture failures'); +}); + +// --- composition: the valid fixture ties all three together -------------- + +test('the valid pipeline-health fixture composes all three schemas coherently', () => { + const health = fixture('valid', 'pipeline-health-record', 'example.json'); + assert.equal(health.schema_version, 'pipeline-health-record/v1'); + assert.equal(health.sla.schema_version, 'sla-metrics/v1'); + assert.ok(health.sla.end_to_end_latency_ms, 'composed fixture carries end-to-end latency'); + assert.ok(health.sla.per_stage.length >= 1, 'composed fixture carries per-stage metrics'); + assert.ok( + health.sla.per_stage.every((s) => typeof s.success_rate === 'number'), + 'every per-stage entry carries a numeric success_rate', + ); + assert.equal(health.backlogs[0].schema_version, 'backlog-status/v1'); + assert.ok(health.alerts.some((a) => a.kind === 'validation_failure')); + assert.equal(typeof health.canary.protocol_version, 'string'); + assert.ok(['pass', 'fail', 'skipped'].includes(health.canary.status)); +});