use std::fmt::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
use agentty::db::{DB_DIR, DB_FILE, Database};
use assert_cmd::cargo::cargo_bin;
fn default_output_dir() -> PathBuf {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
manifest_dir.join("../../docs/site/static/features")
}
fn feature_output_dir() -> PathBuf {
let path = std::env::var("FEATURE_OUTPUT").map_or_else(|_| default_output_dir(), PathBuf::from);
std::fs::create_dir_all(&path).expect("failed to create feature output dir");
path.canonicalize()
.expect("failed to canonicalize output dir")
}
fn build_tape(
output_path: &Path,
binary_path: &Path,
agentty_root: &Path,
workdir: &Path,
steps: &str,
) -> String {
let mut tape = String::new();
let _ = writeln!(tape, "Set Shell \"bash\"");
let _ = writeln!(tape, "Set FontSize 36");
let _ = writeln!(tape, "Set Width 3200");
let _ = writeln!(tape, "Set Height 1600");
let _ = writeln!(tape, "Set Padding 0");
let _ = writeln!(tape, "Set Framerate 30");
let _ = writeln!(tape, "Set TypingSpeed 0");
let _ = writeln!(tape, "Set Theme \"OneDark\"");
let _ = writeln!(tape);
let _ = writeln!(tape, "Output \"{}\"", output_path.display());
let _ = writeln!(tape);
let _ = writeln!(tape, "Hide");
let _ = writeln!(
tape,
"Type \"export AGENTTY_ROOT='{}'\"",
agentty_root.display()
);
let _ = writeln!(tape, "Enter");
let _ = writeln!(tape, "Sleep 200ms");
let _ = writeln!(tape, "Type \"export HOME='{}'\"", agentty_root.display());
let _ = writeln!(tape, "Enter");
let _ = writeln!(tape, "Sleep 200ms");
let _ = writeln!(tape, "Type \"cd '{}'\"", workdir.display());
let _ = writeln!(tape, "Enter");
let _ = writeln!(tape, "Sleep 200ms");
let _ = writeln!(tape, "Type \"clear\"");
let _ = writeln!(tape, "Enter");
let _ = writeln!(tape, "Sleep 200ms");
let _ = writeln!(tape, "Show");
let _ = writeln!(tape);
let _ = writeln!(tape, "Type \"'{}'\"", binary_path.display());
let _ = writeln!(tape, "Enter");
let _ = writeln!(tape);
let _ = write!(tape, "{steps}");
let _ = writeln!(tape);
let _ = writeln!(tape, "Hide");
let _ = writeln!(tape, "Type \"q\"");
let _ = writeln!(tape, "Sleep 1s");
tape
}
fn execute_tape(tape_content: &str, name: &str, output_path: &Path) {
let tape_path = std::env::temp_dir().join(format!("{name}.tape"));
std::fs::write(&tape_path, tape_content).expect("failed to write tape file");
println!("VHS tape written to {}", tape_path.display());
let output = Command::new("vhs")
.arg(&tape_path)
.output()
.expect("failed to run vhs — is it installed? (brew install vhs)");
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
panic!("VHS execution failed: {stderr}");
}
assert!(
output_path.exists(),
"VHS did not produce output at {}",
output_path.display()
);
println!("Feature GIF saved to {}", output_path.display());
}
async fn seed_database(agentty_root: &Path, workdir: &Path) {
let db_path = agentty_root.join(DB_DIR).join(DB_FILE);
let database = Database::open(&db_path)
.await
.expect("failed to open database");
let project_id = database
.upsert_project(&workdir.to_string_lossy(), Some("main"))
.await
.expect("failed to upsert project");
database
.touch_project_last_opened(project_id)
.await
.expect("failed to touch project");
let sessions = [
(
"a1b2c3d4-0001",
"claude-opus-4-6",
"Review",
"Fix auth token refresh on session expiry",
"M",
),
(
"a1b2c3d4-0002",
"claude-sonnet-4-6",
"InProgress",
"Add dark mode toggle to settings page",
"L",
),
(
"a1b2c3d4-0003",
"gemini-3-flash-preview",
"Review",
"Optimize database query performance",
"S",
),
(
"a1b2c3d4-0004",
"claude-opus-4-6",
"Question",
"Refactor API error handling",
"M",
),
(
"a1b2c3d4-0005",
"claude-sonnet-4-6",
"Done",
"Update README with new install steps",
"XS",
),
(
"a1b2c3d4-0006",
"gemini-3-flash-preview",
"Done",
"Add unit tests for auth middleware",
"S",
),
(
"a1b2c3d4-0007",
"claude-opus-4-6",
"Queued",
"Migrate config to TOML format",
"L",
),
];
let wt_dir = agentty_root.join("wt");
for (session_id, model, status, title, size) in &sessions {
database
.insert_session(session_id, model, "main", status, project_id)
.await
.expect("failed to insert session");
database
.update_session_title(session_id, title)
.await
.expect("failed to update session title");
database
.update_session_diff_stats(10, 5, session_id, size)
.await
.expect("failed to update session diff stats");
let session_wt = wt_dir.join(&session_id[..8]);
std::fs::create_dir_all(&session_wt).expect("failed to create session worktree dir");
}
}
async fn setup_feature_env(temp: &tempfile::TempDir) -> (PathBuf, PathBuf) {
let agentty_root = temp.path().join("agentty_root");
let workdir = temp.path().join("my-project");
std::fs::create_dir_all(&agentty_root).expect("failed to create agentty root");
std::fs::create_dir_all(&workdir).expect("failed to create workdir");
let agentty_root = agentty_root
.canonicalize()
.expect("failed to canonicalize agentty root");
let workdir = workdir
.canonicalize()
.expect("failed to canonicalize workdir");
seed_database(&agentty_root, &workdir).await;
(agentty_root, workdir)
}
#[tokio::test]
#[ignore = "requires VHS and a renderable terminal — run explicitly with --ignored"]
async fn feature_sessions_tab() {
let temp = tempfile::TempDir::new().expect("failed to create temp dir");
let (agentty_root, workdir) = setup_feature_env(&temp).await;
let binary_path = cargo_bin("agentty");
let gif_path = feature_output_dir().join("sessions.gif");
let steps = "\
Sleep 2s
Tab
Sleep 1s
Sleep 2s
";
let tape = build_tape(&gif_path, &binary_path, &agentty_root, &workdir, steps);
execute_tape(&tape, "feature_sessions", &gif_path);
}
#[tokio::test]
#[ignore = "requires VHS and a renderable terminal — run explicitly with --ignored"]
async fn feature_tab_tour() {
let temp = tempfile::TempDir::new().expect("failed to create temp dir");
let (agentty_root, workdir) = setup_feature_env(&temp).await;
let binary_path = cargo_bin("agentty");
let gif_path = feature_output_dir().join("tab_tour.gif");
let steps = "\
Sleep 2s
Tab
Sleep 2s
Tab
Sleep 2s
Tab
Sleep 2s
";
let tape = build_tape(&gif_path, &binary_path, &agentty_root, &workdir, steps);
execute_tape(&tape, "feature_tab_tour", &gif_path);
}