sync from monorepo @ 2452e92e
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
{"type":"user","uuid":"r-001","parentUuid":null,"timestamp":"2026-03-23T10:00:00.000Z","sessionId":"test-session-tree","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"isMeta":false,"userType":"external","message":{"role":"user","content":"Help me"}}
|
||||
{"type":"assistant","uuid":"a-001","parentUuid":"r-001","timestamp":"2026-03-23T10:00:01.000Z","sessionId":"test-session-tree","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"requestId":"req-001","message":{"model":"claude-opus-4-6","id":"msg-001","type":"message","role":"assistant","content":[{"type":"text","text":"Sure"}],"stop_reason":"end_turn","usage":{"input_tokens":10,"output_tokens":5}}}
|
||||
{"type":"user","uuid":"u-002","parentUuid":"a-001","timestamp":"2026-03-23T10:00:02.000Z","sessionId":"test-session-tree","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"isMeta":false,"userType":"external","message":{"role":"user","content":"Do option A"}}
|
||||
{"type":"assistant","uuid":"a-003","parentUuid":"u-002","timestamp":"2026-03-23T10:00:03.000Z","sessionId":"test-session-tree","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"requestId":"req-002","message":{"model":"claude-opus-4-6","id":"msg-003","type":"message","role":"assistant","content":[{"type":"text","text":"Doing A"}],"stop_reason":"end_turn","usage":{"input_tokens":15,"output_tokens":5}}}
|
||||
{"type":"user","uuid":"u-002b","parentUuid":"a-001","timestamp":"2026-03-23T10:00:04.000Z","sessionId":"test-session-tree","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"isMeta":false,"userType":"external","message":{"role":"user","content":"Actually, do option B"}}
|
||||
{"type":"assistant","uuid":"a-003b","parentUuid":"u-002b","timestamp":"2026-03-23T10:00:05.000Z","sessionId":"test-session-tree","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"requestId":"req-003","message":{"model":"claude-opus-4-6","id":"msg-003b","type":"message","role":"assistant","content":[{"type":"text","text":"Doing B"}],"stop_reason":"end_turn","usage":{"input_tokens":15,"output_tokens":5}}}
|
||||
@@ -0,0 +1,6 @@
|
||||
{"type":"queue-operation","operation":"enqueue","timestamp":"2026-03-14T21:00:00.000Z","sessionId":"test-session-001"}
|
||||
{"type":"queue-operation","operation":"dequeue","timestamp":"2026-03-14T21:00:00.001Z","sessionId":"test-session-001"}
|
||||
{"type":"user","uuid":"u-001","parentUuid":null,"timestamp":"2026-03-14T21:00:01.000Z","sessionId":"test-session-001","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"isMeta":false,"userType":"external","message":{"role":"user","content":"Hello, help me with this project"}}
|
||||
{"type":"assistant","uuid":"a-001","parentUuid":"u-001","timestamp":"2026-03-14T21:00:02.000Z","sessionId":"test-session-001","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"requestId":"req-001","message":{"model":"claude-opus-4-6","id":"msg-001","type":"message","role":"assistant","content":[{"type":"text","text":"I'll help you."},{"type":"tool_use","id":"toolu_01","name":"Bash","input":{"command":"ls","description":"List files"}}],"stop_reason":"tool_use","usage":{"input_tokens":100,"output_tokens":50}}}
|
||||
{"type":"user","uuid":"u-002","parentUuid":"a-001","timestamp":"2026-03-14T21:00:03.000Z","sessionId":"test-session-001","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"isMeta":true,"userType":"external","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_01","content":"file1.rs\nfile2.rs","is_error":false}]}}
|
||||
{"type":"assistant","uuid":"a-002","parentUuid":"u-002","timestamp":"2026-03-14T21:00:04.000Z","sessionId":"test-session-001","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"requestId":"req-002","message":{"model":"claude-opus-4-6","id":"msg-002","type":"message","role":"assistant","content":[{"type":"text","text":"I can see two Rust files in the directory."}],"stop_reason":"end_turn","usage":{"input_tokens":200,"output_tokens":30}}}
|
||||
@@ -0,0 +1,9 @@
|
||||
{"type":"queue-operation","operation":"enqueue","timestamp":"2026-03-14T21:00:00.000Z","sessionId":"test-session-noise"}
|
||||
{"type":"user","uuid":"u-n-001","parentUuid":null,"timestamp":"2026-03-14T21:00:01.000Z","sessionId":"test-session-noise","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"isMeta":true,"message":{"role":"user","content":"system injected stuff"}}
|
||||
{"type":"user","uuid":"u-n-002","parentUuid":"u-n-001","timestamp":"2026-03-14T21:00:02.000Z","sessionId":"test-session-noise","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"isMeta":false,"message":{"role":"user","content":"Warmup"}}
|
||||
{"type":"user","uuid":"u-n-003","parentUuid":"u-n-002","timestamp":"2026-03-14T21:00:03.000Z","sessionId":"test-session-noise","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"isMeta":false,"message":{"role":"user","content":"[Request interrupted by user"}}
|
||||
{"type":"user","uuid":"u-n-004","parentUuid":"u-n-003","timestamp":"2026-03-14T21:00:04.000Z","sessionId":"test-session-noise","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"isMeta":false,"message":{"role":"user","content":"This session is being continued from a previous conversation"}}
|
||||
{"type":"user","uuid":"u-n-005","parentUuid":"u-n-004","timestamp":"2026-03-14T21:00:05.000Z","sessionId":"test-session-noise","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"isMeta":false,"message":{"role":"user","content":"API Error: rate limit exceeded"}}
|
||||
{"type":"user","uuid":"u-n-006","parentUuid":"u-n-005","timestamp":"2026-03-14T21:00:06.000Z","sessionId":"test-session-noise","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"isMeta":false,"message":{"role":"user","content":"Caveat: The messages below were generated by the user"}}
|
||||
{"type":"user","uuid":"u-n-007","parentUuid":"u-n-006","timestamp":"2026-03-14T21:00:07.000Z","sessionId":"test-session-noise","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"isMeta":false,"message":{"role":"user","content":"Please help me fix this bug"}}
|
||||
{"type":"assistant","uuid":"a-n-001","parentUuid":"u-n-007","timestamp":"2026-03-14T21:00:08.000Z","sessionId":"test-session-noise","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"message":{"id":"msg-n-001","role":"assistant","content":[{"type":"text","text":"Sure, let me help."}],"stop_reason":"end_turn"}}
|
||||
@@ -0,0 +1,6 @@
|
||||
{"type":"user","uuid":"u-100","parentUuid":null,"timestamp":"2026-03-23T10:00:00.000Z","sessionId":"test-session-dedup","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"isMeta":false,"userType":"external","message":{"role":"user","content":"What files are here?"}}
|
||||
{"type":"assistant","uuid":"a-100","parentUuid":"u-100","timestamp":"2026-03-23T10:00:01.000Z","sessionId":"test-session-dedup","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"requestId":"req-100","message":{"model":"claude-opus-4-6","id":"msg-100","type":"message","role":"assistant","content":[{"type":"text","text":"Let me"}],"stop_reason":null,"usage":{"input_tokens":50,"output_tokens":3}}}
|
||||
{"type":"assistant","uuid":"a-100","parentUuid":"u-100","timestamp":"2026-03-23T10:00:01.100Z","sessionId":"test-session-dedup","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"requestId":"req-100","message":{"model":"claude-opus-4-6","id":"msg-100","type":"message","role":"assistant","content":[{"type":"text","text":"Let me look"},{"type":"tool_use","id":"toolu_100","name":"Bash","input":{"command":""}}],"stop_reason":null,"usage":{"input_tokens":50,"output_tokens":12}}}
|
||||
{"type":"assistant","uuid":"a-100","parentUuid":"u-100","timestamp":"2026-03-23T10:00:01.200Z","sessionId":"test-session-dedup","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"requestId":"req-100","message":{"model":"claude-opus-4-6","id":"msg-100","type":"message","role":"assistant","content":[{"type":"text","text":"Let me look at this."},{"type":"tool_use","id":"toolu_100","name":"Bash","input":{"command":"ls"}}],"stop_reason":"tool_use","usage":{"input_tokens":50,"output_tokens":20}}}
|
||||
{"type":"user","uuid":"u-101","parentUuid":"a-100","timestamp":"2026-03-23T10:00:02.000Z","sessionId":"test-session-dedup","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"isMeta":true,"userType":"external","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_100","content":"main.rs\nlib.rs","is_error":false}]}}
|
||||
{"type":"assistant","uuid":"a-101","parentUuid":"u-101","timestamp":"2026-03-23T10:00:03.000Z","sessionId":"test-session-dedup","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"requestId":"req-101","message":{"model":"claude-opus-4-6","id":"msg-101","type":"message","role":"assistant","content":[{"type":"text","text":"Done."}],"stop_reason":"end_turn","usage":{"input_tokens":100,"output_tokens":5}}}
|
||||
@@ -0,0 +1,4 @@
|
||||
{"type":"user","uuid":"u-300","parentUuid":null,"timestamp":"2026-03-23T12:00:00.000Z","sessionId":"test-session-sub","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"isMeta":false,"message":{"role":"user","content":"Search the codebase"}}
|
||||
{"type":"assistant","uuid":"a-300","parentUuid":"u-300","timestamp":"2026-03-23T12:00:01.000Z","sessionId":"test-session-sub","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"requestId":"req-300","message":{"model":"claude-opus-4-6","id":"msg-300","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_300","name":"Agent","input":{"description":"Search codebase","subagent_type":"Explore","prompt":"Find all config files"}}],"stop_reason":"tool_use","usage":{"input_tokens":100,"output_tokens":20}}}
|
||||
{"type":"user","uuid":"u-301","parentUuid":"a-300","timestamp":"2026-03-23T12:00:30.000Z","sessionId":"test-session-sub","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"isMeta":true,"message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_300","content":"Found 3 config files","is_error":false}]}}
|
||||
{"type":"assistant","uuid":"a-301","parentUuid":"u-301","timestamp":"2026-03-23T12:00:31.000Z","sessionId":"test-session-sub","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"requestId":"req-301","message":{"model":"claude-opus-4-6","id":"msg-301","type":"message","role":"assistant","content":[{"type":"text","text":"I found the config files."}],"stop_reason":"end_turn","usage":{"input_tokens":200,"output_tokens":10}}}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
{"type":"user","uuid":"sa-u1","parentUuid":null,"timestamp":"2026-03-23T12:00:02.000Z","sessionId":"agent-abc123","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":true,"isMeta":false,"message":{"role":"user","content":"Find all config files"}}
|
||||
{"type":"assistant","uuid":"sa-a1","parentUuid":"sa-u1","timestamp":"2026-03-23T12:00:03.000Z","sessionId":"agent-abc123","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":true,"requestId":"req-sa1","message":{"model":"claude-opus-4-6","id":"msg-sa1","type":"message","role":"assistant","content":[{"type":"text","text":"Found config.toml, settings.json, .env"}],"stop_reason":"end_turn","usage":{"input_tokens":50,"output_tokens":15}}}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"agentType": "Explore"}
|
||||
@@ -0,0 +1,6 @@
|
||||
{"type":"user","uuid":"u-200","parentUuid":null,"timestamp":"2026-03-23T10:00:00.000Z","sessionId":"test-session-corr","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"isMeta":false,"userType":"external","message":{"role":"user","content":"Fix the bug"}}
|
||||
{"type":"assistant","uuid":"a-200","parentUuid":"u-200","timestamp":"2026-03-23T10:00:01.000Z","sessionId":"test-session-corr","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"requestId":"req-200","message":{"model":"claude-opus-4-6","id":"msg-200","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_200","name":"Bash","input":{"command":"cargo test"}},{"type":"tool_use","id":"toolu_201","name":"Read","input":{"file_path":"src/main.rs"}}],"stop_reason":"tool_use","usage":{"input_tokens":100,"output_tokens":50}}}
|
||||
{"type":"user","uuid":"u-201","parentUuid":"a-200","timestamp":"2026-03-23T10:00:02.000Z","sessionId":"test-session-corr","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"isMeta":true,"userType":"external","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_200","content":"test result output","is_error":false},{"type":"tool_result","tool_use_id":"toolu_201","content":"fn main() {}","is_error":false}]}}
|
||||
{"type":"assistant","uuid":"a-201","parentUuid":"u-201","timestamp":"2026-03-23T10:00:03.000Z","sessionId":"test-session-corr","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"requestId":"req-201","message":{"model":"claude-opus-4-6","id":"msg-201","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_202","name":"Write","input":{"file_path":"src/fix.rs","content":"fixed"}}],"stop_reason":"tool_use","usage":{"input_tokens":150,"output_tokens":30}}}
|
||||
{"type":"user","uuid":"u-202","parentUuid":"a-201","timestamp":"2026-03-23T10:00:04.000Z","sessionId":"test-session-corr","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"isMeta":true,"userType":"external","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_202","content":"File written successfully","is_error":false}]}}
|
||||
{"type":"assistant","uuid":"a-202","parentUuid":"u-202","timestamp":"2026-03-23T10:00:05.000Z","sessionId":"test-session-corr","cwd":"G:\\dev\\test","version":"2.1.71","gitBranch":"main","isSidechain":false,"requestId":"req-202","message":{"model":"claude-opus-4-6","id":"msg-202","type":"message","role":"assistant","content":[{"type":"text","text":"Bug is fixed."}],"stop_reason":"end_turn","usage":{"input_tokens":200,"output_tokens":20}}}
|
||||
@@ -0,0 +1,294 @@
|
||||
use camino::{Utf8Path, Utf8PathBuf};
|
||||
use chrono::Datelike;
|
||||
use dirigent_anth::{
|
||||
correlation::correlate_tools,
|
||||
dedup::dedup_messages,
|
||||
noise::{classify_noise, NoiseKind},
|
||||
parse_session,
|
||||
tree::ConversationTree,
|
||||
types::{ContentBlock, RawMessage},
|
||||
util::parse_timestamp,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn parse_minimal_session() {
|
||||
let path = Utf8Path::new("tests/fixtures/minimal_session.jsonl");
|
||||
let messages = parse_session(path).unwrap();
|
||||
|
||||
assert_eq!(messages.len(), 6, "Expected 6 messages, got {}", messages.len());
|
||||
|
||||
let type_names: Vec<&str> = messages
|
||||
.iter()
|
||||
.map(|m| match m {
|
||||
RawMessage::User(_) => "user",
|
||||
RawMessage::Assistant(_) => "assistant",
|
||||
RawMessage::Progress(_) => "progress",
|
||||
RawMessage::System(_) => "system",
|
||||
RawMessage::QueueOperation(_) => "queue-operation",
|
||||
RawMessage::FileHistorySnapshot(_) => "file-history-snapshot",
|
||||
RawMessage::LastPrompt(_) => "last-prompt",
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
type_names.iter().filter(|&&t| t == "queue-operation").count(),
|
||||
2
|
||||
);
|
||||
assert_eq!(type_names.iter().filter(|&&t| t == "user").count(), 2);
|
||||
assert_eq!(
|
||||
type_names.iter().filter(|&&t| t == "assistant").count(),
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_line_returns_none_for_invalid_json() {
|
||||
assert!(dirigent_anth::parse_line("not valid json", 1).is_none());
|
||||
assert!(dirigent_anth::parse_line("{}", 1).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dedup_streaming_session() {
|
||||
let path = Utf8Path::new("tests/fixtures/streaming_dedup.jsonl");
|
||||
let messages = parse_session(path).unwrap();
|
||||
|
||||
// Raw should have 6 lines (including 3 versions of same assistant message)
|
||||
assert_eq!(messages.len(), 6, "Raw messages: expected 6, got {}", messages.len());
|
||||
|
||||
let deduped = dedup_messages(messages);
|
||||
|
||||
// After dedup: U1, A1(final), U2, A2 = 4
|
||||
assert_eq!(deduped.len(), 4, "Deduped messages: expected 4, got {}", deduped.len());
|
||||
|
||||
// The kept assistant message must be the final version
|
||||
let first_assistant = deduped.iter().find(|m| matches!(m, RawMessage::Assistant(_))).unwrap();
|
||||
if let RawMessage::Assistant(a) = first_assistant {
|
||||
assert!(a.message.stop_reason.is_some(), "Deduped assistant should have stop_reason set");
|
||||
assert_eq!(a.message.stop_reason.as_deref(), Some("tool_use"));
|
||||
assert_eq!(a.message.content.len(), 2, "Final version should have 2 content blocks");
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dedup_preserves_non_streamed_messages() {
|
||||
let path = Utf8Path::new("tests/fixtures/minimal_session.jsonl");
|
||||
let messages = parse_session(path).unwrap();
|
||||
let count_before = messages.len();
|
||||
let deduped = dedup_messages(messages);
|
||||
// No streaming in minimal_session, so count should be same
|
||||
assert_eq!(deduped.len(), count_before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn correlate_parallel_tools() {
|
||||
let path = Utf8Path::new("tests/fixtures/tool_correlation.jsonl");
|
||||
let messages = dirigent_anth::parse_session_deduped(path).unwrap();
|
||||
let exchanges = correlate_tools(&messages);
|
||||
|
||||
// 3 tool calls: 2 parallel (Bash+Read) + 1 sequential (Write)
|
||||
assert_eq!(exchanges.len(), 3);
|
||||
|
||||
// All should have results
|
||||
assert!(exchanges.iter().all(|e| e.result.is_some()));
|
||||
|
||||
// Verify correct pairing by ID
|
||||
for ex in &exchanges {
|
||||
assert_eq!(ex.call.id, ex.result.as_ref().unwrap().tool_use_id);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn correlate_no_tools_returns_empty() {
|
||||
// Test with just a plain user message — no tool calls or results
|
||||
let messages = vec![
|
||||
serde_json::from_str::<RawMessage>(
|
||||
r#"{"type":"user","uuid":"x","timestamp":"2026-01-01T00:00:00Z","sessionId":"s","message":{"role":"user","content":"hello"}}"#,
|
||||
)
|
||||
.unwrap(),
|
||||
];
|
||||
let exchanges = correlate_tools(&messages);
|
||||
assert!(exchanges.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_branching_tree() {
|
||||
let path = Utf8Path::new("tests/fixtures/branching_tree.jsonl");
|
||||
let messages = dirigent_anth::parse_session(path).unwrap();
|
||||
let tree = ConversationTree::build(&messages);
|
||||
|
||||
assert_eq!(tree.roots.len(), 1);
|
||||
assert!(!tree.is_linear());
|
||||
assert_eq!(tree.branch_points().len(), 1); // A1 has 2 children
|
||||
|
||||
let main = tree.main_thread();
|
||||
assert_eq!(main.len(), 4); // R → A1 → U2 → A3 (first branch)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linear_conversation_is_linear() {
|
||||
let path = Utf8Path::new("tests/fixtures/minimal_session.jsonl");
|
||||
let messages = dirigent_anth::parse_session(path).unwrap();
|
||||
let tree = ConversationTree::build(&messages);
|
||||
assert!(tree.is_linear());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classify_noise_from_fixture() {
|
||||
let path = Utf8Path::new("tests/fixtures/noise_patterns.jsonl");
|
||||
let messages = dirigent_anth::parse_session(path).unwrap();
|
||||
|
||||
assert_eq!(messages.len(), 9, "Expected 9 messages in noise fixture");
|
||||
|
||||
let classifications: Vec<Option<NoiseKind>> = messages.iter()
|
||||
.map(classify_noise)
|
||||
.collect();
|
||||
|
||||
assert_eq!(classifications[0], Some(NoiseKind::QueueOp));
|
||||
assert_eq!(classifications[1], Some(NoiseKind::Meta));
|
||||
assert_eq!(classifications[2], Some(NoiseKind::Warmup));
|
||||
assert_eq!(classifications[3], Some(NoiseKind::Interrupted));
|
||||
assert_eq!(classifications[4], Some(NoiseKind::Continuation));
|
||||
assert_eq!(classifications[5], Some(NoiseKind::ApiError));
|
||||
assert_eq!(classifications[6], Some(NoiseKind::SystemCaveat));
|
||||
assert_eq!(classifications[7], None); // normal user
|
||||
assert_eq!(classifications[8], None); // normal assistant
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_subagent_from_fixture() {
|
||||
let artifacts_dir = Utf8Path::new("tests/fixtures/subagent/parent");
|
||||
let subagents = dirigent_anth::load_subagents(artifacts_dir).unwrap();
|
||||
|
||||
assert_eq!(subagents.len(), 1);
|
||||
assert_eq!(subagents[0].agent_id, "abc123");
|
||||
assert_eq!(subagents[0].meta.agent_type.as_deref(), Some("Explore"));
|
||||
assert_eq!(subagents[0].messages.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_subagents_empty_dir() {
|
||||
// Non-existent artifacts dir should return empty vec
|
||||
let artifacts_dir = Utf8Path::new("tests/fixtures/nonexistent");
|
||||
let subagents = dirigent_anth::load_subagents(artifacts_dir).unwrap();
|
||||
assert!(subagents.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_full_session_with_subagents() {
|
||||
use dirigent_anth::types::SessionRef;
|
||||
|
||||
let session_ref = SessionRef {
|
||||
id: "parent".to_string(),
|
||||
jsonl_path: Utf8PathBuf::from("tests/fixtures/subagent/parent.jsonl"),
|
||||
artifacts_dir: Some(Utf8PathBuf::from("tests/fixtures/subagent/parent")),
|
||||
index_entry: None,
|
||||
};
|
||||
|
||||
let session = dirigent_anth::load_session(&session_ref).unwrap();
|
||||
assert!(!session.messages.is_empty());
|
||||
assert!(!session.subagents.is_empty());
|
||||
assert!(!session.tree.roots.is_empty());
|
||||
assert!(!session.tool_exchanges.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_session_without_artifacts() {
|
||||
use dirigent_anth::types::SessionRef;
|
||||
|
||||
let session_ref = SessionRef {
|
||||
id: "minimal".to_string(),
|
||||
jsonl_path: Utf8PathBuf::from("tests/fixtures/minimal_session.jsonl"),
|
||||
artifacts_dir: None,
|
||||
index_entry: None,
|
||||
};
|
||||
|
||||
let session = dirigent_anth::load_session(&session_ref).unwrap();
|
||||
assert_eq!(session.messages.len(), 6); // 2 queue-ops + 2 users + 2 assistants
|
||||
assert!(session.subagents.is_empty());
|
||||
assert!(session.tree.is_linear());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn content_as_string_or_blocks() {
|
||||
// String content
|
||||
let s: dirigent_anth::types::Content = serde_json::from_str(r#""hello""#).unwrap();
|
||||
assert!(matches!(s, dirigent_anth::types::Content::Text(_)));
|
||||
|
||||
// Block content
|
||||
let b: dirigent_anth::types::Content =
|
||||
serde_json::from_str(r#"[{"type":"text","text":"hi"}]"#).unwrap();
|
||||
assert!(matches!(b, dirigent_anth::types::Content::Blocks(_)));
|
||||
|
||||
// Empty blocks
|
||||
let empty: dirigent_anth::types::Content = serde_json::from_str(r#"[]"#).unwrap();
|
||||
assert!(matches!(empty, dirigent_anth::types::Content::Blocks(ref v) if v.is_empty()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_optional_fields_dont_crash() {
|
||||
// Minimal assistant message with many fields missing
|
||||
let json = r#"{
|
||||
"type": "assistant",
|
||||
"message": {
|
||||
"content": [{"type": "text", "text": "hi"}]
|
||||
}
|
||||
}"#;
|
||||
let msg: RawMessage = serde_json::from_str(json).unwrap();
|
||||
assert!(matches!(msg, RawMessage::Assistant(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_result_content_string_and_blocks() {
|
||||
// tool_result with string content
|
||||
let json = r#"{"type":"tool_result","tool_use_id":"t1","content":"output text","is_error":false}"#;
|
||||
let block: ContentBlock = serde_json::from_str(json).unwrap();
|
||||
if let ContentBlock::ToolResult { content, is_error, .. } = block {
|
||||
assert!(!is_error);
|
||||
assert!(content.is_some());
|
||||
} else {
|
||||
panic!("Expected ToolResult");
|
||||
}
|
||||
|
||||
// tool_result with no content
|
||||
let json2 = r#"{"type":"tool_result","tool_use_id":"t2"}"#;
|
||||
let block2: ContentBlock = serde_json::from_str(json2).unwrap();
|
||||
if let ContentBlock::ToolResult { content, is_error, .. } = block2 {
|
||||
assert!(!is_error);
|
||||
assert!(content.is_none());
|
||||
} else {
|
||||
panic!("Expected ToolResult");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extra_unknown_fields_are_ignored() {
|
||||
// Messages with extra fields not in our structs should parse fine
|
||||
let json = r#"{
|
||||
"type": "user",
|
||||
"uuid": "x",
|
||||
"timestamp": "2026-01-01T00:00:00Z",
|
||||
"sessionId": "s",
|
||||
"unknownField": "should be ignored",
|
||||
"anotherExtra": 42,
|
||||
"message": {"role": "user", "content": "hello"}
|
||||
}"#;
|
||||
let msg: RawMessage = serde_json::from_str(json).unwrap();
|
||||
assert!(matches!(msg, RawMessage::User(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timestamp_parsing_all_formats() {
|
||||
// ISO 8601
|
||||
let iso = parse_timestamp(&serde_json::json!("2026-03-22T17:00:13.192Z")).unwrap();
|
||||
assert_eq!(iso.year(), 2026);
|
||||
|
||||
// Unix millis
|
||||
let ms = parse_timestamp(&serde_json::json!(1769461914249_i64)).unwrap();
|
||||
assert!(ms.year() >= 2025);
|
||||
|
||||
// Unix seconds
|
||||
let secs = parse_timestamp(&serde_json::json!(1769461914_i64)).unwrap();
|
||||
assert!(secs.year() >= 2025);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
use dirigent_anth::anth_usage::process_usage_screen;
|
||||
|
||||
const SAMPLE: &str = r#"
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Status Config Usage Stats
|
||||
|
||||
Session
|
||||
Total cost: $0.0000
|
||||
Total duration (API): 0s
|
||||
Total duration (wall): 4s
|
||||
Total code changes: 0 lines added, 0 lines removed
|
||||
Usage: 0 input, 0 output, 0 cache read, 0 cache write
|
||||
|
||||
Current session
|
||||
███████ 14% used
|
||||
Resets 12:30pm (Europe/Vienna)
|
||||
|
||||
Current week (all models)
|
||||
██████ 12% used
|
||||
Resets May 12, 9am (Europe/Vienna)
|
||||
|
||||
Current week (Sonnet only)
|
||||
0% used
|
||||
Resets May 12, 9am (Europe/Vienna)
|
||||
|
||||
What's contributing to your limits usage?
|
||||
Approximate, based on local sessions on this machine — does not include other devices or claude.ai
|
||||
|
||||
Last 24h · these are independent characteristics of your usage, not a breakdown
|
||||
|
||||
97% of your usage came from subagent-heavy sessions
|
||||
Each subagent runs its own requests. Be deliberate about spawning them — and
|
||||
consider configuring a cheaper model for simpler subagents.
|
||||
|
||||
16% of your usage was at >150k context
|
||||
Longer sessions are more expensive even when cached. /compact mid-task, /clear
|
||||
when switches to new tasks.
|
||||
|
||||
Subagents % of usage
|
||||
Explore 3%
|
||||
claude-code-guide 2%
|
||||
|
||||
d to day · w to week
|
||||
|
||||
Esc to cancel
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn parses_gauges() {
|
||||
let result = process_usage_screen(SAMPLE);
|
||||
assert_eq!(result.data.gauges.len(), 3);
|
||||
|
||||
assert_eq!(result.data.gauges[0].name, "Current session");
|
||||
assert_eq!(result.data.gauges[0].percent_used, 14);
|
||||
assert_eq!(
|
||||
result.data.gauges[0].resets.as_deref(),
|
||||
Some("12:30pm (Europe/Vienna)")
|
||||
);
|
||||
|
||||
assert_eq!(result.data.gauges[1].name, "Current week (all models)");
|
||||
assert_eq!(result.data.gauges[1].percent_used, 12);
|
||||
assert_eq!(
|
||||
result.data.gauges[1].resets.as_deref(),
|
||||
Some("May 12, 9am (Europe/Vienna)")
|
||||
);
|
||||
|
||||
assert_eq!(result.data.gauges[2].name, "Current week (Sonnet only)");
|
||||
assert_eq!(result.data.gauges[2].percent_used, 0);
|
||||
|
||||
// resets_iso should be present for all gauges with reset info
|
||||
assert!(result.data.gauges[0].resets_iso.is_some());
|
||||
assert!(result.data.gauges[1].resets_iso.is_some());
|
||||
assert!(result.data.gauges[2].resets_iso.is_some());
|
||||
|
||||
// Week resets should contain the right date
|
||||
let week_iso = result.data.gauges[1].resets_iso.as_ref().unwrap();
|
||||
assert!(week_iso.starts_with("2026-05-12") || week_iso.contains("05-12"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_contributions() {
|
||||
let result = process_usage_screen(SAMPLE);
|
||||
let contrib = result.data.contributions.as_ref().unwrap();
|
||||
|
||||
assert_eq!(contrib.factors.len(), 2);
|
||||
assert_eq!(contrib.factors[0].percent, 97);
|
||||
assert!(contrib.factors[0].description.contains("subagent-heavy"));
|
||||
assert_eq!(contrib.factors[1].percent, 16);
|
||||
|
||||
assert_eq!(contrib.subagents.len(), 2);
|
||||
assert_eq!(contrib.subagents[0].name, "Explore");
|
||||
assert_eq!(contrib.subagents[0].percent, 3);
|
||||
assert_eq!(contrib.subagents[1].name, "claude-code-guide");
|
||||
assert_eq!(contrib.subagents[1].percent, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_screen_starts_with_rule() {
|
||||
let result = process_usage_screen(SAMPLE);
|
||||
assert!(result.raw_screen.starts_with('─'));
|
||||
}
|
||||
Reference in New Issue
Block a user