Skip to content

[testify-expert] Improve Test Quality: pkg/agentdrain/anomaly_test.go #46635

Description

@github-actions

Overview

Item Value
File pkg/agentdrain/anomaly_test.go
Source pair pkg/agentdrain/anomaly.go
Test functions 7
LOC 584
assert.* calls 32
require.* calls 30

Strengths

  • Excellent table-driven coverage in TestAnomalyDetector_Analyze with boundary conditions (exact-threshold, zero-threshold, nil-cluster).
  • anomalyScore helper mirrors production constants — gives compile-time sync between test expectations and source weights.
  • Appropriate require.* for setup / fatal conditions and assert.* for non-fatal field checks.
  • TestAnalyze_FlagMutualExclusivity explicitly guards the IsNewTemplate && LowSimilarity invariant.

Prioritized Improvements

1. Missing / High-Value Tests

a) Score clamping path is never exercised

Analyze contains a defensive clamp (if score > AnomalyMaxScore { score = AnomalyMaxScore }). Production logic prevents this today (mutual exclusivity of IsNewTemplate/LowSimilarity), but future weight changes could trigger it. Calling buildReason directly with all three flags set doesn't exercise the clamp in Analyze. A dedicated test should call Analyze via a test double or by temporarily constructing a scenario where the flags breach AnomalyMaxScore.

Since the flag invariant is enforced inside Analyze, the simplest approach is testing the score never exceeds 1.0 with a property-style loop over all flag combinations. For example:

func TestAnalyze_ScoreAlwaysNormalized(t *testing.T) {
    t.Parallel()
    d, err := NewAnomalyDetector(0.0, 100) // both thresholds trigger easily
    require.NoError(t, err)

    result := &MatchResult{ClusterID: 1, Similarity: 0.0}
    cluster := &Cluster{ID: 1, Size: 1}
    for _, isNew := range []bool{true, false} {
        report := d.Analyze(result, isNew, cluster)
        require.NotNil(t, report)
        assert.LessOrEqual(t, report.AnomalyScore, 1.0, "AnomalyScore must not exceed 1.0 (isNew=%v)", isNew)
        assert.GreaterOrEqual(t, report.AnomalyScore, 0.0, "AnomalyScore must be non-negative (isNew=%v)", isNew)
    }
}

b) NewAnomalyDetector — valid boundary values not stored-field verified

Current TestNewAnomalyDetector_ThresholdBoundaries only checks that valid calls return no error and a non-nil detector. It never verifies that the stored threshold and rareThreshold are actually used in a subsequent Analyze call. Consider extending the success cases:

// After successful construction, confirm the thresholds are wired correctly.
detector, err := NewAnomalyDetector(0.0, 0)
require.NoError(t, err)
// A similarity of 0.0 is not < 0.0 → LowSimilarity must be false.
r := detector.Analyze(&MatchResult{Similarity: 0.0}, false, nil)
assert.False(t, r.LowSimilarity, "similarity == threshold=0.0 should not be flagged")

2. Testify Assertion Upgrades

TestBuildReason — use require for the nil guard

buildReason currently accepts a pointer (*AnomalyReport). If the pointer is nil, it panics. The test never exercises nil input, but more importantly, test helpers creating &AnomalyReport{} inline could be riskier if the type changes. Low risk today, but worth documenting with a require.NotNil before dereferencing in any future pointer-passing helper:

// Before (implicit trust in the literal):
r := &AnomalyReport{...}
got := buildReason(r)
assert.Equal(t, tt.wantReason, got)

// After (explicit guard):
r := &AnomalyReport{...}
require.NotNil(t, r) // defensive; cheap, documents intent
got := buildReason(r)
assert.Equal(t, tt.wantReason, got)

3. Table-Driven Refactors

TestAnalyzeEvent — shared mutable state is fragile

TestAnalyzeEvent uses three sequential sub-tests that mutate a shared *Miner. The comment correctly warns against t.Parallel(), but the pattern means:

  • A failure in sub-test 1 silently invalidates sub-tests 2 and 3 (state is dirty).
  • Adding a 4th step requires reasoning about global miner state.

Recommendation: Either:

  • Use require gates already present (good), and add a comment explaining this is intentional stateful integration, OR
  • Extract into a top-level integration-style test with explicit state-transition assertions, keeping TestAnalyzeEvent_Variants parallel and stateless.
Sketch of the stateful integration test as named steps
func TestAnalyzeEvent_StateProgression(t *testing.T) {
    // NOT parallel — tests miner state machine progression.
    cfg := DefaultConfig()
    m, err := NewMiner(cfg)
    require.NoError(t, err)

    evt := AgentEvent{Stage: "plan", Fields: map[string]string{"action": "start"}}

    // Step 1: first observation creates new template.
    r1, rep1, err1 := m.AnalyzeEvent(evt)
    require.NoError(t, err1, "step 1")
    require.NotNil(t, r1)
    require.True(t, rep1.IsNewTemplate, "step 1: must be new")

    // Step 2: identical observation reuses template.
    r2, rep2, err2 := m.AnalyzeEvent(evt)
    require.NoError(t, err2, "step 2")
    require.NotNil(t, r2)
    assert.False(t, rep2.IsNewTemplate, "step 2: must reuse template")
    assert.Equal(t, r1.ClusterID, r2.ClusterID, "step 2: same cluster expected")
}

4. Organization / Readability

  • TestBuildReason is missing t.Parallel() at the top level (the sub-tests add it, but the outer t could also be parallelized since it only creates the sub-test table without shared state).
// Add at the top of TestBuildReason:
func TestBuildReason(t *testing.T) {
    t.Parallel() // ← add this
    tests := []struct { ... }{ ... }
    ...
}

Acceptance Checklist

  • Add TestAnalyze_ScoreAlwaysNormalized covering both isNew=true and isNew=false paths with thresholds that maximize all applicable flags.
  • Extend TestNewAnomalyDetector_ThresholdBoundaries success cases to verify thresholds are actually applied in Analyze.
  • Add t.Parallel() to TestBuildReason outer function.
  • Add a comment or refactor TestAnalyzeEvent to make the intentional sequential/stateful dependency explicit and resilient to future additions.
  • Run make test-unit and confirm all tests pass with no race conditions (go test -race ./pkg/agentdrain/...).

Generated by 🧪 Daily Testify Uber Super Expert · 36.7 AIC · ⌖ 13.8 AIC · ⊞ 5.2K ·

  • expires on Jul 21, 2026, 10:22 AM UTC-08:00

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions