# Mock LLM client for offline development/testing. import json import logging from server.core.llm_provider.base import LLMClient logger = logging.getLogger("testflow") MOCK_IR_YAML = """meta: prd_title: ZeekerWatchman 产品需求文档 extraction_date: "2026-05-22" skill_used: default features: - module: PRD管理 feature_name: PRD文件上传 description: 支持拖拽上传或文本粘贴,解析.md/.txt/.docx/.pdf格式文件 inputs: - PRD文件 (.md/.txt/.docx/.pdf) outputs: - PRD_Version记录 - 纯文本摘要 preconditions: - 用户已登录平台 constraints: - 文件大小不超过10MB - 仅支持指定格式 priority: P0 dependencies: [] - module: PRD管理 feature_name: PRD版本快照 description: 上传后即时保存为不可变版本,支持历史回溯 inputs: - 已上传的PRD outputs: - 版本快照记录 preconditions: - PRD已成功上传 constraints: - 版本不可修改 priority: P1 dependencies: - PRD文件上传 - module: IR引擎 feature_name: IR生成 description: 调用LLM利用Skill的extract_ir_prompt将PRD转化为符合IR Schema的YAML inputs: - PRD文本 - Skill名称 outputs: - IR YAML preconditions: - PRD解析完成 - Skill已选择 constraints: - 必须符合ir_schema.json - 必须满足principles.yaml约束 priority: P0 dependencies: - PRD文件上传 - module: IR引擎 feature_name: IR可视化确认 description: 双栏展示,左侧YAML编辑器,右侧思维导图实时渲染 inputs: - IR YAML outputs: - 用户确认/编辑后的IR preconditions: - IR已生成 constraints: - 支持实时编辑保存 priority: P1 dependencies: - IR生成 - module: 用例引擎 feature_name: 测试用例生成 description: 基于确认后的IR调用gen_cases_prompt生成JSON格式用例集 inputs: - 最终IR YAML - Skill的gen_cases_prompt outputs: - TestCase_Set preconditions: - IR已确认 constraints: - P0功能必须覆盖异常和边界场景 - 用例标题使用Given-When-Then结构 priority: P0 dependencies: - IR生成 - module: 用例引擎 feature_name: 测试用例导出 description: 支持一键导出为YAML/CSV/XMind格式 inputs: - TestCase_Set - 目标格式 outputs: - 下载文件 preconditions: - 用例已生成 constraints: - YAML用于自动化,CSV用于评审,XMind用于展示 priority: P1 dependencies: - 测试用例生成""" MOCK_TESTCASES = [ { "id": "TC-ZWM-001", "module": "PRD管理", "feature": "PRD文件上传", "case_title": "正向-上传md格式PRD成功", "preconditions": "用户已登录,文件为有效md格式", "steps": "Given 用户在主页面\nWhen 拖拽或选择一个.md文件上传\nThen 系统解析成功并显示PRD摘要", "expected_result": "返回PRD ID,状态为ready,显示文本摘要", "priority": "P0", "tags": ["正向", "冒烟"] }, { "id": "TC-ZWM-002", "module": "PRD管理", "feature": "PRD文件上传", "case_title": "异常-上传不支持的文件格式", "preconditions": "用户已登录", "steps": "Given 用户在主页面\nWhen 上传一个.exe文件\nThen 系统返回错误提示", "expected_result": "返回400错误,提示仅支持.md/.txt/.docx/.pdf", "priority": "P0", "tags": ["异常"] }, { "id": "TC-ZWM-003", "module": "PRD管理", "feature": "PRD文件上传", "case_title": "边界-上传超过10MB的文件", "preconditions": "用户已登录", "steps": "Given 用户在主页面\nWhen 上传一个11MB的md文件\nThen 系统拒绝并提示文件过大", "expected_result": "返回错误,提示文件大小不超过10MB", "priority": "P1", "tags": ["边界"] }, { "id": "TC-ZWM-004", "module": "IR引擎", "feature": "IR生成", "case_title": "正向-从PRD生成IR成功", "preconditions": "PRD已解析,Skill已选择", "steps": "Given PRD文本可用\nWhen 调用IR生成接口\nThen 返回符合IR Schema的YAML", "expected_result": "生成包含features列表的YAML,功能点齐全", "priority": "P0", "tags": ["正向"] }, { "id": "TC-ZWM-005", "module": "IR引擎", "feature": "IR生成", "case_title": "异常-空PRD文本生成IR", "preconditions": "PRD文本为空", "steps": "Given PRD内容为空\nWhen 调用IR生成接口\nThen 返回错误提示", "expected_result": "返回400错误,提示PRD无文本内容", "priority": "P0", "tags": ["异常"] }, { "id": "TC-ZWM-006", "module": "用例引擎", "feature": "测试用例生成", "case_title": "正向-基于IR生成测试用例", "preconditions": "IR已确认", "steps": "Given IR YAML可用\nWhen 调用用例生成接口\nThen 返回JSON格式的用例集", "expected_result": "用例集包含P0功能的正向和异常用例,步骤为Given-When-Then格式", "priority": "P0", "tags": ["正向"] }, { "id": "TC-ZWM-007", "module": "用例引擎", "feature": "测试用例导出", "case_title": "正向-导出用例为YAML格式", "preconditions": "用例集已生成", "steps": "Given 用例集可用\nWhen 选择YAML格式导出\nThen 下载YAML文件", "expected_result": "下载的YAML文件包含所有用例及元数据", "priority": "P1", "tags": ["正向"] }, { "id": "TC-ZWM-008", "module": "用例引擎", "feature": "测试用例导出", "case_title": "正向-导出用例为CSV格式", "preconditions": "用例集已生成", "steps": "Given 用例集可用\nWhen 选择CSV格式导出\nThen 下载CSV文件", "expected_result": "CSV文件包含用例ID、模块、标题、步骤、预期结果等列", "priority": "P1", "tags": ["正向"] } ] MOCK_SEMANTIC_INDEX = { "feature_name": "ZeekerWatchman 测试用例智能管理平台", "concepts": [ {"name": "PRD", "aliases": ["产品需求文档", "需求文档"], "defined_in": ["1"]}, {"name": "IR", "aliases": ["中间表示", "Intermediate Representation"], "defined_in": ["1", "4.2.2"]}, {"name": "Skill", "aliases": ["技能包", "测试方法论"], "defined_in": ["4.1"]}, {"name": "TestCase", "aliases": ["测试用例", "用例"], "defined_in": ["4.2.3"]}, ], "function_units": [ { "unit_id": "FU-001", "name": "PRD文件上传与解析", "description": "用户上传.md/.txt/.docx/.pdf格式的PRD文件,系统调用解析器提取纯文本和图片,生成版本快照", "sources": [{"section": "4.2.1 PRD 输入与解析", "type": "para", "text_snippet": "支持拖拽上传或直接粘贴文本。服务端调用LLM或本地解析库提取纯文本"}], }, { "unit_id": "FU-002", "name": "IR生成", "description": "基于PRD文本和选定的Skill,调用LLM生成符合IR Schema的YAML中间表示", "sources": [{"section": "4.2.2 中间表示 IR 生成与确认", "type": "para", "text_snippet": "reasoning模块调用llm_provider,利用Skill的extract_ir_prompt.j2生成符合ir_schema.json的YAML"}], }, { "unit_id": "FU-003", "name": "IR验证与自检", "description": "对生成的IR进行JSON Schema校验和Principles规则检查,输出审核意见", "sources": [{"section": "4.2.2 中间表示 IR 生成与确认", "type": "para", "text_snippet": "LangGraph节点会校验IR是否满足schema和soul/principles.yaml"}], }, { "unit_id": "FU-004", "name": "IR人工确认与编辑", "description": "用户可在双栏界面编辑YAML并保存,保存触发新版本生成", "sources": [{"section": "4.2.2 中间表示 IR 生成与确认", "type": "para", "text_snippet": "用户可直接编辑YAML并保存,保存操作触发新IR_Version生成"}], }, { "unit_id": "FU-005", "name": "测试用例生成", "description": "基于确认后的IR调用gen_cases_prompt生成JSON格式用例集,P0功能覆盖正向和异常场景", "sources": [{"section": "4.2.3 测试用例生成与导出", "type": "para", "text_snippet": "基于确认后的最终IR,调用gen_cases_prompt.j2生成JSON格式的用例集"}], }, { "unit_id": "FU-006", "name": "测试用例导出", "description": "支持将用例集导出为YAML/CSV/XMind三种格式", "sources": [{"section": "4.2.3 测试用例生成与导出", "type": "para", "text_snippet": "YAML带语法高亮的代码预览和文件下载,CSV文件,XMind服务端生成.xmind文件下载"}], }, ], } MOCK_IR_RULES = [ { "description": "用户上传PRD文件时,系统检查文件格式(.md/.txt/.docx/.pdf)和大小(≤10MB),通过后调用解析器提取纯文本并创建不可变版本快照", "priority": "P0", "sources": [{"type": "para", "section": "4.2.1 PRD 输入与解析", "text_snippet": "支持拖拽上传或直接粘贴文本"}], "precondition": {"app_type": "Web应用", "app_state": "已登录"}, "trigger": { "operator": "AND", "conditions": [ {"signal": "文件格式", "operator": "in", "value": [".md", ".txt", ".docx", ".pdf"]}, {"signal": "文件大小", "operator": "<=", "value": 10, "unit": "MB"}, ], }, "actions": [ {"type": "system", "description": "保存原始文件"}, {"type": "system", "description": "调用解析器提取纯文本"}, {"type": "system", "description": "创建PRD_Version快照"}, ], }, { "description": "上传不支持的文件格式时,系统拒绝并返回错误提示,仅支持.md/.txt/.docx/.pdf", "priority": "P0", "sources": [{"type": "para", "section": "4.2.1", "text_snippet": "支持拖拽上传"}], "precondition": {}, "trigger": { "operator": "OR", "conditions": [ {"signal": "文件格式", "operator": "not_in", "value": [".md", ".txt", ".docx", ".pdf"]}, {"signal": "文件大小", "operator": ">", "value": 10, "unit": "MB"}, ], }, "actions": [ {"type": "user_interaction", "description": "显示错误提示", "content": "不支持的文件格式,仅支持.md/.txt/.docx/.pdf,且不超过10MB"}, ], }, { "description": "基于PRD文本和选定Skill调用DeepSeek生成IR YAML,结果需符合ir_schema.json和principles.yaml", "priority": "P0", "sources": [{"type": "para", "section": "4.2.2", "text_snippet": "reasoning模块调用llm_provider生成IR"}], "precondition": {"app_state": "PRD已解析"}, "trigger": { "operator": "AND", "conditions": [ {"signal": "PRD文本", "operator": "exists", "value": True}, {"signal": "Skill", "operator": "selected", "value": True}, ], }, "actions": [ {"type": "system", "description": "加载Skill的extract_ir_prompt.j2模板"}, {"type": "system", "description": "调用LLM生成IR YAML"}, {"type": "system", "description": "持久化IR_Version"}, ], }, { "description": "IR生成后自动进行JSON Schema校验和Principles规则检查,输出审核意见包含error和warning", "priority": "P1", "sources": [{"type": "para", "section": "4.2.2", "text_snippet": "LangGraph节点会校验IR"}], "precondition": {"app_state": "IR已生成"}, "trigger": {"operator": "AND", "conditions": [{"signal": "IR", "operator": "exists", "value": True}]}, "actions": [ {"type": "system", "description": "JSON Schema校验"}, {"type": "system", "description": "Principles规则检查"}, {"type": "user_interaction", "description": "展示审核意见列表"}, ], }, { "description": "基于确认后的IR生成JSON格式测试用例集,每个P0功能点至少包含正向和异常各1条用例,使用Given-When-Then结构", "priority": "P0", "sources": [{"type": "para", "section": "4.2.3", "text_snippet": "基于确认后的最终IR生成JSON格式的用例集"}], "precondition": {"app_state": "IR已确认"}, "trigger": {"operator": "AND", "conditions": [{"signal": "IR已确认", "operator": "==", "value": True}]}, "actions": [ {"type": "system", "description": "加载Skill的gen_cases_prompt.j2模板"}, {"type": "system", "description": "调用LLM生成用例JSON"}, {"type": "system", "description": "创建TestCase_Set并关联IR_Version"}, ], }, { "description": "用例导出功能:YAML格式用于自动化执行,CSV用于Excel评审归档", "priority": "P1", "sources": [{"type": "para", "section": "4.2.3", "text_snippet": "支持一键导出YAML/CSV/XMind"}], "precondition": {"app_state": "用例已生成"}, "trigger": {"operator": "OR", "conditions": [ {"signal": "导出格式", "operator": "==", "value": "yaml"}, {"signal": "导出格式", "operator": "==", "value": "csv"}, {"signal": "导出格式", "operator": "==", "value": "xmind"}, ]}, "actions": [ {"type": "system", "description": "格式化用例为指定格式"}, {"type": "user_interaction", "description": "触发文件下载"}, ], }, ] def _mock_chat_reply(prompt: str) -> str: """Generate a mock chat reply based on user message content.""" # Extract the last user message user_msg = "" for line in prompt.split("\n"): if line.startswith("## 当前上下文"): break if line and not line.startswith("#") and not line.startswith("```"): user_msg += line + " " if "分析" in user_msg or "功能点" in user_msg: return ( "根据 PRD 内容分析,我发现了以下功能点:\n\n" "1. **用户登录** - 支持邮箱/手机号登录,包含密码验证和错误锁定机制\n" "2. **用户注册** - 新用户注册流程,需要邮箱验证\n" "3. **密码重置** - 通过邮箱验证码重置密码\n\n" "建议操作:\n" "- 点击「生成 IR」将这些功能点转换为结构化 IR\n" "- 我可以帮你检查是否有遗漏的功能点" ) if "用例" in user_msg or "测试" in user_msg: return ( "当前测试用例包含以下覆盖:\n\n" "- P0 正向用例:覆盖核心登录、注册流程\n" "- P0 异常用例:密码错误、账号锁定\n" "- P1 边界用例:连续错误锁定 15 分钟\n\n" "建议:增加并发登录和 Token 过期测试" ) return ( "你好!我是 ZeekerWatchman 助手。我可以帮你:\n\n" "- 分析 PRD 文档,提取功能点\n" "- 审查和修改 IR(中间表示)\n" "- 生成和优化测试用例\n" "- 导出测试用例为 YAML/CSV 格式\n\n" "请上传一个 PRD 文档开始,或者告诉我你需要什么帮助。" ) class MockLLMClient(LLMClient): """Mock client that returns realistic dummy responses for demo/testing.""" def __init__(self, model_name: str = "mock"): self._client = None self._timeout = 60 self._model = model_name self._prompt_tokens = 0 self._completion_tokens = 0 def chat( self, model: str, messages: list[dict], *, timeout: int | None = None, response_format: dict | None = None, temperature: float = 0.0, ) -> str: # Check both system prompt and last message for signal phrases full_prompt = "" sys_prompt = "" last_msg = "" if messages: for m in messages: content = m.get("content", "") content = content if isinstance(content, str) else str(content) full_prompt += content + "\n" if m.get("role") == "system": sys_prompt = content last_msg = messages[-1].get("content", "") last_msg = last_msg if isinstance(last_msg, str) else str(last_msg) # Detect mode: check system prompt first, then last user message is_chat = ( "测试用例管理助手" in full_prompt or "## 当前上下文" in full_prompt ) is_semantic = ( "语义索引" in full_prompt or "function_units" in full_prompt or "semantic" in full_prompt.lower() ) is_tc = ( "生成完整的测试用例集" in full_prompt or "Given-When-Then" in full_prompt or "## IR 内容" in full_prompt ) is_stage2 = ( "精准上下文包" in full_prompt or "IR Schema" in full_prompt or "unit_id" in full_prompt ) if is_chat: result = _mock_chat_reply(last_msg) elif is_semantic: result = json.dumps(MOCK_SEMANTIC_INDEX, ensure_ascii=False) elif is_stage2: result = json.dumps(MOCK_IR_RULES, ensure_ascii=False) elif is_tc: result = json.dumps(MOCK_TESTCASES, ensure_ascii=False) else: result = MOCK_IR_YAML logger.info("[MOCK] → %d chars (mode=%s)", len(result), "chat" if is_chat else "semantic" if is_semantic else "stage2" if is_stage2 else "testcases" if is_tc else "ir_yaml") return result def chat_with_image( self, model: str, image_path: str, prompt: str, *, timeout: int | None = None, ) -> str: return "type: other\nMock image analysis - this is a demo response."