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
Generated by 🧪 Daily Testify Uber Super Expert · 36.7 AIC · ⌖ 13.8 AIC · ⊞ 5.2K · ◷
Overview
pkg/agentdrain/anomaly_test.gopkg/agentdrain/anomaly.goassert.*callsrequire.*callsStrengths
TestAnomalyDetector_Analyzewith boundary conditions (exact-threshold, zero-threshold, nil-cluster).anomalyScorehelper mirrors production constants — gives compile-time sync between test expectations and source weights.require.*for setup / fatal conditions andassert.*for non-fatal field checks.TestAnalyze_FlagMutualExclusivityexplicitly guards theIsNewTemplate && LowSimilarityinvariant.Prioritized Improvements
1. Missing / High-Value Tests
a) Score clamping path is never exercised
Analyzecontains a defensive clamp (if score > AnomalyMaxScore { score = AnomalyMaxScore }). Production logic prevents this today (mutual exclusivity ofIsNewTemplate/LowSimilarity), but future weight changes could trigger it. CallingbuildReasondirectly with all three flags set doesn't exercise the clamp inAnalyze. A dedicated test should callAnalyzevia a test double or by temporarily constructing a scenario where the flags breachAnomalyMaxScore.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:b)
NewAnomalyDetector— valid boundary values not stored-field verifiedCurrent
TestNewAnomalyDetector_ThresholdBoundariesonly checks that valid calls return no error and a non-nil detector. It never verifies that the storedthresholdandrareThresholdare actually used in a subsequentAnalyzecall. Consider extending the success cases:2. Testify Assertion Upgrades
TestBuildReason— userequirefor the nil guardbuildReasoncurrently 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 arequire.NotNilbefore dereferencing in any future pointer-passing helper:3. Table-Driven Refactors
TestAnalyzeEvent— shared mutable state is fragileTestAnalyzeEventuses three sequential sub-tests that mutate a shared*Miner. The comment correctly warns againstt.Parallel(), but the pattern means:Recommendation: Either:
requiregates already present (good), and add a comment explaining this is intentional stateful integration, ORTestAnalyzeEvent_Variantsparallel and stateless.Sketch of the stateful integration test as named steps
4. Organization / Readability
TestBuildReasonis missingt.Parallel()at the top level (the sub-tests add it, but the outertcould also be parallelized since it only creates the sub-test table without shared state).Acceptance Checklist
TestAnalyze_ScoreAlwaysNormalizedcovering bothisNew=trueandisNew=falsepaths with thresholds that maximize all applicable flags.TestNewAnomalyDetector_ThresholdBoundariessuccess cases to verify thresholds are actually applied inAnalyze.t.Parallel()toTestBuildReasonouter function.TestAnalyzeEventto make the intentional sequential/stateful dependency explicit and resilient to future additions.make test-unitand confirm all tests pass with no race conditions (go test -race ./pkg/agentdrain/...).