merge: hoà cập nhật mới từ origin/feature/delta-team/epic-R04 (R08 Chat UI Hub + R10)
Đồng nghiệp đã push thêm 10 commit lên nhánh trong lúc đang xử lý merge
trước đó (R08-T01..T06 chat_panel.py split, R08 folder/dashboard/graph/
scheduling hoàn thiện, R10 CI Quality Gates + Contributor Recipes + E2E
smoke test). Resolve conflict:
- application/monitoring/__init__.py, domain/tasks/__init__.py,
infrastructure/persistence/json/__init__.py: chỉ khác docstring — giữ bản
HEAD (đầy đủ ngữ cảnh EPIC hơn), hợp nhất __all__ khi cần
(MonitoringQueryService).
- tests/fakes/__init__.py: hợp nhất __getattr__ để lazy-load cả
FakeToolExecutor lẫn ToolInvocation (bản HEAD thiếu ToolInvocation), bỏ
entry "FakeClock" bị lặp trong __all__.
- tests/integration/test_routing_surfaces.py (deleted by them): khôi phục
lại bản đã sửa ở lần merge trước — verify lại: API routing
(RoutingApplicationService.resolve/_apply_routing/_apply_co4e_routing)
không đổi sau khi chat_panel.py chuyển sang presentation/chat/*, 9/9 test
vẫn pass trên code đã merge.
Ghi chú (không sửa, ngoài phạm vi merge): tests/fakes/__init__.py trên nhánh
remote export "ToolInvocation" từ fake_tool_executor.py nhưng class này đã
bị xoá nhầm từ commit chung 10739f1 (breakdown folder tree epic R01) — hiện
là dead code, không ai import, nhưng sẽ raise ImportError nếu có test nào
sau này thử dùng.
Đã chạy pytest tests/: 793 passed (không phát sinh fail mới so với lần
merge trước — 8 fail còn lại đều do môi trường sandbox: thiếu package
keyring, và tên thư mục checkout "cowork-local" thay vì "cowork_local"
khiến vài test spawn-subprocess không import được package).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
"""EPIC R08 - Chat UI Hub integration tests.
|
||||
|
||||
Tests the lifecycle, UI component assembly, and event wiring of the refactored
|
||||
presentation/chat/ sub-package (ChatPanel, ComposerWidget, AudioRecorderWidget,
|
||||
ChatHistoryWidget, OutputPanelMixin).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
from cowork_local.config import AppConfig
|
||||
from cowork_local.presentation.chat import (
|
||||
AudioRecorderWidget,
|
||||
ChatHistoryWidget,
|
||||
ChatPanel,
|
||||
ChatView,
|
||||
Composer,
|
||||
ComposerWidget,
|
||||
MessageBubble,
|
||||
)
|
||||
from cowork_local.state import AppContext
|
||||
|
||||
pytest.importorskip("PySide6", reason="Qt required for chat UI integration tests")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def qt_app():
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
return QApplication.instance() or QApplication([])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ctx(qt_app, tmp_path):
|
||||
config_file = tmp_path / "config.json"
|
||||
config = AppConfig.load(config_file)
|
||||
return AppContext(config)
|
||||
|
||||
|
||||
def test_chat_history_widget_adds_and_clears_bubbles(qt_app):
|
||||
"""Verify ChatHistoryWidget / ChatView can append different bubble types and clear them."""
|
||||
view = ChatHistoryWidget()
|
||||
assert isinstance(view, ChatView)
|
||||
|
||||
b_user = view.add_user("Hello agent")
|
||||
assert isinstance(b_user, MessageBubble)
|
||||
assert b_user.role == "user"
|
||||
|
||||
b_assistant = view.add_assistant()
|
||||
assert isinstance(b_assistant, MessageBubble)
|
||||
assert b_assistant.role == "assistant"
|
||||
b_assistant.set_markdown("**Bold response**")
|
||||
|
||||
b_status = view.add_status("Processing task...")
|
||||
assert isinstance(b_status, MessageBubble)
|
||||
|
||||
b_error = view.add_error("Network timeout")
|
||||
assert isinstance(b_error, MessageBubble)
|
||||
|
||||
# Clear transcript
|
||||
view.clear()
|
||||
assert view._lay.count() == 1 # only trailing stretch item remains
|
||||
|
||||
|
||||
def test_composer_widget_queue_and_submission(qt_app):
|
||||
"""Verify ComposerWidget / Composer handles text submission and parallel queueing."""
|
||||
composer = ComposerWidget()
|
||||
assert isinstance(composer, Composer)
|
||||
|
||||
submitted_events = []
|
||||
composer.submitted.connect(lambda text, atts: submitted_events.append((text, atts)))
|
||||
|
||||
# Direct submission via _on_submit
|
||||
composer.set_text("Run command ls")
|
||||
composer._on_submit()
|
||||
assert len(submitted_events) == 1
|
||||
assert submitted_events[0][0] == "Run command ls"
|
||||
assert submitted_events[0][1] == []
|
||||
|
||||
# Submit when busy puts message in queue
|
||||
composer.set_busy(True)
|
||||
composer.enqueue("Queued task 1")
|
||||
composer.enqueue("Queued task 2", attachments=["/tmp/file.txt"])
|
||||
|
||||
assert len(composer._queue) == 2
|
||||
assert composer.has_queue() is True
|
||||
|
||||
# Free up slot via pop_next
|
||||
next_msg = composer.pop_next()
|
||||
assert next_msg is not None
|
||||
assert next_msg["text"] == "Queued task 1"
|
||||
assert len(composer._queue) == 1
|
||||
|
||||
|
||||
def test_audio_recorder_widget_state_transitions(qt_app):
|
||||
"""Verify AudioRecorderWidget transitions from idle -> recording -> stopped."""
|
||||
recorder = AudioRecorderWidget()
|
||||
assert recorder.is_recording() is False
|
||||
|
||||
started_signal = MagicMock()
|
||||
stopped_signal = MagicMock()
|
||||
audio_ready_signal = MagicMock()
|
||||
|
||||
recorder.recording_started.connect(started_signal)
|
||||
recorder.recording_stopped.connect(stopped_signal)
|
||||
recorder.audio_ready.connect(audio_ready_signal)
|
||||
|
||||
# Start recording
|
||||
recorder.start_recording()
|
||||
assert recorder.is_recording() is True
|
||||
started_signal.assert_called_once()
|
||||
|
||||
# Simulate timer tick
|
||||
recorder._on_tick()
|
||||
assert recorder.timer_label.text() == "00:01"
|
||||
|
||||
# Stop recording
|
||||
recorder.stop_recording()
|
||||
assert recorder.is_recording() is False
|
||||
stopped_signal.assert_called_once_with(1)
|
||||
audio_ready_signal.assert_called_once_with(b"", "wav")
|
||||
|
||||
|
||||
def test_chat_panel_initialization(ctx, qt_app):
|
||||
"""Verify ChatPanel builds correctly with its mixed-in panels and sub-widgets."""
|
||||
panel = ChatPanel(ctx, kind="cowork", session_name="test_session")
|
||||
assert panel.ctx is ctx
|
||||
assert panel.kind == "cowork"
|
||||
assert panel.session_name == "test_session"
|
||||
assert hasattr(panel, "chat_view")
|
||||
assert hasattr(panel, "composer")
|
||||
assert hasattr(panel, "input_section")
|
||||
assert hasattr(panel, "output_section")
|
||||
assert isinstance(panel.composer, Composer)
|
||||
Reference in New Issue
Block a user