remem-ai 0.6.79

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use std::collections::BTreeSet;

use rmcp::handler::server::wrapper::Parameters;
use serde_json::{json, Value};

use super::super::MemoryServer;
use super::{assert_mcp_error, McpErrorCode};
use crate::db::test_support::ScopedTestDataDir;
use crate::mcp::types::ContextBundleParams;

fn v1_params() -> ContextBundleParams {
    ContextBundleParams {
        schema_version: 1,
        task: "resume the context compiler work".to_string(),
        project: Some("/repo".to_string()),
        cwd: Some("/repo".to_string()),
        worktree: None,
        branch: None,
        role: None,
        as_of_epoch: Some(0),
        token_budget: Some(4_000),
        risk: None,
        include_superseded: None,
    }
}

#[test]
fn frozen_minimal_v1_request_compiles_a_versioned_bundle() -> anyhow::Result<()> {
    let _dir = ScopedTestDataDir::new("mcp-context-bundle-v1");
    let conn = crate::db::open_db()?;
    let memory_id = crate::memory::insert_memory(
        &conn,
        None,
        "/repo",
        Some("context-bundle-wire"),
        "Context compiler decision",
        "The MCP adapter returns a versioned audited bundle.",
        "decision",
        None,
    )?;
    crate::truth::test_support::seed_current_memory_proof(&conn, memory_id)?;
    drop(conn);

    // Frozen minimal JSON proves existing v1 callers can continue omitting
    // every field that has a documented default.
    let params: ContextBundleParams = serde_json::from_value(json!({
        "schema_version": 1,
        "task": "resume the context compiler work",
        "project": "/repo",
        "cwd": "/repo"
    }))?;
    let server = MemoryServer::new()?;
    let response = server
        .context_bundle(Parameters(params))
        .map_err(anyhow::Error::msg)?;
    let bundle: Value = serde_json::from_str(&response)?;

    assert_eq!(bundle["schema_version"], 1);
    assert_eq!(bundle["audit"]["schema_version"], 1);
    assert_eq!(bundle["audit"]["policy_version"], "retrieval_router_v2");
    assert_eq!(bundle["audit"]["token_budget"], 4_000);
    assert_eq!(bundle["degraded_mode"], "full");
    assert_eq!(
        bundle["current_truth"][0]["title"],
        "Context compiler decision"
    );
    assert_eq!(bundle["plan_hash"], bundle["audit"]["plan_hash"]);
    Ok(())
}

#[test]
fn v1_input_and_output_schemas_are_closed_and_pinned() -> anyhow::Result<()> {
    let server = MemoryServer::new()?;
    let tool = server
        .tool_router
        .get("context_bundle")
        .expect("context_bundle tool should be registered");

    let input = tool.input_schema.as_ref();
    assert_eq!(input.get("additionalProperties"), Some(&Value::Bool(false)));
    let input_properties = input["properties"]
        .as_object()
        .expect("input properties")
        .keys()
        .map(String::as_str)
        .collect::<BTreeSet<_>>();
    assert_eq!(
        input_properties,
        BTreeSet::from([
            "as_of_epoch",
            "branch",
            "cwd",
            "include_superseded",
            "project",
            "risk",
            "role",
            "schema_version",
            "task",
            "token_budget",
            "worktree",
        ])
    );
    let required = input["required"]
        .as_array()
        .expect("input required")
        .iter()
        .filter_map(Value::as_str)
        .collect::<BTreeSet<_>>();
    assert_eq!(required, BTreeSet::from(["schema_version", "task"]));

    let output = tool.output_schema.as_deref().expect("output schema");
    assert_eq!(
        output.get("additionalProperties"),
        Some(&Value::Bool(false))
    );
    let output_properties = output["properties"]
        .as_object()
        .expect("output properties")
        .keys()
        .map(String::as_str)
        .collect::<BTreeSet<_>>();
    assert_eq!(
        output_properties,
        BTreeSet::from([
            "audit",
            "current_truth",
            "degraded_mode",
            "failure_lessons",
            "memory_index",
            "plan_hash",
            "preferences",
            "recent_sessions",
            "schema_version",
            "workstreams",
        ])
    );
    Ok(())
}

#[test]
fn context_bundle_rejects_incompatible_or_invalid_v1_requests() -> anyhow::Result<()> {
    let _dir = ScopedTestDataDir::new("mcp-context-bundle-invalid");
    let server = MemoryServer::new()?;

    let mut cases = Vec::new();
    let mut unsupported = v1_params();
    unsupported.schema_version = 2;
    cases.push((unsupported, "unsupported schema_version"));
    let mut blank_task = v1_params();
    blank_task.task = "  ".to_string();
    cases.push((blank_task, "task is required"));
    let mut invalid_role = v1_params();
    invalid_role.role = Some("owner".to_string());
    cases.push((invalid_role, "unknown role"));
    let mut invalid_risk = v1_params();
    invalid_risk.risk = Some("extreme".to_string());
    cases.push((invalid_risk, "unknown risk"));
    let mut blank_project = v1_params();
    blank_project.project = Some("  ".to_string());
    cases.push((blank_project, "project must not be blank"));
    let mut blank_branch = v1_params();
    blank_branch.branch = Some("  ".to_string());
    cases.push((blank_branch, "branch must not be blank"));
    let mut zero_budget = v1_params();
    zero_budget.token_budget = Some(0);
    cases.push((zero_budget, "token_budget must be greater than zero"));
    let mut historical = v1_params();
    historical.as_of_epoch = Some(1_700_000_000);
    cases.push((historical, "historical as_of_epoch is not supported"));
    let mut superseded = v1_params();
    superseded.include_superseded = Some(true);
    cases.push((superseded, "include_superseded=true is not supported"));

    for (params, message) in cases {
        let error = server
            .context_bundle(Parameters(params))
            .expect_err("invalid request must fail");
        let json = assert_mcp_error(error, McpErrorCode::InvalidRequest, "context_bundle", false);
        assert!(
            json["error"]["message"]
                .as_str()
                .is_some_and(|actual| actual.contains(message)),
            "expected {message:?}, got {json}"
        );
    }
    Ok(())
}