最新资讯
reddit.com OpenAI 称 Astra 仅满足 10% 安全任务标准 dev.to FreeToken:在自有 GPU 上运行前沿 MoE 模型及其内存需求 dev.to 当 AI Agent 说“完成”时,究竟完成了什么? dev.to 部署 Underdog Bark 3.0 Pro:逐字转写、自定义后端与分块设置 dev.to 为艰难对话构建 AI:五个 Agent、一条安全正则与一位盲评监督者 techcrunch.com Palo Alto Networks 洽谈以 5 亿美元收购 AI Agent 安全初创公司 Clutch theinformation.com 黄仁勋被要求就涉华间谍指控向国会作简报 www.infoq.com Edera 推出 Object Capability Bounded Authority,试图将凭证与 AI Agent 工作负载隔离 wechat 这波杀疯了!旗舰级模型不限量、1个月视频额度免费领 bloomberg.com AI 抛售蔓延至信用市场,软银领跌亚洲美元债 dev.to 如何在专用 GPU 服务器上用 OpenAI 兼容 API 部署 LLM dev.to 审计 153 份 Claude Code 与 Codex 会话记录:User-Agent 与 URL 中的邮箱泄露
多元动态
登录 注册
智能体 dev.to · @Finley Zhou

覆盖率变绿但可逆性被破坏:面向 Agent 补丁的往返校验门禁

原文 我的收藏

文章指出 AI Agent 生成的补丁可能提升行覆盖率却破坏可逆性:编码/解码各自单测通过,但 decode(encode(x)) == x 不再成立;测试侧也可能通过放宽断言、增加超时或删除 pytest.raises 来变绿。作者建议在合并时按顺序校验三项不变量:对模块内已注册的可逆函数对做 round-trip、对 normalizer/formatter 做幂等性检查、以及对同一 diff 中测试文件做断言强度变化检查(即使测试套件为绿也拒绝弱化)。不可逆函数(如哈希、HMAC、破坏性删除)应登记为单向并排除在外。文中给出 registry 代码示例,并声明这是流程建议而非实测报告,未声称通过率、延迟或模型排名。

AI推荐理由 文章提出一个针对 AI Agent 生成补丁的合并门禁流程:在合并时按顺序校验三项不变量——已注册可逆函数对的 round-trip、normalizer/formatter 的幂等性、以及同一 diff 中测试文件的断言强度变化(拒绝断言弱化)。作者明确说明这是流程建议而非实测报告,未声称通过率、延迟或模型排名,并给出可复用的 registry 代码示例。具体新信息在于把“覆盖率上升但可逆性被破坏”这一失败模式形式化为可检查的合并条件,对使用 Agent 改代码的团队有直接可操作性;局限是缺少真实项目验证数据,效果未经证实。

62

正文

Agent patches can raise line coverage while destroying invertibility. A merge bar that only asks “did pytest exit 0?” will accept a codec that no longer round-trips, a normalizer that is no longer idempotent, and a test file that passed because assertions got weaker. Gate on those three signals. Coverage is a side effect, not an oracle. This article is a procedure, not a field report. Examples are labeled. No pass-rate, latency, or model ranking is claimed. The failure class coverage does not see An agent that “fixes” a serializer often rewrites one direction of a pair. Encode still produces bytes. Decode still returns an object. Unit tests that check each direction in isolation stay green. The composition decode(encode(x)) == x does not. The same pattern shows up in formatters, schema migrations, feature-flag parsers, and cache keys. Each function looks locally correct. The inverse does not hold. Coverage still climbs because the new branch is executed once with a fixture the agent also wrote. A second, quieter path to green is test-side. Timeouts inflate. assertEqual becomes assertIsNotNone. A pytest.raises block disappears. The production diff can be small. The oracle diff is the real patch. What this gate checks Hold three invariants at merge, in this order: Round-trip on every registered invertible pair in the touched module. Idempotency on every registered normalizer or formatter in that module. Assertion-delta on the test files in the same diff: reject weakenings even when the suite is green. If a function is not invertible, do not fake a round-trip. Register it as one-way and keep it off this list. Hashing, HMAC, and destructive deletes belong there. Artifact 1: a tiny invertible registry Keep the registry in-repo, next to the code, not in chat history. The agent may propose new pairs. It must not silently drop old ones. # invariants/registry.py from __future__ import annotations from dataclasses import dataclass from typing import Any, Callable @dataclass(frozen=True) class RoundTrip: name: str forward: Callable[[Any], Any] backward: Callable[[Any], Any] samples: tuple[Any, ...] @dataclass(frozen=True) class Idempotent: name: str fn: Callable[[Any], Any] samples: tuple[Any, ...] # Example pair only. Replace with your module's real callables. def _encode_record(row: dict) -> bytes: import json return json.dumps(row, sort_keys=True, separators=(',', ':')).encode('utf-8') def _decode_record(blob: bytes) -> dict: import json return json.loads(blob.decode('utf-8')) def _normalize_path(p: str) -> str: return '/'.join(part for part in p.replace('\\', '/').split('/') if part not in ('', '.')) REGISTRY_ROUND_TRIPS: tuple[RoundTrip, ...] = ( RoundTrip( name='record_json', forward=_encode_record, backward=_decode_record, samples=( {}, {'id': 0}, {'id': 7, 'tags': ['a', 'b']}, {'name': 'n', 'nested': {'ok': True}}, ), ), ) REGISTRY_IDEMPOTENT: tuple[Idempotent, ...] = ( Idempotent( name='normalize_path', fn=_normalize_path, samples=('a/b', 'a//b/', './a/b', 'a/./b', 'a\\b'), ), ) Enter fullscreen mode Exit fullscreen mode The samples are the contract. An agent that adds a new code path without a sample has not extended the oracle. It has only extended the implementation. Artifact 2: pytest that fails the composition, not the helper # tests/test_invariants.py import copy import pytest from invariants.registry import REGISTRY_IDEMPOTENT, REGISTRY_ROUND_TRIPS @pytest.mark.parametrize('spec', REGISTRY_ROUND_TRIPS, ids=lambda s: s.name) def test_round_trip(spec): for sample in spec.samples: original = copy.deepcopy(sample) got = spec.backward(spec.forward(sample)) assert got == original, f'{spec.name} lost data on {original!r} -> {got!r}' assert sample == original, f'{spec.name} mutated the input in place' @pytest.mark.parametrize('spec', REGISTRY_IDEMPOTENT, ids=lambda s: s.name) def test_idempotent(spec): for sample in spec.samples: once = spec.fn(sample) twice = spec.fn(copy.deepcopy(once)) assert once == twice, f'{spec.name} not idempotent: {once!r} vs {twice!r}' Enter fullscreen mode Exit fullscreen mode Run it as a required check, isolated from the rest of the suite: python -m pytest tests/test_invariants.py -q --tb=short Enter fullscreen mode Exit fullscreen mode If this file is slow, the problem is the registry, not pytest. Shrink samples. Do not skip the module. Artifact 3: assertion-delta on the test diff Green tests are not evidence when the tests moved. Parse the patch. Count oracles that got weaker. The scanner below is a starting heuristic, not a complete semantics engine. Label it as such when you wire it into CI. # tools/assert_delta.py from __future__ import annotations import argparse import ast import re from pathlib import Path WEAKEN = { 'assertEqual': {'assertTrue', 'assertFalse', 'assertIsNotNone', 'assertIsNone'}, 'assertListEqual': {'assertEqual', 'assertTrue'}, 'assertDictEqual': {'assertEqual', 'assertTrue'}, 'assertRaises': set(), # disappearance is a weakening } TIMEOUT_RE = re.compile(r'timeout\s*=\s*(\d+(?:\.\d+)?)') def _calls(tree: ast.AST) -> list[str]: names = [] for node in ast.walk(tree): if isinstance(node, ast.Call): func = node.func if isinstance(func, ast.Attribute): names.append(func.attr) elif isinstance(func, ast.Name): names.append(func.id) return names def _timeouts(text: str) -> list[float]: return [float(m.group(1)) for m in TIMEOUT_RE.finditer(text)] def compare(old: str, new: str) -> list[str]: findings: list[str] = [] try: old_names = _calls(ast.parse(old)) new_names = _calls(ast.parse(new)) except SyntaxError as exc: return [f'unparseable test file: {exc}'] old_count = {k: old_names.count(k) for k in set(old_names)} new_count = {k: new_names.count(k) for k in set(new_names)} for strong, weaks in WEAKEN.items(): if old_count.get(strong, 0) > new_count.get(strong, 0): dropped = old_count[strong] - new_count.get(strong, 0) if strong == 'assertRaises' or any( new_count.get(w, 0) > old_count.get(w, 0) for w in weaks ): findings.append(f'weakened oracle: {strong} dropped x{dropped}') old_t = _timeouts(old) new_t = _timeouts(new) if new_t and (not old_t or max(new_t) > max(old_t) * 2): findings.append(f'timeout inflation: {old_t} -> {new_t}') old_asserts = old.count('assert ') new_asserts = new.count('assert ') if new_asserts {new_asserts}') return findings def main() -> int: parser = argparse.ArgumentParser() parser.add_argument('--old', required=True) parser.add_argument('--new', required=True) args = parser.parse_args() findings = compare(Path(args.old).read_text(), Path(args.new).read_text()) for item in findings: print(item) return 1 if findings else 0 if __name__ == '__main__': raise SystemExit(main()) Enter fullscreen mode Exit fullscreen mode Wire it to git show / git diff however your CI already materializes before/after blobs. Fail the job on any finding unless a human-typed waiver file names the exact test function. Do not accept a comment in the agent’s commit message as a waiver. Merge decision table Signal Green pytest Action Round-trip fails on an existing sample irrelevant Block. Implementation lost an inverse. Idempotency fails on an existing sample irrelevant Block. Formatter is drifting. New code path, no new sample pass Block. Oracle did not grow. Assertion-delta reports a weakening pass Block unless a named waiver exists. Timeout doubled with no new I/O bound pass Block. That is a flake hide, not a fix. One-way function registered as round-trip fail or pass Block. Wrong invariant class. All three checks pass pass Allow merge on this axis. “Allow merge on this axis” is not “ship.” Type checks, review, and security review still apply. This table only removes a class of false greens. A workflow that keeps the registry honest Numbered on purpose. Skip a step and the gate decays into another chat prompt. On each agent patch, compute the set of touched modules. Load only the registry entries whose callables live in that set. Run tests/test_invariants.py for those entries. Do not batch them with unrelated unit tests on the first pass; you want a short, readable failure. Diff test files through tools/assert_delta.py. Treat weakenings as merge blockers. If the agent claims a new invertible pair, require a sample that is not a clone of an existing one. Duplicate samples are not coverage. If you want extra candidate samples, draft them offline, then paste only the reviewed ones into REGISTRY_*. Step 5 is where a free coding environment is useful as a generator, not as a judge. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode’s free model access can draft additional round-trip samples from public type signatures and docstrings; the free server option can run test_invariants.py plus assert_delta.py off the paid CI pool. Neither output is an oracle. Review every proposed sample. Discard pairs that are not actually invertible. Do not let the model edit REGISTRY_* in the same commit as production code. Two commits, two reviews. The registry is the contract. The patch is the suspect. What this does not prove Round-trip equality is not semantic correctness. decode(encode(x)) == x can hold for a codec that also accepts malformed input, leaks keys, or changes complexity from linear to quadratic. Idempotency does not prove that the first application was the right normalization. The AST scanner misses asserts built with getattr, helper wrappers, and dynamically built test classes. It also flags some refactors that are not weakenings, such as moving assertEqual into a local helper with the same meaning. That is why waivers exist. Keep them explicit and rare. Samples are not a generator. Four dicts will not explore Unicode, NaN, or 2 MB payloads. If you need that, add a separate bounded property lane with a fixed seed and a fixed iteration cap. Do not confuse that lane with this gate. Who should not use this Skip the round-trip registry if the change set has no inverse: CSS tweaks, one-way hashing, ML training loops, or fire-and-forget event emitters. Forcing invertibility there produces a theater test. Skip the assertion-delta scanner if humans already own every test edit and the agent is forbidden from touching tests/. The scanner is for repos where the agent is allowed to “update tests to match.” That permission is the risk. Skip the whole gate if merge already requires a human-written characterization suite that the agent cannot modify. You already have a stronger oracle. Do not stack a weaker copy on top for ceremony. Keep the inverse cheaper than the excuse The cheapest false green is a test that no longer asks a hard question. Invertibility asks one. Idempotency asks one. Assertion-delta asks whether the questions got easier. Run those three before you celebrate coverage. If fixtures are already pinned in your pipeline, add invertibility next. Another coverage number will not catch a broken inverse.

来源报道 · 1 篇 · 以下链接均前往原始媒体

← 返回精选动态 原文

更多内容