blob: afd15162e140e598caafcb94b240c0c72c8626b6 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use super::*;
use maka_event_log::context::{ContextEvent, LatestMainContext, SourceEvidence};
fn invocation(id: &str) -> Invocation {
Invocation {
session_id: "session".into(),
turn_id: id.into(),
run_id: id.into(),
invocation_id: id.into(),
}
}
fn assert_same_base(actual: &ModelContextSource, expected: &ModelContextSource) {
assert_eq!(actual.source_evidence.scope, expected.source_evidence.scope);
assert_eq!(
actual.source_evidence.high_water,
expected.source_evidence.high_water
);
assert_eq!(
actual.source_evidence.digest,
expected.source_evidence.digest
);
assert_eq!(
actual.effective_source_digest,
expected.effective_source_digest
);
assert_eq!(
actual
.baseline
.as_ref()
.map(|b| (&b.event_id, serde_json::to_value(&b.checkpoint).unwrap())),
expected
.baseline
.as_ref()
.map(|b| (&b.event_id, serde_json::to_value(&b.checkpoint).unwrap())),
);
let canonical = |source: &ModelContextSource| {
source
.tail
.iter()
.map(|event| match event {
ContextEvent::Canonical(event) => serde_json::to_value(event).unwrap(),
ContextEvent::Archived(_) => panic!("fixture has no archived tools"),
ContextEvent::ModelItems(items) => serde_json::to_value(items).unwrap(),
})
.collect::<Vec<_>>()
};
assert_eq!(canonical(actual), canonical(expected));
let (LatestMainContext::Selected(actual), LatestMainContext::Selected(expected)) =
(&actual.latest_main, &expected.latest_main)
else {
panic!("expected completed Main evidence")
};
assert_eq!(actual.sequence, expected.sequence);
assert_eq!(actual.finished_at, expected.finished_at);
assert_eq!(actual.requested_at, expected.requested_at);
assert_eq!(actual.model_id, expected.model_id);
assert_eq!(actual.connection_id, expected.connection_id);
assert_eq!(actual.checkpoint_event_id, expected.checkpoint_event_id);
assert_eq!(actual.projection_current, expected.projection_current);
assert_eq!(actual.usage, expected.usage);
}
#[tokio::test]
async fn frozen_base_preserves_original_checkpoint_tail_and_diagnostics_across_later_work() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("frozen.sqlite");
let log = EventLog::open(&path).await.unwrap();
closed(&log, "first", 32, 1).await;
let empty = log
.context_before_run(&invocation("first"), 0, 0)
.await
.unwrap();
assert_eq!(empty.source_evidence.high_water, 0);
assert!(empty.tail.is_empty());
let checkpoint = prepared(&log, "baseline").await;
log.append_batch(&checkpoint).await.unwrap();
let hidden_evidence = closed(&log, "prior", 32, 1).await;
let expected = log.read_model_context("session", None).await.unwrap();
closed(&log, "source", 64, 1).await;
let actual = log
.context_before_run(&invocation("source"), 100, 32768)
.await
.unwrap();
assert_same_base(&actual, &expected);
assert!(
actual.tail.iter().all(
|e| matches!(e, ContextEvent::Canonical(e) if e.event.invocation.run_id != "source")
)
);
let second_checkpoint = prepared(&log, "later-baseline").await;
log.append_batch(&second_checkpoint).await.unwrap();
closed(&log, "later-message", 128, 1).await;
let mut other = fixtures::event(
"other",
Fact::InvocationOpened {
configuration: None,
input: InvocationInput::Message {
content: "other Session".into(),
request_fingerprint: None,
source_messages: Vec::new(),
},
},
)
.event()
.clone();
other.invocation.session_id = "unrelated".into();
log.append(&EventWrite::plain(other).unwrap())
.await
.unwrap();
log.append(&fixtures::event(
"unfinished-later",
Fact::InvocationOpened {
configuration: None,
input: InvocationInput::Message {
content: "later incomplete work".into(),
request_fingerprint: None,
source_messages: Vec::new(),
},
},
))
.await
.unwrap();
assert!(log.read_model_context("session", None).await.is_err());
assert_same_base(
&log.read_frozen_model_context(&expected.source_evidence, 100, 32768)
.await
.unwrap(),
&expected,
);
assert_same_base(
&log.context_before_run(&invocation("source"), 100, 32768)
.await
.unwrap(),
&expected,
);
let mut wrong = invocation("source");
wrong.turn_id = "different-turn".into();
assert!(log.context_before_run(&wrong, 100, 32768).await.is_err());
for (events, bytes) in [(0, 32768), (100, 1)] {
assert!(matches!(
log.read_frozen_model_context(&expected.source_evidence, events, bytes)
.await,
Err(StoreError::PrefixTooLarge)
));
}
for changed in 0..3 {
let mut forged = expected.source_evidence.clone();
match changed {
0 => forged.digest = "sha256:changed".into(),
1 => forged.high_water = u64::MAX,
_ => {
forged.scope = LogScope::Session {
id: "unrelated".into(),
}
}
}
assert!(
log.read_frozen_model_context(&forged, 100, 32768)
.await
.is_err()
);
}
log.close().await.unwrap();
let log = EventLog::open(&path).await.unwrap();
assert_same_base(
&log.read_frozen_model_context(&expected.source_evidence, 100, 32768)
.await
.unwrap(),
&expected,
);
assert!(
log.read_frozen_model_context(&empty.source_evidence, 0, 0)
.await
.unwrap()
.tail
.is_empty()
);
// Same decoded observation, different raw source bytes; this row is not in the rendered tail.
let inspect = rusqlite::Connection::open(&path).unwrap();
inspect
.execute(
"UPDATE event_log SET event_json=' ' || event_json WHERE event_id=?",
[&hidden_evidence],
)
.unwrap();
assert!(
log.read_frozen_model_context(&expected.source_evidence, 100, 32768)
.await
.is_err()
);
drop(inspect);
log.close().await.unwrap();
}
#[tokio::test]
async fn a_closed_frozen_boundary_does_not_prove_that_tool_effects_are_settled() {
let directory = tempfile::tempdir().unwrap();
let log = EventLog::open(&directory.path().join("safety.sqlite"))
.await
.unwrap();
log.append(&fixtures::event(
"live",
Fact::InvocationOpened {
configuration: None,
input: InvocationInput::Message {
content: "unfinished tool".into(),
request_fingerprint: None,
source_messages: Vec::new(),
},
},
))
.await
.unwrap();
log.append(&fixtures::event(
"live",
Fact::ToolDispatched {
presentation: None,
title: None,
operation_id: "write".into(),
call: ToolCallIdentity::standalone("write-call".into()),
name: "write".into(),
input: json!({}),
},
))
.await
.unwrap();
let scope = LogScope::Session {
id: "session".into(),
};
let partial = log.scoped_prefix(scope.clone(), 100, 32768).await.unwrap();
log.append(&fixtures::event(
"live",
Fact::InvocationEnded {
outcome: InvocationOutcome::Failed {
model_failure: None,
class: "outcome_unknown".into(),
message: None,
},
},
))
.await
.unwrap();
let partial = SourceEvidence {
scope: partial.scope,
high_water: partial.high_water,
digest: partial.digest,
};
assert!(
log.read_frozen_model_context(&partial, 100, 32768)
.await
.unwrap_err()
.to_string()
.contains("closed Session boundary")
);
let sealed = log.scoped_prefix(scope, 100, 32768).await.unwrap();
let sealed = SourceEvidence {
scope: sealed.scope,
high_water: sealed.high_water,
digest: sealed.digest,
};
assert!(
log.read_frozen_model_context(&sealed, 100, 32768)
.await
.unwrap_err()
.to_string()
.contains("unresolved prior execution")
);
log.close().await.unwrap();
}