Files
infospace-bench/tests/test_workflow.py

218 lines
7.1 KiB
Python

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"
)