#![cfg(unix)]
use rusqlite::Connection;
use sessionwiki::index;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
fn archived(conn: &Connection, like: &str) -> Option<bool> {
conn.query_row(
"SELECT archived_at IS NOT NULL FROM files WHERE path LIKE ?1",
[format!("%{like}%")],
|r| r.get(0),
)
.ok()
}
fn chmod(path: &Path, mode: u32) {
fs::set_permissions(path, fs::Permissions::from_mode(mode)).unwrap();
}
#[test]
fn partial_walk_does_not_archive_live_sessions() {
if unsafe { libc_geteuid() } == 0 {
eprintln!("skipping: running as root");
return;
}
let home = std::env::temp_dir().join("sessionwiki-test-sync-guard");
let _ = fs::remove_dir_all(&home); let proj = home.join(".claude").join("projects").join("proj-a");
fs::create_dir_all(&proj).unwrap();
let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/claude-code/proj-a/0a000000-0000-4000-8000-000000000001.jsonl");
let session = proj.join("0a000000-0000-4000-8000-000000000001.jsonl");
fs::copy(&fixture, &session).unwrap();
std::env::set_var("HOME", &home);
std::env::set_var("SESSIONWIKI_DATA", home.join("data"));
let mut conn = index::open().unwrap();
index::sync(&mut conn, Some("claude-code")).unwrap();
assert_eq!(
archived(&conn, "0a000000"),
Some(false),
"session indexed live after the first sync"
);
chmod(&proj, 0o000);
let r = index::sync(&mut conn, Some("claude-code"));
chmod(&proj, 0o755); r.unwrap();
assert_eq!(
archived(&conn, "0a000000"),
Some(false),
"a partial walk must not archive live sessions"
);
index::sync(&mut conn, Some("claude-code")).unwrap();
assert_eq!(archived(&conn, "0a000000"), Some(false));
fs::remove_file(&session).unwrap();
index::sync(&mut conn, Some("claude-code")).unwrap();
assert_eq!(
archived(&conn, "0a000000"),
Some(true),
"real deletions still archive on a clean walk"
);
}
#[test]
fn parse_failures_are_warned_and_counted_not_swallowed() {
if unsafe { libc_geteuid() } == 0 {
eprintln!("skipping: running as root");
return;
}
let home = std::env::temp_dir().join("sessionwiki-test-sync-warn");
let _ = fs::remove_dir_all(&home);
let proj = home.join(".claude").join("projects").join("proj-a");
fs::create_dir_all(&proj).unwrap();
let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/claude-code/proj-a/0a000000-0000-4000-8000-000000000001.jsonl");
fs::copy(
&fixture,
proj.join("0a000000-0000-4000-8000-000000000001.jsonl"),
)
.unwrap();
let bad = proj.join("0b000000-0000-4000-8000-000000000002.jsonl");
fs::write(&bad, "{}").unwrap();
chmod(&bad, 0o000);
let out = std::process::Command::new(env!("CARGO_BIN_EXE_sessionwiki"))
.args(["sync", "--tool", "claude-code"])
.env("HOME", &home)
.env("SESSIONWIKI_DATA", home.join("data"))
.output()
.unwrap();
chmod(&bad, 0o644); let stderr = String::from_utf8_lossy(&out.stderr);
assert!(out.status.success(), "sync must not die: {stderr}");
assert!(
stderr.contains("failed to parse"),
"the failure must be warned: {stderr}"
);
assert!(
stderr.contains("(1 failed to parse)"),
"the count line must be honest: {stderr}"
);
assert!(
stderr.contains("indexed 1/2"),
"only the good session counts as indexed: {stderr}"
);
}
#[test]
fn a_partial_shared_store_walk_says_it_skipped_reconciliation() {
if unsafe { libc_geteuid() } == 0 {
eprintln!("skipping: running as root");
return;
}
let t = tempfile::tempdir().unwrap();
let data = t.path().join("data");
let roots = t.path().join("roots");
fs::create_dir_all(&data).unwrap();
let locked = roots.join("locked");
fs::create_dir_all(&locked).unwrap();
chmod(&locked, 0o000);
let out = std::process::Command::new(env!("CARGO_BIN_EXE_sessionwiki"))
.args(["sync", "--tool", "aider"])
.env("SESSIONWIKI_DATA", &data)
.env("SESSIONWIKI_AIDER_ROOTS", &roots)
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
chmod(&locked, 0o755);
assert!(
stderr.contains("skipping deletion reconciliation"),
"a partial shared-store walk must say so rather than skipping in silence:\n{stderr}"
);
}
#[test]
fn aider_capped_history_read_does_not_archive_existing_runs() {
let t = tempfile::tempdir().unwrap();
let data = t.path().join("data");
let roots = t.path().join("roots");
fs::create_dir_all(&roots).unwrap();
let history = roots.join(".aider.chat.history.md");
fs::write(
&history,
"# aider chat started at 2026-06-09 14:01:00\n#### keep this run\nkept\n",
)
.unwrap();
let sync = || {
std::process::Command::new(env!("CARGO_BIN_EXE_sessionwiki"))
.args(["sync", "--tool", "aider"])
.env("SESSIONWIKI_DATA", &data)
.env("SESSIONWIKI_AIDER_ROOTS", &roots)
.output()
.unwrap()
};
let first = sync();
assert!(first.status.success());
let conn = Connection::open(data.join("index.db")).unwrap();
assert_eq!(archived(&conn, "aider.chat.history"), Some(false));
let file = fs::OpenOptions::new().write(true).open(&history).unwrap();
file.set_len(sessionwiki::util::MAX_SESSION_FILE_BYTES + 1)
.unwrap();
let second = sync();
let stderr = String::from_utf8_lossy(&second.stderr);
assert!(second.status.success(), "sync failed: {stderr}");
assert!(
stderr.contains("skipping deletion reconciliation"),
"capped read must mark discovery incomplete: {stderr}"
);
assert_eq!(
archived(&conn, "aider.chat.history"),
Some(false),
"a read-size failure is not deletion"
);
}
extern "C" {
#[link_name = "geteuid"]
fn libc_geteuid() -> u32;
}