Files
kwcode/kaiwu/tests/test_core.py
Val-sss c32e423d04 feat: v1.9.0 — 9项架构优化,工程消化反馈替代raw输出注入
核心思路:32B模型无法有效整合raw pytest输出,工程把反馈解析成
LLM最容易理解的形式(结构化诊断句),同时加入执行反馈内循环、
docstring注入、批次拆解等机制提升通过率。

9项改动:
1. generate_diagnosis() 结构化诊断句
2. _run_execution_feedback() 内循环
3. usage_finder.py 调用关系注入
4. _extract_docstrings() + _inject_docstrings()
5. _maybe_create_missing_module() LLM生成缺失模块
6. _run_whole_file_refactor() 批次拆解
7. _build_retry_hint() delta反馈
8. _clean_code_output() 格式清理(已覆盖)
9. _inject_skill_context() SKILL.md注入

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-09 18:38:03 +08:00

464 lines
19 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Unit tests for Kaiwu v3 core modules.
Tests Gate, Orchestrator, ToolExecutor, Memory without requiring a real LLM.
"""
import json
import os
import tempfile
import pytest
# ── Mock LLM Backend ──────────────────────────────────────────
class MockLLM:
"""Mock LLM that returns predefined responses based on prompt content."""
def __init__(self, responses=None):
self.responses = responses or {}
self.call_log = []
def generate(self, prompt="", system="", max_tokens=1024,
temperature=0.0, stop=None, grammar_str=None):
self.call_log.append({"prompt": prompt[:200], "temperature": temperature})
# Check for matching response
for key, value in self.responses.items():
if key in prompt:
return value
# Default: return a valid Gate JSON
return '{"expert_type": "codegen", "task_summary": "test", "difficulty": "easy"}'
def chat(self, messages, **kwargs):
prompt = " ".join(m.get("content", "") for m in messages)
return self.generate(prompt=prompt, **kwargs)
# ── Test Gate ─────────────────────────────────────────────────
class TestGate:
def test_classify_valid_json(self):
from kaiwu.core.gate import Gate
llm = MockLLM({
"修复": '{"expert_type": "locator_repair", "task_summary": "修复bug", "difficulty": "easy"}',
})
gate = Gate(llm=llm)
result = gate.classify("修复登录bug")
assert result["expert_type"] == "locator_repair"
assert result["difficulty"] == "easy"
assert "_parse_error" not in result
def test_classify_invalid_json_fallback(self):
from kaiwu.core.gate import Gate
llm = MockLLM({
"测试": "这不是JSON我来解释一下...",
})
gate = Gate(llm=llm)
result = gate.classify("测试一下")
# Should fallback to chat/easy (Gate parse failure → chat降级)
assert result["expert_type"] == "chat"
assert result["difficulty"] == "easy"
assert "_parse_error" in result
def test_classify_invalid_expert_type_fallback(self):
from kaiwu.core.gate import Gate
llm = MockLLM({
"写诗": '{"expert_type": "poetry", "task_summary": "写诗", "difficulty": "easy"}',
})
gate = Gate(llm=llm)
result = gate.classify("帮我写诗")
assert result["expert_type"] == "chat" # fallback
assert "_parse_error" in result
def test_classify_json_wrapped_in_text(self):
from kaiwu.core.gate import Gate
llm = MockLLM({
"补全": '好的,分析结果如下:\n{"expert_type": "codegen", "task_summary": "补全函数", "difficulty": "easy"}\n以上是分类结果。',
})
gate = Gate(llm=llm)
result = gate.classify("帮我补全这个函数")
assert result["expert_type"] == "codegen"
assert "_parse_error" not in result
# ── Test ToolExecutor ─────────────────────────────────────────
class TestToolExecutor:
def test_read_write_file(self):
from kaiwu.tools.executor import ToolExecutor
with tempfile.TemporaryDirectory() as tmpdir:
tools = ToolExecutor(project_root=tmpdir)
# Write
assert tools.write_file("test.txt", "hello kaiwu")
# Read
content = tools.read_file("test.txt")
assert content == "hello kaiwu"
def test_read_nonexistent(self):
from kaiwu.tools.executor import ToolExecutor
with tempfile.TemporaryDirectory() as tmpdir:
tools = ToolExecutor(project_root=tmpdir)
result = tools.read_file("nonexistent.txt")
assert "[ERROR]" in result
def test_list_dir(self):
from kaiwu.tools.executor import ToolExecutor
with tempfile.TemporaryDirectory() as tmpdir:
tools = ToolExecutor(project_root=tmpdir)
tools.write_file("a.py", "pass")
tools.write_file("b.py", "pass")
entries = tools.list_dir(".")
assert "a.py" in entries
assert "b.py" in entries
def test_run_bash(self):
from kaiwu.tools.executor import ToolExecutor
with tempfile.TemporaryDirectory() as tmpdir:
tools = ToolExecutor(project_root=tmpdir)
stdout, stderr, rc = tools.run_bash("echo hello")
assert "hello" in stdout
assert rc == 0
def test_run_bash_timeout(self):
from kaiwu.tools.executor import ToolExecutor
with tempfile.TemporaryDirectory() as tmpdir:
tools = ToolExecutor(project_root=tmpdir)
_, stderr, rc = tools.run_bash("sleep 10", timeout=1)
assert rc == -1
assert "timed out" in stderr.lower() or "timeout" in stderr.lower()
def test_apply_patch(self):
from kaiwu.tools.executor import ToolExecutor
with tempfile.TemporaryDirectory() as tmpdir:
tools = ToolExecutor(project_root=tmpdir)
tools.write_file("code.py", "def foo():\n return 1\n")
assert tools.apply_patch("code.py", "return 1", "return 2")
content = tools.read_file("code.py")
assert "return 2" in content
assert "return 1" not in content
def test_get_file_tree(self):
from kaiwu.tools.executor import ToolExecutor
with tempfile.TemporaryDirectory() as tmpdir:
tools = ToolExecutor(project_root=tmpdir)
os.makedirs(os.path.join(tmpdir, "src"))
tools.write_file("src/main.py", "pass")
tree = tools.get_file_tree(".")
assert "src" in tree
assert "main.py" in tree
# ── Test KaiwuMemory ──────────────────────────────────────────
class TestKaiwuMemory:
def test_init_creates_file(self):
from kaiwu.memory.kaiwu_md import KaiwuMemory
with tempfile.TemporaryDirectory() as tmpdir:
mem = KaiwuMemory()
result = mem.init(tmpdir)
assert "Created" in result
assert os.path.exists(os.path.join(tmpdir, ".kaiwu", "PROJECT.md"))
def test_init_no_overwrite(self):
from kaiwu.memory.kaiwu_md import KaiwuMemory
with tempfile.TemporaryDirectory() as tmpdir:
mem = KaiwuMemory()
mem.init(tmpdir)
result = mem.init(tmpdir)
assert "already exists" in result
def test_load_empty(self):
from kaiwu.memory.kaiwu_md import KaiwuMemory
with tempfile.TemporaryDirectory() as tmpdir:
mem = KaiwuMemory()
result = mem.load(tmpdir)
assert result == "" # No KAIWU.md yet
def test_load_after_init(self):
from kaiwu.memory.kaiwu_md import KaiwuMemory
with tempfile.TemporaryDirectory() as tmpdir:
mem = KaiwuMemory()
mem.init(tmpdir)
result = mem.load(tmpdir)
assert "项目信息" in result
def test_save_on_success(self):
from kaiwu.memory.kaiwu_md import KaiwuMemory
from kaiwu.core.context import TaskContext
with tempfile.TemporaryDirectory() as tmpdir:
mem = KaiwuMemory()
mem.init(tmpdir)
ctx = TaskContext(
user_input="修复bug",
project_root=tmpdir,
gate_result={"expert_type": "locator_repair"},
locator_output={"relevant_files": ["src/main.py"], "relevant_functions": ["foo"]},
verifier_output={"passed": True, "syntax_ok": True, "tests_passed": 1, "tests_total": 1},
)
mem.save(tmpdir, ctx)
content = mem.show(tmpdir)
assert "locator_repair" in content
assert "src/main.py" in content
def test_no_save_on_failure(self):
from kaiwu.memory.kaiwu_md import KaiwuMemory
from kaiwu.core.context import TaskContext
with tempfile.TemporaryDirectory() as tmpdir:
mem = KaiwuMemory()
mem.init(tmpdir)
ctx = TaskContext(
user_input="修复bug",
project_root=tmpdir,
gate_result={"expert_type": "locator_repair"},
verifier_output={"passed": False},
)
mem.save(tmpdir, ctx)
# PROJECT.md and EXPERT.md should NOT contain the failed task record
from kaiwu.memory import project_md, expert_md
proj_content = project_md.show(tmpdir)
expert_content = expert_md.show(tmpdir)
assert "locator_repair" not in proj_content
assert "locator_repair" not in expert_content
# ── Test Orchestrator ─────────────────────────────────────────
class TestOrchestrator:
def _make_orchestrator(self, locator_result=None, generator_result=None, verifier_result=None):
"""Create orchestrator with mock experts."""
from kaiwu.core.orchestrator import PipelineOrchestrator
from kaiwu.memory.kaiwu_md import KaiwuMemory
class MockLocator:
def run(self, ctx):
if locator_result:
ctx.locator_output = locator_result
ctx.relevant_code_snippets = {"test.py": "def foo(): pass"}
return locator_result
class MockGenerator:
def run(self, ctx):
if generator_result:
ctx.generator_output = generator_result
return generator_result
class MockVerifier:
def run(self, ctx):
if verifier_result:
ctx.verifier_output = verifier_result
return verifier_result
class MockSearch:
def search(self, ctx):
return "mock search results"
class MockOffice:
def run(self, ctx):
return {"passed": False, "error": "not implemented"}
from kaiwu.tools.executor import ToolExecutor
with tempfile.TemporaryDirectory() as tmpdir:
tools = ToolExecutor(project_root=tmpdir)
memory = KaiwuMemory()
memory.init(tmpdir)
orch = PipelineOrchestrator(
locator=MockLocator(),
generator=MockGenerator(),
verifier=MockVerifier(),
search_augmentor=MockSearch(),
office_handler=MockOffice(),
tool_executor=tools,
memory=memory,
)
return orch, tmpdir
def test_codegen_success(self):
gen = {"patches": [{"file": "test.py", "original": "old", "modified": "new"}], "explanation": "fixed"}
ver = {"passed": True, "syntax_ok": True, "tests_passed": 1, "tests_total": 1, "error_detail": ""}
orch, tmpdir = self._make_orchestrator(generator_result=gen, verifier_result=ver)
result = orch.run(
user_input="写一个函数",
gate_result={"expert_type": "codegen", "difficulty": "easy"},
project_root=tmpdir,
)
assert result["success"] is True
def test_locator_repair_success(self):
loc = {"relevant_files": ["test.py"], "relevant_functions": ["foo"], "edit_locations": []}
gen = {"patches": [{"file": "test.py", "original": "old", "modified": "new"}], "explanation": "fixed"}
ver = {"passed": True, "syntax_ok": True, "tests_passed": 1, "tests_total": 1, "error_detail": ""}
orch, tmpdir = self._make_orchestrator(locator_result=loc, generator_result=gen, verifier_result=ver)
result = orch.run(
user_input="修复bug",
gate_result={"expert_type": "locator_repair", "difficulty": "easy"},
project_root=tmpdir,
)
assert result["success"] is True
def test_max_retries_exceeded(self):
gen = {"patches": [{"file": "test.py", "original": "old", "modified": "new"}], "explanation": "fixed"}
ver = {"passed": False, "syntax_ok": False, "tests_passed": 0, "tests_total": 1, "error_detail": "syntax error"}
orch, tmpdir = self._make_orchestrator(generator_result=gen, verifier_result=ver)
result = orch.run(
user_input="写一个函数",
gate_result={"expert_type": "codegen", "difficulty": "easy"},
project_root=tmpdir,
)
assert result["success"] is False
assert "Max retries" in result["error"]
# test_search_triggered_on_hard_task removed — search trigger mechanism was deleted
# ── Test Expert Sequences ─────────────────────────────────────
class TestExpertSequences:
def test_sequence_mapping(self):
from kaiwu.core.orchestrator import EXPERT_SEQUENCES
assert EXPERT_SEQUENCES["locator_repair"] == ["locator", "generator", "verifier"]
assert EXPERT_SEQUENCES["codegen"] == ["generator", "verifier"]
assert EXPERT_SEQUENCES["refactor"] == ["locator", "generator", "verifier"]
assert EXPERT_SEQUENCES["doc"] == ["locator", "generator"]
assert EXPERT_SEQUENCES["office"] == ["office"]
# ── Test Context ──────────────────────────────────────────────
class TestContext:
def test_default_values(self):
from kaiwu.core.context import TaskContext
ctx = TaskContext()
assert ctx.retry_count == 0
assert ctx.search_triggered is False
assert ctx.locator_output is None
assert ctx.generator_output is None
assert ctx.verifier_output is None
def test_independent_contexts(self):
"""RED-3: Each expert must have independent context."""
from kaiwu.core.context import TaskContext
ctx1 = TaskContext(user_input="task1")
ctx2 = TaskContext(user_input="task2")
ctx1.locator_output = {"files": ["a.py"]}
assert ctx2.locator_output is None # Independent
# ── Test Generator filename extraction ───────────────────────
class TestExtractFilename:
def _extract(self, user_input):
from kaiwu.experts.generator import GeneratorExpert
return GeneratorExpert._extract_filename(user_input)
def test_explicit_filename(self):
assert self._extract("帮我写个 login.py") == "login.py"
assert self._extract("create server.js for me") == "server.js"
assert self._extract("生成 config.yaml") == "config.yaml"
def test_explicit_filename_with_path(self):
# Should extract just the filename part from the regex
result = self._extract("写个 utils.py 工具函数")
assert result == "utils.py"
def test_chinese_codegen_pattern(self):
# English name after Chinese verb → extracted
assert self._extract("写个sort函数") == "sort.py"
assert self._extract("写一个calculator类") == "calculator.py"
def test_english_create_pattern(self):
assert self._extract("create a calculator") == "calculator.py"
assert self._extract("write a parser") == "parser.py"
assert self._extract("generate a scheduler") == "scheduler.py"
def test_skip_generic_words(self):
# "create a new function" → "new" and "function" are generic, fall through
result = self._extract("create a new function")
# Should not be "new.py" or "function.py"
assert result == "output.py"
def test_fallback_to_output(self):
assert self._extract("帮我写段代码") == "output.py"
assert self._extract("随便写点什么") == "output.py"
def test_multiple_extensions(self):
assert self._extract("写 main.go") == "main.go"
assert self._extract("create index.html") == "index.html"
assert self._extract("生成 Makefile.sh") == "Makefile.sh"
def test_language_detection_html(self):
# "写个HTML页面" → should detect .html extension
assert self._extract("帮我写个html页面").endswith(".html")
assert self._extract("写一个网页").endswith(".html")
def test_language_detection_js(self):
assert self._extract("写个javascript函数").endswith(".js")
def test_language_detection_shell(self):
assert self._extract("写个shell脚本").endswith(".sh")
assert self._extract("写个bash脚本").endswith(".sh")
class TestCleanCodeOutput:
def test_strip_tool_call_lines(self):
from kaiwu.experts.generator import GeneratorExpert
raw = "write_file output.html\n<html>\n<body>hello</body>\n</html>"
result = GeneratorExpert._clean_code_output(raw)
assert "write_file" not in result
assert "<html>" in result
def test_strip_markdown_blocks(self):
from kaiwu.experts.generator import GeneratorExpert
raw = "```html\n<h1>hello</h1>\n```"
result = GeneratorExpert._clean_code_output(raw)
assert "```" not in result
assert "<h1>hello</h1>" in result
class TestCodegenOutput:
"""Test that _run_codegen uses real filename instead of new_code.py."""
def test_codegen_uses_extracted_filename(self):
from kaiwu.experts.generator import GeneratorExpert
from kaiwu.core.context import TaskContext
llm = MockLLM({"生成": "def hello():\n print('hello')"})
gen = GeneratorExpert(llm=llm, num_candidates=1)
ctx = TaskContext(
user_input="写个 hello.py",
project_root="/tmp/test_project",
gate_result={"expert_type": "codegen"},
)
result = gen._run_codegen(ctx)
assert result is not None
assert result["patches"][0]["file"] == "hello.py"
assert "hello.py" in result["explanation"]
def test_codegen_fallback_filename(self):
from kaiwu.experts.generator import GeneratorExpert
from kaiwu.core.context import TaskContext
llm = MockLLM({"生成": "x = 1"})
gen = GeneratorExpert(llm=llm, num_candidates=1)
ctx = TaskContext(
user_input="帮我写段代码",
project_root="/tmp/test_project",
gate_result={"expert_type": "codegen"},
)
result = gen._run_codegen(ctx)
assert result is not None
assert result["patches"][0]["file"] == "output.py"
if __name__ == "__main__":
pytest.main([__file__, "-v"])