walrus-daemon 0.0.10

Walrus agent runtime with memory, tools, and local inference
Documentation
//! Integration tests for the memory system using MemStorage (no disk I/O).

use std::path::PathBuf;
use walrus_daemon::hook::system::{
    MemoryConfig,
    memory::{Memory, storage::MemStorage},
};

fn test_memory() -> Memory {
    Memory::open(
        PathBuf::from("/test/memory"),
        MemoryConfig::default(),
        Box::new(MemStorage::new()),
    )
}

#[test]
fn remember_and_recall() {
    let mem = test_memory();

    mem.remember(
        "luna-vet".to_owned(),
        "User's dog Luna has vet appointments on Thursdays".to_owned(),
        "Luna is a golden retriever. Vet is Dr. Chen.".to_owned(),
    );

    let result = mem.recall("luna vet", 5);
    assert!(result.contains("luna-vet"), "should find luna-vet entry");
    assert!(result.contains("Dr. Chen"), "should contain entry content");
}

#[test]
fn recall_ranks_by_relevance() {
    let mem = test_memory();

    mem.remember(
        "weather".to_owned(),
        "User prefers sunny weather".to_owned(),
        "Likes to go outside when sunny.".to_owned(),
    );
    mem.remember(
        "rust-project".to_owned(),
        "User works on a Rust project called Walrus".to_owned(),
        "Walrus is an AI companion daemon written in Rust.".to_owned(),
    );
    mem.remember(
        "cooking".to_owned(),
        "User enjoys cooking Italian food".to_owned(),
        "Favorite dish is carbonara.".to_owned(),
    );

    let result = mem.recall("rust walrus", 5);
    // rust-project should appear first since it matches "rust" and "walrus"
    assert!(
        result.starts_with("## rust-project"),
        "rust-project should rank first, got: {result}"
    );
}

#[test]
fn forget_removes_entry() {
    let mem = test_memory();

    mem.remember(
        "temp-note".to_owned(),
        "Temporary note".to_owned(),
        "Should be deleted.".to_owned(),
    );

    let result = mem.recall("temporary", 5);
    assert!(result.contains("temp-note"));

    let result = mem.forget("temp-note");
    assert!(result.contains("forgot"));

    let result = mem.recall("temporary", 5);
    assert_eq!(result, "no memories found");
}

#[test]
fn forget_nonexistent_returns_error() {
    let mem = test_memory();
    let result = mem.forget("does-not-exist");
    assert!(result.contains("no entry named"));
}

#[test]
fn write_index_and_build_prompt() {
    let mem = test_memory();

    mem.write_index("# My Overview\n\nI know about Luna the dog.");

    let prompt = mem.build_prompt();
    assert!(prompt.contains("<memory>"));
    assert!(prompt.contains("Luna the dog"));
    assert!(prompt.contains("</memory>"));
}

#[test]
fn build_prompt_empty_index() {
    let mem = test_memory();
    let prompt = mem.build_prompt();
    // Should still include the memory prompt instructions, just no <memory> block content
    assert!(!prompt.contains("<memory>\n\n</memory>"));
}

#[test]
fn remember_updates_existing() {
    let mem = test_memory();

    mem.remember(
        "user-pref".to_owned(),
        "User preference".to_owned(),
        "Likes terse responses.".to_owned(),
    );
    mem.remember(
        "user-pref".to_owned(),
        "User preference updated".to_owned(),
        "Likes detailed responses now.".to_owned(),
    );

    let result = mem.recall("preference", 5);
    assert!(result.contains("detailed responses"));
    assert!(!result.contains("terse responses"));
}

#[test]
fn recall_empty_memory() {
    let mem = test_memory();
    let result = mem.recall("anything", 5);
    assert_eq!(result, "no memories found");
}

#[test]
fn recall_respects_limit() {
    let mem = test_memory();

    for i in 0..10 {
        mem.remember(
            format!("note-{i}"),
            format!("Note number {i} about testing"),
            format!("Content for test note {i}."),
        );
    }

    let result = mem.recall("testing note", 3);
    let entries: Vec<&str> = result.split("\n---\n").collect();
    assert!(
        entries.len() <= 3,
        "should return at most 3 entries, got {}",
        entries.len()
    );
}

#[test]
fn migration_converts_legacy_files() {
    let storage = MemStorage::new();
    let dir = PathBuf::from("/test/memory");

    // Seed legacy files
    storage
        .write(
            &dir.join("memory.md"),
            "Luna is a golden retriever\n\nUser works on Walrus",
        )
        .unwrap();
    storage
        .write(&dir.join("user.md"), "Name: Alice\nRole: Developer")
        .unwrap();
    storage
        .write(
            &dir.join("facts.toml"),
            "dog_name = \"Luna\"\nlanguage = \"Rust\"",
        )
        .unwrap();

    let mem = Memory::open(dir.clone(), MemoryConfig::default(), Box::new(storage));

    // Entries should exist
    let result = mem.recall("golden retriever", 5);
    assert!(result.contains("golden retriever"));

    let result = mem.recall("Alice", 5);
    assert!(result.contains("Alice"));

    let result = mem.recall("Luna", 5);
    assert!(result.contains("Luna"));
}

#[test]
fn after_compact_saves_session() {
    let storage = MemStorage::new();
    let dir = PathBuf::from("/test/memory");

    let mem = Memory::open(dir.clone(), MemoryConfig::default(), Box::new(storage));

    mem.after_compact("walrus", "Session summary: discussed Rust patterns");

    // Session file should exist in sessions/
    // (We can't easily check MemStorage contents without exposing it,
    // but at least verify it doesn't panic)
}

#[test]
fn slugify_examples() {
    use walrus_daemon::hook::system::memory::entry::slugify;

    assert_eq!(slugify("Luna's Vet Appointment!"), "luna-s-vet-appointment");
    assert_eq!(slugify("hello world"), "hello-world");
    assert_eq!(slugify("---dashes---"), "dashes");
    assert_eq!(slugify("CamelCase"), "camelcase");
    assert_eq!(slugify(""), "entry");
    assert_eq!(slugify("!!!"), "entry");
}

#[test]
fn entry_parse_roundtrip() {
    use walrus_daemon::hook::system::memory::entry::MemoryEntry;

    let entry = MemoryEntry::new(
        "test-entry".to_owned(),
        "A test entry for round-trip".to_owned(),
        "Some content here.".to_owned(),
        &PathBuf::from("/test/entries"),
    );

    let serialized = entry.serialize();
    let parsed = MemoryEntry::parse(entry.path.clone(), &serialized).unwrap();

    assert_eq!(parsed.name, "test-entry");
    assert_eq!(parsed.description, "A test entry for round-trip");
    assert_eq!(parsed.content, "Some content here.");
}

#[test]
fn bm25_tokenize() {
    use walrus_daemon::hook::system::memory::bm25::tokenize;

    let tokens = tokenize("Hello, World! This is a test.");
    assert!(tokens.contains(&"hello".to_owned()));
    assert!(tokens.contains(&"world".to_owned()));
    assert!(tokens.contains(&"test".to_owned()));
    // Stopwords filtered
    assert!(!tokens.contains(&"this".to_owned()));
    assert!(!tokens.contains(&"is".to_owned()));
    assert!(!tokens.contains(&"a".to_owned()));
}

#[test]
fn bm25_score_ranks() {
    use walrus_daemon::hook::system::memory::bm25::score;

    let docs = vec![
        (0, "the weather is sunny today"),
        (1, "rust programming language systems"),
        (2, "rust compiler and cargo build tool"),
    ];

    let results = score(&docs, "rust programming", 5);
    assert!(!results.is_empty());
    // Doc 1 should rank first (matches both "rust" and "programming")
    assert_eq!(results[0].0, 1);
}

use walrus_daemon::hook::system::memory::storage::Storage;

#[test]
fn soul_defaults_to_compiled_in() {
    let mem = test_memory();
    let soul = mem.build_soul();
    assert!(soul.contains("You are Walrus"));
}

#[test]
fn write_soul_updates_identity() {
    let mem = test_memory();
    mem.write_soul("You are a custom assistant.");
    assert_eq!(mem.build_soul(), "You are a custom assistant.");
}

#[test]
fn write_soul_denied_when_disabled() {
    let config = MemoryConfig {
        soul_editable: false,
        ..Default::default()
    };
    let mem = Memory::open(
        PathBuf::from("/test/memory"),
        config,
        Box::new(MemStorage::new()),
    );
    let result = mem.write_soul("hacked identity");
    assert!(result.contains("disabled"));
    // Soul should still be the default
    assert!(mem.build_soul().contains("You are Walrus"));
}