ht32-code-mcp 0.4.1__tar.gz → 0.4.3__tar.gz
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.
- {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.3}/PKG-INFO +1 -1
- {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.3}/pyproject.toml +1 -1
- {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.3}/src/ht32_code_mcp/materializer.py +126 -6
- {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.3}/src/ht32_code_mcp/server.py +8 -2
- {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.3}/src/ht32_code_mcp/usage.py +5 -2
- ht32_code_mcp-0.4.3/tests/test_recipe_claims.py +92 -0
- {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.3}/tests/test_tools.py +64 -1
- {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.3}/uv.lock +1 -1
- {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.3}/.gitignore +0 -0
- {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.3}/.gitkeep +0 -0
- {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.3}/.pytest_base_final_run.log +0 -0
- {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.3}/DEPLOY.md +0 -0
- {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.3}/README.md +0 -0
- {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.3}/e2e_scenario_a.py +0 -0
- {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.3}/e2e_scenario_b.py +0 -0
- {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.3}/smoke_http.py +0 -0
- {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.3}/smoke_stdio.py +0 -0
- {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.3}/src/ht32_code_mcp/__init__.py +0 -0
- {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.3}/src/ht32_code_mcp/__main__.py +0 -0
- {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.3}/src/ht32_code_mcp/exporter.py +0 -0
- {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.3}/src/ht32_code_mcp/index.py +0 -0
- {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.3}/src/ht32_code_mcp/knowledge.py +0 -0
- {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.3}/src/ht32_code_mcp/release.py +0 -0
- {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.3}/tests/conftest.py +0 -0
- {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.3}/tests/test_export_cloud_mode.py +0 -0
- {ht32_code_mcp-0.4.1 → ht32_code_mcp-0.4.3}/tests/test_knowledge_grounding.py +0 -0
|
@@ -95,6 +95,28 @@ def pack_release_version(pack_path: Path) -> str:
|
|
|
95
95
|
return m.group(1).decode()
|
|
96
96
|
|
|
97
97
|
|
|
98
|
+
def pack_device_algorithm(pack_path: Path, device: str) -> str | None:
|
|
99
|
+
"""DFP pdsc 中该 device 的首个 Flash 算法路径(如 `Flash/HT32F490x1_64.FLM`)。
|
|
100
|
+
|
|
101
|
+
只从随包 DFP 取,不按型号猜:M4 同一子系列不同容量用的是不同 FLM
|
|
102
|
+
(F49041=_64、F49031=_32;F49153=_128、F49163=_256;F49365=_256、F49395=_1024)。
|
|
103
|
+
"""
|
|
104
|
+
try:
|
|
105
|
+
with zipfile.ZipFile(pack_path) as z:
|
|
106
|
+
names = [n for n in z.namelist() if n.lower().endswith(".pdsc")]
|
|
107
|
+
if not names:
|
|
108
|
+
return None
|
|
109
|
+
data = z.read(names[0]).decode("utf-8", errors="replace")
|
|
110
|
+
except zipfile.BadZipFile:
|
|
111
|
+
return None
|
|
112
|
+
m = re.search(r'<device[^>]*Dname="' + re.escape(device) + r'"[^>]*>(.*?)</device>',
|
|
113
|
+
data, re.S)
|
|
114
|
+
if not m:
|
|
115
|
+
return None
|
|
116
|
+
alg = re.search(r'<algorithm[^>]*name="([^"]+\.FLM)"', m.group(1))
|
|
117
|
+
return alg.group(1) if alg else None
|
|
118
|
+
|
|
119
|
+
|
|
98
120
|
# ------------------------------------------------------------------ recipe 装载
|
|
99
121
|
def load_recipes(recipes_dir: Path) -> dict[str, dict]:
|
|
100
122
|
recipes: dict[str, dict] = {}
|
|
@@ -752,12 +774,7 @@ class Materializer:
|
|
|
752
774
|
# uvoptx(受控调试配置;M4 用官方侧车,f5 用净化后的用户侧车,l5/f1 不提供)
|
|
753
775
|
uvoptx_src = self._controlled_uvoptx(r)
|
|
754
776
|
if uvoptx_src is not None:
|
|
755
|
-
data =
|
|
756
|
-
data, n = re.subn(rb"(<TargetName>)[^<]*(</TargetName>)",
|
|
757
|
-
lambda m: m.group(1) + stem.encode() + m.group(2), data, count=1)
|
|
758
|
-
if n == 1:
|
|
759
|
-
deviations.append(dict(kind="uvoptx-targetname", file=uvprojx.name,
|
|
760
|
-
reason="受控 .uvoptx 的 TargetName 与工程名同步"))
|
|
777
|
+
data = self._render_uvoptx(r, uvprojx, uvoptx_src, pack, stem, deviations)
|
|
761
778
|
target = proj_dir / f"{stem}.uvoptx"
|
|
762
779
|
target.write_bytes(data)
|
|
763
780
|
manifest_files.append(dict(
|
|
@@ -884,6 +901,23 @@ class Materializer:
|
|
|
884
901
|
if f"<{tag}>" in text:
|
|
885
902
|
text = rewrite_tag_value(text, tag, stem, what)
|
|
886
903
|
edits.append(f"{tag} -> {stem}")
|
|
904
|
+
# e2) 声明式 uvprojx 选项编辑(如 M4 统一 -O2;命中数必须精确)
|
|
905
|
+
for e in cfg.get("project_edits", []):
|
|
906
|
+
tag = e["tag"]
|
|
907
|
+
old = f"<{tag}>{e['old_value']}</{tag}>"
|
|
908
|
+
new = f"<{tag}>{e['new_value']}</{tag}>"
|
|
909
|
+
hits = text.count(old)
|
|
910
|
+
if hits != e["expected_matches"]:
|
|
911
|
+
raise ContractError(
|
|
912
|
+
RECIPE_MISMATCH,
|
|
913
|
+
f"{what}: <{tag}>{e['old_value']}</{tag}> 预期命中 "
|
|
914
|
+
f"{e['expected_matches']} 次,实际 {hits}")
|
|
915
|
+
text = text.replace(old, new)
|
|
916
|
+
edits.append(f"{tag} {e['old_value']} -> {e['new_value']}")
|
|
917
|
+
deviations.append(dict(
|
|
918
|
+
kind="project-option-edit", file=f"{stem}.uvprojx", tag=tag,
|
|
919
|
+
old=e["old_value"], new=e["new_value"],
|
|
920
|
+
reason=e.get("reason") or "声明式项目选项编辑"))
|
|
887
921
|
# f) PackID -> recipe 锁定版本(声明式)
|
|
888
922
|
pack_id = r.recipe["pack"]["id"]
|
|
889
923
|
if f"<PackID>{pack_id}</PackID>" not in text and "<PackID>" in text:
|
|
@@ -918,6 +952,92 @@ class Materializer:
|
|
|
918
952
|
f"{r.series}-controlled.uvoptx"
|
|
919
953
|
return asset if asset.is_file() else None
|
|
920
954
|
|
|
955
|
+
def _render_uvoptx(self, r: Resolved, uvprojx: Path, src: Path, pack: dict,
|
|
956
|
+
stem: str, deviations: list[dict]) -> bytes:
|
|
957
|
+
"""把受控 .uvoptx 渲染成该 config 的调试侧车。
|
|
958
|
+
|
|
959
|
+
受控资产只保留骨架与占位符;芯片相关值全部来自**已裁决的 recipe.memory +
|
|
960
|
+
随包 DFP 的 Flash 算法**,不按型号猜:
|
|
961
|
+
- device / FLM:`pack_device_algorithm()`(M4 同子系列不同容量 FLM 不同);
|
|
962
|
+
- Flash 基址/容量:`memory.flash[0]`;
|
|
963
|
+
- SW-DP ID:M4=2BA01477,M0+/M3=0BC11477;
|
|
964
|
+
- ListingPath:与 uvprojx 同步(uVision 以 uvoptx 为准);
|
|
965
|
+
- CMSIS_AGDI 注册项含 `-FO15`(Reset and Run),与 M0+ 对齐。
|
|
966
|
+
"""
|
|
967
|
+
cfg = r.config
|
|
968
|
+
what = cfg["config_id"]
|
|
969
|
+
device = cfg["device"]
|
|
970
|
+
flash = cfg["memory"]["flash"][0]
|
|
971
|
+
flash_base = int(flash["base"], 16)
|
|
972
|
+
flash_size = int(flash["size"], 16)
|
|
973
|
+
uv_text = uvprojx.read_text(encoding="utf-8", errors="replace")
|
|
974
|
+
m = re.search(r"<ListingPath>([^<]*)</ListingPath>", uv_text)
|
|
975
|
+
if not m:
|
|
976
|
+
raise ContractError(RECIPE_MISMATCH, f"{what}: uvprojx 缺 <ListingPath>")
|
|
977
|
+
listing = m.group(1)
|
|
978
|
+
m4 = r.series in ("ht32f490x1", "ht32f491x3", "ht32f493x5")
|
|
979
|
+
ram_alg = "1000"
|
|
980
|
+
if m4:
|
|
981
|
+
alg = pack_device_algorithm(pack["asset"], device)
|
|
982
|
+
if not alg:
|
|
983
|
+
raise ContractError(
|
|
984
|
+
RECIPE_MISMATCH,
|
|
985
|
+
f"{what}: 随包 DFP 未给出 {device} 的 Flash 算法(pdsc 无 algorithm)")
|
|
986
|
+
flm_path = alg.replace("/", "\\")
|
|
987
|
+
flm_base = Path(alg).stem
|
|
988
|
+
cmsis = (
|
|
989
|
+
'-X"Any" -UAny -O206 -S0 -C0 -P00 -N00("ARM CoreSight SW-DP") '
|
|
990
|
+
'-D00(2BA01477) -L00(0) -TO18 -TC10000000 -TP20 -TDS8007 -TDT0 -TDC1F '
|
|
991
|
+
'-TIEFFFFFFFF -TIP8 -FO15 -FD20000000 '
|
|
992
|
+
f"-FC{ram_alg} -FN1 -FF0{flm_base}.FLM "
|
|
993
|
+
f"-FS0{flash_base:X} -FL0{flash_size:X} "
|
|
994
|
+
f"-FP0($$Device:{device}${flm_path})")
|
|
995
|
+
ul2 = (
|
|
996
|
+
f"UL2CM3(-S0 -C0 -P0 -FD20000000 -FC{ram_alg} -FN1 -FF0{flm_base} "
|
|
997
|
+
f"-FS0{flash_base:X} -FL0{flash_size:X} "
|
|
998
|
+
f"-FP0($$Device:{device}${flm_path}))")
|
|
999
|
+
else:
|
|
1000
|
+
cmsis = (
|
|
1001
|
+
'-X"Any" -UAny -O206 -S0 -C0 -P00 -N00("ARM CoreSight SW-DP") '
|
|
1002
|
+
'-D00(0BC11477) -L00(0) -TO18 -TC1000000 -TP20 -TDS8007 -TDT0 -TDC1F '
|
|
1003
|
+
'-TIEFFFFFFFF -TIP8 -FO15 -FD20000000 '
|
|
1004
|
+
f"-FC{ram_alg} -FN2 -FF0HT32F.FLM "
|
|
1005
|
+
f"-FS0{flash_base:X} -FL0{flash_size:X} "
|
|
1006
|
+
f"-FP0($$Device:{device}$ARM\\Flash\\HT32F.FLM) "
|
|
1007
|
+
"-FF1HT32F_OPT.FLM -FS11FF00000 -FL11000 "
|
|
1008
|
+
f"-FP1($$Device:{device}$ARM\\Flash\\HT32F_OPT.FLM)")
|
|
1009
|
+
ul2 = (
|
|
1010
|
+
f"UL2CM3(-S0 -C0 -P0 -FD20000000 -FC{ram_alg} -FN2 -FF0HT32F "
|
|
1011
|
+
f"-FS0{flash_base:X} -FL0{flash_size:X} "
|
|
1012
|
+
"-FF1HT32F_OPT -FS11FF00000 -FL11000 "
|
|
1013
|
+
f"-FP0($$Device:{device}$ARM\\Flash\\HT32F.FLM) "
|
|
1014
|
+
f"-FP1($$Device:{device}$ARM\\Flash\\HT32F_OPT.FLM))")
|
|
1015
|
+
data = src.read_bytes()
|
|
1016
|
+
edits: list[str] = []
|
|
1017
|
+
for token, value in (
|
|
1018
|
+
(b"__HT32_LISTING__", listing),
|
|
1019
|
+
(b"__HT32_CMSIS_AGDI_NAME__", cmsis),
|
|
1020
|
+
(b"__HT32_UL2CM3_NAME__", ul2)):
|
|
1021
|
+
hits = data.count(token)
|
|
1022
|
+
if hits != 1:
|
|
1023
|
+
raise ContractError(
|
|
1024
|
+
RECIPE_MISMATCH,
|
|
1025
|
+
f"{what}: 受控 uvoptx 占位符 {token.decode()} 预期 1 次,实际 {hits}")
|
|
1026
|
+
data = data.replace(token, value.encode())
|
|
1027
|
+
edits.append(f"{token.decode()} -> {value[:60]}...")
|
|
1028
|
+
data, n = re.subn(rb"(<TargetName>)[^<]*(</TargetName>)",
|
|
1029
|
+
lambda mm: mm.group(1) + stem.encode() + mm.group(2),
|
|
1030
|
+
data, count=1)
|
|
1031
|
+
if n == 1:
|
|
1032
|
+
edits.append(f"TargetName -> {stem}")
|
|
1033
|
+
if b"__HT32_" in data:
|
|
1034
|
+
raise ContractError(RECIPE_MISMATCH, f"{what}: uvoptx 仍有未替换占位符")
|
|
1035
|
+
deviations.append(dict(
|
|
1036
|
+
kind="uvoptx-materialized", file=f"{stem}.uvoptx", edits=edits,
|
|
1037
|
+
reason="受控 uvoptx 按 device/FLM/memory/uvprojx ListingPath 参数化"
|
|
1038
|
+
"(含 CMSIS_AGDI -FO15 = Reset and Run)"))
|
|
1039
|
+
return data
|
|
1040
|
+
|
|
921
1041
|
# ------------------------------------------------------------ manifest
|
|
922
1042
|
def _write_manifests(self, r: Resolved, mdir: Path, stem: str, files: list[dict],
|
|
923
1043
|
deviations: list[dict], notes: list[str], total_bytes: int,
|
|
@@ -98,7 +98,8 @@ M0+(HT32F5 / L5)、M3(HT32F1 / F175x-275x)、M4(F490x1 / F491x3 / F493
|
|
|
98
98
|
- 知识库缺了用户需要的内容,你只能用推测回答
|
|
99
99
|
- 导出工程有缺陷(编译失败、型号参数错、缺文件)
|
|
100
100
|
- **ht32-skills(编排层:流程/脚本/模板/环境检测)本身有缺陷**,用 issue_type=skill_defect
|
|
101
|
-
上报(本渠道是 skill
|
|
101
|
+
上报(本渠道是 skill 问题的统一入口),`skill_version` 传本机 `ht32.py version` 的版本号,
|
|
102
|
+
details 里附现象与日志路径
|
|
102
103
|
务必附 model_id;submit_feedback 之外的调用已由服务端自动记入用量日志,无需你重复上报。
|
|
103
104
|
"""
|
|
104
105
|
|
|
@@ -561,6 +562,7 @@ def submit_feedback(
|
|
|
561
562
|
details: str = "",
|
|
562
563
|
model_id: str = "",
|
|
563
564
|
tools_called: list[str] | None = None,
|
|
565
|
+
skill_version: str = "",
|
|
564
566
|
) -> str:
|
|
565
567
|
"""提交纠正反馈,改进 HT32 代码知识库 / ht32-skills 编排层。
|
|
566
568
|
|
|
@@ -576,9 +578,12 @@ def submit_feedback(
|
|
|
576
578
|
issue_type: 问题类型,枚举上述其一(skill_defect = ht32-skills 编排层缺陷)
|
|
577
579
|
answer: 你给出的回答原文
|
|
578
580
|
correction: 你认为正确的信息或用户指出的正确内容
|
|
579
|
-
details:
|
|
581
|
+
details: 补充上下文(如编译报错原文、复现步骤、日志路径)
|
|
580
582
|
model_id: 你的模型标识,如 deepseek-v4-flash
|
|
581
583
|
tools_called: 本次调过的查询工具列表(不含 submit_feedback 自身)
|
|
584
|
+
skill_version: 本机 ht32-skills 版本,取 `ht32.py version` 的 skill_version
|
|
585
|
+
(如 "v0.1.19")。服务端跨进程拿不到客户端 skill 版本,**skill_defect 必填**,
|
|
586
|
+
否则无从判断缺陷属于哪个版本
|
|
582
587
|
"""
|
|
583
588
|
if not question or not question.strip():
|
|
584
589
|
return "错误:question 参数不能为空,请提供用户原始需求。"
|
|
@@ -595,6 +600,7 @@ def submit_feedback(
|
|
|
595
600
|
tools_called=tools_called,
|
|
596
601
|
mcp_version=_MCP_VERSION,
|
|
597
602
|
kb_version=index.data.get("code_version", ""),
|
|
603
|
+
skill_version=skill_version,
|
|
598
604
|
)
|
|
599
605
|
if path is None:
|
|
600
606
|
return ("错误:反馈未能写入。服务端可能未配置 FEEDBACK_ROOT,"
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
- **tool_call**(`UsageMiddleware`,服务端自动):拦截所有工具调用,
|
|
10
10
|
记录工具名、参数原文、耗时、成败。不依赖 Agent 配合,数据完整。
|
|
11
11
|
- **feedback**(`record_feedback`,由各 server 的 `submit_feedback` 工具调用):
|
|
12
|
-
记录 Agent 主动上报的语义性问题,middleware
|
|
12
|
+
记录 Agent 主动上报的语义性问题,middleware 无法判断这些。其中 `skill_version`
|
|
13
|
+
由 Agent 自报(服务端跨进程拿不到客户端 skill 版本),用于定位 ht32-skills 缺陷版本。
|
|
13
14
|
|
|
14
15
|
目录布局(同根 + 服务子目录):
|
|
15
16
|
|
|
@@ -38,7 +39,7 @@ from typing import Any
|
|
|
38
39
|
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
|
39
40
|
|
|
40
41
|
# 实现版本号:两仓库副本必须一致(各仓测试会校验,防止只改一边造成漂移)
|
|
41
|
-
_IMPL_VERSION = "1.
|
|
42
|
+
_IMPL_VERSION = "1.2.0"
|
|
42
43
|
|
|
43
44
|
# 服务名 → 子目录名。新增服务在此登记。
|
|
44
45
|
SERVICE_WIKI = "wiki"
|
|
@@ -203,6 +204,7 @@ def record_feedback(
|
|
|
203
204
|
tools_called: list[str] | None = None,
|
|
204
205
|
mcp_version: str = "",
|
|
205
206
|
kb_version: str = "",
|
|
207
|
+
skill_version: str = "",
|
|
206
208
|
) -> Path | None:
|
|
207
209
|
"""记录一条 Agent 主动提交的反馈。"""
|
|
208
210
|
record = {
|
|
@@ -219,5 +221,6 @@ def record_feedback(
|
|
|
219
221
|
"tools_called": [t for t in (tools_called or []) if t and t != "submit_feedback"],
|
|
220
222
|
"mcp_version": mcp_version,
|
|
221
223
|
"kb_version": kb_version,
|
|
224
|
+
"skill_version": skill_version.strip(),
|
|
222
225
|
}
|
|
223
226
|
return write_record(log_dir, FEEDBACK_PREFIX, record)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""回归测试:retarget 串口声明(uart_pin_claims)的文案与真实头文件/宏一致。
|
|
2
|
+
|
|
3
|
+
两个已实测的缺陷(都在 build_recipes.py 的 com1_claim() 里把宏名直接拼字符串):
|
|
4
|
+
1. 版级头文件名:README 写 `per utilities/HT32_Board/ht32f50241-sk.h`,实际文件是
|
|
5
|
+
`ht32f50241_sk.h`(板名 id 用连字符、头文件名用下划线)。从 recipe 数据侧兜底:
|
|
6
|
+
每条 claim 里的头文件名都必须能在官方包 `utilities/HT32_Board/` 下找到。
|
|
7
|
+
2. 引脚宏粘连:README 写 `TX=GPIO_PAAFIO_PIN_4`,实际是「端口宏 GPIO_PA + 引脚宏
|
|
8
|
+
AFIO_PIN_4」两个独立宏,应为 `TX=GPIO_PA/AFIO_PIN_4`。
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
REPO = Path(__file__).resolve().parents[3] # tests -> mcp-server -> .work -> ht32-code
|
|
17
|
+
RECIPES = REPO / ".work" / "template-recipes" / "recipes"
|
|
18
|
+
CLAIM_HEADER = re.compile(r"utilities/HT32_Board/([^)\s]+\.h)")
|
|
19
|
+
# 引脚 = 端口宏 GPIO_Px + 引脚宏 AFIO_PIN_n,中间必须有一个 '/'
|
|
20
|
+
CLAIM_PIN = re.compile(r"GPIO_P[A-H]?/AFIO_PIN_\d+")
|
|
21
|
+
CLAIM_PIN_VALUE = re.compile(r"(?:TX|RX)=([^\s,)]+)")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _recipes() -> list[dict]:
|
|
25
|
+
return [json.loads(p.read_text(encoding="utf-8")) for p in sorted(RECIPES.glob("*.json"))]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_claim_board_headers_exist_in_official_package():
|
|
29
|
+
missing: list[str] = []
|
|
30
|
+
checked = 0
|
|
31
|
+
for recipe in _recipes():
|
|
32
|
+
for cfg in recipe["configs"]:
|
|
33
|
+
for claim in (cfg.get("retarget") or {}).get("uart_pin_claims") or []:
|
|
34
|
+
m = CLAIM_HEADER.search(claim)
|
|
35
|
+
if not m:
|
|
36
|
+
continue
|
|
37
|
+
checked += 1
|
|
38
|
+
header = REPO / "raw" / cfg["origin"]["package"] / "utilities" / "HT32_Board" / m.group(1)
|
|
39
|
+
if not header.is_file():
|
|
40
|
+
missing.append(f"{cfg['config_id']}: {m.group(1)}")
|
|
41
|
+
assert checked > 0, "recipe 里没有可校验的板级头文件声明"
|
|
42
|
+
assert not missing, "claim 引用了不存在的板级头文件:\n" + "\n".join(missing)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_claim_board_headers_use_real_file_names_not_board_ids():
|
|
46
|
+
"""板名 id(连字符)不能直接当文件名;下划线才是官方文件名的分隔符。"""
|
|
47
|
+
wrong: list[str] = []
|
|
48
|
+
for recipe in _recipes():
|
|
49
|
+
for cfg in recipe["configs"]:
|
|
50
|
+
for claim in (cfg.get("retarget") or {}).get("uart_pin_claims") or []:
|
|
51
|
+
m = CLAIM_HEADER.search(claim)
|
|
52
|
+
if m and ("-sk.h" in m.group(1) or "-dvb.h" in m.group(1)):
|
|
53
|
+
wrong.append(f"{cfg['config_id']}: {m.group(1)}")
|
|
54
|
+
assert not wrong, "claim 头文件名仍用板名 id 的连字符:\n" + "\n".join(wrong)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_claim_pins_use_port_macro_slash_pin_macro():
|
|
58
|
+
"""7 份 recipe 的 TX=/RX= 引脚都必须是 GPIO_Px/AFIO_PIN_n 形态。
|
|
59
|
+
|
|
60
|
+
端口宏(GPIO_PA)与引脚宏(AFIO_PIN_4)是两个独立宏;拼成 GPIO_PAAFIO_PIN_4
|
|
61
|
+
会被用户当成宏名照抄,编译不过。
|
|
62
|
+
"""
|
|
63
|
+
bad: list[str] = []
|
|
64
|
+
checked = 0
|
|
65
|
+
for recipe in _recipes():
|
|
66
|
+
for cfg in recipe["configs"]:
|
|
67
|
+
for claim in (cfg.get("retarget") or {}).get("uart_pin_claims") or []:
|
|
68
|
+
for m in CLAIM_PIN_VALUE.finditer(claim):
|
|
69
|
+
checked += 1
|
|
70
|
+
if not CLAIM_PIN.fullmatch(m.group(1)):
|
|
71
|
+
bad.append(f"{cfg['config_id']}: {m.group(0)} <- {claim}")
|
|
72
|
+
assert checked >= 80, f"recipe 里可校验的 TX/RX 引脚过少({checked}),疑似数据缺失"
|
|
73
|
+
assert not bad, ("claim 引脚宏形态不对(应为 GPIO_Px/AFIO_PIN_n,不能写成 "
|
|
74
|
+
"GPIO_PAAFIO_PIN_n):\n" + "\n".join(bad))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_exported_readme_points_at_real_board_header(tmp_path):
|
|
78
|
+
"""导出工程 README 的串口占用应指向真实板级头文件(下划线)、引脚宏用 '/' 分隔。"""
|
|
79
|
+
from ht32_code_mcp import server as srv
|
|
80
|
+
|
|
81
|
+
out = tmp_path / "bundle_f50241"
|
|
82
|
+
text = srv.export_project_bundle("HT32F50241", str(out))
|
|
83
|
+
assert "导出失败" not in text, text
|
|
84
|
+
readme = (out / "README.md").read_text(encoding="utf-8")
|
|
85
|
+
assert "ht32f50241_sk.h" in readme, readme
|
|
86
|
+
assert "ht32f50241-sk.h" not in readme, readme
|
|
87
|
+
# README「占用」行必须给出 GPIO_PA/AFIO_PIN_4(板级头:TX=PA4 / RX=PA5)
|
|
88
|
+
claim_line = next((ln for ln in readme.splitlines() if "占用" in ln), "")
|
|
89
|
+
assert "TX=GPIO_PA/AFIO_PIN_4" in claim_line, claim_line
|
|
90
|
+
assert "RX=GPIO_PA/AFIO_PIN_5" in claim_line, claim_line
|
|
91
|
+
assert "GPIO_PAAFIO" not in readme, readme
|
|
92
|
+
|
|
@@ -478,6 +478,55 @@ def test_export_target_name_custom(tmp_path):
|
|
|
478
478
|
assert (out / "README.md").read_text(encoding="utf-8").startswith("# F52352_UART_Printf")
|
|
479
479
|
|
|
480
480
|
|
|
481
|
+
@pytest.mark.parametrize("config_id,device,flm", [
|
|
482
|
+
("ht32f49041-20tssop.ht32f49041-sk.ac5.hick-safe.retarget",
|
|
483
|
+
"HT32F49041_20TSSOP", "HT32F490x1_64"),
|
|
484
|
+
("ht32f49153-48lqfp.ht32f49163-sk.ac5.hick-safe.retarget",
|
|
485
|
+
"HT32F49153_48LQFP", "HT32F491x3_128"),
|
|
486
|
+
("ht32f49365-64lqfp.ht32f49395-sk.ac5.hick-safe.retarget",
|
|
487
|
+
"HT32F49365_64LQFP", "HT32F493x5_256"),
|
|
488
|
+
])
|
|
489
|
+
def test_export_m4_debug_options_and_optimization(tmp_path, config_id, device, flm):
|
|
490
|
+
"""M4:-O2、CMSIS_AGDI(-FO15)、FLM/device 按芯片参数化、ListingPath 同步。"""
|
|
491
|
+
out = tmp_path / config_id.split(".")[0]
|
|
492
|
+
srv.exporter.export_dir(out_dir=str(out), project_name="verify_proj", config_id=config_id)
|
|
493
|
+
uv = next((out / "project" / "MDK_ARMv5").glob("*.uvprojx"))
|
|
494
|
+
text = uv.read_text(encoding="utf-8")
|
|
495
|
+
assert "<Optim>3</Optim>" in text
|
|
496
|
+
listing = re.search(r"<ListingPath>([^<]*)</ListingPath>", text).group(1)
|
|
497
|
+
side = uv.with_suffix(".uvoptx")
|
|
498
|
+
side_text = side.read_text(encoding="utf-8")
|
|
499
|
+
assert "<Key>CMSIS_AGDI</Key>" in side_text
|
|
500
|
+
assert "<Key>UL2CM3</Key>" in side_text
|
|
501
|
+
assert "-FO15" in side_text
|
|
502
|
+
assert re.search(r"<ListingPath>" + re.escape(listing) + "</ListingPath>", side_text)
|
|
503
|
+
assert f"$$Device:{device}$" in side_text
|
|
504
|
+
assert flm in side_text
|
|
505
|
+
dev = json.loads((out / "manifest" / "deviations.json").read_text(encoding="utf-8"))
|
|
506
|
+
kinds = [d["kind"] for d in dev["deviations"]]
|
|
507
|
+
assert "project-option-edit" in kinds
|
|
508
|
+
assert "uvoptx-materialized" in kinds
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def test_export_m0_debug_sidecar_is_parameterized(tmp_path):
|
|
512
|
+
"""M0+:uvoptx 的 device/Flash 容量与 uvprojx ListingPath 同步(不再固定 F52352)。"""
|
|
513
|
+
out = tmp_path / "f50030"
|
|
514
|
+
srv.exporter.export_dir(
|
|
515
|
+
out_dir=str(out), project_name="verify_proj",
|
|
516
|
+
config_id="ht32f50030.ht32f50030-sk.ac5.hsi-safe.retarget")
|
|
517
|
+
uv = next((out / "project" / "MDK_ARMv5").glob("*.uvprojx"))
|
|
518
|
+
text = uv.read_text(encoding="utf-8")
|
|
519
|
+
listing = re.search(r"<ListingPath>([^<]*)</ListingPath>", text).group(1)
|
|
520
|
+
output_dir = re.search(r"<OutputDirectory>([^<]*)</OutputDirectory>", text).group(1)
|
|
521
|
+
# obj 与 list 是同一芯片目录下的两个并列子目录;list 不得再落回工程根 .\List\
|
|
522
|
+
assert "HT32\\50030\\Obj\\" in output_dir
|
|
523
|
+
assert "HT32\\50030\\List\\" in listing
|
|
524
|
+
side_text = uv.with_suffix(".uvoptx").read_text(encoding="utf-8")
|
|
525
|
+
assert re.search(r"<ListingPath>" + re.escape(listing) + "</ListingPath>", side_text)
|
|
526
|
+
assert "$$Device:HT32F50030$" in side_text
|
|
527
|
+
assert "$$Device:HT32F52352$" not in side_text
|
|
528
|
+
|
|
529
|
+
|
|
481
530
|
def test_export_unknown_chip(tmp_path):
|
|
482
531
|
result = srv.export_project_bundle("HT32F99999", str(tmp_path / "nope"))
|
|
483
532
|
assert "UNKNOWN_DEVICE" in result and "不在契约" in result
|
|
@@ -628,7 +677,7 @@ def test_version_reports_skill_latest(monkeypatch):
|
|
|
628
677
|
def test_usage_impl_version_pinned():
|
|
629
678
|
"""两仓库的 usage.py 是逐字节相同副本,实现版本号用于防止只改一边。"""
|
|
630
679
|
from ht32_code_mcp import usage
|
|
631
|
-
assert usage._IMPL_VERSION == "1.
|
|
680
|
+
assert usage._IMPL_VERSION == "1.2.0"
|
|
632
681
|
assert usage.SERVICE_CODE == "code"
|
|
633
682
|
assert usage.SERVICE_WIKI == "wiki"
|
|
634
683
|
# 噪声参数含 out_dir(code 特有:可能含用户本地路径)
|
|
@@ -686,6 +735,7 @@ def test_submit_feedback_writes_jsonl(tmp_path, monkeypatch):
|
|
|
686
735
|
details="编译报错 too few arguments",
|
|
687
736
|
model_id="deepseek-v4-flash",
|
|
688
737
|
tools_called=["search_code_knowledge", "submit_feedback", "get_file"],
|
|
738
|
+
skill_version="v0.1.19",
|
|
689
739
|
)
|
|
690
740
|
assert "反馈已记录" in out
|
|
691
741
|
f = next((tmp_path / "logs").glob("feedback_*.jsonl"))
|
|
@@ -698,6 +748,8 @@ def test_submit_feedback_writes_jsonl(tmp_path, monkeypatch):
|
|
|
698
748
|
# submit_feedback 自身被过滤,只留查询类工具
|
|
699
749
|
assert rec["tools_called"] == ["search_code_knowledge", "get_file"]
|
|
700
750
|
assert rec["mcp_version"] and rec["kb_version"]
|
|
751
|
+
# 客户端 skill 版本是结构化字段(不再靠 details 散文),未传时为空串而非缺键
|
|
752
|
+
assert rec["skill_version"] == "v0.1.19"
|
|
701
753
|
|
|
702
754
|
|
|
703
755
|
def test_submit_feedback_accepts_skill_defect(tmp_path, monkeypatch):
|
|
@@ -708,11 +760,22 @@ def test_submit_feedback_accepts_skill_defect(tmp_path, monkeypatch):
|
|
|
708
760
|
issue_type="skill_defect",
|
|
709
761
|
details="ht32-skills v0.1.0;导出后 uvprojx 未按工程名重命名",
|
|
710
762
|
model_id="deepseek-v4-flash",
|
|
763
|
+
skill_version="v0.1.0",
|
|
711
764
|
)
|
|
712
765
|
assert "skill_defect" in out
|
|
713
766
|
f = next((tmp_path / "logs").glob("feedback_*.jsonl"))
|
|
714
767
|
rec = json.loads(f.read_text(encoding="utf-8").splitlines()[0])
|
|
715
768
|
assert rec["issue_type"] == "skill_defect"
|
|
769
|
+
assert rec["skill_version"] == "v0.1.0"
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def test_feedback_skill_version_key_always_present(tmp_path, monkeypatch):
|
|
773
|
+
"""没传 skill_version 时字段仍存在(空串),日志消费方不必分支判键。"""
|
|
774
|
+
monkeypatch.setattr(srv, "LOG_DIR", str(tmp_path / "logs"))
|
|
775
|
+
srv.submit_feedback(question="x", issue_type="other")
|
|
776
|
+
f = next((tmp_path / "logs").glob("feedback_*.jsonl"))
|
|
777
|
+
rec = json.loads(f.read_text(encoding="utf-8").splitlines()[0])
|
|
778
|
+
assert rec["skill_version"] == ""
|
|
716
779
|
|
|
717
780
|
|
|
718
781
|
def test_submit_feedback_empty_question(tmp_path, monkeypatch):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|