202 lines
6.9 KiB
Rust
202 lines
6.9 KiB
Rust
use serde_json::{Value, json};
|
|
use std::{
|
|
fs,
|
|
path::Path,
|
|
process::{Command, Output},
|
|
};
|
|
|
|
struct Fixture {
|
|
dir: tempfile::TempDir,
|
|
}
|
|
impl Fixture {
|
|
fn new() -> Self {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
for path in [
|
|
"pi",
|
|
"claude/projects/project",
|
|
"codex/sessions",
|
|
"work",
|
|
"other",
|
|
] {
|
|
fs::create_dir_all(dir.path().join(path)).unwrap();
|
|
}
|
|
let f = Self { dir };
|
|
let root = f.dir.path();
|
|
f.jsonl("pi/pi.jsonl", &[
|
|
json!({"type":"session", "id":"pi-id", "cwd":root.join("work")}),
|
|
json!({"type":"session_info", "name":"Database migration"}),
|
|
json!({"type":"message", "message":{"role":"user","content":"add postgres indexes"}}),
|
|
json!({"type":"message", "message":{"role":"assistant","content":"assistant-only-secret"}}),
|
|
]);
|
|
f.jsonl("claude/projects/project/claude.jsonl", &[
|
|
json!({"type":"user", "sessionId":"claude-id", "cwd":root.join("other"), "message":{"role":"user","content":"consider migration rollback"}}),
|
|
json!({"type":"ai-title", "aiTitle":"Different topic"}),
|
|
]);
|
|
f.jsonl("codex/sessions/codex.jsonl", &[
|
|
json!({"type":"session_meta", "payload":{"id":"codex-id", "cwd":root.join("work"), "source":"cli"}}),
|
|
json!({"type":"event_msg", "payload":{"type":"user_message", "message":"refactor rust"}}),
|
|
]);
|
|
f
|
|
}
|
|
fn jsonl(&self, path: &str, rows: &[Value]) {
|
|
fs::write(
|
|
self.dir.path().join(path),
|
|
rows.iter().map(|v| format!("{v}\n")).collect::<String>(),
|
|
)
|
|
.unwrap();
|
|
}
|
|
fn run(&self, args: &[&str]) -> Output {
|
|
Command::new(env!("CARGO_BIN_EXE_cont"))
|
|
.env("HOME", self.dir.path())
|
|
.env("XDG_DATA_HOME", self.dir.path().join("data"))
|
|
.env_remove("OPENCODE_DB")
|
|
.env("PI_CODING_AGENT_SESSION_DIR", self.dir.path().join("pi"))
|
|
.env("CLAUDE_CONFIG_DIR", self.dir.path().join("claude"))
|
|
.env("CODEX_HOME", self.dir.path().join("codex"))
|
|
.env("RESUME_CACHE_DIR", self.dir.path().join("cache"))
|
|
.current_dir(self.dir.path().join("work"))
|
|
.args(args)
|
|
.output()
|
|
.unwrap()
|
|
}
|
|
fn rows(&self, args: &[&str]) -> Vec<Value> {
|
|
let out = self.run(args);
|
|
assert!(
|
|
out.status.success(),
|
|
"{}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
serde_json::from_slice(&out.stdout).unwrap()
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn cli_scope_agent_and_title_first_ranking() {
|
|
let f = Fixture::new();
|
|
assert_eq!(f.rows(&["--json"]).len(), 2);
|
|
assert_eq!(f.rows(&["--json", "--all"]).len(), 3);
|
|
let rows = f.rows(&["--json", "--all", "migration"]);
|
|
assert_eq!(rows.len(), 2);
|
|
assert_eq!(rows[0]["agent"], "pi");
|
|
assert_eq!(rows[0]["match"], "title");
|
|
assert_eq!(rows[1]["agent"], "claude");
|
|
assert_eq!(rows[1]["match"], "message");
|
|
assert_eq!(
|
|
f.rows(&["--json", "--all", "--agent", "codex"])[0]["id"],
|
|
"codex-id"
|
|
);
|
|
assert!(
|
|
f.rows(&["--json", "--all", "assistant-only-secret"])
|
|
.is_empty()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn omp_discovery_and_custom_opencode_database() {
|
|
let f = Fixture::new();
|
|
let root = f.dir.path();
|
|
fs::create_dir_all(root.join(".omp/agent/sessions/project")).unwrap();
|
|
f.jsonl(
|
|
".omp/agent/sessions/project/session.jsonl",
|
|
&[
|
|
json!({"type":"title","title":"OMP task"}),
|
|
json!({"type":"session","id":"omp-id","cwd":root.join("work")}),
|
|
json!({"type":"message","message":{"role":"user","content":"unicorn request"}}),
|
|
],
|
|
);
|
|
let rows = f.rows(&["--json", "--agent", "omp", "unicorn"]);
|
|
assert_eq!(rows.len(), 1);
|
|
assert_eq!(rows[0]["title"], "OMP task");
|
|
assert_eq!(rows[0]["match"], "message");
|
|
assert!(
|
|
rows[0]["command"]
|
|
.as_str()
|
|
.unwrap()
|
|
.contains("omp '--resume'")
|
|
);
|
|
let database = root.join("custom.db");
|
|
let conn = rusqlite::Connection::open(&database).unwrap();
|
|
conn.execute_batch("CREATE TABLE session (id TEXT, directory TEXT, title TEXT, time_updated INTEGER, parent_id TEXT, time_archived INTEGER);
|
|
CREATE TABLE message (id TEXT, session_id TEXT, time_created INTEGER, data TEXT);
|
|
CREATE TABLE part (id TEXT, message_id TEXT, session_id TEXT, time_created INTEGER, data TEXT);").unwrap();
|
|
conn.execute(
|
|
"INSERT INTO session VALUES ('ses_test', ?1, 'OpenCode task', 1000, NULL, NULL)",
|
|
[root.join("other").to_str().unwrap()],
|
|
)
|
|
.unwrap();
|
|
let rows = f.rows(&[
|
|
"--json",
|
|
"--all",
|
|
"--agent",
|
|
"opencode",
|
|
"--opencode-db",
|
|
database.to_str().unwrap(),
|
|
]);
|
|
assert_eq!(rows.len(), 1);
|
|
assert_eq!(rows[0]["id"], "ses_test");
|
|
assert!(
|
|
rows[0]["command"]
|
|
.as_str()
|
|
.unwrap()
|
|
.contains("OPENCODE_DB=")
|
|
);
|
|
assert!(
|
|
f.rows(&["--json", "--all", "--agent", "opencode"])
|
|
.is_empty()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn additional_root_and_invalid_cache_are_safe() {
|
|
let f = Fixture::new();
|
|
let extra = f.dir.path().join("extra");
|
|
fs::create_dir(&extra).unwrap();
|
|
fs::write(
|
|
extra.join("extra.jsonl"),
|
|
format!(
|
|
"{}\n",
|
|
json!({"type":"session","id":"extra","cwd":f.dir.path().join("work")})
|
|
),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
f.rows(&["--json", "--all", "--pi-dir", extra.to_str().unwrap()])
|
|
.len(),
|
|
4
|
|
);
|
|
// Changing the configured roots must not leak old cached results into the final list.
|
|
assert_eq!(f.rows(&["--json", "--all"]).len(), 3);
|
|
fs::write(f.dir.path().join("cache/sessions-v1.json"), "bad cache").unwrap();
|
|
assert_eq!(f.rows(&["--json", "--all"]).len(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn non_tty_help_and_no_cache() {
|
|
let f = Fixture::new();
|
|
assert!(f.run(&["--help"]).status.success());
|
|
let out = f.run(&[]);
|
|
assert!(!out.status.success());
|
|
assert!(String::from_utf8_lossy(&out.stderr).contains("needs a terminal"));
|
|
assert_eq!(f.rows(&["--json", "--no-cache"]).len(), 2);
|
|
assert!(!f.dir.path().join("cache").exists());
|
|
assert!(f.rows(&["--json", "--limit", "0"]).is_empty());
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
let cache = f.dir.path().join("cache/sessions-v1.json");
|
|
assert_eq!(
|
|
fs::metadata(cache).unwrap().permissions().mode() & 0o777,
|
|
0o600
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(unix)]
|
|
fn symlinked_working_directory_matches() {
|
|
let f = Fixture::new();
|
|
let link = f.dir.path().join("alias");
|
|
std::os::unix::fs::symlink(f.dir.path().join("work"), &link).unwrap();
|
|
assert_eq!(f.rows(&["--json", "-C", link.to_str().unwrap()]).len(), 2);
|
|
assert!(Path::new(f.rows(&["--json"])[0]["cwd"].as_str().unwrap()).is_absolute());
|
|
}
|