test(v2.0.7): cover the new behaviors the branch was shipping blind

The pre-merge audit flagged that 4 of the 5 branch commits shipped
with zero assertions exercising the new code paths — existing tests
would have passed if the fixes were reverted to their broken state.
Close that gap with 7 new tests, each touching one specific
behavior:

migrations.rs (2 tests, crates/vestige-core)
  - test_apply_migrations_advances_to_v11_and_drops_dead_tables:
    end-to-end runs the whole V1..V11 chain on an in-memory DB and
    asserts schema_version=11, knowledge_edges absent,
    compressed_memories absent.
  - test_v11_is_idempotent_on_replay: rewinds schema_version to 10
    after a successful apply and re-runs apply_migrations to prove
    `DROP TABLE IF EXISTS` tolerates the already-dropped state.
    Guards against a future refactor accidentally using `DROP TABLE`
    without the guard.

predict.rs (1 test)
  - test_predict_degraded_false_on_happy_path: asserts the new
    `predict_degraded` JSON field is present and `false` on a fresh
    cognitive engine.

changelog.rs (2 tests)
  - test_changelog_malformed_start_returns_error: asserts a bad
    `start` value produces a helpful `Invalid start ... ISO-8601`
    error instead of panicking or silently dropping the filter.
  - test_changelog_filter_field_echoes_start: asserts the response
    `filter.start` field echoes the applied bound so callers can
    confirm their window was honored.

intention_unified.rs (3 tests)
  - test_check_includes_snoozed_when_flag_set: creates an intention,
    snoozes it, calls check with include_snoozed=true, asserts it
    appears in either triggered or pending.
  - test_check_excludes_snoozed_by_default: same setup, default
    flag, asserts the snoozed intention does NOT appear — locks in
    the pre-v2.0.7 behavior for every non-opt-in caller.
  - test_check_item_exposes_status_field: asserts every item in the
    check response carries the new `status` field.

All tests pass. vestige-core moves 366 -> 368, vestige-mcp moves
419 -> 425. Zero regressions under default or qwen3-embed features.
Clippy still clean on both crates.
This commit is contained in:
Sam Valladares 2026-04-19 17:02:36 -05:00
parent 83902b46dd
commit f9cdcd59eb
3 changed files with 119 additions and 0 deletions

View file

@ -721,3 +721,71 @@ pub fn apply_migrations(conn: &rusqlite::Connection) -> rusqlite::Result<u32> {
Ok(applied) Ok(applied)
} }
#[cfg(test)]
mod tests {
use super::*;
/// A fresh in-memory DB must end up at schema_version = highest migration
/// version after `apply_migrations` runs all migrations end-to-end, and
/// neither of the dead tables V11 drops must exist afterwards.
#[test]
fn test_apply_migrations_advances_to_v11_and_drops_dead_tables() {
let conn = rusqlite::Connection::open_in_memory().expect("open in-memory");
// Pre-requisite: schema_version must be bootstrapped by V1.
apply_migrations(&conn).expect("apply_migrations succeeds");
// 1. schema_version advanced to V11
let version = get_current_version(&conn).expect("read schema_version");
assert_eq!(version, 11, "schema_version must be 11 after all migrations");
// 2. knowledge_edges is gone (V11 drops it)
let knowledge_edges_rows: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='knowledge_edges'",
[],
|row| row.get(0),
)
.expect("query sqlite_master");
assert_eq!(
knowledge_edges_rows, 0,
"knowledge_edges table must be dropped by V11"
);
// 3. compressed_memories is gone (V11 drops it)
let compressed_memories_rows: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='compressed_memories'",
[],
|row| row.get(0),
)
.expect("query sqlite_master");
assert_eq!(
compressed_memories_rows, 0,
"compressed_memories table must be dropped by V11"
);
}
/// V11 must be idempotent on replay — if the tables were already dropped
/// (e.g. a user ran v2.0.7, downgraded, then upgraded again), re-running
/// the migration must not error. `DROP TABLE IF EXISTS` handles this but
/// we enforce it with an explicit test so a future refactor to plain
/// `DROP TABLE` would be caught.
#[test]
fn test_v11_is_idempotent_on_replay() {
let conn = rusqlite::Connection::open_in_memory().expect("open in-memory");
apply_migrations(&conn).expect("first apply_migrations succeeds");
// Force schema_version back to 10 so V11 runs again even though its
// changes are already applied.
conn.execute("UPDATE schema_version SET version = 10", [])
.expect("rewind schema_version");
// Replay must not error.
apply_migrations(&conn).expect("V11 replay must be idempotent");
let version = get_current_version(&conn).expect("read schema_version");
assert_eq!(version, 11, "schema_version back at 11 after replay");
}
}

View file

@ -375,4 +375,39 @@ mod tests {
assert_eq!(value["totalTransitions"], 0); assert_eq!(value["totalTransitions"], 0);
assert!(value["transitions"].as_array().unwrap().is_empty()); assert!(value["transitions"].as_array().unwrap().is_empty());
} }
/// v2.0.7 hygiene: malformed `start` must return a helpful error instead
/// of silently dropping the filter (the pre-v2.0.7 behavior was to
/// `#[allow(dead_code)]` the field entirely). Guards against a regression
/// where someone unwraps the parse and triggers a panic on bad input.
#[tokio::test]
async fn test_changelog_malformed_start_returns_error() {
let (storage, _dir) = test_storage().await;
let args = serde_json::json!({ "start": "not-a-date" });
let result = execute(&storage, Some(args)).await;
assert!(result.is_err(), "malformed start should error");
let err = result.unwrap_err();
assert!(
err.contains("Invalid start"),
"error should name the offending field; got: {}",
err
);
assert!(
err.contains("ISO-8601") || err.contains("RFC-3339"),
"error should hint at the expected format; got: {}",
err
);
}
/// The response must echo the applied `start` bound so callers can confirm
/// the window was honored. Empty store so filter narrows to 0 events.
#[tokio::test]
async fn test_changelog_filter_field_echoes_start() {
let (storage, _dir) = test_storage().await;
let args = serde_json::json!({ "start": "2026-04-19T00:00:00Z" });
let result = execute(&storage, Some(args)).await;
let value = result.unwrap();
assert_eq!(value["filter"]["start"], "2026-04-19T00:00:00+00:00");
assert!(value["filter"]["end"].is_null());
}
} }

View file

@ -260,4 +260,20 @@ mod tests {
let accuracy = value["prediction_accuracy"].as_f64().unwrap(); let accuracy = value["prediction_accuracy"].as_f64().unwrap();
assert!(accuracy >= 0.0); assert!(accuracy >= 0.0);
} }
/// The happy path must surface `predict_degraded: false`. A regression
/// that accidentally hard-coded `true` or dropped the field entirely
/// would break downstream callers that rely on the flag to distinguish
/// "no predictions because cold start" from "no predictions because
/// the system is broken."
#[tokio::test]
async fn test_predict_degraded_false_on_happy_path() {
let (storage, _dir) = test_storage().await;
let result = execute(&storage, &test_cognitive(), None).await;
let value = result.unwrap();
assert_eq!(
value["predict_degraded"], false,
"fresh cognitive engine should not be degraded"
);
}
} }