| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164 |
- from dataclasses import asdict
- import json
- import pytest
- from agent.orchestration.models import (
- AcceptanceCriterion,
- TaskSpec,
- json_values,
- )
- from agent.orchestration.wire import TaskSpecView
- def criterion(criterion_id="criterion-1", description="The result is correct", hard=True):
- return AcceptanceCriterion(criterion_id, description, hard)
- def test_acceptance_criterion_normalizes_text_and_requires_boolean_hard():
- value = AcceptanceCriterion(" criterion-1 ", " The result is correct ")
- assert value.criterion_id == "criterion-1"
- assert value.description == "The result is correct"
- assert value.hard is True
- for criterion_id in ("", " ", None, 1):
- with pytest.raises(ValueError, match="criterion_id"):
- AcceptanceCriterion(criterion_id, "description")
- for description in ("", " ", None, 1):
- with pytest.raises(ValueError, match="description"):
- AcceptanceCriterion("criterion-1", description)
- for hard in (0, 1, "true", None):
- with pytest.raises(ValueError, match="hard"):
- AcceptanceCriterion("criterion-1", "description", hard)
- def test_acceptance_criterion_from_dict_generates_only_missing_id():
- generated = AcceptanceCriterion.from_dict({"description": "description"})
- assert generated.criterion_id
- assert AcceptanceCriterion.from_dict(
- {"id": " legacy-id ", "description": "description"}
- ).criterion_id == "legacy-id"
- for explicit_id in ("", " ", None):
- with pytest.raises(ValueError, match="criterion_id"):
- AcceptanceCriterion.from_dict(
- {"criterion_id": explicit_id, "description": "description"}
- )
- @pytest.mark.parametrize("version", [True, False, 0, -1, 1.0, "1", None])
- def test_task_spec_requires_a_positive_integer_version(version):
- with pytest.raises(ValueError, match="positive integer"):
- TaskSpec(version, "objective", [criterion()])
- @pytest.mark.parametrize("objective", ["", " ", None, 1])
- def test_task_spec_requires_a_non_empty_string_objective(objective):
- with pytest.raises(ValueError, match="objective"):
- TaskSpec(1, objective, [criterion()])
- def test_task_spec_requires_criteria_with_unique_normalized_ids():
- with pytest.raises(ValueError, match="at least one"):
- TaskSpec(1, "objective")
- with pytest.raises(ValueError, match="unique"):
- TaskSpec(
- 1,
- "objective",
- [criterion(" duplicate "), criterion("duplicate", "another check")],
- )
- value = TaskSpec(
- 1,
- "objective",
- [criterion("case"), criterion("CASE", "another check")],
- )
- assert [item.criterion_id for item in value.acceptance_criteria] == ["case", "CASE"]
- def test_task_spec_freezes_collections_and_does_not_track_source_mutation():
- criteria = [criterion()]
- context_refs = ["context-1"]
- value = TaskSpec(1, " objective ", criteria, context_refs)
- criteria.append(criterion("criterion-2", "another check"))
- context_refs.append("context-2")
- assert value.objective == "objective"
- assert isinstance(value.acceptance_criteria, tuple)
- assert isinstance(value.context_refs, tuple)
- assert len(value.acceptance_criteria) == 1
- assert value.context_refs == ("context-1",)
- @pytest.mark.parametrize(
- "context_refs",
- ["context", {"context": True}, None, [""], [" "], [1], [[]]],
- )
- def test_task_spec_requires_immutable_string_context_refs(context_refs):
- with pytest.raises(ValueError, match="context_refs|collections"):
- TaskSpec(1, "objective", [criterion()], context_refs)
- with pytest.raises(ValueError, match="context_refs|collections"):
- TaskSpec.from_dict({
- "objective": "objective",
- "acceptance_criteria": [{"description": "description"}],
- "context_refs": context_refs,
- })
- value = TaskSpec(1, "objective", [criterion()], [" context-1 "])
- assert value.context_refs == ("context-1",)
- def test_task_spec_from_dict_is_strict_and_generates_stable_id_for_instance():
- value = TaskSpec.from_dict(
- {
- "objective": "objective",
- "acceptance_criteria": [{"description": "description"}],
- }
- )
- assert value.version == 1
- assert value.acceptance_criteria[0].criterion_id
- restored = TaskSpec.from_dict(json.loads(json.dumps(json_values(asdict(value)))))
- assert restored.acceptance_criteria[0].criterion_id == value.acceptance_criteria[0].criterion_id
- with pytest.raises(ValueError, match="positive integer"):
- TaskSpec.from_dict(
- {
- "version": "1",
- "objective": "objective",
- "acceptance_criteria": [{"description": "description"}],
- }
- )
- @pytest.mark.parametrize("criteria", [None, "criterion", {"description": "description"}])
- def test_task_spec_from_dict_rejects_invalid_criteria_collections(criteria):
- with pytest.raises(ValueError, match="acceptance_criteria"):
- TaskSpec.from_dict({
- "objective": "objective",
- "acceptance_criteria": criteria,
- })
- def test_task_spec_wire_shape_keeps_json_arrays():
- value = TaskSpec(
- 1,
- "objective",
- [criterion()],
- ["context-1"],
- created_at="2026-01-01T00:00:00+00:00",
- )
- payload = json_values(asdict(value))
- wire = TaskSpecView.model_validate(payload).model_dump(mode="json")
- assert wire["acceptance_criteria"] == [
- {
- "criterion_id": "criterion-1",
- "description": "The result is correct",
- "hard": True,
- }
- ]
- assert wire["context_refs"] == ["context-1"]
- assert isinstance(json.loads(json.dumps(payload))["acceptance_criteria"], list)
|