메뉴
HN
Hacker News 57일 전

100개 이상의 클로드 에이전트 병렬 테스트 사례

IMP
7/10
핵심 요약

이 글은 100개 이상의 AI 코딩 에이전트(Claude)를 병렬로 실행하여 대규모 엔드투엔드(E2E) 테스트를 수행하는 새로운 소프트웨어 테스트 방법론을 소개합니다. AI가 튜토리얼을 기반으로 자동으로 테스트 코드를 생성하고 실행, 디버깅, 수정하는 과정을 거치며, 이를 통해 테스트 자동화의 병목 현상을 해결할 수 있습니다. 개발 실무 관점에서 다수의 에이전트를 활용해 테스트 커버리지와 개발 효율성을 극대화하는 중요한 사례입니다.

번역된 본문

이전 블로그 게시물에서는 수백 개의 병렬 에이전트를 유용하게 실행할 수 있는 도구인 'mngr'를 소개했습니다. 이번 글에서는 mngr의 데모 스크립트를 테스트하여 실제로 mngr를 실행하고 개선하는 과정의 모든 세부 사항을 다룹니다.

고수준 아키텍처 전체 설정은 다음과 같이 작동합니다. 먼저 명령어 블록이 포함된 튜토리얼 스크립트(tutorial.sh)에서 시작합니다. 여기서 블록은 단순히 연속적인 비어 있지 않은 줄의 모음입니다. 각 블록에 대해 하나 이상의 pytest 함수를 도출합니다. 각 pytest 함수에 대해 에이전트를 실행하여 해당 함수를 실행, 디버그, 수정 및 개선합니다. 마지막으로 모든 에이전트의 결과를 통합합니다. 각 단계가 어떻게 작동하는지 자세히 살펴보겠습니다.

튜토리얼 스크립트 작성 이 스크립트는 우리가 직접 작성한 많은 내용을 기반으로 하지만, 약 50개 정도의 예시를 작성하고 나면 다소 지루해집니다. 따라서 우리는 단순히 다음과 같이 했습니다: 파일에 '# 스냅샷 관리'와 같은 주석을 적습니다. 코딩 에이전트에게 빈칸을 채우도록 요청합니다. 마음에 드는 것들을 검토하고 유지합니다. 코드베이스의 다른 곳에 이미 많은 문서, 특히 git 저장소에 커밋된 자동 생성된 man 페이지가 있기 때문에 에이전트는 예시를 생성하는 데 훌륭한 성과를 냅니다! 사실 에이전트가 제대로 수행하지 못할 때조차 유용합니다. 그것은 우리의 인터페이스가 너무 복잡하거나 문서화가 제대로 되지 않았다는 것을 의미하며, 인간 또한 사용 방법을 파악하는 데 어려움을 겪을 수 있음을 시사합니다. 그런 다음 우리는 이 피드백 신호를 활용해 mngr의 인터페이스를 최대한 단순하게 다듬었습니다. 에이전트에게 예시 생성을 요청하는 것은 윈윈(win-win) 상황이었습니다. 좋은 예시를 얻거나, 나쁜 예시를 얻어 그것을 활용해 mngr 자체를 개선할 수 있었으니까요!

튜토리얼 블록을 pytest 함수로 변환 충분한 양의 튜토리얼 명령어를 확보했으니, 이제 코딩 에이전트에게 이를 pytest 함수로 변환하도록 요청할 수 있습니다. 언급할 만한 몇 가지 세부 사항이 있습니다. 튜토리얼 블록은 간결하고 때로는 다소 인위적인 경향이 있지만, 테스트는 정상 경로(happy path)는 물론 예외 상황(unhappy path)까지 아우르는 등 더 철저해야 합니다. 따라서 이는 1:N 대응 관계를 가집니다. 동일한 튜토리얼 블록에 대해 명령어 자체나 환경의 미세한 변화가 다른 결과를 초래할 수 있으며, 이는 종종 별도의 테스트 케이스로 분리할 가치가 있습니다.

튜토리얼 블록과 테스트 함수 간의 대응 관계를 유지하기 위해, 우리는 에이전트가 테스트 픽스처(fixture)의 특정 API를 사용하여 생성한 함수 내에 튜토리얼 블록을 인용함으로써 해당 블록을 '선언'하도록 요청합니다. 에이전트가 이를 정확하게 수행했는지 확인하기 위해 간단한 스크립트도 사용합니다. 즉, 각 튜토리얼 블록에 대해 최소 하나 이상의 pytest 함수가 해당 블록을 인용하고 있는지 확인하는 것입니다. 이러한 모든 과정은 하나의 슬래시 명령어(slash command)인 sync-tutorial-to-e2e-tests로 패키징됩니다.

코딩 에이전트는 이 단계에서 일반적으로 엔드투엔드 테스트를 아주 잘 작성하지 못하며, 이는 완전히 예상된 결과입니다. 엔드투엔드 테스트는 인간이 작성하기에도 어려운데, 그 이유는 테스트의 세 단계 모두에서 본질적인 딜레마가 존재하기 때문이며, 이는 코딩 에이전트에게도 마찬가지로 적용됩니다:

준비(Arrange): 실제 사용 시나리오를 반영하기 위해 설정을 최소화하고 싶지만, 테스트를 적절히 격리된 상태로 유지하려면 적절한 양의 설정이 필요합니다. 실행(Act): 실제 명령어(이 경우 튜토리얼의 명령어)에 최대한 충실하고 싶지만, 테스트에 적합하도록 명령어에 약간의 변형이 자주 필요합니다. 검증(Assert): 명령어의 효과를 최대한 가깝게 테스트하고 싶지만, 예를 들어 파일 내용을 너무 문자 그대로 테스트하면 테스트가 깨지기 쉽거나 불안정해질 수 있습니다.

하지만 코딩 에이전트가 이 단계에서 잘 수행하지 못해도 괜찮습니다! 우리는 다음 단계에서 이 문제를 해결할 것입니다. 다만 그 전에 우리의 테스트 프레임워크에 대해 몇 가지 언급하겠습니다.

테스트 프레임워크 CLI 명령어를 실행하는 것의 가장 큰 장점은 Python(및 다른 모든 프로그래밍 언어)에 이미 이를 위한 API, 즉 subprocess 모듈이 존재한다는 것입니다. 명령어를 입력하면 표준 출력(stdout), 표준 에러(stderr) 및 종료 코드(exit code)를 얻을 수 있습니다. 그럼에도 불구하고 우리는 subprocess 위에 얇은 레이어 역할을 하는 몇 가지 유틸리티를 구축했습니다.

원문 보기
원문 보기 (영어)
In our previous blog post , we introduced mngr and how you can use it to usefully launch hundreds of parallel agents. Here’s all the details of how we are actually using mngr to run and improve itself, by testing its own demo script. High-level architecture This is how the entire setup works: We start from a tutorial script, tutorial.sh , containing blocks of commands. A block is simply a sequence of consecutive non-empty lines. For each block, we derive one or more pytest function. For each pytest function, we launch an agent to run, debug, fix and improve it. Finally, we integrate the outcome of all the agents together. Let’s dive into how each step works. Writing the tutorial script This script is seeded with a lot of content we wrote ourselves, but it is a bit tiring once we have written 50 or so examples. So we simply: Write some comments in the file, like # Managing snapshots Ask a coding agent to fill in the blank. Review and keep the ones that we like. Since we already have a lot of documentation elsewhere in the codebase—in particular, we have auto-generated man pages committed into the git repo, like this one —the agent does a good job of generating examples! In fact, even when it doesn’t, it’s still useful: that means our interface is too confusing or was not properly documented, and a human may have problems figuring out how to use it too. We then used that signal to refine mngr ’s interface to be as simple as possible. Asking agents to generate examples turned out to be a win-win situation–we either get good examples, or get bad examples and use that to improve mngr itself! Converting tutorial blocks to pytest functions Now that we have a healthy amount of tutorial commands, we can ask a coding agent to convert it into pytest functions. There are a few details worth mentioning. Tutorial blocks tend to be concise and sometimes a bit contrived, but we want tests to be more exhaustive, covering both happy and unhappy paths. So this is a 1:N correspondence: for the same tutorial block, we can expect slight variations of the command itself or the environment to result in different outcomes, and they often deserve separate test cases. In order to preserve the correspondence between tutorial blocks and test functions, we also ask the agent to “declare” which tutorial block it corresponds to, by citing the tutorial block in the function it generates, using a specific API in the test fixture. In order to keep the agent honest, we also use a simple script to check that it’s indeed the case - for each tutorial block, there’s at least one pytest function that cites the tutorial block. All of these are packaged into a slash command sync-tutorial-to-e2e-tests . The coding agent usually can’t do a very good job of writing end-to-end tests in this step, and that’s totally expected. End-to-end tests are difficult to write for humans because there are fundamental tensions in all three stages of tests, and the reasons equally hold for coding agents: Arrange : You want to set up as little as possible to reflect real-world usage scenarios, but you need an appropriate amount of setup to make tests appropriately isolated. Act : You want to be as faithful to real-world commands as possible—in this case, the commands from the tutorial—but you often need some variation for the commands to be suitable for testing. Assert : You want to test the effect of the commands as closely as possible, but testing e.g. file contents too literally can end up with fragile or flaky tests. But it’s okay if the coding agent doesn’t do a good job at this stage! We’ll solve this in the next step, but let’s mention a few things about our test framework. The test framework The great thing about running CLI commands is that Python (and any other programming language, really) already has an API for it: the subprocess module. Give it a command, and you can get the stdout, stderr and exit code. Still, we built some utilities (really just a thin layer on top of subprocess ) so that the test functions can be a little bit more concise and carry a little bit more information. A test function looks like this: def test_help_succeeds ( e2e : E2eSession ) - > None : e2e . write_tutorial_block ( """ # or see the other commands--list, destroy, message, connect, push, pull, clone, and more! These other commands are covered in their own sections below. mngr --help """ ) result = e2e . run ( "mngr --help" , comment = "or see the other commands--list, destroy, message, connect, push, pull, clone, and more!" , ) expect ( result ) . to_succeed ( ) expect ( result . stdout ) . to_contain ( "Usage" ) expect ( result . stdout ) . to_contain ( "create" ) expect ( result . stdout ) . to_contain ( "list" ) Building this extra layer also allows us to generate transcripts for the commands, which looks like: # or see the other commands--list, destroy, message, connect, push, pull, clone, and more! $ mngr --help any output to stdout ! any errors to stderr Finally, remember that mngr runs your agent in a tmux session, and tmux can’t be so easily captured as simple CLI transcripts. But fear not: mngr allows us to define a custom “connect command”, and in our test setup, we redirect it to a script by writing the following config: [commands.create] 'connect_command = "mngr-e2e-connect" The mngr-e2e-connect script, in turn, uses asciinema to attach to the agent, and saves the recording in the test output directory. We also built a combined view of all the artifacts–you can see the CLI transcript and TUI recordings on a web page: Orchestrating the tests Now that we have all those tests, let’s run them! Here’s the plan: Collect all the test names using pytest --collect-only For each test, launch an agent to work on it. This means several things: a. If the test is failing, either fix the test code or the code it’s testing b. If the test is passing, think about how to improve the test itself: make it more faithful to the original tutorial block, make the assertions more realistic, create additional tests etc. c. In any case, we instruct the agent to write a result JSON file. Wait for them to finish, pulling their result JSON files and test artifacts. Collect all the code changes they made, and use an agent to merge them all together into one mega PR. Aside from step 1, every other step is implemented using mngr ’s primitives: Use the mngr create primitive to launch testing agents, which also allows us to send an initial prompt. Use the mngr list primitive to poll the state of the agents. When an agent is done, use the mngr pull primitive to pull down the result files and test artifacts, and then the mngr stop primitive to stop it. Finally, use the mngr create primitive again to create the “integrator” agent that merges all the changes together. Integrating changes from all the agents Integrating changes from many agents is not a trivial task, even for an agent. We spent a lot of time thinking about and iterating on this, and this is eventually what we ended up with: Each of the testing agents would divide its commits into implementation fixes, and non-implementation (fixing the test or just making it better, fixing some doc, etc.) When the integrator sees the results from all the testing agents, it just merges all the non-implementation fixes together - since these are usually uncontroversial. For all the implementation fixes, we instruct the integrator to rank them by importance, and keep them as distinct commits, but merge them into a single linear branch, resolving conflicts along the way. This results in a single PR that can be easily reviewed by a human: the non-implementation fixes usually can be merged as is, and the implementation fixes can be reviewed one by one, with undesirable ones reverted. Another thing worth mentioning is that not all testing agents produce changes that can get integrated. Some testing agents just get confused, or can’t make progress unless something in the environment is fixed. We ins