Surface degraded evidence
This commit is contained in:
+128
-5
@@ -110,6 +110,13 @@ impl From<&window::WindowSnapshot> for WindowIdentity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
struct EvidenceObservationKey {
|
||||||
|
runtime_state: RuntimeState,
|
||||||
|
commitment_id: Option<String>,
|
||||||
|
health: EvidenceHealth,
|
||||||
|
}
|
||||||
|
|
||||||
struct App {
|
struct App {
|
||||||
state: State,
|
state: State,
|
||||||
user_intention: String,
|
user_intention: String,
|
||||||
@@ -134,6 +141,7 @@ struct App {
|
|||||||
session_results: Vec<SessionResult>,
|
session_results: Vec<SessionResult>,
|
||||||
acknowledged_unknown_window: Option<WindowIdentity>,
|
acknowledged_unknown_window: Option<WindowIdentity>,
|
||||||
acknowledged_unknown_window_handoff_pending: bool,
|
acknowledged_unknown_window_handoff_pending: bool,
|
||||||
|
last_recorded_evidence: Option<EvidenceObservationKey>,
|
||||||
last_tick_50ms: Instant,
|
last_tick_50ms: Instant,
|
||||||
last_tick_1s: Instant,
|
last_tick_1s: Instant,
|
||||||
}
|
}
|
||||||
@@ -225,6 +233,7 @@ impl App {
|
|||||||
session_results: Vec::new(),
|
session_results: Vec::new(),
|
||||||
acknowledged_unknown_window: None,
|
acknowledged_unknown_window: None,
|
||||||
acknowledged_unknown_window_handoff_pending: false,
|
acknowledged_unknown_window_handoff_pending: false,
|
||||||
|
last_recorded_evidence: None,
|
||||||
last_tick_50ms: now,
|
last_tick_50ms: now,
|
||||||
last_tick_1s: now,
|
last_tick_1s: now,
|
||||||
})
|
})
|
||||||
@@ -449,7 +458,7 @@ impl App {
|
|||||||
State::ShouldQuit => {}
|
State::ShouldQuit => {}
|
||||||
State::Paused => {
|
State::Paused => {
|
||||||
window::minimize_other(&constants::APP_TITLE);
|
window::minimize_other(&constants::APP_TITLE);
|
||||||
update_session_stats(self);
|
update_session_stats(self)?;
|
||||||
}
|
}
|
||||||
State::ViolationPrompt => {}
|
State::ViolationPrompt => {}
|
||||||
State::End => {
|
State::End => {
|
||||||
@@ -471,7 +480,7 @@ impl App {
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let elapsed = self.session_start.elapsed();
|
let elapsed = self.session_start.elapsed();
|
||||||
self.session_remaining = self.user_duration.saturating_sub(elapsed);
|
self.session_remaining = self.user_duration.saturating_sub(elapsed);
|
||||||
let window_snapshot = update_session_stats_with_snapshot(self, window_snapshot);
|
let window_snapshot = update_session_stats_with_snapshot(self, window_snapshot)?;
|
||||||
|
|
||||||
let should_prompt = self.state == State::InProgress
|
let should_prompt = self.state == State::InProgress
|
||||||
&& self.session_controller.runtime_state() == RuntimeState::Active
|
&& self.session_controller.runtime_state() == RuntimeState::Active
|
||||||
@@ -546,6 +555,29 @@ impl App {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn record_evidence_if_needed(&mut self, health: &EvidenceHealth) -> Result<()> {
|
||||||
|
if matches!(health, EvidenceHealth::Available) {
|
||||||
|
self.last_recorded_evidence = None;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = EvidenceObservationKey {
|
||||||
|
runtime_state: self.session_controller.runtime_state(),
|
||||||
|
commitment_id: self
|
||||||
|
.session_controller
|
||||||
|
.active_commitment()
|
||||||
|
.map(|commitment| commitment.id.clone()),
|
||||||
|
health: health.clone(),
|
||||||
|
};
|
||||||
|
if self.last_recorded_evidence.as_ref() == Some(&key) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.session_controller.record_evidence(health.clone())?;
|
||||||
|
self.last_recorded_evidence = Some(key);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn get_session_results(&self) -> Vec<Line<'_>> {
|
fn get_session_results(&self) -> Vec<Line<'_>> {
|
||||||
self.session_results
|
self.session_results
|
||||||
.iter()
|
.iter()
|
||||||
@@ -866,15 +898,16 @@ fn handle_key_press(app: &mut App, key_code: KeyCode) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_session_stats(app: &mut App) -> window::WindowSnapshot {
|
fn update_session_stats(app: &mut App) -> Result<window::WindowSnapshot> {
|
||||||
update_session_stats_with_snapshot(app, window::get_snapshot())
|
update_session_stats_with_snapshot(app, window::get_snapshot())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_session_stats_with_snapshot(
|
fn update_session_stats_with_snapshot(
|
||||||
app: &mut App,
|
app: &mut App,
|
||||||
window_snapshot: window::WindowSnapshot,
|
window_snapshot: window::WindowSnapshot,
|
||||||
) -> window::WindowSnapshot {
|
) -> Result<window::WindowSnapshot> {
|
||||||
app.evidence_health = window_snapshot.health.clone();
|
app.evidence_health = window_snapshot.health.clone();
|
||||||
|
app.record_evidence_if_needed(&window_snapshot.health)?;
|
||||||
let window_title = if app.state == State::Paused {
|
let window_title = if app.state == State::Paused {
|
||||||
constants::PAUSED.to_string()
|
constants::PAUSED.to_string()
|
||||||
} else {
|
} else {
|
||||||
@@ -894,7 +927,7 @@ fn update_session_stats_with_snapshot(
|
|||||||
app.session_ratings = session_stats_as_vec(&app.session_stats);
|
app.session_ratings = session_stats_as_vec(&app.session_stats);
|
||||||
}
|
}
|
||||||
|
|
||||||
window_snapshot
|
Ok(window_snapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ui(frame: &mut Frame, app: &App) {
|
fn ui(frame: &mut Frame, app: &App) {
|
||||||
@@ -1098,6 +1131,16 @@ fn ui(frame: &mut Frame, app: &App) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(text) = evidence_status_text(&app.evidence_health) {
|
||||||
|
if !spans.is_empty() {
|
||||||
|
spans.push(Span::raw(" | "));
|
||||||
|
}
|
||||||
|
spans.push(Span::styled(
|
||||||
|
text,
|
||||||
|
Style::new().fg(evidence_status_color(&app.evidence_health)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let input_status: Vec<Line> = vec![Line::from(spans)];
|
let input_status: Vec<Line> = vec![Line::from(spans)];
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(input_status).block(Block::bordered().title(constants::STATUS_TITLE)),
|
Paragraph::new(input_status).block(Block::bordered().title(constants::STATUS_TITLE)),
|
||||||
@@ -1105,6 +1148,22 @@ fn ui(frame: &mut Frame, app: &App) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn evidence_status_text(health: &EvidenceHealth) -> Option<String> {
|
||||||
|
match health {
|
||||||
|
EvidenceHealth::Available => None,
|
||||||
|
EvidenceHealth::Degraded(reason) => Some(format!("Evidence degraded: {}", reason)),
|
||||||
|
EvidenceHealth::Unavailable(reason) => Some(format!("Evidence unavailable: {}", reason)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evidence_status_color(health: &EvidenceHealth) -> Color {
|
||||||
|
match health {
|
||||||
|
EvidenceHealth::Available => Color::Reset,
|
||||||
|
EvidenceHealth::Degraded(_) => Color::LightYellow,
|
||||||
|
EvidenceHealth::Unavailable(_) => Color::LightRed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn render_violation_prompt(frame: &mut Frame, app: &App) {
|
fn render_violation_prompt(frame: &mut Frame, app: &App) {
|
||||||
let layout = Layout::vertical([
|
let layout = Layout::vertical([
|
||||||
Constraint::Min(5),
|
Constraint::Min(5),
|
||||||
@@ -1840,4 +1899,68 @@ mod tests {
|
|||||||
|
|
||||||
let _ = fs::remove_file(path);
|
let _ = fs::remove_file(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn evidence_status_text_describes_degraded_and_unavailable_health() {
|
||||||
|
assert_eq!(evidence_status_text(&EvidenceHealth::Available), None);
|
||||||
|
assert_eq!(
|
||||||
|
evidence_status_text(&EvidenceHealth::Degraded(
|
||||||
|
"window class unavailable".to_string(),
|
||||||
|
)),
|
||||||
|
Some("Evidence degraded: window class unavailable".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
evidence_status_text(&EvidenceHealth::Unavailable(
|
||||||
|
"xdotool active window unavailable".to_string(),
|
||||||
|
)),
|
||||||
|
Some("Evidence unavailable: xdotool active window unavailable".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn active_ticks_record_degraded_evidence_once_until_health_changes() {
|
||||||
|
let path = unique_event_log_path("evidence-dedup");
|
||||||
|
let mut app = App::new_with_event_log_path(&path).unwrap();
|
||||||
|
|
||||||
|
app.user_intention = "write tests".to_string();
|
||||||
|
app.user_success_condition = "tests pass".to_string();
|
||||||
|
app.user_duration_str = "1".to_string();
|
||||||
|
app.to_in_progress().unwrap();
|
||||||
|
|
||||||
|
let degraded = window::WindowSnapshot {
|
||||||
|
title: "Editor".to_string(),
|
||||||
|
class: None,
|
||||||
|
health: EvidenceHealth::Degraded("window class unavailable".to_string()),
|
||||||
|
};
|
||||||
|
app.tick_active_session_with_snapshot(degraded.clone())
|
||||||
|
.unwrap();
|
||||||
|
app.tick_active_session_with_snapshot(degraded).unwrap();
|
||||||
|
|
||||||
|
let records = EventLog::open(&path).unwrap().records().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
records
|
||||||
|
.iter()
|
||||||
|
.filter(|record| record.event_type == EventType::EvidenceObserved)
|
||||||
|
.count(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
let unavailable = window::WindowSnapshot::unavailable("xdotool active window unavailable");
|
||||||
|
app.tick_active_session_with_snapshot(unavailable).unwrap();
|
||||||
|
|
||||||
|
let records = EventLog::open(&path).unwrap().records().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
records
|
||||||
|
.iter()
|
||||||
|
.filter(|record| record.event_type == EventType::EvidenceObserved)
|
||||||
|
.count(),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
app.evidence_health,
|
||||||
|
EvidenceHealth::Unavailable("xdotool active window unavailable".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = fs::remove_file(path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+227
-2
@@ -1,6 +1,6 @@
|
|||||||
use crate::domain::{
|
use crate::domain::{
|
||||||
unix_secs_now, AllowedContext, Commitment, CommitmentSource, CommitmentState, EnforcementLevel,
|
unix_secs_now, AllowedContext, Commitment, CommitmentSource, CommitmentState, EnforcementLevel,
|
||||||
PolicySnapshot, RuntimeState, POLICY_SCHEMA_VERSION,
|
EvidenceHealth, PolicySnapshot, RuntimeState, POLICY_SCHEMA_VERSION,
|
||||||
};
|
};
|
||||||
use crate::event_log::{EventLog, EventRecord, EventType, PendingEventRecord};
|
use crate::event_log::{EventLog, EventRecord, EventType, PendingEventRecord};
|
||||||
use crate::state_machine::{
|
use crate::state_machine::{
|
||||||
@@ -77,6 +77,13 @@ struct ViolationObservedPayload {
|
|||||||
evidence: Value,
|
evidence: Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct EvidenceObservedPayload {
|
||||||
|
schema_version: u16,
|
||||||
|
health: EvidenceHealth,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
struct PendingTransitionPolicy {
|
struct PendingTransitionPolicy {
|
||||||
commitment_id: String,
|
commitment_id: String,
|
||||||
@@ -561,6 +568,30 @@ impl SessionController {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn record_evidence(&mut self, health: EvidenceHealth) -> Result<()> {
|
||||||
|
let payload = EvidenceObservedPayload {
|
||||||
|
schema_version: POLICY_SCHEMA_VERSION,
|
||||||
|
health,
|
||||||
|
};
|
||||||
|
validate_evidence_observed_payload(&payload)?;
|
||||||
|
let runtime_state = self.runtime_state;
|
||||||
|
let commitment_id = self
|
||||||
|
.active_commitment
|
||||||
|
.as_ref()
|
||||||
|
.map(|commitment| commitment.id.clone());
|
||||||
|
|
||||||
|
self.event_log
|
||||||
|
.append(
|
||||||
|
EventType::EvidenceObserved,
|
||||||
|
runtime_state,
|
||||||
|
commitment_id,
|
||||||
|
serde_json::to_value(payload).context("serialize evidence observed payload")?,
|
||||||
|
)
|
||||||
|
.context("log evidence observation")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hydrate_session(
|
fn hydrate_session(
|
||||||
@@ -914,6 +945,37 @@ fn hydrate_session(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
EventType::EvidenceObserved => {
|
||||||
|
let payload: EvidenceObservedPayload = parse_payload(&record, "evidence observed")?;
|
||||||
|
ensure_schema_version(payload.schema_version, record.sequence)?;
|
||||||
|
validate_evidence_observed_payload(&payload).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"validate evidence observed payload at sequence {}",
|
||||||
|
record.sequence
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if record.runtime_state != runtime_state {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"evidence runtime mismatch at sequence {}",
|
||||||
|
record.sequence
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(commitment_id) = record.commitment_id.as_deref() {
|
||||||
|
let active = active_commitment.as_ref().ok_or_else(|| {
|
||||||
|
anyhow!(
|
||||||
|
"evidence references unknown commitment '{}' at sequence {}",
|
||||||
|
commitment_id,
|
||||||
|
record.sequence
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if active.id != commitment_id {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"evidence commitment mismatch at sequence {}",
|
||||||
|
record.sequence
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
ref unsupported => {
|
ref unsupported => {
|
||||||
return Err(anyhow!(
|
return Err(anyhow!(
|
||||||
"unsupported session event {:?} at sequence {}",
|
"unsupported session event {:?} at sequence {}",
|
||||||
@@ -1082,6 +1144,19 @@ fn validate_violation_observed_payload(payload: &ViolationObservedPayload) -> Re
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate_evidence_observed_payload(payload: &EvidenceObservedPayload) -> Result<()> {
|
||||||
|
match &payload.health {
|
||||||
|
EvidenceHealth::Available => {}
|
||||||
|
EvidenceHealth::Degraded(reason) | EvidenceHealth::Unavailable(reason)
|
||||||
|
if reason.trim().is_empty() =>
|
||||||
|
{
|
||||||
|
return Err(anyhow!("evidence reason is required"));
|
||||||
|
}
|
||||||
|
EvidenceHealth::Degraded(_) | EvidenceHealth::Unavailable(_) => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
@@ -1090,7 +1165,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
use crate::domain::{
|
use crate::domain::{
|
||||||
unix_secs_now, AllowedContext, CommitmentSource, CommitmentState, EnforcementLevel,
|
unix_secs_now, AllowedContext, CommitmentSource, CommitmentState, EnforcementLevel,
|
||||||
RuntimeState, POLICY_SCHEMA_VERSION,
|
EvidenceHealth, RuntimeState, POLICY_SCHEMA_VERSION,
|
||||||
};
|
};
|
||||||
use crate::event_log::{EventLog, EventRecord, EventType, PendingEventRecord};
|
use crate::event_log::{EventLog, EventRecord, EventType, PendingEventRecord};
|
||||||
use crate::session::SessionController;
|
use crate::session::SessionController;
|
||||||
@@ -1317,6 +1392,156 @@ mod tests {
|
|||||||
fs::remove_file(path).unwrap();
|
fs::remove_file(path).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn records_evidence_with_health_runtime_and_active_commitment() {
|
||||||
|
let path = temp_log_path("evidence-record");
|
||||||
|
let mut session = SessionController::new(&path).unwrap();
|
||||||
|
|
||||||
|
session.enter_planning().unwrap();
|
||||||
|
session
|
||||||
|
.start_manual_commitment(
|
||||||
|
"Implement session controller",
|
||||||
|
"session tests pass",
|
||||||
|
Duration::from_secs(1500),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let commitment_id = session.active_commitment().unwrap().id.clone();
|
||||||
|
|
||||||
|
session
|
||||||
|
.record_evidence(EvidenceHealth::Degraded(
|
||||||
|
"window class unavailable".to_string(),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let records = read_records(&path);
|
||||||
|
let evidence = records
|
||||||
|
.iter()
|
||||||
|
.find(|record| record.event_type == EventType::EvidenceObserved)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(evidence.runtime_state, RuntimeState::Active);
|
||||||
|
assert_eq!(
|
||||||
|
evidence.commitment_id.as_deref(),
|
||||||
|
Some(commitment_id.as_str())
|
||||||
|
);
|
||||||
|
assert_eq!(evidence.payload_json["schema_version"], 1);
|
||||||
|
assert_eq!(
|
||||||
|
evidence.payload_json["health"],
|
||||||
|
json!({"degraded": "window class unavailable"})
|
||||||
|
);
|
||||||
|
|
||||||
|
let reopened = SessionController::new(&path).unwrap();
|
||||||
|
assert_eq!(reopened.runtime_state(), RuntimeState::Active);
|
||||||
|
assert_eq!(
|
||||||
|
reopened.active_commitment().unwrap().id.as_str(),
|
||||||
|
commitment_id.as_str()
|
||||||
|
);
|
||||||
|
|
||||||
|
fs::remove_file(path).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replay_ignores_valid_evidence_without_changing_state() {
|
||||||
|
let path = temp_log_path("evidence-replay");
|
||||||
|
let commitment_id = "commitment-test";
|
||||||
|
{
|
||||||
|
let mut log = EventLog::open(&path).unwrap();
|
||||||
|
log.append_batch([
|
||||||
|
PendingEventRecord {
|
||||||
|
event_type: EventType::RuntimeTransition,
|
||||||
|
runtime_state: RuntimeState::Planning,
|
||||||
|
commitment_id: None,
|
||||||
|
payload_json: json!({
|
||||||
|
"schema_version": POLICY_SCHEMA_VERSION,
|
||||||
|
"action": "enter_planning",
|
||||||
|
"from": "locked",
|
||||||
|
"to": "planning",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
PendingEventRecord {
|
||||||
|
event_type: EventType::CommitmentCreated,
|
||||||
|
runtime_state: RuntimeState::Active,
|
||||||
|
commitment_id: Some(commitment_id.to_string()),
|
||||||
|
payload_json: commitment_payload(commitment_id, CommitmentState::Active),
|
||||||
|
},
|
||||||
|
PendingEventRecord {
|
||||||
|
event_type: EventType::PolicyApplied,
|
||||||
|
runtime_state: RuntimeState::Active,
|
||||||
|
commitment_id: Some(commitment_id.to_string()),
|
||||||
|
payload_json: policy_payload(commitment_id),
|
||||||
|
},
|
||||||
|
PendingEventRecord {
|
||||||
|
event_type: EventType::EvidenceObserved,
|
||||||
|
runtime_state: RuntimeState::Active,
|
||||||
|
commitment_id: Some(commitment_id.to_string()),
|
||||||
|
payload_json: json!({
|
||||||
|
"schema_version": POLICY_SCHEMA_VERSION,
|
||||||
|
"health": {"unavailable": "xdotool active window unavailable"},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let reopened = SessionController::new(&path).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(reopened.runtime_state(), RuntimeState::Active);
|
||||||
|
assert_eq!(
|
||||||
|
reopened.active_commitment().unwrap().id.as_str(),
|
||||||
|
commitment_id
|
||||||
|
);
|
||||||
|
fs::remove_file(path).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replay_rejects_evidence_with_empty_reason() {
|
||||||
|
let path = temp_log_path("evidence-empty-reason");
|
||||||
|
let commitment_id = "commitment-test";
|
||||||
|
{
|
||||||
|
let mut log = EventLog::open(&path).unwrap();
|
||||||
|
log.append_batch([
|
||||||
|
PendingEventRecord {
|
||||||
|
event_type: EventType::RuntimeTransition,
|
||||||
|
runtime_state: RuntimeState::Planning,
|
||||||
|
commitment_id: None,
|
||||||
|
payload_json: json!({
|
||||||
|
"schema_version": POLICY_SCHEMA_VERSION,
|
||||||
|
"action": "enter_planning",
|
||||||
|
"from": "locked",
|
||||||
|
"to": "planning",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
PendingEventRecord {
|
||||||
|
event_type: EventType::CommitmentCreated,
|
||||||
|
runtime_state: RuntimeState::Active,
|
||||||
|
commitment_id: Some(commitment_id.to_string()),
|
||||||
|
payload_json: commitment_payload(commitment_id, CommitmentState::Active),
|
||||||
|
},
|
||||||
|
PendingEventRecord {
|
||||||
|
event_type: EventType::PolicyApplied,
|
||||||
|
runtime_state: RuntimeState::Active,
|
||||||
|
commitment_id: Some(commitment_id.to_string()),
|
||||||
|
payload_json: policy_payload(commitment_id),
|
||||||
|
},
|
||||||
|
PendingEventRecord {
|
||||||
|
event_type: EventType::EvidenceObserved,
|
||||||
|
runtime_state: RuntimeState::Active,
|
||||||
|
commitment_id: Some(commitment_id.to_string()),
|
||||||
|
payload_json: json!({
|
||||||
|
"schema_version": POLICY_SCHEMA_VERSION,
|
||||||
|
"health": {"degraded": " "},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let err =
|
||||||
|
SessionController::new(&path).expect_err("evidence with empty reason must be rejected");
|
||||||
|
|
||||||
|
assert!(error_chain_contains(&err, "evidence reason"));
|
||||||
|
fs::remove_file(path).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn record_violation_rejects_empty_reason() {
|
fn record_violation_rejects_empty_reason() {
|
||||||
let path = temp_log_path("violation-empty-reason");
|
let path = temp_log_path("violation-empty-reason");
|
||||||
|
|||||||
Reference in New Issue
Block a user