use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fmt;
use crate::phase::ProcessPhase;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ExportSpec {
pub source: ArtifactSource,
pub channel: VectorChannel,
#[serde(default)]
pub when: ExportTrigger,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub experiment_id_override: Option<String>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ArtifactSource {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub receipts: Option<ReceiptsSource>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub test_report: Option<TestReportSource>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub process_snapshot: Option<ProcessSnapshotSource>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub run_marker: Option<RunMarkerSource>,
}
#[derive(Clone, Debug)]
pub enum ArtifactVariant<'a> {
Receipts(&'a ReceiptsSource),
TestReport(&'a TestReportSource),
ProcessSnapshot(&'a ProcessSnapshotSource),
RunMarker(&'a RunMarkerSource),
}
impl ArtifactVariant<'_> {
pub fn kind(&self) -> ArtifactKind {
match self {
Self::Receipts(_) => ArtifactKind::Receipts,
Self::TestReport(_) => ArtifactKind::TestReport,
Self::ProcessSnapshot(_) => ArtifactKind::ProcessSnapshot,
Self::RunMarker(_) => ArtifactKind::RunMarker,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, tatara_lisp::DeriveClosedSet)]
#[closed_set(via = "as_str", display, generate_unknown)]
pub enum ArtifactKind {
Receipts,
TestReport,
ProcessSnapshot,
RunMarker,
}
impl ArtifactKind {
pub const ALL: [Self; 4] = [
Self::Receipts,
Self::TestReport,
Self::ProcessSnapshot,
Self::RunMarker,
];
pub const fn as_str(self) -> &'static str {
match self {
Self::Receipts => "receipts",
Self::TestReport => "testReport",
Self::ProcessSnapshot => "processSnapshot",
Self::RunMarker => "runMarker",
}
}
pub fn select<'a>(self, source: &'a ArtifactSource) -> Option<ArtifactVariant<'a>> {
match self {
Self::Receipts => source.receipts.as_ref().map(ArtifactVariant::Receipts),
Self::TestReport => source.test_report.as_ref().map(ArtifactVariant::TestReport),
Self::ProcessSnapshot => source
.process_snapshot
.as_ref()
.map(ArtifactVariant::ProcessSnapshot),
Self::RunMarker => source.run_marker.as_ref().map(ArtifactVariant::RunMarker),
}
}
}
#[derive(Clone, Copy, Debug, thiserror::Error, PartialEq, Eq)]
pub enum ArtifactError {
#[error("artifact source has no variant set (one of {0} required)")]
Empty(&'static str),
#[error("artifact source has multiple variants set; exactly one required")]
Ambiguous,
}
const ARTIFACT_KIND_LIST: &str = "receipts/testReport/processSnapshot/runMarker";
impl ArtifactSource {
pub fn variant(&self) -> Result<ArtifactVariant<'_>, ArtifactError> {
use crate::tagged_union::{resolve, ResolveError};
resolve(ArtifactKind::ALL.into_iter().map(|k| k.select(self))).map_err(|e| match e {
ResolveError::None => ArtifactError::Empty(ARTIFACT_KIND_LIST),
ResolveError::Many => ArtifactError::Ambiguous,
})
}
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ReceiptsSource {}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct TestReportSource {
pub configmap: String,
pub key: String,
#[serde(default)]
pub format: ReportFormat,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ProcessSnapshotSource {
#[serde(default)]
pub include_attestation_chain: bool,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct RunMarkerSource {
#[serde(default)]
pub labels: BTreeMap<String, String>,
}
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
JsonSchema,
Default,
tatara_lisp::DeriveClosedSet,
)]
#[serde(rename_all = "PascalCase")]
#[closed_set(via = "as_str", generate_unknown)]
pub enum ReportFormat {
Junit,
TapV13,
NdJson,
#[default]
Raw,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ReportPayloadShape {
NdJsonLines,
OpaqueBytes,
}
impl ReportPayloadShape {
pub const ALL: [Self; 2] = [Self::NdJsonLines, Self::OpaqueBytes];
pub const fn as_str(self) -> &'static str {
match self {
Self::NdJsonLines => "NdJsonLines",
Self::OpaqueBytes => "OpaqueBytes",
}
}
pub const fn payload_field(self) -> &'static str {
match self {
Self::NdJsonLines => "ndjson",
Self::OpaqueBytes => "raw_b64",
}
}
}
impl fmt::Display for ReportPayloadShape {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl ReportFormat {
pub const ALL: [Self; 4] = [Self::Junit, Self::TapV13, Self::NdJson, Self::Raw];
pub const fn as_str(self) -> &'static str {
match self {
Self::Junit => "Junit",
Self::TapV13 => "TapV13",
Self::NdJson => "NdJson",
Self::Raw => "Raw",
}
}
pub const fn payload_shape(self) -> ReportPayloadShape {
match self {
Self::NdJson => ReportPayloadShape::NdJsonLines,
Self::Junit | Self::TapV13 | Self::Raw => ReportPayloadShape::OpaqueBytes,
}
}
}
impl fmt::Display for ReportFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct VectorChannel {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub http_event: Option<HttpEventChannel>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nats_subject: Option<NatsSubjectChannel>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stdout: Option<StdoutChannel>,
}
#[derive(Clone, Debug)]
pub enum ChannelVariant<'a> {
HttpEvent(&'a HttpEventChannel),
NatsSubject(&'a NatsSubjectChannel),
Stdout(&'a StdoutChannel),
}
impl ChannelVariant<'_> {
pub fn kind(&self) -> ChannelKind {
match self {
Self::HttpEvent(_) => ChannelKind::HttpEvent,
Self::NatsSubject(_) => ChannelKind::NatsSubject,
Self::Stdout(_) => ChannelKind::Stdout,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, tatara_lisp::DeriveClosedSet)]
#[closed_set(via = "as_str", generate_unknown)]
pub enum ChannelKind {
HttpEvent,
NatsSubject,
Stdout,
}
impl ChannelKind {
pub const ALL: [Self; 3] = [Self::HttpEvent, Self::NatsSubject, Self::Stdout];
pub const fn as_str(self) -> &'static str {
match self {
Self::HttpEvent => "httpEvent",
Self::NatsSubject => "natsSubject",
Self::Stdout => "stdout",
}
}
pub fn select<'a>(self, channel: &'a VectorChannel) -> Option<ChannelVariant<'a>> {
match self {
Self::HttpEvent => channel.http_event.as_ref().map(ChannelVariant::HttpEvent),
Self::NatsSubject => channel
.nats_subject
.as_ref()
.map(ChannelVariant::NatsSubject),
Self::Stdout => channel.stdout.as_ref().map(ChannelVariant::Stdout),
}
}
}
impl fmt::Display for ChannelKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, thiserror::Error, PartialEq, Eq)]
pub enum ChannelError {
#[error("vector channel has no variant set (one of {0} required)")]
Empty(&'static str),
#[error("vector channel has multiple variants set; exactly one required")]
Ambiguous,
}
const CHANNEL_KIND_LIST: &str = "httpEvent/natsSubject/stdout";
impl VectorChannel {
pub fn variant(&self) -> Result<ChannelVariant<'_>, ChannelError> {
use crate::tagged_union::{resolve, ResolveError};
resolve(ChannelKind::ALL.into_iter().map(|k| k.select(self))).map_err(|e| match e {
ResolveError::None => ChannelError::Empty(CHANNEL_KIND_LIST),
ResolveError::Many => ChannelError::Ambiguous,
})
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct HttpEventChannel {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub endpoint: Option<String>,
pub signal_type: String,
}
pub const DEFAULT_VECTOR_INGEST: &str = "http://vector.observability.svc.cluster.local:8080";
impl HttpEventChannel {
pub fn resolved_endpoint(&self) -> &str {
self.endpoint.as_deref().unwrap_or(DEFAULT_VECTOR_INGEST)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct NatsSubjectChannel {
pub subject: String,
pub stream: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
}
pub const DEFAULT_NATS_URL: &str = "nats://nats.observability.svc.cluster.local:4222";
impl NatsSubjectChannel {
pub fn resolved_url(&self) -> &str {
self.url.as_deref().unwrap_or(DEFAULT_NATS_URL)
}
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct StdoutChannel {
#[serde(default)]
pub pretty: bool,
}
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
JsonSchema,
Default,
tatara_lisp::DeriveClosedSet,
)]
#[serde(rename_all = "PascalCase")]
#[closed_set(via = "as_str", generate_unknown)]
pub enum ExportTrigger {
#[default]
OnAttested,
OnFailed,
Always,
}
impl ExportTrigger {
pub const ALL: [Self; 3] = [Self::OnAttested, Self::OnFailed, Self::Always];
pub const fn as_str(self) -> &'static str {
match self {
Self::OnAttested => "OnAttested",
Self::OnFailed => "OnFailed",
Self::Always => "Always",
}
}
pub const fn fires_on(self, phase: ProcessPhase) -> bool {
match phase {
ProcessPhase::Attested => matches!(self, Self::OnAttested | Self::Always),
ProcessPhase::Failed => matches!(self, Self::OnFailed | Self::Always),
ProcessPhase::Pending
| ProcessPhase::Forking
| ProcessPhase::Execing
| ProcessPhase::Running
| ProcessPhase::Reconverging
| ProcessPhase::Releasing
| ProcessPhase::Exiting
| ProcessPhase::Zombie
| ProcessPhase::Reaped => false,
}
}
pub const fn fires_on_attested(self) -> bool {
self.fires_on(ProcessPhase::Attested)
}
pub const fn fires_on_failed(self) -> bool {
self.fires_on(ProcessPhase::Failed)
}
}
impl fmt::Display for ExportTrigger {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn artifact_source_empty_errors() {
let s = ArtifactSource::default();
match s.variant().unwrap_err() {
ArtifactError::Empty(list) => assert_eq!(list, ARTIFACT_KIND_LIST),
other => panic!("expected Empty, got {other:?}"),
}
}
#[test]
fn artifact_source_receipts_resolves() {
let s = ArtifactSource {
receipts: Some(ReceiptsSource::default()),
..ArtifactSource::default()
};
assert!(matches!(s.variant().unwrap(), ArtifactVariant::Receipts(_)));
}
#[test]
fn artifact_source_two_variants_ambiguous() {
let s = ArtifactSource {
receipts: Some(ReceiptsSource::default()),
test_report: Some(TestReportSource {
configmap: "x".into(),
key: "y".into(),
format: ReportFormat::Junit,
namespace: None,
}),
..ArtifactSource::default()
};
assert_eq!(s.variant().unwrap_err(), ArtifactError::Ambiguous);
}
#[test]
fn vector_channel_empty_errors() {
let c = VectorChannel::default();
match c.variant().unwrap_err() {
ChannelError::Empty(list) => assert_eq!(list, CHANNEL_KIND_LIST),
other => panic!("expected Empty, got {other:?}"),
}
}
#[test]
fn vector_channel_resolves_http_event() {
let c = VectorChannel {
http_event: Some(HttpEventChannel {
endpoint: None,
signal_type: "test-report".into(),
}),
..VectorChannel::default()
};
match c.variant().unwrap() {
ChannelVariant::HttpEvent(h) => {
assert_eq!(h.signal_type, "test-report");
assert_eq!(h.resolved_endpoint(), DEFAULT_VECTOR_INGEST);
}
other => panic!("expected HttpEvent, got {other:?}"),
}
}
#[test]
fn vector_channel_resolves_nats_subject() {
let c = VectorChannel {
nats_subject: Some(NatsSubjectChannel {
subject: "pleme.pleme-dev.ephemeral.{{run_id}}.receipt".into(),
stream: "EPHEMERAL_RECEIPTS".into(),
url: None,
}),
..VectorChannel::default()
};
match c.variant().unwrap() {
ChannelVariant::NatsSubject(n) => {
assert_eq!(n.stream, "EPHEMERAL_RECEIPTS");
assert_eq!(n.resolved_url(), DEFAULT_NATS_URL);
}
other => panic!("expected NatsSubject, got {other:?}"),
}
}
#[test]
fn export_trigger_fire_logic() {
assert!(ExportTrigger::OnAttested.fires_on_attested());
assert!(!ExportTrigger::OnAttested.fires_on_failed());
assert!(ExportTrigger::OnFailed.fires_on_failed());
assert!(!ExportTrigger::OnFailed.fires_on_attested());
assert!(ExportTrigger::Always.fires_on_attested());
assert!(ExportTrigger::Always.fires_on_failed());
}
#[test]
fn export_trigger_is_well_formed_closed_set() {
tatara_lisp::assert_closed_set_well_formed::<ExportTrigger>();
}
#[test]
fn export_trigger_as_str_matches_serde() {
for trigger in ExportTrigger::ALL {
let serialized = serde_json::to_string(&trigger).expect("serialize");
let unquoted = serialized
.trim_start_matches('"')
.trim_end_matches('"')
.to_string();
assert_eq!(
unquoted,
trigger.as_str(),
"as_str drift for {trigger:?}: as_str={} serde={unquoted}",
trigger.as_str()
);
}
}
#[test]
fn export_trigger_display_matches_as_str() {
for trigger in ExportTrigger::ALL {
assert_eq!(trigger.to_string(), trigger.as_str());
}
}
#[test]
fn unknown_export_trigger_errors() {
use std::str::FromStr;
for bad in ["onAttested", "ALWAYS", "Never", "OnSuccess"] {
let err = ExportTrigger::from_str(bad).unwrap_err();
assert_eq!(err.0, bad, "error payload should echo input verbatim");
}
}
#[test]
fn export_trigger_fires_on_truth_table() {
use crate::phase::ProcessPhase::{
Attested, Execing, Exiting, Failed, Forking, Pending, Reaped, Reconverging, Releasing,
Running, Zombie,
};
let table: &[(ExportTrigger, &[(crate::phase::ProcessPhase, bool)])] = &[
(
ExportTrigger::OnAttested,
&[
(Attested, true),
(Failed, false),
(Pending, false),
(Forking, false),
(Execing, false),
(Running, false),
(Reconverging, false),
(Releasing, false),
(Exiting, false),
(Zombie, false),
(Reaped, false),
],
),
(
ExportTrigger::OnFailed,
&[
(Attested, false),
(Failed, true),
(Pending, false),
(Forking, false),
(Execing, false),
(Running, false),
(Reconverging, false),
(Releasing, false),
(Exiting, false),
(Zombie, false),
(Reaped, false),
],
),
(
ExportTrigger::Always,
&[
(Attested, true),
(Failed, true),
(Pending, false),
(Forking, false),
(Execing, false),
(Running, false),
(Reconverging, false),
(Releasing, false),
(Exiting, false),
(Zombie, false),
(Reaped, false),
],
),
];
assert_eq!(table.len(), ExportTrigger::ALL.len());
for (_, row) in table {
assert_eq!(row.len(), crate::phase::ProcessPhase::ALL.len());
}
for (trigger, row) in table {
for (phase, expected) in *row {
assert_eq!(
trigger.fires_on(*phase),
*expected,
"fires_on({trigger:?}, {phase:?}) drift"
);
}
}
}
#[test]
fn export_trigger_legacy_predicates_delegate_to_phase_dispatch() {
for trigger in ExportTrigger::ALL {
assert_eq!(
trigger.fires_on_attested(),
trigger.fires_on(crate::phase::ProcessPhase::Attested),
"legacy fires_on_attested drift for {trigger:?}"
);
assert_eq!(
trigger.fires_on_failed(),
trigger.fires_on(crate::phase::ProcessPhase::Failed),
"legacy fires_on_failed drift for {trigger:?}"
);
}
}
#[test]
fn report_format_is_well_formed_closed_set() {
tatara_lisp::assert_closed_set_well_formed::<ReportFormat>();
}
#[test]
fn report_format_as_str_matches_serde() {
for format in ReportFormat::ALL {
let serialized = serde_json::to_string(&format).expect("serialize");
let unquoted = serialized
.trim_start_matches('"')
.trim_end_matches('"')
.to_string();
assert_eq!(
unquoted,
format.as_str(),
"as_str drift for {format:?}: as_str={} serde={unquoted}",
format.as_str()
);
}
}
#[test]
fn report_format_display_matches_as_str() {
for format in ReportFormat::ALL {
assert_eq!(format.to_string(), format.as_str());
}
}
#[test]
fn unknown_report_format_errors() {
use std::str::FromStr;
for bad in ["junit", "JUNIT", "tap", "Yaml", "TomlV1"] {
let err = ReportFormat::from_str(bad).unwrap_err();
assert_eq!(err.0, bad, "error payload should echo input verbatim");
}
}
#[test]
fn report_format_payload_shape_truth_table() {
let table: &[(ReportFormat, ReportPayloadShape)] = &[
(ReportFormat::Junit, ReportPayloadShape::OpaqueBytes),
(ReportFormat::TapV13, ReportPayloadShape::OpaqueBytes),
(ReportFormat::NdJson, ReportPayloadShape::NdJsonLines),
(ReportFormat::Raw, ReportPayloadShape::OpaqueBytes),
];
assert_eq!(table.len(), ReportFormat::ALL.len());
for (format, expected) in table {
assert_eq!(
format.payload_shape(),
*expected,
"payload_shape({format:?}) drift"
);
}
}
#[test]
fn report_payload_shape_reachable_from_some_report_format() {
for shape in ReportPayloadShape::ALL {
let reachable = ReportFormat::ALL.iter().any(|f| f.payload_shape() == shape);
assert!(
reachable,
"{shape:?} is in ReportPayloadShape::ALL but no ReportFormat projects to it"
);
}
}
#[test]
fn report_payload_shape_all_enumerates_each_variant_exactly_once() {
let mut seen = std::collections::HashSet::new();
for shape in ReportPayloadShape::ALL {
assert!(seen.insert(shape), "duplicate variant in ALL: {shape:?}");
}
assert_eq!(seen.len(), ReportPayloadShape::ALL.len());
for shape in [
ReportPayloadShape::NdJsonLines,
ReportPayloadShape::OpaqueBytes,
] {
assert!(
ReportPayloadShape::ALL.contains(&shape),
"{shape:?} declared but not in ALL"
);
}
}
#[test]
fn report_payload_shape_as_str_unique_per_variant() {
let mut seen = std::collections::HashSet::new();
for shape in ReportPayloadShape::ALL {
assert!(
seen.insert(shape.as_str()),
"as_str collision: {shape:?} → {:?}",
shape.as_str()
);
}
assert_eq!(seen.len(), ReportPayloadShape::ALL.len());
}
#[test]
fn report_payload_shape_display_matches_as_str() {
for shape in ReportPayloadShape::ALL {
assert_eq!(shape.to_string(), shape.as_str());
}
}
#[test]
fn report_payload_shape_payload_field_unique_per_variant() {
let mut seen = std::collections::HashSet::new();
for shape in ReportPayloadShape::ALL {
assert!(
seen.insert(shape.payload_field()),
"payload_field collision: {shape:?} → {:?}",
shape.payload_field()
);
}
assert_eq!(seen.len(), ReportPayloadShape::ALL.len());
}
#[test]
fn report_payload_shape_payload_field_truth_table() {
let table: &[(ReportPayloadShape, &str)] = &[
(ReportPayloadShape::NdJsonLines, "ndjson"),
(ReportPayloadShape::OpaqueBytes, "raw_b64"),
];
assert_eq!(table.len(), ReportPayloadShape::ALL.len());
for (shape, expected) in table {
assert_eq!(
shape.payload_field(),
*expected,
"payload_field({shape:?}) drift"
);
}
}
#[test]
fn report_payload_shape_payload_field_is_a_single_segment() {
for shape in ReportPayloadShape::ALL {
let field = shape.payload_field();
assert!(
!field.is_empty(),
"payload_field({shape:?}) is empty — embed site has no destination"
);
assert!(
!field.contains('.'),
"payload_field({shape:?}) contains '.' ({field:?}) — would flatten the embed into payload's parent map"
);
}
}
#[test]
fn export_spec_serde_round_trip() {
let spec = ExportSpec {
source: ArtifactSource {
test_report: Some(TestReportSource {
configmap: "akeyless-test-results".into(),
key: "junit.xml".into(),
format: ReportFormat::Junit,
namespace: None,
}),
..ArtifactSource::default()
},
channel: VectorChannel {
http_event: Some(HttpEventChannel {
endpoint: None,
signal_type: "test-report".into(),
}),
..VectorChannel::default()
},
when: ExportTrigger::Always,
experiment_id_override: Some("akeyless-run-2026-05-20".into()),
};
let yaml = serde_yaml::to_string(&spec).unwrap();
assert!(yaml.contains("source:"));
assert!(yaml.contains("testReport:"));
assert!(yaml.contains("configmap: akeyless-test-results"));
assert!(yaml.contains("format: Junit"));
assert!(yaml.contains("channel:"));
assert!(yaml.contains("httpEvent:"));
assert!(yaml.contains("signalType: test-report"));
assert!(yaml.contains("when: Always"));
assert!(yaml.contains("experimentIdOverride: akeyless-run-2026-05-20"));
let back: ExportSpec = serde_yaml::from_str(&yaml).unwrap();
assert!(back.source.test_report.is_some());
assert!(back.channel.http_event.is_some());
assert_eq!(back.when, ExportTrigger::Always);
}
#[test]
fn run_marker_labels_round_trip() {
let mut labels = BTreeMap::new();
labels.insert("run-id".into(), "akeyless-run-2026-05-20".into());
labels.insert("phase".into(), "end".into());
let spec = ExportSpec {
source: ArtifactSource {
run_marker: Some(RunMarkerSource { labels }),
..ArtifactSource::default()
},
channel: VectorChannel {
http_event: Some(HttpEventChannel {
endpoint: None,
signal_type: "ephemeral-marker".into(),
}),
..VectorChannel::default()
},
when: ExportTrigger::Always,
experiment_id_override: None,
};
let yaml = serde_yaml::to_string(&spec).unwrap();
assert!(yaml.contains("runMarker:"));
assert!(yaml.contains("run-id: akeyless-run-2026-05-20"));
let back: ExportSpec = serde_yaml::from_str(&yaml).unwrap();
let rm = back.source.run_marker.unwrap();
assert_eq!(rm.labels["phase"], "end");
}
#[test]
fn default_endpoints_are_stable_constants() {
assert_eq!(
DEFAULT_VECTOR_INGEST,
"http://vector.observability.svc.cluster.local:8080"
);
assert_eq!(
DEFAULT_NATS_URL,
"nats://nats.observability.svc.cluster.local:4222"
);
}
#[test]
fn artifact_kind_is_well_formed_closed_set() {
tatara_lisp::assert_closed_set_well_formed::<ArtifactKind>();
}
#[test]
fn artifact_kind_as_str_matches_field_name() {
for kind in ArtifactKind::ALL {
let s = single_slot_source(kind);
let yaml = serde_yaml::to_string(&s).expect("serialize");
let key = kind.as_str();
assert!(
yaml.contains(&format!("{key}:")),
"as_str(={key:?}) for {kind:?} not present in serialized YAML:\n{yaml}"
);
}
}
#[test]
fn artifact_kind_canonical_names_pinned() {
assert_eq!(ArtifactKind::Receipts.as_str(), "receipts");
assert_eq!(ArtifactKind::TestReport.as_str(), "testReport");
assert_eq!(ArtifactKind::ProcessSnapshot.as_str(), "processSnapshot");
assert_eq!(ArtifactKind::RunMarker.as_str(), "runMarker");
}
#[test]
fn artifact_kind_display_matches_as_str() {
for kind in ArtifactKind::ALL {
assert_eq!(kind.to_string(), kind.as_str());
}
}
#[test]
fn unknown_artifact_kind_errors() {
use std::str::FromStr;
for bad in [
"Receipts",
"test_report",
"RECEIPTS",
"snapshot",
"marker",
"Junit",
"OnAttested",
"NdJsonLines",
] {
let err = ArtifactKind::from_str(bad).unwrap_err();
assert_eq!(err.0, bad, "error payload should echo input verbatim");
}
}
#[test]
fn artifact_kind_round_trips_through_variant_kind() {
for kind in ArtifactKind::ALL {
let s = single_slot_source(kind);
let v = kind.select(&s).expect("populated slot must select");
assert_eq!(v.kind(), kind, "round-trip failed for {kind:?}");
assert_eq!(
s.variant().expect("exactly-one variant").kind(),
kind,
"variant() resolver disagreed on {kind:?}"
);
}
}
#[test]
fn artifact_kind_select_returns_none_for_unset_slot() {
let empty = ArtifactSource::default();
for kind in ArtifactKind::ALL {
assert!(
kind.select(&empty).is_none(),
"{kind:?} reported populated on a default ArtifactSource"
);
}
}
#[test]
fn artifact_error_empty_lists_every_kind_in_canonical_order() {
assert_eq!(
<ArtifactKind as tatara_lisp::ClosedSet>::labels_joined("/"),
ARTIFACT_KIND_LIST,
);
}
#[test]
fn artifact_source_two_slots_is_ambiguous_across_every_pair() {
for a in ArtifactKind::ALL {
for b in ArtifactKind::ALL {
if a == b {
continue;
}
let s = two_slot_source(a, b);
assert_eq!(
s.variant().unwrap_err(),
ArtifactError::Ambiguous,
"({a:?}, {b:?}) should resolve Ambiguous"
);
}
}
}
#[test]
fn channel_kind_is_well_formed_closed_set() {
tatara_lisp::assert_closed_set_well_formed::<ChannelKind>();
}
#[test]
fn channel_kind_as_str_matches_field_name() {
for kind in ChannelKind::ALL {
let c = single_slot_channel(kind);
let yaml = serde_yaml::to_string(&c).expect("serialize");
let key = kind.as_str();
assert!(
yaml.contains(&format!("{key}:")),
"as_str(={key:?}) for {kind:?} not present in serialized YAML:\n{yaml}"
);
}
}
#[test]
fn channel_kind_canonical_names_pinned() {
assert_eq!(ChannelKind::HttpEvent.as_str(), "httpEvent");
assert_eq!(ChannelKind::NatsSubject.as_str(), "natsSubject");
assert_eq!(ChannelKind::Stdout.as_str(), "stdout");
}
#[test]
fn channel_kind_display_matches_as_str() {
for kind in ChannelKind::ALL {
assert_eq!(kind.to_string(), kind.as_str());
}
}
#[test]
fn unknown_channel_kind_errors() {
use std::str::FromStr;
for bad in [
"HttpEvent",
"http_event",
"HTTPEVENT",
"nats",
"STDOUT",
"Receipts",
"OnAttested",
"Junit",
"NdJsonLines",
] {
let err = ChannelKind::from_str(bad).unwrap_err();
assert_eq!(err.0, bad, "error payload should echo input verbatim");
}
}
#[test]
fn channel_kind_round_trips_through_variant_kind() {
for kind in ChannelKind::ALL {
let c = single_slot_channel(kind);
let v = kind.select(&c).expect("populated slot must select");
assert_eq!(v.kind(), kind, "round-trip failed for {kind:?}");
assert_eq!(
c.variant().expect("exactly-one variant").kind(),
kind,
"variant() resolver disagreed on {kind:?}"
);
}
}
#[test]
fn channel_kind_select_returns_none_for_unset_slot() {
let empty = VectorChannel::default();
for kind in ChannelKind::ALL {
assert!(
kind.select(&empty).is_none(),
"{kind:?} reported populated on a default VectorChannel"
);
}
}
#[test]
fn channel_error_empty_lists_every_kind_in_canonical_order() {
assert_eq!(
<ChannelKind as tatara_lisp::ClosedSet>::labels_joined("/"),
CHANNEL_KIND_LIST,
);
}
#[test]
fn vector_channel_two_slots_is_ambiguous_across_every_pair() {
for a in ChannelKind::ALL {
for b in ChannelKind::ALL {
if a == b {
continue;
}
let c = two_slot_channel(a, b);
assert_eq!(
c.variant().unwrap_err(),
ChannelError::Ambiguous,
"({a:?}, {b:?}) should resolve Ambiguous"
);
}
}
}
fn single_slot_channel(kind: ChannelKind) -> VectorChannel {
match kind {
ChannelKind::HttpEvent => VectorChannel {
http_event: Some(HttpEventChannel {
endpoint: None,
signal_type: "x".into(),
}),
..VectorChannel::default()
},
ChannelKind::NatsSubject => VectorChannel {
nats_subject: Some(NatsSubjectChannel {
subject: "s".into(),
stream: "S".into(),
url: None,
}),
..VectorChannel::default()
},
ChannelKind::Stdout => VectorChannel {
stdout: Some(StdoutChannel::default()),
..VectorChannel::default()
},
}
}
fn two_slot_channel(a: ChannelKind, b: ChannelKind) -> VectorChannel {
let ca = single_slot_channel(a);
let cb = single_slot_channel(b);
VectorChannel {
http_event: ca.http_event.or(cb.http_event),
nats_subject: ca.nats_subject.or(cb.nats_subject),
stdout: ca.stdout.or(cb.stdout),
}
}
fn single_slot_source(kind: ArtifactKind) -> ArtifactSource {
match kind {
ArtifactKind::Receipts => ArtifactSource {
receipts: Some(ReceiptsSource::default()),
..ArtifactSource::default()
},
ArtifactKind::TestReport => ArtifactSource {
test_report: Some(TestReportSource {
configmap: "cm".into(),
key: "k".into(),
format: ReportFormat::Junit,
namespace: None,
}),
..ArtifactSource::default()
},
ArtifactKind::ProcessSnapshot => ArtifactSource {
process_snapshot: Some(ProcessSnapshotSource::default()),
..ArtifactSource::default()
},
ArtifactKind::RunMarker => ArtifactSource {
run_marker: Some(RunMarkerSource::default()),
..ArtifactSource::default()
},
}
}
fn two_slot_source(a: ArtifactKind, b: ArtifactKind) -> ArtifactSource {
let sa = single_slot_source(a);
let sb = single_slot_source(b);
ArtifactSource {
receipts: sa.receipts.or(sb.receipts),
test_report: sa.test_report.or(sb.test_report),
process_snapshot: sa.process_snapshot.or(sb.process_snapshot),
run_marker: sa.run_marker.or(sb.run_marker),
}
}
}