Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,17 @@ jobs:
- name: Test all features
run: cargo test --all-features

- name: Test optional features independently
run: |
cargo test --no-default-features --features sqlite
cargo test --no-default-features --features tools
cargo test --no-default-features --features multimodal

- name: Coverage gate
uses: taiki-e/install-action@cargo-llvm-cov

- name: Verify line coverage
run: cargo llvm-cov --all-features --workspace --fail-under-lines 90
run: >-
cargo llvm-cov --all-features --workspace
--ignore-filename-regex '(^|/)(tests?|examples)/|/test(_.*)?\.rs$'
--fail-under-lines 85
181 changes: 181 additions & 0 deletions src/session/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,187 @@ fn session_search_params_defaults() {
assert!(params.offset.is_none());
}

// ── Public operation boundary ────────────────────────────────────────────

#[test]
fn public_session_operations_round_trip_every_record_kind() {
let dir = tempfile::tempdir().unwrap();
let workspace = dir.path();

let started = record_session_start(
workspace,
"parent",
"agent",
"Agent Name",
"session-key",
None,
Some("thread-1"),
Some("cli"),
Some("model-1"),
Some("transcripts/parent.jsonl"),
)
.unwrap();
assert_eq!(started.status, SessionStatus::Running);
assert_eq!(started.thread_id.as_deref(), Some("thread-1"));

let message_id = record_message(
workspace,
"parent",
"user",
"inspect the repository",
Some("model-1"),
Some(12),
Some(4),
Some(0.25),
)
.unwrap();
let tool_id = record_tool_call(
workspace,
"parent",
Some(message_id),
"read_file",
Some(r#"{"path":"Cargo.toml"}"#),
Some("package metadata"),
"completed",
Some(7),
)
.unwrap();

let messages = list_messages(workspace, "parent", None).unwrap();
assert_eq!(messages.len(), 1);
assert_eq!(messages[0].id, message_id);
assert_eq!(messages[0].input_tokens, Some(12));
let tools = list_tool_calls(workspace, "parent", None).unwrap();
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].id, tool_id);
assert_eq!(tools[0].message_id, Some(message_id));

let ended = record_session_end(
workspace,
"parent",
SessionStatus::Completed,
2,
12,
4,
3,
0.25,
)
.unwrap();
assert_eq!(ended.status, SessionStatus::Completed);
assert_eq!(ended.turn_count, 2);
assert!(ended.ended_at.is_some());
}

#[test]
fn public_listing_filters_and_caps_results() {
let dir = tempfile::tempdir().unwrap();
let workspace = dir.path();
for (id, parent, status) in [
("root", None, SessionStatus::Running),
("child-a", Some("root"), SessionStatus::Completed),
("child-b", Some("root"), SessionStatus::Failed),
] {
record_session_start(
workspace, id, "agent", "Agent", id, parent, None, None, None, None,
)
.unwrap();
if status != SessionStatus::Running {
record_session_end(workspace, id, status, 0, 0, 0, 0, 0.0).unwrap();
}
}

let children = list_children(workspace, "root").unwrap();
assert_eq!(children.len(), 2);
let failed =
list_sessions(workspace, Some(5000), Some(0), Some("failed"), Some("root")).unwrap();
assert_eq!(failed.total, 1);
assert_eq!(failed.sessions[0].id, "child-b");
let second_page = list_sessions(workspace, Some(1), Some(1), None, None).unwrap();
assert_eq!(second_page.total, 3);
assert_eq!(second_page.sessions.len(), 1);
}

#[test]
fn public_search_combines_all_supported_filters() {
let dir = tempfile::tempdir().unwrap();
let workspace = dir.path();
record_session_start(
workspace, "parent", "agent", "Parent", "parent", None, None, None, None, None,
)
.unwrap();
record_session_start(
workspace,
"match",
"researcher",
"Research Agent",
"key",
Some("parent"),
Some("thread-7"),
Some("api"),
None,
None,
)
.unwrap();
record_message(
workspace,
"match",
"user",
"rare searchable phrase",
None,
None,
None,
None,
)
.unwrap();
record_tool_call(
workspace,
"match",
None,
"lookup",
None,
None,
"completed",
None,
)
.unwrap();

let result = search_sessions(
workspace,
&SessionSearchParams {
query: Some("rare phrase".into()),
agent_id: Some("researcher".into()),
tool_name: Some("lookup".into()),
source_channel: Some("api".into()),
parent_session_id: Some("parent".into()),
status: Some("running".into()),
thread_id: Some("thread-7".into()),
limit: Some(1),
offset: Some(0),
},
)
.unwrap();
assert_eq!(result.total, 1);
assert_eq!(result.sessions[0].id, "match");
}

#[test]
fn public_mark_interrupted_is_idempotent_and_missing_session_errors() {
let dir = tempfile::tempdir().unwrap();
let workspace = dir.path();
assert!(get_session(workspace, "missing").is_err());
record_session_start(
workspace, "running", "agent", "Agent", "key", None, None, None, None, None,
)
.unwrap();

assert_eq!(mark_interrupted(workspace).unwrap(), 1);
assert_eq!(mark_interrupted(workspace).unwrap(), 0);
assert_eq!(
get_session(workspace, "running").unwrap().status,
SessionStatus::Interrupted
);
}

// ── store.rs ───────────────────────────────────────────────────────────

#[test]
Expand Down
94 changes: 94 additions & 0 deletions tests/e2e_session_lifecycle.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//! End-to-end coverage for a host-visible session lifecycle.
#![cfg(feature = "sqlite")]

use chrono::{Duration, Utc};
use tinyagents::session::{self, SessionSearchParams, SessionStatus};

#[test]
fn parent_and_child_sessions_are_recorded_searched_completed_and_retained() {
let dir = tempfile::tempdir().unwrap();
let workspace = dir.path();

session::record_session_start(
workspace,
"parent",
"orchestrator",
"Orchestrator",
"parent-key",
None,
Some("thread"),
Some("test"),
Some("mock"),
None,
)
.unwrap();
session::record_session_start(
workspace,
"child",
"researcher",
"Researcher",
"child-key",
Some("parent"),
Some("thread"),
Some("test"),
Some("mock"),
None,
)
.unwrap();
let message_id = session::record_message(
workspace,
"child",
"assistant",
"the answer contains cobalt",
Some("mock"),
Some(8),
Some(5),
Some(0.01),
)
.unwrap();
session::record_tool_call(
workspace,
"child",
Some(message_id),
"search",
Some(r#"{"q":"cobalt"}"#),
Some("one result"),
"completed",
Some(2),
)
.unwrap();

let found = session::search_sessions(
workspace,
&SessionSearchParams {
query: Some("cobalt".into()),
tool_name: Some("search".into()),
parent_session_id: Some("parent".into()),
..Default::default()
},
)
.unwrap();
assert_eq!(found.total, 1);
assert_eq!(found.sessions[0].id, "child");
assert_eq!(
session::list_children(workspace, "parent").unwrap().len(),
1
);

for id in ["child", "parent"] {
session::record_session_end(workspace, id, SessionStatus::Completed, 1, 8, 5, 0, 0.01)
.unwrap();
}
let completed = session::list_sessions(workspace, None, None, Some("completed"), None).unwrap();
assert_eq!(completed.total, 2);

let report = session::apply_retention(workspace, Utc::now() + Duration::seconds(1)).unwrap();
assert_eq!(report.sessions, 2);
assert_eq!(report.total(), 2);
assert_eq!(
session::list_sessions(workspace, None, None, None, None)
.unwrap()
.total,
0
);
}
Loading