Artifex/tests/test_standalone_scenes.py
2026-08-28 23:51:02 +07:00

318 lines
11 KiB
Python

from __future__ import annotations
import json
import re
from pathlib import Path
import pytest
from control_plane.authoring.models import (
DocumentAuthority,
DocumentType,
SceneDraftStatus,
Series,
StoryProject,
Work,
WorkType,
)
from control_plane.authoring.scene_context import build_scene_context_pack
from control_plane.authoring.sources import register_source
from control_plane.authoring.standalone_scenes import StandaloneSceneService
from control_plane.projects.models import Project
from model_router.router import (
ModelCapability,
ModelChunk,
ModelRequestContract,
ModelResponseContract,
ModelRouter,
)
class FakeSceneProvider:
provider_name = "test"
def complete(self, request: ModelRequestContract) -> ModelResponseContract:
if str(request.purpose) == str(ModelCapability.STORY_PLANNING):
content = json.dumps(
{
"purpose": "Sabine gives Corin a precise answer.",
"pov_character": "Sabine",
"tense": "past",
"location": "Corin's office",
"time_context": "ordinary daytime work",
"present": ["Sabine", "Corin"],
"beats": [
{"text": "Sabine crosses the room.", "required": True},
{"text": "Corin waits for her answer.", "required": True},
{"text": "Sabine leaves on her own timing.", "required": True},
{"text": "Corin does not turn it into a negotiation.", "required": True},
{"text": "Ordinary work remains unfinished.", "required": True},
],
"exact_values": [],
"constraints": [],
"forbidden_events": [],
"ending_state": "Sabine has left the office.",
"final_image": "Corin remains beside the unfinished paperwork.",
"boundary_constraints": [],
"continuity_questions": [],
}
)
else:
requirement_ids = re.findall(r'"id":\s*"([^"]+)"', request.prompt)
content = json.dumps(
{
"passed": True,
"requirement_results": [
{
"requirement_id": requirement_id,
"status": "HIT",
"evidence_quote": "Sabine crossed the room.",
"details": "Satisfied in the scene.",
}
for requirement_id in requirement_ids
],
"findings": [],
"observed_state": {"scene_end": {"location": "office"}},
"proposed_changes": [],
}
)
return ModelResponseContract(model="test-model", content=content, metadata={})
def stream(self, _request: ModelRequestContract):
prose = "Sabine crossed the room. " + "She waited. " * 245
yield ModelChunk(prose + "[[END_OF_SCENE]]")
def health(self) -> str:
return "AVAILABLE"
def scene_work(tmp_path: Path) -> Work:
series = Series.objects.create(title="Labyrinth Hero", slug="labyrinth-hero")
work = Work.objects.create(series=series, title="The Fortune Below", slug="the-fortune-below")
project = Project.objects.create(name="The Fortune Below", project_type="STORY", goal="Write")
StoryProject.objects.create(
project=project,
work=work,
title=work.title,
series=series.title,
slug=work.slug,
artifact_root=str(tmp_path / "artifacts"),
)
return work
def add_canon_source(work: Work, tmp_path: Path) -> None:
root = tmp_path / "sources"
root.mkdir()
source = root / "relationship-canon.md"
source.write_text(
"# Relationship Canon\n\n"
"Sabine and Corin preserve explicit choice and ordinary work boundaries.\n",
encoding="utf-8",
)
register_source(
work=work,
path=source,
root=root,
authority=DocumentAuthority.CANON,
document_type=DocumentType.CANON,
)
def test_context_pack_only_uses_latest_allowed_authority(tmp_path: Path) -> None:
work = scene_work(tmp_path)
root = tmp_path / "sources"
root.mkdir()
source = root / "facts.md"
source.write_text("# Facts\n\nSabine works at the estate.\n", encoding="utf-8")
register_source(
work=work,
path=source,
root=root,
authority=DocumentAuthority.CANON,
document_type=DocumentType.CANON,
)
register_source(
work=work,
path=source,
root=root,
authority=DocumentAuthority.SUPERSEDED,
document_type=DocumentType.CANON,
)
pack, ranked = build_scene_context_pack(work=work, query="Sabine estate")
assert ranked == []
assert pack["citations"] == []
def test_context_pack_includes_series_reference_but_not_sibling_book(tmp_path: Path) -> None:
work = scene_work(tmp_path)
reference = Work.objects.create(
series=work.series,
title="Series Reference",
slug="series-reference",
work_type=WorkType.SERIES_REFERENCE,
)
sibling = Work.objects.create(
series=work.series,
title="Sibling Book",
slug="sibling-book",
)
root = tmp_path / "series-sources"
root.mkdir()
reference_path = root / "shared.md"
sibling_path = root / "sibling.md"
reference_path.write_text("Sabine follows the shared household rule.", encoding="utf-8")
sibling_path.write_text("Sabine ignores a sibling-only invention.", encoding="utf-8")
for source_work, path in [(reference, reference_path), (sibling, sibling_path)]:
register_source(
work=source_work,
path=path,
root=root,
authority=DocumentAuthority.CANON,
document_type=DocumentType.CANON,
)
pack, _ranked = build_scene_context_pack(work=work, query="Sabine shared sibling invention")
keys = {citation["document_key"] for citation in pack["citations"]}
assert "shared.md" in keys
assert "sibling.md" not in keys
def test_pinned_planning_sources_do_not_crowd_out_canon(tmp_path: Path) -> None:
work = scene_work(tmp_path)
root = tmp_path / "mixed-sources"
root.mkdir()
canon_path = root / "canon.md"
planning_path = root / "planning.md"
canon_path.write_text(
"\n\n".join(f"Sabine canon boundary {index}." for index in range(4)),
encoding="utf-8",
)
planning_path.write_text(
"\n\n".join(f"Sabine planning detail {index}." for index in range(10)),
encoding="utf-8",
)
register_source(
work=work,
path=canon_path,
root=root,
authority=DocumentAuthority.CANON,
document_type=DocumentType.CANON,
)
register_source(
work=work,
path=planning_path,
root=root,
authority=DocumentAuthority.PLANNING,
document_type=DocumentType.PLANNING,
)
pack, _ranked = build_scene_context_pack(
work=work,
query="Sabine boundary planning detail",
authorities=[DocumentAuthority.CANON, DocumentAuthority.PLANNING],
pinned_document_keys=["planning.md"],
limit=6,
)
authorities = {citation["authority"] for citation in pack["citations"]}
assert authorities == {DocumentAuthority.CANON, DocumentAuthority.PLANNING}
def test_governing_document_is_supplied_in_full_before_rag(tmp_path: Path) -> None:
work = scene_work(tmp_path)
root = tmp_path / "governing-sources"
root.mkdir()
governing_path = root / "rules.md"
canon_path = root / "canon.md"
governing_text = "# Rules\n\nFirst governing rule.\n\nFinal governing rule.\n"
governing_path.write_text(governing_text, encoding="utf-8")
canon_path.write_text("Sabine has an additional canon fact.", encoding="utf-8")
register_source(
work=work,
path=governing_path,
root=root,
authority=DocumentAuthority.PLANNING,
document_type=DocumentType.PLANNING,
)
register_source(
work=work,
path=canon_path,
root=root,
authority=DocumentAuthority.CANON,
document_type=DocumentType.CANON,
)
pack, _ranked = build_scene_context_pack(
work=work,
query="Sabine canon",
authorities=[DocumentAuthority.CANON, DocumentAuthority.PLANNING],
governing_document_keys=["rules.md"],
limit=2,
)
assert pack["governing_document_keys"] == ["rules.md"]
assert pack["citations"][0]["kind"] == "governing_document"
assert pack["citations"][0]["sha256"]
assert "# Rules" in pack["rendered_context"]
assert "First governing rule." in pack["rendered_context"]
assert "Final governing rule." in pack["rendered_context"]
assert "additional canon fact" in pack["rendered_context"]
def test_standalone_scene_runs_from_cited_plan_to_provisional_approval(tmp_path: Path) -> None:
work = scene_work(tmp_path)
add_canon_source(work, tmp_path)
provider = FakeSceneProvider()
service = StandaloneSceneService(ModelRouter({"terra": provider, "luna": provider}))
scene = service.create(
work=work,
title="Fourteen Seconds",
brief="Sabine interrupts Corin's ordinary work, gives him a deliberate answer, and leaves.",
target_words=300,
constraints=["Sabine owns the timing."],
forbidden_events=["Do not turn this into a scored turn."],
boundary_constraints=["Stop when Sabine leaves the office."],
)
service.prepare_context(scene)
assert scene.status == SceneDraftStatus.PLANNING
assert scene.context_citations.count() == 1
service.plan(scene)
assert scene.status == SceneDraftStatus.PLAN_REVIEW
assert scene.context_citations.count() == 1
assert scene.context_pack["citations"][0]["authority"] == DocumentAuthority.CANON
assert scene.contract_requirements
blocking_beats = [
item
for item in scene.contract_requirements
if item["type"] == "BEAT" and item["blocking"]
]
assert len(blocking_beats) == 5
service.approve_plan(scene)
service.write(scene)
scene.prose = scene.prose.removeprefix("# Fourteen Seconds\n\n")
scene.save()
service.review(scene)
service.approve(scene, actor="test")
scene.refresh_from_db()
assert scene.status == SceneDraftStatus.APPROVED
assert scene.prose.startswith("# Fourteen Seconds\n\n")
assert scene.word_count >= 250
assert scene.review["passed"] is True
assert scene.source_version.authority == DocumentAuthority.PROVISIONAL
assert len(scene.generation_metadata["planning"]["prompt_sha256"]) == 64
assert len(scene.generation_metadata["prose"]["prompt_sha256"]) == 64
assert scene.generation_metadata["approval"]["forced"] is False
assert Path(scene.artifact_uri).exists()
assert Path(scene.review_artifact_uri).exists()
scene.prose += " Changed after approval."
with pytest.raises(ValueError, match="immutable"):
scene.save()