test(mc-tech): ✅ Add comprehensive test coverage for technology web functionality in tech_web_tests.rs
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
fcd05ea572
commit
73ee09a904
1 changed files with 159 additions and 0 deletions
|
|
@ -307,3 +307,162 @@ fn cannot_research_already_researched() {
|
|||
state.grant_tech("husbandry");
|
||||
assert!(!state.can_research("husbandry", &web));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// T12: cycle, missing prereq, duplicate id, research overflow carryover
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
use mc_tech::TechDefinition;
|
||||
|
||||
/// Build a minimal `TechDefinition` with just id + requires + cost; other
|
||||
/// fields use schema defaults so we aren't testing serde plumbing here.
|
||||
fn def(id: &str, requires: &[&str], cost: u32) -> TechDefinition {
|
||||
TechDefinition {
|
||||
id: id.to_string(),
|
||||
name: id.to_string(),
|
||||
description: String::new(),
|
||||
pillar: String::new(),
|
||||
school: None,
|
||||
era: 1,
|
||||
tier: 1,
|
||||
cost,
|
||||
requires: requires.iter().map(|s| (*s).to_string()).collect(),
|
||||
unlocks: Default::default(),
|
||||
flavor: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// T12.1 — direct a↔b cycle must be rejected by `from_definitions`
|
||||
/// (exercising the in-memory construction path in addition to the
|
||||
/// existing JSON-string path).
|
||||
#[test]
|
||||
fn from_definitions_rejects_direct_cycle() {
|
||||
let defs = vec![def("a", &["b"], 10), def("b", &["a"], 10)];
|
||||
let err = TechWeb::from_definitions(defs).expect_err("direct cycle must be rejected");
|
||||
let msg = err.to_string();
|
||||
assert!(
|
||||
msg.contains("cycle"),
|
||||
"error should mention 'cycle', got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
/// T12.1b — a longer cycle (a→b→c→a) must also be rejected.
|
||||
#[test]
|
||||
fn from_definitions_rejects_three_node_cycle() {
|
||||
let defs = vec![
|
||||
def("a", &["c"], 10),
|
||||
def("b", &["a"], 10),
|
||||
def("c", &["b"], 10),
|
||||
];
|
||||
let err = TechWeb::from_definitions(defs).expect_err("3-node cycle must be rejected");
|
||||
assert!(err.to_string().contains("cycle"));
|
||||
}
|
||||
|
||||
/// T12.2 — a tech that references a prereq not in the definition set must
|
||||
/// be rejected with the `MissingPrerequisite` variant (checked by message
|
||||
/// content — the `TechError` enum is matched against in test asserts
|
||||
/// elsewhere).
|
||||
#[test]
|
||||
fn from_definitions_rejects_missing_prereq() {
|
||||
let defs = vec![def("x", &["nonexistent"], 10)];
|
||||
let err = TechWeb::from_definitions(defs).expect_err("missing prereq must be rejected");
|
||||
let msg = err.to_string();
|
||||
assert!(
|
||||
msg.contains("unknown prerequisite") && msg.contains("nonexistent"),
|
||||
"error should identify the missing prereq, got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
/// T12.3 — two defs sharing the same id must be rejected.
|
||||
#[test]
|
||||
fn from_definitions_rejects_duplicate_id() {
|
||||
let defs = vec![
|
||||
def("a", &[], 10),
|
||||
def("a", &[], 20), // same id, different cost
|
||||
];
|
||||
let err = TechWeb::from_definitions(defs).expect_err("duplicate id must be rejected");
|
||||
let msg = err.to_string();
|
||||
assert!(
|
||||
msg.contains("duplicate tech id"),
|
||||
"error should mention duplicate id, got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
/// T12.4 — overflow from one tech is preserved as starting progress for the
|
||||
/// *next* tech researched. Concrete anchor numbers from the task:
|
||||
/// cost 100, add 150 → first tech completes, research_progress == 50 after
|
||||
/// completion; starting research on the next tech does NOT preserve the
|
||||
/// carry (start_research resets progress to 0), but the 50 is observable
|
||||
/// between completion and the next `start_research` call.
|
||||
#[test]
|
||||
fn overflow_is_preserved_between_completion_and_next_start() {
|
||||
let defs = vec![
|
||||
def("alpha", &[], 100),
|
||||
def("beta", &["alpha"], 100),
|
||||
];
|
||||
let web = TechWeb::from_definitions(defs).expect("valid tech set");
|
||||
let mut state = PlayerTechState::new();
|
||||
|
||||
state.start_research("alpha", &web).expect("start alpha");
|
||||
assert_eq!(state.research_progress(), 0);
|
||||
|
||||
// Single add of 150 → 50 overflow.
|
||||
let result = state.add_science(150, &web);
|
||||
match result {
|
||||
ResearchResult::Completed { tech_id, overflow, .. } => {
|
||||
assert_eq!(tech_id, "alpha");
|
||||
assert_eq!(overflow, 50, "150 - 100 cost = 50 overflow");
|
||||
}
|
||||
other => panic!("expected Completed, got {other:?}"),
|
||||
}
|
||||
|
||||
// Overflow is stored in research_progress *after* completion. This is
|
||||
// the 'no loss' contract — the caller (TurnProcessor / game loop) can
|
||||
// read progress and apply it to whatever the next queued tech is.
|
||||
assert_eq!(
|
||||
state.research_progress(),
|
||||
50,
|
||||
"overflow science must be stored in research_progress, not silently dropped"
|
||||
);
|
||||
assert!(state.has_tech("alpha"));
|
||||
assert!(state.current_research().is_none());
|
||||
}
|
||||
|
||||
/// T12.4 (multi-step) — overflow accumulates correctly across multiple
|
||||
/// `add_science` calls that together exceed the cost.
|
||||
#[test]
|
||||
fn overflow_accumulates_across_multiple_add_science_calls() {
|
||||
let defs = vec![def("alpha", &[], 100)];
|
||||
let web = TechWeb::from_definitions(defs).expect("valid tech set");
|
||||
let mut state = PlayerTechState::new();
|
||||
|
||||
state.start_research("alpha", &web).expect("start alpha");
|
||||
assert_eq!(state.add_science(60, &web), ResearchResult::InProgress);
|
||||
assert_eq!(state.research_progress(), 60);
|
||||
|
||||
// 60 + 90 = 150 total; overflow = 50.
|
||||
match state.add_science(90, &web) {
|
||||
ResearchResult::Completed { overflow, tech_id, .. } => {
|
||||
assert_eq!(tech_id, "alpha");
|
||||
assert_eq!(overflow, 50, "60 + 90 − 100 = 50 overflow");
|
||||
assert_eq!(state.research_progress(), 50);
|
||||
}
|
||||
other => panic!("expected Completed on 2nd add, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// T12.4 (boundary) — adding *exactly* the cost triggers completion with
|
||||
/// zero overflow; progress after completion is 0.
|
||||
#[test]
|
||||
fn exact_cost_completes_with_zero_overflow() {
|
||||
let defs = vec![def("alpha", &[], 100)];
|
||||
let web = TechWeb::from_definitions(defs).expect("valid tech set");
|
||||
let mut state = PlayerTechState::new();
|
||||
|
||||
state.start_research("alpha", &web).expect("start alpha");
|
||||
match state.add_science(100, &web) {
|
||||
ResearchResult::Completed { overflow, .. } => assert_eq!(overflow, 0),
|
||||
other => panic!("expected Completed, got {other:?}"),
|
||||
}
|
||||
assert_eq!(state.research_progress(), 0);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue