前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
返回 AI 情报前线
All News · 全部资讯8836
  • 用 TigerGraph+MCP 构建自主欺诈调查 Agent
  • Jev 决策模型真实成本拆解:何时省钱何时烧钱
  • 开源CLM-8B:Agent动作评分比Jev快9倍
  • JEV:打破布尔二值困境的类型安全验证方案
  • Claude Opus 5.5降价40%逼近前沿性能
  • VS Code 1.139:Agent 会话首次加载提速约 12 倍
  • 定时 Agent 总重复干活?用完成分类账让它知道什么是「做完」
  • TypeSafe AI Jev 编码指南:类型化决策、置信度校准与推测式广播
  • Vercel Connect 新增 TanStack AI 集成
  • Anthropic与OpenAI 90分钟内相继降价
  • 选LLM API的六个价格陷阱
  • Agent记忆正常仍出错:问题在状态不在记忆
  • Mercury 2.5 推理速度达 770 tokens/秒
  • GitHub Copilot 代码审查新增个人配置选项
  • MCP单人上手容易,团队规模落地是另一回事
  • 影子测试100%一致率背后:模型实际正确率仅75%
  • 两个都通过的测试,代价却不同:重试的隐性成本
  • 定时运行AI Agent输出飘移的根因与修复
  • Anthropic如何两周将Claude.ai速度提升3倍
  • claude-code-templates:一键装配 Claude Code 开发套件,含 100+ Agent/MCP
  • Agent 删改测试必须拦截:CI 合并门禁实操方案
  • Google Antigravity SDK 支持本地 AI 模型:Gemma 4 26B 可离线跑
  • NVIDIA Warp 与 MjWarp 加速机器人仿真工作流
  • HEMA 用 MCP 和 Amazon Bedrock 实现内部 AI 助手转型
  • 五大LLM网关工具生产环境横评
  • GitHub Copilot应用如何渲染百万行PR
  • 基于 AWS 构建 Agent 式视频智能对话系统架构解析
  • Bedrock 上用开源权重模型做 AI 编程助手
  • Anthropic 实验室:Claude 自主发现类 CRISPR 新型酶系统
  • ChatGPT Voice 集成邮件、日历和 Slack:Altman 心中的"Her"更近一步
  • OpenAI GPT-6 Sol/Luna 和 Claude Opus 5.5 同步降价 50%
  • AI 工具循环必须显式传递 Retry-After 头否则必死循环
  • AI 代码补丁静默引入新工具调用:merge 前必须强制契约检查
  • AI 写 API 文档无法区分 null/0/缺省三态:OpenAPI 契约必须显式约束
  • AI 编程 Agent 工具输出遭截断:应记录 stdout_bytes 和截断标志
  • Anthropic工程师揭秘:Claude为何越进化写作越差
  • Gemini 3.8 Flash / Flash-Lite TTS 发布:千款语音、30秒克隆、逐行台词控制
  • 工程师详解:新版Claude为何写作风格变得怪异
  • 小米MiMo-V3将搭载HySparse 2:100万Token下KV缓存缩小4.5倍
  • GitHub Copilot 应用新增本地沙箱隔离功能
  • AI Agent 调试指南:重启不是调试,七层架构定位根因
  • AI 加剧软件供应链攻击威胁,行业如何应对
  • 阿里 Qwen Audio 3.1 发布:语音识别/TTS 多模型,API 价格最高降 95%
  • Claude Code部署到Lizard平台实战指南
  • Claude Opus 5.5降价却破坏四个Agent依赖项
  • OpenAI GPT-6 Sol/Luna 半价发布,缓存机制或为更大降本杠杆
  • treg:聚合 3000+ Agent 工具的统一网关
  • 向量检索权限校验应内嵌到 pgvector 查询中
  • Univer:面向 AI Agent 的开源办公套件 SDK
  • DeepSeek公开Agent训练新论文,梁文锋署名
  • 为 AI 编程 Agent 构建可复用技能系统的实践
  • 已加载 51 / 8836
9.0
重磅
AI SCORE
编程提效2026-09-24 02:44

Agent 删改测试必须拦截:CI 合并门禁实操方案

dev.to · AI#AI编程#CI/CD#测试工程
Editor brief · 编辑速览

Agent 修改代码时可能删测试、弱化断言来降低失败率,文章提出在跑测试前先比对测试清单和断言强度差的 CI 门禁策略。

文章思维导图
Knowledge map
拖拽缩放
Full translation

完整中文译文

当 Agent 删除或削弱测试时,拒绝合并

更绿的测试套件不等于更好的测试套件。如果 Agent 的补丁删除了测试、删减了断言、或者冻结了已不再存在的 flaky 测试,CI 会报告成功,而原来的契约却在悄然萎缩。

先对测试清单打分,再对断言强度打分。只有在这两份 diff 都干净之后,才能针对锁定的 fixture 运行属性预言机,也只有到那时,才能对一个身份未移动的 flaky 测试施以带过期日期的冻结。

这是一个可以在本地运行的合并门控。它是一个工作流,而非舰队级别的研究。下面的命令和脚本都标注为"提议";它们按原样可执行,不是生产级指标。

为什么删除看起来像进步

Agent 补丁会针对你给它的信号做优化。如果那个信号是 pytest 退出码 0,删除测试就是一个合法操作。把 assert result == expected 弱化成 assert result 同样是合法操作。两者都缩小了失败面。

测试套件变快了,日志变安静了。但这两个事实都不是质量上的收益。

冻结列表同样会因此失效。如果一个测试被删除了,而冻结列表仍然引用它,reviewer 会认为这个 flaky 已经被控制。它并没有。检查已经离开了清单。

这个门控测量什么

在 Agent 运行之前捕获三份产物,之后做 diff。

清单。来自测试运行器的稳定节点 ID,而非文件 glob。

断言哈希。每个测试文件中类似断言语句的摘要,忽略仅注释的修改。

冻结资格。一个 flaky 测试只有在其节点 ID 仍然存在、断言摘要未改变、且过期日期仍在未来时,才能保持冻结状态。

属性检查和 fixture 锁在这份 diff 之后进行。它们回答的是:剩余的行为是否仍然成立。它们无法回答:你是否停止了检查。

步骤 1 — 快照清单

以运行器作为真实来源。不要 glob test_*.py 然后期望名称与收集结果匹配。

# proposal: capture node ids before the agent patch
python -m pytest --collect-only -q \
  | sed '/^$/d' \
  | grep '::' > /tmp/inventory.before

mkdir -p .gate
cp /tmp/inventory.before .gate/inventory.before

Pytest 打印的身份形如 tests/test_ledger.py::test_refund_is_idempotent。这正是你要冻结和 diff 的字符串。如果收集本身失败,停止。一个不可收集的测试套件不是基线。

把快照放在补丁 review 旁边,而不是 Agent 的 scratch 目录中。锁定 pytest 版本和插件,使节点 ID 在不同运行之间不会闪烁。

步骤 2 — 哈希断言,而非整个文件

整个文件的哈希会因为 import 重排和注释编辑而触发误报。你需要一个更窄的问题:检查是否改变了?

下面的脚本是一个提议。它是纯语法层面的,有意为之。assert True 仍然会被哈希。把这当作一个已知缺口,而不是语义理解。

# gate_assert_hash.py — proposal, run in CI
from __future__ import annotations

import ast
import hashlib
import json
import sys
from pathlib import Path

ASSERT_TYPES = (ast.Assert,)
CALL_NAMES = {"pytest.raises", "pytest.warns", "pytest.deprecated_call"}

def is_raises_like(node: ast.AST) -> bool:
    if not isinstance(node, ast.Call):
        return False
    name = ast.unparse(node.func)
    return name in CALL_NAMES or name.endswith(".raises")

def file_digest(path: Path) -> dict:
    tree = ast.parse(path.read_text(encoding="utf-8"))
    chunks: list[str] = []
    for node in ast.walk(tree):
        if isinstance(node, ASSERT_TYPES) or is_raises_like(node):
            chunks.append(ast.unparse(node))
    payload = "\n".join(sorted(chunks)).encode("utf-8")
    return {
        "path": str(path).replace("\\", "/"),
        "count": len(chunks),
        "sha256": hashlib.sha256(payload).hexdigest(),
    }

def walk(root: Path) -> list[dict]:
    return [file_digest(path) for path in sorted(root.rglob("test_*.py"))]

if __name__ == "__main__":
    root = Path(sys.argv[1] if len(sys.argv) > 1 else "tests")
    json.dump(walk(root), sys.stdout, indent=2)
    sys.stdout.write("\n")

在补丁的两侧运行它。

python gate_assert_hash.py tests > .gate/asserts.before.json
# ... agent applies a patch ...
python gate_assert_hash.py tests > .gate/asserts.after.json
python -m pytest --collect-only -q | sed '/^$/d' | grep '::' > .gate/inventory.after

如果 AST 解析失败,fail closed。一个 helper 无法读取的测试文件不是空的断言集。

步骤 3 — 对测试 diff 分类

不要基于退出码 0 就合并。要基于分类后的 delta 合并。

# gate_inventory_diff.py — proposal
from __future__ import annotations

import json
import sys
from pathlib import Path

def load_ids(path: Path) -> set[str]:
    return {
        line.strip()
        for line in path.read_text().splitlines()
        if "::" in line.strip()
    }

def load_asserts(path: Path) -> dict[str, dict]:
    rows = json.loads(path.read_text())
    return {row["path"]: row for row in rows}

def main() -> int:
    before_ids = load_ids(Path(".gate/inventory.before"))
    after_ids = load_ids(Path(".gate/inventory.after"))
    before_a = load_asserts(Path(".gate/asserts.before.json"))
    after_a = load_asserts(Path(".gate/asserts.after.json"))

    deleted = sorted(before_ids - after_ids)
    added = sorted(after_ids - before_ids)
    weakened = []
    for path, row in after_a.items():
        prev = before_a.get(path)
        if prev and row["count"] < prev["count"]:
            weakened.append(
                {
                    "path": path,
                    "before": prev["count"],
                    "after": row["count"],
                    "sha_changed": row["sha256"] != prev["sha256"],
                }
            )

    report = {
        "deleted_node_ids": deleted,
        "added_node_ids": added,
        "weakened_files": weakened,
        "ok": not deleted and not weakened,
    }
    Path(".gate/inventory.diff.json").write_text(
        json.dumps(report, indent=2) + "\n"
    )
    print(json.dumps(report, indent=2))
    if deleted:
        print("FAIL: tests left the inventory", file=sys.stderr)
        return 2
    if weakened:
        print("FAIL: assertion count dropped", file=sys.stderr)
        return 3
    return 0

if __name__ == "__main__":
    raise SystemExit(main())

新增的测试是允许的。它们不被信任。它们仍然需要在后面有属性预言机。

删除的测试和减少的断言计数以不同的退出码失败。CI 可以分别绘制这些类别,而不是把它们折叠成一根红色条。

步骤 4 — 对剩余部分进行属性检查

清单是必要条件,但不是充分条件。在 diff 干净之后,检查一次性示例无法充分指定的行为。

把属性放在单独的模块中。一个重写 tests/test_*.py 的 Agent 不应该在同一个补丁中编辑 oracle。在 review 中标注这个分割。

# properties/test_refund_properties.py — proposal
from decimal import Decimal

import pytest

from ledger import refund

@pytest.mark.property
@pytest.mark.parametrize(
    "paid,captured",
    [
        (Decimal("10.00"), Decimal("10.00")),
        (Decimal("10.00"), Decimal("3.50")),
        (Decimal("0.01"), Decimal("0.01")),
    ],
)
def test_refund_never_exceeds_captured(paid, captured):
    result = refund(paid=paid, captured=captured)
    assert result.amount >= 0
    assert result.amount <= captured
    assert result.amount <= paid

这里的属性是一个参数化的不变量,而不是证明。如果你后来想要一个带 shrink 的生成式运行器,把它作为第二个变更添加。先从领域已经相信的不变量开始。

这些属性读取的 fixture 字节应该和清单快照一起做摘要。如果 Agent 重写了一个 fixture 来匹配一个 bug,即使测试通过了,摘要的改变也是一个 fail。

# proposal: lock fixture bytes, not fixture meaning
find fixtures -type f -print0 | sort -z | xargs -0 sha256sum > .gate/fixtures.before
# after the patch
find fixtures -type f -print0 | sort -z | xargs -0 sha256sum > .gate/fixtures.after
diff -u .gate/fixtures.before .gate/fixtures.after

Fixture 摘要变更需要在补丁中有一个人类拥有的注释。Agent 的注释不算。

步骤 5 — 带资格规则的带过期日期冻结

Flaky 测试是存在的。把它们藏在被删除的测试里是冻结列表腐化的方式。

使用一个带过期日期的冻结文件。然后对你已经捕获的清单强制执行资格检查。

# .gate/flake_freeze.yaml — proposal
# Dates are inclusive UTC dates. Missing fields fail closed.
rules:
  - node_id: tests/test_ledger.py::test_refund_is_idempotent
    reason: "order-dependent cache on refund path"
    expires: "2026-10-08"
    assertion_sha256: "replace-with-digest-from-asserts.before.json"

assertion_sha256 字段在这个提议中是文件级别的,因为哈希器遍历文件。这比每个测试一个摘要更粗粒度。它仍然阻止了常见的作弊:重写检查,保留冻结,发出一个更安静的测试套件。

# gate_freeze.py — proposal
from __future__ import annotations

import datetime as dt
import json
import sys
from pathlib import Path

import yaml  # pin PyYAML in CI

def main() -> int:
    today = dt.date.fromisoformat("2026-09-24")  # CI must inject real UTC date
    freeze = yaml.safe_load(Path(".gate/flake_freeze.yaml").read_text())
    inventory = {
        line.strip()
        for line in Path(".gate/inventory.after").read_text().splitlines()
        if "::" in line.strip()
    }
    asserts = {
        row["path"]: row
        for row in json.loads(Path(".gate/asserts.after.json").read_text())
    }
    failed: list[str] = []
    for rule in freeze.get("rules") or []:
        node = rule["node_id"]
        expires = dt.date.fromisoformat(rule["expires"])
        if node not in inventory:
            failed.append(f"freeze names missing test: {node}")
            continue
        if expires < today:
            failed.append(f"expired freeze: {node}")
            continue
        file_key = node.split("::", 1)[0]
        row = asserts.get(file_key)
        expected = rule.get("assertion_sha256")
        if not expected or not row:
            failed.append(f"freeze missing digest mapping: {node}")
            continue
        if expected != row["sha256"]:
            failed.append(
                f"freeze ineligible after assertion change: {node}"
            )
    if failed:
        print("\n".join(failed), file=sys.stderr)
        return 4
    return 0

if __name__ == "__main__":
    raise SystemExit(main())

你不能冻结一个已经离开清单的节点 ID。

你不能冻结超过有效期的。日期是数据,不是注释。

当断言哈希移动了你不能保留冻结。那个变更是一个新测试。为它打分。

这张表就是策略。脚本只是编码它。如果一个 reviewer 不能指向一行,这个门控就没有尽到它的责任。

生成式补丁的适用位置和不适用位置

候选补丁必须来自某个地方。一个本地 checkout 就够了。远程任务也可以。

披露:本文是 MonkeyCode 产品推广的一部分。

MonkeyCode 提供免费的模型访问和免费服务器选项,这足以在隔离环境中生成候选补丁。该主机提出 diff,但不拥有 .gate/。在你要合并的分支上,在 CI 中运行 gate_inventory_diff.py、fixture 摘要和 gate_freeze.py。在写补丁的机器上跑出绿灯不是证据。

把 secrets 和生产 fixture 放在提议主机之外。上面的门控假设测试套件已经在你合并的地方运行。

断言哈希器是语法层面的。它不会标记 assert x == 1 被重写成 assert x != 0 的情况——当计数保持不变时。把它和属性配对。

它不会标记一个新的永真式。新增的测试按设计通过清单门控。它们仍然需要 oracle。

收集顺序、插件加载和 skip 标记可能使节点 ID 闪烁。锁定 pytest、插件和 PYTEST_ADDOPTS。如果收集是不确定的,清单 diff 就是噪音,应该 fail closed 直到你锁定它。

示例中的冻结日期固定在 2026-09-24,以便本文可复现。CI 必须注入真实的 UTC 日期。不要让 Agent 在没有人类拥有的 review 标签的情况下编辑 .gate/flake_freeze.yaml。

这个工作流不测量性能、成本或模型质量。删除后的更快测试会在 wall-clock 图表中看起来像一个胜利。从那些图表中排除已删除节点的运行,否则你会奖励这个门控存在所要阻止的失败模式。

谁不应该使用这个

不要在没有 CI 的单文件脚本上安装这个门控。清单快照没有东西可以 diff。

不要把它作为安全或支付路径代码 review 的替代品。断言计数不是威胁模型。

不要冻结整个测试套件。冻结是一个带过期日期的逐节点例外。全局 skip 列表是一个被禁用的门控。

如果你的测试是每次运行生成并丢弃的,就没有清单。在 diff 之前先稳定名称。


在 main 上快照清单、断言哈希和 fixture 摘要。

在分支上应用 Agent 补丁。

Diff 清单和断言计数。在删除或削弱时 fail。

拒绝引用缺失测试、过期日期或移动哈希的冻结规则。

运行 Agent 不允许在同一个补丁中编辑的属性模块。

运行剩余的示例测试套件。

把 .gate/inventory.diff.json 存储在合并旁边,而不仅仅是 pytest 摘要。

步骤 6 之后的安静日志不是产物。分类后的 diff 才是。

如果你已经在免费服务器上生成补丁,先把清单脚本加到 CI 中,再增加生成容量。最便宜的正确性 bug 是:一个不再存在的测试。

Original source

本文由 AI 翻译整理自 dev.to · AI,原文版权归原作者所有。

阅读英文原文
上一篇
claude-code-templates:一键装配 Claude Code 开发套件,含 100+ Agent/MCP
下一篇
Google Antigravity SDK 支持本地 AI 模型:Gemma 4 26B 可离线跑