use std::collections::BTreeSet;
use std::path::PathBuf;
use onemessagebus::{EmitterError, Reading, Redactor};
use serde_json::{Map, Value};
use crate::error::{self, Result};
use crate::event::{
phase_of, ArtifactRef, Dimensions, Envelope, EventFilter, EventKind, Known, Labels, Line,
Phase, Source, SOURCE_WORD,
};
use crate::git::ObjectId;
use crate::landed::Landed;
use crate::rules::MergePolicy;
use crate::session::SessionToken;
use crate::vocabulary::{BusReader, Emitter};
use crate::{home, ids, policy, release, status, store, workspace};
pub const ENVELOPE_VERSION: u32 = 1;
#[derive(Debug)]
pub struct Stream {
path: PathBuf,
id: String,
labels: Labels,
emitter: Emitter,
attempt: Option<u32>,
operation: Option<Emitter>,
}
impl Stream {
pub fn open(token: &str) -> Result<Self> {
let path = path_for(token)?;
home::ensure_dir(path.parent().expect("a stream lives in a directory"))?;
let mut labels = Labels::default();
labels
.extra
.insert("session".to_owned(), Value::String(token.to_owned()));
let emitter = emitter(token, &path, &labels);
Ok(Self {
path,
id: token.to_owned(),
labels,
emitter,
attempt: None,
operation: None,
})
}
pub(crate) fn operation(
&mut self,
operation: &crate::ops::OperationHandle,
identity: &str,
) -> Result<()> {
self.label("identity", identity);
self.label("operation_id", operation.id());
let path = operation.directory()?.join("events.ndjson");
self.operation = Some(emitter(operation.id(), &path, &self.labels));
Ok(())
}
pub(crate) fn inherit_operation(&mut self, parent: &Stream) {
if let Some(operation) = &parent.operation {
for key in ["identity", "operation_id"] {
if let Some(value) = parent.labels.extra.get(key).and_then(Value::as_str) {
self.label(key, value);
}
}
self.operation = Some(operation.clone().with_labels(self.labels.clone()));
}
}
pub(crate) fn in_operation(&self) -> bool {
self.operation.is_some()
}
pub(crate) fn base_arrived(&mut self, base: &str, commit: &str, branches: &[String]) {
if self.in_operation() {
let Value::Object(payload) =
serde_json::json!({"base": base, "commit": commit, "branches": branches})
else {
unreachable!("object literal")
};
self.emit(EventKind::BaseArrived, payload);
}
}
pub fn releases(identity: &str) -> Result<Self> {
let mut stream = Self::open(&releases_token(identity))?;
stream.labels.extra.remove("session");
stream.label("identity", identity);
Ok(stream)
}
pub fn label(&mut self, key: &str, value: &str) {
self.labels
.extra
.insert(key.to_owned(), Value::String(value.to_owned()));
self.emitter = emitter(&self.id, &self.path, &self.labels);
}
pub fn emit(&mut self, kind: EventKind, payload: Map<String, Value>) {
self.emit_with(kind, payload, Vec::new());
}
pub fn begin_attempt(&mut self) {
self.attempt = None;
}
pub(crate) fn emit_gate_run(&mut self, run: &crate::gate_run::Run) {
let attempt = match self.attempt {
Some(attempt) => attempt,
None => {
let attempt = self.recorded_attempts() + 1;
self.attempt = Some(attempt);
attempt
}
};
self.append_stamped(
EventKind::GateRun,
run.gate.phase(),
run.payload(attempt),
Vec::new(),
run.verdict.into(),
);
}
fn recorded_attempts(&self) -> u32 {
let Ok(text) = std::fs::read_to_string(&self.path) else {
return 0;
};
text.lines()
.filter_map(|line| match Line::read(line).ok()? {
Line::Known(known) if known.kind == EventKind::GateRun => known
.envelope
.payload
.get("attempt")
.and_then(Value::as_u64)
.and_then(|attempt| u32::try_from(attempt).ok()),
_ => None,
})
.max()
.unwrap_or(0)
}
pub fn emit_with(
&mut self,
kind: EventKind,
payload: Map<String, Value>,
artifacts: Vec<ArtifactRef>,
) {
self.append_stamped(
kind,
phase_of(kind),
payload,
artifacts,
crate::event::EmissionResult::Success,
);
}
pub(crate) fn emit_result(
&mut self,
kind: EventKind,
payload: Map<String, Value>,
artifacts: Vec<ArtifactRef>,
result: crate::event::EmissionResult,
) {
self.append_stamped(kind, phase_of(kind), payload, artifacts, result);
}
pub(crate) fn push_result(
&mut self,
phase: Phase,
payload: Map<String, Value>,
artifacts: Vec<ArtifactRef>,
accepted: bool,
) {
let result = if accepted {
crate::event::EmissionResult::Success
} else {
crate::event::EmissionResult::Failed
};
self.append_stamped(EventKind::Push, phase, payload, artifacts, result);
}
fn append_stamped(
&mut self,
kind: EventKind,
phase: Phase,
payload: Map<String, Value>,
artifacts: Vec<ArtifactRef>,
result: crate::event::EmissionResult,
) {
let significance = crate::event::significance(kind, result);
let stamp = serde_json::to_value(significance).expect("significance is a wire word");
self.labels
.extra
.insert("significance".to_owned(), stamp.clone());
self.emitter = emitter(&self.id, &self.path, &self.labels);
if let Some(operation) = &self.operation {
self.operation = Some(operation.clone().with_labels(self.labels.clone()));
}
if let Some(operation) = &self.operation {
if let Err(unrecorded) = operation.try_emit_stamped(
kind,
Dimensions::at(phase),
payload.clone(),
artifacts.clone(),
) {
eprintln!(
"onevcs: warning: cannot record operation event: {}",
unrecorded.error
);
}
}
let Err(unrecorded) =
self.emitter
.try_emit_stamped(kind, Dimensions::at(phase), payload, artifacts)
else {
return;
};
let path = self.path.display();
match &unrecorded.error {
EmitterError::Lock { source, .. } => {
eprintln!("onevcs: warning: cannot order a {kind:?} event in {path}: {source}");
}
EmitterError::Write { source, .. } => {
eprintln!("onevcs: warning: cannot record a {kind:?} event in {path}: {source}");
}
EmitterError::Serialize { source, .. } => {
eprintln!("onevcs: warning: cannot record a {kind:?} event in {path}: {source}");
}
}
}
}
fn emitter(id: &str, path: &std::path::Path, labels: &Labels) -> Emitter {
Emitter::shared(id, Source::from(SOURCE_WORD), path)
.with_version(ENVELOPE_VERSION)
.with_labels(labels.clone())
}
#[derive(Debug)]
pub struct OperationEvents {
operation: crate::ops::OperationHandle,
offset: u64,
identity: String,
}
impl OperationEvents {
pub fn open(operation: &crate::ops::OperationHandle) -> Result<Self> {
Ok(Self {
operation: crate::ops::OperationHandle::open(operation.id())?,
offset: 0,
identity: operation.repository()?,
})
}
pub fn resume(operation: &crate::ops::OperationHandle, cursor: &str) -> Result<Self> {
let mut reader = Self::open(operation)?;
let prefix = format!("{}:", operation.id());
reader.offset = cursor
.strip_prefix(&prefix)
.and_then(|offset| offset.parse().ok())
.ok_or_else(|| error::invalid("cursor does not belong to this operation"))?;
let path = operation.directory()?.join("events.ndjson");
let bytes = std::fs::read(&path).map_err(error::at("validate operation cursor", &path))?;
let offset = usize::try_from(reader.offset)
.map_err(|_| error::invalid("operation cursor is too large"))?;
if offset > bytes.len() || (offset != 0 && bytes[offset - 1] != b'\n') {
return Err(error::invalid(
"operation cursor is not a complete record boundary",
));
}
Ok(reader)
}
pub fn read(&mut self) -> Result<Vec<Envelope>> {
use std::io::{Read, Seek, SeekFrom};
let path = self.operation.directory()?.join("events.ndjson");
let mut file =
std::fs::File::open(&path).map_err(error::at("open operation events", &path))?;
file.seek(SeekFrom::Start(self.offset))
.map_err(error::at("seek operation events", &path))?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)
.map_err(error::at("read operation events", &path))?;
let mut events = Vec::new();
let mut consumed = 0;
for record in bytes.split_inclusive(|byte| *byte == b'\n') {
if record.last() != Some(&b'\n') {
break;
}
let envelope: Envelope = serde_json::from_slice(record)
.map_err(error::at("parse operation event", &path))?;
if envelope
.labels
.extra
.get("identity")
.and_then(Value::as_str)
!= Some(self.identity.as_str())
|| envelope.stream != self.operation.id()
|| envelope
.labels
.extra
.get("operation_id")
.and_then(Value::as_str)
!= Some(self.operation.id())
{
return Err(error::invalid("event does not belong to its operation"));
}
let stamp = envelope.labels.extra.get("significance").ok_or_else(|| {
error::invalid("operation event is missing its producer significance stamp")
})?;
serde_json::from_value::<crate::event::Significance>(stamp.clone()).map_err(
|error| crate::error::invalid(format!("invalid event significance: {error}")),
)?;
events.push(envelope);
consumed += record.len() as u64;
}
self.offset += consumed;
Ok(events)
}
pub fn cursor(&self) -> String {
format!("{}:{}", self.operation.id(), self.offset)
}
}
#[derive(Debug)]
pub struct Reader {
path: PathBuf,
position: u64,
read: usize,
}
pub(crate) struct Record {
pub number: usize,
pub text: String,
pub read: std::result::Result<Envelope, String>,
}
impl Reader {
pub fn open(token: &str) -> Result<Self> {
let path = path_for(token)?;
if !path.is_file() {
return Err(error::invalid(format!("no event stream for {token:?}")));
}
Ok(Self {
path,
position: 0,
read: 0,
})
}
pub(crate) fn records(&mut self) -> Result<Vec<Record>> {
let reading =
BusReader::open_at(&self.path, self.position).map_err(error::at("read", &self.path))?;
let bytes = std::fs::read(&self.path).map_err(error::at("read", &self.path))?;
let mut records = Vec::new();
for read in reading {
let (end, envelope) = match read {
Reading::Record(record) => (record.position, Ok(record.envelope)),
Reading::Refused(refused) => (refused.position, Err(refused.reason)),
Reading::Torn(_) => break,
};
let text = line_between(&bytes, self.position, end);
self.position = end;
self.read += 1;
records.push(Record {
number: self.read,
text,
read: envelope,
});
}
Ok(records)
}
}
fn line_between(bytes: &[u8], start: u64, end: u64) -> String {
let (start, end) = (to_index(start), to_index(end).saturating_sub(1));
String::from_utf8_lossy(bytes.get(start..end).unwrap_or_default()).into_owned()
}
fn to_index(position: u64) -> usize {
usize::try_from(position).unwrap_or(usize::MAX)
}
#[derive(Debug, Clone, PartialEq)]
pub struct EventLine {
pub text: String,
pub envelope: Option<Envelope>,
}
#[derive(Debug)]
pub struct EventLines {
session: SessionToken,
reader: Reader,
filter: Option<EventFilter>,
}
impl EventLines {
pub fn open(session: &SessionToken, filter: Option<EventFilter>) -> Result<Self> {
Ok(Self {
session: session.clone(),
reader: Reader::open(&session.0)?,
filter,
})
}
pub fn session(&self) -> &SessionToken {
&self.session
}
pub fn read(&mut self) -> Result<Vec<EventLine>> {
let mut lines = Vec::new();
for record in self.reader.records()? {
let text = record.text.clone();
let Some(filter) = &self.filter else {
let envelope = match attributed_record(record, &self.session.0) {
Ok(Line::Known(known)) => Some(known.envelope),
Ok(Line::Unknown(_)) | Err(_) => None,
};
lines.push(EventLine { text, envelope });
continue;
};
let Line::Known(known) = attributed_record(record, &self.session.0)? else {
continue;
};
if !filter.matches(&known.envelope) {
continue;
}
lines.push(EventLine {
text,
envelope: Some(known.envelope),
});
}
Ok(lines)
}
}
#[derive(Debug)]
pub struct EventStream {
session: SessionToken,
reader: Reader,
filter: EventFilter,
phases: BTreeSet<Phase>,
releases: Option<Correlated>,
}
#[derive(Debug)]
struct Correlated {
identity: String,
token: String,
landing: Option<ObjectId>,
handed: BTreeSet<u64>,
}
impl EventStream {
pub fn open(session: &SessionToken) -> Result<Self> {
Self::open_filtered(session, EventFilter::default())
}
pub fn open_filtered(session: &SessionToken, filter: EventFilter) -> Result<Self> {
let reader = Reader::open(&session.0)?;
let (phases, identity) = supported(session);
for named in filter
.include
.iter()
.chain(&filter.exclude)
.filter_map(|matcher| matcher.fields.phase)
{
if !phases.contains(&named) {
return Err(error::invalid(format!(
"the event filter names the {named} phase, which the session {session} does \
not have: it has {had}. A filter that named it would be answered with \
nothing, and nothing is what a filter for the wrong phase and a session \
that did nothing look alike as",
session = session.0,
had = listed(&phases),
)));
}
}
let releases = match (phases.contains(&Phase::Release), identity) {
(true, Some(identity)) => Some(Correlated {
token: releases_token(&identity),
identity,
landing: None,
handed: BTreeSet::new(),
}),
_ => None,
};
Ok(Self {
session: session.clone(),
reader,
filter,
phases,
releases,
})
}
#[must_use]
pub fn session(&self) -> &SessionToken {
&self.session
}
pub fn read(&mut self) -> Result<Vec<Envelope>> {
let mut events = Vec::new();
for record in self.reader.records()? {
let Line::Known(known) = attributed_record(record, &self.session.0)? else {
continue;
};
let envelope = known.envelope;
let mut matching = envelope.clone();
if let Some(stamp) = matching.labels.extra.get("significance") {
serde_json::from_value::<crate::event::Significance>(stamp.clone()).map_err(
|error| crate::error::invalid(format!("invalid event significance: {error}")),
)?;
} else {
matching.labels.extra.insert(
"significance".to_owned(),
Value::String("progress".to_owned()),
);
}
if !self.filter.matches(&matching) {
continue;
}
if !envelope
.dimensions
.phase
.is_some_and(|phase| self.phases.contains(&phase))
{
continue;
}
events.push(envelope);
}
if let Some(correlated) = &mut self.releases {
events.extend(correlated.fresh(&self.session, &self.filter)?);
}
Ok(events)
}
}
impl Correlated {
fn fresh(&mut self, session: &SessionToken, filter: &EventFilter) -> Result<Vec<Envelope>> {
let path = path_for(&self.token)?;
let reading = match BusReader::open(&path) {
Ok(reading) => reading,
Err(failure) if failure.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(failure) => {
return Err(error::invalid(format!(
"the release record for {identity} cannot be read: {failure}",
identity = self.identity,
)))
}
};
let mut candidates = Vec::new();
for (index, read) in reading.enumerate() {
let known = match read {
Reading::Record(record) => self.attributed(Ok(record.envelope), index + 1)?,
Reading::Refused(refused) => self.attributed(Err(refused.reason), index + 1)?,
Reading::Torn(_) => break,
};
if !matches!(
known.kind,
EventKind::ReleaseObserved | EventKind::ReleaseAcknowledged
) || self.handed.contains(&known.envelope.seq)
{
continue;
}
candidates.push((index + 1, known.envelope));
}
if candidates.is_empty() {
return Ok(Vec::new());
}
let landing = match &self.landing {
Some(landing) => landing.clone(),
None => match landed_at(session) {
None => return Ok(Vec::new()),
Some(landing) => {
self.landing = Some(landing.clone());
landing
}
},
};
let mut fresh = Vec::new();
for (line_number, envelope) in candidates {
let named = envelope
.payload
.get("landing_commit")
.and_then(Value::as_str)
.and_then(ObjectId::parse)
.ok_or_else(|| {
self.refusal(
line_number,
"records a release that names no landing commit, so nothing can be said \
about which work it released",
)
})?;
if named != landing {
continue;
}
self.handed.insert(envelope.seq);
if filter.matches(&envelope) {
fresh.push(envelope);
}
}
Ok(fresh)
}
fn attributed(
&self,
read: std::result::Result<Envelope, String>,
line_number: usize,
) -> Result<Known> {
let envelope = read.map_err(|failure| {
self.refusal(line_number, &format!("is not an event envelope: {failure}"))
})?;
if envelope.stream != self.token {
return Err(self.refusal(line_number, "carries an event of another stream"));
}
match Line::of(envelope) {
Line::Known(known) => Ok(*known),
Line::Unknown(header) => Err(self.refusal(
line_number,
&format!(
"is not an event envelope: unknown variant {:?} for an event kind",
header.kind.as_str()
),
)),
}
}
fn refusal(&self, line_number: usize, what: &str) -> crate::Error {
error::invalid(format!(
"line {line_number} of the release record for {identity} {what}",
identity = self.identity,
))
}
}
fn supported(session: &SessionToken) -> (BTreeSet<Phase>, Option<String>) {
let every = || (BTreeSet::from(Phase::every()), None);
let (Ok(record), Ok(registry)) = (workspace::load(&session.0), store::load()) else {
return every();
};
let identity = record.identity.clone();
let (Ok(resolution), Ok((rules, source))) = (
store::resolve(®istry, &identity),
policy::load(®istry),
) else {
return every();
};
let mut phases = BTreeSet::from([Phase::Development, Phase::Integrate]);
let resolved = policy::resolve(
&rules,
&source,
&store::normalize(&resolution.identity.origin),
&resolution.publication,
);
if resolved.policy.publication != MergePolicy::LocalDirect {
phases.insert(Phase::Review);
}
let releases_nothing = release::for_repository(®istry, &identity).is_ok_and(|located| {
located.releases.targets.is_empty() && located.releases.declaration.unreadable().is_none()
});
if !releases_nothing {
phases.insert(Phase::Release);
}
(phases, Some(identity))
}
fn landed_at(session: &SessionToken) -> Option<ObjectId> {
let registry = store::load().ok()?;
match status::landing_of(®istry, &session.0, None).ok()?.landed {
Landed::Yes { evidence } | Landed::InPart { evidence, .. } => {
ObjectId::parse(evidence.commit())
}
Landed::No | Landed::Unknown => None,
}
}
fn listed(phases: &BTreeSet<Phase>) -> String {
phases
.iter()
.map(|phase| phase.as_str())
.collect::<Vec<&str>>()
.join(", ")
}
pub fn attributed(line: &str, session: &str, line_number: usize) -> Result<Line> {
attribute(
Line::read(line).map_err(|failure| failure.to_string()),
session,
line_number,
)
}
pub(crate) fn attributed_record(record: Record, session: &str) -> Result<Line> {
attribute(record.read.map(Line::of), session, record.number)
}
fn attribute(
read: std::result::Result<Line, String>,
session: &str,
line_number: usize,
) -> Result<Line> {
let read = read.map_err(|e| {
error::invalid(format!(
"line {line_number} of the stream for {session:?} is not an event envelope: {e}"
))
})?;
if read.stream() != session {
return Err(error::invalid(format!(
"line {line_number} of the stream for {session:?} carries an event of stream {:?}",
read.stream()
)));
}
Ok(read)
}
fn releases_token(identity: &str) -> String {
format!("releases-{}", ids::short_digest(identity))
}
pub fn path_for(token: &str) -> Result<PathBuf> {
if !ids::is_safe_name(token) {
return Err(error::invalid(format!("{token:?} is not a session token")));
}
Ok(home::streams_dir()?.join(format!("{token}.ndjson")))
}
pub fn redact(text: &str) -> String {
Redactor::from_env().redact(text)
}
pub fn store_artifact(kind: &str, contents: &str) -> Result<ArtifactRef> {
let id = ids::artifact_id();
let directory = home::artifacts_dir()?;
home::ensure_dir(&directory)?;
let clean = redact(contents);
let path = directory.join(&id);
std::fs::write(&path, &clean).map_err(error::at("store the artifact at", &path))?;
Ok(ArtifactRef {
id,
kind: kind.to_owned(),
bytes: clean.len() as u64,
})
}
pub fn read_artifact(id: &str) -> Result<String> {
if !ids::is_safe_name(id) {
return Err(error::invalid(format!("{id:?} is not an artifact id")));
}
let path = home::artifacts_dir()?.join(id);
std::fs::read_to_string(&path)
.map_err(|_| error::invalid(format!("no artifact {id:?} is stored")))
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
fn inhabit() -> tempfile::TempDir {
let home = tempfile::tempdir().expect("a temporary state root");
std::env::set_var(home::HOME_ENV, home.path());
home
}
fn envelopes(written: &str) -> Vec<Value> {
written
.lines()
.map(|line| serde_json::from_str(line).expect("every line is an envelope"))
.collect()
}
#[test]
fn a_credential_nested_in_an_object_and_a_list_never_reaches_a_sessions_stream() {
let _home = inhabit();
let token = "nested-credentials";
let credentials = [
"ghp_0123456789abcdef",
"github_pat_0123456789abcdef",
"AKIA0123456789ABCDEF",
];
let payload = json!({
"name": "check",
"detail": {
"said": format!("pushed with {}", credentials[0]),
"attempts": [
{"output": credentials[1]},
["retried", format!("{},", credentials[2])],
],
},
});
Stream::open(token)
.expect("the session's stream")
.emit_with(
EventKind::ChangeCheck,
payload.as_object().expect("an object").clone(),
Vec::new(),
);
let written = std::fs::read_to_string(path_for(token).expect("a stream path"))
.expect("the stream was written");
for credential in credentials {
assert!(
!written.contains(credential),
"{credential:?} reached the stream file:\n{written}"
);
}
let read = EventStream::open(&SessionToken(token.to_owned()))
.expect("the session's stream")
.read()
.expect("every event the session wrote");
assert_eq!(read.len(), 1, "{written}");
assert_eq!(
Value::Object(read[0].payload.clone()),
json!({
"name": "check",
"detail": {
"said": "pushed with [redacted]",
"attempts": [
{"output": "[redacted]"},
["retried", "[redacted],"],
],
},
}),
"every string of the payload is redacted, however deeply it is nested"
);
}
#[test]
fn a_record_a_writer_died_part_way_through_is_healed_before_the_next_event_is_appended() {
let _home = inhabit();
let token = "torn-tail";
let path = path_for(token).expect("a stream path");
Stream::open(token)
.expect("the session's stream")
.emit(EventKind::SessionOpened, Map::new());
let whole = std::fs::read_to_string(&path).expect("the stream was written");
let torn = "{\"v\":1,\"ts\":\"2026-09-13T00:00:00.000Z\",\"stream\":\"";
{
use std::io::Write;
std::fs::OpenOptions::new()
.append(true)
.open(&path)
.and_then(|mut file| file.write_all(torn.as_bytes()))
.expect("the stream takes the bytes");
}
Stream::open(token)
.expect("the session's stream, reopened")
.emit(EventKind::SessionClosed, Map::new());
let written = std::fs::read_to_string(&path).expect("the stream");
assert!(
written.starts_with(&whole) && written.ends_with('\n') && !written.contains(torn),
"the torn bytes were not truncated away before the append:\n{written}"
);
let envelopes = envelopes(&written);
assert_eq!(
envelopes
.iter()
.map(|envelope| envelope["seq"].as_u64().expect("a seq"))
.collect::<Vec<u64>>(),
vec![1, 2],
"one gapless series:\n{written}"
);
assert_eq!(
envelopes[1]["kind"], "session-closed",
"the last record is the one the healing writer wrote:\n{written}"
);
}
#[test]
fn operation_progress_cursor_holds_partial_records_and_reports_invalid_stamps_without_loss() {
use std::io::Write;
let _home = inhabit();
let repo = _home.path().join("repository");
std::fs::create_dir(&repo).expect("repository directory");
crate::git::checked(&["init"], Some(&repo)).expect("real git repository");
let origin = crate::Url::from_file_path(&repo).expect("absolute local identity");
let registered =
crate::ops::register_checkout(&repo, Some(&origin)).expect("registered identity");
let operation =
crate::ops::OperationHandle::prepare(&crate::ops::DirectOperation::Integrate {
repository: registered.identity.clone(),
})
.expect("prepared invocation");
let directory = operation.directory().expect("private fixture directory");
let path = directory.join("events.ndjson");
let mut file = std::fs::File::create(&path).expect("actual stream file");
let mut reader = OperationEvents::open(&operation).expect("reader");
let record = serde_json::json!({"v":1,"ts":"2026-01-01T00:00:00.000Z","stream":operation.id(),"seq":1,"source":"vcs","kind":"future-kind","phase":"integrate","labels":{"identity":registered.identity,"operation_id":operation.id(),"significance":"milestone"},"payload":{"future":"preserved"},"artifacts":[]});
let bytes = serde_json::to_vec(&record).expect("record bytes");
file.write_all(&bytes[..bytes.len() / 2])
.expect("partial append");
let initial = reader.cursor();
assert!(reader.read().expect("partial record is held").is_empty());
assert_eq!(reader.cursor(), initial);
file.write_all(&bytes[bytes.len() / 2..])
.expect("remaining append");
assert!(reader.read().expect("newline still absent").is_empty());
file.write_all(b"\n").expect("complete append");
let complete = reader.read().expect("future kind is handed on");
assert_eq!(complete.len(), 1);
assert_eq!(
serde_json::to_value(&complete[0]).expect("original envelope"),
record
);
let cursor = reader.cursor();
let mut bad = record.clone();
bad["seq"] = serde_json::json!(2);
bad["labels"]["significance"] = serde_json::json!("important");
writeln!(file, "{bad}").expect("malformed stamp");
assert!(reader
.read()
.expect_err("invalid stamp refuses")
.to_string()
.contains("significance"));
assert_eq!(
reader.cursor(),
cursor,
"failed batch never consumes records"
);
assert!(OperationEvents::resume(&operation, &format!("{}:1", operation.id())).is_err());
assert!(
OperationEvents::resume(&operation, &format!("{}:unknown", operation.id())).is_err()
);
assert!(OperationEvents::resume(
&operation,
&format!("{}:18446744073709551615", operation.id())
)
.is_err());
std::fs::write(&path, format!("{record}\n{record}\n")).expect("repair malformed tail");
let mut resumed = OperationEvents::resume(&operation, &cursor).expect("saved cursor");
assert_eq!(resumed.read().expect("one repaired record").len(), 1);
assert!(resumed.read().expect("drained, not completed").is_empty());
let mut unstamped = record.clone();
unstamped["labels"]
.as_object_mut()
.expect("labels")
.remove("significance");
std::fs::write(&path, format!("{unstamped}\n")).expect("missing operation stamp");
assert!(OperationEvents::open(&operation)
.expect("reader")
.read()
.expect_err("operation records require a producer stamp")
.to_string()
.contains("significance"));
let mut wrong_identity = record.clone();
wrong_identity["labels"]["identity"] = json!("/different/repository");
std::fs::write(&path, format!("{wrong_identity}\n")).expect("foreign identity fixture");
assert!(OperationEvents::open(&operation)
.expect("reader")
.read()
.expect_err("foreign identity refuses")
.to_string()
.contains("does not belong"));
std::fs::write(
directory.join("operation.json"),
serde_json::json!({"version":2,"operation":{"kind":"integrate","repository":registered.identity}}).to_string(),
)
.expect("future manifest");
assert!(crate::ops::OperationHandle::open(operation.id())
.expect_err("unknown storage schema")
.to_string()
.contains("schema version 2"));
for selector in [
json!({"kind":"publish-branch","repository":registered.identity,"branch":""}),
json!({"kind":"integrate","repository":"unregistered"}),
] {
std::fs::write(
directory.join("operation.json"),
json!({"version":1,"operation":selector}).to_string(),
)
.expect("malformed selector fixture");
assert!(
crate::ops::OperationHandle::open(operation.id()).is_err(),
"persisted selector invariants are rechecked"
);
}
}
#[test]
fn operation_progress_legacy_significance_matching_preserves_bytes_and_rejects_malformed_stamps(
) {
let _home = inhabit();
let token = "s-significance";
let mut stream = Stream::open(token).expect("real stream writer");
stream.emit(EventKind::ChangeOpened, Map::new());
let path = path_for(token).expect("private fixture path");
let text = std::fs::read_to_string(&path).expect("producer record");
let mut legacy: Value = serde_json::from_str(text.trim()).expect("record");
legacy["labels"]
.as_object_mut()
.expect("labels")
.remove("significance");
let bytes = format!("{legacy}\n");
std::fs::write(&path, &bytes).expect("legacy unstamped fixture");
let progress = EventFilter::parse("include: [{significance: progress}]")
.expect("significance matcher");
let mut reader =
EventStream::open_filtered(&SessionToken(token.to_owned()), progress.clone())
.expect("reader adapter");
let batch = reader.read().expect("legacy matches progress");
assert_eq!(batch.len(), 1);
assert!(!batch[0].labels.extra.contains_key("significance"));
assert!(
!progress.matches(&batch[0]),
"raw matching intentionally has no vocabulary default hook"
);
assert_eq!(std::fs::read_to_string(&path).expect("legacy bytes"), bytes);
for stamp in [json!("important"), json!(3), Value::Null] {
let mut malformed = legacy.clone();
malformed["labels"]["significance"] = stamp;
std::fs::write(&path, format!("{malformed}\n")).expect("malformed legacy record");
let filter = EventFilter::parse("include: [{significance: milestone}]")
.expect("a filter that would omit progress");
let mut reader = EventStream::open_filtered(&SessionToken(token.to_owned()), filter)
.expect("stream still opens");
assert!(reader
.read()
.expect_err("invalid stamp is reported even if filtered out")
.to_string()
.contains("significance"));
}
}
}