Files
boshu2__agentops/skills/standards/references/test-pyramid.md
T
Boden Fuller 03a97978c5 fix(docs): align ASCII box-drawing diagrams across 23 files
Fix right-side │ border alignment in box diagrams. Three fix types:
- Trailing space adjustment for border alignment
- Box border widening for content that exceeded width
- Bottom border dash count matching top border
2026-03-29 07:55:25 -04:00

18 KiB
Raw Blame History

Test Pyramid — L0 through L7

Shared reference for RPI lifecycle skills. Loaded by /discovery, /plan, /pre-mortem, /implement, /crank, /validation, and /post-mortem.

The Full Testing Lifecycle

Level Name What It Tests When It Runs Who Writes It Context Needed
L0 Contract Tests Spec boundaries — registration, imports, file existence Every commit (CI) Agent from SPEC.md Just the spec
L1 Unit Tests Single function/class behavior in isolation Every commit (CI) Agent via TDD, before code Spec + function signature
L2 Integration Tests Multiple modules working together within a subsystem Every commit (CI) Agent after units pass Subsystem spec
L3 Component Tests Full subsystem end-to-end with mocked external deps Pre-merge gate Agent or human Subsystem + adapter specs
L4 Smoke Tests Critical path works after deployment — "does it boot?" Post-deploy (staging) Human defines, agent implements Deployment runbook
L5 E2E Tests Full system flow across subsystems, real infrastructure Staging environment Human designs, agent executes Architecture doc
L6 Acceptance Tests Does it do what the user actually needed? Staging with real data Human validates PRODUCT.md
L7 Canary / Prod Validation Does it work under real load with real users? Production (gradual rollout) Automated monitors + human judgment Prod observability

Agent Autonomy Boundaries

┌───────────────────────────────────────────────────────┐
│  AGENT-AUTONOMOUS (L0–L3)                             │
│  Agent writes tests AND implementation.               │
│  No human input needed for test design.               │
│                                                       │
│  L0: Contract — from SPEC.md alone                    │
│  L1: Unit     — TDD RED→GREEN from spec               │
│  L2: Integration — from subsystem spec + adapters     │
│  L3: Component — agent writes, human defines scenarios│
├───────────────────────────────────────────────────────┤
│  HUMAN-GUIDED (L4–L7)                                 │
│  Human defines WHAT to test.                          │
│  Agent builds the test infrastructure.                │
│                                                       │
│  L4: Smoke     — human defines "critical path"        │
│  L5: E2E       — human designs flow, agent harness    │
│  L6: Acceptance— human only validates                 │
│  L7: Prod      — monitors + human judgment            │
└───────────────────────────────────────────────────────┘

RPI Phase Mapping

RPI Phase Test Levels What Happens
Discovery (/discovery, /plan) L0–L3 scoping Plan identifies which test levels apply. Issues include test_level metadata.
Pre-mortem (/pre-mortem) L0–L3 coverage check Validates plan covers appropriate test levels. Flags gaps.
Implementation (/implement, /crank) L0–L2 writing + execution TDD writes L1 tests first (RED). L0 contracts from specs. L2 after units pass.
Validation (/vibe, /post-mortem) L0–L3 coverage audit Assesses test pyramid coverage. Flags missing levels as findings.

Test Level Selection Guide

Use this decision tree when planning which test levels to include:

Does the change touch external APIs or I/O?
  YES → L0 (contract) + L1 (unit) + L2 (integration) minimum
  NO  → L1 (unit) minimum

Does it cross module boundaries?
  YES → Add L2 (integration)
  NO  → L1 sufficient

Does it affect a full subsystem workflow?
  YES → Add L3 (component)
  NO  → Skip L3

Is it deploying to staging/prod?
  YES → L4 (smoke) required, L5 (E2E) recommended
  NO  → Skip L4–L7

Test Level Metadata for Issues

When creating issues in /plan, include test level metadata:

{
  "test_levels": {
    "required": ["L0", "L1"],
    "recommended": ["L2"],
    "deferred": ["L3"],
    "rationale": "Pure internal refactor — L0 contracts verify spec, L1 units verify behavior, L2 recommended for cross-module calls"
  }
}

Bug-Finding Levels (Agent-Autonomous)

Proven 2026-03-14 on jren-cm: 3,321 L1 unit tests found 0 new bugs. These levels found 8. Evidence: /Users/fullerbt/gt/jren_cm/crew/ichigo/scripts/.agents/council/2026-03-14-post-mortem-full-session-methodology.md

L0–L3 are the coverage pyramid — they verify code works as designed. These are the bug-finding pyramid — they find bugs the coverage pyramid misses.

Level Name What It Finds Agent Writes? Bugs Found (jren-cm)
BF1 Property-Based Edge cases from randomized inputs (non-IPv4, empty strings, unicode) Yes 1 crash
BF2 Golden/Snapshot Output drift, template regression Yes 0 (regression guard)
BF3 Mutation Untested code paths (operator flips that tests don't catch) Yes (config) 13 gaps
BF4 Chaos/Negative Unhandled exceptions at system boundaries (corrupt DB, permissions, timeouts) Yes 4 bugs
BF5 Script Functional Bash runtime crashes, jq logic errors, undefined functions Yes 2 critical bugs
BF6 Regression Reintroduced bugs that were previously fixed Yes N/A (guard)
BF7 Performance Speed regressions in parsers, renderers, hot paths Yes (config) N/A (baseline)
BF8 Backward Compat Breaking changes to input formats consumers depend on Yes N/A (guard)
BF9 Security (In-Test) Secrets leakage in output, unsanitized inputs, unsafe permissions Yes N/A (guard)

Bug-Finding Level Details

BF1 — Property-Based Testing

Randomize inputs to every data transformation. Catches crashes on inputs no human would write.

Language Tool Example
Python hypothesis @given(st.text()) on parsers
Go gopter or rapid rapid.Check(t, func(t *rapid.T) { ... })
TypeScript fast-check fc.assert(fc.property(fc.string(), (s) => ...))
Rust proptest proptest! { fn test(s in ".*") { ... } }

BF2 — Golden/Snapshot Testing

Generate canonical output, save as golden file. Test asserts exact match. Use GOLDEN_UPDATE=1 env var to regenerate.

Language Tool Pattern
Python pytest + tmp_path assert output == golden_path.read_text()
Go testdata/ convention golden.Update(t, got) / golden.Get(t)
TypeScript vitest snapshot expect(output).toMatchSnapshot()
Rust insta insta::assert_snapshot!(output)

BF3 — Mutation Testing

Flip operators and conditions in source. If tests still pass, there's an untested path.

Language Tool Command
Python mutmut mutmut run --paths-to-mutate src/critical.py
Go go-mutesting go-mutesting ./pkg/critical/...
TypeScript stryker npx stryker run --mutate 'src/critical/**'
Rust cargo-mutants cargo mutants --package critical

Target critical modules only. Full-codebase mutation is too slow (hours). Pick the 3-5 modules with highest blast radius.

BF4 — Chaos/Negative Testing

Inject failures at every system boundary. The bugs L1 mocks away.

# Python pattern
@patch("module.external_call", side_effect=TimeoutError)
def test_timeout_returns_specific_error(mock):
    result = function_under_test()
    assert result.error_class == "timeout"  # NOT "unknown"
// Go pattern
func TestHandlesCorruptDB(t *testing.T) {
    db := newCorruptDB()
    store, err := NewStore(db)
    require.NoError(t, err)
    assert.False(t, store.Enabled())  // graceful degradation, not crash
}

Checklist for every subsystem:

  • External API timeout → specific error, not crash
  • External API connection refused → specific error
  • File permission denied → graceful failure
  • Database corruption → degraded mode, not crash
  • Invalid/missing config → clear error message

BF5 — Script Functional Testing

Stub external tools via PATH override, source the script, call its functions, verify output.

# Create stub directory with fake oc/kubectl
STUB_DIR=$(mktemp -d)
cat > "${STUB_DIR}/oc" << 'EOF'
#!/bin/bash
echo '{"items": [{"status": {"phase": "Failed"}}]}'
EOF
chmod +x "${STUB_DIR}/oc"

# Run specialist with stubbed tools
PATH="${STUB_DIR}:${ORIGINAL_PATH}" source specialist.sh
result=$(scan 2>&1)
echo "$result" | python3 -c "import json,sys; json.load(sys.stdin)"  # valid JSON?

BF6 — Regression Testing (Bug-Specific Replay)

Every bug fix gets a test that reproduces the exact failure. Name it after the bug ID. These prevent regressions and serve as executable documentation.

Language Pattern Naming
Python def test_bug_ag_xyz_empty_input_crashes(): test_bug_<id>_<description>
Go func TestBug_AG_XYZ_EmptyInputCrashes(t *testing.T) { TestBug_<ID>_<Description>
Shell Separate test file: tests/regression/test_ag_xyz.sh test_<id>.sh
# Python pattern
def test_bug_ag_m0r_parse_reader_crashes_on_empty_value():
    """Regression: parse_reader crashed on config lines with empty values (ag-m0r)."""
    stream = io.StringIO("SITE_NAME=\nDB_HOST=prod-db")
    ctx = parse_reader(stream)  # must not raise
    assert ctx.site_name == ""  # empty, not crash
    assert ctx.db_host == "prod-db"
// Go pattern
func TestBug_AG_3B7_NilMapPanic(t *testing.T) {
    // Regression: passing nil options caused panic in processGoals (ag-3b7)
    result, err := processGoals(nil)
    require.NoError(t, err)
    assert.Empty(t, result)
}

BF7 — Performance/Benchmark Testing

Detect speed regressions mechanically. Set baselines, fail on significant deviation.

Language Tool Pattern
Python pytest-benchmark def test_parse_speed(benchmark): benchmark(parse_reader, large_input)
Go Built-in testing.B func BenchmarkParseConfig(b *testing.B) { for b.Loop() { parse(input) } }
TypeScript vitest bench bench('parse', () => { parseConfig(input) })
Rust criterion c.bench_function("parse", |b| b.iter(|| parse(input)))
# Python pattern — pytest-benchmark
def test_parse_config_performance(benchmark):
    """Parser must handle 1000-line config in <100ms."""
    large_config = "\n".join(f"KEY_{i}=value_{i}" for i in range(1000))
    result = benchmark(parse_reader, io.StringIO(large_config))
    assert isinstance(result, SiteContext)
// Go pattern — built-in benchmarks
func BenchmarkParseConfig(b *testing.B) {
    input := generateLargeConfig(1000) // 1000 key-value pairs
    b.ResetTimer()
    for b.Loop() {
        parseConfig(input)
    }
}

Target critical hot paths only. Full-codebase benchmarks are maintenance burden. Pick parsers, renderers, and high-frequency functions.

BF8 — Backward Compatibility Testing

Old inputs must still parse after code changes. Maintain a corpus of real inputs from prior versions (anonymized) as fixtures.

Language Pattern Fixture Location
Python @pytest.mark.parametrize("fixture", glob("tests/fixtures/compat/*.env")) tests/fixtures/compat/
Go testdata/compat/ convention testdata/compat/
Shell tests/fixtures/compat/ tests/fixtures/compat/
# Python pattern
@pytest.mark.parametrize("fixture", sorted(glob("tests/fixtures/compat/*.env")))
def test_legacy_config_parses(fixture):
    """Every historical config.env must still parse without error."""
    ctx = parse_config_env(fixture)
    assert ctx.site_name  # minimal validity — at least one required field populated
// Go pattern
func TestBackwardCompat(t *testing.T) {
    fixtures, _ := filepath.Glob("testdata/compat/*.env")
    for _, f := range fixtures {
        t.Run(filepath.Base(f), func(t *testing.T) {
            cfg, err := ParseConfigFile(f)
            require.NoError(t, err, "legacy config must still parse")
            assert.NotEmpty(t, cfg.SiteName)
        })
    }
}

Maintenance: When changing input formats, add the OLD format as a new compat fixture BEFORE making the change. This prevents silent breakage for consumers that haven't upgraded yet.

BF9 — Security Testing (In-Test)

Test that secrets are redacted, inputs are sanitized, and sensitive data never leaks into output. Distinct from security scanning (semgrep/gitleaks) — these are behavioral tests.

Category What to Test Pattern
Secrets redaction render_export() output contains no raw passwords/tokens Assert output lacks patterns like PASSWORD=actual_value
Input sanitization Untrusted input can't inject commands/paths Parameterize with injection payloads (../, ; rm, ${VAR})
Error message safety Stack traces and error messages don't leak secrets Assert error output contains no sensitive env vars
File permission safety Generated files have correct permissions (not world-readable) os.stat() after creation, assert mode
# Python pattern — secrets redaction
def test_render_export_redacts_secrets():
    """render_export must never emit raw secret values."""
    ctx = SiteContext(site_name="test", db_password="s3cr3t!", api_key="ak-12345")
    output = render_export(ctx)
    assert "s3cr3t!" not in output, "raw password leaked in export"
    assert "ak-12345" not in output, "raw API key leaked in export"
    assert "REDACTED" in output or "****" in output, "secrets not visibly redacted"
// Go pattern — path traversal rejection
func TestRejectsPathTraversal(t *testing.T) {
    payloads := []string{"../../../etc/passwd", "..\\windows\\system32", "foo/../bar"}
    for _, p := range payloads {
        t.Run(p, func(t *testing.T) {
            _, err := LoadConfig(p)
            assert.Error(t, err, "must reject path traversal")
        })
    }
}

When to Use Bug-Finding Levels

After L0–L3 coverage is complete, run bug-finding levels:

  Has data transformations (parse/render/serialize)?
    YES → BF1 (property-based) — randomize all inputs

  Has output generators (config files, reports, manifests)?
    YES → BF2 (golden) — snapshot every output

  Has critical modules (auth, state, error handling)?
    YES → BF3 (mutation) — targeted mutation on those modules

  Has external boundaries (APIs, databases, filesystems)?
    YES → BF4 (chaos) — inject failures at every boundary

  Has bash/shell scripts?
    YES → BF5 (functional) — stub tools, verify behavior

  Fixing a bug?
    YES → BF6 (regression) — reproduce the exact failure before fixing

  Has hot-path functions (parsers, renderers, serializers)?
    YES → BF7 (performance) — benchmark and set baseline

  Has input formats that external consumers depend on?
    YES → BF8 (backward compat) — maintain fixture corpus

  Has secrets, credentials, or sensitive data in scope?
    YES → BF9 (security) — test redaction and sanitization

Bug-Finding in RPI Phases

RPI Phase Bug-Finding Action
/plan Classify which BF levels apply per issue
/pre-mortem Verify BF levels are planned for boundary-touching code
/implement Write BF tests alongside L0–L3 (or as separate wave)
/vibe Check BF coverage before council — flag missing chaos/property tests on boundary code
/post-mortem Assess BF bug discovery count. If BF4 found 0 bugs → either code is solid or chaos tests are too weak
/implement (bug fix) BF6 mandatory — reproduce bug as failing test BEFORE writing fix
/vibe (performance) Check BF7 benchmarks if hot-path code changed
/plan (format changes) Flag BF8 backward compat — add old format as fixture before changing
/pre-mortem (security) Verify BF9 tests planned for code handling secrets or user input

Coverage Assessment Template

Used by /post-mortem and /vibe to assess test pyramid health:

Coverage Pyramid (L0–L3)

Level Tests Exist? Tests Pass? Coverage Gap? Action
L0 Contract yes/no yes/no/na description add/fix/ok
L1 Unit yes/no yes/no/na description add/fix/ok
L2 Integration yes/no yes/no/na description add/fix/ok
L3 Component yes/no yes/no/na description add/fix/ok
L4+ human-gated — — defer to human

Bug-Finding Pyramid (BF1–BF9)

Level Tests Exist? Bugs Found? Gap? Action
BF1 Property yes/no count data transforms without property tests add for parsers/renderers
BF2 Golden yes/no N/A output generators without golden files add for config/report generators
BF3 Mutation yes/no surviving mutants critical modules with <80% mutation score target top 3 modules
BF4 Chaos yes/no count external boundaries without failure injection add for every API/DB/FS boundary
BF5 Script Functional yes/no count bash scripts without functional tests add for top 10 by complexity
BF6 Regression yes/no count bug fixes without reproducing tests add for every bug fix
BF7 Performance yes/no baseline set? hot-path functions without benchmarks add for parsers/renderers
BF8 Backward Compat yes/no fixtures count input formats without compat corpus add fixture corpus
BF9 Security yes/no count code handling secrets without redaction tests add for every secret boundary