mirror of
https://github.com/Dicklesworthstone/coding_agent_session_search.git
synced 2026-09-22 11:59:25 +08:00
06fa30f081
Truthfulness sweep replacing placeholder behavior and stale comments with real implementations across three feature areas: **Headless asciicast sentinel (lib.rs, tui tests):** Renamed write_headless_asciicast_stub to write_headless_asciicast_sentinel. The vacuous empty-frame output is replaced with a labeled sentinel cast carrying cass_artifact_kind, recording_available: false, and a human-readable explanation frame. Updated CLI help text for --asciicast to describe headless behavior. New unit tests assert sentinel is non-empty and properly labeled. **Analytics tokens timeseries source-filter fix (query.rs, tests):** Added query_track_a_timeseries_from_raw — a raw-message fallback that fires when filtering by remote/specific source and rollup tables lack reliable provenance (blank source_id with origin_host as fallback). Added track_a_timeseries_requires_source_fallback predicate to gate the new path. Updated render_analytics_docs to document messages and message_metrics as valid source_table values. New CLI integration test for the source-filter fallback path. **Stale comment/doc-comment cleanup (encryption.rs, lib.rs, tests):** Replaced "placeholder implementation" doc-comment on encrypt_content() with accurate production description. Removed "deferred to child beads / not yet implemented" comment on run_analytics dispatcher. Updated --from-file test error messages to no longer say "unimplemented". Added robot_docs_analytics_describes_safe_fix_mode test asserting docs don't claim --fix is unavailable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
524 lines
18 KiB
Rust
524 lines
18 KiB
Rust
//! TUI Integration Smoke Tests (bead z61x9)
|
|
//!
|
|
//! These tests verify the TUI works correctly with the fully integrated stack:
|
|
//! - frankensqlite (storage backend)
|
|
//! - frankensearch (search pipeline)
|
|
//! - franken_agent_detection (connector discovery)
|
|
//!
|
|
//! Test scenarios:
|
|
//! 1. Launch TUI with test index → verify initial render (no crash)
|
|
//! 2. Search query → verify pipeline executes without panic
|
|
//! 3. Apply agent filter → verify filtered search executes
|
|
//! 4. Switch search mode (lexical/semantic/hybrid) → verify no panic
|
|
//! 5. Verify footer stats (from frankensqlite)
|
|
//! 6. Verify asciicast recording with populated index
|
|
//! 7. Multi-agent integrated stack exercise
|
|
//!
|
|
//! NOTE: Some search queries that return results currently fail with frankensqlite
|
|
//! "OpenRead" errors during the result-loading phase. This is a known limitation
|
|
//! of the frankensqlite migration (the search index pipeline works, but loading
|
|
//! full conversation details from the DB fails for certain SQL patterns).
|
|
//! Tests assert no-panic rather than success for these paths.
|
|
//!
|
|
//! All tests use `--once` / `TUI_HEADLESS=1` or CLI search (--json) for
|
|
//! non-interactive execution.
|
|
|
|
use assert_cmd::Command;
|
|
use std::fs;
|
|
use std::path::Path;
|
|
use tempfile::TempDir;
|
|
|
|
// =============================================================================
|
|
// Helpers
|
|
// =============================================================================
|
|
|
|
/// Create a base command with isolated environment for testing.
|
|
fn base_cmd(temp_home: &Path) -> Command {
|
|
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("cass"));
|
|
cmd.env("CODING_AGENT_SEARCH_NO_UPDATE_PROMPT", "1");
|
|
cmd.env("TUI_HEADLESS", "1");
|
|
cmd.env("HOME", temp_home);
|
|
cmd.env("XDG_DATA_HOME", temp_home.join(".local/share"));
|
|
cmd.env("XDG_CONFIG_HOME", temp_home.join(".config"));
|
|
cmd.env("RUST_LOG", "info,coding_agent_search=debug");
|
|
cmd
|
|
}
|
|
|
|
/// Create a Codex fixture with searchable content.
|
|
fn make_codex_fixture(root: &Path) {
|
|
let sessions = root.join("sessions/2025/11/21");
|
|
fs::create_dir_all(&sessions).unwrap();
|
|
let file = sessions.join("rollout-1.jsonl");
|
|
let sample = r#"{"role":"user","timestamp":1700000000000,"content":"hello world test"}
|
|
{"role":"assistant","timestamp":1700000001000,"content":"hi there from codex"}
|
|
{"role":"user","timestamp":1700000002000,"content":"fix the authentication bug in login.rs"}
|
|
{"role":"assistant","timestamp":1700000003000,"content":"I found the authentication issue in the login module. The session token was not being refreshed correctly."}
|
|
"#;
|
|
fs::write(file, sample).unwrap();
|
|
}
|
|
|
|
/// Create a Claude Code fixture with searchable content.
|
|
fn make_claude_fixture(root: &Path) {
|
|
let session_dir = root.join("projects/testproject");
|
|
fs::create_dir_all(&session_dir).unwrap();
|
|
let file = session_dir.join("session.jsonl");
|
|
let sample = r#"{"type":"user","timestamp":"2025-01-15T10:00:00Z","message":{"content":"refactor the database migration code"}}
|
|
{"type":"assistant","timestamp":"2025-01-15T10:00:05Z","message":{"content":"I'll restructure the database migration to use a proper migration framework with versioned schemas."}}
|
|
{"type":"user","timestamp":"2025-01-15T10:00:10Z","message":{"content":"add connection pooling"}}
|
|
{"type":"assistant","timestamp":"2025-01-15T10:00:15Z","message":{"content":"Added connection pooling with configurable min/max pool size and idle timeout."}}
|
|
"#;
|
|
fs::write(file, sample).unwrap();
|
|
}
|
|
|
|
/// Build the full index for the given data directory.
|
|
fn build_full_index(temp_home: &Path, data_dir: &Path, codex_home: &Path) {
|
|
let mut cmd = base_cmd(temp_home);
|
|
cmd.env("CODEX_HOME", codex_home);
|
|
cmd.args(["index", "--full", "--data-dir", data_dir.to_str().unwrap()]);
|
|
cmd.assert().success();
|
|
}
|
|
|
|
/// Assert that a search command completes without panicking.
|
|
/// Tolerates known frankensqlite "OpenRead" errors during migration.
|
|
/// Returns (success: bool, stdout: String, stderr: String).
|
|
fn assert_search_no_panic(output: &std::process::Output) -> (bool, String, String) {
|
|
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
|
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
|
|
|
// Must not panic regardless of frankensqlite support
|
|
assert!(
|
|
!stderr.contains("panicked") && !stderr.contains("RUST_BACKTRACE"),
|
|
"search command panicked!\nstderr: {}",
|
|
&stderr[..stderr.len().min(2000)]
|
|
);
|
|
|
|
(output.status.success(), stdout, stderr)
|
|
}
|
|
|
|
/// Set up a temp dir with Codex fixtures and a full index.
|
|
fn setup_codex_env() -> (TempDir, std::path::PathBuf) {
|
|
let tmp = TempDir::new().unwrap();
|
|
let data_dir = tmp.path().join("data");
|
|
fs::create_dir_all(&data_dir).unwrap();
|
|
make_codex_fixture(&data_dir);
|
|
build_full_index(tmp.path(), &data_dir, &data_dir);
|
|
(tmp, data_dir)
|
|
}
|
|
|
|
/// Set up a temp dir with both Codex and Claude fixtures and a full index.
|
|
fn setup_multi_agent_env() -> (TempDir, std::path::PathBuf) {
|
|
let tmp = TempDir::new().unwrap();
|
|
let data_dir = tmp.path().join("data");
|
|
let claude_home = tmp.path().join(".claude");
|
|
fs::create_dir_all(&data_dir).unwrap();
|
|
fs::create_dir_all(&claude_home).unwrap();
|
|
|
|
make_codex_fixture(&data_dir);
|
|
make_claude_fixture(&claude_home);
|
|
|
|
let mut cmd = base_cmd(tmp.path());
|
|
cmd.env("CODEX_HOME", &data_dir);
|
|
cmd.env("CLAUDE_HOME", &claude_home);
|
|
cmd.args(["index", "--full", "--data-dir", data_dir.to_str().unwrap()]);
|
|
cmd.assert().success();
|
|
|
|
(tmp, data_dir)
|
|
}
|
|
|
|
// =============================================================================
|
|
// 1. Launch TUI with test index → verify initial render (no crash)
|
|
// =============================================================================
|
|
|
|
#[test]
|
|
fn integration_tui_launches_with_populated_index() {
|
|
let (tmp, data_dir) = setup_codex_env();
|
|
|
|
let mut cmd = base_cmd(tmp.path());
|
|
cmd.args(["tui", "--once", "--data-dir", data_dir.to_str().unwrap()]);
|
|
cmd.assert().success();
|
|
|
|
assert!(
|
|
data_dir.join("agent_search.db").exists(),
|
|
"frankensqlite DB should exist"
|
|
);
|
|
assert!(
|
|
data_dir.join("index").exists(),
|
|
"frankensearch index dir should exist"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn integration_tui_ftui_runtime_with_populated_index() {
|
|
let (tmp, data_dir) = setup_codex_env();
|
|
|
|
let mut cmd = base_cmd(tmp.path());
|
|
cmd.env("CASS_TUI_RUNTIME", "ftui");
|
|
cmd.args(["tui", "--once", "--data-dir", data_dir.to_str().unwrap()]);
|
|
cmd.assert().success();
|
|
}
|
|
|
|
// =============================================================================
|
|
// 2. Search pipeline exercises (frankensearch + frankensqlite)
|
|
// =============================================================================
|
|
|
|
#[test]
|
|
fn integration_search_pipeline_no_panic() {
|
|
let (tmp, data_dir) = setup_codex_env();
|
|
|
|
// Query that triggers result-loading from DB. May fail with frankensqlite
|
|
// OpenRead error, but must never panic.
|
|
let output = base_cmd(tmp.path())
|
|
.env("CODEX_HOME", &data_dir)
|
|
.args([
|
|
"search",
|
|
"authentication",
|
|
"--json",
|
|
"--data-dir",
|
|
data_dir.to_str().unwrap(),
|
|
])
|
|
.output()
|
|
.expect("search command should execute");
|
|
|
|
let (success, stdout, _stderr) = assert_search_no_panic(&output);
|
|
if success {
|
|
assert!(
|
|
stdout.contains("authentication") || stdout.contains("hits"),
|
|
"successful search should contain result data"
|
|
);
|
|
}
|
|
// If !success, it's the known frankensqlite OpenRead limitation - acceptable
|
|
}
|
|
|
|
#[test]
|
|
fn integration_search_lexical_mode() {
|
|
let (tmp, data_dir) = setup_codex_env();
|
|
|
|
// "hello world" returns 0 hits from index → exercises pipeline without
|
|
// triggering the frankensqlite result-loading path
|
|
let output = base_cmd(tmp.path())
|
|
.env("CODEX_HOME", &data_dir)
|
|
.args([
|
|
"search",
|
|
"hello world",
|
|
"--json",
|
|
"--mode",
|
|
"lexical",
|
|
"--data-dir",
|
|
data_dir.to_str().unwrap(),
|
|
])
|
|
.output()
|
|
.expect("lexical search should execute");
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"lexical search should succeed: {}",
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
// The JSON output should contain query metadata even with 0 results
|
|
assert!(
|
|
stdout.contains("query") || stdout.contains("hello"),
|
|
"lexical search JSON should contain query metadata"
|
|
);
|
|
}
|
|
|
|
// =============================================================================
|
|
// 3. Agent filter → verify filtered search executes
|
|
// =============================================================================
|
|
|
|
#[test]
|
|
fn integration_search_agent_filter_no_panic() {
|
|
let (tmp, data_dir) = setup_multi_agent_env();
|
|
|
|
// Search with codex agent filter - exercises the filter pipeline
|
|
let codex_output = base_cmd(tmp.path())
|
|
.env("CODEX_HOME", &data_dir)
|
|
.env("CLAUDE_HOME", tmp.path().join(".claude"))
|
|
.args([
|
|
"search",
|
|
"authentication",
|
|
"--json",
|
|
"--agent",
|
|
"codex",
|
|
"--data-dir",
|
|
data_dir.to_str().unwrap(),
|
|
])
|
|
.output()
|
|
.expect("filtered search should execute");
|
|
|
|
assert_search_no_panic(&codex_output);
|
|
|
|
// Search with claude agent filter
|
|
let claude_output = base_cmd(tmp.path())
|
|
.env("CODEX_HOME", &data_dir)
|
|
.env("CLAUDE_HOME", tmp.path().join(".claude"))
|
|
.args([
|
|
"search",
|
|
"database migration",
|
|
"--json",
|
|
"--agent",
|
|
"claude_code",
|
|
"--data-dir",
|
|
data_dir.to_str().unwrap(),
|
|
])
|
|
.output()
|
|
.expect("claude-filtered search should execute");
|
|
|
|
assert_search_no_panic(&claude_output);
|
|
}
|
|
|
|
// =============================================================================
|
|
// 4. Search mode switching → verify no panic
|
|
// =============================================================================
|
|
|
|
#[test]
|
|
fn integration_search_mode_switching_no_panic() {
|
|
let (tmp, data_dir) = setup_codex_env();
|
|
|
|
for mode in ["lexical", "semantic", "hybrid"] {
|
|
let output = base_cmd(tmp.path())
|
|
.env("CODEX_HOME", &data_dir)
|
|
.args([
|
|
"search",
|
|
"session token",
|
|
"--json",
|
|
"--mode",
|
|
mode,
|
|
"--data-dir",
|
|
data_dir.to_str().unwrap(),
|
|
])
|
|
.output()
|
|
.unwrap_or_else(|_| panic!("{mode} mode search should execute"));
|
|
|
|
let (_success, _stdout, stderr) = assert_search_no_panic(&output);
|
|
|
|
// For semantic/hybrid: graceful degradation if no embeddings
|
|
if mode != "lexical" {
|
|
assert!(
|
|
!stderr.contains("panicked"),
|
|
"{mode} mode should not panic: {}",
|
|
&stderr[..stderr.len().min(500)]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// 5. Footer stats from frankensqlite
|
|
// =============================================================================
|
|
|
|
#[test]
|
|
fn integration_stats_from_frankensqlite() {
|
|
let (tmp, data_dir) = setup_codex_env();
|
|
|
|
let output = base_cmd(tmp.path())
|
|
.args(["stats", "--json", "--data-dir", data_dir.to_str().unwrap()])
|
|
.output()
|
|
.expect("stats command should execute");
|
|
|
|
assert!(output.status.success(), "stats should succeed");
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
assert!(
|
|
stdout.contains("sessions") || stdout.contains("messages") || stdout.contains("total"),
|
|
"stats should contain session/message counts: {}",
|
|
&stdout[..stdout.len().min(500)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn integration_diag_reports_stack_health() {
|
|
let (tmp, data_dir) = setup_codex_env();
|
|
|
|
let output = base_cmd(tmp.path())
|
|
.args(["diag", "--json", "--data-dir", data_dir.to_str().unwrap()])
|
|
.output()
|
|
.expect("diag command should execute");
|
|
|
|
assert!(output.status.success(), "diag should succeed");
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
assert!(
|
|
stdout.contains("data_dir") || stdout.contains("index") || stdout.contains("{"),
|
|
"diag should report stack health"
|
|
);
|
|
}
|
|
|
|
// =============================================================================
|
|
// 6. Asciicast recording with populated index
|
|
// =============================================================================
|
|
|
|
#[test]
|
|
fn integration_asciicast_records_with_data() {
|
|
let (tmp, data_dir) = setup_codex_env();
|
|
let cast_path = tmp.path().join("captures").join("integration.cast");
|
|
|
|
let mut cmd = base_cmd(tmp.path());
|
|
cmd.args([
|
|
"tui",
|
|
"--once",
|
|
"--data-dir",
|
|
data_dir.to_str().unwrap(),
|
|
"--asciicast",
|
|
cast_path.to_str().unwrap(),
|
|
]);
|
|
cmd.assert().success();
|
|
|
|
assert!(cast_path.exists(), "asciicast file should be created");
|
|
let cast = fs::read_to_string(&cast_path).expect("read asciicast");
|
|
assert!(
|
|
cast.contains("\"version\":2"),
|
|
"asciicast should be v2 format"
|
|
);
|
|
assert!(
|
|
cast.contains("\"cass_artifact_kind\":\"headless_once_asciicast_sentinel\""),
|
|
"non-interactive headless --once should emit a labeled sentinel cast"
|
|
);
|
|
assert!(
|
|
cast.contains("sentinel artifact, not a real terminal session recording"),
|
|
"sentinel cast should explain why no live recording exists"
|
|
);
|
|
}
|
|
|
|
// =============================================================================
|
|
// 7. Multi-agent integrated stack
|
|
// =============================================================================
|
|
|
|
#[test]
|
|
fn integration_multi_agent_index_and_tui() {
|
|
let (tmp, data_dir) = setup_multi_agent_env();
|
|
|
|
let mut cmd = base_cmd(tmp.path());
|
|
cmd.env("CODEX_HOME", &data_dir);
|
|
cmd.env("CLAUDE_HOME", tmp.path().join(".claude"));
|
|
cmd.args(["tui", "--once", "--data-dir", data_dir.to_str().unwrap()]);
|
|
cmd.assert().success();
|
|
}
|
|
|
|
#[test]
|
|
fn integration_multi_agent_search_no_panic() {
|
|
let (tmp, data_dir) = setup_multi_agent_env();
|
|
|
|
// Search for Codex-specific content
|
|
let codex_hit = base_cmd(tmp.path())
|
|
.env("CODEX_HOME", &data_dir)
|
|
.env("CLAUDE_HOME", tmp.path().join(".claude"))
|
|
.args([
|
|
"search",
|
|
"hello world",
|
|
"--json",
|
|
"--data-dir",
|
|
data_dir.to_str().unwrap(),
|
|
])
|
|
.output()
|
|
.expect("cross-search codex content");
|
|
|
|
assert_search_no_panic(&codex_hit);
|
|
|
|
// Search for Claude-specific content
|
|
let claude_hit = base_cmd(tmp.path())
|
|
.env("CODEX_HOME", &data_dir)
|
|
.env("CLAUDE_HOME", tmp.path().join(".claude"))
|
|
.args([
|
|
"search",
|
|
"database migration",
|
|
"--json",
|
|
"--data-dir",
|
|
data_dir.to_str().unwrap(),
|
|
])
|
|
.output()
|
|
.expect("cross-search claude content");
|
|
|
|
assert_search_no_panic(&claude_hit);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Edge cases: TUI with reset-state
|
|
// =============================================================================
|
|
|
|
#[test]
|
|
fn integration_tui_reset_state_with_data() {
|
|
let (tmp, data_dir) = setup_codex_env();
|
|
|
|
base_cmd(tmp.path())
|
|
.args(["tui", "--once", "--data-dir", data_dir.to_str().unwrap()])
|
|
.assert()
|
|
.success();
|
|
|
|
base_cmd(tmp.path())
|
|
.args([
|
|
"tui",
|
|
"--once",
|
|
"--reset-state",
|
|
"--data-dir",
|
|
data_dir.to_str().unwrap(),
|
|
])
|
|
.assert()
|
|
.success();
|
|
}
|
|
|
|
// =============================================================================
|
|
// Performance: integrated stack should be reasonably fast
|
|
// =============================================================================
|
|
|
|
#[test]
|
|
fn integration_search_completes_quickly() {
|
|
let (tmp, data_dir) = setup_codex_env();
|
|
|
|
let start = std::time::Instant::now();
|
|
|
|
let output = base_cmd(tmp.path())
|
|
.env("CODEX_HOME", &data_dir)
|
|
.args([
|
|
"search",
|
|
"authentication",
|
|
"--json",
|
|
"--data-dir",
|
|
data_dir.to_str().unwrap(),
|
|
])
|
|
.output()
|
|
.expect("timed search");
|
|
|
|
let elapsed = start.elapsed();
|
|
|
|
// Must not panic
|
|
assert_search_no_panic(&output);
|
|
// Whether it succeeds or fails with frankensqlite, should complete quickly
|
|
assert!(
|
|
elapsed.as_secs() < 10,
|
|
"integrated search took too long: {:?}",
|
|
elapsed
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn integration_tui_launch_completes_quickly() {
|
|
let (tmp, data_dir) = setup_codex_env();
|
|
|
|
let start = std::time::Instant::now();
|
|
|
|
base_cmd(tmp.path())
|
|
.args(["tui", "--once", "--data-dir", data_dir.to_str().unwrap()])
|
|
.assert()
|
|
.success();
|
|
|
|
let elapsed = start.elapsed();
|
|
assert!(
|
|
elapsed.as_secs() < 10,
|
|
"TUI launch with integrated stack took too long: {:?}",
|
|
elapsed
|
|
);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Health check validates the integrated stack
|
|
// =============================================================================
|
|
|
|
#[test]
|
|
fn integration_health_check_passes() {
|
|
let (tmp, data_dir) = setup_codex_env();
|
|
|
|
base_cmd(tmp.path())
|
|
.args(["health", "--data-dir", data_dir.to_str().unwrap()])
|
|
.assert()
|
|
.success();
|
|
}
|