use std::sync::OnceLock;
use onemessagebus::{Read, Registry};
use onemessagebus_agent::registry::{EVENT_ENVELOPE_FAMILY, EVENT_ENVELOPE_READS};
pub use onemessagebus::{Kind as EventKind, MAX_PAYLOAD_TEXT_BYTES};
pub use onemessagebus_agent::event::{ArtifactRef, Envelope, Labels, Phase, Source};
pub const ENVELOPE_VERSION: u32 = EVENT_ENVELOPE_READS[0];
pub const ENVELOPE_VERSIONS_READ: &[u32] = EVENT_ENVELOPE_READS;
#[must_use]
pub(crate) fn written_at_a_known_version(envelope: &Envelope) -> bool {
static READ_AT: OnceLock<Vec<u32>> = OnceLock::new();
READ_AT
.get_or_init(|| {
registry()
.read_set(EVENT_ENVELOPE_FAMILY)
.into_iter()
.filter(|version| {
matches!(
registry().read_at(EVENT_ENVELOPE_FAMILY, *version),
Read::At(_)
)
})
.collect()
})
.contains(&envelope.v)
}
pub(crate) fn registry() -> &'static Registry {
static REGISTRY: OnceLock<Registry> = OnceLock::new();
REGISTRY.get_or_init(crate::payload::registry)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
pub enum PipelineKind {
RunStarted,
ConcurrentAcknowledged,
NodeReady,
NodeDispatched,
NodeSettled,
EditCommitted,
CommandAccepted,
EditRejected,
PlannerSurfaceQueued,
PlannerSurfaced,
PlannerReplied,
HumanAttested,
DriverAdopted,
RunStopped,
QuietWorker,
NodeHeld,
NodeUnheld,
DecisionPending,
DecisionCleared,
CrossDagSatisfied,
UpstreamModified,
CompletionRequested,
ReleaseWait,
ReleaseArrived,
ReleaseAdopted,
CriterionChecked,
BodyNotDrafted,
NoteShown,
RunHookFired,
RunHookFinished,
RunHookWithheld,
}
impl PipelineKind {
pub const fn as_str(self) -> &'static str {
match self {
Self::RunStarted => "run-started",
Self::ConcurrentAcknowledged => "concurrent-acknowledged",
Self::NodeReady => "node-ready",
Self::NodeDispatched => "node-dispatched",
Self::NodeSettled => "node-settled",
Self::EditCommitted => "edit-committed",
Self::CommandAccepted => "command-accepted",
Self::EditRejected => "edit-rejected",
Self::PlannerSurfaceQueued => "planner-surface-queued",
Self::PlannerSurfaced => "planner-surfaced",
Self::PlannerReplied => "planner-replied",
Self::HumanAttested => "human-attested",
Self::DriverAdopted => "driver-adopted",
Self::RunStopped => "run-stopped",
Self::QuietWorker => "quiet-worker",
Self::NodeHeld => "node-held",
Self::NodeUnheld => "node-unheld",
Self::DecisionPending => "decision-pending",
Self::DecisionCleared => "decision-cleared",
Self::CrossDagSatisfied => "cross-dag-satisfied",
Self::UpstreamModified => "upstream-modified",
Self::CompletionRequested => "completion-requested",
Self::ReleaseWait => "release-wait",
Self::ReleaseArrived => "release-arrived",
Self::ReleaseAdopted => "release-adopted",
Self::CriterionChecked => "criterion-checked",
Self::BodyNotDrafted => "body-not-drafted",
Self::NoteShown => "note-shown",
Self::RunHookFired => "run-hook-fired",
Self::RunHookFinished => "run-hook-finished",
Self::RunHookWithheld => "run-hook-withheld",
}
}
pub fn from_wire(kind: &EventKind) -> Option<Self> {
PIPELINE_KINDS
.iter()
.copied()
.find(|candidate| candidate.as_str() == kind.0)
}
}
impl std::fmt::Display for PipelineKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl From<PipelineKind> for EventKind {
fn from(kind: PipelineKind) -> Self {
Self(kind.as_str().to_string())
}
}
pub const PIPELINE_KINDS: &[PipelineKind] = &[
PipelineKind::RunStarted,
PipelineKind::ConcurrentAcknowledged,
PipelineKind::NodeReady,
PipelineKind::NodeDispatched,
PipelineKind::NodeSettled,
PipelineKind::EditCommitted,
PipelineKind::CommandAccepted,
PipelineKind::EditRejected,
PipelineKind::PlannerSurfaceQueued,
PipelineKind::PlannerSurfaced,
PipelineKind::PlannerReplied,
PipelineKind::HumanAttested,
PipelineKind::DriverAdopted,
PipelineKind::RunStopped,
PipelineKind::QuietWorker,
PipelineKind::NodeHeld,
PipelineKind::NodeUnheld,
PipelineKind::DecisionPending,
PipelineKind::DecisionCleared,
PipelineKind::CrossDagSatisfied,
PipelineKind::UpstreamModified,
PipelineKind::CompletionRequested,
PipelineKind::ReleaseWait,
PipelineKind::ReleaseArrived,
PipelineKind::ReleaseAdopted,
PipelineKind::CriterionChecked,
PipelineKind::BodyNotDrafted,
PipelineKind::NoteShown,
PipelineKind::RunHookFired,
PipelineKind::RunHookFinished,
PipelineKind::RunHookWithheld,
];
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
)]
#[serde(transparent)]
pub struct ArtifactId(pub String);
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
const GOLDEN: &str = include_str!("../tests/golden/envelope-v2.json");
const GOLDEN_BEFORE: &str = include_str!("../tests/golden/envelope-v1.json");
#[test]
fn the_envelope_this_build_writes_is_the_committed_golden() {
let golden: serde_json::Value = serde_json::from_str(GOLDEN).expect("the golden is JSON");
assert_eq!(
golden["v"],
json!(ENVELOPE_VERSION),
"the golden is not at the version this build writes. Bump ENVELOPE_VERSION and \
add tests/golden/envelope-v<n>.json together, keeping the older file as the \
read-compatibility reference"
);
let envelope: Envelope = serde_json::from_str(GOLDEN).expect("it parses");
assert_eq!(envelope.v, ENVELOPE_VERSION);
assert_eq!(envelope.source, Source::Pipeline);
assert_eq!(envelope.kind, EventKind("edit-committed".into()));
assert!(written_at_a_known_version(&envelope));
assert_eq!(
serde_json::to_string_pretty(&envelope).expect("it serializes"),
GOLDEN.trim_end(),
"the envelope changed shape. Bump ENVELOPE_VERSION, add the golden for the new \
one, and keep this file as what the older version looked like"
);
}
#[test]
fn the_envelope_version_before_this_one_is_still_read_and_never_restamped() {
let before: serde_json::Value =
serde_json::from_str(GOLDEN_BEFORE).expect("the golden is JSON");
assert_eq!(before["v"], json!(1));
assert!(
ENVELOPE_VERSIONS_READ.contains(&1),
"this build no longer reads the version its committed fixture is written at"
);
let envelope: Envelope = serde_json::from_str(GOLDEN_BEFORE).expect("it parses");
assert!(written_at_a_known_version(&envelope));
assert_eq!(envelope.v, 1, "a version this build read was rewritten");
assert_eq!(
serde_json::to_string_pretty(&envelope).expect("it serializes"),
GOLDEN_BEFORE.trim_end()
);
let ahead: Envelope = serde_json::from_value(json!({
"v": ENVELOPE_VERSION + 1,
"ts": "2026-09-09T04:00:00.000Z",
"stream": "onepipeline-7f3a",
"seq": 43,
"source": "pipeline",
"kind": "edit-committed",
"labels": {},
"payload": {},
"artifacts": []
}))
.expect("a newer build's record still parses structurally");
assert!(!written_at_a_known_version(&ahead));
}
#[test]
fn the_registry_answers_the_envelope_versions_this_build_writes_and_reads() {
let registry = registry();
assert_eq!(
registry.read_set(EVENT_ENVELOPE_FAMILY),
ENVELOPE_VERSIONS_READ.to_vec()
);
assert_eq!(
registry.writes(EVENT_ENVELOPE_FAMILY),
Some(ENVELOPE_VERSION)
);
assert_eq!(Source::Pipeline.write_version(), ENVELOPE_VERSION);
for version in ENVELOPE_VERSIONS_READ {
assert_eq!(
registry.read_at(EVENT_ENVELOPE_FAMILY, *version),
Read::At(ENVELOPE_VERSION)
);
}
match registry.read_at(EVENT_ENVELOPE_FAMILY, ENVELOPE_VERSION + 1) {
Read::Unknown(unknown) => {
assert_eq!(unknown.declared, ENVELOPE_VERSION + 1);
assert_eq!(unknown.read_set, ENVELOPE_VERSIONS_READ.to_vec());
}
Read::At(at) => panic!("a version nothing published was read at {at}"),
}
}
#[test]
fn an_envelopes_optional_fields_round_trip_and_are_omitted_when_empty() {
let bare = json!({
"v": ENVELOPE_VERSION,
"ts": "2026-09-09T04:00:00.000Z",
"stream": "onepipeline-7f3a",
"seq": 1,
"source": "pipeline",
"kind": "node-ready",
"labels": {},
"payload": {},
"artifacts": []
});
let envelope: Envelope = serde_json::from_value(bare.clone()).expect("it parses");
assert_eq!(envelope.dimensions.phase, None);
assert_eq!(serde_json::to_value(&envelope).expect("serializes"), bare);
let mut with = bare.clone();
with["phase"] = json!("release");
let envelope: Envelope = serde_json::from_value(with.clone()).expect("it parses");
assert_eq!(envelope.dimensions.phase, Some(Phase::Release));
assert_eq!(serde_json::to_value(&envelope).expect("serializes"), with);
let minimal = json!({
"v": ENVELOPE_VERSION,
"ts": "2026-09-09T04:00:00.000Z",
"stream": "onepipeline-7f3a",
"seq": 2,
"source": "pipeline",
"kind": "node-ready"
});
let envelope: Envelope = serde_json::from_value(minimal).expect("it parses");
assert!(envelope.payload.is_empty() && envelope.artifacts.is_empty());
assert_eq!(envelope.labels, Labels::default());
}
}