Files
Dicklesworthstone 06fa30f081 feat(analytics,tui): replace stubs with honest implementations — headless asciicast sentinel, timeseries source-filter fallback, and doc-comment accuracy sweep
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>
2026-04-02 21:06:21 -04:00

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();
}