ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

用 ctags 仓库地图驱动黑盒测试:从 aider 实战对话看 AI 如何仅凭符号签名编写单元测试

2026/9/9 12:29:20 拓冰建站 浏览量
用 ctags 仓库地图驱动黑盒测试:从 aider 实战对话看 AI 如何仅凭符号签名编写单元测试 用 ctags 仓库地图驱动黑盒测试从 aider 实战对话看 AI 如何仅凭符号签名编写单元测试【免费下载链接】aideraider is AI pair programming in your terminal项目地址: https://gitcode.com/GitHub_Trending/ai/aider本篇文章围绕 aider 官方示例对话记录 examples/add-test.md 展开逐段解读一次真实的“黑盒测试”开发过程在完全没有拿到被测函数源码、也不知道仓库其余代码的情况下aider 仅凭借一份基于 ctags 的高层仓库地图repo map自主推断出被测方法的签名、所需依赖类与实例化方式写出单元测试并在/run执行失败的反馈中自动修正调用方式最终让测试通过。读完本文你将理解 aider 的仓库地图机制如何把“符号元数据”变成 LLM 的编码依据掌握/add、/run、自动提交在“测试先行”工作流中的配合方式并看到同一个测试在当前仓库源码与测试集中的真实落点。这份对话记录是什么aider/website/examples/目录收录了一系列真实的聊天记录chat transcripts用于展示 aider 在不同场景下的行为其索引见 examples/README.md。本份add-test.md是其中聚焦“黑盒测试”的一份This transcript shows aider creating a black box test case,without being given access to the source code of the function being tested or any of the other code in the repo.也就是说用户只告诉 aider 一个目标——为cmd_add()编写测试并运行验证——但从不把被测函数或相关源码加入对话。aider 能够完成这个任务靠的是它自主读取的仓库地图repo map中携带的“符号与函数签名”元数据。记录中特别指出需要先说明的是这段对话录制于 aider 较早期的版本对话中的cmd_add实现与当前源码已有差异但其展示的核心机制——仓库地图驱动的符号级推理、以/run串联“写测试→跑测试→读报错→改代码”的闭环——在今天的代码库中依然成立且后续我们会用当前源码与测试逐一印证。黑盒能力的来源ctags / 仓库地图里的符号元数据什么是 aider 的仓库地图aider 每次把仓库概况发送给模型时并不会完整读取所有文件而是生成一份高层的仓库地图。它通过 tree-sitter / ctags 语言标签抽取出仓库中各类语言的所有符号定义和函数签名例如函数名、参数列表、类名却不包含函数体实现。关于仓库地图的机制与设计动机可参考 repomap 说明文档、ctags 说明文档 以及发布仓库地图功能的历史文章 2023-05-25-ctags.md。核心思想是对 LLM 而言“仓库里有哪些符号、它们长什么签名”这一层元数据往往比文件正文更高效——既大幅节省上下文 token又足以支撑跨文件的结构性推理。对话中 LLM 仅凭地图完成了哪些推理记录中详细列出了 aider 在没有源码的前提下仅依赖地图元数据做到的事找到函数签名定位用户要求补测试的cmd_add()函数签名确定所属类判断它是Commands对话文本中写作Command类的方法因此测试需要先实例化一个对象才能调用推导依赖识别出构造Commands实例需要传入InputOutput与Coder两个对象逐层拆解构造参数推断出实例化InputOutput所需的参数对复杂依赖做替身判断Coder类过于复杂、不适合真实构造于是决定用MagicMock打桩。这五步是典型的“符号地图→调用链推导→测试脚手架”推理路径。LLM 并不需要读懂Commands的实现只要地图里有类的层次、构造签名与被测方法签名它就能像开发者阅读 API 文档一样拼出合法的调用序列。对话全记录解读一编写第一个测试以下是记录中完整的首次交互。aider 通过命令行aider tests/test_commands.py启动把尚不存在的测试文件加入对话并将其纳入 git $ aider tests/test_commands.py Creating empty file tests/test_commands.py Added tests/test_commands.py to the chat Files not tracked in .git: - tests/test_commands.py Add them? y Added tests/test_commands.py to the git repo Commit b9cea2c Added new files to the git repo: tests/test_commands.py Using git repo: .git用户随后下达需求明确把测试约束为“跑在临时目录里、验证文件被真正创建”add a test for cmd_add that passes in foo.txt and bar.txt and ensures they have both been created after the test. run the test in a tmp dir.aider 给出了它的测试方案——新建test_cmd_add方法用tempfile.TemporaryDirectory()建临时目录并切换工作目录然后调用cmd_add并断言foo.txt、bar.txt都被创建。注意因为cmd_add()定义时没有类型注解aider 只能“合理但不正确”地猜测它接收一个文件列表。这一猜测构成了后续失败的伏笔。aider 以编辑块edit block格式给出改动tests/test_commands.py ORIGINAL import os import tempfile from unittest import TestCase from unittest.mock import MagicMock from aider.commands import Commands from aider.io import InputOutput class TestCommands(TestCase): def test_cmd_add(self): # Create a temporary directory and change the current working directory with tempfile.TemporaryDirectory() as tmpdir: os.chdir(tmpdir) # Initialize the Commands and InputOutput objects io InputOutput(prettyFalse, yesTrue) coder MagicMock() commands Commands(io, coder) # Call the cmd_add method with foo.txt and bar.txt commands.cmd_add([foo.txt, bar.txt]) # Check if both files have been created in the temporary directory self.assertTrue(os.path.exists(foo.txt)) self.assertTrue(os.path.exists(bar.txt)) UPDATEDApplied edit to tests/test_commands.py Commit e2d5212 aider: Added a test methodtest_cmd_addthat creates a temporary directory, calls thecmd_addmethod with foo.txt and bar.txt, and checks if both files have been created in the temporary directory.值得注意的测试设计用InputOutput(prettyFalse, yesTrue)构造一个不依赖真实交互的输入输出对象yesTrue使确认类提问自动通过而把更复杂的Coder用MagicMock()顶替。这正是对话开头强调“地图足以支撑实例化推理”的体现。写入后 aider 自动提交提交信息以aider:前缀概括本次改动。对话全记录解读二/run 执行失败与自我纠错aider 接下来用/run命令执行测试。/run是 aider 内置的 shell 执行命令会在子进程中运行给定命令并把输出带回对话详见下文源码分析。第一次执行以失败告终/run pytest tests/test_commands.py test session starts platform darwin -- Python 3.11.3, pytest-7.3.1, pluggy-1.0.0 rootdir: /Users/gauthier/Projects/aider collected 1 item tests/test_commands.py F [100%] FAILURES __________________________ TestCommands.test_cmd_add ___________________________ self tests.test_commands.TestCommands testMethodtest_cmd_add def test_cmd_add(self): # Create a temporary directory and change the current working directory with tempfile.TemporaryDirectory() as tmpdir: os.chdir(tmpdir) # Initialize the Commands and InputOutput objects io InputOutput(prettyFalse, yesTrue) coder MagicMock() commands Commands(io, coder) # Call the cmd_add method with foo.txt and bar.txt commands.cmd_add([foo.txt, bar.txt]) /Users/gauthier/Projects/aider/tests/test_commands.py:20: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self aider.commands.Commands object at 0x10b412b50 args [foo.txt, bar.txt] def cmd_add(self, args): Add matching files to the chat session added_fnames [] files self.coder.get_all_relative_files() for word in args.split(): E AttributeError: list object has no attribute split /Users/gauthier/Projects/aider/aider/commands.py:161: AttributeError short test summary info FAILED tests/test_commands.py::TestCommands::test_cmd_add - AttributeError: ... 1 failed in 0.34s Add the output to the chat? [y/n] (y): y在“把输出加入对话”得到确认后报错栈被回灌给模型。LLM 立刻定位到问题本质cmd_add内部执行args.split()说明它期望的是字符串而不是列表——于是把调用改为传一个“以空格分隔文件名列表”的字符串It seems that thecmd_addmethod inaider/commands.pyis expecting a string as input, but in the test, we are passing a list. I will update the test to pass a string instead of a list.tests/test_commands.py ORIGINAL # Call the cmd_add method with foo.txt and bar.txt commands.cmd_add([foo.txt, bar.txt]) # Call the cmd_add method with foo.txt and bar.txt as a single string commands.cmd_add(foo.txt bar.txt) UPDATEDApplied edit to tests/test_commands.py Commit d1551b4 aider: Updated test to pass a string instead of a list when callingcmd_addmethod.对话全记录解读三测试通过修正后的再次执行得到绿色结果黑盒测试最终落地/run pytest tests/test_commands.py test session starts platform darwin -- Python 3.11.3, pytest-7.3.1, pluggy-1.0.0 rootdir: /Users/gauthier/Projects/aider collected 1 item tests/test_commands.py . [100%] 1 passed in 0.38s 整个工作流呈现一个清晰的闭环地图驱动猜测 → 写出测试 → /run 执行 → 真实报错校正猜测 → 测试转绿。值得注意的是模型犯的“类型猜测错误”并未造成多大代价因为/run提供了低成本、强信号的验证渠道——这正是“让 AI 动手写代码时搭配可执行反馈”的意义所在。源码印证一cmd_add 的“字符串参数”契约延续至今对话中暴露的AttributeError: list object has no attribute split发生在当时的旧实现上在当前仓库中cmd_add的实现已重写为 commands.pydef cmd_add(self, args): Add files to the chat so aider can edit them or review them in detail all_matched_files set() filenames parse_quoted_filenames(args)参数args依然是单个字符串内部通过 parse_quoted_filenames 解析def parse_quoted_filenames(args): filenames re.findall(r\(.?)\|(\S), args) filenames [name for sublist in filenames for name in sublist if name] return filenames这个函数用正则re.findall处理args天然要求字符串输入——如果今天仍向cmd_add传入一个list同样会在解析阶段失败re.findall无法处理列表。也就是说对话中总结出的契约“cmd_add接受一个用空格分隔文件名的字符串而不是文件名列表”在当前代码中依然成立只是解析更健壮了现在它支持带引号的文件名例如含空格的文件可写作my file.txt并且支持 glob 通配、目录递归等行为cmd_add后续逻辑通过glob_filtered_to_repo展开通配符并对 gitignore、只读文件、图片、越界路径做了完整校验。当前测试集中已经“记录”了这一正确用法对话最终的修复形态——commands.cmd_add(foo.txt bar.txt)——与当前仓库 tests/basic/test_commands.py 中同名的test_cmd_add几乎如出一辙def test_cmd_add(self): # Initialize the Commands and InputOutput objects io InputOutput(prettyFalse, fancy_inputFalse, yesTrue) from aider.coders import Coder coder Coder.create(self.GPT35, None, io) commands Commands(io, coder) # Call the cmd_add method with foo.txt and bar.txt as a single string commands.cmd_add(foo.txt bar.txt) # Check if both files have been created in the temporary directory self.assertTrue(os.path.exists(foo.txt)) self.assertTrue(os.path.exists(bar.txt))两处差异体现了代码库演进其一当前测试的InputOutput额外传入fancy_inputFalse因为今天InputOutput默认启用 fancy 交互输入见 io.py 的构造签名纯单元测试场景需要显式关闭其二当前测试不再用MagicMock顶替Coder而是通过Coder.create(self.GPT35, None, io)创建真实编码器——这说明如今的cmd_add实现依赖更真实的仓库上下文git 跟踪文件、路径校验等mock 难以满足。测试基类也演进出专门工具setUp/tearDown与ChdirTemporaryDirectory、GitTemporaryDirectorytests/basic/test_commands.py。围绕cmd_add当前测试集还覆盖了 glob 模式、空匹配创建、引号文件名、特殊字符目录、子目录相对路径、越界路径、只读文件提升等约二十种场景——把黑盒测试的“一个正确用例”扩展成了系统的契约验证矩阵。源码印证二/run 如何把命令输出变成模型的上下文对话中“Add the output to the chat? [y/n] (y): y”这个交互在源码里对应 cmd_rundef cmd_run(self, args, add_on_nonzero_exitFalse): Run a shell command and optionally add the output to the chat (alias: !) exit_status, combined_output run_cmd( args, verboseself.verbose, error_printself.io.tool_error, cwdself.coder.root ) ... token_count self.coder.main_model.token_count(combined_output) k_tokens token_count / 1000 if add_on_nonzero_exit: add exit_status ! 0 else: add self.io.confirm_ask(fAdd {k_tokens:.1f}k tokens of command output to the chat?)run_cmd负责实际执行子进程见 run_cmd.py。命令完成后aider 先估算输出 token 数并询问用户是否回灌确认后输出被包装进prompts.run_output模板定义于 prompts.py以一条用户消息形式追加进cur_messages紧接着补一条assistant: Ok.占位。之后模型就能“看到”完整的 pytest 堆栈并自行修复。若执行失败非零退出码还会把输入占位符设为Whats wrong? Fix主动引导下一轮修复——对话中报错后模型立即给出修正方案正是这条机制在起作用。这个“工具输出即模型上下文”的设计是黑盒测试闭环成立的关键技术前提模型无需猜测实现细节只需运行真实测试、阅读真实报错就能收敛到正确调用方式。从这段对话提炼的实战要点仓库地图是“以最少 token 获得全局符号信息”的手段只要地图质量足够符号完整、签名准确LLM 就能对未读源码的模块做合理推断。要理解地图的生成范围与取舍可查看 repomap.py 的实现及 repomap 文档。黑盒测试的脚手架策略可复用复杂依赖对话中的Coder用MagicMock真正参与断言的 IO 层则构造真实最小实例InputOutput(prettyFalse, yesTrue)并把测试隔离到临时目录。这是让“AI 生成的测试”在不确定环境中可复现的通用手法。/run是把 AI 从“猜”推向“验证”的杠杆写完测试立刻跑、把真实失败回灌模型就能基于报错而非臆测做修正。若希望在测试失败时无条件自动回灌输出用于 lint/test 自动执行场景cmd_run的add_on_nonzero_exit参数提供了支持。无类型注解的边界最容易猜错对话中cmd_add未声明参数类型模型按直觉推断为list。反过来说cmd_add在当前实现中接收空格分隔的字符串可含引号与 glob在 commands.py 有明确实现依据为被测代码补充类型注解能显著减少这类“合理但不正确”的猜测。自动提交让每次 AI 改动都可追溯对话中b9cea2c、e2d5212、d1551b4三个提交分别对应“加入文件”“新增测试”“修正调用”每次Applied edit后立即以aider:前缀提交。这种“一个改动一个提交、提交信息即变更摘要”的习惯让失败与修复的因果链条一目了然。总体而言这份对话记录演示的不只是“写一个测试”而是一套可复用的方法论让 LLM 借助符号级仓库地图建立调用契约用最小化测试脚手架验证契约再通过真实命令反馈迭代修正契约。结合当前仓库的 commands.py、tests/basic/test_commands.py 与 io.py你可以清楚地看到这段历史对话中的用例如何演化为今天仍然有效的回归测试。【免费下载链接】aideraider is AI pair programming in your terminal项目地址: https://gitcode.com/GitHub_Trending/ai/aider创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考