lwc 0.17.11

Agent-driven proactive memory CLI for AI agents — autonomously recall, maintain, and evolve persistent, source-grounded knowledge across sessions.
use serde_json::Value;
use std::{
    fs,
    path::PathBuf,
    process::{Command, Output},
};

struct World {
    _temp: tempfile::TempDir,
    project: PathBuf,
    home: PathBuf,
}
impl World {
    fn new() -> Self {
        let temp = tempfile::tempdir().unwrap();
        let project = temp.path().join("project");
        let home = temp.path().join("home");
        fs::create_dir_all(&project).unwrap();
        fs::create_dir_all(&home).unwrap();
        let w = Self {
            _temp: temp,
            project,
            home,
        };
        w.ok(&["init"]);
        w.ok(&["config", "set", "--todo", "enabled", "--plan", "enabled"]);
        w
    }
    fn output(&self, args: &[&str]) -> Output {
        Command::new(env!("CARGO_BIN_EXE_lwc"))
            .current_dir(&self.project)
            .env("HOME", &self.home)
            .args(args)
            .output()
            .unwrap()
    }
    fn ok(&self, args: &[&str]) -> Value {
        let o = self.output(args);
        assert!(
            o.status.success(),
            "{args:?}\nstdout: {}\nstderr: {}",
            String::from_utf8_lossy(&o.stdout),
            String::from_utf8_lossy(&o.stderr)
        );
        serde_json::from_slice(&o.stdout).unwrap()
    }
    fn error(&self, args: &[&str]) -> Value {
        let o = self.output(args);
        assert!(!o.status.success(), "expected failure: {args:?}");
        serde_json::from_slice(&o.stderr).unwrap()
    }
}

#[test]
fn plan_create_advance_block_brief_and_complete() {
    let w = World::new();
    let created = w.ok(&[
        "plan",
        "create",
        "Todo/Plan delivery",
        "--objective",
        "ship usable commands",
        "--done-when",
        "acceptance passes",
        "--constraint",
        "no new deps",
        "--tag",
        "release",
        "--step",
        "schema",
        "--step",
        "cli",
    ]);
    assert_eq!(created["action"], "created");
    let plan = &created["plan"];
    assert_eq!(plan["state"], "active");
    assert_eq!(plan["revision"], 1);
    assert_eq!(plan["id"].as_str().unwrap().len(), 32);
    assert_eq!(plan["steps"][0]["status"], "in_progress");
    assert_eq!(plan["steps"][1]["status"], "pending");
    let id = plan["id"].as_str().unwrap();
    let first = plan["steps"][0]["id"].as_str().unwrap();
    let second = plan["steps"][1]["id"].as_str().unwrap();
    let advanced = w.ok(&[
        "plan",
        "advance",
        id,
        "--if-revision",
        "1",
        "--done",
        first,
        "--result",
        "schema ready",
        "--next",
        second,
    ]);
    assert_eq!(advanced["plan"]["revision"], 2);
    assert_eq!(advanced["plan"]["steps"][1]["status"], "in_progress");
    assert_eq!(
        w.error(&[
            "plan",
            "block",
            id,
            "--if-revision",
            "1",
            "--step",
            second,
            "--reason",
            "bad"
        ])["error"]["code"],
        "plan_revision_conflict"
    );
    assert_eq!(
        w.ok(&[
            "plan",
            "block",
            id,
            "--if-revision",
            "2",
            "--step",
            second,
            "--reason",
            "waiting"
        ])["plan"]["steps"][1]["status"],
        "blocked"
    );
    let brief = w.ok(&["plan", "brief", id]);
    assert_eq!(brief["plan"]["id"], id);
    assert!(brief["terminal_steps"].as_array().unwrap().len() <= 20);
    assert_eq!(
        w.error(&[
            "plan",
            "complete",
            id,
            "--if-revision",
            "3",
            "--result",
            "done",
            "--evidence",
            "tests",
            "--done-when-checked"
        ])["error"]["code"],
        "plan_completion_incomplete"
    );
}

#[test]
fn plan_tracking_is_exact_and_isolated_by_agent_context() {
    let w = World::new();
    let first = w.ok(&[
        "plan",
        "create",
        "first",
        "--objective",
        "o",
        "--done-when",
        "d",
        "--step",
        "only",
    ]);
    let second = w.ok(&[
        "plan",
        "create",
        "second",
        "--objective",
        "o",
        "--done-when",
        "d",
        "--step",
        "only",
    ]);
    let first_id = first["plan"]["id"].as_str().unwrap();
    let second_id = second["plan"]["id"].as_str().unwrap();
    let context_a = format!("lwcctx-v1-{}", "a".repeat(64));
    let context_b = format!("lwcctx-v1-{}", "b".repeat(64));

    assert_eq!(
        w.ok(&["plan", "track", first_id, "--context", &context_a])["action"],
        "tracked"
    );
    assert_eq!(
        w.ok(&["plan", "track", first_id, "--context", &context_a])["action"],
        "unchanged"
    );
    assert_eq!(
        w.ok(&["plan", "current", "--context", &context_a])["plan"]["id"],
        first_id
    );
    assert!(w.ok(&["plan", "current", "--context", &context_b])["plan"].is_null());
    assert_eq!(
        w.error(&["plan", "track", second_id, "--context", &context_a])["error"]["code"],
        "plan_tracking_conflict"
    );
    assert_eq!(
        w.ok(&["plan", "untrack", second_id, "--context", &context_a])["action"],
        "unchanged"
    );
    assert_eq!(
        w.ok(&["plan", "untrack", first_id, "--context", &context_a])["action"],
        "untracked"
    );
    assert_eq!(
        w.ok(&["plan", "untrack", first_id, "--context", &context_a])["action"],
        "unchanged"
    );
    assert_eq!(
        w.error(&["plan", "current", "--context", "bad"])["error"]["code"],
        "invalid_agent_context"
    );
    assert_eq!(
        w.error(&[
            "--scope",
            "all",
            "plan",
            "track",
            first_id,
            "--context",
            &context_a,
        ])["error"]["code"],
        "scope_not_supported"
    );
}

#[test]
fn plan_can_finish_and_is_idempotent_by_request() {
    let w = World::new();
    let a = w.ok(&[
        "plan",
        "create",
        "one",
        "--objective",
        "o",
        "--done-when",
        "d",
        "--step",
        "only",
        "--request-id",
        "p-1",
    ]);
    assert_eq!(
        w.ok(&[
            "plan",
            "create",
            "one",
            "--objective",
            "o",
            "--done-when",
            "d",
            "--step",
            "only",
            "--request-id",
            "p-1"
        ])["action"],
        "unchanged"
    );
    assert_eq!(
        w.error(&[
            "plan",
            "create",
            "two",
            "--objective",
            "o",
            "--done-when",
            "d",
            "--step",
            "only",
            "--request-id",
            "p-1"
        ])["error"]["code"],
        "plan_request_conflict"
    );
    let id = a["plan"]["id"].as_str().unwrap();
    let step = a["plan"]["steps"][0]["id"].as_str().unwrap();
    w.ok(&[
        "plan",
        "advance",
        id,
        "--if-revision",
        "1",
        "--done",
        step,
        "--result",
        "ok",
    ]);
    let completed = w.ok(&[
        "plan",
        "complete",
        id,
        "--if-revision",
        "2",
        "--result",
        "shipped",
        "--evidence",
        "gate green",
        "--done-when-checked",
    ]);
    assert_eq!(completed["plan"]["state"], "completed");
    assert_eq!(completed["plan"]["revision"], 3);
    assert_eq!(w.ok(&["plan", "current"])["returned"], 0);
    assert_eq!(
        w.error(&["--changeset", "x", "plan", "list"])["error"]["code"],
        "changeset_command_unsupported"
    );
}

#[test]
fn plan_json_revision_search_and_abandon_work() {
    let w = World::new();
    let raw = r#"{"title":"重构","objective":"减少复杂度","done_when":"测试通过","tags":["工程"],"constraints":["不加依赖"],"steps":[{"title":"旧步骤","verify":null}],"request_id":null}"#;
    let created = w.ok(&["plan", "create", "--json", raw]);
    let id = created["plan"]["id"].as_str().unwrap();
    let revised = w.ok(&[
        "plan",
        "revise",
        id,
        "--if-revision",
        "1",
        "--reason",
        "发现更短路径",
        "--json",
        r#"{"steps":[{"title":"新步骤","verify":"unit"}],"focal":0}"#,
    ]);
    assert_eq!(revised["plan"]["revision"], 2);
    assert_eq!(revised["plan"]["steps"][0]["status"], "skipped");
    assert_eq!(revised["plan"]["steps"][1]["status"], "in_progress");
    assert_eq!(
        w.ok(&["plan", "search", "复杂度", "--tag", "工程"])["returned"],
        1
    );
    assert_eq!(
        w.ok(&[
            "plan",
            "abandon",
            id,
            "--if-revision",
            "2",
            "--reason",
            "superseded"
        ])["plan"]["state"],
        "abandoned"
    );
}