Private
Public Access
Per post_module_taxonomy_de_cruft_20260627 Phase 2 (FR7). Each
'from src.models import X' for a moved class is rewritten to
'from src.<destination> import X':
Ticket, Track, WorkerContext, TrackState, TrackMetadata,
ThinkingSegment, EMPTY_TRACK_STATE -> src.mma
ProjectContext, ProjectMeta, ProjectOutput, ProjectFiles,
ProjectScreenshots, ProjectDiscussion, EMPTY_PROJECT_CONTEXT -> src.project
FileItem, Preset, ContextPreset, ContextFileEntry,
NamedViewPreset -> src.project_files
Tool, ToolPreset -> src.tool_presets
BiasProfile -> src.tool_bias
TextEditorConfig, ExternalEditorConfig,
EMPTY_TEXT_EDITOR_CONFIG -> src.external_editor
Persona -> src.personas
WorkspaceProfile -> src.workspace_manager
MCPServerConfig, MCPConfiguration, VectorStoreConfig,
RAGConfig, load_mcp_config -> src.mcp_client
NOT touched (kept on src.models; Phase 3 or Phase 4 will move them):
GenerateRequest, ConfirmRequest, DEFAULT_TOOL_CATEGORIES, Metadata, PROVIDERS
Migration was performed by the one-time script
scripts/tier2/artifacts/post_module_taxonomy_de_cruft_20260627/migrate_imports.py
which uses a class-to-module map and re.sub() to rewrite each
'from src.models import X' line.
Total: 85 import lines rewritten across 71 files.
Note: this commit depends on the v2 SHIPPED work
(origin/tier2/module_taxonomy_refactor_20260627) being merged into
this branch NEXT. On master (without the v2 SHIPPED commits), the
destination modules do not exist and these imports would fail.
191 lines
7.4 KiB
Python
191 lines
7.4 KiB
Python
"""
|
|
Phase 1 of metadata_promotion_20260624.
|
|
|
|
Verifies:
|
|
1. self.active_tickets load boundaries convert dicts to models.Ticket
|
|
2. conductor_tech_lead.topological_sort returns list[models.Ticket]
|
|
3. gui_2.py consumer sites use direct field access (not .get())
|
|
4. app_controller.py consumer sites use direct field access (not .get())
|
|
"""
|
|
import inspect
|
|
from unittest.mock import patch
|
|
|
|
from src.mma import Ticket
|
|
|
|
|
|
class TestActiveTicketsType:
|
|
def test_active_tickets_annotation_is_list_of_ticket(self) -> None:
|
|
"""self.active_tickets type hint must be list[models.Ticket], not list[Metadata]."""
|
|
from src.app_controller import AppController
|
|
src_text = inspect.getsource(AppController.__init__)
|
|
assert "list[models.Ticket]" in src_text, (
|
|
"AppController.__init__ must declare self.active_tickets: list[models.Ticket]"
|
|
)
|
|
assert "list[Metadata]" not in src_text.split("self.active_tickets")[1].split("\n")[0], (
|
|
"AppController.__init__ must NOT declare self.active_tickets: list[Metadata]"
|
|
)
|
|
|
|
|
|
class TestActiveTicketsLoadBoundaries:
|
|
def test_load_at_data_converts_dicts_to_tickets(self) -> None:
|
|
"""_deserialize_active_track_result boundary must wrap dicts as models.Ticket."""
|
|
from src.app_controller import AppController
|
|
with patch.object(AppController, "load_config", return_value={
|
|
'ai': {'provider': 'gemini', 'model': 'gemini-2.5-flash-lite'},
|
|
'projects': {'paths': [], 'active': ''},
|
|
'gui': {'show_windows': {}},
|
|
}), patch.object(AppController, "save_config"), \
|
|
patch.object(AppController, "_prune_old_logs"), \
|
|
patch.object(AppController, "start_services"), \
|
|
patch.object(AppController, "_init_ai_and_hooks"):
|
|
ctrl = AppController.__new__(AppController)
|
|
ctrl.__init__()
|
|
at_data = {
|
|
"id": "track-x",
|
|
"title": "Track X",
|
|
"tickets": [
|
|
{"id": "T1", "description": "first", "status": "todo"},
|
|
{"id": "T2", "description": "second", "status": "todo"},
|
|
],
|
|
}
|
|
ctrl._deserialize_active_track_result(at_data)
|
|
assert ctrl.active_tickets, "load path should populate active_tickets"
|
|
for t in ctrl.active_tickets:
|
|
assert isinstance(t, Ticket), (
|
|
f"active_tickets must contain Ticket instances, got {type(t).__name__}: {t!r}"
|
|
)
|
|
|
|
def test_load_active_tickets_beads_branch_converts_dicts_to_tickets(self) -> None:
|
|
"""_load_active_tickets (beads branch) must wrap bead dicts as models.Ticket."""
|
|
from src.app_controller import AppController
|
|
from src.mma import Ticket
|
|
ctrl = AppController.__new__(AppController)
|
|
ctrl._last_request_errors = []
|
|
ctrl.ui_project_execution_mode = "beads"
|
|
ctrl.ui_files_base_dir = None
|
|
class _Bead:
|
|
def __init__(self, bid: str, title: str, desc: str, status: str) -> None:
|
|
self.id = bid; self.title = title; self.description = desc; self.status = status
|
|
with patch.object(AppController, "_load_beads_from_path_result") as mock_load:
|
|
mock_load.return_value = (lambda: type("R", (), {"ok": True, "data": [
|
|
_Bead("B1", "T1", "first", "todo"), _Bead("B2", "T2", "second", "todo")
|
|
]})())
|
|
ctrl._load_active_tickets()
|
|
for t in ctrl.active_tickets:
|
|
assert isinstance(t, Ticket), (
|
|
f"beads branch must populate active_tickets with Ticket instances, got {type(t).__name__}"
|
|
)
|
|
|
|
|
|
class TestTopologicalSortReturnsTicketList:
|
|
def test_topological_sort_returns_ticket_instances(self) -> None:
|
|
"""conductor_tech_lead.topological_sort must return list[models.Ticket]."""
|
|
from src import conductor_tech_lead
|
|
sig = inspect.signature(conductor_tech_lead.topological_sort)
|
|
assert sig.return_annotation is not inspect.Signature.empty
|
|
assert "Ticket" in str(sig.return_annotation), (
|
|
f"topological_sort return annotation must reference Ticket, got {sig.return_annotation}"
|
|
)
|
|
|
|
|
|
class TestGuiConsumersDirectFieldAccess:
|
|
def test_reorder_ticket_uses_direct_field_access(self) -> None:
|
|
"""gui_2.App._reorder_ticket must use t.id / t.depends_on (not .get())."""
|
|
import inspect
|
|
from src import gui_2
|
|
src = inspect.getsource(gui_2.App._reorder_ticket)
|
|
assert "t.get(" not in src, (
|
|
"_reorder_ticket must not call t.get() — use t.id and t.depends_on directly"
|
|
)
|
|
|
|
def test_bulk_execute_uses_direct_field_access(self) -> None:
|
|
"""gui_2.App.bulk_execute must use t.id (not .get())."""
|
|
import inspect
|
|
from src import gui_2
|
|
src = inspect.getsource(gui_2.App.bulk_execute)
|
|
assert "t.get(" not in src, (
|
|
"bulk_execute must not call t.get() — use t.id directly"
|
|
)
|
|
|
|
def test_bulk_skip_uses_direct_field_access(self) -> None:
|
|
"""gui_2.App.bulk_skip must use t.id (not .get())."""
|
|
import inspect
|
|
from src import gui_2
|
|
src = inspect.getsource(gui_2.App.bulk_skip)
|
|
assert "t.get(" not in src, (
|
|
"bulk_skip must not call t.get() — use t.id directly"
|
|
)
|
|
|
|
def test_bulk_block_uses_direct_field_access(self) -> None:
|
|
"""gui_2.App.bulk_block must use t.id (not .get())."""
|
|
import inspect
|
|
from src import gui_2
|
|
src = inspect.getsource(gui_2.App.bulk_block)
|
|
assert "t.get(" not in src, (
|
|
"bulk_block must not call t.get() — use t.id directly"
|
|
)
|
|
|
|
def test_cb_block_ticket_uses_direct_field_access(self) -> None:
|
|
"""gui_2.App._cb_block_ticket must use direct field access (not .get())."""
|
|
import inspect
|
|
from src import gui_2
|
|
src = inspect.getsource(gui_2.App._cb_block_ticket)
|
|
assert "t.get(" not in src, (
|
|
"_cb_block_ticket must not call t.get() — use direct field access"
|
|
)
|
|
|
|
def test_cb_unblock_ticket_uses_direct_field_access(self) -> None:
|
|
"""gui_2.App._cb_unblock_ticket must use direct field access (not .get())."""
|
|
import inspect
|
|
from src import gui_2
|
|
src = inspect.getsource(gui_2.App._cb_unblock_ticket)
|
|
assert "t.get(" not in src, (
|
|
"_cb_unblock_ticket must not call t.get() — use direct field access"
|
|
)
|
|
|
|
def test_dag_cycle_check_uses_direct_field_access(self) -> None:
|
|
"""gui_2._dag_cycle_check_result must use t.id / t.depends_on (not .get())."""
|
|
import inspect
|
|
from src import gui_2
|
|
src = inspect.getsource(gui_2._dag_cycle_check_result)
|
|
assert "t.get(" not in src, (
|
|
"_dag_cycle_check_result must not call t.get() — use t.id and t.depends_on directly"
|
|
)
|
|
|
|
|
|
class TestAppControllerConsumersDirectFieldAccess:
|
|
def test_cb_ticket_retry_uses_direct_field_access(self) -> None:
|
|
"""app_controller._cb_ticket_retry must use t.id (not .get())."""
|
|
import inspect
|
|
from src import app_controller
|
|
src = inspect.getsource(app_controller.AppController._cb_ticket_retry)
|
|
assert "t.get(" not in src, (
|
|
"_cb_ticket_retry must not call t.get() — use t.id directly"
|
|
)
|
|
|
|
def test_cb_ticket_skip_uses_direct_field_access(self) -> None:
|
|
"""app_controller._cb_ticket_skip must use t.id (not .get())."""
|
|
import inspect
|
|
from src import app_controller
|
|
src = inspect.getsource(app_controller.AppController._cb_ticket_skip)
|
|
assert "t.get(" not in src, (
|
|
"_cb_ticket_skip must not call t.get() — use t.id directly"
|
|
)
|
|
|
|
def test_approve_ticket_uses_direct_field_access(self) -> None:
|
|
"""app_controller.approve_ticket must use t.id (not .get())."""
|
|
import inspect
|
|
from src import app_controller
|
|
src = inspect.getsource(app_controller.AppController.approve_ticket)
|
|
assert "t.get(" not in src, (
|
|
"approve_ticket must not call t.get() — use t.id directly"
|
|
)
|
|
|
|
def test_mutate_dag_uses_direct_field_access(self) -> None:
|
|
"""app_controller.mutate_dag must use t.id and t.depends_on (not .get())."""
|
|
import inspect
|
|
from src import app_controller
|
|
src = inspect.getsource(app_controller.AppController.mutate_dag)
|
|
assert "t.get(" not in src, (
|
|
"mutate_dag must not call t.get() — use t.id and t.depends_on directly"
|
|
) |