opencode-collaboration 2.1.0__py3-none-any.whl → 2.2.0.post1__py3-none-any.whl
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.
- opencode_collaboration-2.2.0.post1.dist-info/METADATA +136 -0
- {opencode_collaboration-2.1.0.dist-info → opencode_collaboration-2.2.0.post1.dist-info}/RECORD +10 -5
- src/core/agent_manager.py +553 -0
- src/core/meeting_manager.py +502 -0
- src/core/project_manager.py +549 -0
- src/core/resource_lock.py +468 -0
- src/core/story_manager.py +712 -0
- opencode_collaboration-2.1.0.dist-info/METADATA +0 -99
- {opencode_collaboration-2.1.0.dist-info → opencode_collaboration-2.2.0.post1.dist-info}/WHEEL +0 -0
- {opencode_collaboration-2.1.0.dist-info → opencode_collaboration-2.2.0.post1.dist-info}/entry_points.txt +0 -0
- {opencode_collaboration-2.1.0.dist-info → opencode_collaboration-2.2.0.post1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
"""用户故事管理模块 - v2.2.0 M4 用户故事管理
|
|
2
|
+
|
|
3
|
+
提供用户故事创建、列表、详情、关联测试、验收确认等功能。
|
|
4
|
+
"""
|
|
5
|
+
import uuid
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
import yaml
|
|
11
|
+
import logging
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class StoryStatus(Enum):
|
|
19
|
+
"""用户故事状态枚举。"""
|
|
20
|
+
DRAFT = "draft" # 草稿
|
|
21
|
+
IN_PROGRESS = "in_progress" # 进行中
|
|
22
|
+
PENDING_ACCEPTANCE = "pending_acceptance" # 待验收
|
|
23
|
+
ACCEPTED = "accepted" # 已验收
|
|
24
|
+
ARCHIVED = "archived" # 已归档
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AcceptanceCriteriaStatus(Enum):
|
|
28
|
+
"""验收标准状态枚举。"""
|
|
29
|
+
PENDING = "pending" # 待验收
|
|
30
|
+
PASSED = "passed" # 通过
|
|
31
|
+
FAILED = "failed" # 失败
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class InteractionStep:
|
|
36
|
+
"""交互步骤。"""
|
|
37
|
+
step_number: int
|
|
38
|
+
user_action: str
|
|
39
|
+
system_response: str
|
|
40
|
+
|
|
41
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
42
|
+
return {
|
|
43
|
+
"step_number": self.step_number,
|
|
44
|
+
"user_action": self.user_action,
|
|
45
|
+
"system_response": self.system_response
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def from_dict(cls, data: Dict[str, Any]) -> "InteractionStep":
|
|
50
|
+
return cls(
|
|
51
|
+
step_number=data["step_number"],
|
|
52
|
+
user_action=data["user_action"],
|
|
53
|
+
system_response=data["system_response"]
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class AcceptanceCriteria:
|
|
59
|
+
"""验收标准。"""
|
|
60
|
+
criteria_id: str
|
|
61
|
+
description: str
|
|
62
|
+
status: AcceptanceCriteriaStatus = AcceptanceCriteriaStatus.PENDING
|
|
63
|
+
notes: str = ""
|
|
64
|
+
|
|
65
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
66
|
+
return {
|
|
67
|
+
"criteria_id": self.criteria_id,
|
|
68
|
+
"description": self.description,
|
|
69
|
+
"status": self.status.value,
|
|
70
|
+
"notes": self.notes
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_dict(cls, data: Dict[str, Any]) -> "AcceptanceCriteria":
|
|
75
|
+
return cls(
|
|
76
|
+
criteria_id=data["criteria_id"],
|
|
77
|
+
description=data["description"],
|
|
78
|
+
status=AcceptanceCriteriaStatus(data.get("status", "pending")),
|
|
79
|
+
notes=data.get("notes", "")
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class E2ETestCoverage:
|
|
85
|
+
"""E2E 测试覆盖。"""
|
|
86
|
+
test_file: str
|
|
87
|
+
test_case: str
|
|
88
|
+
description: str
|
|
89
|
+
status: str = "pending" # pending, passed, failed
|
|
90
|
+
|
|
91
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
92
|
+
return {
|
|
93
|
+
"test_file": self.test_file,
|
|
94
|
+
"test_case": self.test_case,
|
|
95
|
+
"description": self.description,
|
|
96
|
+
"status": self.status
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def from_dict(cls, data: Dict[str, Any]) -> "E2ETestCoverage":
|
|
101
|
+
return cls(
|
|
102
|
+
test_file=data["test_file"],
|
|
103
|
+
test_case=data["test_case"],
|
|
104
|
+
description=data["description"],
|
|
105
|
+
status=data.get("status", "pending")
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class ExpectedResult:
|
|
111
|
+
"""预期结果。"""
|
|
112
|
+
scenario_type: str # success, failure
|
|
113
|
+
description: str
|
|
114
|
+
handling: Optional[str] = None # For failure scenarios
|
|
115
|
+
|
|
116
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
117
|
+
return {
|
|
118
|
+
"scenario_type": self.scenario_type,
|
|
119
|
+
"description": self.description,
|
|
120
|
+
"handling": self.handling
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ExpectedResult":
|
|
125
|
+
return cls(
|
|
126
|
+
scenario_type=data["scenario_type"],
|
|
127
|
+
description=data["description"],
|
|
128
|
+
handling=data.get("handling")
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass
|
|
133
|
+
class UserStory:
|
|
134
|
+
"""用户故事。"""
|
|
135
|
+
story_id: str
|
|
136
|
+
title: str
|
|
137
|
+
role: str # 用户角色
|
|
138
|
+
goal: str # 希望完成什么
|
|
139
|
+
value: str # 以便获得什么价值
|
|
140
|
+
preconditions: List[str] = field(default_factory=list)
|
|
141
|
+
interaction_steps: List[InteractionStep] = field(default_factory=list)
|
|
142
|
+
expected_results: List[ExpectedResult] = field(default_factory=list)
|
|
143
|
+
e2e_tests: List[E2ETestCoverage] = field(default_factory=list)
|
|
144
|
+
acceptance_criteria: List[AcceptanceCriteria] = field(default_factory=list)
|
|
145
|
+
status: StoryStatus = StoryStatus.DRAFT
|
|
146
|
+
version: str = "v2.2.0"
|
|
147
|
+
evidence: Optional[str] = None # 验收证据
|
|
148
|
+
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
149
|
+
updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
150
|
+
|
|
151
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
152
|
+
return {
|
|
153
|
+
"story_id": self.story_id,
|
|
154
|
+
"title": self.title,
|
|
155
|
+
"role": self.role,
|
|
156
|
+
"goal": self.goal,
|
|
157
|
+
"value": self.value,
|
|
158
|
+
"preconditions": self.preconditions,
|
|
159
|
+
"interaction_steps": [s.to_dict() for s in self.interaction_steps],
|
|
160
|
+
"expected_results": [r.to_dict() for r in self.expected_results],
|
|
161
|
+
"e2e_tests": [t.to_dict() for t in self.e2e_tests],
|
|
162
|
+
"acceptance_criteria": [c.to_dict() for c in self.acceptance_criteria],
|
|
163
|
+
"status": self.status.value,
|
|
164
|
+
"version": self.version,
|
|
165
|
+
"evidence": self.evidence,
|
|
166
|
+
"created_at": self.created_at,
|
|
167
|
+
"updated_at": self.updated_at
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
def from_dict(cls, data: Dict[str, Any]) -> "UserStory":
|
|
172
|
+
return cls(
|
|
173
|
+
story_id=data["story_id"],
|
|
174
|
+
title=data["title"],
|
|
175
|
+
role=data["role"],
|
|
176
|
+
goal=data["goal"],
|
|
177
|
+
value=data["value"],
|
|
178
|
+
preconditions=data.get("preconditions", []),
|
|
179
|
+
interaction_steps=[
|
|
180
|
+
InteractionStep.from_dict(s) for s in data.get("interaction_steps", [])
|
|
181
|
+
],
|
|
182
|
+
expected_results=[
|
|
183
|
+
ExpectedResult.from_dict(r) for r in data.get("expected_results", [])
|
|
184
|
+
],
|
|
185
|
+
e2e_tests=[
|
|
186
|
+
E2ETestCoverage.from_dict(t) for t in data.get("e2e_tests", [])
|
|
187
|
+
],
|
|
188
|
+
acceptance_criteria=[
|
|
189
|
+
AcceptanceCriteria.from_dict(c) for c in data.get("acceptance_criteria", [])
|
|
190
|
+
],
|
|
191
|
+
status=StoryStatus(data.get("status", "draft")),
|
|
192
|
+
version=data.get("version", "v2.2.0"),
|
|
193
|
+
evidence=data.get("evidence"),
|
|
194
|
+
created_at=data.get("created_at", datetime.now().isoformat()),
|
|
195
|
+
updated_at=data.get("updated_at", datetime.now().isoformat())
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def to_markdown(self) -> str:
|
|
199
|
+
"""转换为 Markdown 格式。"""
|
|
200
|
+
lines = [
|
|
201
|
+
f"## Story {self.story_id}: {self.title}",
|
|
202
|
+
"",
|
|
203
|
+
"### 用户目标",
|
|
204
|
+
f"**作为** {self.role}",
|
|
205
|
+
f"**我希望** {self.goal}",
|
|
206
|
+
f"**以便** {self.value}",
|
|
207
|
+
""
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
if self.preconditions:
|
|
211
|
+
lines.extend(["### 前置条件", ""])
|
|
212
|
+
for precondition in self.preconditions:
|
|
213
|
+
lines.append(f"- {precondition}")
|
|
214
|
+
lines.append("")
|
|
215
|
+
|
|
216
|
+
if self.interaction_steps:
|
|
217
|
+
lines.extend(["### 交互流程", ""])
|
|
218
|
+
lines.append("| 步骤 | 用户操作 | 系统响应 |")
|
|
219
|
+
lines.append("|------|----------|----------|")
|
|
220
|
+
for step in self.interaction_steps:
|
|
221
|
+
lines.append(f"| {step.step_number} | {step.user_action} | {step.system_response} |")
|
|
222
|
+
lines.append("")
|
|
223
|
+
|
|
224
|
+
if self.expected_results:
|
|
225
|
+
lines.append("### 预期结果")
|
|
226
|
+
for result in self.expected_results:
|
|
227
|
+
if result.scenario_type == "success":
|
|
228
|
+
lines.append("**成功场景**:")
|
|
229
|
+
else:
|
|
230
|
+
lines.append("**失败场景**:")
|
|
231
|
+
lines.append(f"- {result.description}")
|
|
232
|
+
if result.handling:
|
|
233
|
+
lines.append(f" - 处理方式: {result.handling}")
|
|
234
|
+
lines.append("")
|
|
235
|
+
|
|
236
|
+
if self.e2e_tests:
|
|
237
|
+
lines.extend(["### E2E 测试覆盖", ""])
|
|
238
|
+
lines.append("| 测试用例 | 说明 |")
|
|
239
|
+
lines.append("|----------|------|")
|
|
240
|
+
for test in self.e2e_tests:
|
|
241
|
+
lines.append(f"| {test.test_case} | {test.description} |")
|
|
242
|
+
lines.append("")
|
|
243
|
+
|
|
244
|
+
if self.acceptance_criteria:
|
|
245
|
+
lines.extend(["### 验收标准", ""])
|
|
246
|
+
for criteria in self.acceptance_criteria:
|
|
247
|
+
checkbox = "[x]" if criteria.status == AcceptanceCriteriaStatus.PASSED else "[ ]"
|
|
248
|
+
lines.append(f"- {checkbox} {criteria.description}")
|
|
249
|
+
lines.append("")
|
|
250
|
+
|
|
251
|
+
return "\n".join(lines)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class StoryManagerError(Exception):
|
|
255
|
+
"""用户故事管理异常基类。"""
|
|
256
|
+
pass
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class StoryNotFoundError(StoryManagerError):
|
|
260
|
+
"""用户故事未找到异常。"""
|
|
261
|
+
pass
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class StoryManager:
|
|
265
|
+
"""用户故事管理器。"""
|
|
266
|
+
|
|
267
|
+
STORIES_FILE = "stories.yaml"
|
|
268
|
+
|
|
269
|
+
def __init__(
|
|
270
|
+
self,
|
|
271
|
+
project_path: str,
|
|
272
|
+
stories_file: Optional[str] = None
|
|
273
|
+
):
|
|
274
|
+
"""初始化用户故事管理器。
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
project_path: 项目路径
|
|
278
|
+
stories_file: 故事数据文件
|
|
279
|
+
"""
|
|
280
|
+
self.project_path = Path(project_path)
|
|
281
|
+
self.stories_file = self.project_path / (stories_file or self.STORIES_FILE)
|
|
282
|
+
self.stories: Dict[str, UserStory] = {}
|
|
283
|
+
self._ensure_directories()
|
|
284
|
+
self._load_stories()
|
|
285
|
+
|
|
286
|
+
def _ensure_directories(self) -> None:
|
|
287
|
+
"""确保目录存在。"""
|
|
288
|
+
self.project_path.mkdir(parents=True, exist_ok=True)
|
|
289
|
+
|
|
290
|
+
def _load_stories(self) -> None:
|
|
291
|
+
"""加载故事数据。"""
|
|
292
|
+
if self.stories_file.exists():
|
|
293
|
+
try:
|
|
294
|
+
with open(self.stories_file, 'r', encoding='utf-8') as f:
|
|
295
|
+
data = yaml.safe_load(f)
|
|
296
|
+
if data and "stories" in data:
|
|
297
|
+
for story_data in data.get("stories", []):
|
|
298
|
+
story = UserStory.from_dict(story_data)
|
|
299
|
+
self.stories[story.story_id] = story
|
|
300
|
+
except Exception as e:
|
|
301
|
+
logger.warning(f"加载用户故事数据失败: {e}")
|
|
302
|
+
|
|
303
|
+
def _save_stories(self) -> None:
|
|
304
|
+
"""保存故事数据。"""
|
|
305
|
+
data = {
|
|
306
|
+
"stories": [s.to_dict() for s in self.stories.values()],
|
|
307
|
+
"updated_at": datetime.now().isoformat()
|
|
308
|
+
}
|
|
309
|
+
with open(self.stories_file, 'w', encoding='utf-8') as f:
|
|
310
|
+
yaml.dump(data, f, allow_unicode=True)
|
|
311
|
+
|
|
312
|
+
def create_story(
|
|
313
|
+
self,
|
|
314
|
+
title: str,
|
|
315
|
+
role: str,
|
|
316
|
+
goal: str,
|
|
317
|
+
value: str,
|
|
318
|
+
version: Optional[str] = None
|
|
319
|
+
) -> UserStory:
|
|
320
|
+
"""创建用户故事。
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
title: 故事标题
|
|
324
|
+
role: 用户角色
|
|
325
|
+
goal: 希望完成什么
|
|
326
|
+
value: 以便获得什么价值
|
|
327
|
+
version: 关联版本
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
创建的用户故事
|
|
331
|
+
"""
|
|
332
|
+
story_id = f"S-{len(self.stories) + 1:03d}"
|
|
333
|
+
story = UserStory(
|
|
334
|
+
story_id=story_id,
|
|
335
|
+
title=title,
|
|
336
|
+
role=role,
|
|
337
|
+
goal=goal,
|
|
338
|
+
value=value,
|
|
339
|
+
version=version or "v2.2.0"
|
|
340
|
+
)
|
|
341
|
+
self.stories[story_id] = story
|
|
342
|
+
self._save_stories()
|
|
343
|
+
logger.info(f"创建用户故事: {story_id} - {title}")
|
|
344
|
+
return story
|
|
345
|
+
|
|
346
|
+
def get_story(self, story_id: str) -> UserStory:
|
|
347
|
+
"""获取用户故事。
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
story_id: 故事 ID
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
用户故事
|
|
354
|
+
|
|
355
|
+
Raises:
|
|
356
|
+
StoryNotFoundError: 故事未找到
|
|
357
|
+
"""
|
|
358
|
+
if story_id not in self.stories:
|
|
359
|
+
raise StoryNotFoundError(f"用户故事未找到: {story_id}")
|
|
360
|
+
return self.stories[story_id]
|
|
361
|
+
|
|
362
|
+
def list_stories(
|
|
363
|
+
self,
|
|
364
|
+
status: Optional[StoryStatus] = None,
|
|
365
|
+
version: Optional[str] = None
|
|
366
|
+
) -> List[UserStory]:
|
|
367
|
+
"""列出用户故事。
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
status: 状态过滤
|
|
371
|
+
version: 版本过滤
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
用户故事列表
|
|
375
|
+
"""
|
|
376
|
+
stories = list(self.stories.values())
|
|
377
|
+
|
|
378
|
+
if status:
|
|
379
|
+
stories = [s for s in stories if s.status == status]
|
|
380
|
+
if version:
|
|
381
|
+
stories = [s for s in stories if s.version == version]
|
|
382
|
+
|
|
383
|
+
return sorted(stories, key=lambda s: s.story_id)
|
|
384
|
+
|
|
385
|
+
def add_precondition(self, story_id: str, precondition: str) -> UserStory:
|
|
386
|
+
"""添加前置条件。
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
story_id: 故事 ID
|
|
390
|
+
precondition: 前置条件
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
更新后的故事
|
|
394
|
+
|
|
395
|
+
Raises:
|
|
396
|
+
StoryNotFoundError: 故事未找到
|
|
397
|
+
"""
|
|
398
|
+
story = self.get_story(story_id)
|
|
399
|
+
story.preconditions.append(precondition)
|
|
400
|
+
story.updated_at = datetime.now().isoformat()
|
|
401
|
+
self._save_stories()
|
|
402
|
+
logger.info(f"添加前置条件: {story_id} - {precondition[:50]}...")
|
|
403
|
+
return story
|
|
404
|
+
|
|
405
|
+
def add_interaction_step(
|
|
406
|
+
self,
|
|
407
|
+
story_id: str,
|
|
408
|
+
user_action: str,
|
|
409
|
+
system_response: str
|
|
410
|
+
) -> UserStory:
|
|
411
|
+
"""添加交互步骤。
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
story_id: 故事 ID
|
|
415
|
+
user_action: 用户操作
|
|
416
|
+
system_response: 系统响应
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
更新后的故事
|
|
420
|
+
|
|
421
|
+
Raises:
|
|
422
|
+
StoryNotFoundError: 故事未找到
|
|
423
|
+
"""
|
|
424
|
+
story = self.get_story(story_id)
|
|
425
|
+
step = InteractionStep(
|
|
426
|
+
step_number=len(story.interaction_steps) + 1,
|
|
427
|
+
user_action=user_action,
|
|
428
|
+
system_response=system_response
|
|
429
|
+
)
|
|
430
|
+
story.interaction_steps.append(step)
|
|
431
|
+
story.updated_at = datetime.now().isoformat()
|
|
432
|
+
self._save_stories()
|
|
433
|
+
logger.info(f"添加交互步骤: {story_id} - 步骤 {step.step_number}")
|
|
434
|
+
return story
|
|
435
|
+
|
|
436
|
+
def add_expected_result(
|
|
437
|
+
self,
|
|
438
|
+
story_id: str,
|
|
439
|
+
scenario_type: str,
|
|
440
|
+
description: str,
|
|
441
|
+
handling: Optional[str] = None
|
|
442
|
+
) -> UserStory:
|
|
443
|
+
"""添加预期结果。
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
story_id: 故事 ID
|
|
447
|
+
scenario_type: 场景类型 (success/failure)
|
|
448
|
+
description: 描述
|
|
449
|
+
handling: 处理方式(失败场景)
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
更新后的故事
|
|
453
|
+
|
|
454
|
+
Raises:
|
|
455
|
+
StoryNotFoundError: 故事未找到
|
|
456
|
+
"""
|
|
457
|
+
story = self.get_story(story_id)
|
|
458
|
+
result = ExpectedResult(
|
|
459
|
+
scenario_type=scenario_type,
|
|
460
|
+
description=description,
|
|
461
|
+
handling=handling
|
|
462
|
+
)
|
|
463
|
+
story.expected_results.append(result)
|
|
464
|
+
story.updated_at = datetime.now().isoformat()
|
|
465
|
+
self._save_stories()
|
|
466
|
+
logger.info(f"添加预期结果: {story_id} - {scenario_type}")
|
|
467
|
+
return story
|
|
468
|
+
|
|
469
|
+
def link_e2e_test(
|
|
470
|
+
self,
|
|
471
|
+
story_id: str,
|
|
472
|
+
test_file: str,
|
|
473
|
+
test_case: str,
|
|
474
|
+
description: str
|
|
475
|
+
) -> UserStory:
|
|
476
|
+
"""关联 E2E 测试用例。
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
story_id: 故事 ID
|
|
480
|
+
test_file: 测试文件
|
|
481
|
+
test_case: 测试用例名
|
|
482
|
+
description: 说明
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
更新后的故事
|
|
486
|
+
|
|
487
|
+
Raises:
|
|
488
|
+
StoryNotFoundError: 故事未找到
|
|
489
|
+
"""
|
|
490
|
+
story = self.get_story(story_id)
|
|
491
|
+
test = E2ETestCoverage(
|
|
492
|
+
test_file=test_file,
|
|
493
|
+
test_case=test_case,
|
|
494
|
+
description=description
|
|
495
|
+
)
|
|
496
|
+
story.e2e_tests.append(test)
|
|
497
|
+
story.updated_at = datetime.now().isoformat()
|
|
498
|
+
self._save_stories()
|
|
499
|
+
logger.info(f"关联E2E测试: {story_id} - {test_case}")
|
|
500
|
+
return story
|
|
501
|
+
|
|
502
|
+
def add_acceptance_criteria(
|
|
503
|
+
self,
|
|
504
|
+
story_id: str,
|
|
505
|
+
criteria_id: str,
|
|
506
|
+
description: str
|
|
507
|
+
) -> UserStory:
|
|
508
|
+
"""添加验收标准。
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
story_id: 故事 ID
|
|
512
|
+
criteria_id: 标准 ID
|
|
513
|
+
description: 标准描述
|
|
514
|
+
|
|
515
|
+
Returns:
|
|
516
|
+
更新后的故事
|
|
517
|
+
|
|
518
|
+
Raises:
|
|
519
|
+
StoryNotFoundError: 故事未找到
|
|
520
|
+
"""
|
|
521
|
+
story = self.get_story(story_id)
|
|
522
|
+
criteria = AcceptanceCriteria(
|
|
523
|
+
criteria_id=criteria_id,
|
|
524
|
+
description=description
|
|
525
|
+
)
|
|
526
|
+
story.acceptance_criteria.append(criteria)
|
|
527
|
+
story.updated_at = datetime.now().isoformat()
|
|
528
|
+
self._save_stories()
|
|
529
|
+
logger.info(f"添加验收标准: {story_id} - {criteria_id}")
|
|
530
|
+
return story
|
|
531
|
+
|
|
532
|
+
def update_acceptance_status(
|
|
533
|
+
self,
|
|
534
|
+
story_id: str,
|
|
535
|
+
criteria_id: str,
|
|
536
|
+
status: AcceptanceCriteriaStatus,
|
|
537
|
+
notes: Optional[str] = None
|
|
538
|
+
) -> UserStory:
|
|
539
|
+
"""更新验收标准状态。
|
|
540
|
+
|
|
541
|
+
Args:
|
|
542
|
+
story_id: 故事 ID
|
|
543
|
+
criteria_id: 标准 ID
|
|
544
|
+
status: 新状态
|
|
545
|
+
notes: 备注
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
更新后的故事
|
|
549
|
+
|
|
550
|
+
Raises:
|
|
551
|
+
StoryNotFoundError: 故事未找到
|
|
552
|
+
"""
|
|
553
|
+
story = self.get_story(story_id)
|
|
554
|
+
for criteria in story.acceptance_criteria:
|
|
555
|
+
if criteria.criteria_id == criteria_id:
|
|
556
|
+
criteria.status = status
|
|
557
|
+
if notes:
|
|
558
|
+
criteria.notes = notes
|
|
559
|
+
break
|
|
560
|
+
story.updated_at = datetime.now().isoformat()
|
|
561
|
+
self._save_stories()
|
|
562
|
+
logger.info(f"更新验收状态: {story_id} - {criteria_id} -> {status.value}")
|
|
563
|
+
return story
|
|
564
|
+
|
|
565
|
+
def accept_story(self, story_id: str, evidence: str) -> UserStory:
|
|
566
|
+
"""标记故事验收通过。
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
story_id: 故事 ID
|
|
570
|
+
evidence: 验收证据
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
更新后的故事
|
|
574
|
+
|
|
575
|
+
Raises:
|
|
576
|
+
StoryNotFoundError: 故事未找到
|
|
577
|
+
"""
|
|
578
|
+
story = self.get_story(story_id)
|
|
579
|
+
story.status = StoryStatus.ACCEPTED
|
|
580
|
+
story.evidence = evidence
|
|
581
|
+
story.updated_at = datetime.now().isoformat()
|
|
582
|
+
self._save_stories()
|
|
583
|
+
logger.info(f"验收通过: {story_id}")
|
|
584
|
+
return story
|
|
585
|
+
|
|
586
|
+
def update_story_status(
|
|
587
|
+
self,
|
|
588
|
+
story_id: str,
|
|
589
|
+
status: StoryStatus
|
|
590
|
+
) -> UserStory:
|
|
591
|
+
"""更新故事状态。
|
|
592
|
+
|
|
593
|
+
Args:
|
|
594
|
+
story_id: 故事 ID
|
|
595
|
+
status: 新状态
|
|
596
|
+
|
|
597
|
+
Returns:
|
|
598
|
+
更新后的故事
|
|
599
|
+
|
|
600
|
+
Raises:
|
|
601
|
+
StoryNotFoundError: 故事未找到
|
|
602
|
+
"""
|
|
603
|
+
story = self.get_story(story_id)
|
|
604
|
+
story.status = status
|
|
605
|
+
story.updated_at = datetime.now().isoformat()
|
|
606
|
+
self._save_stories()
|
|
607
|
+
logger.info(f"更新状态: {story_id} -> {status.value}")
|
|
608
|
+
return story
|
|
609
|
+
|
|
610
|
+
def get_story_summary(self) -> Dict[str, Any]:
|
|
611
|
+
"""获取故事管理摘要。
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
摘要信息
|
|
615
|
+
"""
|
|
616
|
+
by_status = {}
|
|
617
|
+
by_version = {}
|
|
618
|
+
|
|
619
|
+
for story in self.stories.values():
|
|
620
|
+
by_status[story.status.value] = by_status.get(story.status.value, 0) + 1
|
|
621
|
+
by_version[story.version] = by_version.get(story.version, 0) + 1
|
|
622
|
+
|
|
623
|
+
return {
|
|
624
|
+
"total_stories": len(self.stories),
|
|
625
|
+
"by_status": by_status,
|
|
626
|
+
"by_version": by_version,
|
|
627
|
+
"accepted_count": len([s for s in self.stories.values() if s.status == StoryStatus.ACCEPTED]),
|
|
628
|
+
"pending_count": len([s for s in self.stories.values() if s.status == StoryStatus.PENDING_ACCEPTANCE])
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
def export_stories(self, output_path: Optional[str] = None) -> Dict[str, Any]:
|
|
632
|
+
"""导出故事数据。
|
|
633
|
+
|
|
634
|
+
Args:
|
|
635
|
+
output_path: 输出路径(可选)
|
|
636
|
+
|
|
637
|
+
Returns:
|
|
638
|
+
故事数据字典
|
|
639
|
+
"""
|
|
640
|
+
data = {
|
|
641
|
+
"stories": [s.to_dict() for s in self.stories.values()],
|
|
642
|
+
"summary": self.get_story_summary(),
|
|
643
|
+
"exported_at": datetime.now().isoformat()
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if output_path:
|
|
647
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
648
|
+
yaml.dump(data, f, allow_unicode=True)
|
|
649
|
+
|
|
650
|
+
return data
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
if __name__ == "__main__":
|
|
654
|
+
import tempfile
|
|
655
|
+
|
|
656
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
657
|
+
manager = StoryManager(tmpdir)
|
|
658
|
+
|
|
659
|
+
story = manager.create_story(
|
|
660
|
+
title="用户登录",
|
|
661
|
+
role="终端用户",
|
|
662
|
+
goal="使用用户名密码登录系统",
|
|
663
|
+
value="保护账户安全"
|
|
664
|
+
)
|
|
665
|
+
print(f"创建用户故事: {story.story_id}")
|
|
666
|
+
|
|
667
|
+
manager.add_precondition(story.story_id, "用户已注册账户")
|
|
668
|
+
manager.add_precondition(story.story_id, "用户知道正确的用户名和密码")
|
|
669
|
+
|
|
670
|
+
manager.add_interaction_step(
|
|
671
|
+
story.story_id,
|
|
672
|
+
"输入用户名和密码",
|
|
673
|
+
"系统验证凭据"
|
|
674
|
+
)
|
|
675
|
+
manager.add_interaction_step(
|
|
676
|
+
story.story_id,
|
|
677
|
+
"点击登录按钮",
|
|
678
|
+
"系统重定向到主页"
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
manager.add_expected_result(
|
|
682
|
+
story.story_id,
|
|
683
|
+
"success",
|
|
684
|
+
"用户成功登录,显示欢迎信息"
|
|
685
|
+
)
|
|
686
|
+
manager.add_expected_result(
|
|
687
|
+
story.story_id,
|
|
688
|
+
"failure",
|
|
689
|
+
"用户名或密码错误",
|
|
690
|
+
handling="显示错误提示,不跳转页面"
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
manager.link_e2e_test(
|
|
694
|
+
story.story_id,
|
|
695
|
+
"test_stories/test_story_S001.py",
|
|
696
|
+
"test_login_success",
|
|
697
|
+
"测试用户登录成功场景"
|
|
698
|
+
)
|
|
699
|
+
manager.link_e2e_test(
|
|
700
|
+
story.story_id,
|
|
701
|
+
"test_stories/test_story_S001.py",
|
|
702
|
+
"test_login_failure",
|
|
703
|
+
"测试用户登录失败场景"
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
manager.add_acceptance_criteria(story.story_id, "AC-001", "使用有效凭据可成功登录")
|
|
707
|
+
manager.add_acceptance_criteria(story.story_id, "AC-002", "使用无效凭据显示错误提示")
|
|
708
|
+
|
|
709
|
+
print(f"\n用户故事 Markdown:\n{story.to_markdown()}")
|
|
710
|
+
|
|
711
|
+
summary = manager.get_story_summary()
|
|
712
|
+
print(f"\n摘要: {summary}")
|