Skip to content

Commit 8bddd50

Browse files
authored
Merge pull request #235 from JiusiServe/feat/164-ut-coverage
Ask for a unit test when a PR adds a public function no test names
2 parents 7123e10 + 952a886 commit 8bddd50

14 files changed

Lines changed: 736 additions & 5 deletions

File tree

‎adapters/vllm_omni/manifest.yaml‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,3 +423,11 @@ knowledge_lifecycle:
423423
protected_rules: []
424424
calibration_set: kb-calibration # adapters/vllm_omni/kb-calibration/cases/*.json
425425
model_dir: repos/vllm-omni/models
426+
427+
# Unit-test coverage candidates for new public functions (#164). Sources are
428+
# the package; tests live under tests/. Entry points and generated/registry
429+
# files are skipped: their coverage is end-to-end by design.
430+
ut_coverage:
431+
source_roots: [vllm_omni/]
432+
test_globs: ["tests/*", "*/test_*.py", "*/conftest.py"]
433+
exclude: ["*/__init__.py", "vllm_omni/entrypoints/cli/*", "vllm_omni/version.py"]

‎doc/architecture/SPEC/direct_routing.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# direct_routing.py —— 规范
22

3-
<!-- verified-against: 2026-09-28 -->
3+
<!-- verified-against: 2026-09-29 -->
44

55
`LOC ~880 · Direct 模式完整策略包与仓库中立的知识路由 · refactor-status: stable`
66

@@ -47,6 +47,10 @@ Direct policy bundle)、`direct_knowledge_routes`、
4747
`evidence_head_sha` 7–40 位十六进制、`existing_feedback_status` 枚举、
4848
`finding_dispositions` 的 anchor/disposition/existing_thread/
4949
head_recheck 约束。
50+
- **未测公开函数候选(#164)**:`direct_review_plan(..., diff="")` 有 diff 时带
51+
`untested_public_api`(`status=ok` + 候选 + 评审规则),只基于 diff 自身的测试
52+
文件——provider 没有 PR head 的 checkout,树内测试搜索由 agent 按 checklist 完成;
53+
无 diff 为 `{"status": "no_diff"}`,adapter 关闭时为 `disabled`。
5054
- 叶子模块:**绝不** import 任何 server 模块
5155
(`test_contract.py::test_direct_routing_does_not_import_a_server_module`)。
5256

‎doc/architecture/SPEC/engine/steps/review.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ agent、评审结果的有界精炼、评测调优过的 prompt 数据和确定
5454
要做的改动、`evidence` 引用该 `[resolved]` 行);覆盖补充(coverage editor)
5555
的输入也剔除 `[resolved]` 行。此前按关键词从散文里“提拔”残留,会把明确
5656
写着不再追究的行发成 P2(#155)。
57+
- **未测公开函数(#164)**:evidence 带 `untested_public_api`——`ut_coverage.analyze`
58+
在 PR-time worktree 上找出无测试引用的新增公开函数,由 lens 判断间接覆盖/trivial/
59+
真缺口;评审者把缺测试评论标为 `kind: untested_api`;finalize 之后 `cap_gap_comments`
60+
只对这类评论至多保留 3 条,其余名字追加到 summary,其他发现不受影响。
5761
- **`review_verdict` 是发布的 state 字段,不是散文**:由
5862
`_review_verdict(review_comments, pr_state)` 计算(**与渲染器同一个
5963
helper,绝不第二份校准规则**),随 `review_text`/`review_summary`/

‎doc/architecture/SPEC/sdk.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# sdk/ —— 规范
22

3-
<!-- verified-against: 2026-09-28 -->
3+
<!-- verified-against: 2026-09-29 -->
44

55

66
`Python SDK v1 · 跨仓库唯一 typed 边界 · refactor-status: ok`
@@ -133,3 +133,7 @@ Direct 1.1 / Strict 1.3 accept typed `CarriedFinding` inputs (unique IDs, source
133133
`DirectReviewPlan` 新增 `knowledge_snapshot`(激活快照 ID、`packaged` 或 `unverified`,从不暴露开发树路径)
134134
与 `knowledge_tree_sha256`(已验证快照的清单树哈希,否则为空)。同一 `review_context_id` 下的文档读取都来自这份
135135
快照,RB 应随每次 review 记录这两个值。
136+
137+
## 2026-09-29 未测公开函数候选
138+
`DirectReviewRequest` 新增可选 `diff`(冻结 head 的 unified diff),`DirectReviewPlan` 新增
139+
`untested_public_api`(`direct_routing` 的同名块,默认空字典)。两者都是可选增量字段,旧调用方不受影响(#164)。

‎doc/architecture/SPEC/thin_mcp_server.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# thin_mcp_server.py —— 规范
22

3-
<!-- verified-against: 2026-09-28 -->
3+
<!-- verified-against: 2026-09-29 -->
44

55
`LOC ~490 · 默认 MCP:Direct 门面 + Strict 入口 · refactor-status: ok`
66

@@ -26,6 +26,8 @@
2626
委托 —— 没有任何东西从那两个模块向上 import 回 server。下面关于
2727
quick-map fail-closed、路由不静默替换、仓库守卫先跑的不变量**仍然为真**,
2828
但其实现体在 `direct_routing.py`(规范见其页)。
29+
- `review` 的可选 `diff` 只对 Direct 生效:原样交给 `direct_review_plan`,
30+
由其生成 `untested_public_api` 候选(#164);仍然零模型。
2931
- **Strict 分支透传快照绑定**:`_strict_review_request` 把
3032
`expected_head_sha`、`repo_path`、`idempotency_key` 一并送进内部请求;
3133
`review()` 的 Strict 路径按 `strict_readiness(repo, repo_path)`(两参,
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# ut_coverage.py —— 规范
2+
3+
<!-- verified-against: 2026-09-29 -->
4+
5+
`LOC ~290 · 新增公开函数的单测覆盖候选(#164) · refactor-status: ok`
6+
7+
## 职责
8+
两段式检查的**确定性一半**:从 PR diff 找出新增的公开函数/方法,再看 diff 中的
9+
测试文件、以及(有 checkout 时)被评审树的测试文件是否按名字引用它们。没有任何
10+
测试引用的名字只是**候选**,从不直接判定为“缺测试”——由评审者(Strict lens 或
11+
Direct agent)逐个判断:经调用方被间接覆盖、trivial,还是真缺口。
12+
13+
## 公开契约
14+
`UTCoverageRules`(`from_manifest(manifest)`:读 adapter 的 `ut_coverage:`
15+
段——`source_roots`、`test_globs`、`exclude`、`enabled`;非 Python 仓库或
16+
`enabled: false` 返回 `None`);`PublicDef`;`CoverageReport`;
17+
`new_public_defs(diff, rules)`;`analyze(diff, rules, repo=None)`;
18+
`render(report)`(Strict evidence 文本,无候选时为空);
19+
`cap_gap_comments(comments, report)`;`REVIEWER_INSTRUCTIONS`;
20+
`MAX_CANDIDATES=15`;`MAX_COMMENTS=3`。
21+
22+
## 不变量
23+
- **只提名,不裁决**:模块不产生评论;评论只来自评审者对候选的判断。
24+
- 新增 = diff 中 `+def` 且同一**限定名**(`类.方法` 或模块函数名)没有在 diff
25+
任何位置被删除(移动、改签名不算新 API);另一个类的同名方法不受影响。跳过:`_` 开头与 dunder 名、嵌套函数、`@overload`/
26+
`@abstractmethod` 桩、测试文件、`exclude` 命中的文件;私有类的方法跳过。
27+
- 方法判定只看同一 hunk 内可见的 `class`(上下文或新增行)或 hunk 头;看不到
28+
父作用域的缩进 def 按模块函数处理。
29+
- 树内搜索用 `git grep -l -w -F`,只计入 `test_globs` 命中的文件,逐名限时;
30+
失败按“无引用”处理(只会多提名,交给评审者判断)。
31+
- 发布上限在评审之后确定性执行,且只作用于评审者**显式分类**为
32+
`kind: untested_api` 的评论:最多保留 3 条,其余的名字进 summary 一行,不丢失。
33+
同一函数上的其他发现(不论锚点、严重度)从不被截断;`kind` 是内部字段,
34+
发布前一律移除。
35+
- 仓库中立:仓库差异全部来自 adapter 的 `ut_coverage:` 段。
36+
37+
## 边界 —— 不属于这里
38+
评审 prompt 与评论发布(`engine/steps/review/`);Direct 策略包
39+
(`direct_routing.py`);运行任何测试或覆盖率工具。
40+
41+
## 依赖(允许)
42+
标准库(`re`、`subprocess`、`fnmatch`、`dataclasses`)。
43+
44+
## 测试
45+
`test/test_ut_coverage.py`。

‎src/infermatrix_copilot/direct_routing.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
from .adapters import AdapterError, AdapterRegistry, RepoAdapter
2929
from .knowledge_view import KnowledgeView
3030
from .sdk._resources import adapters_root
31+
from .ut_coverage import UTCoverageRules, analyze as _ut_analyze
32+
from .ut_coverage import REVIEWER_INSTRUCTIONS as _UT_INSTRUCTIONS
3133

3234
_ROOT = Path(__file__).resolve().parents[2]
3335
_ADAPTERS = adapters_root()
@@ -158,6 +160,7 @@ def _validate_routes(data: object, relative: str, view: KnowledgeView) -> dict:
158160
"Classify every candidate finding as new, duplicate, extends_existing, or resolved_or_outdated. Suppress duplicates; for extensions, point to the existing thread instead of opening a parallel inline comment. Reverify resolved/outdated concerns at the pinned head and suppress them only when fixed. Use disabled only for PR_CONTEXT_MODE=no_discussion evaluation, record unavailable feedback as a validation gap, and use not_applicable only for local/worktree reviews.",
159161
"Run subtraction only when the diff adds or expands a helper, class, fallback, compatibility branch, or public behavior; otherwise mark no subtraction signal.",
160162
"When subtraction is triggered, read the mandatory simplification guide and prove consumers, trust boundaries, and lifecycle ownership before calling code dead or over-defensive.",
163+
"When the plan's untested_public_api lists candidates, judge each new public function as indirectly covered, trivial, or a real unit-test gap after reading it and searching the head's tests; when the host passed no diff, list the diff's new public functions and search the tests yourself. File at most three real gaps as minor findings on the def's file:line and name any further ones in one line.",
161164
"Report the non-test added-line count the host supplies rather than counting the diff yourself; above the stated budget, ask for a split plan or a concrete exemption on the largest contributing file, and record the reason instead when the PR body already gives one. Size is never a correctness finding and never raises another finding's severity.",
162165
"Plan exactly one consolidated final review comment.",
163166
)
@@ -586,9 +589,14 @@ def direct_review_plan(
586589
body: str = "",
587590
changed_files: list[str] | None = None,
588591
view: KnowledgeView | None = None,
592+
diff: str = "",
589593
) -> dict:
590594
"""Return the complete Direct policy bundle for one frozen review.
591595
596+
``diff`` is optional: with it, the bundle carries the new public
597+
functions no test in the diff names (#164) for the agent to judge; without
598+
it, the checklist asks the agent to find them itself.
599+
592600
This is the canonical provider operation used by both the Python SDK and
593601
the MCP adapter. Keeping the full bundle here prevents downstream hosts
594602
from reconstructing a smaller, divergent protocol out of routing helpers.
@@ -620,6 +628,7 @@ def direct_review_plan(
620628
),
621629
)
622630
budget_ms = int((time.perf_counter() - budget_started) * 1000)
631+
untested = _direct_untested_public_api(repo, diff)
623632
return {
624633
"mode": "direct",
625634
"knowledge_entry": (
@@ -648,6 +657,7 @@ def direct_review_plan(
648657
"fallback_entry": _knowledge_path("AGENTS.md", view),
649658
},
650659
"execution_budget": execution_budget,
660+
"untested_public_api": untested,
651661
"first_review_checklist": list(DIRECT_REVIEW_CHECKLIST),
652662
"progress_update": {
653663
**DIRECT_PROGRESS_UPDATE,
@@ -697,6 +707,21 @@ def direct_review_plan(
697707
}
698708

699709

710+
def _direct_untested_public_api(repo: str, diff: str) -> dict:
711+
"""Candidates from the diff alone: the provider holds no checkout of the
712+
PR head, so the head-tree test search is the agent's (see checklist)."""
713+
if not diff:
714+
return {"status": "no_diff"}
715+
adapter = _adapter_for_repo(repo)
716+
rules = UTCoverageRules.from_manifest(
717+
adapter.manifest if adapter is not None else None)
718+
if rules is None:
719+
return {"status": "disabled"}
720+
report = _ut_analyze(diff, rules)
721+
return {"status": "ok", **report.to_dict(),
722+
"instructions": _UT_INSTRUCTIONS if report.candidates else ""}
723+
724+
700725
def _direct_completion_result(
701726
subtraction_signal: str = "",
702727
subtraction: list[dict[str, str]] | None = None,

‎src/infermatrix_copilot/engine/steps/review/steps.py‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
from pathlib import Path
2020

2121
from ....review.planner import DEFAULT_STANDARD_LENSES, DEPTHS, plan_review
22+
from ....ut_coverage import UTCoverageRules, analyze as _ut_analyze
23+
from ....ut_coverage import cap_gap_comments, render as _ut_render
2224
from ...step import FailureKind, StepContext, StepResult
2325
from .._common import gh_read_tools as _gh_read_tools
2426
from .._common import repo_path as _repo_path
@@ -145,6 +147,17 @@ async def _review_diff(ctx: StepContext) -> StepResult:
145147
except OSError:
146148
continue
147149

150+
# #164: new public functions no test names — candidates for the lenses
151+
# to judge (indirect coverage, trivial, or a real gap), never findings
152+
ut_rules = UTCoverageRules.from_manifest(
153+
adapter.manifest if adapter is not None else None)
154+
ut_report = (await asyncio.to_thread(_ut_analyze, str(diff), ut_rules,
155+
_repo_path(ctx))
156+
if ut_rules is not None else None)
157+
if ut_report is not None:
158+
ctx.trace.record("review_ut_candidates", step="agent.review_diff",
159+
**ut_report.to_dict())
160+
148161
common = dict(
149162
step_name="agent.review_diff",
150163
purpose=f"Review PR #{spec.get('pr')} like an engaged maintainer: "
@@ -158,14 +171,18 @@ async def _review_diff(ctx: StepContext) -> StepResult:
158171
"gate_report": ctx.state.get("gate_report", ""),
159172
"sweep_targets": _sweep_targets(str(diff), language),
160173
"changed_symbol_consumers": await asyncio.to_thread(
161-
_consumer_sweep, _repo_path(ctx), str(diff))},
174+
_consumer_sweep, _repo_path(ctx), str(diff)),
175+
"untested_public_api": (_ut_render(ut_report)
176+
if ut_report is not None else "")},
162177
output_extension={"review_comments":
163178
"list of {file, line, anchor_snippet, severity: "
164179
"blocker|major|minor|nit, comment, evidence, "
165180
"suggestion: optional replacement code for the "
166181
"cited lines — the concrete edit, no prose, "
167182
"disposition: publish|excluded|duplicate|resolved|"
168-
"no_issue — only publish is published}"},
183+
"no_issue — only publish is published, kind: "
184+
"optional, `untested_api` only for a missing unit "
185+
"test on a listed untested-function candidate}"},
169186
extra_tools={**_gh_read_tools(_repo_path(ctx)),
170187
**review_repo_tools(_repo_path(ctx))},
171188
)
@@ -301,6 +318,12 @@ async def _review_diff(ctx: StepContext) -> StepResult:
301318
# finalized set (#141).
302319
all_candidates = list(output.get("review_comments") or [])
303320
published, withheld = finalize_review_dispositions(output)
321+
# at most three untested-function comments; the rest are named, not lost
322+
published, ut_dropped = cap_gap_comments(published, ut_report)
323+
if ut_dropped:
324+
output["summary"] = (str(output.get("summary") or "").rstrip()
325+
+ "\n\nAlso without a unit test: "
326+
+ ", ".join(f"`{n}`" for n in ut_dropped) + ".").lstrip()
304327
output["review_comments"] = published
305328
output["_withheld_findings"] = withheld
306329
comments = sorted(output.get("review_comments") or [],

‎src/infermatrix_copilot/sdk/v1/direct.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ def plan(self, request: DirectReviewRequest) -> DirectReviewPlan:
312312
body=request.body,
313313
changed_files=changed_files,
314314
view=view,
315+
diff=request.diff,
315316
)
316317
routes = tuple(
317318
KnowledgeRoute(
@@ -380,6 +381,7 @@ def plan(self, request: DirectReviewRequest) -> DirectReviewPlan:
380381
carried_findings=request.carried_findings,
381382
knowledge_snapshot=view.public_snapshot,
382383
knowledge_tree_sha256=view.tree_sha256,
384+
untested_public_api=dict(raw.get("untested_public_api") or {}),
383385
)
384386
self._remember_context(review_context_id, expected_head, carried, view)
385387
return plan

‎src/infermatrix_copilot/sdk/v1/models.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@ class DirectReviewRequest(_Serializable):
229229
body: str
230230
changed_paths: tuple[ChangedPath, ...]
231231
carried_findings: tuple[CarriedFinding, ...] = ()
232+
# optional unified diff of the frozen head; enables the provider's
233+
# untested-public-function candidates (#164)
234+
diff: str = ""
232235

233236

234237
@dataclass(frozen=True)
@@ -254,6 +257,9 @@ class DirectReviewPlan(_Serializable):
254257
# review_context_id) was served from: record it with the review
255258
knowledge_snapshot: str = ""
256259
knowledge_tree_sha256: str = ""
260+
# new public functions no test in the request's diff names (#164);
261+
# {"status": "no_diff"} when the request carried no diff
262+
untested_public_api: dict[str, Any] = field(default_factory=dict)
257263

258264
FeedbackStatus = Literal[
259265
"checked", "disabled", "unavailable", "not_applicable"

0 commit comments

Comments
 (0)