Files
Dicklesworthstone 71aefb7e54 feat(connectors): current Kimi Code layout + Oh My Pi coverage via FAD 1a258873 (coding_agent_session_search-gh351-kimi-code-current-format-dcpuc)
Closes GH #351. franken-agent-detection moves f7f38440 → 1a258873, two
commits:

- f685a69: dual-layout Kimi connector. Modern `$KIMI_CODE_HOME`
  (default ~/.kimi-code) tree with sessions/<workDirKey>/<sessionId>/
  state.json and per-agent agents/<agentId>/wire.jsonl transcripts, the
  new event envelope (turn.prompt deduplicated against its
  context.append_message echo, content.part text/think with
  author="reasoning", tool.call with call IDs, tool.result folded onto
  the originating call), collision-free IDs (<sessionId>, sub-agents
  <sessionId>:<agentId>), and the registry probe roots that were the
  actual reason `cass diag` reported kimi "(not detected)". Legacy
  ~/.kimi behavior preserved verbatim. FAD suite: 999 passed.
- 1a25887: Oh My Pi (`omp`) surfacing in the pi_agent connector —
  .omp/agent probe roots (registry-driven detection previously reported
  pi_agent not-detected on omp-only machines) and sub-agent transcripts
  (<session-dir>/<AgentName>.jsonl, each a complete session document)
  now matched by the shared scan/discovery file filter.

cass side: pin bump across the dependency contract (Cargo.toml,
Cargo.lock, build.rs), README connector rows for both layouts and omp,
and an end-to-end modern-layout integration test through the re-export
(session-ID/workspace/title from state.json, prompt dedup, invocation
call IDs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 21:17:48 -04:00

314 lines
11 KiB
Rust

//! Conformance harness for the Kimi connector via CASS's FAD re-export.
use coding_agent_search::connectors::kimi::KimiConnector;
use coding_agent_search::connectors::{Connector, ScanContext, ScanRoot};
use std::fs::{self, OpenOptions};
use std::path::{Path, PathBuf};
use tempfile::TempDir;
fn kimi_storage(tmp: &TempDir) -> PathBuf {
let storage = tmp.path().join(".kimi/sessions");
fs::create_dir_all(&storage).unwrap();
storage
}
fn write_wire_file(
storage: &Path,
workspace_hash: &str,
session_id: &str,
bytes: &[u8],
) -> PathBuf {
let session_dir = storage.join(workspace_hash).join(session_id);
fs::create_dir_all(&session_dir).unwrap();
fs::write(
session_dir.join("state.json"),
r#"{"cwd":"/workspace/kimi-real-service"}"#,
)
.unwrap();
let wire_path = session_dir.join("wire.jsonl");
fs::write(&wire_path, bytes).unwrap();
wire_path
}
fn write_wire_file_without_state(
storage: &Path,
workspace_hash: &str,
session_id: &str,
bytes: &[u8],
) -> PathBuf {
let session_dir = storage.join(workspace_hash).join(session_id);
fs::create_dir_all(&session_dir).unwrap();
let wire_path = session_dir.join("wire.jsonl");
fs::write(&wire_path, bytes).unwrap();
wire_path
}
fn scan_storage(storage: &Path) -> Vec<coding_agent_search::connectors::NormalizedConversation> {
let connector = KimiConnector::new();
let ctx = ScanContext::with_roots(
PathBuf::new(),
vec![ScanRoot::local(storage.to_path_buf())],
None,
);
connector.scan(&ctx).expect("kimi scan should not panic")
}
#[test]
fn kimi_happy_path_preserves_wire_session_fields() {
let tmp = TempDir::new().unwrap();
let storage = kimi_storage(&tmp);
let wire = r#"{"type":"metadata","protocol_version":"1.3"}
{"timestamp":1772857971.158,"message":{"type":"TurnBegin","payload":{"role":"human","content":"Read the Kimi session format"}}}
{"timestamp":1772857972.250,"message":{"type":"ToolCall","payload":{"name":"Read","input":{"file_path":"/workspace/kimi-real-service/wire.jsonl"}}}}
{"timestamp":1772857973.500,"message":{"type":"ContentPart","payload":{"content":"Kimi uses wire.jsonl content parts."}}}
"#;
let wire_path = write_wire_file(
&storage,
"workspace-hash",
"session-kimi-1",
wire.as_bytes(),
);
let convs = scan_storage(&storage);
assert_eq!(convs.len(), 1);
let conv = &convs[0];
assert_eq!(conv.agent_slug, "kimi");
assert_eq!(conv.external_id.as_deref(), Some("session-kimi-1"));
assert_eq!(conv.title.as_deref(), Some("Read the Kimi session format"));
assert_eq!(
conv.workspace,
Some(PathBuf::from("/workspace/kimi-real-service"))
);
assert_eq!(conv.source_path, wire_path);
assert_eq!(conv.started_at, Some(1_772_857_971_158));
assert_eq!(conv.ended_at, Some(1_772_857_973_500));
assert_eq!(conv.metadata["sessionId"], "session-kimi-1");
assert_eq!(conv.messages.len(), 3);
assert_eq!(conv.messages[0].idx, 0);
assert_eq!(conv.messages[0].role, "user");
assert!(conv.messages[0].content.contains("Kimi session"));
assert_eq!(conv.messages[1].idx, 1);
assert_eq!(conv.messages[1].role, "assistant");
assert_eq!(conv.messages[1].invocations.len(), 1);
assert_eq!(conv.messages[1].invocations[0].name, "Read");
assert!(conv.messages[1].content.contains("[Tool: Read"));
assert_eq!(conv.messages[2].idx, 2);
assert_eq!(conv.messages[2].role, "assistant");
assert!(conv.messages[2].content.contains("wire.jsonl"));
}
#[test]
fn kimi_empty_wire_file_returns_empty_result() {
let tmp = TempDir::new().unwrap();
let storage = kimi_storage(&tmp);
write_wire_file(&storage, "hash", "empty-session", b"");
assert!(scan_storage(&storage).is_empty());
}
#[test]
fn kimi_truncated_tail_line_preserves_complete_messages() {
let tmp = TempDir::new().unwrap();
let storage = kimi_storage(&tmp);
let wire = r#"{"timestamp":1772857971.0,"message":{"type":"TurnBegin","payload":{"role":"human","content":"complete request before truncation"}}}
{"timestamp":1772857972.0,"message":{"type":"ContentPart","payload":{"content":"unterminated tail"}
"#;
write_wire_file(&storage, "hash", "truncated-session", wire.as_bytes());
let convs = scan_storage(&storage);
assert_eq!(convs.len(), 1);
let conv = &convs[0];
assert_eq!(conv.external_id.as_deref(), Some("truncated-session"));
assert_eq!(conv.messages.len(), 1);
assert!(conv.messages[0].content.contains("complete request"));
assert_eq!(conv.started_at, Some(1_772_857_971_000));
assert_eq!(conv.ended_at, Some(1_772_857_971_000));
}
#[test]
fn kimi_missing_state_json_keeps_session_without_workspace() {
let tmp = TempDir::new().unwrap();
let storage = kimi_storage(&tmp);
let wire = r#"{"timestamp":1772857971.0,"message":{"type":"TurnBegin","payload":{"role":"human","content":"state file is absent"}}}
"#;
let wire_path = write_wire_file_without_state(
&storage,
"workspace-hash",
"missing-state-session",
wire.as_bytes(),
);
let convs = scan_storage(&storage);
assert_eq!(convs.len(), 1);
let conv = &convs[0];
assert_eq!(conv.external_id.as_deref(), Some("missing-state-session"));
assert_eq!(conv.source_path, wire_path);
assert_eq!(conv.workspace, None);
assert_eq!(conv.messages.len(), 1);
}
#[test]
fn kimi_multiple_sessions_remain_isolated_and_sorted() {
let tmp = TempDir::new().unwrap();
let storage = kimi_storage(&tmp);
write_wire_file(
&storage,
"hash-b",
"session-b",
br#"{"timestamp":1772857972.0,"message":{"type":"TurnBegin","payload":{"role":"human","content":"second session content"}}}
"#,
);
write_wire_file(
&storage,
"hash-a",
"session-a",
br#"{"timestamp":1772857971.0,"message":{"type":"TurnBegin","payload":{"role":"human","content":"first session content"}}}
"#,
);
let convs = scan_storage(&storage);
assert_eq!(convs.len(), 2);
assert_eq!(convs[0].external_id.as_deref(), Some("session-a"));
assert_eq!(convs[1].external_id.as_deref(), Some("session-b"));
assert!(convs[0].messages[0].content.contains("first session"));
assert!(convs[1].messages[0].content.contains("second session"));
}
#[test]
fn kimi_ignores_non_wire_files_and_empty_session_dirs() {
let tmp = TempDir::new().unwrap();
let storage = kimi_storage(&tmp);
let session_dir = storage.join("hash").join("empty-session");
fs::create_dir_all(&session_dir).unwrap();
fs::write(
session_dir.join("context.jsonl"),
br#"{"message":{"type":"TurnBegin","payload":{"content":"not the wire file"}}}"#,
)
.unwrap();
fs::write(
session_dir.join("state.json"),
r#"{"cwd":"/workspace/ignored"}"#,
)
.unwrap();
assert!(scan_storage(&storage).is_empty());
}
#[test]
fn kimi_malformed_mid_file_is_skipped_without_panic() {
let tmp = TempDir::new().unwrap();
let storage = kimi_storage(&tmp);
let wire = r#"{"timestamp":1772857971.0,"message":{"type":"TurnBegin","payload":{"role":"human","content":"valid before corruption"}}}
{ this is not valid json
{"timestamp":1772857972.0,"message":{"type":"ContentPart","payload":{"content":"valid after corruption"}}}
"#;
write_wire_file(&storage, "hash", "malformed-session", wire.as_bytes());
let convs = scan_storage(&storage);
assert_eq!(convs.len(), 1);
assert_eq!(convs[0].messages.len(), 2);
assert!(convs[0].messages[0].content.contains("valid before"));
assert!(convs[0].messages[1].content.contains("valid after"));
}
#[test]
fn kimi_non_utf8_bytes_return_empty_result_without_panic() {
let tmp = TempDir::new().unwrap();
let storage = kimi_storage(&tmp);
write_wire_file(
&storage,
"hash",
"non-utf8-session",
&[0xff, 0xfe, 0xfd, b'\n', 0x80],
);
assert!(scan_storage(&storage).is_empty());
}
#[test]
fn kimi_oversized_sparse_wire_file_returns_empty_result_without_panic() {
let tmp = TempDir::new().unwrap();
let storage = kimi_storage(&tmp);
let session_dir = storage.join("hash").join("huge-session");
fs::create_dir_all(&session_dir).unwrap();
let wire_path = session_dir.join("wire.jsonl");
let file = OpenOptions::new()
.create_new(true)
.write(true)
.open(&wire_path)
.unwrap();
file.set_len(101 * 1024 * 1024).unwrap();
drop(file);
assert!(scan_storage(&storage).is_empty());
}
/// GH #351: the modern Kimi Code layout (`$KIMI_CODE_HOME`-style tree with
/// `sessions/<workDirKey>/<sessionId>/agents/<agentId>/wire.jsonl` and a
/// session-root `state.json`) must parse end-to-end through the cass
/// re-export: collision-free session IDs, state.json metadata, the
/// turn.prompt/context.append_message dedup rule, and tool invocations with
/// call IDs. Deep schema coverage lives in franken_agent_detection; this pins
/// the integration.
#[test]
fn kimi_modern_layout_parses_end_to_end() {
let tmp = TempDir::new().unwrap();
let storage = tmp.path().join(".kimi-code/sessions");
let session_dir = storage.join("wd_example").join("session-modern-1");
let wire_dir = session_dir.join("agents").join("main");
fs::create_dir_all(&wire_dir).unwrap();
fs::write(
session_dir.join("state.json"),
r#"{"title":"Modern session title","createdAt":"2026-01-01T00:00:00Z","updatedAt":"2026-01-01T00:01:00Z","workDir":"/path/to/project","agents":{"main":{"type":"main"}}}"#,
)
.unwrap();
fs::write(
wire_dir.join("wire.jsonl"),
concat!(
r#"{"type":"turn.prompt","input":[{"type":"text","text":"find the bug"}],"time":"2026-01-01T00:00:00Z"}"#,
"\n",
r#"{"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"find the bug"}]},"time":"2026-01-01T00:00:00Z"}"#,
"\n",
r#"{"type":"context.append_loop_event","event":{"type":"content.part","part":{"type":"text","text":"looking now"}},"time":"2026-01-01T00:00:02Z"}"#,
"\n",
r#"{"type":"context.append_loop_event","event":{"type":"tool.call","toolCallId":"call_1","name":"ReadFile","args":{"path":"src/lib.rs"}},"time":"2026-01-01T00:00:03Z"}"#,
"\n",
),
)
.unwrap();
let convs = scan_storage(&storage);
assert_eq!(convs.len(), 1, "one modern session expected");
let conv = &convs[0];
assert_eq!(
conv.external_id.as_deref(),
Some("session-modern-1"),
"main-agent external_id must be the session ID, not 'main'"
);
assert_eq!(
conv.workspace.as_deref(),
Some(Path::new("/path/to/project")),
"workspace must come from state.json workDir"
);
assert_eq!(conv.title.as_deref(), Some("Modern session title"));
let user_messages: Vec<_> = conv
.messages
.iter()
.filter(|message| message.role == "user")
.collect();
assert_eq!(
user_messages.len(),
1,
"turn.prompt and its context.append_message echo must dedupe to one user message"
);
let invocation = conv
.messages
.iter()
.flat_map(|message| message.invocations.iter())
.next()
.expect("tool.call must produce an invocation");
assert_eq!(invocation.name, "ReadFile");
assert_eq!(invocation.call_id.as_deref(), Some("call_1"));
}