Skip to content

Commit 117c7f8

Browse files
committed
2603220525: 2603220459: 添加全面的测试套件覆盖核心功能。同时更新了CI工作流配置。
feat 1. 2603220459: 添加全面的测试套件覆盖核心功能。同时更新了CI工作流配置。
1 parent be428c4 commit 117c7f8

20 files changed

Lines changed: 756 additions & 10 deletions

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ jobs:
1010

1111
steps:
1212
- name: Check out repository
13-
uses: actions/checkout@v4
13+
uses: actions/checkout@v5
1414

1515
- name: Set up Python
16-
uses: actions/setup-python@v5
16+
uses: actions/setup-python@v6
1717
with:
1818
python-version: "3.13"
1919

.github/workflows/release.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ jobs:
1414

1515
steps:
1616
- name: Check out repository
17-
uses: actions/checkout@v4
17+
uses: actions/checkout@v5
1818

1919
- name: Set up Python
20-
uses: actions/setup-python@v5
20+
uses: actions/setup-python@v6
2121
with:
2222
python-version: "3.13"
2323

@@ -39,7 +39,7 @@ jobs:
3939
powershell -ExecutionPolicy Bypass -File ./.scripts/package_release.ps1
4040
4141
- name: Upload release artifact
42-
uses: actions/upload-artifact@v4
42+
uses: actions/upload-artifact@v6
4343
with:
4444
name: weekflow-windows-release
4545
path: |
@@ -51,10 +51,10 @@ jobs:
5151

5252
steps:
5353
- name: Check out repository
54-
uses: actions/checkout@v4
54+
uses: actions/checkout@v5
5555

5656
- name: Set up Python
57-
uses: actions/setup-python@v5
57+
uses: actions/setup-python@v6
5858
with:
5959
python-version: "3.13"
6060

@@ -81,7 +81,7 @@ jobs:
8181
./.scripts/package_macos_release.sh
8282
8383
- name: Upload release artifact
84-
uses: actions/upload-artifact@v4
84+
uses: actions/upload-artifact@v6
8585
with:
8686
name: weekflow-macos-release
8787
path: dist/*.zip
@@ -94,7 +94,7 @@ jobs:
9494

9595
steps:
9696
- name: Download built artifacts
97-
uses: actions/download-artifact@v4
97+
uses: actions/download-artifact@v5
9898
with:
9999
path: release-assets
100100
merge-multiple: true

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ build/
1010
dist/
1111
releases/
1212
WeekFlow.spec
13-
tests/
1413
# Local environment files
1514
.env
1615

tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Test package for WeekFlow."""

tests/test_ai_config.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from WeekFlow.services.ai_config import (
2+
DEFAULT_AI_CONFIGS,
3+
normalize_ai_provider,
4+
)
5+
6+
7+
def test_normalize_ai_provider_prefers_operator_url_for_plan():
8+
provider, defaults = normalize_ai_provider(
9+
"volcengine_ark",
10+
"https://operator.las.cn-beijing.volces.com/api/v1",
11+
)
12+
13+
assert provider == "volcengine_plan"
14+
assert defaults["base_url"] == "https://operator.las.cn-beijing.volces.com/api/v1"
15+
assert defaults["model"] == DEFAULT_AI_CONFIGS["volcengine_plan"]["model"]
16+
17+
18+
def test_normalize_ai_provider_prefers_ark_url_for_ark():
19+
provider, defaults = normalize_ai_provider(
20+
"volcengine_plan",
21+
"https://ark.cn-beijing.volces.com/api/v3",
22+
)
23+
24+
assert provider == "volcengine_ark"
25+
assert defaults["base_url"] == "https://ark.cn-beijing.volces.com/api/v3"
26+
assert defaults["model"] == DEFAULT_AI_CONFIGS["volcengine_ark"]["model"]
27+
28+
29+
def test_normalize_ai_provider_maps_legacy_operator_name():
30+
provider, defaults = normalize_ai_provider("volcengine_operator", "")
31+
32+
assert provider == "volcengine_plan"
33+
assert defaults["base_url"] == DEFAULT_AI_CONFIGS["volcengine_plan"]["base_url"]

tests/test_ai_service.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import json
2+
from io import BytesIO
3+
from urllib.error import HTTPError
4+
5+
import pytest
6+
7+
from WeekFlow.models.report import ProjectItem, WeeklyReport
8+
from WeekFlow.services.ai import AIConfigError, AIService
9+
10+
11+
def _configured_report(**kwargs) -> WeeklyReport:
12+
report = WeeklyReport(**kwargs)
13+
report.ai["config"]["api_key"] = "test-key"
14+
return report
15+
16+
17+
def test_ai_service_requires_configuration():
18+
report = WeeklyReport()
19+
report.ai["config"]["api_key"] = ""
20+
21+
service = AIService()
22+
23+
try:
24+
service.test_connection(report)
25+
except AIConfigError as exc:
26+
assert "API Key" in str(exc)
27+
else: # pragma: no cover
28+
raise AssertionError("Expected AIConfigError when API Key is missing")
29+
30+
31+
def test_ai_service_summarizes_basic_info_from_full_report(monkeypatch):
32+
report = _configured_report(
33+
topic="社区活动筹备",
34+
achievements=["完成了志愿者分工表", "整理了活动物资清单"],
35+
projects=[ProjectItem(name="项目 A", summary="完善报名说明", next_step="继续确认现场安排")],
36+
)
37+
service = AIService()
38+
seen_messages = {}
39+
40+
def fake_chat(_config, messages):
41+
seen_messages["messages"] = messages
42+
return json.dumps({"one_line_summary": "集中整理了活动安排与资料信息。"}, ensure_ascii=False)
43+
44+
monkeypatch.setattr(service, "_chat", fake_chat)
45+
46+
polished = service.polish_current_section(report, section_key="basic_info")
47+
48+
assert polished.one_line_summary == "集中整理了活动安排与资料信息。"
49+
prompt_text = seen_messages["messages"][1]["content"]
50+
assert "项目 A" in prompt_text
51+
assert "完成了志愿者分工表" in prompt_text
52+
53+
54+
def test_ai_service_polishes_weekly_results_with_stubbed_response(monkeypatch):
55+
report = _configured_report(achievements=["旧成果"])
56+
service = AIService()
57+
58+
def fake_chat(_config, _messages):
59+
return json.dumps({"achievements": ["新成果", "补充说明"]}, ensure_ascii=False)
60+
61+
monkeypatch.setattr(service, "_chat", fake_chat)
62+
63+
polished = service.polish_current_section(report, section_key="overview")
64+
65+
assert polished.achievements == ["新成果", "补充说明"]
66+
67+
68+
def test_ai_service_polishes_current_project_with_stubbed_response(monkeypatch):
69+
report = _configured_report(projects=[ProjectItem(name="项目 A", summary="旧内容", next_step="旧计划")])
70+
service = AIService()
71+
72+
def fake_chat(_config, _messages):
73+
return json.dumps({"summary": "新内容", "next_step": "新计划"}, ensure_ascii=False)
74+
75+
monkeypatch.setattr(service, "_chat", fake_chat)
76+
77+
polished = service.polish_current_section(report, section_key="projects", project_index=0)
78+
79+
assert polished.projects[0].name == "项目 A"
80+
assert polished.projects[0].summary == "新内容"
81+
assert polished.projects[0].next_step == "新计划"
82+
83+
84+
def test_ai_service_uses_volcengine_plan_defaults():
85+
report = WeeklyReport()
86+
87+
assert report.ai["provider"] == "volcengine_plan"
88+
assert report.ai["config"]["model"] == "doubao-seed-2-0-pro-260215"
89+
90+
91+
def test_ai_service_uses_ark_sdk_for_volcengine_ark(monkeypatch):
92+
report = _configured_report()
93+
report.ai["provider"] = "volcengine_ark"
94+
report.ai["config"]["base_url"] = "https://ark.cn-beijing.volces.com/api/v3"
95+
report.ai["config"]["model"] = "doubao-seed-2-0-lite-260215"
96+
service = AIService()
97+
called = {}
98+
99+
def fake_ark(self, config, messages):
100+
called["provider"] = config.provider
101+
called["message_count"] = len(messages)
102+
return "连接成功"
103+
104+
def fail_openai(self, config, messages): # pragma: no cover
105+
pytest.fail("volcengine_ark provider should use Ark SDK path")
106+
107+
monkeypatch.setattr(AIService, "_chat_with_ark", fake_ark, raising=False)
108+
monkeypatch.setattr(AIService, "_chat_with_openai_compatible", fail_openai, raising=False)
109+
110+
assert service.test_connection(report) == "连接成功"
111+
assert called["provider"] == "volcengine_ark"
112+
assert called["message_count"] == 2
113+
114+
115+
def test_ai_service_maps_http_401_to_clear_configuration_hint(monkeypatch):
116+
report = _configured_report()
117+
service = AIService()
118+
119+
def fake_urlopen(_req, timeout=60):
120+
raise HTTPError(
121+
url="https://operator.las.cn-beijing.volces.com/api/v1/chat/completions",
122+
code=401,
123+
msg="Unauthorized",
124+
hdrs=None,
125+
fp=BytesIO(b'{"path":"/api/v1/chat/completions","message":"Unauthorized"}'),
126+
)
127+
128+
monkeypatch.setattr("WeekFlow.services.ai.request.urlopen", fake_urlopen)
129+
130+
try:
131+
service.test_connection(report)
132+
except AIConfigError as exc:
133+
message = str(exc)
134+
assert "HTTP 401" in message
135+
assert "火山引擎plan" in message
136+
assert "API Key" in message
137+
assert "Base URL" in message
138+
else: # pragma: no cover
139+
raise AssertionError("Expected AIConfigError for HTTP 401")

tests/test_build_icon.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from pathlib import Path
2+
3+
4+
ROOT = Path(__file__).resolve().parents[1]
5+
6+
7+
def test_build_script_uses_versioned_app_icon():
8+
ico_icon_path = ROOT / "src" / "weekflow_logo.ico"
9+
build_script = (ROOT / ".scripts" / "build_exe.ps1").read_text(encoding="utf-8")
10+
11+
assert ico_icon_path.exists()
12+
assert "--icon" in build_script
13+
assert "src\\weekflow_logo.ico" in build_script or "src/weekflow_logo.ico" in build_script

tests/test_demo_assets.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import json
2+
from pathlib import Path
3+
4+
5+
ROOT = Path(__file__).resolve().parents[1]
6+
OLD_DEMO_JSON = ROOT / "examples" / "demo-2611.data.json"
7+
NEW_DEMO_JSON = ROOT / "examples" / "demo-community.data.json"
8+
NEW_DEMO_MD = ROOT / "examples" / "demo-community.md"
9+
10+
11+
def test_old_demo_json_is_removed_and_new_demo_assets_exist():
12+
assert not OLD_DEMO_JSON.exists()
13+
assert NEW_DEMO_JSON.exists()
14+
assert NEW_DEMO_MD.exists()
15+
16+
17+
def test_new_demo_assets_use_new_chinese_content():
18+
payload = json.loads(NEW_DEMO_JSON.read_text(encoding="utf-8"))
19+
markdown_text = NEW_DEMO_MD.read_text(encoding="utf-8")
20+
21+
assert payload["report_id"] == "第08周"
22+
assert payload["topic"] == "社区活动筹备与资料整理"
23+
assert payload["projects"][0]["name"] == "场地与物资确认"
24+
assert payload["achievements"][0] == "整理了活动流程、物资清单和志愿者分工表。"
25+
assert "Offline RL" not in markdown_text
26+
assert "社区活动筹备与资料整理" in markdown_text

tests/test_editor_controller.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import json
2+
from pathlib import Path
3+
4+
from WeekFlow.controllers.editor_controller import EditorController
5+
from WeekFlow.models.report import WeeklyReport
6+
7+
8+
def test_new_report_state_creation_uses_requested_identity(tmp_path: Path):
9+
controller = EditorController(default_directory=tmp_path)
10+
11+
controller.create_new_report(
12+
report_id="2611",
13+
cycle="2026.03.12 - 2026.03.18",
14+
topic="社区活动筹备",
15+
)
16+
17+
assert controller.report.report_id == "2611"
18+
assert controller.report.cycle == "2026.03.12 - 2026.03.18"
19+
assert controller.current_json_path is None
20+
assert controller.current_markdown_path is None
21+
assert controller.report.one_line_summary == ""
22+
assert controller.report.achievements == []
23+
assert len(controller.report.projects) == 1
24+
assert controller.report.projects[0].name == ""
25+
assert controller.report.projects[0].records == []
26+
assert controller.report.ai["provider"] == "volcengine_plan"
27+
assert controller.is_dirty is True
28+
29+
30+
def test_load_existing_json_updates_controller_state(tmp_path: Path):
31+
controller = EditorController(default_directory=tmp_path)
32+
payload = WeeklyReport(report_id="2611", topic="社区活动筹备")
33+
json_path = tmp_path / "2611.data.json"
34+
json_path.write_text(
35+
json.dumps(payload.to_dict(), ensure_ascii=False),
36+
encoding="utf-8",
37+
)
38+
39+
controller.load_from_json(json_path)
40+
41+
assert controller.report.report_id == payload.report_id
42+
assert controller.report.topic == payload.topic
43+
assert controller.current_json_path == json_path
44+
assert controller.current_markdown_path == tmp_path / "2611.md"
45+
assert controller.is_dirty is False
46+
47+
48+
def test_save_current_pair_updates_current_paths(tmp_path: Path):
49+
controller = EditorController(default_directory=tmp_path)
50+
controller.create_new_report(report_id="2611")
51+
52+
json_path, markdown_path = controller.save_current()
53+
54+
assert json_path == tmp_path / "2611.data.json"
55+
assert markdown_path == tmp_path / "2611.md"
56+
assert controller.current_json_path == json_path
57+
assert controller.current_markdown_path == markdown_path
58+
assert controller.is_dirty is False
59+
60+
61+
def test_save_as_named_version_uses_custom_stem(tmp_path: Path):
62+
controller = EditorController(default_directory=tmp_path)
63+
controller.create_new_report(report_id="2611", topic="社区活动筹备")
64+
65+
json_path, markdown_path = controller.save_as_named_version("2611-lab-note")
66+
67+
assert json_path == tmp_path / "2611-lab-note.data.json"
68+
assert markdown_path == tmp_path / "2611-lab-note.md"
69+
assert controller.current_json_path == json_path
70+
assert controller.current_markdown_path == markdown_path
71+
assert controller.current_stem == "2611-lab-note"

tests/test_editor_window.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import os
2+
from pathlib import Path
3+
4+
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
5+
6+
from PySide6.QtWidgets import QApplication, QPushButton
7+
8+
from WeekFlow.controllers.editor_controller import EditorController
9+
from WeekFlow.ui.editor_window import EditorWindow
10+
11+
12+
def _build_window(tmp_path: Path) -> EditorWindow:
13+
app = QApplication.instance() or QApplication([])
14+
controller = EditorController(default_directory=tmp_path)
15+
controller.create_new_report(report_id="2611", topic="中文演示项目")
16+
window = EditorWindow(controller)
17+
QApplication.processEvents()
18+
return window
19+
20+
21+
def test_editor_window_does_not_show_generate_report_button(tmp_path: Path):
22+
window = _build_window(tmp_path)
23+
24+
button_texts = [button.text() for button in window.findChildren(QPushButton)]
25+
26+
assert "生成周报" not in button_texts
27+
28+
window.close()
29+
30+
31+
def test_editor_window_status_row_shows_only_rendered_title(tmp_path: Path):
32+
window = _build_window(tmp_path)
33+
34+
assert window.report_label.text() == "Week 11"
35+
36+
window.close()

0 commit comments

Comments
 (0)