use uuid::Uuid;
use super::faults;
use super::log_sink::{FinalizerAuditGap, LogSink};
use super::outcome::{
ExemptionCommit, FailureClass, FailureDiagnostic, FinalizerOutcome, ManifestFault, Substrate,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ExemptionMatch {
pub(crate) digest_sha256: String,
pub(crate) field_scope: &'static str,
pub(crate) manifest_id: String,
}
pub(crate) trait FinalizationEffects {
fn resolve_manifest(&mut self) -> Result<ExemptionMatch, ManifestFault>;
fn write_record(&mut self) -> Result<(), ()>;
fn write_stamp(&mut self) -> Result<(), ()>;
fn write_success_audit(&mut self) -> Result<(), ()>;
fn rollback(&mut self);
fn write_failure_audit(&mut self, diagnostic: &FailureDiagnostic) -> Result<(), ()>;
}
#[derive(Debug, Clone)]
pub(crate) struct FinalizationInput {
pub(crate) record_id: Uuid,
pub(crate) substrate: Substrate,
pub(crate) namespace: String,
pub(crate) entry_point: &'static str,
}
pub(crate) fn finalize<E: FinalizationEffects>(
effects: &mut E,
sink: &dyn LogSink,
input: &FinalizationInput,
) -> FinalizerOutcome {
let ns = input.namespace.as_str();
let manifest_match = match faults::consume_manifest_invalid(ns) {
Some(fault) => return FinalizerOutcome::ManifestInvalid(fault),
None => match effects.resolve_manifest() {
Ok(matched) => matched,
Err(fault) => return FinalizerOutcome::ManifestInvalid(fault),
},
};
let record_write_failed =
faults::consume_record_write_fail(ns) || effects.write_record().is_err();
if record_write_failed {
return fail(effects, sink, input, FailureClass::RecordWrite);
}
let stamp_write_failed = faults::consume_stamp_fail(ns) || effects.write_stamp().is_err();
if stamp_write_failed {
return fail(effects, sink, input, FailureClass::Stamp);
}
let success_audit_failed =
faults::consume_success_audit_fail(ns) || effects.write_success_audit().is_err();
if success_audit_failed {
return fail(effects, sink, input, FailureClass::SuccessAudit);
}
FinalizerOutcome::Exempted(ExemptionCommit {
record_id: input.record_id,
substrate: input.substrate,
entry_point: input.entry_point,
digest_sha256: manifest_match.digest_sha256,
field_scope: manifest_match.field_scope,
manifest_id: manifest_match.manifest_id,
})
}
fn fail<E: FinalizationEffects>(
effects: &mut E,
sink: &dyn LogSink,
input: &FinalizationInput,
class: FailureClass,
) -> FinalizerOutcome {
effects.rollback();
let diagnostic = FailureDiagnostic::new(
input.record_id,
input.substrate,
&input.namespace,
input.entry_point,
class,
);
let failure_audit_failed = faults::consume_failure_audit_fail(&input.namespace)
|| effects.write_failure_audit(&diagnostic).is_err();
if failure_audit_failed {
sink.record_audit_gap(&FinalizerAuditGap {
failure_class: diagnostic.failure_class,
diagnostic_id: diagnostic.diagnostic_id,
record_id: diagnostic.record_id,
substrate: diagnostic.substrate,
namespace: diagnostic.namespace.clone(),
entry_point: diagnostic.entry_point,
});
}
match class {
FailureClass::RecordWrite => FinalizerOutcome::RecordWriteFailed(diagnostic),
FailureClass::Stamp => FinalizerOutcome::StampFailed(diagnostic),
FailureClass::SuccessAudit => FinalizerOutcome::AuditFailed(diagnostic),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::secret_gate_finalizer::log_sink::CapturingLogSink;
#[derive(Default)]
struct FakeEffects {
record_written: bool,
stamp_written: bool,
audit_written: bool,
failure_audits: Vec<FailureDiagnostic>,
resolve_err: Option<ManifestFault>,
}
impl FakeEffects {
fn state(&self) -> (bool, bool, bool) {
(self.record_written, self.stamp_written, self.audit_written)
}
}
impl FinalizationEffects for FakeEffects {
fn resolve_manifest(&mut self) -> Result<ExemptionMatch, ManifestFault> {
match self.resolve_err {
Some(fault) => Err(fault),
None => Ok(ExemptionMatch {
digest_sha256: "deadbeef".to_string(),
field_scope: "record-content",
manifest_id: "khive-secret-gate-empty-v1".to_string(),
}),
}
}
fn write_record(&mut self) -> Result<(), ()> {
self.record_written = true;
Ok(())
}
fn write_stamp(&mut self) -> Result<(), ()> {
self.stamp_written = true;
Ok(())
}
fn write_success_audit(&mut self) -> Result<(), ()> {
self.audit_written = true;
Ok(())
}
fn rollback(&mut self) {
self.record_written = false;
self.stamp_written = false;
self.audit_written = false;
}
fn write_failure_audit(&mut self, diagnostic: &FailureDiagnostic) -> Result<(), ()> {
self.failure_audits.push(diagnostic.clone());
Ok(())
}
}
fn input(ns: &str) -> FinalizationInput {
FinalizationInput {
record_id: Uuid::new_v4(),
substrate: Substrate::Entity,
namespace: ns.to_string(),
entry_point: "entity.create",
}
}
#[test]
fn happy_path_exempts_and_writes_all_three_states() {
let ns = "txn-test-happy-path";
let mut effects = FakeEffects::default();
let sink = CapturingLogSink::new();
let outcome = finalize(&mut effects, &sink, &input(ns));
assert!(matches!(outcome, FinalizerOutcome::Exempted(_)));
assert_eq!(effects.state(), (true, true, true));
assert!(sink.snapshot().is_empty());
}
#[test]
fn manifest_invalid_touches_no_transactional_state() {
let ns = "txn-test-manifest-invalid";
let mut effects = FakeEffects {
resolve_err: Some(ManifestFault::UnknownSchemaVersion),
..Default::default()
};
let sink = CapturingLogSink::new();
let outcome = finalize(&mut effects, &sink, &input(ns));
assert_eq!(
outcome,
FinalizerOutcome::ManifestInvalid(ManifestFault::UnknownSchemaVersion)
);
assert_eq!(effects.state(), (false, false, false));
}
#[test]
fn manifest_invalid_seam_overrides_effects_and_is_one_shot() {
let ns = "txn-test-manifest-invalid-seam";
let _arm = faults::arm_manifest_invalid(ns, ManifestFault::RefreshFailure);
let mut effects = FakeEffects::default();
let sink = CapturingLogSink::new();
let outcome = finalize(&mut effects, &sink, &input(ns));
assert_eq!(
outcome,
FinalizerOutcome::ManifestInvalid(ManifestFault::RefreshFailure)
);
assert_eq!(effects.state(), (false, false, false));
let outcome2 = finalize(&mut effects, &sink, &input(ns));
assert!(matches!(outcome2, FinalizerOutcome::Exempted(_)));
}
#[test]
fn record_write_failure_rolls_back_to_pre_transaction_state() {
let ns = "txn-test-record-write-fail";
let pre_state = FakeEffects::default().state();
let _arm = faults::arm_record_write_fail(ns);
let mut effects = FakeEffects::default();
let sink = CapturingLogSink::new();
let outcome = finalize(&mut effects, &sink, &input(ns));
match outcome {
FinalizerOutcome::RecordWriteFailed(diagnostic) => {
assert_eq!(diagnostic.failure_class, FailureClass::RecordWrite);
}
other => panic!("expected RecordWriteFailed, got {other:?}"),
}
assert_eq!(
effects.state(),
pre_state,
"post-failure state must equal pre-transaction state"
);
assert_eq!(effects.failure_audits.len(), 1);
assert!(
sink.snapshot().is_empty(),
"primary failure-audit succeeded, sink must stay empty"
);
}
#[test]
fn stamp_failure_rolls_back_record_too() {
let ns = "txn-test-stamp-fail";
let pre_state = FakeEffects::default().state();
let _arm = faults::arm_stamp_fail(ns);
let mut effects = FakeEffects::default();
let sink = CapturingLogSink::new();
let outcome = finalize(&mut effects, &sink, &input(ns));
match outcome {
FinalizerOutcome::StampFailed(diagnostic) => {
assert_eq!(diagnostic.failure_class, FailureClass::Stamp);
}
other => panic!("expected StampFailed, got {other:?}"),
}
assert_eq!(effects.state(), pre_state);
assert!(sink.snapshot().is_empty());
}
#[test]
fn success_audit_failure_rolls_back_record_and_stamp() {
let ns = "txn-test-success-audit-fail";
let pre_state = FakeEffects::default().state();
let _arm = faults::arm_success_audit_fail(ns);
let mut effects = FakeEffects::default();
let sink = CapturingLogSink::new();
let outcome = finalize(&mut effects, &sink, &input(ns));
match outcome {
FinalizerOutcome::AuditFailed(diagnostic) => {
assert_eq!(diagnostic.failure_class, FailureClass::SuccessAudit);
}
other => panic!("expected AuditFailed, got {other:?}"),
}
assert_eq!(effects.state(), pre_state);
assert!(sink.snapshot().is_empty());
}
#[test]
fn second_order_failure_audit_failure_reaches_the_log_sink_exactly_once() {
let ns = "txn-test-second-order";
let pre_state = FakeEffects::default().state();
let _record_arm = faults::arm_record_write_fail(ns);
let _failure_audit_arm = faults::arm_failure_audit_fail(ns);
let mut effects = FakeEffects::default();
let sink = CapturingLogSink::new();
let outcome = finalize(&mut effects, &sink, &input(ns));
let diagnostic = match outcome {
FinalizerOutcome::RecordWriteFailed(diagnostic) => diagnostic,
other => panic!("expected RecordWriteFailed, got {other:?}"),
};
assert_eq!(
effects.state(),
pre_state,
"rollback must still hold under second-order failure"
);
assert!(
effects.failure_audits.is_empty(),
"failure-audit write was injected to fail; it must not have recorded anything"
);
let gaps = sink.snapshot();
assert_eq!(
gaps.len(),
1,
"exactly one audit-gap record for the second-order case"
);
let gap = &gaps[0];
assert_eq!(gap.diagnostic_id, diagnostic.diagnostic_id);
assert_eq!(gap.failure_class, FailureClass::RecordWrite);
assert_eq!(gap.record_id, diagnostic.record_id);
assert_eq!(gap.substrate, diagnostic.substrate);
assert_eq!(gap.namespace, diagnostic.namespace);
assert_eq!(gap.entry_point, diagnostic.entry_point);
}
#[test]
fn distinct_namespaces_do_not_cross_contaminate_seams() {
let ns_a = "txn-test-ns-a";
let ns_b = "txn-test-ns-b";
let _arm = faults::arm_record_write_fail(ns_a);
let mut effects_b = FakeEffects::default();
let sink = CapturingLogSink::new();
let outcome_b = finalize(&mut effects_b, &sink, &input(ns_b));
assert!(matches!(outcome_b, FinalizerOutcome::Exempted(_)));
}
}