remem-ai 0.5.205

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use anyhow::Result;
use serde_json::json;

use super::*;
use crate::rules::test_support::{package_manager_rule, test_dir};
use crate::rules::{write_artifact_atomic, CompiledRulesArtifact, RuleAction, RuleOverrideState};

fn hook_input(project: &Path, session_id: &str, command: &str) -> String {
    json!({
        "session_id": session_id,
        "cwd": project,
        "hook_event_name": "PreToolUse",
        "tool_name": "Bash",
        "tool_input": {"command": command}
    })
    .to_string()
}

fn write_rule(data_dir: &Path, project: &Path, action: RuleAction) -> Result<()> {
    let project = crate::db::project_from_cwd(&project.to_string_lossy());
    let mut rule = package_manager_rule(action);
    rule.override_state = RuleOverrideState {
        disabled: false,
        action_override: (action == RuleAction::Block).then_some(RuleAction::Block),
    };
    let artifact = CompiledRulesArtifact::new(123, vec![rule]);
    write_artifact_atomic(artifact_path_for_project(data_dir, &project), &artifact)
}

#[test]
fn claude_warn_is_visible_without_overriding_host_permissions() -> Result<()> {
    let data_dir = test_dir("hook-warn-data");
    let project = test_dir("hook-warn-project");
    std::fs::create_dir_all(&project)?;
    write_rule(&data_dir, &project, RuleAction::Warn)?;

    let evaluated = evaluate_pre_tool_use(
        &hook_input(&project, "session-warn", "npm install"),
        Some("claude-code"),
        &data_dir,
        true,
    )?;
    let output = evaluated.output.context("warning output")?;

    assert!(evaluated.diagnostics.is_empty());
    assert!(output["systemMessage"].as_str().is_some_and(|message| {
        message.contains("warning") && message.contains("source memory(s) 123")
    }));
    assert_eq!(output["hookSpecificOutput"]["hookEventName"], "PreToolUse");
    assert!(output["hookSpecificOutput"]
        .get("permissionDecision")
        .is_none());
    Ok(())
}

#[test]
fn claude_block_denies_before_command_execution() -> Result<()> {
    let data_dir = test_dir("hook-block-data");
    let project = test_dir("hook-block-project");
    std::fs::create_dir_all(&project)?;
    write_rule(&data_dir, &project, RuleAction::Block)?;

    let evaluated = evaluate_pre_tool_use(
        &hook_input(&project, "session-block", "npm install"),
        Some("claude-code"),
        &data_dir,
        true,
    )?;
    let output = evaluated.output.context("block output")?;

    assert_eq!(output["hookSpecificOutput"]["permissionDecision"], "deny");
    assert!(output["hookSpecificOutput"]["permissionDecisionReason"]
        .as_str()
        .is_some_and(|message| message.contains("blocked")));
    Ok(())
}

#[test]
fn missing_artifact_fails_open_with_diagnostic() -> Result<()> {
    let data_dir = test_dir("hook-missing-data");
    let project = test_dir("hook-missing-project");
    std::fs::create_dir_all(&project)?;

    let evaluated = evaluate_pre_tool_use(
        &hook_input(&project, "session-missing", "npm install"),
        Some("claude-code"),
        &data_dir,
        true,
    )?;

    assert!(evaluated.output.is_none());
    assert_eq!(evaluated.diagnostics.len(), 1);
    assert!(evaluated.diagnostics[0].contains("artifact missing"));
    Ok(())
}

#[test]
fn unsupported_host_does_not_claim_command_enforcement() {
    let error = evaluate_pre_tool_use("{}", Some("codex-cli"), Path::new("/tmp"), true)
        .expect_err("Codex has no pre-execution Bash enforcement hook");
    assert!(error
        .to_string()
        .contains("unsupported for host 'codex-cli'"));
}

#[test]
fn disabled_rollout_ignores_existing_artifact() -> Result<()> {
    let data_dir = test_dir("hook-disabled-data");
    let project = test_dir("hook-disabled-project");
    std::fs::create_dir_all(&project)?;
    write_rule(&data_dir, &project, RuleAction::Block)?;

    let evaluated = evaluate_pre_tool_use(
        &hook_input(&project, "session-disabled", "npm install"),
        Some("claude-code"),
        &data_dir,
        false,
    )?;

    assert!(evaluated.output.is_none());
    assert!(evaluated.diagnostics.is_empty());
    Ok(())
}

#[test]
fn evaluation_error_marker_is_hashed_and_once_per_session() -> Result<()> {
    let data_dir = test_dir("hook-diagnostic-dedup");
    log_evaluation_error_once(&data_dir, Some("../../private-session"), "first error");
    log_evaluation_error_once(&data_dir, Some("../../private-session"), "second error");

    let marker_dir = data_dir
        .join("compiled_rules")
        .join(".evaluation-error-sessions");
    let markers = std::fs::read_dir(marker_dir)?.collect::<std::io::Result<Vec<_>>>()?;
    assert_eq!(markers.len(), 1);
    let name = markers[0].file_name().to_string_lossy().to_string();
    assert_eq!(name.len(), 64);
    assert!(!name.contains("private-session"));
    Ok(())
}

#[test]
fn evaluation_error_without_session_is_not_globally_suppressed() {
    let data_dir = test_dir("hook-diagnostic-no-session");

    log_evaluation_error_once(&data_dir, None, "malformed input");

    assert!(!data_dir
        .join("compiled_rules")
        .join(".evaluation-error-sessions")
        .exists());
}