//! T6 integration tests for [`ClaudeTranscriptInspector`] against a real temp //! directory and a real [`LocalFileSystem`], exercising the full transcript //! discovery + parsing path: //! //! - a realistic `~/.claude/projects//.jsonl` fixture → //! `last_topic` + `token_count` extracted; //! - a missing transcript → [`InspectError::NotFound`]; //! - malformed lines in the middle → skipped, valid lines still extracted; //! - [`SessionInspector::supports`] true for a `CLAUDE.md` profile, false for an //! `AGENTS.md` one or a non-`conventionFile` injection. use std::path::PathBuf; use std::sync::Arc; use domain::ids::ProfileId; use domain::ports::{FileSystem, InspectError, SessionInspector}; use domain::profile::{AgentProfile, ContextInjection}; use domain::project::ProjectPath; use infrastructure::{ClaudeTranscriptInspector, LocalFileSystem}; use uuid::Uuid; /// A unique scratch directory under the OS temp dir, cleaned up on drop. Plays /// the role of the user's `$HOME` so the inspector reads /// `/.claude/projects/...` entirely inside the fixture. struct TempHome(PathBuf); impl TempHome { fn new() -> Self { let p = std::env::temp_dir().join(format!("idea-t6-claude-{}", Uuid::new_v4())); std::fs::create_dir_all(&p).unwrap(); Self(p) } fn path(&self) -> String { self.0.to_string_lossy().into_owned() } /// Writes a transcript at `/.claude/projects//.jsonl`, /// mirroring the adapter's own cwd-encoding convention. fn write_transcript(&self, cwd: &str, conversation_id: &str, body: &str) { let encoded: String = cwd .chars() .map(|c| if c == '/' || c == '\\' { '-' } else { c }) .collect(); let dir = self.0.join(".claude").join("projects").join(&encoded); std::fs::create_dir_all(&dir).unwrap(); std::fs::write(dir.join(format!("{conversation_id}.jsonl")), body).unwrap(); } } impl Drop for TempHome { fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.0); } } fn inspector(home: &TempHome) -> ClaudeTranscriptInspector { let fs: Arc = Arc::new(LocalFileSystem::new()); ClaudeTranscriptInspector::with_base_dir(fs, home.path()) } fn claude_profile() -> AgentProfile { AgentProfile::new( ProfileId::from_uuid(Uuid::from_u128(1)), "Claude Code", "claude", Vec::new(), ContextInjection::convention_file("CLAUDE.md").unwrap(), Some("claude --version".to_owned()), "{agentRunDir}", None, ) .unwrap() } #[tokio::test] async fn extracts_last_topic_and_token_count_from_fixture() { let home = TempHome::new(); let cwd = "/home/dev/Projects/Demo"; let id = "11111111-2222-3333-4444-555555555555"; let body = concat!( r#"{"role":"user","content":"set up the project"}"#, "\n", r#"{"role":"assistant","content":"on it","usage":{"input_tokens":12,"output_tokens":8}}"#, "\n", r#"{"role":"user","content":"now add tests"}"#, "\n", r#"{"role":"assistant","content":"done","usage":{"input_tokens":30,"output_tokens":20}}"#, "\n", ); home.write_transcript(cwd, id, body); let insp = inspector(&home); let details = insp .details(&claude_profile(), id, &ProjectPath::new(cwd).unwrap()) .await .expect("transcript should be found and parsed"); assert_eq!(details.last_topic.as_deref(), Some("now add tests")); assert_eq!(details.token_count, Some(70)); } #[tokio::test] async fn missing_transcript_is_not_found() { let home = TempHome::new(); let insp = inspector(&home); let err = insp .details( &claude_profile(), "does-not-exist", &ProjectPath::new("/home/dev/nope").unwrap(), ) .await .expect_err("absent transcript must error"); assert_eq!(err, InspectError::NotFound); } #[tokio::test] async fn malformed_lines_are_skipped_not_fatal() { let home = TempHome::new(); let cwd = "/home/dev/Projects/Resilient"; let id = "aaaa"; let body = concat!( r#"{"role":"user","content":"valid first"}"#, "\n", "{ this is not valid json", "\n", "plain garbage line", "\n", r#"{"role":"assistant","usage":{"input_tokens":5,"output_tokens":6}}"#, "\n", r#"{"role":"user","content":"valid last"}"#, "\n", ); home.write_transcript(cwd, id, body); let insp = inspector(&home); let details = insp .details(&claude_profile(), id, &ProjectPath::new(cwd).unwrap()) .await .expect("readable transcript with bad lines should still parse"); assert_eq!(details.last_topic.as_deref(), Some("valid last")); assert_eq!(details.token_count, Some(11)); } #[test] fn supports_only_claude_md_profiles() { let fs: Arc = Arc::new(LocalFileSystem::new()); let insp = ClaudeTranscriptInspector::new(fs, "/tmp/whatever"); // CLAUDE.md → supported. assert!(insp.supports(&claude_profile())); // AGENTS.md (Codex) → not supported. let codex = AgentProfile::new( ProfileId::from_uuid(Uuid::from_u128(2)), "Codex", "codex", Vec::new(), ContextInjection::convention_file("AGENTS.md").unwrap(), None, "{agentRunDir}", None, ) .unwrap(); assert!(!insp.supports(&codex)); // Non-conventionFile injection (stdin) → not supported. let stdin_profile = AgentProfile::new( ProfileId::from_uuid(Uuid::from_u128(3)), "Piped", "aider", Vec::new(), ContextInjection::stdin(), None, "{agentRunDir}", None, ) .unwrap(); assert!(!insp.supports(&stdin_profile)); }