remem-ai 0.5.145

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use serde_json::Value;

use super::*;

fn status_report_fixture() -> StatusReport {
    StatusReport {
        version: "0.4.5".to_string(),
        database: StatusDatabase {
            path: "/tmp/remem-test".to_string(),
            size_bytes: 1_048_576,
            size_mb: 1.0,
        },
        totals: StatusTotals {
            memories: 1,
            observations: 2,
            sessions: 3,
            raw_messages: 4,
        },
        raw_archive: RawArchiveStatus {
            messages: 4,
            ingest_failures: 0,
            parse_errors: 0,
            insert_errors: 0,
            latest_failure_epoch: None,
            latest_failure_age_secs: None,
            latest_failure_kind: None,
            latest_failure_path: None,
            latest_failure_message: None,
        },
        capture_pipeline: CapturePipelineStatus {
            captured: 5,
            dropped: 0,
            unrecovered_spills: 0,
            latest_drop_epoch: None,
            latest_drop_age_secs: None,
            latest_drop_reason: None,
            latest_drop_detail: None,
            extract_todo: 6,
            extract_running: 7,
            extract_expired: 0,
            extract_failed: 0,
            retryable_replay_ranges: 0,
            active_replay_ranges: 0,
            quarantined_replay_ranges: 0,
            pending_candidates: 9,
            pending_graph_candidates: 10,
            oldest_task_epoch: Some(10),
            oldest_task_age_secs: Some(11),
        },
        promotion_funnel: PromotionFunnelStatus {
            captured_events: 5,
            observations: 4,
            observation_rate_percent: 80.0,
            candidates: 3,
            candidate_rate_percent: 75.0,
            promoted: 2,
            promoted_rate_percent: 66.66666666666667,
            pending_review: 1,
            pending_review_rate_percent: 33.333333333333336,
        },
        usage_feedback: UsageFeedbackStatus {
            citation_events: 7,
            citation_line_present_events: 5,
            citation_line_present_rate_percent: 71.42857142857143,
            matched_events: 4,
            match_rate_percent: 80.0,
            inserted_events: 4,
            no_citation_events: 2,
            unmatched_events: 1,
            usage_events: 6,
        },
        pending_observations: PendingObservationStatus {
            ready: 12,
            delayed: 13,
            processing: 14,
            expired: 0,
            failed: 0,
            oldest_ready_epoch: Some(17),
            oldest_ready_age_secs: Some(18),
        },
        candidate_promotion: vec![CandidatePromotionStatus {
            source_kind: "summary".to_string(),
            review_status: "pending_review".to_string(),
            block_reason: Some("summary_gate_shadow".to_string()),
            total: 41,
            last_7_days: 6,
        }],
        jobs: JobStatus {
            pending: 19,
            processing: 20,
            failed: 0,
            stuck: 0,
        },
        worker_daemon: WorkerDaemonStatus {
            health: "healthy".to_string(),
            heartbeat_age_secs: Some(23),
            owner: Some("worker-1".to_string()),
        },
        latest_session_memory_spend: Some(LatestSessionMemorySpendStatus {
            session_id: "sess-1".to_string(),
            project: "/tmp/remem".to_string(),
            latest_context_epoch: 1_800_000_000,
            context_rows: 2,
            context_output_chars: 3_201,
            context_estimated_tokens: 801,
            context_emit_count: 3,
            context_suppress_count: 1,
            ai_usage_attribution: "partial".to_string(),
            ai_calls: 2,
            ai_total_tokens: 1_234,
            ai_estimated_cost_usd: 0.0123,
            ai_unattributed_legacy_calls: 1,
        }),
        today: DailyStatus {
            new_memories: 24,
            new_observations: 25,
        },
        top_projects: vec![TopProjectStatus {
            project: "proj".to_string(),
            count: 26,
        }],
    }
}

#[test]
fn cli_status_json_report_is_machine_parseable() -> std::result::Result<(), serde_json::Error> {
    let mut report = status_report_fixture();
    report.raw_archive.ingest_failures = 1;
    report.raw_archive.parse_errors = 2;
    report.raw_archive.insert_errors = 3;
    report.raw_archive.latest_failure_kind = Some("mixed_errors".to_string());
    report.raw_archive.latest_failure_path = Some("/bad/raw.jsonl".to_string());
    report.pending_observations.expired = 15;
    report.pending_observations.failed = 16;
    report.jobs.failed = 21;
    report.jobs.stuck = 22;

    let text = serde_json::to_string(&report)?;
    let parsed: Value = serde_json::from_str(&text)?;

    assert_eq!(parsed["version"], "0.4.5");
    assert_eq!(parsed["database"]["size_bytes"], 1_048_576);
    assert_eq!(parsed["totals"]["memories"], 1);
    assert_eq!(parsed["raw_archive"]["messages"], 4);
    assert_eq!(parsed["raw_archive"]["ingest_failures"], 1);
    assert_eq!(parsed["raw_archive"]["parse_errors"], 2);
    assert_eq!(parsed["raw_archive"]["insert_errors"], 3);
    assert_eq!(parsed["raw_archive"]["latest_failure_kind"], "mixed_errors");
    assert_eq!(
        parsed["raw_archive"]["latest_failure_path"],
        "/bad/raw.jsonl"
    );
    assert_eq!(parsed["capture_pipeline"]["extract_todo"], 6);
    assert_eq!(parsed["capture_pipeline"]["pending_graph_candidates"], 10);
    assert_eq!(parsed["promotion_funnel"]["captured_events"], 5);
    assert_eq!(parsed["promotion_funnel"]["observations"], 4);
    assert_eq!(parsed["promotion_funnel"]["candidates"], 3);
    assert_eq!(parsed["promotion_funnel"]["promoted"], 2);
    assert_eq!(parsed["promotion_funnel"]["pending_review"], 1);
    assert_eq!(parsed["usage_feedback"]["citation_events"], 7);
    assert_eq!(parsed["usage_feedback"]["citation_line_present_events"], 5);
    assert_eq!(parsed["usage_feedback"]["matched_events"], 4);
    assert_eq!(parsed["usage_feedback"]["no_citation_events"], 2);
    assert_eq!(parsed["usage_feedback"]["unmatched_events"], 1);
    assert_eq!(parsed["usage_feedback"]["usage_events"], 6);
    assert_eq!(parsed["pending_observations"]["failed"], 16);
    assert_eq!(parsed["candidate_promotion"][0]["source_kind"], "summary");
    assert_eq!(
        parsed["candidate_promotion"][0]["review_status"],
        "pending_review"
    );
    assert_eq!(
        parsed["candidate_promotion"][0]["block_reason"],
        "summary_gate_shadow"
    );
    assert_eq!(parsed["candidate_promotion"][0]["total"], 41);
    assert_eq!(parsed["candidate_promotion"][0]["last_7_days"], 6);
    assert_eq!(parsed["worker_daemon"]["health"], "healthy");
    assert_eq!(
        parsed["latest_session_memory_spend"]["session_id"],
        "sess-1"
    );
    assert_eq!(
        parsed["latest_session_memory_spend"]["context_estimated_tokens"],
        801
    );
    assert_eq!(
        parsed["latest_session_memory_spend"]["ai_usage_attribution"],
        "partial"
    );
    assert_eq!(
        parsed["latest_session_memory_spend"]["ai_unattributed_legacy_calls"],
        1
    );
    assert_eq!(
        parsed["latest_session_memory_spend"]["ai_total_tokens"],
        1234
    );
    assert_eq!(parsed["top_projects"][0]["project"], "proj");
    Ok(())
}

#[test]
fn cli_status_has_no_action_block_when_runtime_is_clear() {
    let report = status_report_fixture();
    let actions = status_health_actions(&report);

    assert!(render_action_block(&actions).is_empty());
}

#[test]
fn cli_status_renders_action_block_for_runtime_failures() {
    let mut report = status_report_fixture();
    report.pending_observations.failed = 43;
    report.pending_observations.expired = 1;
    report.capture_pipeline.extract_failed = 4;
    report.jobs.failed = 2;
    report.jobs.stuck = 3;

    let actions = status_health_actions(&report);
    let text = render_action_block(&actions);

    assert!(text.contains("Needs attention:"));
    assert!(text.contains("43 failed pending observations"));
    assert!(text.contains("inspect: remem pending list-failed --limit 20"));
    assert!(text.contains("preview retry: remem pending retry-failed --dry-run"));
    assert!(text.contains("1 expired processing pending observation"));
    assert!(text.contains("4 failed extraction tasks"));
    assert!(text.contains("2 failed jobs"));
    assert!(text.contains("3 stuck jobs"));
    assert!(text.contains("inspect counts: remem status --json"));
    assert!(text.contains("recover: remem worker --once"));
}