前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
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 · 全部资讯8655
  • 像物理学家一样剪枝 LLM:区块移除的伊辛模型优化
  • Mac mini上的AI开发新范式:OpenClaw与Codex实战
  • Cloudflare Python Workers 正式上线,可用纯 Python 开发边缘应用
  • 浏览器直接给 ESP32 烧录 Claude 写的宏,无需 IDE 或工具链
  • Google 发布 Agent 安全风险报告:5 万美元循环消耗与凭证窃取案例
  • 多 Agent 合规流水线:用 RAG + 自修正架构对抗 WCAG 幻觉问题
  • Anthropic 发布金融领域 Claude 参考智能体套件
  • OpenAI 披露强化学习模型在上下文压缩时插入 Prompt 注入
  • Jev 决策模型实测:0.3秒完成意图分类和工具路由,$0.00004/次
  • Kimi Code Desktop 上线:图形界面 + 内置终端 + Git 状态,支持 Swarm 多 Agent 协
  • StepFun Step 5 Preview:600B 总参数 MoE 模型,1M 超长上下文,10 月开源
  • JSON-Render:通吃 React/Vue/Svelte/React Native 等 10+ 框架的生成式 UI
  • Agent-Native:让 Agent 能力同时暴露给 LLM 工具调用和 UI 操作的 TypeScript 框架
  • 清华联合无问芯穹开源具身智能体RPent,GPT-6 Astra注入机器人
  • AI Agent工具调用边界case:路由错误比想象中更脆弱
  • AI按它能读的契约编程,而非你想要的
  • MCP 远程服务器实现 AI 驱动的确定性 UI 编排
  • 阶跃Step 5 Preview实测:27B参数开源模型冲至Top2
  • 用Responses API构建有边界的GPT-6 Astra Agent
  • 用破坏不变量法审查 AI 生成代码
  • AI 编程测试冻结:保存异常指纹而非测试名
  • 程序员亲历:全公司用 Claude Code 批量生产代码,没人读代码,12 小时工作制
  • 自托管Llama部署成本全解析:GPU之外容易被忽视的隐性开销
  • Builder.io开源Agent-Native框架:让人与AI共享同一操作层
  • 让AI编程代理在中断后从断点恢复:session化执行轨迹方案
  • CI如何验证AI代理真的跑了测试而不是伪造结果
  • llm-keys-ui:让Coding Agent安全获取API密钥的插件
  • AI产品定价数学:固定费率如何悄悄亏损
  • ZCode事件:AI编程工具静默上传全部Git历史
  • Anthropic自评:R&D自动化率26%,但完全自主为0
  • 阿里Qwen-Image-2.1:70亿参数开源图像生成拳打闭源模型
  • OWASP CRS 规则引擎:给 LLM 和 MCP 加上 WAF
  • 智谱 MaaS 上线数据不留存机制,可申请开通
  • 如何界定可完成的AI工程范围
  • Cursor中直接查询技术文档的MCP方案
  • AI Agent的过度自信陷阱
  • Cursor与.NET周报:七个实战规则
  • AI Agent失效?模型可能不是根源
  • Qwen-Image-2.1开源:7B参数兼顾生图与编辑
  • 阿里开源 Qwen-Image-2.1:7B 参数兼顾透明图生成与多图编辑
  • 免费模型Trace不能支持的五个评估主张
  • AI重构前的副作用冻结术:先录 Ledger 再动格式
  • Agent能改Oracle则Green Build不足为信:独立检查三原则
  • AI编程导致代码质量下降?根子在质量管理
  • AI连接数据库必须先脱敏:PII保护实战指南
  • Anthropic与埃森哲20亿美元共建AI安全评估体系
  • Copilot CLI恢复检查点导致1GB未跟踪文件被删
  • AI代码审查升级:结果直发PR评论,淘汰复制粘贴
  • Higgsfield:开源万亿参数模型分布式训练框架
  • LlamaIndex多Agent工作流:事件驱动共享状态编排
  • 用AI从45条Ruff PR评论中挖掘团队隐式代码规范
  • 已加载 51 / 8655
8.0
热点
AI SCORE
编程提效2026-09-21 07:18

AI 编程测试冻结:保存异常指纹而非测试名

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

指出 AI 改代码后测试仍通过是因为 freeze 锁了测试名而非失败特征,提出用异常摘要哈希、种子日志、只读 fixture 树三重保真。

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

完整中文译文

一个通过的 CI 任务并不能作为 Agent patch 合并的依据——当属性检查未设种子、当 fixtures 放在可写目录树中、或者当一个 flaky freeze 仅以 pytest node id 作为键时。上述三个漏洞会让一个 patch 看起来稳定,而预言机(oracle)却在漂移。正确做法是:用一次带种子的试验日志、用哈希锁定的 fixture 语料库、用绑定到异常摘要的 freeze 账本来给 patch 打分。

测试名只是标签,不是预言机。Agent 可以重命名 test_retry_backoff、拆分模块、或者把同样的断言封装到一个辅助函数里。只存储 tests/test_retry.py::test_retry_backoff 的 freeze 会在错误的 node 上失效,或者根本不会失效。用来支撑这个 freeze 的那个失败已经从账本中消失了。

未设种子的属性试验同样具有这种虚假信心的形状。本次运行通过,下次运行可能就失败。如果评分器无法重放产生反例的那个确切随机抽取,就没有任何东西可以绑定到这个 freeze 上。重放才是证据,颜色不是。

账本必须存储什么

将三个产物存放在 patch 旁边,而不是内嵌在 patch 内部。把它们放在创作流程无法写入的目录树中。

一份属性试验日志,包含显式的 RNG 种子、试验次数、命中次数,以及当检查失败时的最小化输入。

一份 fixture 清单,对评分器允许读取的每一个人类拥有的输入文件做哈希。

一条 freeze 记录,以失败特征为键:node id + 种子 + 异常类型 + 消息体的摘要。

三者任一缺失,合并评分都是不完整的。不要把不完整的评分当作通过,要把它当作一个建议性缺口,让 patch 保持未合并状态。

Agent 无法写入的目录布局

将可写源目录树和预言机目录树分离。Agent 编写的测试可以存在,但不得计入评分。

repo/
  src/                  # agent may patch
  tests/generated/      # agent-authored, never scored
  oracle/
    fixtures/           # human-owned, hashed
    properties/         # seeded checks
    freeze_ledger.json  # signature-bound
    manifest.sha256

评分器读取 oracle/。Agent 进程不得拥有该路径的写权限。一个触及 oracle/ 的 patch 是进程失败,不是测试失败。在评分之前就拒绝它。

步骤 1:对 fixture 语料库做哈希

基于文件内容计算清单,而非基于 mtime。内容哈希在 checkout 顺序变化后依然有效,时间戳则不然。

# oracle_hash.py
from __future__ import annotations

import hashlib
import json
from pathlib import Path

ORACLE = Path("oracle")
FIXTURES = ORACLE / "fixtures"
MANIFEST = ORACLE / "manifest.sha256"

def file_digest(path: Path) -> str:
    h = hashlib.sha256()
    h.update(path.read_bytes())
    return h.hexdigest()

def build_manifest() -> dict[str, str]:
    rows: dict[str, str] = {}
    for path in sorted(FIXTURES.rglob("*")):
        if path.is_file():
            rel = path.relative_to(ORACLE).as_posix()
            rows[rel] = file_digest(path)
    return rows

def write_manifest() -> None:
    payload = json.dumps(build_manifest(), indent=2, sort_keys=True) + "\n"
    MANIFEST.write_text(payload)

def verify_manifest() -> list[str]:
    expected = json.loads(MANIFEST.read_text())
    actual = build_manifest()
    problems: list[str] = []
    for key in sorted(set(expected) | set(actual)):
        if expected.get(key) != actual.get(key):
            problems.append(f"hash mismatch: {key}")
    return problems

if __name__ == "__main__":
    import sys

    mode = sys.argv[1] if len(sys.argv) > 1 else "verify"
    if mode == "write":
        write_manifest()
    else:
        problems = verify_manifest()
        if problems:
            print("\n".join(problems))
            raise SystemExit(2)
        print("oracle fixture hashes match")

仅在人类拥有的 checkout 上运行 python oracle_hash.py write。Agent patch 的 CI 运行 python oracle_hash.py verify。哈希不匹配是评分无效,不是 flaky。不要重试它,不要 freeze 它。

步骤 2:为每条属性试验设种子

调用 random 时必须记录种子到试验日志中。一个无法重放的属性无法被 freeze。下面的 runner 是一个提议示例,不是经过实测的生产套件。

# oracle/properties/retry_budget.py
from __future__ import annotations

import json
import random
from dataclasses import asdict, dataclass
from pathlib import Path

@dataclass(frozen=True)
class TrialLog:
    property_id: str
    seed: int
    trials: int
    hits: int
    counterexample: dict | None

def retry_budget(delay_ms: list[int], cap_ms: int) -> bool:
    if not delay_ms:
        return False
    total = 0
    for delay in delay_ms:
        if delay < 0 or delay > cap_ms:
            return False
        total += delay
        if total > cap_ms:
            return False
    return True

def run(seed: int, trials: int = 200) -> TrialLog:
    rng = random.Random(seed)
    hits = 0
    counter = None
    for _ in range(trials):
        n = rng.randint(1, 8)
        cap = rng.choice([50, 100, 250, 1000])
        delays = [rng.randint(-5, cap + 40) for _ in range(n)]
        ok = retry_budget(delays, cap)
        # Count a hit only when the draw exercised a non-empty, mostly valid path.
        if any(delay >= 0 for delay in delays):
            hits += 1
        if not ok and counter is None:
            counter = {"delays": delays, "cap_ms": cap}
    return TrialLog("retry_budget", seed, trials, hits, counter)

if __name__ == "__main__":
    log = run(seed=20260921)
    Path("oracle/trial_retry_budget.json").write_text(
        json.dumps(asdict(log), indent=2) + "\n"
    )
    print(json.dumps(asdict(log)))

关键的字段是 seed、trials、hits 和 counterexample。hits == 0 的日志不是 freeze 的候选。没有种子的日志不计分。在试验日志中存档最小化输入,而不是塞进一个 pytest 名称里。

步骤 3:将 freeze 绑定到失败特征

node id 只是一个字段,不是键。要把 skip 绑定到你实际观察到的那个失败。

# freeze_bind.py
from __future__ import annotations

import hashlib
import json
import re
from pathlib import Path

LEDGER = Path("oracle/freeze_ledger.json")

def digest_message(message: str) -> str:
    normalized = re.sub(r"\d+", "<n>", message.strip())
    return hashlib.sha256(normalized.encode()).hexdigest()[:16]

def signature(nodeid: str, seed: int | None, exc_type: str, message: str) -> str:
    body = f"{nodeid}|{seed}|{exc_type}|{digest_message(message)}"
    return hashlib.sha256(body.encode()).hexdigest()[:24]

def load_ledger() -> dict:
    if not LEDGER.exists():
        return {"freezes": {}}
    return json.loads(LEDGER.read_text())

def freeze_applies(
    nodeid: str,
    seed: int | None,
    exc_type: str,
    message: str,
    today: str,
) -> bool:
    recs = load_ledger()["freezes"]
    sig = signature(nodeid, seed, exc_type, message)
    rec = recs.get(sig)
    if rec is None:
        return False
    return rec["expires_on"] >= today

对消息中的数字做归一化,这样时间戳不会在每次运行中产生新的 freeze。种子保留在键中。如果属性被重新设了种子,旧的 freeze 不再适用——这是刻意设计的。freeze 是重放保险,不是永久 skip。

提议的账本结构:

{
  "freezes": {
    "a1b2c3d4e5f6a7b8c9d0e1f2": {
      "nodeid": "oracle/properties/test_retry_budget.py::test_replay",
      "seed": 20260921,
      "exc_type": "AssertionError",
      "message_digest": "9f3c1a2b4d5e6f70",
      "first_seen": "2026-09-21",
      "expires_on": "2026-10-05",
      "reason": "human: intermittent cap rounding on slow runner"
    }
  }
}

人类撰写的 reason 是必填项。Agent 输出不得插入行。自动生成的 freeze 是用额外 JSON 重现了 node-id 问题。

步骤 4:在评分之前拒绝预言机编辑

# ci/reject_oracle_writes.sh
set -euo pipefail
BASE="${1:-origin/main}"
CHANGED="$(git diff --name-only "$BASE"...HEAD)"
if printf '%s\n' "$CHANGED" | grep -E '^oracle/'; then
  echo "reject: agent patch touched oracle/"
  printf '%s\n' "$CHANGED"
  exit 2
fi
python oracle_hash.py verify

在 pytest 之前运行此脚本。一个通过"修改账本"来"修复"flake 的 patch 不是修复,是预言机变更。Fail closed。

步骤 5:输出一个评分对象,而非任务颜色

颜色把三个问题坍缩成了一个比特。把它们分开。下面的脚本是一个提议的评分器。

# score_patch.py
from __future__ import annotations

import json
from pathlib import Path

from freeze_bind import freeze_applies
from oracle_hash import verify_manifest
from oracle.properties.retry_budget import run

def score(today: str, seed: int = 20260921) -> dict:
    hash_problems = verify_manifest()
    if hash_problems:
        return {"result": "void", "reason": hash_problems}

    log = run(seed=seed)
    Path("oracle/trial_retry_budget.json").write_text(
        json.dumps(
            {
                "property_id": log.property_id,
                "seed": log.seed,
                "trials": log.trials,
                "hits": log.hits,
                "counterexample": log.counterexample,
            },
            indent=2,
        )
        + "\n"
    )
    if log.hits == 0 or log.seed is None:
        return {"result": "incomplete", "trial": log.property_id}

    if log.counterexample is not None:
        message = (
            f"total delay exceeded cap {log.counterexample['cap_ms']}"
        )
        skipped = freeze_applies(
            nodeid="oracle/properties/retry_budget.py::run",
            seed=log.seed,
            exc_type="AssertionError",
            message=message,
            today=today,
        )
        if skipped:
            return {"result": "advisory_skip", "seed": log.seed}
        return {"result": "fail", "counterexample": log.counterexample}

    return {"result": "pass", "seed": log.seed, "hits": log.hits}

if __name__ == "__main__":
    print(json.dumps(score(today="2026-09-21"), indent=2))

把 JSON 送入合并规则。不要把 advisory_skip 并入 pass。也不要将 incomplete 并入 fail。Incomplete 表示预言机没有运行,这是评分器的 bug 或空的生成器问题,不是 src/ 中的产品缺陷。

advisory_skip 行是特征绑定的意义所在。这个 skip 仅适用于那个异常摘要和种子。同一个 node id 上出现了新的异常类型仍然会让任务失败。同一个摘要但测试被重命名了,仍然需要新的人类 freeze。两种情况都不是静默变绿。

一个生效的失败特征

假设 runner 报告了一个反例:cap_ms = 1000,延迟之和超过 cap,种子为 20260921,异常类型为 AssertionError。账本键不是 test_replay,而是那个 node、那个 seed、那个异常类型以及归一化消息的签名。

如果下次运行抛出的是 TimeoutError,freeze 不适用。如果下次运行使用种子 20260922,freeze 不适用。如果 Agent 重写了断言文本导致摘要变化,freeze 不适用。Patch 必须要么通过重放,要么等人类记录一条新签名。

这个规则看起来很严格,它就是很严格。存活周期超过其所描述的失败的 flake freeze 会变成没有取证痕迹的 skip。Node-id freeze 就是这样发生的。Signature freeze 让这条痕迹变得显式。

独立评分器应该运行在哪里

创作 src/ 的 Agent 不应该是写入 oracle/trial_*.json 的进程。把这两者分开。本地工作站可以创作 patch。第二个环境应该 checkout 同一个 commit,验证哈希,运行带种子的属性,并应用账本。

声明:本文是 MonkeyCode 产品推广的一部分。

MonkeyCode 的免费模型访问和免费服务器选项契合这种分离——当你想要在不同机器上进行创作和评分、但又不想先搭建一个私有 runner 时。评分器仍然需要预言机目录树、种子和账本。它不需要提出 patch 的模型。如果你已经有了一个锁定的 CI worker,用那个 worker。产品在当前工作流中是可选的,但哈希、种子和签名产物不是。

哈希锁检测字节变化,但不检测后续提交了更弱 fixture 的人类。Signature freeze 检测匹配的异常,但不检测通过吞掉异常并返回默认值来"修复"的 patch。Seeded property 检测可重放的随机抽取,但不检测生成器分布之外的超纲错误。

此工作流还假设你有人类拥有的 fixture 和至少一个具有真实命中率的属性。空语料库和重言式检查产生看起来完整的日志但毫无意义。试验日志仍然会序列化,评分仍然是谎言。不要 freeze 一个从未命中的属性。不要哈希 Agent 被允许重写的目录。

特征归一化有代价。折叠数字可能会合并两个仅在错误码上有差异的不同 bug。如果你的消息携带了区分故障的枚举值,就把这些 token 排除在数字清洗之外,或者把 freeze 绑定到一个结构化的错误码字段而非自由文本。

谁不应该使用这个

不要在没有 flake 历史的新原型上安装 freeze ledger。如果期望 Agent 拥有测试目录树,就不要对 fixtures 做哈希。如果你的失败在异常类型层面是非确定性的——比如同一个 bug 既可能抛出 TimeoutError 也可能抛出 ConnectionError——就不要绑定特征。后一种情况,修掉这个竞态,或者把两个签名记录为一个人类审查对。不要从 Agent 输出中自动生成 freeze。

已经在 tests/ 下拒绝任何 Agent 编辑的团队可以采用哈希和种子步骤而不加账本。账本是为那些你拒绝忽略又拒绝按名称跳过的已知 flake 准备的。如果你无法配备人力来审查 freeze 行,就跳过账本。一个未使用的账本比没有更糟糕——它看起来像是控制手段,实际上行为却像一个静默白名单。

合并的问题不是 CI 变绿了没有

合并的问题不是 CI 变绿了没有,而是那个绿是否来自一次可重放的属性、一份未被触及的 fixture 哈希、以及一条仍然描述着同一个失败的 freeze。如果你无法指向这三样产物,你就没有评分。

Original source

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

阅读英文原文
上一篇
用破坏不变量法审查 AI 生成代码
下一篇
程序员亲历:全公司用 Claude Code 批量生产代码,没人读代码,12 小时工作制