feat(infospace): add lifecycle CLI commands — init, status, entities, viability (S2.2)
Adds 'markitect infospace' command group with init (create config), status (entity count/domains/disciplines), entities (list with sort), and viability (threshold dashboard with pass/fail). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
225
tests/unit/infospace/test_cli.py
Normal file
225
tests/unit/infospace/test_cli.py
Normal file
@@ -0,0 +1,225 @@
|
||||
"""Tests for markitect.infospace.cli."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from markitect.infospace.cli import infospace_commands
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def runner():
|
||||
return CliRunner()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def infospace_dir(tmp_path):
|
||||
"""Create a minimal infospace directory with config and entities."""
|
||||
config_yaml = """\
|
||||
topic:
|
||||
name: "Test Infospace"
|
||||
domain: "Testing"
|
||||
|
||||
disciplines:
|
||||
- name: "Test Discipline"
|
||||
|
||||
viability:
|
||||
coverage_ratio:
|
||||
min: 0.60
|
||||
redundancy_ratio:
|
||||
max: 0.05
|
||||
"""
|
||||
(tmp_path / "infospace.yaml").write_text(config_yaml)
|
||||
|
||||
entities = tmp_path / "output" / "entities"
|
||||
entities.mkdir(parents=True)
|
||||
(entities / "alpha.md").write_text(
|
||||
"# Alpha\n\n## Definition\n\nAlpha is a test entity.\n\n"
|
||||
"## Source Chapter\n\nChapter 1\n\n"
|
||||
"## Domain\n\nProduction\n"
|
||||
)
|
||||
(entities / "beta.md").write_text(
|
||||
"# Beta\n\n## Definition\n\nBeta is another test entity with more words "
|
||||
"to make it longer.\n\n"
|
||||
"## Source Chapter\n\nChapter 2\n\n"
|
||||
"## Domain\n\nDistribution\n"
|
||||
)
|
||||
return tmp_path
|
||||
|
||||
|
||||
# ── init ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestInitCommand:
|
||||
def test_creates_config_file(self, runner, tmp_path):
|
||||
out = tmp_path / "infospace.yaml"
|
||||
result = runner.invoke(
|
||||
infospace_commands,
|
||||
["init", "--topic", "My Topic", "--domain", "Science", "-o", str(out)],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert out.exists()
|
||||
assert "Created" in result.output
|
||||
|
||||
def test_config_contains_topic(self, runner, tmp_path):
|
||||
out = tmp_path / "infospace.yaml"
|
||||
runner.invoke(
|
||||
infospace_commands,
|
||||
["init", "--topic", "My Topic", "-o", str(out)],
|
||||
)
|
||||
text = out.read_text()
|
||||
assert "My Topic" in text
|
||||
|
||||
def test_refuses_overwrite(self, runner, tmp_path):
|
||||
out = tmp_path / "infospace.yaml"
|
||||
out.write_text("existing")
|
||||
result = runner.invoke(
|
||||
infospace_commands,
|
||||
["init", "--topic", "X", "-o", str(out)],
|
||||
)
|
||||
assert result.exit_code != 0
|
||||
assert "already exists" in result.output
|
||||
|
||||
def test_with_disciplines(self, runner, tmp_path):
|
||||
out = tmp_path / "infospace.yaml"
|
||||
result = runner.invoke(
|
||||
infospace_commands,
|
||||
[
|
||||
"init", "--topic", "T",
|
||||
"--discipline", "VSM",
|
||||
"--discipline", "Category Theory",
|
||||
"-o", str(out),
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
text = out.read_text()
|
||||
assert "VSM" in text
|
||||
assert "Category Theory" in text
|
||||
|
||||
|
||||
# ── status ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestStatusCommand:
|
||||
def test_shows_topic_and_count(self, runner, infospace_dir):
|
||||
result = runner.invoke(
|
||||
infospace_commands,
|
||||
["status", "--config", str(infospace_dir / "infospace.yaml")],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "Test Infospace" in result.output
|
||||
assert "2" in result.output # 2 entities
|
||||
|
||||
def test_shows_domain_field(self, runner, infospace_dir):
|
||||
result = runner.invoke(
|
||||
infospace_commands,
|
||||
["status", "--config", str(infospace_dir / "infospace.yaml")],
|
||||
)
|
||||
# Domain from config (topic.domain), not entity domains
|
||||
assert "Testing" in result.output
|
||||
|
||||
def test_shows_disciplines(self, runner, infospace_dir):
|
||||
result = runner.invoke(
|
||||
infospace_commands,
|
||||
["status", "--config", str(infospace_dir / "infospace.yaml")],
|
||||
)
|
||||
assert "Test Discipline" in result.output
|
||||
|
||||
def test_no_config_exits(self, runner, tmp_path):
|
||||
result = runner.invoke(
|
||||
infospace_commands,
|
||||
["status", "--config", str(tmp_path / "nonexistent.yaml")],
|
||||
)
|
||||
assert result.exit_code != 0
|
||||
|
||||
|
||||
# ── entities ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestEntitiesCommand:
|
||||
def test_lists_entities(self, runner, infospace_dir):
|
||||
result = runner.invoke(
|
||||
infospace_commands,
|
||||
["entities", "--config", str(infospace_dir / "infospace.yaml")],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "alpha" in result.output
|
||||
assert "beta" in result.output
|
||||
assert "Total: 2" in result.output
|
||||
|
||||
def test_sort_by_domain(self, runner, infospace_dir):
|
||||
result = runner.invoke(
|
||||
infospace_commands,
|
||||
[
|
||||
"entities",
|
||||
"--config", str(infospace_dir / "infospace.yaml"),
|
||||
"--sort-by", "domain",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
lines = result.output.strip().split("\n")
|
||||
# Distribution comes before Production alphabetically
|
||||
data_lines = [l for l in lines if "alpha" in l or "beta" in l]
|
||||
assert len(data_lines) == 2
|
||||
|
||||
def test_no_entities_dir(self, runner, tmp_path):
|
||||
(tmp_path / "infospace.yaml").write_text("topic:\n name: X\n")
|
||||
result = runner.invoke(
|
||||
infospace_commands,
|
||||
["entities", "--config", str(tmp_path / "infospace.yaml")],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "No entities" in result.output
|
||||
|
||||
|
||||
# ── viability ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestViabilityCommand:
|
||||
def test_no_metrics_shows_thresholds(self, runner, infospace_dir):
|
||||
result = runner.invoke(
|
||||
infospace_commands,
|
||||
["viability", "--config", str(infospace_dir / "infospace.yaml")],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "coverage_ratio" in result.output
|
||||
|
||||
def test_with_metrics_file(self, runner, infospace_dir):
|
||||
import yaml
|
||||
metrics_dir = infospace_dir / "output" / "metrics"
|
||||
metrics_dir.mkdir(parents=True, exist_ok=True)
|
||||
metrics = {"coverage_ratio": 0.85, "redundancy_ratio": 0.02}
|
||||
(metrics_dir / "metrics.yaml").write_text(yaml.safe_dump(metrics))
|
||||
|
||||
result = runner.invoke(
|
||||
infospace_commands,
|
||||
["viability", "--config", str(infospace_dir / "infospace.yaml")],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "PASS" in result.output
|
||||
assert "Viable: YES" in result.output
|
||||
|
||||
def test_failing_threshold(self, runner, infospace_dir):
|
||||
import yaml
|
||||
metrics_dir = infospace_dir / "output" / "metrics"
|
||||
metrics_dir.mkdir(parents=True, exist_ok=True)
|
||||
metrics = {"coverage_ratio": 0.3, "redundancy_ratio": 0.02}
|
||||
(metrics_dir / "metrics.yaml").write_text(yaml.safe_dump(metrics))
|
||||
|
||||
result = runner.invoke(
|
||||
infospace_commands,
|
||||
["viability", "--config", str(infospace_dir / "infospace.yaml")],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "FAIL" in result.output
|
||||
assert "Viable: NO" in result.output
|
||||
|
||||
def test_no_thresholds_configured(self, runner, tmp_path):
|
||||
(tmp_path / "infospace.yaml").write_text("topic:\n name: X\n")
|
||||
result = runner.invoke(
|
||||
infospace_commands,
|
||||
["viability", "--config", str(tmp_path / "infospace.yaml")],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "No viability thresholds" in result.output
|
||||
Reference in New Issue
Block a user