#![cfg(all(unix, feature = "workflow"))]
mod common;
use std::process::{Command, Stdio};
fn run(cfg_text: &str) -> (Option<i32>, String) {
let dir = common::unique_path("rtev", "d");
std::fs::create_dir_all(&dir).unwrap();
let cfg = format!("{dir}/c.yaml");
std::fs::write(&cfg, cfg_text.replace("__STATE__", &format!("{dir}/state"))).unwrap();
let out = Command::new(env!("CARGO_BIN_EXE_agentd"))
.args(["--config", &cfg])
.stdin(Stdio::null())
.stdout(Stdio::null())
.output()
.expect("run");
let log = String::from_utf8_lossy(&out.stderr).to_string();
let _ = std::fs::remove_dir_all(&dir);
(out.status.code(), log)
}
const BASE: &str = "config_version: \"1\"\nagent: { name: r }\n\
store: { kind: file, file: { path: __STATE__ } }\n";
#[test]
fn a_runtime_event_can_start_a_run() {
let (code, log) = run(&format!(
"{BASE}lifecycle: {{ run_until: idle, idle_grace: 3s }}\n\
streams: {{ _runtime: {{ retention: {{ max_events: 500 }} }} }}\n\
observability:\n log_level: info\n\
\x20 runtime_events: {{ stream: _runtime, include: [run] }}\n\
workflows:\n\
\x20 - name: work\n steps:\n\
\x20 s: {{ kind: once }}\n\
\x20 f: {{ kind: finish, depends_on: [s], status: completed, output: did-work }}\n\
\x20 - name: watcher\n steps:\n\
\x20 s: {{ kind: stream, stream: _runtime, subject: \"run.done\" }}\n\
\x20 f: {{ kind: finish, depends_on: [s], status: completed, output: reacted }}\n"
));
assert_eq!(code, Some(0), "{log}");
assert!(
log.contains("\"event\":\"stream.tap\""),
"the tap should have armed at startup\n{log}"
);
assert!(
log.contains("\"workflow\":\"watcher\"") && log.contains("\"event\":\"run.start\""),
"a runtime event should have started the watcher run\n{log}"
);
}
#[test]
fn families_not_included_never_reach_the_stream() {
let (code, log) = run(&format!(
"{BASE}lifecycle: {{ run_until: idle, idle_grace: 2s }}\n\
streams: {{ _runtime: {{ retention: {{ max_events: 500 }} }} }}\n\
observability:\n log_level: info\n\
\x20 runtime_events: {{ stream: _runtime, include: [breaker] }}\n\
workflows:\n\
\x20 - name: work\n steps:\n\
\x20 s: {{ kind: once }}\n\
\x20 f: {{ kind: finish, depends_on: [s], status: completed, output: done }}\n\
\x20 - name: watcher\n steps:\n\
\x20 s: {{ kind: stream, stream: _runtime, subject: \"run.done\" }}\n\
\x20 f: {{ kind: finish, depends_on: [s], status: completed, output: reacted }}\n"
));
assert_eq!(code, Some(0), "{log}");
assert!(
!log.contains("\"workflow\":\"watcher\",\"...\"") && !log.contains("\"run\":\"watcher-"),
"run.* was not included, so nothing should have consumed it\n{log}"
);
}
#[test]
fn an_undeclared_stream_is_refused_at_startup() {
let (code, log) = run(&format!(
"{BASE}lifecycle: {{ run_until: idle, idle_grace: 1s }}\n\
observability:\n log_level: info\n\
\x20 runtime_events: {{ stream: nope, include: [run] }}\n"
));
assert_eq!(code, Some(2), "{log}");
assert!(
log.contains("is not declared"),
"the refusal should name the missing stream\n{log}"
);
}
#[test]
fn an_unknown_family_is_refused_at_startup() {
let (code, log) = run(&format!(
"{BASE}lifecycle: {{ run_until: idle, idle_grace: 1s }}\n\
streams: {{ _runtime: {{ retention: {{ max_events: 10 }} }} }}\n\
observability:\n log_level: info\n\
\x20 runtime_events: {{ stream: _runtime, include: [pressur] }}\n"
));
assert_eq!(code, Some(2), "{log}");
assert!(
log.contains("unknown event family"),
"the refusal should name the typo\n{log}"
);
}
#[test]
fn the_tap_does_not_feed_itself() {
let (code, log) = run(&format!(
"{BASE}lifecycle: {{ run_until: idle, idle_grace: 3s }}\n\
streams: {{ _runtime: {{ retention: {{ max_events: 2 }} }} }}\n\
observability:\n log_level: info\n\
\x20 runtime_events: {{ stream: _runtime, include: [stream, run] }}\n\
workflows:\n\
\x20 - name: work\n steps:\n\
\x20 s: {{ kind: once }}\n\
\x20 f: {{ kind: finish, depends_on: [s], status: completed, output: done }}\n"
));
assert_eq!(code, Some(0), "{log}");
let trims = log.matches("\"event\":\"stream.trimmed\"").count();
assert!(
trims < 100,
"runaway trimming ({trims}) suggests the tap is appending its own output\n{log}"
);
}
#[test]
fn the_audit_stream_sink_requires_a_declared_stream() {
let (code, log) = run(&format!(
"{BASE}lifecycle: {{ run_until: idle, idle_grace: 1s }}\n\
observability:\n log_level: info\n audit: {{ sink: [stream] }}\n"
));
assert_eq!(code, Some(2), "{log}");
assert!(
log.contains("needs `stream:"),
"the refusal should say what is missing\n{log}"
);
}