use serde::{Deserialize, Serialize};
use crate::reference::CardRef;
use crate::vala::api::BifrostTableName;
use crate::vala::ids::RunId;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct ObservationEnvelope {
pub card_ref: CardRef,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub run_id: Option<RunId>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ObservationKind {
Record(RecordObservation),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct RecordObservation {
pub table: BifrostTableName,
}
#[cfg(test)]
mod observation_tests {
use crate::envelope::CardKind;
use crate::ids::{CardName, SpaceName};
use crate::reference::CardRef;
use crate::vala::api::BifrostTableName;
use crate::vala::ids::RunId;
use crate::vala::observation::{ObservationEnvelope, ObservationKind, RecordObservation};
use wyrd_semver::VersionBlock;
fn card_ref() -> CardRef {
CardRef {
kind: CardKind::Agent,
name: CardName::new("my-agent").unwrap(),
version: VersionBlock::parse("1.0.0").unwrap(),
space: Some(SpaceName::new("default").unwrap()),
uid: None,
}
}
fn record_obs() -> RecordObservation {
RecordObservation {
table: BifrostTableName::new("agent_runs"),
}
}
fn envelope_with_run_id() -> ObservationEnvelope {
ObservationEnvelope {
card_ref: card_ref(),
run_id: Some(RunId::from_string("test-run-1".to_owned())),
}
}
fn envelope_no_run_id() -> ObservationEnvelope {
ObservationEnvelope {
card_ref: card_ref(),
run_id: None,
}
}
#[test]
fn observation_envelope_round_trips() {
let env = envelope_with_run_id();
let json = serde_json::to_string(&env).unwrap();
let back: ObservationEnvelope = serde_json::from_str(&json).unwrap();
assert_eq!(env, back);
}
#[test]
fn observation_kind_round_trips() {
let kind = ObservationKind::Record(record_obs());
let json = serde_json::to_string(&kind).unwrap();
let back: ObservationKind = serde_json::from_str(&json).unwrap();
assert_eq!(kind, back);
}
#[test]
fn record_observation_round_trips() {
let rec = record_obs();
let json = serde_json::to_string(&rec).unwrap();
let back: RecordObservation = serde_json::from_str(&json).unwrap();
assert_eq!(rec, back);
}
#[test]
fn observation_kind_tagged_union_encoding() {
let kind = ObservationKind::Record(record_obs());
let value = serde_json::to_value(&kind).unwrap();
let obj = value.as_object().unwrap();
assert_eq!(
obj.get("kind").and_then(|v| v.as_str()),
Some("record"),
"ObservationKind must serialize with kind tag = 'record'"
);
}
#[test]
fn envelope_run_id_omitted_when_none() {
let env = envelope_no_run_id();
let value = serde_json::to_value(&env).unwrap();
let obj = value.as_object().unwrap();
assert!(
!obj.contains_key("run_id"),
"run_id must be absent from the wire when None"
);
}
#[test]
fn envelope_run_id_present_when_some() {
let env = envelope_with_run_id();
let value = serde_json::to_value(&env).unwrap();
let obj = value.as_object().unwrap();
assert!(
obj.contains_key("run_id"),
"run_id must appear on the wire when Some"
);
}
#[test]
fn envelope_no_record_id_field() {
let env = envelope_with_run_id();
let value = serde_json::to_value(&env).unwrap();
let obj = value.as_object().unwrap();
assert!(
!obj.contains_key("record_id"),
"ObservationEnvelope must not carry record_id (removed with vala-client)"
);
}
#[test]
fn envelope_card_ref_required() {
let missing = r#"{"run_id": "r1"}"#;
let err = serde_json::from_str::<ObservationEnvelope>(missing).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("card_ref") || msg.contains("missing field"),
"expected missing card_ref error, got: {msg}"
);
}
#[test]
fn observation_kind_is_closed_exhaustive_match() {
let kind = ObservationKind::Record(record_obs());
let _label = match kind {
ObservationKind::Record(_) => "record",
};
}
}