agentmem 0.1.1

Local-first memory engine for AI coding agents
Documentation
use agentmem::store::migration::STORE_FORMAT_VERSION;
use agentmem::store::persist::{
    empty_store_file, map_to_file, parse_store_file, serialize_store_file, StoreFile,
};
use agentmem::types::Entry;

#[test]
fn malformed_json_is_rejected() {
    let raw = "{not-json";
    assert!(parse_store_file(raw).is_err());
}

#[test]
fn truncated_json_is_rejected() {
    let raw = r#"{"version":1,"records":["#;
    assert!(parse_store_file(raw).is_err());
}

#[test]
fn trailing_garbage_after_json_is_rejected() {
    let raw = r#"{"version":1,"records":[]} trailing"#;
    assert!(parse_store_file(raw).is_err());
}

#[test]
fn missing_version_field_is_rejected() {
    let raw = r#"{"records":[]}"#;
    assert!(parse_store_file(raw).is_err());
}

#[test]
fn missing_records_field_is_rejected() {
    let raw = r#"{"version":1}"#;
    assert!(parse_store_file(raw).is_err());
}

#[test]
fn unsupported_version_is_rejected() {
    let raw = r#"{"version":9999,"records":[]}"#;
    assert!(parse_store_file(raw).is_err());
}

#[test]
fn invalid_key_is_rejected() {
    let raw = format!(
        r#"{{"version":{},"records":[{{"key":"bad key with space","value":"ok"}}]}}"#,
        STORE_FORMAT_VERSION
    );
    assert!(parse_store_file(&raw).is_err());
}

#[test]
fn invalid_value_with_nul_byte_is_rejected() {
    let raw = format!(
        "{{\"version\":{},\"records\":[{{\"key\":\"agent/codex/task\",\"value\":\"bad\\u0000value\"}}]}}",
        STORE_FORMAT_VERSION
    );
    assert!(parse_store_file(&raw).is_err());
}

#[test]
fn oversized_but_structurally_valid_value_is_rejected() {
    let huge = "a".repeat(70_000);
    let raw = format!(
        r#"{{"version":{},"records":[{{"key":"agent/codex/task","value":"{}"}}]}}"#,
        STORE_FORMAT_VERSION, huge
    );
    assert!(parse_store_file(&raw).is_err());
}

#[test]
fn empty_records_array_is_accepted() {
    let raw = format!(r#"{{"version":{},"records":[]}}"#, STORE_FORMAT_VERSION);
    let parsed = parse_store_file(&raw).expect("empty records should parse");
    assert_eq!(parsed.records.len(), 0);
}

#[test]
fn valid_store_with_multiple_records_roundtrips() {
    let mut file = empty_store_file();
    file.records.push(
        map_to_file(
            &vec![
                Entry::try_new("agent/codex/task", "build index").expect("entry"),
                Entry::try_new("agent/claude/task", "review docs").expect("entry"),
            ]
            .into_iter()
            .collect(),
        )
        .records
        .remove(0),
    );
    file.records.push(
        map_to_file(
            &vec![Entry::try_new("project/demo/root", "/tmp/demo").expect("entry")]
                .into_iter()
                .collect(),
        )
        .records
        .remove(0),
    );

    let raw = serialize_store_file(&file).expect("serialize");
    let reparsed = parse_store_file(&raw).expect("parse");
    assert_eq!(reparsed.version, STORE_FORMAT_VERSION);
    assert_eq!(reparsed.records.len(), 2);
}

#[test]
fn duplicate_keys_parse_but_remain_visible_at_file_level() {
    let raw = format!(
        r#"{{"version":{},"records":[{{"key":"agent/codex/task","value":"one"}},{{"key":"agent/codex/task","value":"two"}}]}}"#,
        STORE_FORMAT_VERSION
    );
    let parsed = parse_store_file(&raw).expect("parse should succeed");
    assert_eq!(parsed.records.len(), 2);
}

#[test]
fn weird_but_valid_unicode_value_is_accepted() {
    let raw = format!(
        r#"{{"version":{},"records":[{{"key":"agent/codex/task","value":"zażółć gęślą jaźń"}}]}}"#,
        STORE_FORMAT_VERSION
    );
    let parsed = parse_store_file(&raw).expect("parse");
    assert_eq!(parsed.records.len(), 1);
}

#[test]
fn empty_store_file_roundtrips() {
    let file = StoreFile {
        version: STORE_FORMAT_VERSION,
        records: Vec::new(),
    };

    let raw = serialize_store_file(&file).expect("serialize");
    let parsed = parse_store_file(&raw).expect("parse");
    assert_eq!(parsed, file);
}