feat(memory_diff): read-marker watermark, real text diffs, item titles (#4100)

This commit is contained in:
Steven Enamakel
2026-06-25 01:50:39 -07:00
committed by GitHub
parent 4d664fa2f8
commit febc1b5365
11 changed files with 1178 additions and 28 deletions
+229
View File
@@ -4232,6 +4232,235 @@ async fn json_rpc_memory_tree_end_to_end() {
let _ = mock_join.await;
}
/// `openhuman.memory_diff_*` full lifecycle over JSON-RPC.
///
/// Drives the snapshot-based change tracker end to end: register a folder
/// source, ingest chunks under its `mem_src:<id>:%` prefix across several
/// snapshots, then exercise take_snapshot, diff_since_last, the read-marker
/// watermark (diff_since_read + commit → empty on re-read → only-new after a
/// later snapshot), mark_read, and cross-source checkpoints. This is the
/// turn-to-turn world-diff assertion. (Per-item add/remove/modify detection
/// and text diffs are covered exhaustively by the ops unit tests.)
#[tokio::test]
async fn json_rpc_memory_diff_snapshot_diff_and_read_marker_lifecycle() {
let _env_lock = json_rpc_e2e_env_lock();
let tmp = tempdir().expect("tempdir");
let home = tmp.path();
let openhuman_home = home.join(".openhuman");
let _home_guard = EnvVarGuard::set_to_path("HOME", home);
let _workspace_guard = EnvVarGuard::unset("OPENHUMAN_WORKSPACE");
let _backend_url_guard = EnvVarGuard::unset("BACKEND_URL");
let _vite_backend_guard = EnvVarGuard::unset("VITE_BACKEND_URL");
// Fall back to the inert (zero-vector) embedder; CI has no local Ollama.
let _embed_strict_guard = EnvVarGuard::set("OPENHUMAN_MEMORY_EMBED_STRICT", "false");
let _embed_endpoint_guard = EnvVarGuard::set("OPENHUMAN_MEMORY_EMBED_ENDPOINT", "");
let _embed_model_guard = EnvVarGuard::set("OPENHUMAN_MEMORY_EMBED_MODEL", "");
let (mock_addr, mock_join) = serve_on_ephemeral(mock_upstream_router()).await;
let mock_origin = format!("http://{}", mock_addr);
write_min_config(&openhuman_home, &mock_origin);
let (rpc_addr, rpc_join) = serve_on_ephemeral(build_core_http_router(false)).await;
let rpc_base = format!("http://{rpc_addr}");
tokio::time::sleep(Duration::from_millis(100)).await;
// ── Register a folder memory source; capture its generated id ─────────
let add = post_json_rpc(
&rpc_base,
9001,
"openhuman.memory_sources_add",
json!({
"kind": "folder",
"label": "Diff E2E Docs",
"path": home.join("docs").to_string_lossy(),
}),
)
.await;
let add_result = assert_no_jsonrpc_error(&add, "memory_sources_add");
let add_result = add_result.get("result").unwrap_or(add_result);
let source_id = add_result
.pointer("/source/id")
.and_then(Value::as_str)
.expect("source id")
.to_string();
// Chunks belonging to a reader-backed source live under `mem_src:<id>:%`.
let ingest = |id: i64, item: &str, body: &str| {
let composite = format!("mem_src:{source_id}:{item}");
post_json_rpc(
&rpc_base,
id,
"openhuman.memory_tree_ingest",
json!({
"source_kind": "document",
"source_id": composite,
"owner": "alice@example.com",
"payload": {
"provider": "folder",
"title": item,
"body": body,
"modified_at": 1700000000000_i64,
"source_ref": format!("file://{item}"),
}
}),
)
};
// ── Generation 1: one doc, then snapshot ──────────────────────────────
let g1 = ingest(9002, "doc1.md", "First version of the launch plan.").await;
assert_no_jsonrpc_error(&g1, "ingest g1");
let snap1 = post_json_rpc(
&rpc_base,
9003,
"openhuman.memory_diff_take_snapshot",
json!({ "source_id": source_id }),
)
.await;
let snap1 = assert_no_jsonrpc_error(&snap1, "take_snapshot 1");
let snap1 = snap1.get("result").unwrap_or(snap1);
assert_eq!(
snap1.pointer("/snapshot/item_count"),
Some(&json!(1)),
"first snapshot has one item: {snap1}"
);
// ── Generation 2: add doc2, then snapshot ─────────────────────────────
let g2b = ingest(9005, "doc2.md", "Rollout checklist and staging notes.").await;
assert_no_jsonrpc_error(&g2b, "ingest g2b");
let snap2 = post_json_rpc(
&rpc_base,
9006,
"openhuman.memory_diff_take_snapshot",
json!({ "source_id": source_id }),
)
.await;
let snap2 = assert_no_jsonrpc_error(&snap2, "take_snapshot 2");
let snap2 = snap2.get("result").unwrap_or(snap2);
assert_eq!(
snap2.pointer("/snapshot/item_count"),
Some(&json!(2)),
"second snapshot has two items: {snap2}"
);
// ── diff_since_last: doc2 added, doc1 unchanged ───────────────────────
let dsl = post_json_rpc(
&rpc_base,
9007,
"openhuman.memory_diff_diff_since_last",
json!({ "source_id": source_id, "include_text_diff": true }),
)
.await;
let dsl = assert_no_jsonrpc_error(&dsl, "diff_since_last");
let dsl = dsl.get("result").unwrap_or(dsl);
assert_eq!(dsl.pointer("/diff/summary/added"), Some(&json!(1)), "{dsl}");
assert_eq!(
dsl.pointer("/diff/summary/unchanged"),
Some(&json!(1)),
"{dsl}"
);
// ── diff_since_read (commit) then re-read → empty (marker advanced) ───
let read1 = post_json_rpc(
&rpc_base,
9008,
"openhuman.memory_diff_diff_since_read",
json!({ "source_id": source_id }),
)
.await;
let read1 = assert_no_jsonrpc_error(&read1, "diff_since_read 1");
let read1 = read1.get("result").unwrap_or(read1);
// First read with no prior marker reports everything as added.
assert_eq!(
read1.pointer("/diff/summary/added"),
Some(&json!(2)),
"{read1}"
);
let read2 = post_json_rpc(
&rpc_base,
9009,
"openhuman.memory_diff_diff_since_read",
json!({ "source_id": source_id }),
)
.await;
let read2 = assert_no_jsonrpc_error(&read2, "diff_since_read 2");
let read2 = read2.get("result").unwrap_or(read2);
assert_eq!(
read2.pointer("/diff/summary/added"),
Some(&json!(0)),
"{read2}"
);
assert_eq!(
read2.pointer("/diff/summary/modified"),
Some(&json!(0)),
"second read after commit is empty: {read2}"
);
// ── mark_read is idempotent on an already-acknowledged source ─────────
let mark = post_json_rpc(
&rpc_base,
9010,
"openhuman.memory_diff_mark_read",
json!({ "source_ids": [source_id] }),
)
.await;
let mark = assert_no_jsonrpc_error(&mark, "mark_read");
let mark = mark.get("result").unwrap_or(mark);
assert_eq!(mark.get("marked"), Some(&json!(1)), "{mark}");
// ── Checkpoint + cross-source diff after a further change ─────────────
let ckpt = post_json_rpc(
&rpc_base,
9011,
"openhuman.memory_diff_create_checkpoint",
json!({ "label": "baseline" }),
)
.await;
let ckpt = assert_no_jsonrpc_error(&ckpt, "create_checkpoint");
let ckpt = ckpt.get("result").unwrap_or(ckpt);
let checkpoint_id = ckpt
.pointer("/checkpoint/id")
.and_then(Value::as_str)
.expect("checkpoint id")
.to_string();
// Add doc3, snapshot, then diff since the checkpoint.
let g3 = ingest(9012, "doc3.md", "Post-launch retro and metrics.").await;
assert_no_jsonrpc_error(&g3, "ingest g3");
let snap3 = post_json_rpc(
&rpc_base,
9013,
"openhuman.memory_diff_take_snapshot",
json!({ "source_id": source_id }),
)
.await;
assert_no_jsonrpc_error(&snap3, "take_snapshot 3");
let since_ckpt = post_json_rpc(
&rpc_base,
9014,
"openhuman.memory_diff_diff_since_checkpoint",
json!({ "checkpoint_id": checkpoint_id }),
)
.await;
let since_ckpt = assert_no_jsonrpc_error(&since_ckpt, "diff_since_checkpoint");
let since_ckpt = since_ckpt.get("result").unwrap_or(since_ckpt);
assert_eq!(
since_ckpt.pointer("/diff/summary/added"),
Some(&json!(1)),
"doc3 added since checkpoint: {since_ckpt}"
);
rpc_join.abort();
let _ = rpc_join.await;
mock_join.abort();
let _ = mock_join.await;
}
/// `openhuman.memory_tree_cover_window` over RPC: ingest a chunk, then assert
/// the windowed minimum-cover returns it raw inside the window and nothing
/// outside it. One ingested chunk doesn't reach the seal fanout, so this also