fix(qa): resolve DF-002/DF-003/DF-006, log Defect Management sheet

DF-002 (Co4E): node đã chạy xong (STEP_DONE) vẫn bị khoá edit — chỉ khoá
khi node đang thực sự chạy (STEP_RUNNING). Phần "status sai khi back
tab" đã được xử lý từ trước (2a5ee29/13e2c22), xác minh vẫn nguyên vẹn.

DF-003: run.bat chỉ ẩn console ngay trước lúc chạy app, nên các bước
kiểm tra (Python/thư viện/junction) phía trước vẫn hiện console một
chút — ẩn ngay từ đầu file, hiện lại đúng lúc báo lỗi.

DF-006 (Monitoring): options số dòng/trang đổi thành 5/10/20/50/100
(mặc định 20); EventTable giờ phân trang thật (next/prev, page_count)
thay vì chỉ cắt top-N rồi bỏ phần còn lại.

Thêm tests/test_co4e_edit_lock.py (chưa có test nào phủ hành vi khoá
edit trước đó); viết lại tests/test_monitoring_page_size.py cho đúng
options/phân trang mới.

Cập nhật Task Tracking Template.xlsx — điền Bước tái hiện/Root Cause/
Cách xử lý/Regression Test/Evidence cho DF-002, DF-003, DF-005, DF-006
và dòng "Cowork/GraphRAG chỉ hiện khi chọn project" (qua Excel COM, để
không làm mất shape/data-validation nâng cao khác trong file).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 12:07:13 +09:00
co-authored by Claude Sonnet 5
parent 57af508971
commit 4f88171c64
13 changed files with 275 additions and 55 deletions
+55
View File
@@ -0,0 +1,55 @@
"""DF-002 (phần b) — node đang chạy HOẶC đã chạy xong không cho edit thông
tin trong Node. Trước khi sửa, ``_LOCKED_NODE_STATUSES`` khoá cả STEP_RUNNING
lẫn STEP_DONE, nên một bước đã chạy xong không bao giờ sửa lại được nữa.
Fix: chỉ khoá khi bước ĐANG chạy (STEP_RUNNING) — chạy xong rồi thì mở khoá
trở lại. Test này chốt cả nguồn sự thật (tuple
``co4e_workflow_crud._LOCKED_NODE_STATUSES``) lẫn hành vi ở widget
(``StepConfigPanel.set_locked``), để không bị hồi quy về hành vi cũ.
"""
from __future__ import annotations
import os
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
import pytest
QApplication = pytest.importorskip("PySide6.QtWidgets").QApplication
from cowork_local.core.co4e import (
STEP_DONE, STEP_ERROR, STEP_IDLE, STEP_PLANNED, STEP_RUNNING,
)
from cowork_local.presentation.co4e.co4e_workflow_crud import _LOCKED_NODE_STATUSES
from cowork_local.presentation.co4e.node_property_panel import StepConfigPanel
@pytest.fixture(scope="module")
def qapp():
app = QApplication.instance() or QApplication([])
yield app
def test_only_running_status_is_locked() -> None:
"""Một bước đã chạy xong (STEP_DONE) phải sửa lại được — chỉ bước đang
thực sự chạy (STEP_RUNNING) mới bị khoá."""
assert _LOCKED_NODE_STATUSES == (STEP_RUNNING,)
assert STEP_DONE not in _LOCKED_NODE_STATUSES
assert STEP_IDLE not in _LOCKED_NODE_STATUSES
assert STEP_ERROR not in _LOCKED_NODE_STATUSES
assert STEP_PLANNED not in _LOCKED_NODE_STATUSES
def test_set_locked_disables_then_reenables_edit_fields(qapp) -> None:
panel = StepConfigPanel()
panel.setEnabled(True) # panel starts disabled until a step is loaded
panel.set_locked(True)
assert not panel.label_edit.isEnabled()
assert not panel.instructions_edit.isEnabled()
assert not panel.model_combo.isEnabled()
panel.set_locked(False)
assert panel.label_edit.isEnabled()
assert panel.instructions_edit.isEnabled()
assert panel.model_combo.isEnabled()
+89 -19
View File
@@ -1,9 +1,11 @@
"""DF-006 — the "Số dòng/trang" (rows per page) control: EventTable's
page-size state (presentation/monitoring/shared/event_table.py) and its
QComboBox wiring in build_filter_scaffold (.../shared/filter_scaffold.py).
No dedicated test existed for this before — the design called for a
user-visible/choosable item-per-page control, and this exercises it end to
end (combo selection -> EventTable actually re-trimming its rows)."""
"""DF-006 — the "Số dòng/trang" (rows per page) control plus real prev/next
pagination: EventTable's page-size/page-index state
(presentation/monitoring/shared/event_table.py) and its QComboBox + pager
button wiring in build_filter_scaffold (.../shared/filter_scaffold.py).
Options are 5/10/20/50/100 with a next/prev pager, per the QA follow-up on
DF-006 — the earlier fix only trimmed to a page size (dropping every row past
it with no way back); this exercises the real paging end to end."""
from __future__ import annotations
import os
@@ -32,19 +34,62 @@ def _events(n: int):
"name": f"e{i}", "ok": True, "detail": ""} for i in range(n)]
def test_default_page_size_matches_old_max_rows(qapp) -> None:
table = EventTable()
assert table.page_size() == 300
table.set_events(_events(500))
assert table.rowCount() == 300
def test_page_size_options_are_5_10_20_50_100() -> None:
assert PAGE_SIZE_OPTIONS == (5, 10, 20, 50, 100)
def test_set_page_size_retrims_without_reloading(qapp) -> None:
def test_default_page_size(qapp) -> None:
table = EventTable()
table.set_events(_events(500))
assert table.page_size() == 20
table.set_events(_events(45))
assert table.rowCount() == 20
assert table.page_count() == 3
assert table.current_page() == 0
def test_set_page_size_resets_to_first_page(qapp) -> None:
table = EventTable()
table.set_events(_events(45))
table.next_page()
assert table.current_page() == 1
table.set_page_size(50)
assert table.page_size() == 50
assert table.rowCount() == 50
assert table.current_page() == 0
assert table.rowCount() == 45 # only 45 events total, fits in one page of 50
def test_next_prev_page_navigate_without_dropping_rows(qapp) -> None:
table = EventTable()
table.set_events(_events(45))
table.set_page_size(20)
assert table.rowCount() == 20
table.next_page()
assert table.current_page() == 1
assert table.rowCount() == 20
table.next_page()
assert table.current_page() == 2
assert table.rowCount() == 5 # last page: remainder
table.next_page() # already on last page — stays put
assert table.current_page() == 2
table.prev_page()
assert table.current_page() == 1
table.prev_page()
table.prev_page() # already on first page — stays put
assert table.current_page() == 0
def test_page_changed_signal_reports_current_and_total(qapp) -> None:
table = EventTable()
seen = []
table.page_changed.connect(lambda cur, total: seen.append((cur, total)))
table.set_events(_events(45))
table.set_page_size(20)
table.next_page()
assert seen[-1] == (1, 3)
def test_page_size_combo_is_only_added_when_requested(qapp) -> None:
@@ -52,19 +97,44 @@ def test_page_size_combo_is_only_added_when_requested(qapp) -> None:
table = EventTable()
parts = build_filter_scaffold(page, table, on_refresh=lambda: None, with_page_size=False)
assert "page_size_combo" not in parts
assert "page_prev_btn" not in parts
def test_page_size_combo_changes_the_table(qapp) -> None:
page = QWidget()
table = EventTable()
table.set_events(_events(500))
table.set_events(_events(45))
parts = build_filter_scaffold(page, table, on_refresh=lambda: None, with_page_size=True)
combo = parts["page_size_combo"]
assert combo.count() == len(PAGE_SIZE_OPTIONS)
assert combo.currentData() == 300 # matches EventTable's current page_size
assert combo.currentData() == 20 # matches EventTable's current page_size
idx = PAGE_SIZE_OPTIONS.index(50)
idx = PAGE_SIZE_OPTIONS.index(10)
combo.setCurrentIndex(idx)
assert table.page_size() == 50
assert table.rowCount() == 50
assert table.page_size() == 10
assert table.rowCount() == 10
def test_pager_buttons_disable_at_bounds_and_indicator_updates(qapp) -> None:
page = QWidget()
table = EventTable()
table.set_events(_events(45))
parts = build_filter_scaffold(page, table, on_refresh=lambda: None, with_page_size=True)
table.set_page_size(20)
prev_btn, next_btn = parts["page_prev_btn"], parts["page_next_btn"]
indicator = parts["page_indicator_label"]
assert not prev_btn.isEnabled()
assert next_btn.isEnabled()
assert indicator.text() == "Trang 1/3"
next_btn.click()
assert prev_btn.isEnabled()
assert next_btn.isEnabled()
assert indicator.text() == "Trang 2/3"
next_btn.click()
assert prev_btn.isEnabled()
assert not next_btn.isEnabled()
assert indicator.text() == "Trang 3/3"