import json import os import subprocess import sys from pathlib import Path import pytest import yaml from infospace_bench import InfospaceError, add_artifact, create_infospace, load_infospace from infospace_bench.workflow import ( load_workflows, plan_workflow, run_workflow, ) SOURCE = """# Chapter One Division of labour increases output by specializing tasks. """ SUMMARY_TEMPLATE = """# {{ input.title }} Summary Lens: {{ macros.discipline }} Source: {{ input.content }} """ ASSISTED_TEMPLATE = """Review {{ input.title }} through {{ macros.discipline }}. {{ input.content }} """ def cli_env() -> dict[str, str]: env = os.environ.copy() env["PYTHONPATH"] = "src:/home/worsch/markitect-tool/src" return env def make_workflow_infospace(tmp_path: Path) -> Path: infospace = create_infospace(tmp_path, "pilot", name="Pilot") source = tmp_path / "chapter.md" source.write_text(SOURCE, encoding="utf-8") add_artifact(infospace.root, source, kind="source", title="Chapter One") template_dir = infospace.root / "workflows" / "templates" template_dir.mkdir(parents=True, exist_ok=True) (template_dir / "summary.md").write_text(SUMMARY_TEMPLATE, encoding="utf-8") (template_dir / "review.md").write_text(ASSISTED_TEMPLATE, encoding="utf-8") config_path = infospace.root / "infospace.yaml" config = yaml.safe_load(config_path.read_text(encoding="utf-8")) config["workflows"] = [ { "id": "source-summary", "description": "Render deterministic summaries for source artifacts.", "inputs": {"source": {"kind": "source"}}, "static_macros": {"discipline": "Viable System Model"}, "stages": [ { "id": "render-summary", "kind": "template", "input": "source", "template": "workflows/templates/summary.md", "output": { "path": "artifacts/generated/{{ input.slug }}-summary.md", "artifact_id": "generated/{{ input.slug }}-summary.md", "kind": "generated", "title": "{{ input.title }} Summary", }, } ], "expected_evaluations": ["metrics"], }, { "id": "assisted-review", "description": "Plan an assisted review without binding to a provider.", "inputs": {"source": {"kind": "source"}}, "static_macros": {"discipline": "Viable System Model"}, "stages": [ { "id": "draft-review", "kind": "assisted", "input": "source", "template": "workflows/templates/review.md", "output": { "path": "artifacts/generated/{{ input.slug }}-review.md", "artifact_id": "generated/{{ input.slug }}-review.md", "kind": "generated", "title": "{{ input.title }} Review", }, } ], }, ] config_path.write_text(yaml.safe_dump(config, sort_keys=False), encoding="utf-8") return infospace.root def test_load_workflow_definitions_from_infospace_yaml(tmp_path: Path) -> None: root = make_workflow_infospace(tmp_path) workflows = load_workflows(root) assert [workflow.id for workflow in workflows] == [ "source-summary", "assisted-review", ] assert workflows[0].inputs["source"].kind == "source" assert workflows[0].stages[0].kind == "template" assert workflows[0].expected_evaluations == ["metrics"] def test_plan_workflow_resolves_inputs_outputs_and_assisted_requests( tmp_path: Path, ) -> None: root = make_workflow_infospace(tmp_path) plan = plan_workflow(root, "source-summary") assisted_plan = plan_workflow(root, "assisted-review") assert plan.dry_run is True assert plan.status == "planned" assert plan.inputs[0].artifact_id == "source/chapter.md" assert plan.outputs[0].artifact_id == "generated/chapter-summary.md" assert plan.outputs[0].path == "artifacts/generated/chapter-summary.md" assert not (root / "artifacts" / "generated" / "chapter-summary.md").exists() assert assisted_plan.assisted_requests[0].stage_id == "draft-review" assert "Chapter One" in assisted_plan.assisted_requests[0].prompt assert assisted_plan.assisted_requests[0].provider_hint is None def test_run_workflow_writes_generated_artifact_manifest_and_run_record( tmp_path: Path, ) -> None: root = make_workflow_infospace(tmp_path) result = run_workflow(root, "source-summary") output_path = root / "artifacts" / "generated" / "chapter-summary.md" run_record = Path(result.run_record_path) loaded = load_infospace(root) generated = next(item for item in loaded.artifacts if item.kind == "generated") assert result.status == "completed" assert output_path.is_file() assert "Lens: Viable System Model" in output_path.read_text(encoding="utf-8") assert generated.id == "generated/chapter-summary.md" assert generated.provenance["workflow_id"] == "source-summary" assert generated.provenance["input_artifact_id"] == "source/chapter.md" assert run_record.is_file() assert yaml.safe_load(run_record.read_text(encoding="utf-8"))["status"] == "completed" def test_assisted_stage_requires_explicit_adapter_for_run(tmp_path: Path) -> None: root = make_workflow_infospace(tmp_path) with pytest.raises(InfospaceError) as raised: run_workflow(root, "assisted-review") assert raised.value.code == "assisted_stage_requires_adapter" assert raised.value.detail["stage_id"] == "draft-review" def test_cli_workflow_inspect_plan_and_run(tmp_path: Path) -> None: root = make_workflow_infospace(tmp_path) inspected = subprocess.run( [sys.executable, "-m", "infospace_bench", "workflow", "inspect", str(root)], check=False, env=cli_env(), text=True, capture_output=True, ) planned = subprocess.run( [ sys.executable, "-m", "infospace_bench", "workflow", "plan", str(root), "source-summary", ], check=False, env=cli_env(), text=True, capture_output=True, ) run = subprocess.run( [ sys.executable, "-m", "infospace_bench", "workflow", "run", str(root), "source-summary", ], check=False, env=cli_env(), text=True, capture_output=True, ) assert inspected.returncode == 0, inspected.stderr assert planned.returncode == 0, planned.stderr assert run.returncode == 0, run.stderr assert json.loads(inspected.stdout)["workflows"][0]["id"] == "source-summary" assert json.loads(planned.stdout)["status"] == "planned" assert json.loads(run.stdout)["outputs"][0]["artifact_id"] == ( "generated/chapter-summary.md" )