blob: d5c603d0ec6d9e4b636a26e9a5440900b6d45722 [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 maka_agent_facts::continuation::ContinuationClaim;
use maka_agent_facts::continuation::REPLAY_VERSION;
use maka_agent_facts::continuation::ReplayEvidence;
use maka_agent_facts::continuation::SessionBase;
use maka_agent_facts::event::EventWrite;
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::Invocation;
use maka_agent_facts::event::InvocationOutcome;
use maka_agent_facts::event::RuntimeEvent;
use maka_agent_facts::event::StoredEvent;
use maka_agent_facts::execution::BehaviorId;
use maka_agent_facts::execution::CollaborationMode;
use maka_agent_facts::execution::InvocationConfiguration;
use maka_agent_facts::execution::SandboxMode;
use maka_agent_facts::execution::ToolMode;
use maka_agent_facts::execution::WorkspaceIdentity;
use maka_agent_facts::input::InvocationInput;
use maka_event_log::EventLog;
use maka_presentation::{Content, InvocationView, TurnState};
#[path = "continuation/bundle.rs"]
mod bundle;
#[path = "continuation/fixtures.rs"]
mod fixtures;
#[path = "continuation/handoff.rs"]
mod handoff;
#[path = "continuation/handoff_budget.rs"]
mod handoff_budget;
#[path = "continuation/handoff_cancel.rs"]
mod handoff_cancel;
#[path = "continuation/handoff_claim.rs"]
mod handoff_claim;
#[path = "continuation/handoff_consumers.rs"]
mod handoff_consumers;
#[path = "continuation/imported.rs"]
mod imported;
use fixtures::*;
#[tokio::test]
async fn canonical_claim_is_atomic_unique_and_authenticates_the_entire_lineage_after_reopen() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("continuation.sqlite");
let log = EventLog::open(&path).await.unwrap();
log.create_session("session", "history-source", &serde_json::json!({}), 1)
.await
.unwrap();
let prior = opening("prior", None);
append(&log, &prior).await;
close(&log, &prior).await;
let mut source = opening("source", None);
if let Fact::InvocationOpened {
input:
InvocationInput::Message {
content,
source_messages,
..
},
..
} = &mut source.fact
{
source_messages.push(maka_agent_facts::message::RootSourceMessage {
unprepared_content: content.clone(),
message: maka_agent_facts::input::DeliveredMessage {
origin: None,
message_id: "origin-message".into(),
submitted_content_digest: content.content_digest().unwrap(),
content: content.clone(),
},
submitted_placement: maka_agent_facts::message::Placement::NextTurn,
disposition: maka_agent_facts::message::MessageDisposition::TurnStarted,
submitted_intent: None,
});
}
append(&log, &source).await;
close(&log, &source).await;
let context = log
.context_before_run(&source.invocation, 100, 65536)
.await
.unwrap();
let base = SessionBase {
high_water: context.source_evidence.high_water,
digest: context.source_evidence.digest,
};
assert_eq!(base.high_water, 2);
let original = claim(&log, "claim-first", &source, base.clone()).await;
let before = log.prefix(100, 65536).await.unwrap();
assert!(
log.continuation_for_source(&original.source)
.await
.unwrap()
.is_none()
);
// Invalid raw evidence and workspace observations must not acquire the boundary.
for mutation in 0..5 {
let mut changed = original.clone();
match mutation {
0 => changed.source.digest = digest('c'),
1 => changed.base.digest = digest('c'),
2 => changed.source.invocation.turn_id = "wrong-source-turn".into(),
_ => {}
}
let mut target = opening("first", Some(changed));
if let Fact::InvocationOpened {
configuration: Some(configuration),
..
} = &mut target.fact
{
match mutation {
3 => configuration.workspace_identity = None,
4 => {
configuration.workspace_identity = Some(
WorkspaceIdentity::from_marker_id("bedbc850-d324-435a-9374-a02ae5037244")
.unwrap(),
)
}
_ => {}
}
}
assert!(
log.append(&EventWrite::plain(target).unwrap())
.await
.is_err()
);
}
for mutation in 0..3 {
let mut target = opening("first", Some(original.clone()));
match mutation {
0 => target.invocation.turn_id = source.invocation.turn_id.clone(),
1 => target.invocation.session_id = "other-session".into(),
_ => {
if let Fact::InvocationOpened {
input: InvocationInput::Continuation { claim, .. },
..
} = &mut target.fact
{
claim.replay.version += 1;
}
}
}
assert!(EventWrite::plain(target).is_err());
}
let first = opening("first", Some(original.clone()));
let db = rusqlite::Connection::open(&path).unwrap();
db.execute_batch(
"CREATE TRIGGER reject_continuation BEFORE INSERT ON event_log
WHEN json_extract(NEW.event_json,'$.fact.input.kind')='continuation'
BEGIN SELECT RAISE(ABORT,'injected opening failure'); END;",
)
.unwrap();
assert!(
log.append(&EventWrite::plain(first.clone()).unwrap())
.await
.is_err()
);
assert!(
log.continuation_for_source(&original.source)
.await
.unwrap()
.is_none()
);
assert_eq!(
serde_json::to_value(log.prefix(100, 65536).await.unwrap()).unwrap(),
serde_json::to_value(&before).unwrap()
);
db.execute_batch("DROP TRIGGER reject_continuation")
.unwrap();
let sequence = append(&log, &first).await;
let committed = log
.continuation_for_source(&original.source)
.await
.unwrap()
.unwrap();
assert_eq!(committed.event, first);
assert_eq!(committed.sequence, sequence);
let mut view = InvocationView::new();
assert!(
view.push(&committed).unwrap().iter().all(|row| matches!(
row.message.content,
maka_presentation::Content::TurnState {
state: maka_presentation::TurnState::Running
}
)),
"continuation must not fabricate a user message"
);
let terminal = RuntimeEvent::new(
first.invocation.clone(),
Fact::InvocationEnded {
outcome: InvocationOutcome::Completed,
},
);
let terminal_sequence = append(&log, &terminal).await;
let revision = log
.get_session::<serde_json::Value>("session")
.await
.unwrap()
.unwrap()
.revision;
use maka_event_log::context::{ContextEvent, HistoryCapture, HistoryCut};
for (cut, expected) in [
(
HistoryCut::BeforeTurn(first.invocation.turn_id.clone()),
sequence - 1,
),
(
HistoryCut::ThroughTurn(first.invocation.turn_id.clone()),
terminal_sequence,
),
] {
let HistoryCapture::Captured(history) = log
.capture_session_history("session", revision, cut, 100, 65536)
.await
.unwrap()
else {
panic!("unchanged source");
};
assert_eq!(history.context.source_evidence.high_water, expected);
assert_eq!(
history.context.tail.iter().any(|entry| matches!(entry,
ContextEvent::Canonical(entry) if entry.event == first
)),
expected == terminal_sequence
);
}
let rows = view
.push(&StoredEvent {
sequence: terminal_sequence,
event: terminal,
})
.unwrap();
assert_eq!(rows.len(), 1);
assert!(matches!(
rows[0].message.content,
Content::TurnState {
state: TurnState::Completed
}
));
let mut duplicate = original.clone();
duplicate.id = "different-claim".into();
assert!(
log.append(&EventWrite::plain(opening("duplicate", Some(duplicate))).unwrap())
.await
.is_err()
);
assert_eq!(
append(&log, &first).await,
sequence,
"exact retry remains legal after sealing"
);
let mut wrong_lookup = original.source.clone();
wrong_lookup.digest = digest('e');
assert!(log.continuation_for_source(&wrong_lookup).await.is_err());
let second_claim = claim(&log, "claim-second", &first, base.clone()).await;
let reused_turn = opening("prior", Some(second_claim.clone()));
assert!(
log.append(&EventWrite::plain(reused_turn).unwrap())
.await
.is_err()
);
let second = opening("second", Some(second_claim.clone()));
append(&log, &second).await;
close(&log, &second).await;
let third_claim = claim(&log, "claim-third", &second, base).await;
let unrelated = opening("unrelated", None);
append(&log, &unrelated).await;
close(&log, &unrelated).await;
log.close().await.unwrap();
let log = EventLog::open(&path).await.unwrap();
assert!(
matches!(
log.message_execution("session", "origin-message").await.unwrap(),
maka_event_log::message_resolution::MessageExecution::Owned(owner) if owner.invocation == second.invocation
),
"Message ownership follows continuation claims, never the latest Session root"
);
assert_eq!(
log.continuation_for_source(&original.source)
.await
.unwrap()
.unwrap()
.event,
first
);
assert_eq!(
log.continuation_for_source(&second_claim.source)
.await
.unwrap()
.unwrap()
.event,
second
);
assert_eq!(append(&log, &first).await, sequence);
let committed_before_corruption = log.prefix(100, 65536).await.unwrap().high_water;
// Current parent still hashes identically, but an ancestor's raw bytes changed.
db.execute(
"UPDATE event_log SET event_json=event_json || ' ' WHERE event_id=?",
[&source.id],
)
.unwrap();
assert!(
log.append(&EventWrite::plain(opening("third", Some(third_claim.clone()))).unwrap())
.await
.is_err()
);
assert!(
log.continuation_for_source(&third_claim.source)
.await
.unwrap()
.is_none()
);
assert_eq!(
log.prefix(100, 65536).await.unwrap().high_water,
committed_before_corruption
);
log.close().await.unwrap();
}
#[tokio::test]
async fn message_owner_rejects_ambiguous_private_successors_without_repair_after_reopen() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("ambiguous-successors.sqlite");
let log = EventLog::open(&path).await.unwrap();
log.create_session("session", "history-source", &serde_json::json!({}), 1)
.await
.unwrap();
let mut source = opening("source", None);
let Fact::InvocationOpened {
input:
InvocationInput::Message {
content,
source_messages,
..
},
..
} = &mut source.fact
else {
unreachable!()
};
source_messages.push(maka_agent_facts::message::RootSourceMessage {
unprepared_content: content.clone(),
message: maka_agent_facts::input::DeliveredMessage {
origin: None,
message_id: "origin-message".into(),
submitted_content_digest: content.content_digest().unwrap(),
content: content.clone(),
},
submitted_placement: maka_agent_facts::message::Placement::NextTurn,
disposition: maka_agent_facts::message::MessageDisposition::TurnStarted,
submitted_intent: None,
});
append(&log, &source).await;
close(&log, &source).await;
let context = log
.context_before_run(&source.invocation, 100, 65536)
.await
.unwrap();
let original = claim(
&log,
"valid-claim",
&source,
SessionBase {
high_water: context.source_evidence.high_water,
digest: context.source_evidence.digest,
},
)
.await;
let successor = opening("successor", Some(original.clone()));
append(&log, &successor).await;
assert!(matches!(
log.message_execution("session", "origin-message")
.await
.unwrap(),
maka_event_log::message_resolution::MessageExecution::Owned(owner)
if owner.invocation == successor.invocation
));
// Malformed private evidence, not a supported live dual admission: the
// distinct high-water and claim IDs preserve the schema's unique indexes.
let mut competing = original;
competing.id = "malformed-claim".into();
competing.source.high_water += 1;
let malformed = opening("malformed-successor", Some(competing));
let db = rusqlite::Connection::open(&path).unwrap();
db.execute(
"INSERT INTO event_log (event_id, invocation_id, kind, event_json)
VALUES (?1, ?2, 'invocation_opened', ?3)",
rusqlite::params![
malformed.id,
malformed.invocation.invocation_id,
serde_json::to_string(&malformed).unwrap(),
],
)
.unwrap();
let raw = || {
let mut statement = db
.prepare("SELECT sequence, event_json FROM event_log ORDER BY sequence")
.unwrap();
statement
.query_map([], |row| {
Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?))
})
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap()
};
let raw_before = raw();
let canonical_before = serde_json::to_value(log.prefix(100, 65536).await.unwrap()).unwrap();
assert!(matches!(
log.message_execution("session", "origin-message").await,
Err(maka_event_log::StoreError::InvalidTransition(reason))
if reason == "Message owner has ambiguous continuations"
));
assert_eq!(raw(), raw_before);
assert_eq!(
serde_json::to_value(log.prefix(100, 65536).await.unwrap()).unwrap(),
canonical_before
);
log.close().await.unwrap();
let log = EventLog::open(&path).await.unwrap();
assert!(matches!(
log.message_execution("session", "origin-message").await,
Err(maka_event_log::StoreError::InvalidTransition(reason))
if reason == "Message owner has ambiguous continuations"
));
assert_eq!(raw(), raw_before);
assert_eq!(
serde_json::to_value(log.prefix(100, 65536).await.unwrap()).unwrap(),
canonical_before
);
log.close().await.unwrap();
}