Communitygithub.com

lguidolin/tests-as-a-control

Use when writing or modifying tests, when a test breaks during a refactor, or when testing permission/role rules. Symptoms — tempted to edit a test to make it pass, testing only the happy path, a deny-test that started passing, flaky tests, or unsure what to assert.

tests-as-a-control란 무엇인가요?

tests-as-a-control is a Claude Code agent skill that use when writing or modifying tests, when a test breaks during a refactor, or when testing permission/role rules. Symptoms — tempted to edit a test to make it pass, testing only the happy path, a deny-test that started passing, flaky tests, or unsure what to assert.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/lguidolin/agent-skills/tree/main/skills/tests-as-a-control

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Tests as a Control

Overview

A test is a control specimen: it holds an expectation fixed so any change in behavior becomes visible. The discipline is don't fix the test to match the code; understand why they disagree.

The Rules

  • Test-first for behavioral change. Write the failing test, watch it fail for the right reason, then write the minimal code that passes. If you did not watch it fail, you do not know it tests what you think. The loop itself is superpowers:test-driven-development; this skill governs what the test must contain and what a failure means once it exists. The rule holds whether or not that skill is installed.

  • Structural change is verified by the unchanged suite. A pure structural change — rename, move, split, extract, with no observable behavior change — requires no new test. Its gate is the opposite: the entire existing suite passes with no test edited. This is why test-first does not apply here — there is no behavior to specify, so no test can meaningfully fail first. If a structural change forces a test edit, it was not structural. Stop and reclassify using the next rule.

  • A rename that crosses a contract boundary is not a refactor. The question is not "did the code change behavior?" but "is this name observable to something I do not control?"

    Renamed thingClassificationGate
    Internal symbol, private helper, local fileStructuralExisting suite passes unedited
    Exported function, API/GraphQL field, route, CLI flag, DB column, event name, config keyBehavioral — it breaks consumersTest-first + add-and-deprecate

    A public rename does the same thing to the same data and is still a breaking change, because the name was the contract (Hyrum's Law — see performance-and-scale). Treat it as behavioral: add the new name with a failing-test-first cycle, keep the old name and its passing test alive through the deprecation window, remove the old only once consumers have migrated.

    The third case is a test that references a name as a string — snapshots, DI container keys, reflection, fixture paths. These break on an internal rename. That is not the rule failing; it is the signal that the test was coupled to implementation detail. Fix the coupling, don't re-baseline.

  • Refactors must not edit tests; requirement changes must. Different acts:

    • A refactor breaks a test → the test caught an unintended behavior change. Stop and understand it. Editing the test to make a refactor pass destroys your instrumentation.
    • A requirement genuinely changed → update the test deliberately, as its own reviewable change, recorded as such.
    • The forbidden move is silent re-baselining.
  • Symmetric coverage — every rule tested both directions. Prove the authorized actor can and the unauthorized actor cannot. Happy-path-only tests half a rule.

  • Permission/role rules are a matrix: every meaningful (role × action × resource) cell, allow and deny. A new role is incomplete until its deny cases exist. A deny-test that suddenly passes-through is a security regression — review and accept explicitly, never re-baseline.

  • Entry points are never added/removed silently. Routes, commands, endpoints each get existence-and-smoke coverage so add/remove forces a test change.

  • Test by size: small (pure logic, no I/O, constant), medium (real DB/process, hermetic), large (full stack). Many small, fewer medium, fewest large. Watch the missing middle — server/app logic neither unit nor integration covers because each layer assumes the other tests it.

  • Flaky tests are quarantined on sight. A test that passes/fails without a code change destroys trust in the whole suite. Quarantine immediately (out of the blocking gate, file a fix) — never leave intermittently failing, never delete silently. Small tests must be deterministic: no real clock, randomness, or network.

  • Test code is DAMP, not DRY. Descriptive And Meaningful Phrases over de-duplication. A test must be obvious read in isolation. Applying DRY to tests is a violation, not a virtue (see change-hygiene-and-code-craft).

Common Rationalizations

ExcuseReality
"I'll write the code first, then add tests"Then the test was fitted to the code, not the requirement. You never watched it fail, so you don't know it tests anything.
"It's just a rename, no test needed"Correct — if the name is internal. If it's exported/public, the name was the contract and the rename is behavioral.
"I renamed it and had to fix three tests, that's normal"It isn't. Either you crossed a contract boundary, or those tests were coupled to implementation detail. Reclassify.
"The test is wrong now, I'll just update it"Only if a requirement changed. If you're refactoring, the test caught a real change — investigate.
"I tested the happy path, that's enough"A rule isn't tested until the deny case is too.
"This deny-test passes now, great, ship it"A forbidden action succeeding is a security regression, not progress.
"The test is flaky, just re-run CI"Flakiness masks real bugs and trains everyone to ignore failures. Quarantine and fix.
"Let me DRY up these tests with a shared helper"Tests are DAMP. Duplication that aids in-place clarity stays.

Red Flags — STOP

  • Writing production code for a behavior change before a failing test exists
  • A structural change whose diff also touches test files
  • Renaming a public/exported name in place instead of add-and-deprecate
  • Editing a test during a refactor to make it green
  • A new permission with no deny-case test
  • Re-baselining a snapshot/assertion without understanding why it changed
  • A test with a real sleep, Date.now(), random data, or network call
  • Refactoring test code to remove duplication at the cost of readability

Full rationale: Article IV of the constitution, bundled at engineering-constitution/references/engineering-constitution.md. For the GraphQL/route contract specifics see graphql-contract-testing.

Individual skills in this repo

This repo contains 18 individual skills — each has its own dedicated page.

lguidolin/change-hygiene-and-code-craft

Use when writing or refactoring code, structuring a commit or PR, or deciding whether to abstract duplication. Symptoms — mixing reorg with logic changes, a PR doing several things at once, a file growing large, the second copy of similar code, or unsure whether to DRY something up.

lguidolin/cloud-delivery-aks

Use when deploying to Kubernetes or Azure Kubernetes Service (AKS), configuring cloud secrets, setting up progressive rollout/canary, per-PR ephemeral environments, or k8s health probes. Keywords — Kubernetes, AKS, Key Vault, Argo Rollouts, Flagger, canary, blue-green, liveness, readiness, PodDisruptionBudget, HPA, rollback, GHCR.

lguidolin/commit-history-rewrite

Use when an existing repository has messy commit history that needs to conform to conventional commits before adopting release-please, or when intermediate WIP/fixup/merge commits need to be cleaned up.

lguidolin/conventional-commits-and-releases

Use when committing, writing a commit message, opening a PR that will be squash-merged, or configuring automated versioning/changelogs. Keywords — conventional commits, release-please, semver, feat/fix/chore, breaking change, changelog.

lguidolin/defense-in-depth-security

Use when handling untrusted input, secrets, authentication/authorization, or dependencies — or threat-modeling a new surface. Keywords — STRIDE, threat model, least privilege, secrets management, supply chain, dependency scanning, input validation, audit log, defense in depth.

lguidolin/designing-before-building

Use when starting a feature, fixing a non-trivial bug, or about to write implementation code — before any code exists. Symptoms you need this: "this is simple, I'll just code it", reaching for the editor before a design is approved, or an idea that hasn't been turned into a spec and plan.

lguidolin/engineering-constitution

Use when starting work in a project that follows the engineering constitution, orienting to its rules, or deciding which engineering practice applies to a task — spec writing, commits, testing, security, deploys, database, or UI work.

lguidolin/graphql-contract-testing

Use when writing a GraphQL query/mutation that the UI and a test will share, or building route/schema contract or smoke tests. Symptoms — copying a query into a test, a test asserting on query text, schema change that didn't break the UI build, or RLS/permission drift. Keywords — graphql-codegen, typed document, contract test, route smoke test.

lguidolin/init-repo-CI

Use when setting up a new repository with conventional commits, release-please, and CI automation, or when retrofitting an existing repository that lacks automated versioning and PR validation workflows.

lguidolin/interface-craft-and-accessibility

Use when building or styling UI — components, layouts, forms, design tokens — or making accessibility decisions. Keywords — a11y, WCAG, keyboard navigation, focus state, contrast, design system, minimalist UI, component reuse, ARIA, semantic HTML.

lguidolin/merge-gates-and-automation

Use when setting up or changing CI, pre-push hooks, or a task runner, or deciding what must pass before merge. Symptoms — tempted to put authoritative checks only in a local hook, skip CI, bypass with --no-verify, or unsure what gates a merge vs. runs locally.

lguidolin/observability-and-slos

Use when adding logging, metrics, tracing, health checks, SLOs, or alerting — or when building a service surface that needs to be operable and debuggable. Keywords — structured logs, OpenTelemetry, correlation id, RED metrics, liveness, readiness, SLI, SLO, error budget, alerting.

lguidolin/performance-and-scale

Use when working on hot paths, list endpoints, pagination, data-access in loops, or public interfaces/schemas. Symptoms — unbounded queries, N+1 access, no latency budget, optimizing without measuring, or changing an interface many consumers depend on. Keywords — pagination, N+1, Hyrum's Law, performance budget, bundle size.

lguidolin/postgres-postgraphile-rls-and-sql

Use when writing PostgreSQL, PostGraphile config, Row-Level Security policies, SQL schema files, or working on the Browser→App→PostGraphile→Postgres data path. Keywords — RLS, SECURITY DEFINER, search_path, pgSettings, grants, roles, GraphQL depth limit, query cost, statement_timeout, SQL file organization.

lguidolin/recording-decisions

Use when a design or architecture decision has been made and needs to be captured — writing a decision record or ADR, updating a decision index, noting a deferred idea, or superseding a past decision. Keywords — ADR, decision record, rationale, rejected alternatives, dependency index.

lguidolin/resilience-and-deploy-safety

Use when planning a deploy, designing a rollback, or responding to an incident or writing a postmortem. Keywords — deploy safety, rollback, immutable artifact, progressive delivery, canary, blast radius, incident response, blameless postmortem, error budget.

lguidolin/ship-it

Use when the user wants to ship work — push, PR, archive decision records, merge, and clean up. Handles the full lifecycle from committing final changes through post-merge cleanup including converting specs/plans to compact decision records.

lguidolin/zero-downtime-migrations

Use when changing a database schema where data must survive the change — adding/removing/renaming columns, constraints, indexes, or backfilling. Symptoms — a destructive migration bundled with a code deploy, a NOT NULL column with a backfill, a table-locking UPDATE, or a rename. Keywords — expand/contract, parallel change, backfill, NOT VALID, CREATE INDEX CONCURRENTLY, graphile-migrate.

관련 스킬