test contracts

This commit is contained in:
2026-02-26 19:40:00 +03:00
parent 626449604f
commit 2b8e20981e
35 changed files with 1811 additions and 759 deletions

View File

@@ -47,6 +47,19 @@ class ReportStatus(str, Enum):
# @INVARIANT: The properties accurately describe error state.
# @SEMANTICS: error, context, payload
# @PURPOSE: Error and recovery context for failed/partial reports.
#
# @TEST_CONTRACT: ErrorContextModel ->
# {
# required_fields: {
# message: str
# },
# optional_fields: {
# code: str,
# next_actions: list[str]
# }
# }
# @TEST_FIXTURE: basic_error -> {"message": "Connection timeout", "code": "ERR_504", "next_actions": ["retry"]}
# @TEST_EDGE: missing_message -> {"code": "ERR_504"}
class ErrorContext(BaseModel):
code: Optional[str] = None
message: str
@@ -59,6 +72,36 @@ class ErrorContext(BaseModel):
# @INVARIANT: Must represent canonical task record attributes.
# @SEMANTICS: report, model, summary
# @PURPOSE: Canonical normalized report envelope for one task execution.
#
# @TEST_CONTRACT: TaskReportModel ->
# {
# required_fields: {
# report_id: str,
# task_id: str,
# task_type: TaskType,
# status: ReportStatus,
# updated_at: datetime,
# summary: str
# },
# invariants: [
# "report_id is a non-empty string",
# "task_id is a non-empty string",
# "summary is a non-empty string"
# ]
# }
# @TEST_FIXTURE: valid_task_report ->
# {
# report_id: "rep-123",
# task_id: "task-456",
# task_type: "migration",
# status: "success",
# updated_at: "2026-02-26T12:00:00Z",
# summary: "Migration completed successfully"
# }
# @TEST_EDGE: empty_report_id -> {"report_id": " ", "task_id": "task-456", "task_type": "migration", "status": "success", "updated_at": "2026-02-26T12:00:00Z", "summary": "Done"}
# @TEST_EDGE: empty_summary -> {"report_id": "rep-123", "task_id": "task-456", "task_type": "migration", "status": "success", "updated_at": "2026-02-26T12:00:00Z", "summary": ""}
# @TEST_EDGE: invalid_task_type -> {"report_id": "rep-123", "task_id": "task-456", "task_type": "invalid_type", "status": "success", "updated_at": "2026-02-26T12:00:00Z", "summary": "Done"}
# @TEST_INVARIANT: non_empty_validators -> verifies: [empty_report_id, empty_summary]
class TaskReport(BaseModel):
report_id: str
task_id: str
@@ -85,6 +128,25 @@ class TaskReport(BaseModel):
# @INVARIANT: Time and pagination queries are mutually consistent.
# @SEMANTICS: query, filter, search
# @PURPOSE: Query object for server-side report filtering, sorting, and pagination.
#
# @TEST_CONTRACT: ReportQueryModel ->
# {
# optional_fields: {
# page: int, page_size: int, task_types: list[TaskType], statuses: list[ReportStatus],
# time_from: datetime, time_to: datetime, search: str, sort_by: str, sort_order: str
# },
# invariants: [
# "page >= 1", "1 <= page_size <= 100",
# "sort_by in {'updated_at', 'status', 'task_type'}",
# "sort_order in {'asc', 'desc'}",
# "time_from <= time_to if both exist"
# ]
# }
# @TEST_FIXTURE: valid_query -> {"page": 1, "page_size":20, "sort_by": "updated_at", "sort_order": "desc"}
# @TEST_EDGE: invalid_page_size_large -> {"page_size": 150}
# @TEST_EDGE: invalid_sort_by -> {"sort_by": "unknown_field"}
# @TEST_EDGE: invalid_time_range -> {"time_from": "2026-02-26T12:00:00Z", "time_to": "2026-02-25T12:00:00Z"}
# @TEST_INVARIANT: attribute_constraints_enforced -> verifies: [invalid_page_size_large, invalid_sort_by, invalid_time_range]
class ReportQuery(BaseModel):
page: int = Field(default=1, ge=1)
page_size: int = Field(default=20, ge=1, le=100)
@@ -124,6 +186,16 @@ class ReportQuery(BaseModel):
# @INVARIANT: Represents paginated data correctly.
# @SEMANTICS: collection, pagination
# @PURPOSE: Paginated collection of normalized task reports.
#
# @TEST_CONTRACT: ReportCollectionModel ->
# {
# required_fields: {
# items: list[TaskReport], total: int, page: int, page_size: int, has_next: bool, applied_filters: ReportQuery
# },
# invariants: ["total >= 0", "page >= 1", "page_size >= 1"]
# }
# @TEST_FIXTURE: empty_collection -> {"items": [], "total": 0, "page": 1, "page_size": 20, "has_next": False, "applied_filters": {}}
# @TEST_EDGE: negative_total -> {"items": [], "total": -5, "page": 1, "page_size": 20, "has_next": False, "applied_filters": {}}
class ReportCollection(BaseModel):
items: List[TaskReport]
total: int = Field(ge=0)
@@ -139,6 +211,14 @@ class ReportCollection(BaseModel):
# @INVARIANT: Incorporates a report and logs correctly.
# @SEMANTICS: view, detail, logs
# @PURPOSE: Detailed report representation including diagnostics and recovery actions.
#
# @TEST_CONTRACT: ReportDetailViewModel ->
# {
# required_fields: {report: TaskReport},
# optional_fields: {timeline: list[dict], diagnostics: dict, next_actions: list[str]}
# }
# @TEST_FIXTURE: valid_detail -> {"report": {"report_id": "rep-1", "task_id": "task-1", "task_type": "backup", "status": "success", "updated_at": "2026-02-26T12:00:00Z", "summary": "Done"}}
# @TEST_EDGE: missing_report -> {}
class ReportDetailView(BaseModel):
report: TaskReport
timeline: List[Dict[str, Any]] = Field(default_factory=list)

View File

@@ -39,6 +39,61 @@ class TaskRecord(Base):
# @TIER: CRITICAL
# @RELATION: DEPENDS_ON -> TaskRecord
# @INVARIANT: Each log entry belongs to exactly one task.
#
# @TEST_CONTRACT: TaskLogCreate ->
# {
# required_fields: {
# task_id: str,
# timestamp: datetime,
# level: str,
# source: str,
# message: str
# },
# optional_fields: {
# metadata_json: str,
# id: int
# },
# invariants: [
# "task_id matches an existing TaskRecord.id"
# ]
# }
#
# @TEST_FIXTURE: basic_info_log ->
# {
# task_id: "00000000-0000-0000-0000-000000000000",
# timestamp: "2026-02-26T12:00:00Z",
# level: "INFO",
# source: "system",
# message: "Task initialization complete"
# }
#
# @TEST_EDGE: missing_required_field ->
# {
# timestamp: "2026-02-26T12:00:00Z",
# level: "ERROR",
# source: "system",
# message: "Missing task_id"
# }
#
# @TEST_EDGE: invalid_type ->
# {
# task_id: "00000000-0000-0000-0000-000000000000",
# timestamp: "2026-02-26T12:00:00Z",
# level: 500,
# source: "system",
# message: "Integer level"
# }
#
# @TEST_EDGE: empty_message ->
# {
# task_id: "00000000-0000-0000-0000-000000000000",
# timestamp: "2026-02-26T12:00:00Z",
# level: "DEBUG",
# source: "system",
# message: ""
# }
#
# @TEST_INVARIANT: exact_one_task_association -> verifies: [basic_info_log, missing_required_field]
class TaskLogRecord(Base):
__tablename__ = "task_logs"