use std::cell::Cell;
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::process::{Child, ChildStdin, Command, Stdio};
use std::sync::{Arc, Mutex, mpsc};
use std::time::{Duration, Instant};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use serde_json::{Value, json};
use tempfile::TempDir;
use tix::app::{App, Mode};
use tix::capacity::calculate::{CapacityCalculation, CapacityScope};
use tix::capacity::scope::IssueSnapshot;
use tix::capacity::{CapacityScopeKind, schedule_index};
use tix::config::Config;
use tix::model::Issue;
use tix::ui::schedule::ScheduleTab;
const BOARD: &str = "[kanban.default]
columns = [\"todo\", \"done\"]
done_column = \"done\"
category_map = { todo = \"unstarted\", done = \"completed\" }
[kanban.default.wip.units]
d = 1
w = 5
[teams.Platform]
members = [\"ada\"]
";
const SCHEDULE: &str = "
[cycle_schedules.sprint]
name = \"Weekly sprint\"
cadence = { kind = \"weekly\" }
duration = { count = 1, unit = \"weeks\" }
start_date = \"2026-01-05\"
timezone = \"UTC\"
look_ahead = 1
";
const INSTANCE_NAME: &str = "Weekly sprint 2026-01-05 000001";
const INSTANCE_ID: &str = "sprint:000001";
fn tix_bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_tix"))
}
fn capacity_entry(reference: &str, value: i64, unit: &str) -> String {
format!(
"\n[capacity.cycle-sprint-000001]\ncycle = \"{reference}\"\ncapacity = {value}\n\
unit = \"{unit}\"\n"
)
}
struct Project {
dir: TempDir,
}
impl Project {
fn new(capacity: &str) -> Self {
Self::with_board_and_capacity(BOARD, capacity)
}
fn with_board(board: &str) -> Self {
Self::with_board_and_capacity(board, "")
}
fn with_board_and_capacity(board: &str, capacity: &str) -> Self {
let dir = TempDir::new().unwrap();
let issues = dir.path().join("issues");
fs::create_dir_all(&issues).unwrap();
fs::write(
dir.path().join("tix.toml"),
format!("issues_dir = {issues:?}\n{board}{SCHEDULE}{capacity}"),
)
.unwrap();
Self { dir }
}
fn root(&self) -> &Path {
self.dir.path()
}
fn config(&self) -> Config {
Config::load(self.root()).expect("the fixture loads")
}
fn app(&self) -> App {
App::new(&self.config()).expect("the TUI opens")
}
fn generate(&self) -> String {
let _ = stdout(
self.root(),
&["cycle", "schedule", "generate", "sprint", "--confirm"],
);
INSTANCE_ID.to_string()
}
fn set(&self, id: &str, value: i64, unit: &str) {
let _ = stdout(
self.root(),
&[
"capacity",
"set",
"--scope",
"cycle",
"--id",
id,
"--value",
&value.to_string(),
"--unit",
unit,
],
);
}
fn set_team(&self, id: &str, value: i64, unit: &str) {
let _ = stdout(
self.root(),
&[
"capacity",
"set",
"--scope",
"team",
"--id",
id,
"--value",
&value.to_string(),
"--unit",
unit,
],
);
}
fn create(&self, title: &str, estimate: &str, status: &str) {
let _ = stdout(
self.root(),
&[
"create",
"--title",
title,
"--estimate",
estimate,
"--cycles",
INSTANCE_NAME,
"--assignee",
"ada",
"--status",
status,
],
);
}
fn assembled(capacity: &str) -> Self {
let project = Self::new(capacity);
let record = tix::cycle_store::CycleRecord {
name: INSTANCE_NAME.to_string(),
start_date: Some("2026-01-05".to_string()),
due_date: Some("2026-01-12".to_string()),
schedule_id: Some("sprint".to_string()),
sequence: Some(1),
state: Some("active".to_string()),
..tix::cycle_store::CycleRecord::default()
};
tix::cycle_store::CycleStore::new(project.root())
.save_record(&record, Some("The weekly instance.\n"))
.expect("the generated cycle is stored");
project
}
fn write_issue(&self, number: u32, status: &str, extra: &str) {
fs::write(
self.root().join(format!("issues/2026-01-01-issue-{number}.md")),
format!(
"---\nnumber: {number}\ntitle: \"Issue {number}\"\nstatus: \"{status}\"\n\
labels: []\npriority: \"medium\"\ncreated: \"2026-01-01\"\nupdated: \"2026-01-01\"\n\
{extra}---\n\nBody.\n"
),
)
.unwrap();
}
fn close(&self, id: &str) {
let _ = stdout(
self.root(),
&[
"cycle",
"schedule",
"close",
"sprint",
"--cycle",
id,
"--reason",
"iteration complete",
"--confirm",
],
);
}
fn reopen(&self, id: &str) {
let _ = stdout(
self.root(),
&[
"cycle",
"schedule",
"reopen",
"sprint",
"--cycle",
id,
"--reason",
"correction",
"--confirm",
],
);
}
fn snapshot_path(&self) -> PathBuf {
self.root().join("docs/cycle_snapshots/sprint.000001.md")
}
fn snapshot_bytes(&self) -> Vec<u8> {
fs::read(self.snapshot_path()).expect("the close wrote its snapshot")
}
}
fn run(dir: &Path, args: &[&str]) -> (i32, String, String) {
let out = Command::new(tix_bin())
.args(args)
.current_dir(dir)
.output()
.unwrap_or_else(|error| panic!("spawn tix {args:?}: {error}"));
(
out.status.code().unwrap_or(-1),
String::from_utf8(out.stdout).unwrap(),
String::from_utf8(out.stderr).unwrap(),
)
}
fn stdout(dir: &Path, args: &[&str]) -> String {
let (code, out, err) = run(dir, args);
assert_eq!(code, 0, "tix {args:?} failed: {err}");
out
}
fn calculate_cli(project: &Project, args: &[&str]) -> BTreeMap<String, String> {
fields(&stdout(project.root(), args))
}
fn fields(text: &str) -> BTreeMap<String, String> {
text.lines()
.filter_map(|line| line.split_once(": "))
.map(|(key, value)| (key.to_string(), value.to_string()))
.collect()
}
fn cli_calculation(project: &Project, id: &str) -> BTreeMap<String, String> {
calculate_cli(
project,
&["capacity", "calculate", "--scope", "cycle", "--id", id],
)
}
fn cli_number(map: &BTreeMap<String, String>, key: &str) -> u64 {
map.get(key)
.unwrap_or_else(|| panic!("the CLI printed `{key}`: {map:?}"))
.parse()
.unwrap_or_else(|_| panic!("the CLI's `{key}` is a number: {map:?}"))
}
fn cli_percent(map: &BTreeMap<String, String>, key: &str) -> u32 {
map.get(key)
.unwrap_or_else(|| panic!("the CLI printed `{key}`: {map:?}"))
.trim_end_matches('%')
.parse()
.unwrap_or_else(|_| panic!("the CLI's `{key}` is a percentage: {map:?}"))
}
fn press(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::NONE)
}
fn open_capacity_tab(app: &mut App) {
let _ = app.handle_key(press(KeyCode::F(8))).unwrap();
assert!(matches!(app.mode, Mode::Schedule), "F8 opens the page");
let _ = app.handle_key(press(KeyCode::Tab)).unwrap();
let _ = app.handle_key(press(KeyCode::Tab)).unwrap();
assert_eq!(
app.schedule_state().tab,
ScheduleTab::Capacity,
"`Tab` twice reaches the Capacity tab"
);
}
fn tui_fields(app: &App, selected: usize) -> BTreeMap<String, String> {
app.capacity_detail(selected)
.unwrap_or_else(|| panic!("scope {selected} has a detail pane"))
.fields
.iter()
.map(|(key, value)| (key.clone(), value.clone()))
.collect()
}
fn tree(root: &Path) -> BTreeMap<String, Vec<u8>> {
let mut out = BTreeMap::new();
let mut stack = vec![root.to_path_buf()];
while let Some(current) = stack.pop() {
for entry in fs::read_dir(¤t).expect("the directory reads") {
let path = entry.expect("the entry reads").path();
if path.is_dir() {
stack.push(path);
} else {
let name = path
.strip_prefix(root)
.expect("under the root")
.to_string_lossy()
.into_owned();
out.insert(name, fs::read(&path).expect("the file reads"));
}
}
}
out
}
fn export(project: &Project, extra: &[&str]) -> Value {
let mut args = vec!["export", "--target", "capacity"];
args.extend_from_slice(extra);
serde_json::from_str(&stdout(project.root(), &args))
.expect("the export writes one JSON document")
}
fn portable_equals_cli(
document: &Value,
calculation: &Value,
field: &str,
cli: &str,
expected: u64,
) {
assert_eq!(
calculation[field].as_u64(),
Some(expected),
"`{field}` must be the CLI's `{cli}` ({expected}): {document}"
);
}
fn portable_findings<'a>(document: &'a Value, kind: &str) -> Vec<&'a Value> {
document["diagnostics"]
.as_array()
.expect("diagnostics is an array")
.iter()
.filter(|finding| finding["kind"] == json!(kind))
.collect()
}
struct Reader {
thread: std::thread::JoinHandle<()>,
}
fn spawn_reader(
stdout: std::process::ChildStdout,
tx: mpsc::Sender<String>,
sink: Arc<Mutex<Vec<String>>>,
) -> Reader {
let thread = std::thread::spawn(move || {
let mut reader = BufReader::new(stdout);
loop {
let mut line = String::new();
match reader.read_line(&mut line) {
Ok(0) | Err(_) => break,
Ok(_) => {
let trimmed = line
.trim_end_matches('\n')
.trim_end_matches('\r')
.to_string();
sink.lock().unwrap().push(trimmed.clone());
if tx.send(trimmed).is_err() {
break;
}
}
}
}
});
Reader { thread }
}
struct Session {
child: Child,
stdin: Option<ChildStdin>,
rx: mpsc::Receiver<String>,
all_lines: Arc<Mutex<Vec<String>>>,
reader: Option<Reader>,
id: Cell<u32>,
}
impl Session {
fn start(repo: &Path) -> Self {
let mut child = Command::new(tix_bin())
.arg("mcp")
.current_dir(repo)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn tix mcp");
let stdin = child.stdin.take().expect("stdin piped");
let stdout = child.stdout.take().expect("stdout piped");
let (tx, rx) = mpsc::channel();
let sink = Arc::new(Mutex::new(Vec::new()));
let reader = spawn_reader(stdout, tx, Arc::clone(&sink));
Session {
child,
stdin: Some(stdin),
rx,
all_lines: sink,
reader: Some(reader),
id: Cell::new(1),
}
}
fn write_line(&mut self, line: &str) {
writeln!(self.stdin.as_mut().unwrap(), "{line}").unwrap();
self.stdin.as_mut().unwrap().flush().unwrap();
}
fn next_id(&self) -> u32 {
let id = self.id.get();
self.id.set(id + 1);
id
}
fn send_request(&mut self, method: &str, params: &Value) {
let id = self.next_id();
let msg = json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params});
self.write_line(&serde_json::to_string(&msg).unwrap());
}
fn recv_json(&self) -> Value {
let line = self
.rx
.recv_timeout(Duration::from_secs(10))
.expect("no response within 10 s");
serde_json::from_str(&line).unwrap_or_else(|e| panic!("invalid JSON: {e}\nraw: {line}"))
}
fn handshake(&mut self) {
self.send_request(
"initialize",
&json!({"protocolVersion": "2025-06-18", "capabilities": {}}),
);
let resp = self.recv_json();
assert_eq!(resp["result"]["serverInfo"]["name"], "tix");
self.write_line(
&serde_json::to_string(
&json!({"jsonrpc": "2.0", "method": "notifications/initialized"}),
)
.unwrap(),
);
assert!(
self.rx.recv_timeout(Duration::from_millis(300)).is_err(),
"the initialized notification expects no response"
);
}
fn call_tool(&mut self, name: &str, args: &Value) -> Value {
let id = self.next_id();
let msg = json!({
"jsonrpc": "2.0",
"id": id,
"method": "tools/call",
"params": {"name": name, "arguments": args}
});
self.write_line(&serde_json::to_string(&msg).unwrap());
let resp = self.recv_json();
resp.get("result").or(resp.get("error")).cloned().unwrap()
}
fn close(mut self) -> (std::process::ExitStatus, Vec<String>) {
drop(self.stdin.take());
let status = self.child.wait().expect("wait");
self.reader
.take()
.expect("reader present")
.thread
.join()
.ok();
let lines = std::mem::take(self.all_lines.lock().unwrap().as_mut());
(status, lines)
}
}
fn assert_pure_stdout(lines: &[String]) {
for (i, line) in lines.iter().enumerate() {
assert!(
serde_json::from_str::<Value>(line).is_ok(),
"non-JSON on stdout line {i}: {line:?}"
);
}
}
fn tool_result(value: &Value) -> &Value {
value.get("result").or(value.get("error")).unwrap_or(value)
}
fn tool_text(value: &Value) -> &str {
tool_result(value)["content"][0]["text"]
.as_str()
.expect("a tool result carries text")
}
fn tool_structured(value: &Value) -> &Value {
&tool_result(value)["structuredContent"]
}
fn open_session(project: &Project) -> Session {
let mut session = Session::start(project.root());
session.handshake();
session
}
#[test]
fn c_005_fr_002_same_unit_values_are_one_answer_across_cli_tui_mcp_and_portable() {
let project = Project::new("");
let id = project.generate();
project.set(&id, 10, "d");
project.create("one", "6d", "todo");
project.create("two", "2d", "done");
let cli = cli_calculation(&project, &id);
assert_eq!(cli.get("scope").map(String::as_str), Some("cycle"));
assert_eq!(cli.get("reference").map(String::as_str), Some(INSTANCE_ID));
assert_eq!(cli.get("display").map(String::as_str), Some(INSTANCE_NAME));
assert_eq!(cli.get("state").map(String::as_str), Some("configured"));
assert_eq!(cli.get("capacity").map(String::as_str), Some("10"));
assert_eq!(cli.get("unit").map(String::as_str), Some("d"));
assert_eq!(cli.get("committed").map(String::as_str), Some("8"));
assert_eq!(cli.get("completed").map(String::as_str), Some("2"));
assert_eq!(cli.get("remaining").map(String::as_str), Some("6"));
assert_eq!(cli.get("overflow").map(String::as_str), Some("0"));
assert_eq!(cli.get("utilization").map(String::as_str), Some("80%"));
assert_eq!(cli.get("counted_issues").map(String::as_str), Some("2"));
assert_eq!(cli.get("uncounted_issues").map(String::as_str), Some("0"));
assert_eq!(cli.get("unestimated_issues").map(String::as_str), Some("0"));
let mut app = project.app();
open_capacity_tab(&mut app);
assert_eq!(
tui_fields(&app, 0),
cli,
"the page prints exactly the CLI's fields, no more and no fewer"
);
let summaries = app.capacity_summaries().expect("the board read ran");
for key in [INSTANCE_ID, INSTANCE_NAME] {
assert_eq!(
summaries.get(key).map(String::as_str),
Some("8/10 d · 80%"),
"the board cell for `{key}` is the shared numbers"
);
}
let mut session = open_session(&project);
let calculated = session.call_tool(
"capacity_calculate",
&json!({"scope": "cycle", "id": INSTANCE_ID}),
);
assert_eq!(
tool_text(&calculated),
stdout(
project.root(),
&[
"capacity",
"calculate",
"--scope",
"cycle",
"--id",
INSTANCE_ID
]
),
"MCP words the calculation as the CLI does"
);
let mcp = tool_structured(&calculated);
assert_eq!(mcp["state"], json!("configured"));
assert_eq!(mcp["scope"]["display"], json!(INSTANCE_NAME));
assert_eq!(mcp["unit"], json!("d"));
for (field, key) in [
("capacity", "capacity"),
("committed", "committed"),
("completed", "completed"),
("remaining", "remaining"),
("overflow", "overflow"),
("counted_issues", "counted_issues"),
("uncounted_issues", "uncounted_issues"),
("unestimated_issues", "unestimated_issues"),
] {
assert_eq!(
mcp[field].as_u64(),
Some(cli_number(&cli, key)),
"MCP `{field}` is the CLI's `{key}`"
);
}
assert_eq!(
mcp["utilization"].as_u64(),
Some(u64::from(cli_percent(&cli, "utilization")))
);
let (status, lines) = session.close();
assert!(status.success());
assert_pure_stdout(&lines);
let document = export(&project, &[]);
assert_eq!(document["format"], json!("tix-capacity"));
assert_eq!(document["version"], json!(1));
let portable = &document["scopes"][0]["calculation"];
assert_eq!(portable["state"], json!("configured"));
assert_eq!(portable["unit"], json!("d"));
assert_eq!(portable["scope"]["reference"], json!(INSTANCE_ID));
assert_eq!(portable["scope"]["display"], json!(INSTANCE_NAME));
for (field, key) in [
("capacity", "capacity"),
("committed", "committed"),
("completed", "completed"),
("remaining", "remaining"),
("overflow", "overflow"),
("counted_issues", "counted_issues"),
("uncounted_issues", "uncounted_issues"),
("unestimated_issues", "unestimated_issues"),
] {
portable_equals_cli(&document, portable, field, key, cli_number(&cli, key));
}
assert_eq!(
portable["utilization"].as_u64(),
Some(u64::from(cli_percent(&cli, "utilization")))
);
let control = Project::new("");
let control_id = control.generate();
control.set(&control_id, 10, "d");
control.create("only", "6d", "done");
let control_cli = cli_calculation(&control, &control_id);
assert_eq!(control_cli.get("committed").map(String::as_str), Some("6"));
assert_eq!(control_cli.get("completed").map(String::as_str), Some("6"));
assert_ne!(
control_cli, cli,
"the control fixture is a different answer"
);
let mut control_app = control.app();
open_capacity_tab(&mut control_app);
assert_eq!(
tui_fields(&control_app, 0),
control_cli,
"the parity assertion tracks the fixture, not a constant"
);
let control_document = export(&control, &[]);
assert_eq!(
control_document["scopes"][0]["calculation"]["committed"].as_u64(),
Some(6)
);
}
#[test]
fn c_004_explicit_conversion_uses_the_declared_ratio_and_is_never_implicit() {
let project = Project::new("");
let id = project.generate();
project.set(&id, 10, "d");
project.create("week", "1w", "todo");
let cli = cli_calculation(&project, &id);
assert_eq!(
cli.get("committed").map(String::as_str),
Some("5"),
"{cli:?}"
);
assert_eq!(cli.get("counted_issues").map(String::as_str), Some("1"));
assert_eq!(cli.get("uncounted_issues").map(String::as_str), Some("0"));
assert_eq!(cli.get("unestimated_issues").map(String::as_str), Some("0"));
assert_eq!(cli.get("utilization").map(String::as_str), Some("50%"));
assert_ne!(cli.get("committed").map(String::as_str), Some("1"));
let mut app = project.app();
open_capacity_tab(&mut app);
assert_eq!(tui_fields(&app, 0), cli);
let mut session = open_session(&project);
let mcp = session.call_tool(
"capacity_calculate",
&json!({"scope": "cycle", "id": INSTANCE_ID}),
);
assert_eq!(tool_structured(&mcp)["committed"].as_u64(), Some(5));
assert_eq!(tool_structured(&mcp)["unit"], json!("d"));
let (status, _) = session.close();
assert!(status.success());
let document = export(&project, &[]);
assert_eq!(
document["scopes"][0]["calculation"]["committed"].as_u64(),
Some(5),
"the portable document converts through the same ratio: {document}"
);
}
#[test]
fn c_005_fr_002_an_unestimated_issue_is_visible_and_contributes_nothing() {
let project = Project::new("");
let id = project.generate();
project.set(&id, 10, "d");
project.create("estimated", "4d", "todo");
project.create("no estimate", "", "todo");
let cli = cli_calculation(&project, &id);
assert_eq!(
cli.get("committed").map(String::as_str),
Some("4"),
"{cli:?}"
);
assert_eq!(cli.get("counted_issues").map(String::as_str), Some("1"));
assert_eq!(cli.get("unestimated_issues").map(String::as_str), Some("1"));
assert_eq!(cli.get("uncounted_issues").map(String::as_str), Some("0"));
assert_eq!(cli.get("utilization").map(String::as_str), Some("40%"));
let mut app = project.app();
open_capacity_tab(&mut app);
assert_eq!(tui_fields(&app, 0), cli);
let mut session = open_session(&project);
let mcp = session.call_tool(
"capacity_calculate",
&json!({"scope": "cycle", "id": INSTANCE_ID}),
);
let structured = tool_structured(&mcp);
assert_eq!(structured["committed"].as_u64(), Some(4));
assert_eq!(structured["unestimated_issues"].as_u64(), Some(1));
assert_eq!(structured["uncounted_issues"].as_u64(), Some(0));
let (status, _) = session.close();
assert!(status.success());
let document = export(&project, &[]);
let portable = &document["scopes"][0]["calculation"];
assert_eq!(portable["committed"].as_u64(), Some(4));
assert_eq!(portable["unestimated_issues"].as_u64(), Some(1));
}
#[test]
fn c_004_fr_002_an_undeclared_unit_is_reported_and_never_a_silent_zero() {
let project = Project::new("");
let id = project.generate();
project.set(&id, 10, "d");
project.create("countable", "6d", "todo");
project.create("hours", "3h", "todo");
let (out, err) = {
let (code, out, err) = run(
project.root(),
&[
"capacity",
"calculate",
"--scope",
"cycle",
"--id",
INSTANCE_ID,
],
);
assert_eq!(code, 0, "{err}");
(out, err)
};
let cli = fields(&out);
assert_eq!(cli.get("state").map(String::as_str), Some("configured"));
assert_eq!(cli.get("committed").map(String::as_str), Some("6"), "{out}");
assert_eq!(cli.get("counted_issues").map(String::as_str), Some("1"));
assert_eq!(cli.get("uncounted_issues").map(String::as_str), Some("1"));
assert!(err.contains("missing_conversion"), "{err}");
assert!(err.contains("unit: d"), "{err}");
assert!(err.contains("issues: #2"), "{err}");
assert!(err.contains("help:"), "the finding carries a remedy: {err}");
let mut app = project.app();
open_capacity_tab(&mut app);
assert_eq!(tui_fields(&app, 0), cli);
let detail = app.capacity_detail(0).expect("the scope has a detail");
let diagnostics = detail.diagnostics.join("\n");
assert!(diagnostics.contains("missing_conversion"), "{diagnostics}");
assert!(diagnostics.contains("help:"), "{diagnostics}");
assert!(app.capacity_rows()[0].warned, "the row carries the finding");
let mut session = open_session(&project);
let calculated = session.call_tool(
"capacity_calculate",
&json!({"scope": "cycle", "id": INSTANCE_ID}),
);
assert_eq!(tool_structured(&calculated)["committed"].as_u64(), Some(6));
let finding = tool_structured(&calculated)["diagnostics"]
.as_array()
.expect("diagnostics")
.iter()
.find(|finding| finding["kind"] == json!("missing_conversion"))
.expect("a missing_conversion finding");
assert_eq!(finding["unit"], json!("d"));
assert_eq!(finding["issues"], json!([2]));
assert!(
finding["remedy"]
.as_str()
.is_some_and(|remedy| !remedy.is_empty()),
"the remedy travels: {finding}"
);
let (status, _) = session.close();
assert!(status.success());
let document = export(&project, &[]);
let portable = portable_findings(&document["scopes"][0]["calculation"], "missing_conversion");
assert_eq!(portable.len(), 1, "{document}");
assert_eq!(portable[0]["unit"], json!("d"));
assert_eq!(portable[0]["issues"], json!([2]));
assert_ne!(cli.get("committed").map(String::as_str), Some("0"));
}
#[test]
fn c_003_c_004_an_unconfigured_scope_is_explicit_and_never_a_zero() {
let project = Project::new("");
let id = project.generate();
let _ = stdout(
project.root(),
&[
"capacity",
"set",
"--scope",
"cycle",
"--id",
&id,
"--value",
"10",
"--unit",
"d",
"--disabled",
],
);
project.create("work", "6d", "todo");
let cli = cli_calculation(&project, &id);
assert_eq!(cli.get("state").map(String::as_str), Some("unconfigured"));
for absent in [
"capacity",
"unit",
"committed",
"completed",
"remaining",
"overflow",
"utilization",
"counted_issues",
"uncounted_issues",
"unestimated_issues",
] {
assert_eq!(
cli.get(absent).map(String::as_str),
Some("-"),
"`{absent}`: {cli:?}"
);
}
let mut app = project.app();
open_capacity_tab(&mut app);
assert_eq!(tui_fields(&app, 0), cli, "the page agrees with the CLI");
assert!(
app.capacity_rows()[0].subtitle.contains("unconfigured"),
"the row states the explicit unconfigured state: {}",
app.capacity_rows()[0].subtitle
);
assert!(
!app.capacity_nothing_committed(),
"no answer is not the statement that the answer is zero"
);
let mut session = open_session(&project);
let disabled = session.call_tool(
"capacity_calculate",
&json!({"scope": "cycle", "id": INSTANCE_ID}),
);
let absent = tool_structured(&disabled);
assert_eq!(absent["state"], json!("unconfigured"));
for key in [
"capacity",
"unit",
"committed",
"completed",
"remaining",
"overflow",
"utilization",
"counted_issues",
"uncounted_issues",
"unestimated_issues",
] {
assert_eq!(absent[key], json!(null), "`{key}` must be null, never 0");
}
assert!(
tool_text(&disabled).contains("capacity: -"),
"{}",
tool_text(&disabled)
);
let unknown = session.call_tool(
"capacity_calculate",
&json!({"scope": "cycle", "id": "Ghost"}),
);
let unknown_structured = tool_structured(&unknown);
assert_eq!(unknown_structured["state"], json!("unconfigured"));
let finding = unknown_structured["diagnostics"]
.as_array()
.expect("diagnostics")
.iter()
.find(|finding| finding["kind"] == json!("unknown_scope"))
.expect("an unknown_scope finding");
assert!(finding["remedy"].as_str().is_some(), "{finding}");
let (status, _) = session.close();
assert!(status.success());
let enabled = Project::new("");
let enabled_id = enabled.generate();
enabled.set(&enabled_id, 10, "d");
enabled.create("work", "6d", "todo");
let enabled_cli = cli_calculation(&enabled, &enabled_id);
assert_eq!(
enabled_cli.get("state").map(String::as_str),
Some("configured")
);
assert_eq!(enabled_cli.get("committed").map(String::as_str), Some("6"));
let mut enabled_app = enabled.app();
open_capacity_tab(&mut enabled_app);
assert_eq!(tui_fields(&enabled_app, 0), enabled_cli);
}
#[test]
fn fr_005_c_008_a_close_captures_the_shared_calculation_as_immutable_evidence() {
let project = Project::new("");
let id = project.generate();
project.set(&id, 10, "d");
project.create("one", "6d", "todo");
project.create("two", "2d", "done");
let live = cli_calculation(&project, &id);
let (code, _out, missing) = run(project.root(), &["capacity", "snapshot", &id]);
assert_ne!(code, 0, "no snapshot exists yet");
assert!(missing.contains("has no snapshot"), "{missing}");
project.close(&id);
let readback = stdout(project.root(), &["capacity", "snapshot", &id]);
assert!(readback.contains("evidence: present"), "{readback}");
let evidence = fields(&readback);
for key in [
"state",
"capacity",
"unit",
"committed",
"completed",
"remaining",
"overflow",
"utilization",
"counted_issues",
"uncounted_issues",
"unestimated_issues",
] {
assert_eq!(
evidence.get(key),
live.get(key),
"the evidence's `{key}` is the live calculation's"
);
}
assert_eq!(evidence.get("committed").map(String::as_str), Some("8"));
assert_eq!(evidence.get("utilization").map(String::as_str), Some("80%"));
let snapshot_id = evidence.get("snapshot_id").cloned().expect("a snapshot id");
let mut session = open_session(&project);
let tool = session.call_tool("capacity_snapshot", &json!({"cycle": INSTANCE_ID}));
assert_eq!(
tool_text(&tool),
readback,
"MCP reads the snapshot as the CLI does"
);
let structured = tool_structured(&tool);
assert_eq!(structured["evidence"], json!("present"));
assert_eq!(structured["capacity"]["snapshot_id"], json!(snapshot_id));
assert_eq!(structured["capacity"]["committed"].as_u64(), Some(8));
let history = structured["history"].as_array().expect("history");
assert_eq!(history.len(), 1, "{structured}");
assert_eq!(history[0]["kind"], json!("cycle_closed"));
let (status, _) = session.close();
assert!(status.success());
let document = export(&project, &[]);
let snapshots = document["snapshots"].as_array().expect("snapshots");
assert_eq!(snapshots.len(), 1, "{document}");
assert_eq!(snapshots[0]["snapshot_id"], json!(snapshot_id));
assert_eq!(snapshots[0]["cycle_id"], json!(INSTANCE_ID));
assert_eq!(snapshots[0]["committed"].as_u64(), Some(8));
assert_eq!(snapshots[0]["utilization"].as_u64(), Some(80));
let history = document["history"].as_array().expect("history");
assert_eq!(history.len(), 1);
assert_eq!(history[0]["kind"], json!("cycle_closed"));
let bytes = project.snapshot_bytes();
let _ = stdout(project.root(), &["capacity", "snapshot", &id]);
let _ = export(&project, &[]);
assert_eq!(
project.snapshot_bytes(),
bytes,
"the evidence is immutable under reads"
);
}
#[test]
fn c_002_completion_is_the_shared_status_category_on_every_surface() {
let board = "[kanban.default]\ncolumns = [\"todo\", \"done\", \"shipped\"]\n\
done_column = \"done\"\n\
category_map = { todo = \"unstarted\", done = \"active\", shipped = \"completed\" }\n\
\n\
[kanban.default.wip.units]\nd = 1\n";
let project = Project::with_board(board);
let id = project.generate();
project.set(&id, 10, "d");
project.create("complete", "2d", "shipped");
project.create("not yet", "5d", "done");
project.create("todo", "1d", "todo");
let cli = cli_calculation(&project, &id);
assert_eq!(
cli.get("committed").map(String::as_str),
Some("8"),
"{cli:?}"
);
assert_eq!(
cli.get("completed").map(String::as_str),
Some("2"),
"the category answer, not the `done` column: {cli:?}"
);
assert_eq!(cli.get("remaining").map(String::as_str), Some("6"));
let mut app = project.app();
open_capacity_tab(&mut app);
assert_eq!(tui_fields(&app, 0), cli, "the page agrees with the CLI");
let mut session = open_session(&project);
let mcp = session.call_tool(
"capacity_calculate",
&json!({"scope": "cycle", "id": INSTANCE_ID}),
);
let structured = tool_structured(&mcp);
assert_eq!(structured["committed"].as_u64(), Some(8));
assert_eq!(
structured["completed"].as_u64(),
Some(2),
"the tool agrees with the CLI"
);
let (status, _) = session.close();
assert!(status.success());
let document = export(&project, &[]);
assert_eq!(
document["scopes"][0]["calculation"]["completed"].as_u64(),
Some(2),
"the document agrees with the CLI: {document}"
);
let control = Project::new("");
let control_id = control.generate();
control.set(&control_id, 10, "d");
control.create("done", "5d", "done");
let control_cli = cli_calculation(&control, &control_id);
assert_eq!(
control_cli.get("completed").map(String::as_str),
Some("5"),
"{control_cli:?}"
);
assert_ne!(control_cli.get("completed"), cli.get("completed"));
}
#[test]
fn c_006_fr_003_overflow_is_a_shared_report_and_writes_nothing() {
let project = Project::new("");
let id = project.generate();
project.set(&id, 5, "d");
project.create("six", "6d", "todo");
let before = tree(project.root());
let (code, out, err) = run(
project.root(),
&[
"capacity",
"calculate",
"--scope",
"cycle",
"--id",
INSTANCE_ID,
"--warnings",
],
);
assert_eq!(code, 0, "{err}");
let cli = fields(&out);
assert_eq!(cli.get("capacity").map(String::as_str), Some("5"));
assert_eq!(cli.get("committed").map(String::as_str), Some("6"));
assert_eq!(cli.get("overflow").map(String::as_str), Some("1"));
assert_eq!(cli.get("utilization").map(String::as_str), Some("120%"));
assert!(err.contains("capacity_overflow"), "{err}");
assert!(
err.contains("exceeds the configured capacity of 5d by 1d"),
"{err}"
);
assert!(err.contains("issues: #1"), "{err}");
assert!(err.contains("help:"), "the report carries a remedy: {err}");
let mut app = project.app();
open_capacity_tab(&mut app);
assert_eq!(tui_fields(&app, 0), cli);
assert!(app.capacity_rows()[0].overflowing);
assert!(
app.capacity_rows()[0].subtitle.contains("overflow 1d"),
"{}",
app.capacity_rows()[0].subtitle
);
assert!(
app.capacity_rows()[0].subtitle.contains("120%"),
"{}",
app.capacity_rows()[0].subtitle
);
let mut session = open_session(&project);
let calculated = session.call_tool(
"capacity_calculate",
&json!({"scope": "cycle", "id": INSTANCE_ID, "warnings": true}),
);
let calculation = tool_structured(&calculated);
assert_eq!(calculation["overflow"].as_u64(), Some(1));
assert_eq!(calculation["over_capacity"], json!(true));
let overflow = calculation["diagnostics"]
.as_array()
.expect("diagnostics")
.iter()
.find(|finding| finding["kind"] == json!("capacity_overflow"))
.expect("an overflow warning");
assert_eq!(overflow["current"].as_u64(), Some(6));
assert_eq!(overflow["limit"].as_u64(), Some(5));
assert_eq!(overflow["overflow"].as_u64(), Some(1));
assert_eq!(overflow["unit"], json!("d"));
assert!(
overflow["reason"]
.as_str()
.is_some_and(|r| r.contains("exceeds"))
);
assert!(overflow["remedy"].as_str().is_some_and(|r| !r.is_empty()));
let (status, lines) = session.close();
assert!(status.success());
assert_pure_stdout(&lines);
let document = export(&project, &[]);
let portable = portable_findings(&document["scopes"][0]["calculation"], "capacity_overflow");
assert_eq!(portable.len(), 1, "{document}");
assert_eq!(portable[0]["current"].as_u64(), Some(6));
assert_eq!(portable[0]["limit"].as_u64(), Some(5));
assert_eq!(portable[0]["overflow"].as_u64(), Some(1));
assert_eq!(portable[0]["unit"], json!("d"));
assert_eq!(
tree(project.root()),
before,
"a shared report never mutates an issue, a record or a snapshot"
);
let at_ceiling = Project::new("");
let at_id = at_ceiling.generate();
at_ceiling.set(&at_id, 6, "d");
at_ceiling.create("six", "6d", "todo");
let (code, out, err) = run(
at_ceiling.root(),
&[
"capacity",
"calculate",
"--scope",
"cycle",
"--id",
INSTANCE_ID,
"--warnings",
],
);
assert_eq!(code, 0, "{err}");
let cli = fields(&out);
assert_eq!(cli.get("overflow").map(String::as_str), Some("0"), "{out}");
assert_eq!(cli.get("utilization").map(String::as_str), Some("100%"));
assert!(
!err.contains("capacity_overflow"),
"no warning at the ceiling: {err}"
);
let mut app = at_ceiling.app();
open_capacity_tab(&mut app);
assert!(!app.capacity_rows()[0].overflowing);
}
#[test]
fn nfr_002_c_008_a_reopen_appends_one_event_and_leaves_the_prior_snapshot_identical() {
let project = Project::new("");
let id = project.generate();
project.set(&id, 10, "d");
project.create("one", "6d", "todo");
project.create("two", "2d", "done");
project.close(&id);
let snapshot_before = project.snapshot_bytes();
let config_before = fs::read(project.root().join("tix.toml")).unwrap();
project.reopen(&id);
assert_eq!(
project.snapshot_bytes(),
snapshot_before,
"a reopen never rewrites the closed snapshot"
);
assert_eq!(
fs::read(project.root().join("tix.toml")).unwrap(),
config_before,
"a reopen never rewrites the configuration"
);
let mut session = open_session(&project);
let tool = session.call_tool("capacity_snapshot", &json!({"cycle": INSTANCE_ID}));
let history = tool_structured(&tool)["history"]
.as_array()
.expect("history")
.clone();
let kinds: Vec<&str> = history
.iter()
.filter_map(|event| event["kind"].as_str())
.collect();
assert_eq!(
kinds,
vec!["cycle_closed", "cycle_reopened"],
"exactly the close and the one reopen: {history:?}"
);
let (status, _) = session.close();
assert!(status.success());
let document = export(&project, &[]);
let portable_kinds: Vec<&str> = document["history"]
.as_array()
.expect("history")
.iter()
.filter_map(|event| event["kind"].as_str())
.collect();
assert_eq!(
portable_kinds,
vec!["cycle_closed", "cycle_reopened"],
"{document}"
);
assert_eq!(
document["snapshots"][0]["committed"].as_u64(),
Some(8),
"the snapshot is carried, not recomputed"
);
let readback = stdout(project.root(), &["capacity", "snapshot", &id]);
assert!(readback.contains("evidence: present"), "{readback}");
let record = fs::read_to_string(
project
.root()
.join("docs/cycles/weekly-sprint-2026-01-05-000001.md"),
)
.expect("the cycle record reads");
assert!(
record.contains("cycle_reopened"),
"the reopen is a real write to the cycle record: {record}"
);
}
#[test]
fn nfr_003_c_007_every_surface_agrees_on_scope_values_rounding_warnings_and_snapshot() {
let project = Project::new("");
let id = project.generate();
project.set(&id, 10, "d");
project.create("six", "6d", "todo");
project.create("two", "2d", "done");
project.create("week", "1w", "todo");
project.create("hours", "3h", "todo");
project.create("blank", "", "todo");
project.close(&id);
project.reopen(&id);
let (code, out, err) = run(
project.root(),
&[
"capacity",
"calculate",
"--scope",
"cycle",
"--id",
INSTANCE_ID,
"--warnings",
],
);
assert_eq!(code, 0, "{err}");
let cli = fields(&out);
assert_eq!(
cli.get("committed").map(String::as_str),
Some("13"),
"{out}"
);
assert_eq!(cli.get("completed").map(String::as_str), Some("2"));
assert_eq!(cli.get("remaining").map(String::as_str), Some("11"));
assert_eq!(cli.get("capacity").map(String::as_str), Some("10"));
assert_eq!(cli.get("overflow").map(String::as_str), Some("3"));
assert_eq!(cli.get("utilization").map(String::as_str), Some("130%"));
assert_eq!(cli.get("counted_issues").map(String::as_str), Some("3"));
assert_eq!(cli.get("uncounted_issues").map(String::as_str), Some("1"));
assert_eq!(cli.get("unestimated_issues").map(String::as_str), Some("1"));
for kind in ["missing_conversion", "capacity_overflow"] {
assert!(err.contains(kind), "the CLI reports `{kind}`: {err}");
}
let mut app = project.app();
open_capacity_tab(&mut app);
assert_eq!(
tui_fields(&app, 0),
cli,
"the page prints exactly the CLI's fields"
);
let detail = app.capacity_detail(0).expect("the scope has a detail");
let tui_text = detail.diagnostics.join("\n");
let summaries = app.capacity_summaries().expect("the board read ran");
assert_eq!(
summaries.get(INSTANCE_ID).map(String::as_str),
Some("13/10 d · 130% · over"),
"the board cell is the same values, rounding and overflow flag"
);
let mut session = open_session(&project);
let calculated = session.call_tool(
"capacity_calculate",
&json!({"scope": "cycle", "id": INSTANCE_ID, "warnings": true}),
);
assert_eq!(
tool_text(&calculated),
format!("{out}\n{err}"),
"MCP words the calculation and its findings as the CLI does"
);
let mcp = tool_structured(&calculated);
assert_eq!(mcp["committed"].as_u64(), Some(13));
assert_eq!(mcp["completed"].as_u64(), Some(2));
assert_eq!(mcp["remaining"].as_u64(), Some(11));
assert_eq!(mcp["overflow"].as_u64(), Some(3));
assert_eq!(
mcp["utilization"].as_u64(),
Some(130),
"the same floored percentage"
);
assert_eq!(mcp["unit"], json!("d"));
assert_eq!(mcp["scope"]["display"], json!(INSTANCE_NAME));
let mcp_kinds: BTreeSet<String> = mcp["diagnostics"]
.as_array()
.expect("diagnostics")
.iter()
.filter_map(|finding| finding["kind"].as_str().map(str::to_string))
.collect();
assert_eq!(
mcp_kinds,
BTreeSet::from([
"capacity_overflow".to_string(),
"missing_conversion".to_string()
]),
"the fixture raises exactly the overflow and the missing conversion"
);
for kind in &mcp_kinds {
assert!(
err.contains(kind),
"the CLI's stderr carries `{kind}`: {err}"
);
assert!(
tui_text.contains(kind),
"the TUI's detail carries `{kind}`: {tui_text}"
);
}
let snapshot = session.call_tool("capacity_snapshot", &json!({"cycle": INSTANCE_ID}));
let structured = tool_structured(&snapshot);
let history_kinds: Vec<String> = structured["history"]
.as_array()
.expect("history")
.iter()
.filter_map(|event| event["kind"].as_str().map(str::to_string))
.collect();
assert_eq!(history_kinds, vec!["cycle_closed", "cycle_reopened"]);
let (status, lines) = session.close();
assert!(status.success());
assert_pure_stdout(&lines);
let document = export(&project, &[]);
let portable = &document["scopes"][0]["calculation"];
assert_eq!(portable["state"], json!("configured"));
assert_eq!(portable["unit"], json!("d"));
assert_eq!(portable["scope"]["reference"], json!(INSTANCE_ID));
assert_eq!(portable["scope"]["display"], json!(INSTANCE_NAME));
for (field, key) in [
("capacity", "capacity"),
("committed", "committed"),
("completed", "completed"),
("remaining", "remaining"),
("overflow", "overflow"),
("counted_issues", "counted_issues"),
("uncounted_issues", "uncounted_issues"),
("unestimated_issues", "unestimated_issues"),
] {
portable_equals_cli(&document, portable, field, key, cli_number(&cli, key));
}
assert_eq!(portable["utilization"].as_u64(), Some(130));
let portable_kinds: BTreeSet<String> = portable["diagnostics"]
.as_array()
.expect("diagnostics")
.iter()
.filter_map(|finding| finding["kind"].as_str().map(str::to_string))
.collect();
assert_eq!(
portable_kinds, mcp_kinds,
"the document preserves every finding kind the tool structured"
);
assert_eq!(
document["snapshots"][0]["committed"].as_u64(),
Some(13),
"the close captured the same 13"
);
let portable_history: Vec<String> = document["history"]
.as_array()
.expect("history")
.iter()
.filter_map(|event| event["kind"].as_str().map(str::to_string))
.collect();
assert_eq!(
portable_history, history_kinds,
"the History summary is shared"
);
}
#[test]
fn c_001_every_capacity_read_writes_nothing() {
let project = Project::assembled(&capacity_entry(INSTANCE_ID, 10, "d"));
project.write_issue(
1,
"todo",
&format!("cycles: [\"{INSTANCE_NAME}\"]\nassignee: \"ada\"\nestimate: \"6d\"\n"),
);
project.write_issue(
2,
"todo",
&format!("cycles: [\"{INSTANCE_NAME}\"]\nassignee: \"ada\"\nestimate: \"3h\"\n"),
);
let before = tree(project.root());
let _ = stdout(project.root(), &["capacity", "list"]);
let _ = stdout(project.root(), &["capacity", "show", INSTANCE_ID]);
let _ = stdout(
project.root(),
&[
"capacity",
"calculate",
"--scope",
"cycle",
"--id",
INSTANCE_ID,
],
);
let (code, _out, _err) = run(
project.root(),
&["capacity", "snapshot", INSTANCE_ID, "--warnings"],
);
assert_ne!(code, 0, "no snapshot exists yet");
assert_eq!(tree(project.root()), before, "the CLI reads wrote nothing");
let mut app = project.app();
open_capacity_tab(&mut app);
assert!(app.capacity_read(), "the TUI read ran");
assert_eq!(tree(project.root()), before, "the TUI read wrote nothing");
let mut session = open_session(&project);
let _ = session.call_tool("capacity_list", &json!({}));
let _ = session.call_tool("capacity_show", &json!({"reference": INSTANCE_ID}));
let _ = session.call_tool(
"capacity_calculate",
&json!({"scope": "cycle", "id": INSTANCE_ID, "warnings": true}),
);
let _ = session.call_tool("capacity_snapshot", &json!({"cycle": INSTANCE_ID}));
let (status, _) = session.close();
assert!(status.success());
assert_eq!(tree(project.root()), before, "the MCP reads wrote nothing");
let _ = export(&project, &[]);
assert_eq!(
tree(project.root()),
before,
"the portable read wrote nothing"
);
project.close(INSTANCE_ID);
let closed = tree(project.root());
assert_ne!(
closed, before,
"the close wrote its snapshot and its history — the fingerprint sees writes"
);
let _ = stdout(
project.root(),
&["capacity", "snapshot", INSTANCE_ID, "--warnings"],
);
let mut app = project.app();
open_capacity_tab(&mut app);
let mut session = open_session(&project);
let _ = session.call_tool("capacity_snapshot", &json!({"cycle": INSTANCE_ID}));
let (status, _) = session.close();
assert!(status.success());
let _ = export(&project, &[]);
assert_eq!(
tree(project.root()),
closed,
"reads over the closed evidence wrote nothing"
);
project.set_team("Platform", 12, "d");
let after = tree(project.root());
assert_ne!(
after, closed,
"`tix capacity set` writes, and the fingerprint sees it"
);
assert_ne!(
after.get("tix.toml"),
closed.get("tix.toml"),
"the changed file is the configuration"
);
}
fn performance_issue(number: u32) -> (String, bool, bool) {
let mut extra = String::new();
if number % 2 == 0 {
extra.push_str("assignee: \"ada\"\n");
} else if number % 5 == 0 {
extra.push_str("assignee: \"grace\"\n");
}
if number % 4 != 0 {
extra.push_str("cycles: [\"Cycle 1\"]\n");
} else {
extra.push_str("cycles: [\"Ghost\"]\n");
}
let residue = number % 6;
extra.push_str(match residue {
0 => "estimate: \"3d\"\n",
1 => "estimate: \"1w\"\n",
2 => "estimate: \"5\"\n",
3 => "estimate: \"3h\"\n",
4 => "estimate: \"twenty\"\n",
_ => "estimate: \"\"\n",
});
(extra, residue != 5, residue <= 3)
}
#[test]
fn nfr_001_ten_thousand_issues_are_indexed_once_and_calculated_structurally() {
let dir = TempDir::new().unwrap();
let issues = dir.path().join("issues");
fs::create_dir_all(&issues).unwrap();
fs::write(
dir.path().join("tix.toml"),
format!(
"issues_dir = {issues:?}\n\
{BOARD}\n\
[cycles.\"Cycle 1\"]\nstart_date = \"2026-01-01\"\n\n\
[capacity.sprint]\ncycle = \"Cycle 1\"\ncapacity = 100000\nunit = \"d\"\n\n\
[capacity.platform]\nteam = \"Platform\"\ncapacity = 100000\nunit = \"d\"\n\n\
[capacity.ada]\nassignee = \"ada\"\ncapacity = 100000\nunit = \"d\"\n",
issues = issues.to_string_lossy(),
),
)
.unwrap();
let config = Config::load(dir.path()).expect("the fixture loads");
let index = schedule_index(&config, dir.path()).expect("the cycle index builds");
const COUNT: u32 = 10_000;
let mut parsed = 0usize;
let mut readable = 0usize;
let parsed_issues: Vec<Issue> = (1..=COUNT)
.map(|number| {
let (extra, stated, valid) = performance_issue(number);
if stated {
parsed += 1;
}
if valid {
readable += 1;
}
Issue::parse(&format!(
"---\nnumber: {number}\ntitle: \"Issue {number}\"\nstatus: \"{status}\"\n\
labels: []\npriority: \"medium\"\ncreated: \"2026-01-01\"\nupdated: \"2026-01-01\"\n{extra}---\n\nBody.\n",
status = if number % 3 == 0 { "done" } else { "todo" },
))
.expect("the generated issue parses")
})
.collect();
assert_eq!(parsed_issues.len(), COUNT as usize);
let started = Instant::now();
let snapshot = IssueSnapshot::build(&config, &index, &parsed_issues);
let build = started.elapsed();
let units = snapshot.targets().len();
assert_eq!(
units,
2,
"the board declares two units: {:?}",
snapshot.targets()
);
assert_eq!(snapshot.len(), COUNT as usize, "every issue is indexed");
assert_eq!(
snapshot.issue_visits(),
COUNT as usize,
"each issue is visited exactly once by the one pass"
);
assert_eq!(
snapshot.estimates_parsed(),
parsed,
"each stated estimate is parsed exactly once"
);
assert_eq!(
snapshot.conversions_resolved(),
readable * units,
"the conversion seam is consulted once per readable estimate, per unit — a second \
pass, or one resolution repeated per queried scope, would exceed this"
);
let cycle_calc = CapacityCalculation::for_scope(
&snapshot,
&index,
&config,
CapacityScope::new(CapacityScopeKind::Cycle, "Cycle 1", &index),
);
let team_calc = CapacityCalculation::for_scope(
&snapshot,
&index,
&config,
CapacityScope::new(CapacityScopeKind::Team, "Platform", &index),
);
let assignee_calc = CapacityCalculation::for_scope(
&snapshot,
&index,
&config,
CapacityScope::new(CapacityScopeKind::Assignee, "ada", &index),
);
assert!(cycle_calc.committed.is_some(), "{cycle_calc:?}");
assert!(team_calc.committed.is_some());
assert!(assignee_calc.committed.is_some());
assert_eq!(
cycle_calc.state(),
tix::capacity::calculate::CapacityState::Configured
);
let doubled: Vec<Issue> = parsed_issues
.iter()
.chain(parsed_issues.iter())
.cloned()
.collect();
let twice = IssueSnapshot::build(&config, &index, &doubled);
assert_eq!(twice.issue_visits(), 2 * COUNT as usize);
assert_eq!(twice.estimates_parsed(), 2 * parsed);
assert_eq!(twice.conversions_resolved(), 2 * readable * units);
assert_ne!(twice.issue_visits(), snapshot.issue_visits());
println!(
"NFR-001: {COUNT} issues indexed in {build:?} \
({} estimates parsed, {} conversions resolved for {units} units); \
three scopes then calculated over that one index",
snapshot.estimates_parsed(),
snapshot.conversions_resolved(),
);
}