trusty-mpm 1.8.2

trusty-mpm: unified multi-agent orchestration platform (core, daemon, CLI, TUI, Telegram)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
//! Tests for the SM dedicated memory palace + recall/remember wiring (SM-4).
//!
//! Why: prove the §8 contract — idempotent palace creation, scoped
//! remember→recall, strict SM-only scope, restart survival, and that writes
//! never target a non-SM palace. All tests point the engine at a `tempdir` so
//! they never touch the real `~/.trusty-mpm` data, and seed the MOCK embedder so
//! they run without the ONNX model (no `#[ignore]` needed — the lexical/L2 path
//! with a deterministic mock embedding is sufficient).
//! What: a `tempfile::TempDir` per test plus `seed_shared_embedder_with_mock()`
//! to avoid any HuggingFace download.

use super::*;
use tempfile::TempDir;
use trusty_common::memory_core::retrieval::seed_shared_embedder_with_mock;

use crate::core::sm::config::SmMemoryConfig;

/// Build an `SmMemory` rooted at a fresh tempdir with the default SM palace
/// name, seeding the mock embedder first.
///
/// Why: every test needs an isolated store + a deterministic embedder; this
/// keeps each test focused on the behaviour it asserts.
/// What: seeds the mock embedder, returns `(SmMemory, TempDir)` — the caller
/// holds the `TempDir` so it isn't dropped (which would delete the store).
/// Test: used by every test below.
fn sm_memory() -> (SmMemory, TempDir) {
    seed_shared_embedder_with_mock();
    let dir = TempDir::new().expect("tempdir");
    let cfg = SmMemoryConfig::default();
    let mem = SmMemory::open(dir.path(), &cfg).expect("open SM memory");
    (mem, dir)
}

/// Read `created_at` out of a palace's `palace.json`.
///
/// Why: `created_at` is the field `create_palace` destroys when it is reached
/// for a palace that already exists — it stamps `Utc::now()` on every save.
/// Reading it directly makes the assertion name the thing that is lost.
/// What: parses the metadata file and returns the `created_at` string.
/// Test: used by
/// `ensure_palace_never_rewrites_metadata_of_a_present_but_unopenable_palace`.
fn created_at_of(metadata: &std::path::Path) -> String {
    let raw = std::fs::read_to_string(metadata).expect("read palace.json");
    let json: serde_json::Value = serde_json::from_str(&raw).expect("parse palace.json");
    json["created_at"]
        .as_str()
        .expect("palace.json carries created_at")
        .to_string()
}

/// Materialise a palace on disk that exists but cannot be opened, WITHOUT this
/// process ever opening it.
///
/// Why: two traps make the obvious fixture (create a palace, then corrupt it)
/// silently vacuous. The store keeps a process-wide cache of open redb
/// databases keyed by path, so a palace this process already opened is served
/// from that cache and never touches the filesystem — the corruption is never
/// read. And `PalaceHandle` opens `<dir>/kg.db` through
/// `KnowledgeGraph::open_with_intent`, which rewrites the extension, so the
/// bytes that actually matter live at `kg.redb`. Hand-writing the metadata
/// avoids the first trap; naming `kg.redb` avoids the second.
/// What: writes a valid `palace.json` carrying `created_at`, then puts a
/// DIRECTORY where the KG store file belongs. Opening a directory as a database
/// file fails at the OS layer on every platform and every redb version, so the
/// failure is deterministic — no lock, no race, no format dependence. Returns
/// the palace directory.
/// Test: used by
/// `ensure_palace_never_rewrites_metadata_of_a_present_but_unopenable_palace`.
fn seed_unopenable_palace_on_disk(
    data_root: &std::path::Path,
    palace_id: &str,
) -> std::path::PathBuf {
    let palace_dir = data_root.join(palace_id);
    std::fs::create_dir_all(&palace_dir).expect("create palace dir");
    let metadata = serde_json::json!({
        "id": palace_id,
        "name": palace_id,
        "description": serde_json::Value::Null,
        "created_at": "2020-01-02T03:04:05Z",
        "data_dir": palace_dir,
        "schema_version": 1,
    });
    std::fs::write(
        palace_dir.join("palace.json"),
        serde_json::to_vec_pretty(&metadata).expect("serialise palace.json"),
    )
    .expect("write palace.json");
    std::fs::create_dir(palace_dir.join("kg.redb")).expect("directory where the KG store belongs");
    palace_dir
}

/// Why (issue #4911): `ensure_palace` treated EVERY `open_palace` failure as
/// "the palace does not exist" and fell through to `create_palace`, which
/// rewrites `palace.json` unconditionally via `PalaceStore::save_palace` — so a
/// palace that merely could not be READ lost its `created_at` and only then
/// re-failed against the same unreadable store. Making a read-only open of an
/// incompatible-format store refuse (rather than silently recreate it empty)
/// turned that latent path into a live one, which is how a fix for data
/// destruction reintroduced data destruction one layer out.
/// What: seeds a present-but-unopenable SM palace on disk (see
/// `seed_unopenable_palace_on_disk`), records its `created_at`, then constructs
/// an `SmMemory` against that root. Asserts the construction fails AND that
/// `palace.json` is byte-identical. The byte-identity assertion is what proves
/// `create_palace` was never reached: reaching it rewrites the file with a fresh
/// `created_at`, so this test cannot pass while that call happens.
/// Test: this is the test.
#[test]
fn ensure_palace_never_rewrites_metadata_of_a_present_but_unopenable_palace() {
    seed_shared_embedder_with_mock();
    let dir = TempDir::new().expect("tempdir");
    let cfg = SmMemoryConfig::default();

    let palace_dir = seed_unopenable_palace_on_disk(dir.path(), &cfg.palace);
    let metadata = palace_dir.join("palace.json");
    let created_at_before = created_at_of(&metadata);
    let metadata_before = std::fs::read(&metadata).expect("read palace.json");

    // `SmMemory` is not `Debug`, so `expect_err` is unavailable here.
    let err = match SmMemory::open(dir.path(), &cfg) {
        Ok(_) => {
            panic!("opening a present-but-unopenable palace must fail, not silently recreate it")
        }
        Err(e) => e,
    };

    assert_eq!(
        created_at_before,
        created_at_of(&metadata),
        "created_at was rewritten, so `ensure_palace` fell through to \
         `create_palace` for a palace that exists but could not be read; the \
         open error was: {err}"
    );
    assert_eq!(
        metadata_before,
        std::fs::read(&metadata).expect("read palace.json"),
        "palace.json must be byte-identical — any rewrite means `create_palace` ran"
    );
    assert!(
        palace_dir.join("kg.redb").is_dir(),
        "the unopenable store must be left exactly as it is, not recovered destructively"
    );
}

/// Why: idempotency is the headline SM-4 guarantee — ensuring the palace twice
/// (here: a second `SmMemory::open` against the same root) must NOT create a
/// duplicate or wipe the first; exactly one palace must persist.
/// What: opens SM memory twice against the same data root, then asserts exactly
/// one persisted palace named `"session-manager"`.
/// Test: this is the test.
#[test]
fn palace_create_is_idempotent() {
    seed_shared_embedder_with_mock();
    let dir = TempDir::new().expect("tempdir");
    let cfg = SmMemoryConfig::default();

    let first = SmMemory::open(dir.path(), &cfg).expect("first open");
    assert_eq!(first.persisted_palace_count().expect("count"), 1);

    // Second construction against the same root must be a no-op create.
    let second = SmMemory::open(dir.path(), &cfg).expect("second open");
    assert_eq!(
        second.persisted_palace_count().expect("count"),
        1,
        "creating the SM palace twice must yield exactly ONE palace"
    );
    assert_eq!(second.palace_id().as_str(), "session-manager");
}

/// Why: the TOCTOU-race fix in `ensure_palace` must take the OPEN branch (not
/// re-create) when the palace already exists on disk but is absent from THIS
/// instance's registry cache — the exact situation a second concurrent or
/// restarted SM faces after another process has already created the palace.
/// This guards against the create-when-exists fallback wiping or duplicating an
/// existing palace.
/// What: builds a first `SmMemory`, writes a distinctive fact (forcing the
/// palace + content onto disk), drops it, then builds a SECOND `SmMemory` with a
/// FRESH registry against the same root. The second instance must converge on
/// the existing palace (exactly one persisted) AND surface the first instance's
/// already-written fact via recall — proving it opened the existing palace
/// rather than creating a fresh empty one.
/// Test: this is the test.
#[tokio::test]
async fn ensure_palace_falls_back_to_open_when_palace_already_exists() {
    seed_shared_embedder_with_mock();
    let dir = TempDir::new().expect("tempdir");
    let cfg = SmMemoryConfig::default();

    // First instance materialises the palace and writes a marker fact.
    {
        let first = SmMemory::open(dir.path(), &cfg).expect("first open");
        assert_eq!(first.persisted_palace_count().expect("count"), 1);
        first
            .remember("TOCTOU_MARKER existing palace content written by first SM")
            .await
            .expect("remember in first instance");
        // `first` (and its registry cache) drops here — the palace now lives
        // ONLY on disk, exactly as a second process would observe it.
    }

    // Second instance has a brand-new registry, so `ensure_palace` cannot hit
    // the cached fast path — it must take the open-first branch against disk.
    let second = SmMemory::open(dir.path(), &cfg).expect("second open");
    assert_eq!(
        second.persisted_palace_count().expect("count"),
        1,
        "create-when-exists fallback must open the existing palace, not add a second"
    );

    let hits = second
        .recall("what marker did the first SM write")
        .await
        .expect("recall from second instance");
    assert!(
        hits.iter()
            .any(|h| h.drawer.content().contains("TOCTOU_MARKER")),
        "second SM must open the EXISTING palace (seeing prior content), not a fresh one; got {hits:?}"
    );
}

/// Why: the core round-trip — a remembered fact must be retrievable via recall
/// from the SM palace.
/// What: remembers a distinctive sentence, recalls a related query, asserts the
/// remembered content appears among the hits.
/// Test: this is the test.
#[tokio::test]
async fn remember_then_recall_round_trips() {
    let (mem, _dir) = sm_memory();

    mem.remember("The session manager delegates all engineering work to t-mpm sessions")
        .await
        .expect("remember");

    let hits = mem
        .recall("who does the session manager delegate work to")
        .await
        .expect("recall");

    assert!(
        hits.iter().any(|h| h
            .drawer
            .content()
            .contains("delegates all engineering work")),
        "recall must surface the remembered SM fact; got {hits:?}"
    );
}

/// Why: deep recall (L0+L1+L3) must also return SM facts, proving the deep path
/// is wired and scoped.
/// What: remembers a fact, runs `recall_deep`, asserts the fact is present.
/// Test: this is the test.
#[tokio::test]
async fn recall_deep_round_trips() {
    let (mem, _dir) = sm_memory();

    mem.remember("Decision: adopt worktree-per-ticket discipline for all SM work")
        .await
        .expect("remember");

    let hits = mem
        .recall_deep("what discipline did we adopt for tickets")
        .await
        .expect("recall_deep");

    assert!(
        hits.iter()
            .any(|h| h.drawer.content().contains("worktree-per-ticket")),
        "recall_deep must surface the remembered SM fact; got {hits:?}"
    );
}

/// Why: `note` stores short curated facts that the normal `remember` token gate
/// would reject — proving the curated-fact path works end to end.
/// What: notes a terse decision, recalls it, asserts it is retrievable.
/// Test: this is the test.
#[tokio::test]
async fn note_stores_short_fact() {
    let (mem, _dir) = sm_memory();

    // Short enough to trip the min-token gate on the plain `remember` path.
    mem.note("Goal: ship SM-4").await.expect("note short fact");

    let hits = mem.recall("what is the goal").await.expect("recall");
    assert!(
        hits.iter()
            .any(|h| h.drawer.content().contains("ship SM-4")),
        "note must store a short curated fact retrievable via recall; got {hits:?}"
    );
}

/// Why: scope isolation — facts written to the SM palace must surface ONLY when
/// recalling the SM palace; a sibling palace under the same data root must not
/// leak its content into SM recall (and vice versa).
/// What: writes a distinctive fact to the SM palace, then builds a SECOND
/// `SmMemory` bound to a different palace name under the SAME root, writes a
/// different fact there, and asserts each recall sees only its own palace's fact.
/// Test: this is the test.
#[tokio::test]
async fn recall_is_scoped_to_sm_palace() {
    seed_shared_embedder_with_mock();
    let dir = TempDir::new().expect("tempdir");

    let sm_cfg = SmMemoryConfig::default(); // "session-manager"
    let sm = SmMemory::open(dir.path(), &sm_cfg).expect("open SM");
    sm.remember("SM_PALACE_SECRET zebra orchestration delegation marker")
        .await
        .expect("remember in SM palace");

    let other_cfg = SmMemoryConfig {
        palace: "some-other-project".to_string(),
        recall_top_k: 6,
    };
    let other = SmMemory::open(dir.path(), &other_cfg).expect("open other");
    other
        .remember("OTHER_PALACE_SECRET giraffe unrelated project marker")
        .await
        .expect("remember in other palace");

    // SM recall must NOT see the other palace's marker.
    let sm_hits = sm.recall("secret marker").await.expect("sm recall");
    assert!(
        sm_hits
            .iter()
            .all(|h| !h.drawer.content().contains("OTHER_PALACE_SECRET")),
        "SM recall must never surface another palace's content; got {sm_hits:?}"
    );

    // The other palace's recall must NOT see the SM marker.
    let other_hits = other.recall("secret marker").await.expect("other recall");
    assert!(
        other_hits
            .iter()
            .all(|h| !h.drawer.content().contains("SM_PALACE_SECRET")),
        "non-SM recall must never surface SM content; got {other_hits:?}"
    );
}

/// Why: restart survival — remembered SM data must persist across a FRESH
/// `SmMemory` construction pointing at the same storage (process restart).
/// What: remembers a fact, drops the first `SmMemory`, builds a new one against
/// the same root, and recalls the fact successfully.
/// Test: this is the test.
#[tokio::test]
async fn data_survives_fresh_construction() {
    seed_shared_embedder_with_mock();
    let dir = TempDir::new().expect("tempdir");
    let cfg = SmMemoryConfig::default();

    {
        let mem = SmMemory::open(dir.path(), &cfg).expect("first open");
        mem.remember("Persistent SM outcome: SM-3 merged to main successfully")
            .await
            .expect("remember");
        // `mem` drops here, simulating a process restart.
    }

    // Fresh construction against the same storage.
    let reopened = SmMemory::open(dir.path(), &cfg).expect("reopen");
    assert_eq!(
        reopened.persisted_palace_count().expect("count"),
        1,
        "reopen must reuse the existing palace, not create a second"
    );

    let hits = reopened
        .recall("which SM ticket merged to main")
        .await
        .expect("recall after restart");
    assert!(
        hits.iter()
            .any(|h| h.drawer.content().contains("SM-3 merged to main")),
        "remembered data must survive a fresh SmMemory construction; got {hits:?}"
    );
}

/// Why: the structural "SM never writes to a non-SM palace" guarantee — every
/// write goes to the bound palace id and to no other namespace, regardless of
/// how many writes occur.
/// What: performs `remember`, `note`, and another `remember`, then asserts the
/// bound `palace_id` is the configured SM name and that exactly ONE palace
/// exists on disk (so no stray palace was created by any write).
/// Test: this is the test.
#[tokio::test]
async fn writes_target_only_the_sm_palace() {
    let (mem, _dir) = sm_memory();

    assert_eq!(
        mem.palace_id().as_str(),
        "session-manager",
        "SM must be bound to the configured palace name"
    );

    mem.remember("first SM write — a goal for the milestone")
        .await
        .expect("remember 1");
    mem.note("terse SM decision").await.expect("note");
    mem.remember("third SM write — an outcome record for the milestone")
        .await
        .expect("remember 2");

    assert_eq!(
        mem.persisted_palace_count().expect("count"),
        1,
        "no write may create or touch a palace other than the bound SM palace"
    );
}

/// Why: graceful degradation — when the palace cannot be ensured (here: a data
/// root that cannot be created because a FILE exists at that path), `open` must
/// return a structured `SmMemoryError::Palace`, not panic.
/// What: creates a regular file, then tries to root `SmMemory` at a path BELOW
/// that file (so `create_dir_all` fails), and asserts the `Palace` error variant.
/// Test: this is the test.
#[test]
fn construct_on_unwritable_root_is_error() {
    seed_shared_embedder_with_mock();
    let dir = TempDir::new().expect("tempdir");
    let file_path = dir.path().join("not-a-dir");
    std::fs::write(&file_path, b"x").expect("write blocking file");

    // Rooting under a path whose parent is a file makes create_dir_all fail.
    let bad_root = file_path.join("child");
    let cfg = SmMemoryConfig::default();

    // `SmMemory` is not `Debug` (it owns a non-`Debug` registry), so match on the
    // error directly rather than via `expect_err`.
    match SmMemory::open(&bad_root, &cfg) {
        Ok(_) => panic!("must fail to create store under a file"),
        Err(err) => assert!(
            matches!(err, SmMemoryError::Palace { .. }),
            "unavailable backend must degrade to SmMemoryError::Palace, got {err:?}"
        ),
    }
}