crtx 0.1.1

CLI for the Cortex supervisory memory substrate.
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
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
//! End-to-end integration test for the full memory loop.
//!
//! ## What this covers
//!
//! Two tests are provided:
//!
//! ### `full_memory_loop_session_close_commit_search_outcome`
//!
//! The complete CLI-driven path:
//!
//! ```text
//! cortex init
//!   → cortex session close   (activates memories immediately in CLI mode)
//!   → cortex memory list     (confirms memories are active)
//!   → cortex memory health   (confirms health report structure and counts)
//!   → cortex memory outcome record (records a helpful verdict)
//!   → cortex memory health   (confirms no_outcome_count decreases)
//!   → second cortex session close (accumulates additional memories)
//!   → cortex memory list     (confirms total active count grew)
//! ```
//!
//! ### `memory_search_returns_active_properly_lineaged_memories`
//!
//! Verifies that `cortex memory search` returns results for active memories
//! whose source events exist in the SQLite `events` table (proof closure passes).
//! This is the positive counterpart to `session_close_pending_memories_not_returned_by_search`
//! in `cli_session_close.rs`.
//!
//! ## CLI vs MCP activation path
//!
//! The CLI `cortex session close` command activates memories immediately by
//! calling `MemoryRepo::set_active` (ADR 0047 §2 Alternatives — the CLI is an
//! operator-initiated synchronous path). The MCP path (`cortex-session` library)
//! instead writes `pending_mcp_commit` status and waits for
//! `cortex_session_commit`. The pending-state search exclusion is tested in
//! `cli_session_close.rs` (`session_close_pending_memories_not_returned_by_search`).
//!
//! ## Proof closure and `cortex memory search`
//!
//! `cortex memory search` calls `verify_memory_proof_closure` for each active
//! memory; if any memory's source events are absent from the SQLite `events`
//! table, the proof closure is `Quarantine` and the command exits non-zero.
//! The CLI `cortex ingest` (called by `session close`) writes events only to
//! the JSONL ledger, NOT to the SQLite `events` table. Therefore memories
//! activated by `cortex session close` cannot be searched via `cortex memory
//! search` until their source events are mirrored into the SQLite store.
//! `cortex memory list` and `cortex memory health` do NOT apply proof closure
//! checks, so the main E2E loop test uses those commands to confirm activation.
//! The search sub-test uses a separate isolated store containing ONLY memories
//! with proper SQLite event lineage, inserted via the store API.

use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};

use chrono::{TimeZone, Utc};
use cortex_core::{AuditRecordId, Event, EventId, EventSource, EventType, TraceId};
use cortex_llm::{blake3_hex, LlmMessage, LlmRequest, LlmRole};
use cortex_reflect::{session_reflection_json_schema, DEFAULT_REFLECTION_MODEL};
use cortex_store::migrate::apply_pending;
use cortex_store::repo::memories::accept_candidate_policy_decision_test_allow;
use cortex_store::repo::{EventRepo, MemoryAcceptanceAudit, MemoryCandidate, MemoryRepo};
use rusqlite::Connection;
use serde_json::json;

fn cortex_bin() -> PathBuf {
    PathBuf::from(env!("CARGO_BIN_EXE_cortex"))
}

/// Run a cortex command with an isolated XDG/HOME so each test gets its own store.
fn run_in(cwd: &Path, args: &[&str]) -> std::process::Output {
    Command::new(cortex_bin())
        .current_dir(cwd)
        .env("XDG_DATA_HOME", cwd.join("xdg"))
        .env("HOME", cwd)
        .args(args)
        .output()
        .expect("spawn cortex")
}

fn assert_exit(out: &std::process::Output, expected: i32) {
    let code = out.status.code().expect("process exited via signal");
    assert_eq!(
        code,
        expected,
        "expected exit {expected}, got {code}\nstdout: {}\nstderr: {}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr),
    );
}

/// Run `cortex init` and return the resolved DB path.
fn init(tmp: &Path) -> PathBuf {
    let out = run_in(tmp, &["init"]);
    assert_exit(&out, 0);
    let stdout = String::from_utf8_lossy(&out.stdout);
    let db_line = stdout
        .lines()
        .find(|line| line.starts_with("cortex init: db"))
        .expect("init stdout includes db path");
    let path = db_line
        .split_once('=')
        .expect("db line has equals")
        .1
        .trim()
        .split_once(" (")
        .expect("db line has status suffix")
        .0;
    PathBuf::from(path)
}

fn tmp_dir(test_name: &str) -> tempfile::TempDir {
    tempfile::Builder::new()
        .prefix(&format!("cortex-e2e-loop-{test_name}-"))
        .tempdir()
        .expect("create temp dir")
}

/// Build a minimal two-event session fixture for the given trace and event ids.
fn write_session_fixture(
    dir: &Path,
    name: &str,
    trace_id: &str,
    event_id_a: &str,
    event_id_b: &str,
) -> PathBuf {
    let path = dir.join(format!("{name}.json"));
    let events = json!({
        "events": [
            {
                "id": event_id_a,
                "schema_version": 1,
                "observed_at": "2026-05-13T10:00:00Z",
                "recorded_at": "2026-05-13T10:00:00Z",
                "source": { "type": "child_agent", "model": "replay" },
                "event_type": "cortex.event.agent_response.v1",
                "trace_id": trace_id,
                "session_id": "e2e-loop-session",
                "domain_tags": ["testing"],
                "payload": { "text": "E2E memory loop test event one." },
                "payload_hash": "",
                "prev_event_hash": null,
                "event_hash": ""
            },
            {
                "id": event_id_b,
                "schema_version": 1,
                "observed_at": "2026-05-13T10:00:05Z",
                "recorded_at": "2026-05-13T10:00:05Z",
                "source": { "type": "child_agent", "model": "replay" },
                "event_type": "cortex.event.agent_response.v1",
                "trace_id": trace_id,
                "session_id": "e2e-loop-session",
                "domain_tags": ["testing"],
                "payload": { "text": "E2E memory loop test event two." },
                "payload_hash": "",
                "prev_event_hash": null,
                "event_hash": ""
            }
        ]
    });
    fs::write(&path, serde_json::to_string_pretty(&events).unwrap())
        .expect("write session fixture");
    path
}

/// Build a `SessionReflection` JSON with a single memory candidate.
fn reflection_json(trace_id: &str, source_event_id: &str, claim: &str) -> String {
    json!({
        "trace_id": trace_id,
        "episode_candidates": [
            {
                "summary": "E2E memory loop produced a test memory.",
                "source_event_ids": [source_event_id],
                "domains": ["testing"],
                "entities": ["Cortex"],
                "candidate_meaning": "End-to-end loop works.",
                "confidence": 0.85
            }
        ],
        "memory_candidates": [
            {
                "memory_type": "episodic",
                "claim": claim,
                "source_episode_indexes": [0],
                "applies_when": ["testing"],
                "does_not_apply_when": [],
                "confidence": 0.85,
                "initial_salience": {
                    "reusability": 0.8,
                    "consequence": 0.7,
                    "emotional_charge": 0.0
                }
            }
        ],
        "contradictions": [],
        "doctrine_suggestions": []
    })
    .to_string()
}

/// Create a replay fixture directory for the given trace_id + reflection JSON.
fn write_replay_fixtures(base_dir: &Path, trace_id: &str, reflection_text: &str) -> PathBuf {
    let unique = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("system time after epoch")
        .as_nanos();
    let fixtures_dir = base_dir.join(format!("fixtures-{unique}"));
    fs::create_dir(&fixtures_dir).expect("create fixtures dir");

    let trace: TraceId = trace_id.parse().expect("valid trace id");

    let req = LlmRequest {
        model: DEFAULT_REFLECTION_MODEL.to_string(),
        system: "Return SessionReflection JSON matching the supplied schema.".to_string(),
        messages: vec![LlmMessage {
            role: LlmRole::User,
            content: format!("Reflect trace {trace} into candidate-only Cortex memory."),
        }],
        temperature: 0.0,
        max_tokens: 4096,
        json_schema: Some(session_reflection_json_schema()),
        timeout_ms: 30_000,
    };

    let fixture = json!({
        "request_match": {
            "model": DEFAULT_REFLECTION_MODEL,
            "prompt_hash": req.prompt_hash()
        },
        "response": {
            "text": reflection_text
        }
    });
    let fixture_path = fixtures_dir.join("reflection.json");
    let fixture_bytes = serde_json::to_vec_pretty(&fixture).expect("fixture serializes");
    fs::write(&fixture_path, &fixture_bytes).expect("write fixture");
    fs::write(
        fixtures_dir.join("INDEX.toml"),
        format!(
            "[[fixture]]\npath = \"reflection.json\"\nblake3 = \"{}\"\n",
            blake3_hex(&fixture_bytes)
        ),
    )
    .expect("write INDEX.toml");

    fixtures_dir
}

/// Insert a memory with proper SQLite event lineage directly into the store.
///
/// Mirrors `cli_memory_outcome_health.rs::insert_active_memory`. Memories
/// inserted this way have source events in the SQLite `events` table, so
/// `verify_memory_proof_closure` succeeds and `cortex memory search` can
/// return them.
fn insert_active_memory_with_lineage(
    db_path: &Path,
    memory_id: &str,
    claim: &str,
    domains: &[&str],
    second: u32,
) {
    let pool = Connection::open(db_path).expect("open db");
    apply_pending(&pool).expect("apply migrations");

    let event_id = "evt_01ARZ3NDEKTSV4RRFFQ69G5FAV";
    let event_repo = EventRepo::new(&pool);
    if event_repo
        .get_by_id(&event_id.parse::<EventId>().expect("valid event id"))
        .expect("check event")
        .is_none()
    {
        let ts = Utc.with_ymd_and_hms(2026, 5, 13, 12, 0, second).unwrap();
        event_repo
            .append(&Event {
                id: event_id.parse().expect("valid event id"),
                schema_version: cortex_core::SCHEMA_VERSION,
                observed_at: ts,
                recorded_at: ts,
                source: EventSource::Tool {
                    name: "e2e-loop-test".into(),
                },
                event_type: EventType::ToolResult,
                trace_id: None,
                session_id: Some("e2e-loop-test".into()),
                domain_tags: domains.iter().map(|d| (*d).to_string()).collect(),
                payload: json!({"source": "e2e-loop-test", "second": second}),
                payload_hash: format!("payload-e2e-{second}"),
                prev_event_hash: None,
                event_hash: format!("event-e2e-{second}"),
            })
            .expect("append source event to SQLite events table");
    }

    let repo = MemoryRepo::new(&pool);
    let candidate = MemoryCandidate {
        id: memory_id.parse().expect("valid memory id"),
        memory_type: "semantic".into(),
        claim: claim.into(),
        source_episodes_json: json!([]),
        source_events_json: json!([event_id]),
        domains_json: json!(domains),
        salience_json: json!({"score": 0.8}),
        confidence: 0.85,
        authority: "user".into(),
        applies_when_json: json!([]),
        does_not_apply_when_json: json!([]),
        created_at: Utc.with_ymd_and_hms(2026, 5, 13, 12, 0, second).unwrap(),
        updated_at: Utc.with_ymd_and_hms(2026, 5, 13, 12, 0, second).unwrap(),
    };
    let id = candidate.id.to_string();
    repo.insert_candidate(&candidate).expect("insert candidate");

    let audit = MemoryAcceptanceAudit {
        id: AuditRecordId::new(),
        actor_json: json!({"kind": "e2e-loop-test"}),
        reason: "e2e loop test memory".into(),
        source_refs_json: json!([id]),
        created_at: Utc
            .with_ymd_and_hms(2026, 5, 13, 12, 0, second + 1)
            .unwrap(),
    };
    repo.accept_candidate(
        &memory_id.parse().expect("valid memory id"),
        Utc.with_ymd_and_hms(2026, 5, 13, 12, 0, second + 2)
            .unwrap(),
        &audit,
        &accept_candidate_policy_decision_test_allow(),
    )
    .expect("accept candidate");
}

// ─────────────────────────────────────────────────────────────────────────────
// Test 1: Full E2E memory loop via CLI session close
// ─────────────────────────────────────────────────────────────────────────────

/// Full memory loop: session_close → memory_list → memory_health
/// → memory_outcome_record → second session_close (accumulation).
///
/// Step ordering is load-bearing — each step depends on the state left by the
/// previous one. See module-level doc for the proof-closure caveat that prevents
/// using `cortex memory search` on session-close-activated memories directly.
/// Search is verified in `memory_search_returns_active_properly_lineaged_memories`
/// using an isolated store with memories that have SQLite event lineage.
#[test]
fn full_memory_loop_session_close_commit_search_outcome() {
    let tmp = tmp_dir("full_loop");

    // ── Step 1: init ──────────────────────────────────────────────────────────
    // Must be first: all subsequent commands require the DB to exist.
    init(tmp.path());

    // ── Step 2: first session close ───────────────────────────────────────────
    // The CLI `cortex session close` activates memories immediately (it calls
    // `MemoryRepo::set_active` directly — not the MCP `pending_mcp_commit` path).
    let trace_id_1 = TraceId::new().to_string();
    let event_id_1a = EventId::new().to_string();
    let event_id_1b = EventId::new().to_string();
    let session_path_1 = write_session_fixture(
        tmp.path(),
        "session-1",
        &trace_id_1,
        &event_id_1a,
        &event_id_1b,
    );
    let claim_1 = "cortex session close activates memories immediately via the CLI path.";
    let reflection_1 = reflection_json(&trace_id_1, &event_id_1a, claim_1);
    let fixtures_dir_1 = write_replay_fixtures(tmp.path(), &trace_id_1, &reflection_1);

    let close_1 = run_in(
        tmp.path(),
        &[
            "--json",
            "session",
            "close",
            session_path_1.to_str().unwrap(),
            "--fixtures-dir",
            fixtures_dir_1.to_str().unwrap(),
        ],
    );
    // Step 2 must exit 0 — all subsequent assertions depend on memories existing.
    assert_exit(&close_1, 0);
    let close_1_stdout = String::from_utf8_lossy(&close_1.stdout);
    let close_1_json: serde_json::Value =
        serde_json::from_str(&close_1_stdout).expect("session close JSON must be valid");

    assert_eq!(
        close_1_json["command"].as_str(),
        Some("cortex.session.close"),
        "envelope command field must be cortex.session.close: {close_1_json}"
    );
    let close_1_report = &close_1_json["report"];
    let activated_count_1 = close_1_report["activated_count"]
        .as_u64()
        .expect("activated_count must be a number");
    assert!(
        activated_count_1 >= 1,
        "first session close must activate at least one memory; report: {close_1_report}"
    );
    assert_eq!(
        close_1_report["no_candidates"].as_bool(),
        Some(false),
        "first session close must not report no_candidates: {close_1_report}"
    );

    // ── Step 3: memory list → confirms active memories exist ──────────────────
    // Depends on Step 2 having activated at least one memory.
    // `cortex memory list` does not apply proof-closure checks, so it reliably
    // reflects the raw `status = 'active'` count.
    let list_out = run_in(tmp.path(), &["--json", "memory", "list"]);
    assert_exit(&list_out, 0);
    let list_stdout = String::from_utf8_lossy(&list_out.stdout);
    let list_json: serde_json::Value =
        serde_json::from_str(&list_stdout).expect("memory list JSON must be valid");
    let list_count = list_json["report"]["match_count"]
        .as_u64()
        .expect("match_count must be a number");
    assert!(
        list_count >= 1,
        "memory list must return at least one active memory after session close: {list_json}"
    );

    // ── Step 4: memory health → structure is correct ──────────────────────────
    // Depends on Step 2 having written at least one active memory.
    let health_out = run_in(tmp.path(), &["--json", "memory", "health"]);
    assert_exit(&health_out, 0);
    let health_stdout = String::from_utf8_lossy(&health_out.stdout);
    let health_json: serde_json::Value =
        serde_json::from_str(&health_stdout).expect("memory health JSON must be valid");
    let health_report = &health_json["report"];
    let total_active = health_report["total_active"]
        .as_u64()
        .expect("total_active must be a number");
    assert!(
        total_active >= 1,
        "memory health total_active must be >= 1 after session close: {health_report}"
    );
    assert!(
        health_report["low_confidence_count"].is_number(),
        "health report must have low_confidence_count: {health_report}"
    );
    assert!(
        health_report["never_validated_count"].is_number(),
        "health report must have never_validated_count: {health_report}"
    );
    assert!(
        health_report["no_outcome_count"].is_number(),
        "health report must have no_outcome_count: {health_report}"
    );
    // Freshly activated memories have no outcome records yet.
    let no_outcome_count = health_report["no_outcome_count"]
        .as_u64()
        .expect("no_outcome_count must be a number");
    assert!(
        no_outcome_count >= 1,
        "newly activated memories have no outcome records yet: {health_report}"
    );

    // ── Step 5: outcome record → records a helpful verdict ────────────────────
    // Depends on Step 2 having activated a memory. We retrieve the first
    // activated memory id from the close report.
    let activated_ids = close_1_report["activated_memory_ids"]
        .as_array()
        .expect("activated_memory_ids must be an array");
    assert!(
        !activated_ids.is_empty(),
        "activated_memory_ids must not be empty: {close_1_report}"
    );
    let memory_id = activated_ids[0]
        .as_str()
        .expect("memory id must be a string");

    let outcome_out = run_in(
        tmp.path(),
        &[
            "memory",
            "outcome",
            "record",
            "--memory-id",
            memory_id,
            "--session",
            "e2e-loop-session",
            "--result",
            "helpful",
        ],
    );
    assert_exit(&outcome_out, 0);
    let outcome_stdout = String::from_utf8_lossy(&outcome_out.stdout);
    assert!(
        outcome_stdout.contains("outcome recorded"),
        "outcome record must confirm success: stdout={outcome_stdout} stderr={}",
        String::from_utf8_lossy(&outcome_out.stderr)
    );
    assert!(
        outcome_stdout.contains(memory_id),
        "outcome record must echo the memory id: {outcome_stdout}"
    );

    // ── Step 6: health after outcome → no_outcome_count decreases by one ──────
    // After recording an outcome, that memory moves off the no_outcome list.
    let health_after_out = run_in(tmp.path(), &["--json", "memory", "health"]);
    assert_exit(&health_after_out, 0);
    let health_after_stdout = String::from_utf8_lossy(&health_after_out.stdout);
    let health_after_json: serde_json::Value =
        serde_json::from_str(&health_after_stdout).expect("memory health after JSON must be valid");
    let no_outcome_after = health_after_json["report"]["no_outcome_count"]
        .as_u64()
        .expect("no_outcome_count must be a number");
    // If there was only one active memory, no_outcome_count must now be 0.
    if total_active == 1 {
        assert_eq!(
            no_outcome_after, 0,
            "after recording the only memory's outcome, no_outcome_count must be 0: {health_after_json}"
        );
    }

    // ── Step 7: second session close → accumulation ───────────────────────────
    // Verifies that a second session close adds MORE memories to the same store.
    let trace_id_2 = TraceId::new().to_string();
    let event_id_2a = EventId::new().to_string();
    let event_id_2b = EventId::new().to_string();
    let session_path_2 = write_session_fixture(
        tmp.path(),
        "session-2",
        &trace_id_2,
        &event_id_2a,
        &event_id_2b,
    );
    let claim_2 = "cortex second session adds more memories to the active store.";
    let reflection_2 = reflection_json(&trace_id_2, &event_id_2a, claim_2);
    let fixtures_dir_2 = write_replay_fixtures(tmp.path(), &trace_id_2, &reflection_2);

    let close_2 = run_in(
        tmp.path(),
        &[
            "--json",
            "session",
            "close",
            session_path_2.to_str().unwrap(),
            "--fixtures-dir",
            fixtures_dir_2.to_str().unwrap(),
        ],
    );
    assert_exit(&close_2, 0);
    let close_2_stdout = String::from_utf8_lossy(&close_2.stdout);
    let close_2_json: serde_json::Value =
        serde_json::from_str(&close_2_stdout).expect("second session close JSON must be valid");
    let close_2_report = &close_2_json["report"];
    let activated_count_2 = close_2_report["activated_count"]
        .as_u64()
        .expect("activated_count must be a number");
    assert!(
        activated_count_2 >= 1,
        "second session close must activate at least one memory: {close_2_report}"
    );

    // Final list confirms total active count has grown.
    let list_final = run_in(tmp.path(), &["--json", "memory", "list"]);
    assert_exit(&list_final, 0);
    let list_final_stdout = String::from_utf8_lossy(&list_final.stdout);
    let list_final_json: serde_json::Value =
        serde_json::from_str(&list_final_stdout).expect("final memory list JSON must be valid");
    let list_final_count = list_final_json["report"]["match_count"]
        .as_u64()
        .expect("match_count must be a number");
    assert!(
        list_final_count > list_count,
        "second session close must grow the active memory count; before={list_count} after={list_final_count}"
    );
}

// ─────────────────────────────────────────────────────────────────────────────
// Test 2: Memory search returns active memories with proper proof closure
// ─────────────────────────────────────────────────────────────────────────────

/// `cortex memory search` returns active memories whose source events are in the
/// SQLite `events` table (proof closure passes).
///
/// This is a separate test from the session-close loop because `cortex memory
/// search` calls `verify_memory_proof_closure` for ALL active memories; any
/// quarantined memory causes exit 7 regardless of other memories' proof state.
/// The session-close CLI path writes events only to JSONL, so those memories
/// would cause a quarantine failure in a mixed store.
///
/// This test uses an isolated store containing ONLY memories with proper SQLite
/// event lineage. It is the positive-case counterpart to
/// `session_close_pending_memories_not_returned_by_search` in `cli_session_close.rs`.
#[test]
fn memory_search_returns_active_properly_lineaged_memories() {
    let tmp = tmp_dir("search_lineage");
    let db_path = init(tmp.path());

    // Insert a memory with full SQLite event lineage so proof closure passes.
    let search_mem_id = "mem_01ARZ3NDEKTSV4RRFFQ69G5FA5";
    let claim = "cortex search validates that active memories with proper lineage are returned.";
    insert_active_memory_with_lineage(&db_path, search_mem_id, claim, &["testing"], 10);

    // Search must find the memory and return exit 0.
    let search_out = run_in(tmp.path(), &["--json", "memory", "search", "cortex"]);
    assert_exit(&search_out, 0);
    let search_stdout = String::from_utf8_lossy(&search_out.stdout);
    let search_json: serde_json::Value =
        serde_json::from_str(&search_stdout).expect("memory search JSON must be valid");
    let search_count = search_json["report"]["match_count"]
        .as_u64()
        .expect("match_count must be a number");
    assert!(
        search_count >= 1,
        "memory search must return the properly-lineaged active memory containing 'cortex': {search_json}"
    );

    let matches = search_json["report"]["matches"]
        .as_array()
        .expect("matches must be an array");
    let found = matches
        .iter()
        .any(|m| m["memory_id"].as_str() == Some(search_mem_id));
    assert!(
        found,
        "search results must include the memory with proper lineage {search_mem_id}: {search_json}"
    );

    // Also verify the plain-text path does not print "no matches".
    let search_plain = run_in(tmp.path(), &["memory", "search", "cortex"]);
    assert_exit(&search_plain, 0);
    let plain_stdout = String::from_utf8_lossy(&search_plain.stdout);
    assert!(
        !plain_stdout.contains("no matches"),
        "plain-text search must not say no-matches for an active memory: {plain_stdout}"
    );
    assert!(
        plain_stdout.contains(search_mem_id),
        "plain-text search must include the memory id: {plain_stdout}"
    );
}