IB-WP-0019-T06: workspace budget CLI

infospace-bench budget list <workspace> walks <workspace>/infospaces/*
and prints one row per infospace with slug, plans_count, runs_count,
total_tokens, total_cost_usd_known, total_cost_usd_estimated,
last_run_at, and latest_snapshot_id. infospace-bench budget show
<root> dumps the full plans/usage/summary structure for a single
infospace.

Missing budget directories are treated as zero rows rather than errors,
so the CLI is safe to run on partially-populated or fresh workspaces.

120 tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-17 20:44:40 +02:00
parent 110c78b9ad
commit 816a95b3ef
4 changed files with 190 additions and 1 deletions

View File

@@ -623,6 +623,105 @@ def test_run_generation_never_fails_when_hub_is_down(tmp_path: Path, monkeypatch
assert status["completed"] is True
def test_budget_show_returns_full_per_infospace_structure(tmp_path: Path) -> None:
from infospace_bench.budget import budget_show
from infospace_bench.generator import run_generation
root = _build_infospace(tmp_path)
fixture = tmp_path / "responses.yaml"
_write_minimal_fixture(fixture)
plan_generation(root)
run_generation(root, fixture_responses=fixture)
payload = budget_show(root)
assert payload["root"] == str(root)
assert payload["plans"]["snapshots"], "plans must round-trip"
assert payload["usage"]["runs"], "usage runs must round-trip"
assert payload["summary"] is not None
assert payload["summary"]["snapshot_resolved"] is True
def test_budget_list_workspace_rolls_up_multiple_infospaces(tmp_path: Path) -> None:
from infospace_bench.budget import budget_list_workspace
from infospace_bench.generator import init_generation_infospace, run_generation
book = tmp_path / "book.epub"
_write_three_chapter_epub(book)
fixture = tmp_path / "responses.yaml"
_write_minimal_fixture(fixture)
a = init_generation_infospace(tmp_path, book, "alpha", name="Alpha")
b = init_generation_infospace(tmp_path, book, "beta", name="Beta")
plan_generation(a.root, from_chapter=1, to_chapter=2)
plan_generation(b.root)
run_generation(a.root, fixture_responses=fixture)
# leave beta with a plan but no run, to verify partial rollups
rollups = budget_list_workspace(tmp_path)
by_slug = {item["slug"]: item for item in rollups}
assert {"alpha", "beta"} <= set(by_slug.keys())
assert by_slug["alpha"]["plans_count"] >= 1
assert by_slug["alpha"]["runs_count"] == 1
assert by_slug["alpha"]["last_run_at"] is not None
assert by_slug["beta"]["plans_count"] >= 1
assert by_slug["beta"]["runs_count"] == 0
assert by_slug["beta"]["last_run_at"] is None
def test_budget_list_workspace_handles_missing_or_empty_directories(tmp_path: Path) -> None:
from infospace_bench.budget import budget_list_workspace
from infospace_bench.generator import init_generation_infospace
# No infospaces/ at all → empty list, not an error
assert budget_list_workspace(tmp_path) == []
# An infospace with no budget dir → rollup with zeros, not a crash
book = tmp_path / "book.epub"
_write_three_chapter_epub(book)
init_generation_infospace(tmp_path, book, "zero", name="Zero")
rollups = budget_list_workspace(tmp_path)
assert len(rollups) == 1
assert rollups[0]["slug"] == "zero"
assert rollups[0]["plans_count"] == 0
assert rollups[0]["runs_count"] == 0
assert rollups[0]["total_tokens"] == 0
assert rollups[0]["total_cost_usd_known"] == 0.0
def test_budget_cli_list_and_show(tmp_path: Path) -> None:
from infospace_bench.generator import run_generation
root = _build_infospace(tmp_path)
fixture = tmp_path / "responses.yaml"
_write_minimal_fixture(fixture)
plan_generation(root)
run_generation(root, fixture_responses=fixture)
env = os.environ.copy()
env["PYTHONPATH"] = "src:/home/worsch/markitect-tool/src"
list_result = subprocess.run(
[sys.executable, "-m", "infospace_bench", "budget", "list", str(tmp_path)],
check=False, env=env, text=True, capture_output=True,
)
show_result = subprocess.run(
[sys.executable, "-m", "infospace_bench", "budget", "show", str(root)],
check=False, env=env, text=True, capture_output=True,
)
assert list_result.returncode == 0, list_result.stderr
assert show_result.returncode == 0, show_result.stderr
list_payload = json.loads(list_result.stdout)
show_payload = json.loads(show_result.stdout)
assert list_payload["workspace"] == str(tmp_path)
assert any(item["slug"] == "budget-test" for item in list_payload["infospaces"])
assert show_payload["plans"]["snapshots"]
assert show_payload["usage"]["runs"]
def test_plan_cli_writes_snapshot(tmp_path: Path) -> None:
root = _build_infospace(tmp_path)
env = os.environ.copy()