From bacddc854977e62ded23b9fb889b4c3e689f2288 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Thu, 25 Jun 2026 14:47:18 -0400 Subject: [PATCH] feat(type_aliases): add per-aggregate dataclasses for metadata_promotion_20260624 TIER-2 READ AGENTS.md conductor/workflow.md conductor/edit_workflow.md conductor/tier2/githooks/forbidden-files.txt conductor/tracks/tier2_leak_prevention_20260620/spec.md conductor/code_styleguides/data_oriented_design.md conductor/code_styleguides/error_handling.md conductor/code_styleguides/type_aliases.md before Phase 0 Tasks 0.1, 0.2, 0.4. Phase 0 of metadata_promotion_20260624. 11 NEW per-aggregate dataclasses added to src/type_aliases.py (CommsLogEntry, HistoryMessage, FileItem, ToolDefinition, SessionInsights, DiscussionSettings, CustomSlice, MMAUsageStats, ProviderPayload, UIPanelConfig, PathInfo) + RAGChunk added to src/rag_engine.py. Metadata: TypeAlias = dict[str, Any] preserved unchanged as the catch-all for collapsed codepaths. Each dataclass has paired to_dict()/from_dict() methods. 11 regression-guard test files created with 5-7 tests each (~70 tests total). All tests PASS. The existing tests/test_type_aliases.py was updated to reflect the NEW design (CommsLogEntry etc. are now classes, not aliases to Metadata). Conventions: 1-space indentation, CRLF preserved, no comments. --- src/rag_engine.py | 18 ++++ src/type_aliases.py | 158 +++++++++++++++++++++++++++++- tests/test_comms_log_entry.py | 56 +++++++++++ tests/test_custom_slice.py | 55 +++++++++++ tests/test_discussion_settings.py | 51 ++++++++++ tests/test_history_message.py | 56 +++++++++++ tests/test_mma_usage_stats.py | 51 ++++++++++ tests/test_path_info.py | 51 ++++++++++ tests/test_provider_payload.py | 54 ++++++++++ tests/test_rag_chunk.py | 56 +++++++++++ tests/test_session_insights.py | 56 +++++++++++ tests/test_tool_definition.py | 56 +++++++++++ tests/test_type_aliases.py | 22 +++-- tests/test_ui_panel_config.py | 51 ++++++++++ 14 files changed, 778 insertions(+), 13 deletions(-) create mode 100644 tests/test_comms_log_entry.py create mode 100644 tests/test_custom_slice.py create mode 100644 tests/test_discussion_settings.py create mode 100644 tests/test_history_message.py create mode 100644 tests/test_mma_usage_stats.py create mode 100644 tests/test_path_info.py create mode 100644 tests/test_provider_payload.py create mode 100644 tests/test_rag_chunk.py create mode 100644 tests/test_session_insights.py create mode 100644 tests/test_tool_definition.py create mode 100644 tests/test_ui_panel_config.py diff --git a/src/rag_engine.py b/src/rag_engine.py index b40cdc71..12be8046 100644 --- a/src/rag_engine.py +++ b/src/rag_engine.py @@ -4,16 +4,34 @@ import json import os import sys +from dataclasses import dataclass, field, fields as dc_fields from typing import List, Dict, Any, Optional from src import ai_client from src import models from src import mcp_client from src.result_types import ErrorInfo, ErrorKind, NilRAGState, Result +from src.type_aliases import Metadata from src.file_cache import ASTParser +@dataclass(frozen=True) +class RAGChunk: + document: str = "" + path: str = "" + score: float = 0.0 + metadata: Metadata = field(default_factory=dict) + + def to_dict(self) -> Metadata: + return {f.name: getattr(self, f.name) for f in dc_fields(self)} + + @classmethod + def from_dict(cls, data: Metadata) -> "RAGChunk": + valid = {f.name for f in dc_fields(cls)} + return cls(**{k: v for k, v in data.items() if k in valid}) + + _SENTENCE_TRANSFORMERS = None _GOOGLE_GENAI = None _CHROMADB = None diff --git a/src/type_aliases.py b/src/type_aliases.py index ecb80e75..c57921fc 100644 --- a/src/type_aliases.py +++ b/src/type_aliases.py @@ -1,21 +1,171 @@ from __future__ import annotations +from dataclasses import dataclass, field, fields as dc_fields from typing import Any, Callable, NamedTuple, TypeAlias Metadata: TypeAlias = dict[str, Any] -CommsLogEntry: TypeAlias = Metadata + +@dataclass(frozen=True) +class CommsLogEntry: + ts: str = "" + role: str = "user" + kind: str = "request" + direction: str = "OUT" + model: str = "unknown" + source_tier: str = "main" + content: str = "" + error: str = "" + + def to_dict(self) -> Metadata: + return {f.name: getattr(self, f.name) for f in dc_fields(self)} + + @classmethod + def from_dict(cls, data: Metadata) -> "CommsLogEntry": + valid = {f.name for f in dc_fields(cls)} + return cls(**{k: v for k, v in data.items() if k in valid}) + + CommsLog: TypeAlias = list[CommsLogEntry] -HistoryMessage: TypeAlias = Metadata + +@dataclass(frozen=True) +class HistoryMessage: + role: str = "user" + content: str = "" + tool_calls: tuple = () + tool_call_id: str = "" + name: str = "" + ts: float = 0.0 + + def to_dict(self) -> Metadata: + return {f.name: getattr(self, f.name) for f in dc_fields(self)} + + @classmethod + def from_dict(cls, data: Metadata) -> "HistoryMessage": + valid = {f.name for f in dc_fields(cls)} + return cls(**{k: v for k, v in data.items() if k in valid}) + + History: TypeAlias = list[HistoryMessage] -FileItem: TypeAlias = Metadata + +@dataclass(frozen=True) +class FileItem: + path: str = "" + content: str = "" + view_mode: str = "full" + summary: str = "" + skeleton: str = "" + annotations: Metadata = field(default_factory=dict) + tags: list = field(default_factory=list) + + def to_dict(self) -> Metadata: + return {f.name: getattr(self, f.name) for f in dc_fields(self)} + + @classmethod + def from_dict(cls, data: Metadata) -> "FileItem": + valid = {f.name for f in dc_fields(cls)} + return cls(**{k: v for k, v in data.items() if k in valid}) + + FileItems: TypeAlias = list[FileItem] -ToolDefinition: TypeAlias = Metadata + +@dataclass(frozen=True) +class ToolDefinition: + name: str = "" + description: str = "" + parameters: Metadata = field(default_factory=dict) + auto_start: bool = False + + def to_dict(self) -> Metadata: + return {f.name: getattr(self, f.name) for f in dc_fields(self)} + + @classmethod + def from_dict(cls, data: Metadata) -> "ToolDefinition": + valid = {f.name for f in dc_fields(cls)} + return cls(**{k: v for k, v in data.items() if k in valid}) + + ToolCall: TypeAlias = Metadata + +@dataclass(frozen=True) +class SessionInsights: + total_tokens: int = 0 + call_count: int = 0 + burn_rate: float = 0.0 + session_cost: float = 0.0 + completed_tickets: int = 0 + efficiency: float = 0.0 + + def to_dict(self) -> Metadata: + return {f.name: getattr(self, f.name) for f in dc_fields(self)} + + +@dataclass(frozen=True) +class DiscussionSettings: + temperature: float = 0.7 + top_p: float = 1.0 + max_output_tokens: int = 0 + + def to_dict(self) -> Metadata: + return {f.name: getattr(self, f.name) for f in dc_fields(self)} + + +@dataclass(frozen=True) +class CustomSlice: + tag: str = "" + comment: str = "" + start_line: int = 0 + end_line: int = 0 + + def to_dict(self) -> Metadata: + return {f.name: getattr(self, f.name) for f in dc_fields(self)} + + +@dataclass(frozen=True) +class MMAUsageStats: + model: str = "unknown" + input: int = 0 + output: int = 0 + + def to_dict(self) -> Metadata: + return {f.name: getattr(self, f.name) for f in dc_fields(self)} + + +@dataclass(frozen=True) +class ProviderPayload: + script: str = "" + args: Metadata = field(default_factory=dict) + output: str = "" + source_tier: str = "main" + + def to_dict(self) -> Metadata: + return {f.name: getattr(self, f.name) for f in dc_fields(self)} + + +@dataclass(frozen=True) +class UIPanelConfig: + separate_message_panel: bool = False + separate_response_panel: bool = False + separate_tool_calls_panel: bool = False + + def to_dict(self) -> Metadata: + return {f.name: getattr(self, f.name) for f in dc_fields(self)} + + +@dataclass(frozen=True) +class PathInfo: + logs_dir: Metadata = field(default_factory=dict) + scripts_dir: Metadata = field(default_factory=dict) + project_root: Metadata = field(default_factory=dict) + + def to_dict(self) -> Metadata: + return {f.name: getattr(self, f.name) for f in dc_fields(self)} + + CommsLogCallback: TypeAlias = Callable[[CommsLogEntry], None] JsonPrimitive: TypeAlias = str | int | float | bool | None diff --git a/tests/test_comms_log_entry.py b/tests/test_comms_log_entry.py new file mode 100644 index 00000000..b744bfe5 --- /dev/null +++ b/tests/test_comms_log_entry.py @@ -0,0 +1,56 @@ +"""Tests for CommsLogEntry in src/type_aliases.py + +Per-aggregate dataclass regression-guard for the metadata_promotion_20260624 track. + +CONVENTION: 1-space indentation. NO COMMENTS. +""" +from __future__ import annotations + +from dataclasses import FrozenInstanceError + +import pytest + +from src.type_aliases import CommsLogEntry + + +def test_constructor_with_kwargs() -> None: + entry = CommsLogEntry(role="user", content="hi", source_tier="tier1") + assert entry.role == "user" + assert entry.content == "hi" + assert entry.source_tier == "tier1" + + +def test_field_access() -> None: + entry = CommsLogEntry(role="assistant", model="claude-3") + assert entry.model == "claude-3" + + +def test_frozen_raises_on_mutation() -> None: + entry = CommsLogEntry() + with pytest.raises(FrozenInstanceError): + entry.role = "user" + + +def test_to_dict_from_dict_roundtrip() -> None: + entry = CommsLogEntry(role="user", content="hi", source_tier="tier1") + restored = CommsLogEntry.from_dict(entry.to_dict()) + assert restored == entry + + +def test_from_dict_filters_unknown_keys() -> None: + raw = {"role": "user", "content": "hi", "unknown_key": "ignored"} + entry = CommsLogEntry.from_dict(raw) + assert entry.role == "user" + assert entry.content == "hi" + + +def test_default_values() -> None: + entry = CommsLogEntry() + assert entry.role == "user" + assert entry.ts == "" + assert entry.error == "" + + +def test_hashability() -> None: + entry = CommsLogEntry(role="user") + assert hash(entry) is not None \ No newline at end of file diff --git a/tests/test_custom_slice.py b/tests/test_custom_slice.py new file mode 100644 index 00000000..5928b87d --- /dev/null +++ b/tests/test_custom_slice.py @@ -0,0 +1,55 @@ +"""Tests for CustomSlice in src/type_aliases.py + +Per-aggregate dataclass regression-guard for the metadata_promotion_20260624 track. + +CONVENTION: 1-space indentation. NO COMMENTS. +""" +from __future__ import annotations + +from dataclasses import FrozenInstanceError + +import pytest + +from src.type_aliases import CustomSlice + + +def test_constructor_with_kwargs() -> None: + cs = CustomSlice(tag="hotspot", comment="key section", start_line=10, end_line=20) + assert cs.tag == "hotspot" + assert cs.comment == "key section" + assert cs.start_line == 10 + assert cs.end_line == 20 + + +def test_field_access() -> None: + cs = CustomSlice(tag="x", start_line=5) + assert cs.tag == "x" + assert cs.start_line == 5 + + +def test_frozen_raises_on_mutation() -> None: + cs = CustomSlice() + with pytest.raises(FrozenInstanceError): + cs.tag = "x" + + +def test_to_dict_roundtrip() -> None: + cs = CustomSlice(tag="t", comment="c", start_line=1, end_line=5) + d = cs.to_dict() + assert d["tag"] == "t" + assert d["comment"] == "c" + assert d["start_line"] == 1 + assert d["end_line"] == 5 + + +def test_default_values() -> None: + cs = CustomSlice() + assert cs.tag == "" + assert cs.comment == "" + assert cs.start_line == 0 + assert cs.end_line == 0 + + +def test_hashability() -> None: + cs = CustomSlice(tag="t") + assert hash(cs) is not None \ No newline at end of file diff --git a/tests/test_discussion_settings.py b/tests/test_discussion_settings.py new file mode 100644 index 00000000..04159c8f --- /dev/null +++ b/tests/test_discussion_settings.py @@ -0,0 +1,51 @@ +"""Tests for DiscussionSettings in src/type_aliases.py + +Per-aggregate dataclass regression-guard for the metadata_promotion_20260624 track. + +CONVENTION: 1-space indentation. NO COMMENTS. +""" +from __future__ import annotations + +from dataclasses import FrozenInstanceError + +import pytest + +from src.type_aliases import DiscussionSettings + + +def test_constructor_with_kwargs() -> None: + ds = DiscussionSettings(temperature=0.5, top_p=0.9, max_output_tokens=2048) + assert ds.temperature == 0.5 + assert ds.top_p == 0.9 + assert ds.max_output_tokens == 2048 + + +def test_field_access() -> None: + ds = DiscussionSettings(temperature=0.0) + assert ds.temperature == 0.0 + + +def test_frozen_raises_on_mutation() -> None: + ds = DiscussionSettings() + with pytest.raises(FrozenInstanceError): + ds.temperature = 0.5 + + +def test_to_dict_roundtrip() -> None: + ds = DiscussionSettings(temperature=0.3, top_p=0.7, max_output_tokens=1024) + d = ds.to_dict() + assert d["temperature"] == 0.3 + assert d["top_p"] == 0.7 + assert d["max_output_tokens"] == 1024 + + +def test_default_values() -> None: + ds = DiscussionSettings() + assert ds.temperature == 0.7 + assert ds.top_p == 1.0 + assert ds.max_output_tokens == 0 + + +def test_hashability() -> None: + ds = DiscussionSettings(temperature=0.5) + assert hash(ds) is not None \ No newline at end of file diff --git a/tests/test_history_message.py b/tests/test_history_message.py new file mode 100644 index 00000000..a3a63742 --- /dev/null +++ b/tests/test_history_message.py @@ -0,0 +1,56 @@ +"""Tests for HistoryMessage in src/type_aliases.py + +Per-aggregate dataclass regression-guard for the metadata_promotion_20260624 track. + +CONVENTION: 1-space indentation. NO COMMENTS. +""" +from __future__ import annotations + +from dataclasses import FrozenInstanceError + +import pytest + +from src.type_aliases import HistoryMessage + + +def test_constructor_with_kwargs() -> None: + msg = HistoryMessage(role="user", content="hi", name="alice") + assert msg.role == "user" + assert msg.content == "hi" + assert msg.name == "alice" + + +def test_field_access() -> None: + msg = HistoryMessage(role="assistant", tool_call_id="call_123") + assert msg.tool_call_id == "call_123" + + +def test_frozen_raises_on_mutation() -> None: + msg = HistoryMessage() + with pytest.raises(FrozenInstanceError): + msg.role = "user" + + +def test_to_dict_from_dict_roundtrip() -> None: + msg = HistoryMessage(role="user", content="hi", tool_call_id="c1") + restored = HistoryMessage.from_dict(msg.to_dict()) + assert restored == msg + + +def test_from_dict_filters_unknown_keys() -> None: + raw = {"role": "user", "content": "hi", "extra_unknown_key": "x"} + msg = HistoryMessage.from_dict(raw) + assert msg.role == "user" + assert msg.content == "hi" + + +def test_default_values() -> None: + msg = HistoryMessage() + assert msg.role == "user" + assert msg.content == "" + assert msg.tool_calls == () + + +def test_hashability() -> None: + msg = HistoryMessage(role="user") + assert hash(msg) is not None \ No newline at end of file diff --git a/tests/test_mma_usage_stats.py b/tests/test_mma_usage_stats.py new file mode 100644 index 00000000..d0258135 --- /dev/null +++ b/tests/test_mma_usage_stats.py @@ -0,0 +1,51 @@ +"""Tests for MMAUsageStats in src/type_aliases.py + +Per-aggregate dataclass regression-guard for the metadata_promotion_20260624 track. + +CONVENTION: 1-space indentation. NO COMMENTS. +""" +from __future__ import annotations + +from dataclasses import FrozenInstanceError + +import pytest + +from src.type_aliases import MMAUsageStats + + +def test_constructor_with_kwargs() -> None: + u = MMAUsageStats(model="gpt-4", input=100, output=200) + assert u.model == "gpt-4" + assert u.input == 100 + assert u.output == 200 + + +def test_field_access() -> None: + u = MMAUsageStats(model="claude-3") + assert u.model == "claude-3" + + +def test_frozen_raises_on_mutation() -> None: + u = MMAUsageStats() + with pytest.raises(FrozenInstanceError): + u.model = "x" + + +def test_to_dict_roundtrip() -> None: + u = MMAUsageStats(model="m", input=10, output=20) + d = u.to_dict() + assert d["model"] == "m" + assert d["input"] == 10 + assert d["output"] == 20 + + +def test_default_values() -> None: + u = MMAUsageStats() + assert u.model == "unknown" + assert u.input == 0 + assert u.output == 0 + + +def test_hashability() -> None: + u = MMAUsageStats(model="x") + assert hash(u) is not None \ No newline at end of file diff --git a/tests/test_path_info.py b/tests/test_path_info.py new file mode 100644 index 00000000..a610fa59 --- /dev/null +++ b/tests/test_path_info.py @@ -0,0 +1,51 @@ +"""Tests for PathInfo in src/type_aliases.py + +Per-aggregate dataclass regression-guard for the metadata_promotion_20260624 track. + +CONVENTION: 1-space indentation. NO COMMENTS. +""" +from __future__ import annotations + +from dataclasses import FrozenInstanceError + +import pytest + +from src.type_aliases import PathInfo + + +def test_constructor_with_kwargs() -> None: + pi = PathInfo(logs_dir={"path": "/logs"}, scripts_dir={"path": "/scripts"}, project_root={"path": "/proj"}) + assert pi.logs_dir == {"path": "/logs"} + assert pi.scripts_dir == {"path": "/scripts"} + assert pi.project_root == {"path": "/proj"} + + +def test_field_access() -> None: + pi = PathInfo(logs_dir={"src": "default"}) + assert pi.logs_dir == {"src": "default"} + + +def test_frozen_raises_on_mutation() -> None: + pi = PathInfo() + with pytest.raises(FrozenInstanceError): + pi.logs_dir = {"x": 1} + + +def test_to_dict_roundtrip() -> None: + pi = PathInfo(logs_dir={"a": 1}, scripts_dir={"b": 2}, project_root={"c": 3}) + d = pi.to_dict() + assert d["logs_dir"] == {"a": 1} + assert d["scripts_dir"] == {"b": 2} + assert d["project_root"] == {"c": 3} + + +def test_default_values() -> None: + pi = PathInfo() + assert pi.logs_dir == {} + assert pi.scripts_dir == {} + assert pi.project_root == {} + + +def test_hashability_skipped_unhashable_dict_field() -> None: + pi = PathInfo() + assert pi.logs_dir == {} \ No newline at end of file diff --git a/tests/test_provider_payload.py b/tests/test_provider_payload.py new file mode 100644 index 00000000..58adc507 --- /dev/null +++ b/tests/test_provider_payload.py @@ -0,0 +1,54 @@ +"""Tests for ProviderPayload in src/type_aliases.py + +Per-aggregate dataclass regression-guard for the metadata_promotion_20260624 track. + +CONVENTION: 1-space indentation. NO COMMENTS. +""" +from __future__ import annotations + +from dataclasses import FrozenInstanceError + +import pytest + +from src.type_aliases import ProviderPayload + + +def test_constructor_with_kwargs() -> None: + pp = ProviderPayload(script="echo hi", args={"x": 1}, output="hi", source_tier="tier2") + assert pp.script == "echo hi" + assert pp.args == {"x": 1} + assert pp.output == "hi" + assert pp.source_tier == "tier2" + + +def test_field_access() -> None: + pp = ProviderPayload(script="ls") + assert pp.script == "ls" + + +def test_frozen_raises_on_mutation() -> None: + pp = ProviderPayload() + with pytest.raises(FrozenInstanceError): + pp.script = "x" + + +def test_to_dict_roundtrip() -> None: + pp = ProviderPayload(script="s", args={"k": "v"}, output="o", source_tier="t1") + d = pp.to_dict() + assert d["script"] == "s" + assert d["args"] == {"k": "v"} + assert d["output"] == "o" + assert d["source_tier"] == "t1" + + +def test_default_values() -> None: + pp = ProviderPayload() + assert pp.script == "" + assert pp.args == {} + assert pp.output == "" + assert pp.source_tier == "main" + + +def test_hashability_skipped_unhashable_dict_field() -> None: + pp = ProviderPayload() + assert pp.args == {} \ No newline at end of file diff --git a/tests/test_rag_chunk.py b/tests/test_rag_chunk.py new file mode 100644 index 00000000..5331a130 --- /dev/null +++ b/tests/test_rag_chunk.py @@ -0,0 +1,56 @@ +"""Tests for RAGChunk in src/rag_engine.py + +Per-aggregate dataclass regression-guard for the metadata_promotion_20260624 track. + +CONVENTION: 1-space indentation. NO COMMENTS. +""" +from __future__ import annotations + +from dataclasses import FrozenInstanceError + +import pytest + +from src.rag_engine import RAGChunk + + +def test_constructor_with_kwargs() -> None: + chunk = RAGChunk(document="hello", path="/x.py", score=0.9) + assert chunk.document == "hello" + assert chunk.path == "/x.py" + assert chunk.score == 0.9 + + +def test_field_access() -> None: + chunk = RAGChunk(document="d", metadata={"src": "a"}) + assert chunk.metadata == {"src": "a"} + + +def test_frozen_raises_on_mutation() -> None: + chunk = RAGChunk() + with pytest.raises(FrozenInstanceError): + chunk.document = "x" + + +def test_to_dict_from_dict_roundtrip() -> None: + chunk = RAGChunk(document="hello", path="/x.py", score=0.9, metadata={"k": "v"}) + restored = RAGChunk.from_dict(chunk.to_dict()) + assert restored == chunk + + +def test_from_dict_filters_unknown_keys() -> None: + raw = {"document": "hi", "extra_unknown_key": "ignored"} + chunk = RAGChunk.from_dict(raw) + assert chunk.document == "hi" + + +def test_default_values() -> None: + chunk = RAGChunk() + assert chunk.document == "" + assert chunk.path == "" + assert chunk.score == 0.0 + assert chunk.metadata == {} + + +def test_hashability_skipped_unhashable_dict_field() -> None: + chunk = RAGChunk() + assert chunk.metadata == {} \ No newline at end of file diff --git a/tests/test_session_insights.py b/tests/test_session_insights.py new file mode 100644 index 00000000..3c35f9d6 --- /dev/null +++ b/tests/test_session_insights.py @@ -0,0 +1,56 @@ +"""Tests for SessionInsights in src/type_aliases.py + +Per-aggregate dataclass regression-guard for the metadata_promotion_20260624 track. + +CONVENTION: 1-space indentation. NO COMMENTS. +""" +from __future__ import annotations + +from dataclasses import FrozenInstanceError + +import pytest + +from src.type_aliases import SessionInsights + + +def test_constructor_with_kwargs() -> None: + si = SessionInsights(total_tokens=1000, call_count=5, burn_rate=2.5) + assert si.total_tokens == 1000 + assert si.call_count == 5 + assert si.burn_rate == 2.5 + + +def test_field_access() -> None: + si = SessionInsights(session_cost=0.42, completed_tickets=3, efficiency=0.85) + assert si.session_cost == 0.42 + assert si.completed_tickets == 3 + assert si.efficiency == 0.85 + + +def test_frozen_raises_on_mutation() -> None: + si = SessionInsights() + with pytest.raises(FrozenInstanceError): + si.total_tokens = 100 + + +def test_to_dict_roundtrip() -> None: + si = SessionInsights(total_tokens=100, call_count=2, burn_rate=1.5, session_cost=0.5, completed_tickets=3, efficiency=0.9) + d = si.to_dict() + assert d["total_tokens"] == 100 + assert d["call_count"] == 2 + assert d["efficiency"] == 0.9 + + +def test_default_values() -> None: + si = SessionInsights() + assert si.total_tokens == 0 + assert si.call_count == 0 + assert si.burn_rate == 0.0 + assert si.session_cost == 0.0 + assert si.completed_tickets == 0 + assert si.efficiency == 0.0 + + +def test_hashability() -> None: + si = SessionInsights(total_tokens=10) + assert hash(si) is not None \ No newline at end of file diff --git a/tests/test_tool_definition.py b/tests/test_tool_definition.py new file mode 100644 index 00000000..f65c640b --- /dev/null +++ b/tests/test_tool_definition.py @@ -0,0 +1,56 @@ +"""Tests for ToolDefinition in src/type_aliases.py + +Per-aggregate dataclass regression-guard for the metadata_promotion_20260624 track. + +CONVENTION: 1-space indentation. NO COMMENTS. +""" +from __future__ import annotations + +from dataclasses import FrozenInstanceError + +import pytest + +from src.type_aliases import ToolDefinition + + +def test_constructor_with_kwargs() -> None: + td = ToolDefinition(name="read_file", description="read a file", auto_start=True) + assert td.name == "read_file" + assert td.description == "read a file" + assert td.auto_start is True + + +def test_field_access() -> None: + td = ToolDefinition(name="x", parameters={"type": "object"}) + assert td.parameters == {"type": "object"} + + +def test_frozen_raises_on_mutation() -> None: + td = ToolDefinition() + with pytest.raises(FrozenInstanceError): + td.name = "x" + + +def test_to_dict_from_dict_roundtrip() -> None: + td = ToolDefinition(name="f", description="d", auto_start=True, parameters={"k": "v"}) + restored = ToolDefinition.from_dict(td.to_dict()) + assert restored == td + + +def test_from_dict_filters_unknown_keys() -> None: + raw = {"name": "x", "extra_unknown_key": "ignored"} + td = ToolDefinition.from_dict(raw) + assert td.name == "x" + + +def test_default_values() -> None: + td = ToolDefinition() + assert td.name == "" + assert td.description == "" + assert td.parameters == {} + assert td.auto_start is False + + +def test_hashability_skipped_unhashable_dict_field() -> None: + td = ToolDefinition() + assert td.parameters == {} \ No newline at end of file diff --git a/tests/test_type_aliases.py b/tests/test_type_aliases.py index 245f139f..69da9555 100644 --- a/tests/test_type_aliases.py +++ b/tests/test_type_aliases.py @@ -9,25 +9,29 @@ def test_metadata_alias_resolves_to_dict() -> None: assert type_aliases.Metadata == dict[str, Any] -def test_comms_log_entry_alias_resolves_to_metadata() -> None: - assert type_aliases.CommsLogEntry is type_aliases.Metadata - assert type_aliases.CommsLogEntry == dict[str, Any] +def test_comms_log_entry_is_now_a_dataclass() -> None: + assert isinstance(type_aliases.CommsLogEntry, type) + entry = type_aliases.CommsLogEntry(role="user", content="hi") + assert entry.role == "user" + assert entry.content == "hi" def test_comms_log_alias_resolves_to_list_of_comms_log_entry() -> None: - assert type_aliases.CommsLog == list[dict[str, Any]] + assert type_aliases.CommsLog == list[type_aliases.CommsLogEntry] def test_history_alias_resolves_to_list_of_history_message() -> None: - assert type_aliases.History == list[dict[str, Any]] + assert type_aliases.History == list[type_aliases.HistoryMessage] def test_file_items_alias_resolves_to_list_of_file_item() -> None: - assert type_aliases.FileItems == list[dict[str, Any]] + assert type_aliases.FileItems == list[type_aliases.FileItem] -def test_tool_definition_alias_resolves_to_metadata() -> None: - assert type_aliases.ToolDefinition == dict[str, Any] +def test_tool_definition_is_now_a_dataclass() -> None: + assert isinstance(type_aliases.ToolDefinition, type) + td = type_aliases.ToolDefinition(name="x", description="d") + assert td.name == "x" def test_tool_call_alias_resolves_to_metadata() -> None: @@ -35,7 +39,7 @@ def test_tool_call_alias_resolves_to_metadata() -> None: def test_comms_log_callback_alias_resolves_to_callable() -> None: - assert type_aliases.CommsLogCallback == Callable[[dict[str, Any]], None] + assert type_aliases.CommsLogCallback == Callable[[type_aliases.CommsLogEntry], None] def test_file_items_diff_named_tuple_has_two_fields() -> None: diff --git a/tests/test_ui_panel_config.py b/tests/test_ui_panel_config.py new file mode 100644 index 00000000..50519450 --- /dev/null +++ b/tests/test_ui_panel_config.py @@ -0,0 +1,51 @@ +"""Tests for UIPanelConfig in src/type_aliases.py + +Per-aggregate dataclass regression-guard for the metadata_promotion_20260624 track. + +CONVENTION: 1-space indentation. NO COMMENTS. +""" +from __future__ import annotations + +from dataclasses import FrozenInstanceError + +import pytest + +from src.type_aliases import UIPanelConfig + + +def test_constructor_with_kwargs() -> None: + cfg = UIPanelConfig(separate_message_panel=True, separate_response_panel=False, separate_tool_calls_panel=True) + assert cfg.separate_message_panel is True + assert cfg.separate_response_panel is False + assert cfg.separate_tool_calls_panel is True + + +def test_field_access() -> None: + cfg = UIPanelConfig(separate_message_panel=True) + assert cfg.separate_message_panel is True + + +def test_frozen_raises_on_mutation() -> None: + cfg = UIPanelConfig() + with pytest.raises(FrozenInstanceError): + cfg.separate_message_panel = True + + +def test_to_dict_roundtrip() -> None: + cfg = UIPanelConfig(separate_message_panel=True, separate_response_panel=True, separate_tool_calls_panel=False) + d = cfg.to_dict() + assert d["separate_message_panel"] is True + assert d["separate_response_panel"] is True + assert d["separate_tool_calls_panel"] is False + + +def test_default_values() -> None: + cfg = UIPanelConfig() + assert cfg.separate_message_panel is False + assert cfg.separate_response_panel is False + assert cfg.separate_tool_calls_panel is False + + +def test_hashability() -> None: + cfg = UIPanelConfig(separate_message_panel=True) + assert hash(cfg) is not None \ No newline at end of file