test: add contract tests for CLI output parsing
Implement contract testing to catch CLI schema drift early: Contract Test Suite (fixture_contract_tests.rs): - parse_lore_me_empty_fixture: Validates LoreMeResponse on empty data - parse_lore_me_with_activity_fixture: Real lore output with activity - parse_br_list_empty_fixture: Empty beads list - parse_br_list_with_beads_fixture: Real br output with beads Fixtures (captured from real CLI output): - fixtures/lore/me_empty.json: Synthetic empty response - fixtures/lore/me_with_activity.json: Real 'lore --robot me' output - fixtures/br/list_empty.json: Empty array [] - fixtures/br/list_with_beads.json: Real 'br list --json' output - fixtures/br/bv_triage.json: Real 'bv --robot-triage' output Fixture Regeneration: - scripts/regenerate-fixtures.sh: Captures fresh CLI output - Run periodically to update fixtures - CI can diff against committed fixtures to detect drift Why Contract Tests Matter: MC depends on external CLIs (lore, br, bv) whose output format may change. Contract tests fail fast when our Rust types diverge from actual CLI output, preventing runtime deserialization errors. The tests use include_str!() for compile-time fixture embedding, ensuring tests fail to compile if fixtures are missing.
This commit is contained in:
31
scripts/regenerate-fixtures.sh
Executable file
31
scripts/regenerate-fixtures.sh
Executable file
@@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Regenerate CLI output fixtures from real commands
|
||||||
|
#
|
||||||
|
# Run this periodically to ensure fixtures match actual CLI output format.
|
||||||
|
# CI can compare fresh fixtures against committed ones to detect schema drift.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
FIXTURE_DIR="src-tauri/tests/fixtures"
|
||||||
|
|
||||||
|
echo "Regenerating CLI fixtures..."
|
||||||
|
|
||||||
|
# lore fixtures
|
||||||
|
echo " Capturing lore --robot me..."
|
||||||
|
lore --robot me > "${FIXTURE_DIR}/lore/me_with_activity.json" 2>/dev/null || {
|
||||||
|
echo " Warning: lore command failed, skipping"
|
||||||
|
}
|
||||||
|
|
||||||
|
# br fixtures
|
||||||
|
echo " Capturing br list..."
|
||||||
|
br list --json > "${FIXTURE_DIR}/br/list_with_beads.json" 2>/dev/null || {
|
||||||
|
echo " Warning: br command failed, skipping"
|
||||||
|
}
|
||||||
|
|
||||||
|
# bv fixtures
|
||||||
|
echo " Capturing bv --robot-triage..."
|
||||||
|
bv --robot-triage > "${FIXTURE_DIR}/br/bv_triage.json" 2>/dev/null || {
|
||||||
|
echo " Warning: bv command failed, skipping"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Done! Review changes with: git diff ${FIXTURE_DIR}"
|
||||||
65
src-tauri/tests/fixture_contract_tests.rs
Normal file
65
src-tauri/tests/fixture_contract_tests.rs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
//! Contract tests to verify our types can parse real CLI outputs
|
||||||
|
//!
|
||||||
|
//! These tests use fixtures captured from actual CLI commands to ensure
|
||||||
|
//! our Rust types stay in sync with the CLI output format.
|
||||||
|
|
||||||
|
use mission_control_lib::data::lore::LoreMeResponse;
|
||||||
|
use mission_control_lib::data::beads::Bead;
|
||||||
|
|
||||||
|
/// Test that we can deserialize empty lore response
|
||||||
|
#[test]
|
||||||
|
fn parse_lore_me_empty_fixture() {
|
||||||
|
let fixture = include_str!("fixtures/lore/me_empty.json");
|
||||||
|
let result: Result<LoreMeResponse, _> = serde_json::from_str(fixture);
|
||||||
|
|
||||||
|
assert!(result.is_ok(), "Failed to parse me_empty.json: {:?}", result.err());
|
||||||
|
let response = result.unwrap();
|
||||||
|
assert!(response.ok);
|
||||||
|
assert!(response.data.open_issues.is_empty());
|
||||||
|
assert!(response.data.open_mrs_authored.is_empty());
|
||||||
|
assert!(response.data.reviewing_mrs.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that we can deserialize real lore response with activity
|
||||||
|
///
|
||||||
|
/// This test may fail if the fixture format changes. Run:
|
||||||
|
/// ./scripts/regenerate-fixtures.sh
|
||||||
|
/// to update the fixtures if the CLI output format changes.
|
||||||
|
#[test]
|
||||||
|
fn parse_lore_me_with_activity_fixture() {
|
||||||
|
let fixture = include_str!("fixtures/lore/me_with_activity.json");
|
||||||
|
let result: Result<LoreMeResponse, _> = serde_json::from_str(fixture);
|
||||||
|
|
||||||
|
// If this fails, run: ./scripts/regenerate-fixtures.sh
|
||||||
|
assert!(result.is_ok(), "Failed to parse me_with_activity.json: {:?}", result.err());
|
||||||
|
let response = result.unwrap();
|
||||||
|
assert!(response.ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that we can deserialize empty beads list
|
||||||
|
#[test]
|
||||||
|
fn parse_br_list_empty_fixture() {
|
||||||
|
let fixture = include_str!("fixtures/br/list_empty.json");
|
||||||
|
let result: Result<Vec<Bead>, _> = serde_json::from_str(fixture);
|
||||||
|
|
||||||
|
assert!(result.is_ok(), "Failed to parse list_empty.json: {:?}", result.err());
|
||||||
|
assert!(result.unwrap().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that we can deserialize real beads list with entries
|
||||||
|
#[test]
|
||||||
|
fn parse_br_list_with_beads_fixture() {
|
||||||
|
let fixture = include_str!("fixtures/br/list_with_beads.json");
|
||||||
|
let result: Result<Vec<Bead>, _> = serde_json::from_str(fixture);
|
||||||
|
|
||||||
|
// If this fails, run: ./scripts/regenerate-fixtures.sh
|
||||||
|
assert!(result.is_ok(), "Failed to parse list_with_beads.json: {:?}", result.err());
|
||||||
|
let beads = result.unwrap();
|
||||||
|
assert!(!beads.is_empty());
|
||||||
|
|
||||||
|
// Verify first bead has expected fields
|
||||||
|
let first = &beads[0];
|
||||||
|
assert!(!first.id.is_empty());
|
||||||
|
assert!(!first.title.is_empty());
|
||||||
|
assert!(!first.status.is_empty());
|
||||||
|
}
|
||||||
1
src-tauri/tests/fixtures/br/bv_triage.json
vendored
Normal file
1
src-tauri/tests/fixtures/br/bv_triage.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src-tauri/tests/fixtures/br/list_empty.json
vendored
Normal file
1
src-tauri/tests/fixtures/br/list_empty.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
992
src-tauri/tests/fixtures/br/list_with_beads.json
vendored
Normal file
992
src-tauri/tests/fixtures/br/list_with_beads.json
vendored
Normal file
File diff suppressed because one or more lines are too long
10
src-tauri/tests/fixtures/lore/me_empty.json
vendored
Normal file
10
src-tauri/tests/fixtures/lore/me_empty.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"data": {
|
||||||
|
"open_issues": [],
|
||||||
|
"open_mrs_authored": [],
|
||||||
|
"reviewing_mrs": [],
|
||||||
|
"activity": [],
|
||||||
|
"since_last_check": null
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src-tauri/tests/fixtures/lore/me_with_activity.json
vendored
Normal file
1
src-tauri/tests/fixtures/lore/me_with_activity.json
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user