|
|
@@ -78,7 +78,14 @@ class _MissionService:
|
|
|
|
|
|
class _Bindings:
|
|
|
async def get_by_build(self, script_build_id: int) -> object:
|
|
|
- return SimpleNamespace(script_build_id=script_build_id, root_trace_id="root-7")
|
|
|
+ return SimpleNamespace(
|
|
|
+ script_build_id=script_build_id,
|
|
|
+ root_trace_id="root-7",
|
|
|
+ input_snapshot_id=11,
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+_CONTRACT_REF = "script-build://task-contracts/sha256/" + "c" * 64
|
|
|
|
|
|
|
|
|
class _TaskStore:
|
|
|
@@ -88,7 +95,7 @@ class _TaskStore:
|
|
|
version=1,
|
|
|
objective="bounded task",
|
|
|
acceptance_criteria=(criterion,),
|
|
|
- context_refs=("script-build://task-kinds/paragraph",),
|
|
|
+ context_refs=("script-build://task-kinds/paragraph", _CONTRACT_REF),
|
|
|
)
|
|
|
self.task = SimpleNamespace(
|
|
|
task_id="task-7",
|
|
|
@@ -96,6 +103,8 @@ class _TaskStore:
|
|
|
display_path="Root/task-7",
|
|
|
status="completed",
|
|
|
current_spec=current_spec,
|
|
|
+ current_spec_version=1,
|
|
|
+ specs=[current_spec],
|
|
|
child_task_ids=(),
|
|
|
attempt_ids=(),
|
|
|
validation_ids=(),
|
|
|
@@ -118,12 +127,78 @@ class _TaskStore:
|
|
|
)
|
|
|
|
|
|
|
|
|
+class _Contract:
|
|
|
+ def to_payload(self) -> dict[str, object]:
|
|
|
+ return {
|
|
|
+ "schema_version": "script-task-contract/v1",
|
|
|
+ "task_kind": "paragraph",
|
|
|
+ "scope_ref": "script-build://scopes/body",
|
|
|
+ "intent_class": "explore",
|
|
|
+ "objective": "write the evidence paragraph",
|
|
|
+ "input_decision_refs": [],
|
|
|
+ "base_artifact_ref": None,
|
|
|
+ "write_scope": ["script-build://scopes/body"],
|
|
|
+ "gap_ref": None,
|
|
|
+ "output_schema": "paragraph-artifact/v1",
|
|
|
+ "criteria": [
|
|
|
+ {"criterion_id": "grounded", "description": "grounded", "hard": True}
|
|
|
+ ],
|
|
|
+ "budget": {"max_tokens": 1},
|
|
|
+ "goal_ids": ["goal-1"],
|
|
|
+ "supersedes_decision_ids": [],
|
|
|
+ "candidate_closure_decision_refs": [],
|
|
|
+ "adopted_decision_ids": [],
|
|
|
+ "held_or_rejected_decision_ids": [],
|
|
|
+ "compose_order": [],
|
|
|
+ "comparison_decision_refs": [],
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+class _Contracts:
|
|
|
+ async def read(self, root_trace_id: str, uri: str) -> object:
|
|
|
+ assert root_trace_id == "root-7"
|
|
|
+ assert uri == _CONTRACT_REF
|
|
|
+ return SimpleNamespace(
|
|
|
+ uri=uri,
|
|
|
+ digest="sha256:" + "c" * 64,
|
|
|
+ contract=_Contract(),
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+class _InputSnapshots:
|
|
|
+ async def get(self, snapshot_id: str, *, script_build_id: int) -> object:
|
|
|
+ assert snapshot_id == "11"
|
|
|
+ assert script_build_id == 7
|
|
|
+ return SimpleNamespace(
|
|
|
+ snapshot_id="11",
|
|
|
+ script_build_id=7,
|
|
|
+ execution_id=1,
|
|
|
+ topic_build_id=2,
|
|
|
+ topic_id=3,
|
|
|
+ topic={"topic": {"result": "How one reversal changes a belief"}},
|
|
|
+ account={"account_name": "acct", "resolved_account_name": "acct"},
|
|
|
+ persona_points=({"point_type": "voice"},),
|
|
|
+ section_patterns=({}, {}),
|
|
|
+ strategies=(
|
|
|
+ {
|
|
|
+ "id": 5,
|
|
|
+ "name": "contrast",
|
|
|
+ "description": "show the turn clearly",
|
|
|
+ "content": "private strategy body",
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ canonical_sha256="sha256:" + "d" * 64,
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
def _app(
|
|
|
principal: Principal | None,
|
|
|
*,
|
|
|
trace_store: object | None = None,
|
|
|
uploaded_topics: object | None = None,
|
|
|
business_artifacts: object | None = None,
|
|
|
+ task_contract_store: object | None = None,
|
|
|
+ input_snapshots: object | None = None,
|
|
|
) -> tuple[object, _MissionService]:
|
|
|
mission = _MissionService()
|
|
|
app = create_app(
|
|
|
@@ -143,6 +218,8 @@ def _app(
|
|
|
frozenset({"data.example"}),
|
|
|
resolver=_public_resolver,
|
|
|
),
|
|
|
+ task_contract_store=task_contract_store,
|
|
|
+ input_snapshots=input_snapshots,
|
|
|
)
|
|
|
return app, mission
|
|
|
|
|
|
@@ -333,6 +410,13 @@ class _TraceStore:
|
|
|
del since
|
|
|
return []
|
|
|
|
|
|
+ async def get_trace_messages(self, trace_id: str) -> list[object]:
|
|
|
+ assert trace_id in {"root-7", "worker-7"}
|
|
|
+ return [
|
|
|
+ SimpleNamespace(sequence=value, to_dict=lambda value=value: {"sequence": value})
|
|
|
+ for value in (1, 2, 3)
|
|
|
+ ]
|
|
|
+
|
|
|
|
|
|
class _BusinessArtifacts:
|
|
|
def __init__(self) -> None:
|
|
|
@@ -432,6 +516,14 @@ async def test_phase_two_observation_hides_foreign_resources_and_returns_owned_a
|
|
|
assert owned_trace.json()["trace"]["trace_id"] == "worker-7"
|
|
|
assert owned_trace.json()["goal_tree"] is None
|
|
|
|
|
|
+ incremental_messages = await client.get(
|
|
|
+ "/api/pattern/script_builds/7/traces/worker-7/messages?after_sequence=1"
|
|
|
+ )
|
|
|
+ assert incremental_messages.status_code == 200
|
|
|
+ assert incremental_messages.json() == {
|
|
|
+ "messages": [{"sequence": 2}, {"sequence": 3}]
|
|
|
+ }
|
|
|
+
|
|
|
foreign_trace = await client.get("/api/pattern/script_builds/7/traces/worker-8")
|
|
|
assert foreign_trace.status_code == 404
|
|
|
assert foreign_trace.json()["detail"]["error_code"] == "TRACE_NOT_FOUND"
|
|
|
@@ -441,6 +533,38 @@ async def test_phase_two_observation_hides_foreign_resources_and_returns_owned_a
|
|
|
assert forged_root.json()["detail"]["error_code"] == "ROOT_NOT_FOUND"
|
|
|
|
|
|
|
|
|
+@pytest.mark.asyncio
|
|
|
+async def test_safe_contract_and_input_summaries_are_build_scoped() -> None:
|
|
|
+ app, _ = _app(
|
|
|
+ Principal("owner"),
|
|
|
+ task_contract_store=_Contracts(),
|
|
|
+ input_snapshots=_InputSnapshots(),
|
|
|
+ )
|
|
|
+ async with httpx.AsyncClient(
|
|
|
+ transport=httpx.ASGITransport(app=app), base_url="http://test"
|
|
|
+ ) as client:
|
|
|
+ contract = await client.get(
|
|
|
+ "/api/pattern/script_builds/7/tasks/task-7/contract"
|
|
|
+ )
|
|
|
+ assert contract.status_code == 200, contract.text
|
|
|
+ assert contract.json()["task_kind"] == "paragraph"
|
|
|
+ assert contract.json()["goal_ids"] == ["goal-1"]
|
|
|
+ assert contract.json()["current_spec_version"] == 1
|
|
|
+ assert len(contract.json()["versions"]) == 1
|
|
|
+ assert "budget" not in contract.json()
|
|
|
+
|
|
|
+ summary = await client.get("/api/pattern/script_builds/7/input-summary")
|
|
|
+ assert summary.status_code == 200, summary.text
|
|
|
+ payload = summary.json()
|
|
|
+ assert payload["topic"] == "How one reversal changes a belief"
|
|
|
+ assert payload["persona"]["point_types"] == {"voice": 1}
|
|
|
+ assert payload["strategies"][0]["name"] == "contrast"
|
|
|
+ assert "private strategy body" not in summary.text
|
|
|
+
|
|
|
+ denied = await client.get("/api/pattern/script_builds/8/input-summary")
|
|
|
+ assert denied.status_code == 404
|
|
|
+
|
|
|
+
|
|
|
class _UploadedTopics:
|
|
|
async def parse(self, _value: object) -> dict[str, object]:
|
|
|
return {
|