spool-memory 0.2.3

Local-first developer memory system — persistent, structured knowledge for AI coding tools
Documentation
use assert_cmd::cargo::cargo_bin;
use serde_json::{Value, json};
use std::fs;
use std::io::{BufRead, BufReader, Write};
use std::process::{Command, Stdio};
use tempfile::tempdir;

#[test]
fn daemon_should_match_direct_lifecycle_read_behavior() {
    let temp = tempdir().unwrap();
    let config_path = temp.path().join("spool.toml");
    fs::write(&config_path, "[vault]\nroot = \"/tmp\"\n").unwrap();

    let create = Command::new(cargo_bin("spool"))
        .args([
            "memory",
            "record-manual",
            "--config",
            config_path.to_str().unwrap(),
            "--title",
            "简洁输出",
            "--summary",
            "偏好简洁",
            "--memory-type",
            "preference",
            "--scope",
            "user",
            "--source-ref",
            "manual:cli",
            "--user-id",
            "long",
        ])
        .output()
        .unwrap();
    assert!(create.status.success());
    let stdout = String::from_utf8(create.stdout).unwrap();
    let record_id = stdout
        .lines()
        .find(|line| line.contains("record_id"))
        .unwrap()
        .split('`')
        .nth(1)
        .unwrap()
        .to_string();

    let mut child = Command::new(cargo_bin("spool-daemon"))
        .args(["--config", config_path.to_str().unwrap()])
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()
        .unwrap();

    let mut stdin = child.stdin.take().unwrap();
    let stdout = child.stdout.take().unwrap();
    let mut reader = BufReader::new(stdout);

    write_message(&mut stdin, json!({ "command": "workbench" }));
    let workbench = read_message(&mut reader);
    assert_eq!(workbench["ok"], json!(true));
    assert_eq!(workbench["wakeup_ready"].as_array().unwrap().len(), 1);

    write_message(
        &mut stdin,
        json!({ "command": "record", "record_id": record_id }),
    );
    let record = read_message(&mut reader);
    assert_eq!(record["ok"], json!(true));
    assert_eq!(record["record"]["record"]["state"], json!("accepted"));

    drop(stdin);
    let status = child.wait().unwrap();
    assert!(status.success());
}

fn write_message(stdin: &mut std::process::ChildStdin, value: Value) {
    serde_json::to_writer(&mut *stdin, &value).unwrap();
    stdin.write_all(b"\n").unwrap();
    stdin.flush().unwrap();
}

#[test]
fn daemon_should_return_structured_error_for_malformed_json_and_continue() {
    let temp = tempdir().unwrap();
    let config_path = temp.path().join("spool.toml");
    fs::write(&config_path, "[vault]\nroot = \"/tmp\"\n").unwrap();

    let mut child = Command::new(cargo_bin("spool-daemon"))
        .args(["--config", config_path.to_str().unwrap()])
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()
        .unwrap();

    let mut stdin = child.stdin.take().unwrap();
    let stdout = child.stdout.take().unwrap();
    let mut reader = BufReader::new(stdout);

    stdin.write_all(b"{not json}\n").unwrap();
    stdin.flush().unwrap();
    let invalid = read_message(&mut reader);
    assert_eq!(invalid["ok"], json!(false));
    assert!(invalid["error"].as_str().unwrap().contains("invalid json"));

    write_message(&mut stdin, json!({ "command": "ping" }));
    let ping = read_message(&mut reader);
    assert_eq!(ping["ok"], json!(true));
    assert_eq!(ping["command"], json!("pong"));

    drop(stdin);
    let status = child.wait().unwrap();
    assert!(status.success());
}

fn read_message(reader: &mut BufReader<std::process::ChildStdout>) -> Value {
    let mut line = String::new();
    reader.read_line(&mut line).unwrap();
    serde_json::from_str(line.trim()).unwrap()
}