sophhub 0.4.34 → 0.4.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,284 @@
1
+ """Unit tests for intern_daily_report.py"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from datetime import date
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
12
+
13
+ from intern_daily_report import (
14
+ find_roster_file,
15
+ parse_roster_table,
16
+ parse_daily_entries,
17
+ parse_leave_table,
18
+ is_date_in_leave_range,
19
+ judge_status,
20
+ format_report,
21
+ )
22
+
23
+
24
+ # ── Fixtures ──────────────────────────────────────────────────
25
+
26
+ @pytest.fixture
27
+ def roster_md():
28
+ return """# 实习生列表
29
+
30
+ ### 1. 实习生维度
31
+
32
+ | 序号 | 姓名 | 入职日期 | 工位 | 实习生Base | 实际导师 | 挂职导师 | 导师Base | 挂职导师Base | 所属PDT | 备注 |
33
+ |------|------|----------|------|------------|----------|----------|----------|--------------|--------|------|
34
+ | 1 | 张三 | 2026/05/01 | 2#3F-001 | 北京 | 李四 | 李四 | 北京 | 北京 | 场景PDT | |
35
+ | 2 | 王五 | 2026/06/01 | 2#3F-002 | 北京 | 赵六 | 赵六 | 北京 | 北京 | 架构PDT | |
36
+ | 3 | 钱七 | 2026/07/01 | 2#3F-003 | 北京 | 孙八 | 孙八 | 北京 | 北京 | | |
37
+ | 4 | 已走的人 | 2026/05/01 | 2#3F-004 | 北京 | 某某 | 某某 | 北京 | 北京 | 场景PDT | ⚠️ 已离职 |
38
+ | 5 | 放弃的人 | 2026/05/01 | 2#3F-005 | 北京 | 某某 | 某某 | 北京 | 北京 | 架构PDT | ⚠️ 放弃入职 |
39
+ | 6 | 待定的人 | 待定 | 待分配 | 北京 | 某某 | 某某 | 北京 | 北京 | | |
40
+ """
41
+
42
+
43
+ @pytest.fixture
44
+ def roster_file(roster_md, tmp_path):
45
+ d = tmp_path / "memory"
46
+ d.mkdir()
47
+ f = d / "实习生信息表-v2.0.0.md"
48
+ f.write_text(roster_md, encoding="utf-8")
49
+ return str(d)
50
+
51
+
52
+ @pytest.fixture
53
+ def daily_md():
54
+ return """
55
+ ---
56
+ ### 收到时间:2026-06-04 18:00(Asia/Shanghai)
57
+ - **类型:** 日报
58
+ - **原始正文(原文摘录)**
59
+ 姓名:张三
60
+ 今天主要工作:修复自动化测试bug
61
+ 当前进度:已完成
62
+ 是否存在卡点:否
63
+ 下一步计划:继续推进测试
64
+ """
65
+
66
+
67
+ @pytest.fixture
68
+ def daily_file(daily_md, tmp_path):
69
+ d = tmp_path / "records"
70
+ d.mkdir(parents=True)
71
+ f = d / "2026-06-04.md"
72
+ f.write_text(daily_md, encoding="utf-8")
73
+ return d
74
+
75
+
76
+ @pytest.fixture
77
+ def leave_md():
78
+ return """## 2026-06
79
+
80
+ | 报备日期 | 姓名 | 导师 | 请假时间 | 备注 |
81
+ |----------|------|------|----------|------|
82
+ | 2026-06-01 | 王五 | 赵六 | 2026-06-04~2026-06-05 | 回学校考试,OA 已提交 |
83
+ | 2026-06-02 | 钱七 | 孙八 | 2026-06-04(下午) | 开会 |
84
+ """
85
+
86
+
87
+ @pytest.fixture
88
+ def leave_file(leave_md, tmp_path):
89
+ d = tmp_path / "records"
90
+ d.mkdir(parents=True)
91
+ f = d / "leave.md"
92
+ f.write_text(leave_md, encoding="utf-8")
93
+ return d
94
+
95
+
96
+ # ── Roster Parsing Tests ─────────────────────────────────────
97
+
98
+ class TestRosterParsing:
99
+ def test_find_roster_file(self, roster_file):
100
+ path = find_roster_file(roster_file)
101
+ assert path.name.startswith("实习生信息表-")
102
+
103
+ def test_find_roster_file_not_found(self):
104
+ with pytest.raises(SystemExit):
105
+ find_roster_file("/nonexistent/path")
106
+
107
+ def test_parse_active_interns(self, roster_file):
108
+ path = find_roster_file(roster_file)
109
+ interns = parse_roster_table(path)
110
+ names = [i["name"] for i in interns]
111
+ assert "张三" in names
112
+ assert "王五" in names
113
+ assert "钱七" in names
114
+
115
+ def test_exclude_resigned(self, roster_file):
116
+ path = find_roster_file(roster_file)
117
+ interns = parse_roster_table(path)
118
+ names = [i["name"] for i in interns]
119
+ assert "已走的人" not in names
120
+
121
+ def test_exclude_abandoned(self, roster_file):
122
+ path = find_roster_file(roster_file)
123
+ interns = parse_roster_table(path)
124
+ names = [i["name"] for i in interns]
125
+ assert "放弃的人" not in names
126
+
127
+ def test_empty_pdt_defaults(self, roster_file):
128
+ path = find_roster_file(roster_file)
129
+ interns = parse_roster_table(path)
130
+ qianqi = [i for i in interns if i["name"] == "钱七"][0]
131
+ assert qianqi["pdt"] == "未分配PDT"
132
+
133
+ def test_tbd_date_is_none(self, roster_file):
134
+ path = find_roster_file(roster_file)
135
+ interns = parse_roster_table(path)
136
+ daiding = [i for i in interns if i["name"] == "待定的人"][0]
137
+ assert daiding["start_date"] is None
138
+
139
+
140
+ # ── Daily Parsing Tests ──────────────────────────────────────
141
+
142
+ class TestDailyParsing:
143
+ def test_parse_daily_entries(self, daily_file):
144
+ daily = parse_daily_entries(daily_file / "2026-06-04.md")
145
+ assert "张三" in daily
146
+ assert "修复自动化测试bug" in daily["张三"]["summary"]
147
+ assert daily["张三"]["blocker"] == "否"
148
+ assert "继续推进测试" in daily["张三"]["next_step"]
149
+
150
+ def test_parse_nonexistent_file(self, tmp_path):
151
+ daily = parse_daily_entries(tmp_path / "2026-06-99.md")
152
+ assert daily == {}
153
+
154
+
155
+ # ── Leave Parsing Tests ──────────────────────────────────────
156
+
157
+ class TestLeaveParsing:
158
+ def test_parse_leave_table(self, leave_file):
159
+ leaves = parse_leave_table(leave_file / "leave.md")
160
+ names = [lv["name"] for lv in leaves]
161
+ assert "王五" in names
162
+ assert "钱七" in names
163
+
164
+ def test_parse_nonexistent_file(self, tmp_path):
165
+ leaves = parse_leave_table(tmp_path / "leave.md")
166
+ assert leaves == []
167
+
168
+
169
+ # ── Leave Date Coverage Tests ────────────────────────────────
170
+
171
+ class TestLeaveDateCoverage:
172
+ def test_range_covers_middle(self):
173
+ assert is_date_in_leave_range(date(2026, 6, 4), "2026-06-04~2026-06-05")
174
+
175
+ def test_range_covers_start(self):
176
+ assert is_date_in_leave_range(date(2026, 6, 3), "2026-06-03~2026-06-05")
177
+
178
+ def test_range_covers_end(self):
179
+ assert is_date_in_leave_range(date(2026, 6, 5), "2026-06-03~2026-06-05")
180
+
181
+ def test_range_excludes_before(self):
182
+ assert not is_date_in_leave_range(date(2026, 6, 2), "2026-06-03~2026-06-05")
183
+
184
+ def test_range_excludes_after(self):
185
+ assert not is_date_in_leave_range(date(2026, 6, 6), "2026-06-03~2026-06-05")
186
+
187
+ def test_single_date_matches(self):
188
+ assert is_date_in_leave_range(date(2026, 6, 4), "2026-06-04(下午)")
189
+
190
+ def test_expired_leave_not_covered(self):
191
+ assert not is_date_in_leave_range(date(2026, 6, 14), "2026-05-12")
192
+
193
+ def test_empty_string(self):
194
+ assert not is_date_in_leave_range(date(2026, 6, 4), "")
195
+
196
+
197
+ # ── Status Judgment Tests ────────────────────────────────────
198
+
199
+ class TestJudgeStatus:
200
+ def test_submitted(self):
201
+ entry = {"name": "张三", "pdt": "场景PDT", "start_date": date(2026, 5, 1)}
202
+ daily = {"张三": {"summary": "修bug", "blocker": "否", "next_step": "测试", "ts": ""}}
203
+ status, detail = judge_status(entry, daily, [], date(2026, 6, 4))
204
+ assert status == "submitted"
205
+ assert detail["summary"] == "修bug"
206
+
207
+ def test_on_leave(self):
208
+ entry = {"name": "王五", "pdt": "架构PDT", "start_date": date(2026, 6, 1)}
209
+ leaves = [{"name": "王五", "leave_time": "2026-06-04~2026-06-05", "reason": "考试"}]
210
+ status, detail = judge_status(entry, {}, leaves, date(2026, 6, 4))
211
+ assert status == "on_leave"
212
+ assert detail["reason"] == "考试"
213
+
214
+ def test_missing(self):
215
+ entry = {"name": "钱七", "pdt": "架构PDT", "start_date": date(2026, 6, 1)}
216
+ status, detail = judge_status(entry, {}, [], date(2026, 6, 4))
217
+ assert status == "missing"
218
+
219
+ def test_not_onboarded_future_date(self):
220
+ entry = {"name": "新人", "pdt": "场景PDT", "start_date": date(2026, 7, 1)}
221
+ status, detail = judge_status(entry, {}, [], date(2026, 6, 4))
222
+ assert status == "not_onboarded"
223
+
224
+ def test_not_onboarded_none_date(self):
225
+ entry = {"name": "待定", "pdt": "场景PDT", "start_date": None}
226
+ status, detail = judge_status(entry, {}, [], date(2026, 6, 4))
227
+ assert status == "not_onboarded"
228
+
229
+ def test_today_onboard_marked(self):
230
+ entry = {"name": "新人", "pdt": "场景PDT", "start_date": date(2026, 6, 4)}
231
+ status, detail = judge_status(entry, {}, [], date(2026, 6, 4))
232
+ assert status == "missing"
233
+ assert detail["is_today_onboard"] is True
234
+
235
+
236
+ # ── Format Report Tests ──────────────────────────────────────
237
+
238
+ class TestFormatReport:
239
+ def test_format_output_contains_pdt_groups(self):
240
+ roster = [
241
+ {"name": "张三", "pdt": "场景PDT", "start_date": date(2026, 5, 1)},
242
+ {"name": "王五", "pdt": "架构PDT", "start_date": date(2026, 6, 1)},
243
+ {"name": "钱七", "pdt": "架构PDT", "start_date": date(2026, 6, 1)},
244
+ ]
245
+ daily = {"张三": {"summary": "修bug", "blocker": "否", "next_step": "测试", "ts": ""}}
246
+ leaves = [{"name": "王五", "leave_time": "2026-06-04", "reason": "考试", "report_date": "2026-06-01"}]
247
+ report = format_report(roster, daily, leaves, date(2026, 6, 4))
248
+ assert "## 场景PDT" in report
249
+ assert "## 架构PDT" in report
250
+ assert "✅" in report
251
+ assert "🏖️" in report
252
+ assert "❌" in report
253
+
254
+ def test_today_onboard_tag(self):
255
+ roster = [
256
+ {"name": "新人", "pdt": "场景PDT", "start_date": date(2026, 6, 4)},
257
+ ]
258
+ report = format_report(roster, {}, [], date(2026, 6, 4))
259
+ assert "🆕" in report
260
+
261
+ def test_missing_reminder(self):
262
+ roster = [
263
+ {"name": "张三", "pdt": "场景PDT", "mentor": "李四", "start_date": date(2026, 5, 1)},
264
+ {"name": "王五", "pdt": "架构PDT", "mentor": "赵六", "start_date": date(2026, 6, 1)},
265
+ ]
266
+ report = format_report(roster, {}, [], date(2026, 6, 4))
267
+ assert "⚠️ **未提交提醒:**" in report
268
+ assert "@李四:张三" in report
269
+ assert "@赵六:王五" in report
270
+
271
+ def test_missing_reminder_skips_no_mentor(self):
272
+ roster = [
273
+ {"name": "张三", "pdt": "场景PDT", "start_date": date(2026, 5, 1)},
274
+ ]
275
+ report = format_report(roster, {}, [], date(2026, 6, 4))
276
+ assert "⚠️" not in report # No mentor → no reminder
277
+
278
+ def test_missing_reminder_skips_when_all_submitted(self):
279
+ roster = [
280
+ {"name": "张三", "pdt": "场景PDT", "mentor": "李四", "start_date": date(2026, 5, 1)},
281
+ ]
282
+ daily = {"张三": {"summary": "修bug", "blocker": "否", "next_step": "测试", "ts": ""}}
283
+ report = format_report(roster, daily, [], date(2026, 6, 4))
284
+ assert "⚠️" not in report # All submitted → no reminder
@@ -1,12 +1,25 @@
1
1
  {
2
2
  "name": "sophnet-docx",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "types": [
5
5
  "builtin"
6
6
  ],
7
7
  "displayName": "",
8
8
  "description": "",
9
9
  "changelog": [
10
+ {
11
+ "version": "1.1.0",
12
+ "date": "2026-06-08",
13
+ "changes": [
14
+ "新增任务分类决策表(READ/EDIT/CREATE),渐进式披露拆分为 references/ 独立文件",
15
+ "新增 READ 工作流(pandoc/unpack → 分析回复)",
16
+ "新增 EDIT 工作流(unpack → 编辑XML → pack → upload)",
17
+ "修复批注显示未知:comment.py 新增 w:authors + people.xml 三层注册",
18
+ "CREATE 新增 Step 0 npm install 自动安装依赖",
19
+ "JS 临时文件迁移到 /tmp/ 并含 PID,失败保留策略(&& 替代 ;)",
20
+ ".npmrc 和 pyproject.toml 配置阿里云镜像加速"
21
+ ]
22
+ },
10
23
  {
11
24
  "version": "1.0.0",
12
25
  "date": "2026-04-14",
@@ -16,5 +29,5 @@
16
29
  }
17
30
  ],
18
31
  "createdAt": "2026-04-14",
19
- "updatedAt": "2026-04-14"
32
+ "updatedAt": "2026-06-08"
20
33
  }