Permet de recharger la conversation CLI précédente de chaque cellule à la
réouverture du projet, de façon universelle (indépendant du modèle/CLI).
- profil AgentRuntime: bloc déclaratif optionnel `session { assignFlag, resumeFlag }`
- LeafCell: `conversationId` (persistant, distinct du SessionId PTY) + `agentWasRunning`
- runtime: SessionPlan (None/Assign/Resume) + composition pure des args
- LaunchAgent: décide Assign vs Resume, génère l'UUID, remonte l'id assigné
(persistance par l'appelant via setCellConversation — découplage SRP)
- close: SnapshotRunningAgents fige `agentWasRunning` avant le kill-all
(statut clot/en cours universel, sans parsing CLI)
- SessionInspector: port optionnel best-effort + adapter ClaudeTranscriptInspector
- popup de reprise par cellule (statut + sujet/tokens si dispo), intercalée
avant le Resume auto, jamais sur le chemin reattach
fix(terminals): sérialise les écritures PTY (file FIFO par handle) — corrige
les caractères mélangés/accents dus au réordonnancement des invoke Tauri concurrents
fix(layout): l'opération `move` préservait mal les champs du leaf (perdait `agent`)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
179 lines
5.8 KiB
Rust
179 lines
5.8 KiB
Rust
//! 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/<encoded-cwd>/<id>.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
|
|
/// `<home>/.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 `<home>/.claude/projects/<encoded-cwd>/<id>.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<dyn FileSystem> = 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<dyn FileSystem> = 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));
|
|
}
|