#![allow(clippy::unwrap_used, clippy::expect_used)]
#![allow(unsafe_code)]
use std::path::Path;
use difflore_cli::hook::runtime as hook_runtime;
use sqlx::SqlitePool;
use sqlx::sqlite::SqliteConnectOptions;
fn seed_skip_cache(home: &Path, file_path: &str) {
let _ = home;
let project_root = difflore_core::infra::db::current_project_root();
let project_hash = difflore_core::infra::db::project_hash_from_root(&project_root);
let signal = "post-edit\n-let x = 1;\n+let x = 2;\n";
difflore_cli::hook::cache::remember_injection_for_project_hash_with_signal(
file_path,
"post-edit",
3,
&project_hash,
Some(signal),
);
}
fn post_tool_use_payload(file_path: &str) -> String {
serde_json::json!({
"session_id": "test-session",
"cwd": ".",
"hook_event_name": "PostToolUse",
"tool_name": "Edit",
"tool_input": {
"file_path": file_path,
"old_string": "let x = 1;\n",
"new_string": "let x = 2;\n"
},
"tool_response": {}
})
.to_string()
}
async fn count_cloud_outbox_rows(data_db: &Path) -> i64 {
if !data_db.exists() {
return 0;
}
let opts = SqliteConnectOptions::new()
.filename(data_db)
.create_if_missing(false);
let pool: SqlitePool = sqlx::sqlite::SqlitePoolOptions::new()
.max_connections(1)
.connect_with(opts)
.await
.expect("open data.db");
let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM cloud_outbox")
.fetch_one(&pool)
.await
.unwrap_or(0);
pool.close().await;
count
}
#[tokio::test]
async fn post_tool_use_dedup_skip_short_circuits_before_db_and_outbox() {
let home = tempfile::tempdir().expect("temp DIFFLORE_HOME");
unsafe {
std::env::set_var("DIFFLORE_HOME", home.path());
}
unsafe {
std::env::remove_var("DIFFLORE_CAPTURE");
}
unsafe {
std::env::set_var(
difflore_core::infra::env::DIFFLORE_DISABLE_OUTBOX_DAEMON,
"1",
);
}
let skip_file = "src/example_skip.rs";
seed_skip_cache(home.path(), skip_file);
let raw = post_tool_use_payload(skip_file);
let out = hook_runtime::output_for_raw("claude-code", &raw, false)
.await
.expect("hook output");
let json: serde_json::Value = serde_json::from_str(&out).expect("parseable output");
assert_eq!(
json.get("continue").and_then(serde_json::Value::as_bool),
Some(true),
"skip path must return a continue:true noop, got: {json}"
);
let data_db = home.path().join("data.db");
let obs_db = home.path().join("observations_outbox.db");
assert!(
!data_db.exists(),
"skip path must not init data.db (was {})",
data_db.display()
);
assert!(
!obs_db.exists(),
"skip path must not open observations_outbox.db (was {})",
obs_db.display()
);
let rows_after_skip = count_cloud_outbox_rows(&data_db).await;
assert_eq!(
rows_after_skip, 0,
"skip path must not enqueue any cloud_outbox row, found {rows_after_skip}"
);
let proceed_file = "src/example_proceed.rs";
let raw = post_tool_use_payload(proceed_file);
let out = hook_runtime::output_for_raw("claude-code", &raw, false)
.await
.expect("hook output");
let _: serde_json::Value = serde_json::from_str(&out).expect("parseable output");
assert!(
data_db.exists(),
"non-skip path must init data.db (was {})",
data_db.display()
);
let rows_after_proceed = count_cloud_outbox_rows(&data_db).await;
assert!(
rows_after_proceed >= 1,
"non-skip path must enqueue at least one cloud_outbox row, found {rows_after_proceed}"
);
unsafe {
std::env::remove_var("DIFFLORE_HOME");
std::env::remove_var(difflore_core::infra::env::DIFFLORE_DISABLE_OUTBOX_DAEMON);
}
drop(home);
}