ClaudeMods
☰
KO
● 0 명 접속 중 · 조회 0 회
후원프로젝트 제출
GitHub 저장소 · 작성자 ccdwyer

assertion-guardian

테스트 파일을 약화시키는 수정을 거부하는 Claude Code 모드입니다. 어설션 제거·완화, 케이스 삭제, 스킵 추가, 실패 삼키기, 스냅샷 재작성, 셸을 통한 테스트 변조를 막고 프롬프트 위에 한 번만 허용하는 오버라이드 행을 제공합니다.

번역 완료

이 mod 소개

Assertion Guardian은 테스트를 약하게 만들어 통과시키려는 에이전트를 막는 Claude Code 플러그인입니다. 테스트 파일에 Edit, Write 또는 NotebookEdit를 실행하기 전에 변경 전후의 파일 전체를 계산하고, 테스트가 약해졌으면 변경을 거부합니다. 가벼운 스캐너는 코드, 주석, 문자열 리터럴을 구분하므로 독스트링, 주석 또는 fixture 문자열 안의 어설션은 집계하지 않습니다. 제거된 어설션, 삭제된 테스트 케이스, 바뀌거나 빠진 기대값, 완화되거나 교체된 정확한 매처, 모호한 매처, 상수 어설션, 극성 반전, 삭제된 테이블 행, 추가된 스킵 또는 포커스 마커, 주석 처리된 어설션, 삼켜진 실패, 도달할 수 없는 분기, 직접 다시 쓴 스냅샷, 대량 스냅샷 업데이트, 셸로 변경한 테스트 파일(rm, mv, sed -i, >, tee, git rm, find -delete, git checkout, 인라인 python -c/node -e)을 감지합니다. 삭제된 테스트 파일은 git으로 마지막 커밋 버전과 비교하므로 처음부터 다시 작성해도 검사를 우회할 수 없습니다. 거부 메시지에는 작동한 신호를 나열하고 테스트 대상 코드를 수정하거나 사용자에게 물으라고 모델에 안내합니다. 일반 리팩터링은 통과합니다. 프롬프트 위의 한 번 허용/닫기 행이나 /allow-test-change 명령으로 오버라이드할 수 있으며, 오버라이드는 정확한 도구 호출에 연결됩니다. 테스트 파일은 일반적인 이름 패턴과 스냅샷 경로로 인식합니다. Hooks는 session.start, command.run{allow-test-change}, tool.call, ui.render{AbovePrompt}입니다. 네트워크나 텔레메트리 없이 전부 로컬에서 실행되며 라이선스는 MIT입니다.

설치

먼저 작성자의 README에서 marketplace와 플러그인 이름을 확인하세요. 저장소 구조에 따라 명령어가 달라질 수 있습니다.

claude plugin marketplace add ccdwyer/assertion-guardian
claude plugin install assertion-guardian
원문 / README

Assertion Guardian

Assertion Guardian demo

The model is asked to change an expected value so a failing test passes. Assertion Guardian refuses and names the signal, the band above the prompt offers allow once, and the real bug gets fixed instead. MP4

A Claude Code mod that stops the agent from making tests pass by making the tests weaker.

Before any Edit, Write or NotebookEdit to a test file runs, the mod works out the whole file before and after the change. It refuses the change if the tests got weaker. A light scanner first separates code from comments and string literals, so an expect( inside a docstring, a comment or a fixture string is never counted as code.

| Signal | Example | |---|---| | Assertions removed | expect(...), assert, assert.strictEqual, XCTAssert*, assertThat, t.Error, #expect | | Test cases deleted | it(...), test(...), def test_*, func Test*, @Test | | Expected values changed or dropped | Jest, Node assert, unittest/pytest, AssertJ, chai, RSpec, XCTest: toBe(3) changed to toBe(4), toHaveBeenCalledWith(1) changed to toHaveBeenCalled(), an edited inline snapshot, assert f() == 1 changed to == 2 | | Exact matchers replaced or loosened | toBe(3) changed to toBeTruthy(), toBeGreaterThanOrEqual(3) or toBeCloseTo(3), toStrictEqual changed to toEqual, toBeGreaterThan(3) changed to toBeGreaterThanOrEqual(3) or toBeLessThan(3), toBeCloseTo(3) changed to toBeCloseTo(3, 0) | | Vague matchers added | toEqual(0) changed to toEqual(expect.any(Number)) | | Constant assertions added | expect(3).toBe(3), assert.strictEqual(3, 3), assert 3 == 3, XCTAssertEqual(3, 3), assert True | | Assertion polarity flipped | .toBe(3) changed to .not.toBe(3), strictEqual changed to notStrictEqual, assert x == 3 changed to != 3, is None changed to is not None | | Test data rows removed | a row deleted from a Go table test or a test.each table | | Skips added | it.skip, xit, it.todo, test.fail, this.skip(), @pytest.mark.skip/xfail, @Disabled, @Ignore, t.Skip, XCTSkip | | Focus added | .only / fit(, which silently drops every other test | | Assertions commented out | a line that held an assertion reappears as a comment | | Assertion failures swallowed | a try around an assertion whose catch / except / rescue doesn't rethrow, pytest.raises(AssertionError), expect(() => expect(...)).toThrow() | | Dead branches | if (false) { expect(...) }, return before the assertions | | Snapshot files rewritten by hand | *.snap, __snapshots__/ | | Snapshots updated in bulk | jest -u, npm test -- -u, --update-snapshots, cargo insta accept, INSTA_UPDATE=1, loki update, backstop approve, chromatic --auto-accept-changes. Wrappers such as env, npx, pnpm exec, pnpm --filter and python -m are looked through | | Test files changed through the shell | rm, mv, sed -i, perl -pi, > redirects, tee, git rm, rm -rf tests, find ... -delete, git checkout <rev> -- file, bash -c "...", one-off python -c / node -e scripts that name a test file |

Suppose a test file has been deleted and is then written again from scratch. The new version is compared with what git last committed, so deleting the file doesn't get around the check.

The refusal lists which signals fired. It tells the model to fix the code under test, or to ask you if it thinks the test itself is wrong.

Normal refactors pass: renaming the value under test, reformatting, reordering, moving assertions between tests, adding tests, switching quote style, swapping in stronger matchers, and changing toBe(null) to toBeNull().

Override: a band above the prompt shows the last blocked change, with allow once and dismiss buttons. Or run /allow-test-change, which runs immediately, even mid-turn. An allowance is tied to that exact call (same tool, same file, same content), so the model has to retry the identical change. A different edit to the same file is checked from scratch.

This is a guardrail, not a sandbox. It checks what tool calls say they'll do. A determined process could still change files some other way, such as a script that never names the test file.

Test files are recognised by *.test.*, *.spec.*, test_*.py, tests.py, *_test.{py,go,rb,ex,dart}, *_spec.rb, *Tests.swift, *Test.{kt,java,cs}, and snapshot paths. Code files under __tests__/, test/, tests/ or spec/ count too, except those inside fixtures/, support/, helpers/, factories/, __mocks__/ or testdata/.

Install

/plugin marketplace add ccdwyer/claude-mods
/plugin install assertion-guardian@ccdwyer-mods
/reload-plugins

Develop

claude plugin validate .
claude plugin test .

What it hooks

Events this mod hooks, as claude plugin validate reads the module:

  • session.start
  • command.run{command=allow-test-change}
  • tool.call
  • ui.render{component=AbovePrompt}

Engine calls it makes: $.command.register, $.fs.exists (via previous), $.fs.read (via previous), $.process.run (via previous), $.state.get, $.state.set, $.ui.resolve.

A tool.call hook sits in the middle of every tool call: it can see the call, refuse it, or add context to its result. This mod uses that only for the behaviour described above.

Privacy

It runs entirely on your machine. It sends nothing over the network. It runs git locally to read the previous version of a test file.

The mod collects no analytics or telemetry, and its author receives no data from it.

Full policy: PRIVACY.md.

License

MIT

비슷한 프로젝트