nils-memo-cli 0.3.8

CLI crate for nils-memo-cli in the nils-cli workspace.
Documentation
use memo_cli::output::parse_item_id;
use pretty_assertions::assert_eq;
use serde_json::json;

mod support;

use support::{parse_json_stdout, run_memo_cli, test_db_path};

#[test]
fn update_and_delete_keep_layers_consistent() {
    let db_path = test_db_path("update_and_delete_keep_layers_consistent");

    let add_output = run_memo_cli(&db_path, &["--json", "add", "buy 1tb ssd for mom"], None);
    assert_eq!(
        add_output.code,
        0,
        "add failed: {}",
        add_output.stderr_text()
    );
    let add_json = parse_json_stdout(&add_output);
    let item_id_str = add_json["result"]["item_id"]
        .as_str()
        .expect("item_id should be a string");
    let item_id = parse_item_id(item_id_str).expect("item_id should parse");

    let apply_payload = json!({
        "agent_run_id": "agent-run-update-delete-flow",
        "items": [{
            "item_id": item_id_str,
            "derivation_hash": "hash-update-delete-flow-1",
            "summary": "buy ssd for mom",
            "category": "shopping",
            "normalized_text": "buy 1tb ssd for mom",
            "confidence": 0.93,
            "tags": ["family", "shopping"],
            "payload": {"source":"test"}
        }]
    });
    let apply_output = run_memo_cli(
        &db_path,
        &["--json", "apply", "--stdin"],
        Some(&apply_payload.to_string()),
    );
    assert_eq!(
        apply_output.code,
        0,
        "apply failed: {}",
        apply_output.stderr_text()
    );

    let conn = rusqlite::Connection::open(&db_path).expect("open db");
    conn.execute(
        "insert into workflow_item_anchors(item_id, workflow_type) values (?1, 'game')",
        rusqlite::params![item_id],
    )
    .expect("insert workflow anchor");
    let anchor_id: i64 = conn
        .query_row(
            "select anchor_id from workflow_item_anchors where item_id = ?1 and workflow_type = 'game'",
            rusqlite::params![item_id],
            |row| row.get(0),
        )
        .expect("lookup anchor id");
    conn.execute(
        "insert into workflow_game_entries(anchor_id, game_name, source_url, description)
         values(?1, 'elden ring', 'https://example.com', 'wishlist')",
        rusqlite::params![anchor_id],
    )
    .expect("insert game entry");
    drop(conn);

    let update_output = run_memo_cli(
        &db_path,
        &["--json", "update", item_id_str, "buy 2tb ssd for mom"],
        None,
    );
    assert_eq!(
        update_output.code,
        0,
        "update failed: {}",
        update_output.stderr_text()
    );
    let update_json = parse_json_stdout(&update_output);
    assert_eq!(update_json["schema_version"], "memo-cli.update.v1");
    assert_eq!(update_json["result"]["state"], "pending");

    let fetch_after_update = run_memo_cli(&db_path, &["--json", "fetch", "--limit", "20"], None);
    assert_eq!(
        fetch_after_update.code,
        0,
        "fetch after update failed: {}",
        fetch_after_update.stderr_text()
    );
    let fetch_after_update_json = parse_json_stdout(&fetch_after_update);
    let rows = fetch_after_update_json["results"]
        .as_array()
        .expect("results array should exist");
    assert_eq!(rows.len(), 1);
    assert_eq!(rows[0]["item_id"], item_id_str);

    let search_old = run_memo_cli(
        &db_path,
        &["--json", "search", "shopping", "--limit", "10"],
        None,
    );
    assert_eq!(
        search_old.code,
        0,
        "search after update failed: {}",
        search_old.stderr_text()
    );
    let search_old_json = parse_json_stdout(&search_old);
    let search_old_rows = search_old_json["results"]
        .as_array()
        .expect("search results should be array");
    assert!(
        search_old_rows.is_empty(),
        "old derived tags should be gone"
    );

    let conn = rusqlite::Connection::open(&db_path).expect("open db");
    let derivation_count: i64 = conn
        .query_row(
            "select count(*) from item_derivations where item_id = ?1",
            rusqlite::params![item_id],
            |row| row.get(0),
        )
        .expect("query derivation count");
    assert_eq!(derivation_count, 0);
    let anchor_count: i64 = conn
        .query_row(
            "select count(*) from workflow_item_anchors where item_id = ?1",
            rusqlite::params![item_id],
            |row| row.get(0),
        )
        .expect("query workflow anchor count");
    assert_eq!(anchor_count, 0);
    let game_entry_count: i64 = conn
        .query_row("select count(*) from workflow_game_entries", [], |row| {
            row.get(0)
        })
        .expect("query workflow game count");
    assert_eq!(game_entry_count, 0);
    drop(conn);

    let delete_without_hard = run_memo_cli(&db_path, &["delete", item_id_str], None);
    assert_eq!(delete_without_hard.code, 64);

    let delete_output = run_memo_cli(&db_path, &["--json", "delete", item_id_str, "--hard"], None);
    assert_eq!(
        delete_output.code,
        0,
        "delete failed: {}",
        delete_output.stderr_text()
    );
    let delete_json = parse_json_stdout(&delete_output);
    assert_eq!(delete_json["schema_version"], "memo-cli.delete.v1");
    assert_eq!(delete_json["result"]["deleted"], true);

    let list_after_delete = run_memo_cli(&db_path, &["--json", "list", "--limit", "20"], None);
    assert_eq!(
        list_after_delete.code,
        0,
        "list after delete failed: {}",
        list_after_delete.stderr_text()
    );
    let list_after_delete_json = parse_json_stdout(&list_after_delete);
    assert_eq!(
        list_after_delete_json["results"]
            .as_array()
            .expect("list results array")
            .len(),
        0
    );

    let conn = rusqlite::Connection::open(&db_path).expect("open db");
    let inbox_count: i64 = conn
        .query_row("select count(*) from inbox_items", [], |row| row.get(0))
        .expect("query inbox count");
    let search_doc_count: i64 = conn
        .query_row("select count(*) from item_search_documents", [], |row| {
            row.get(0)
        })
        .expect("query search docs count");
    assert_eq!(inbox_count, 0);
    assert_eq!(search_doc_count, 0);
}