Skip to content

Commit ee709b6

Browse files
committed
fix: address Copilot PR review - schema anyOf, encoding, tests
- Add anyOf to 5 build tool schemas enforcing code/source_path requirement - Fix CHANGELOG: remove misleading GBK example for latin-1 fallback - Add encoding="utf-8" to stress_test.py file reads - Add tests for source_path (success, not found, neither provided) - Add test for stress_test generator failure diagnostics
1 parent d48c121 commit ee709b6

9 files changed

Lines changed: 124 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Features
1111

1212
- **source_path 参数**: 所有构建工具(solution_build, generator_build, validator_build, checker_build, interactor_build)新增 `source_path` 参数,可直接指定源文件路径,无需传入完整源码字符串。`code` 参数不再为必填,与 `source_path` 二选一。
13-
- **source_path 编码回退**: 自动处理非 UTF-8 编码的源文件(如 GBK),先尝试 UTF-8 读取,失败后回退到 latin-1。
13+
- **source_path 编码回退**: 自动处理非 UTF-8 编码的源文件,先尝试 UTF-8 读取,失败后回退到 latin-1(宽松解码,不会抛异常但可能产生乱码)
1414
- **source_path 相对 include 支持**: 当 `source_path` 指向外部文件时,自动将源文件父目录加入编译 include 路径,确保 `#include "helper.h"` 等相对引用正常工作。
1515

1616
### Improvements

src/autocode_mcp/tools/checker.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ def input_schema(self) -> dict:
7676
},
7777
},
7878
"required": ["problem_dir"],
79+
"anyOf": [
80+
{"required": ["code"]},
81+
{"required": ["source_path"]},
82+
],
7983
}
8084

8185
async def execute(

src/autocode_mcp/tools/generator.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ def input_schema(self) -> dict:
6060
},
6161
},
6262
"required": ["problem_dir"],
63+
"anyOf": [
64+
{"required": ["code"]},
65+
{"required": ["source_path"]},
66+
],
6367
}
6468

6569
async def execute(

src/autocode_mcp/tools/interactor.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ def input_schema(self) -> dict:
6868
},
6969
},
7070
"required": ["problem_dir"],
71+
"anyOf": [
72+
{"required": ["code"]},
73+
{"required": ["source_path"]},
74+
],
7175
}
7276

7377
async def execute(

src/autocode_mcp/tools/solution.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ def input_schema(self) -> dict:
6262
},
6363
},
6464
"required": ["problem_dir", "solution_type"],
65+
"anyOf": [
66+
{"required": ["code"]},
67+
{"required": ["source_path"]},
68+
],
6569
}
6670

6771
async def execute(

src/autocode_mcp/tools/stress_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ async def execute(
169169

170170
# 2. 验证输入(如果有 validator)
171171
if os.path.exists(val_exe):
172-
with open(input_path) as f:
172+
with open(input_path, encoding="utf-8") as f:
173173
input_data = f.read()
174174
val_result = await run_binary(val_exe, input_data, timeout=timeout)
175175
if val_result.return_code != 0:
@@ -179,7 +179,7 @@ async def execute(
179179
break
180180

181181
# 3. 运行 sol 和 brute,比较输出
182-
with open(input_path) as f:
182+
with open(input_path, encoding="utf-8") as f:
183183
input_data = f.read()
184184
last_input = input_data
185185

src/autocode_mcp/tools/validator.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ def input_schema(self) -> dict:
7575
},
7676
},
7777
"required": ["problem_dir"],
78+
"anyOf": [
79+
{"required": ["code"]},
80+
{"required": ["source_path"]},
81+
],
7882
}
7983

8084
async def execute(

tests/test_packaging.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,3 +290,103 @@ async def test_checker_fail_verdict():
290290
test_results = result.structuredContent.get("test_results", [])
291291
if test_results:
292292
assert test_results[0].get("actual_verdict") == "FAIL"
293+
294+
295+
# ============== source_path 参数测试 ==============
296+
297+
298+
@pytest.mark.asyncio
299+
async def test_solution_build_source_path():
300+
"""测试 solution_build 使用 source_path 参数。"""
301+
from autocode_mcp.server import call_tool, register_all_tools
302+
303+
register_all_tools()
304+
305+
import tempfile
306+
307+
with tempfile.TemporaryDirectory() as tmpdir:
308+
os.makedirs(os.path.join(tmpdir, "solutions"))
309+
source_file = os.path.join(tmpdir, "solutions", "sol.cpp")
310+
with open(source_file, "w", encoding="utf-8") as f:
311+
f.write('#include <iostream>\nint main() { std::cout << 42; return 0; }')
312+
313+
result = await call_tool(
314+
"solution_build",
315+
{"problem_dir": tmpdir, "solution_type": "sol", "source_path": source_file},
316+
)
317+
assert result.isError is False
318+
319+
320+
@pytest.mark.asyncio
321+
async def test_solution_build_source_path_not_found():
322+
"""测试 source_path 文件不存在时报错。"""
323+
from autocode_mcp.server import call_tool, register_all_tools
324+
325+
register_all_tools()
326+
327+
import tempfile
328+
329+
with tempfile.TemporaryDirectory() as tmpdir:
330+
result = await call_tool(
331+
"solution_build",
332+
{
333+
"problem_dir": tmpdir,
334+
"solution_type": "sol",
335+
"source_path": os.path.join(tmpdir, "nonexistent.cpp"),
336+
},
337+
)
338+
assert result.isError is True
339+
assert "not found" in result.structuredContent.get("error", "").lower()
340+
341+
342+
@pytest.mark.asyncio
343+
async def test_solution_build_neither_code_nor_source_path():
344+
"""测试既不提供 code 也不提供 source_path 时报错。"""
345+
from autocode_mcp.server import call_tool, register_all_tools
346+
347+
register_all_tools()
348+
349+
import tempfile
350+
351+
with tempfile.TemporaryDirectory() as tmpdir:
352+
result = await call_tool(
353+
"solution_build",
354+
{"problem_dir": tmpdir, "solution_type": "sol"},
355+
)
356+
assert result.isError is True
357+
error = result.structuredContent.get("error", "").lower()
358+
assert "either" in error or "must be provided" in error
359+
360+
361+
# ============== stress_test 错误诊断测试 ==============
362+
363+
364+
@pytest.mark.asyncio
365+
async def test_stress_test_generator_timeout_hint():
366+
"""测试 generator 超时时返回特定提示和数据字段。"""
367+
from autocode_mcp.server import call_tool, register_all_tools
368+
369+
register_all_tools()
370+
371+
import tempfile
372+
373+
with tempfile.TemporaryDirectory() as tmpdir:
374+
os.makedirs(os.path.join(tmpdir, "files"))
375+
os.makedirs(os.path.join(tmpdir, "solutions"))
376+
377+
gen_code = '#include "testlib.h"\nint main(int argc, char* argv[]) { while(true); return 0; }'
378+
gen_result = await call_tool("generator_build", {"problem_dir": tmpdir, "code": gen_code})
379+
if gen_result.isError:
380+
pytest.skip("Generator compilation failed (g++ not available)")
381+
382+
simple_code = '#include <iostream>\nint main() { int x; std::cin >> x; std::cout << x; return 0; }'
383+
await call_tool("solution_build", {"problem_dir": tmpdir, "solution_type": "sol", "code": simple_code})
384+
await call_tool("solution_build", {"problem_dir": tmpdir, "solution_type": "brute", "code": simple_code})
385+
386+
result = await call_tool("stress_test_run", {"problem_dir": tmpdir, "trials": 1, "timeout": 2})
387+
assert result.isError is True
388+
error_msg = result.structuredContent.get("error", "").lower()
389+
assert "generator failed" in error_msg
390+
data = result.structuredContent.get("data", {})
391+
assert "seed" in data
392+
assert "cmd_args" in data

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)