Skip to content

pkg/agentdrain: add nil-guard to Analyze and improve anomaly test quality#44455

Merged
pelikhan merged 7 commits into
mainfrom
copilot/testify-expert-improve-test-quality-again
Jul 9, 2026
Merged

pkg/agentdrain: add nil-guard to Analyze and improve anomaly test quality#44455
pelikhan merged 7 commits into
mainfrom
copilot/testify-expert-improve-test-quality-again

Conversation

Copilot AI commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Analyze would panic on a nil *MatchResult. The testify-expert issue also identified several test quality gaps: missing nil-result coverage, flat TestAnalyzeEvent (no sub-tests), no TestAnalyzeEvent_Variants for edge-case event shapes, missing t.Parallel() in stateless sub-tests, and an assert.Nil that should be require.Nil in an error path.

Production change

  • anomaly.go — nil-guard in Analyze: returns a zero-value AnomalyReport{Reason: "no anomaly detected"} when result == nil instead of panicking.

Test changes (anomaly_test.go)

  • anomalyScore helper: mirrors the weight constants from Analyze (1.0 / 0.7 / 0.3 / 2.0) so expected score literals in tests stay in sync with source changes.
  • TestAnalyze_NilResult: verifies the nil-guard returns a safe, zero-score report without panicking.
  • TestAnalyzeEventt.Run sub-tests: three sequential steps (first occurrence, repeat, distinct event) are now named sub-tests; not parallelised because they share a miner and depend on accumulated state.
  • TestAnalyzeEvent_Variants: new parallel table covering empty stage with fields, stage with nil fields, and fully empty events (both nil and map[string]string{}), confirming the correct error path.
  • t.Parallel() added to every sub-test in the four stateless table-driven tests (TestAnomalyDetector_Analyze, TestNewAnomalyDetector_ThresholdBoundaries, TestBuildReason, TestAnalyze_FlagMutualExclusivity).
  • assert.Nilrequire.Nil in the error branch of TestNewAnomalyDetector_ThresholdBoundaries to fail fast on a nil detector before any further assertions.

Copilot AI and others added 2 commits July 9, 2026 03:33
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
- Add nil-guard to Analyze in anomaly.go to prevent panic on nil result
- Add TestAnalyze_NilResult test verifying nil-guard behavior
- Refactor TestAnalyzeEvent to use t.Run sub-tests
- Add TestAnalyzeEvent_Variants table covering empty stage and nil fields
- Add t.Parallel() to sub-tests in stateless table-driven tests
- Upgrade assert.Nil → require.Nil in error path of TestNewAnomalyDetector_ThresholdBoundaries
- Add anomalyScore helper to keep expected-score values in sync with weights

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Improve test quality for anomaly_test.go pkg/agentdrain: add nil-guard to Analyze and improve anomaly test quality Jul 9, 2026
Copilot AI requested a review from pelikhan July 9, 2026 03:42
@pelikhan pelikhan marked this pull request as ready for review July 9, 2026 04:10
Copilot AI review requested due to automatic review settings July 9, 2026 04:10
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request hardens pkg/agentdrain anomaly detection by preventing a panic in AnomalyDetector.Analyze when called with a nil *MatchResult, and improves the anomaly-related test suite structure and coverage for edge-case event shapes.

Changes:

  • Added a nil-guard in AnomalyDetector.Analyze to return a safe, zero-score report instead of panicking.
  • Refactored and expanded tests with an anomalyScore helper, sequential TestAnalyzeEvent sub-tests, and a new parallel variant table for edge-case event shapes.
  • Improved test quality via broader t.Parallel() usage in stateless sub-tests and require.Nil on error paths.
Show a summary per file
File Description
pkg/agentdrain/anomaly.go Adds a nil-guard to Analyze to avoid panics and return a safe default report.
pkg/agentdrain/anomaly_test.go Adds helper + restructures/extends tests (sub-tests, variants, parallelism, stronger assertions).

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Low

Comment thread pkg/agentdrain/anomaly_test.go Outdated
Comment on lines +12 to +14
// anomalyScore computes the expected normalized anomaly score from the individual flag weights,
// mirroring the scoring logic in Analyze. Using this helper keeps test expectations in sync
// with the weight constants defined in the source.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 95d68be — the comment now reads "Using the exported constants (AnomalyWeightNew, AnomalyWeightLow, AnomalyWeightRare, AnomalyMaxScore) gives a compile-time sync guarantee".

Comment thread pkg/agentdrain/anomaly.go
Comment on lines +38 to +40
if result == nil {
return &AnomalyReport{Reason: buildReason(&AnomalyReport{})}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 95d68be — replaced buildReason(&AnomalyReport{}) with an explicit "no anomaly detected" literal and added a log line before the return, so there's a single allocation and no indirect call.

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 92/100 — Excellent

Analyzed 7 test(s): 6 design, 1 implementation, 0 violation(s).

📊 Metrics (7 tests)
Metric Value
Analyzed 7 (Go: 7, JS: 0)
✅ Design 6 (85.7%)
⚠️ Implementation 1 (14.3%)
Edge/error coverage 5 tests (71.4%)
Boundary conditions 15+ cases
Duplicate clusters 0
Inflation (45:1 ratio) Justified — targeted test additions
🚨 Violations 0
Test File Classification Issues
TestAnomalyDetector_Analyze anomaly_test.go:38–261 Design High-value; comprehensive boundaries; includes new nil-guard case
TestNewAnomalyDetector_ThresholdBoundaries anomaly_test.go:263–317 Design High-value; validates constructor error paths
TestBuildReason anomaly_test.go:319–399 Implementation Semi-design; verifies message formatting; 8 combinations tested
TestAnalyzeEvent anomaly_test.go:401–448 Design Medium-value; stateful integration test; 3 sequential steps
TestAnalyzeEvent_Variants anomaly_test.go:450–506 Design High-value; tests error paths; 2 new error cases added
TestAnalyze_NilResult anomaly_test.go:508–522 Design NEW — directly tests nil-guard safety fix
TestAnalyze_FlagMutualExclusivity anomaly_test.go:524–575 Design NEW — enforces IsNewTemplate ⊕ LowSimilarity invariant
✅ Test Strengths
  1. Nil-guard safety (NEW): TestAnalyze_NilResult explicitly verifies that Analyze(nil, ...) returns a safe zero-value report without panicking — directly testing the 3-line fix in anomaly.go line 38–40.

  2. Invariant enforcement (NEW): TestAnalyze_FlagMutualExclusivity prevents regression of the critical design contract that IsNewTemplate and LowSimilarity are mutually exclusive.

  3. Comprehensive boundaries: 15+ test cases across all tests cover rare-threshold edges, similarity-threshold edges, and zero-threshold limits.

  4. Error path coverage: 14+ error-related assertions test invalid thresholds, nil inputs, and empty events.

  5. Best practices: All table-driven tests use t.Run() for parallelization; no mock libraries; clean assertion messages.

Verdict

PASSED. 14.3% implementation tests (threshold: 30%). 0 violations. Excellent test quality: 6/7 tests are design tests covering behavioral contracts, boundaries, and safety guarantees. The 45:1 test-to-code ratio is justified by targeted additions to catch the nil-guard fix and invariant enforcement.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • awmgmcpg

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "awmgmcpg"

See Network Configuration for more information.

🧪 Test quality analysis by Test Quality Sentinel · 23.3 AIC · ⌖ 10.9 AIC · ⊞ 6.8K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 92/100. 14.3% implementation tests (threshold: 30%). Excellent test quality with 6/7 design tests covering behavioral contracts, boundaries, and safety guarantees.

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (138 new lines in pkg/) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/44455-nil-safe-analyze-boundary-and-test-score-helper.md — review and complete it before merging.

🔒 This PR cannot merge until an ADR is linked in the PR body.

📋 What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff
  2. Complete the missing sections — add context the AI could not infer, refine the decision rationale, and list real alternatives you considered
  3. Commit the finalized ADR to docs/adr/ on your branch
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-44455: Nil-safe Analyze boundary and test score helper

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 44455-nil-safe-analyze-boundary-and-test-score-helper.md for PR #44455).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 43 AIC · ⌖ 10.3 AIC · ⊞ 8.5K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: pkg/agentdrain nil-guard + test improvements

Overall the change is correct and well-structured. Two non-blocking suggestions:

Production (anomaly.go)

  • The nil-guard is sound. buildReason(&AnomalyReport{}) correctly returns "no anomaly detected" and the score stays 0.0, matching the test assertions.
  • The only gap is that the early-return path skips the anomalyLog.Printf call that every other non-nil result emits — inline comment left on line 39.

Tests (anomaly_test.go)

  • anomalyScore helper: good idea — ties expected values to weights rather than hardcoding 0.65/0.15. Weight constants are duplicated from production, but that is unavoidable in Go since production uses untyped consts inside the function body.
  • Sequential sub-tests in TestAnalyzeEvent are correctly not parallelised (shared stateful miner). Good call.
  • TestAnalyze_NilResult is the only new stateless test missing t.Parallel() — inline comment left on line 515.
  • assert.Nil to require.Nil in the error path: correct; avoids operating on a nil detector.

No blocking issues found.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 84.6 AIC · ⌖ 5.98 AIC · ⊞ 4.8K

Comment thread pkg/agentdrain/anomaly.go Outdated
Comment thread pkg/agentdrain/anomaly_test.go

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /diagnosing-bugs and /tdd — requesting changes on three targeted issues.

📋 Key Themes & Highlights

Key Themes

  • Weight constant duplication (anomalyScore helper, lines 17–20): the helper copies production constants instead of referencing them, breaking the sync guarantee it was designed to provide.
  • Indirect nil-guard implementation (anomaly.go, line 39): buildReason(&AnomalyReport{}) is an opaque way to construct a literal string — the indirection introduces fragility.
  • Missing t.Parallel() in TestAnalyze_NilResult (line 510): the only new stateless test that wasn't parallelised, inconsistent with the PR's stated intent.
  • Stateful sub-tests lack fail-fast gates (TestAnalyzeEvent, line 420): sequential sub-tests that depend on accumulated state should use require for the assertion that gates later steps.
  • Nil result with non-default isNew/cluster untested (line 517): the nil-guard silently drops isNew=true and a non-nil cluster — no test confirms callers notice if they incorrectly pass these.

Positive Highlights

  • ✅ Nil-guard production fix is correct and targeted — zero new dependencies, clean return path.
  • anomalyScore helper concept is valuable: tests now express intent rather than magic numbers.
  • TestAnalyzeEvent refactor to named sequential sub-tests with state-dependency comment is much clearer than the flat predecessor.
  • assert.Nil → require.Nil fix in error path is exactly right.
  • t.Parallel() rollout across four stateless table-driven tests is consistent and complete — except TestAnalyze_NilResult (see inline comment).

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 95.9 AIC · ⌖ 8.41 AIC · ⊞ 6.6K
Comment /matt to run again

Comment thread pkg/agentdrain/anomaly_test.go Outdated
weightNew = 1.0
weightLow = 0.7
weightRare = 0.3
maxScore = 2.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] Weight constants in anomalyScore duplicate unexported constants from anomaly.go (lines 52–65). If production weights change, this helper silently diverges and tests pass while exercising stale logic.

💡 Suggested fix: export constants from production code

In anomaly.go, promote the constants to package-level:

const (
    WeightNew       = 1.0
    WeightLow       = 0.7
    WeightRare      = 0.3
    MaxAnomalyScore = 2.0
)

Then anomalyScore references them directly for a compile-time sync guarantee:

func anomalyScore(isNew, lowSim, rare bool) float64 {
    var score float64
    if isNew { score += WeightNew }
    if lowSim { score += WeightLow }
    if rare   { score += WeightRare }
    if score > MaxAnomalyScore { score = MaxAnomalyScore }
    return score / MaxAnomalyScore
}

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 95d68be — exported AnomalyWeightNew, AnomalyWeightLow, AnomalyWeightRare, and AnomalyMaxScore as package-level constants in anomaly.go, updated the scoring loop to use them, and rewrote anomalyScore in the test to reference these constants directly for compile-time sync.

Comment thread pkg/agentdrain/anomaly.go Outdated
// - cluster is the cluster that was matched or created.
func (d *AnomalyDetector) Analyze(result *MatchResult, isNew bool, cluster *Cluster) *AnomalyReport {
if result == nil {
return &AnomalyReport{Reason: buildReason(&AnomalyReport{})}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] The nil-guard returns &AnomalyReport{Reason: buildReason(&AnomalyReport{})} which calls buildReason on a zero-value struct just to get "no anomaly detected". This is indirect and fragile — if buildReason's no-flags branch ever changes its wording, the nil path silently changes too.

💡 Simpler and more explicit
if result == nil {
    return &AnomalyReport{Reason: "no anomaly detected"}
}

The string literal directly documents the intent. If you want to keep parity with buildReason, at least add a comment explaining why the indirection is intentional.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 95d68be — replaced buildReason(&AnomalyReport{}) with the explicit literal "no anomaly detected" as suggested.

report := d.Analyze(nil, false, nil)
require.NotNil(t, report, "Analyze should return a non-nil report even for nil result")
assert.Equal(t, "no anomaly detected", report.Reason, "nil result should produce no-anomaly reason")
assert.InDelta(t, 0.0, report.AnomalyScore, 1e-9, "nil result should produce zero anomaly score")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] TestAnalyze_NilResult does not call t.Parallel() even though it is entirely stateless (creates its own detector, no shared state). This is inconsistent with the other stateless tests that were correctly parallelised in this PR.

💡 Add t.Parallel()
func TestAnalyze_NilResult(t *testing.T) {
    t.Parallel()
    d, err := NewAnomalyDetector(0.4, 2)
    ...
}

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 95d68be — added t.Parallel() to TestAnalyze_NilResult.

// on the state from the previous step.

t.Run("first occurrence trains template and is flagged new", func(t *testing.T) {
resultFirst, reportFirst, errFirst := m.AnalyzeEvent(evtPlan)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The comment says sub-tests are sequential because "they share a miner and depend on accumulated state", but there is no assertion or t.Helper call that makes a failure in step 1 skip steps 2 and 3. If reportFirst fails its assertions, step 2 will still run against a state that is now unexpected.

💡 Use require in stateful sub-tests, or document the skip behaviour

For the final assertions on reportFirst.IsNewTemplate and reportFirst.AnomalyScore, prefer require so a failure in step 1 immediately surfaces without confusing step 2/3 failures:

t.Run("first occurrence trains template and is flagged new", func(t *testing.T) {
    resultFirst, reportFirst, errFirst := m.AnalyzeEvent(evtPlan)
    require.NoError(t, errFirst)
    require.NotNil(t, resultFirst)
    require.NotNil(t, reportFirst)
    require.True(t, reportFirst.IsNewTemplate, "IsNewTemplate mismatch")  // gate for steps 2+3
    require.InDelta(t, anomalyScore(true, false, true), reportFirst.AnomalyScore, 1e-9)
    ...
})

This makes the dependency chain explicit.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 95d68be and 9cfa5ef — both IsNewTemplate and AnomalyScore assertions in the first sub-test now use require, with a comment above both clarifying they are gates for steps 2 and 3.

// Nil result must not panic; the nil-guard in Analyze returns a safe report.
report := d.Analyze(nil, false, nil)
require.NotNil(t, report, "Analyze should return a non-nil report even for nil result")
assert.Equal(t, "no anomaly detected", report.Reason, "nil result should produce no-anomaly reason")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] The nil-guard test does not verify what happens when isNew is true and cluster is also non-nil alongside result == nil. The nil-guard short-circuits before reading isNew or cluster, so those arguments are silently ignored — but there's no assertion confirming that. A caller passing isNew=true while result==nil (perhaps indicating a logic error upstream) gets a zero-score report with no signal.

💡 Add a variant that confirms arguments are ignored on nil result
// Confirm isNew and cluster are ignored when result is nil
cluster := &Cluster{ID: 1, Template: []string{"x"}, Size: 1}
report := d.Analyze(nil, true, cluster)
assert.Equal(t, "no anomaly detected", report.Reason)
assert.InDelta(t, 0.0, report.AnomalyScore, 1e-9)
assert.False(t, report.IsNewTemplate, "isNew should be ignored when result is nil")

Or consider logging a warning when result == nil && isNew to surface upstream anomalies.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 95d68be — added a second assertion block in TestAnalyze_NilResult that calls d.Analyze(nil, true, cluster) with isNew=true and a non-nil cluster and asserts the result is still the zero-value report, documenting that those arguments are silently ignored when result is nil.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES — one correctness issue must be fixed before merge

The nil guard in Analyze is semantically broken: it ignores isNew and cluster, producing a zero-score "no anomaly detected" report even when the caller passes meaningful flags. This is a correctness regression.

Issues raised

Blocking

  • anomaly.go:40 — nil guard discards isNew and cluster, wrong report for any caller that passes nil result + non-trivial flags.

Non-blocking (should be addressed)

  • anomaly_test.go:417 — sequential sub-tests share a *Miner with no guard against future t.Parallel() data races; outer test silently violates the file's established t.Parallel() convention with no comment explaining why.
  • anomaly_test.go:510TestAnalyze_NilResult missing t.Parallel(), inconsistent with every other stateless test in this PR.

Already covered by existing comments (not duplicated)

  • Wasteful &AnomalyReport{} allocation in nil guard
  • anomalyScore helper duplicating production weight constants

🔎 Code quality review by PR Code Quality Reviewer · 161.5 AIC · ⌖ 5.5 AIC · ⊞ 5.4K
Comment /review to run again

Comment thread pkg/agentdrain/anomaly.go
func (d *AnomalyDetector) Analyze(result *MatchResult, isNew bool, cluster *Cluster) *AnomalyReport {
if result == nil {
return &AnomalyReport{Reason: buildReason(&AnomalyReport{})}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nil guard ignores isNew and cluster arguments — callers that pass a nil result but a meaningful isNew=true or non-nil cluster silently get a zero-score report: the guard discards flags the caller set deliberately.

💡 Why this matters / suggested fix

In the sole production call site (miner.go:161) isNew is derived from inferResult == nil, so result == nil and isNew == true are always the same branch. But the nil guard here strips both from the report, meaning any direct caller that passes a non-nil cluster alongside a nil result will receive AnomalyScore=0.0 / Reason="no anomaly detected" when it should receive a scored new-template report.

Consider scoring the nil-result branch the same way as the normal path, just skipping the similarity check:

if result == nil {
    report := &AnomalyReport{
        IsNewTemplate: isNew,
        RareCluster:   cluster != nil && cluster.Size <= d.rareThreshold,
    }
    // compute score and reason the same way...
    return report
}

Alternatively, if nil is a programming error rather than an expected input, log a warning/panic rather than silently returning a clean report.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nil guard intentionally returns a zero-value report because in the sole production call site (miner.go:161) result is always non-nil when the method reaches Analyze — nil would indicate a logic error upstream, not a scored new-template event. The nil path is a defensive safety net, not a normal scoring branch. To make that intent explicit, I've added t.Parallel() to TestAnalyze_NilResult and a second assertion block (in 95d68be) confirming that isNew=true and a non-nil cluster are ignored on a nil result, documenting the contract.

Comment thread pkg/agentdrain/anomaly_test.go Outdated
assert.InDelta(t, 0.65, reportFirst.AnomalyScore, 1e-9, "AnomalyScore mismatch for first event")
assert.Equal(t, "new log template discovered; rare cluster (few observations)", reportFirst.Reason, "Reason mismatch for first event")
// Sub-tests are sequential: each step mutates the shared miner state and depends
// on the state from the previous step.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sequential sub-tests share a *Miner with no enforcement guard: the comment says sub-tests must stay sequential, but there is nothing stopping a future contributor from adding t.Parallel() to any sub-test, which would cause a data race on the shared miner's internal state.

💡 Suggested fix

Add //nolint:paralleltest or, better, add a short comment that will make the race obvious to Go tooling. The most reliable approach is to hold the sequential guarantee in a way the test runner enforces:

// Do NOT call t.Parallel() in these sub-tests — they share a Miner
// and depend on accumulated drain state from previous steps.
func TestAnalyzeEvent(t *testing.T) {
    // intentionally not t.Parallel()
    ...

Also note that the outer TestAnalyzeEvent function does not call t.Parallel(), making it the only top-level test in the file without it. That inconsistency alone will confuse reviewers; a comment explaining why it is sequential prevents the "was this forgotten?" question.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 95d68be — updated the comment to: "Do NOT add t.Parallel() to any of these sub-tests — doing so would cause a data race on the shared Miner."


// TestAnalyze_NilResult ensures that Analyze does not panic when result is nil
// and instead returns a zero-value report with the "no anomaly detected" reason.
func TestAnalyze_NilResult(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TestAnalyze_NilResult is the only new test in this PR missing t.Parallel(), inconsistent with every other stateless test updated in this diff.

💡 Fix
func TestAnalyze_NilResult(t *testing.T) {
    t.Parallel()
    d, err := NewAnomalyDetector(0.4, 2)
    ...

The test accesses no shared state, so parallelism is safe. Omitting it will confuse future contributors checking why this test is different.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 95d68be — added t.Parallel() to TestAnalyze_NilResult.

@pelikhan

pelikhan commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

…ard and tests

- Export AnomalyWeightNew/Low/Rare and AnomalyMaxScore constants from anomaly.go;
  use them in scoring loop for compile-time sync between production and tests
- Nil-guard: add anomalyLog.Printf observability line; replace buildReason
  indirection with explicit 'no anomaly detected' literal (single allocation)
- anomalyScore helper: rewrite to use exported constants; update comment to
  correctly say 'exported constants' not 'weight constants defined in the source'
- TestAnalyze_NilResult: add t.Parallel(); add second assertion block confirming
  isNew=true and non-nil cluster are ignored when result is nil
- TestAnalyzeEvent: strengthen 'do NOT add t.Parallel()' comment; change
  IsNewTemplate and AnomalyScore assertions to require in first sub-test (gate)
- Lock files regenerated (test-dispatcher drift)

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…lyzeEvent

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher skill

Done. Here's a summary of what was addressed across the 12 unresolved review threads:

anomaly.go

  • Exported AnomalyWeightNew, AnomalyWeightLow, AnomalyWeightRare, AnomalyMaxScore as package-level constants; scoring loop updated to use them.
  • Nil-guard: replaced buildReason(&AnomalyReport{}) with explicit "no anomaly detected" literal (single allocation, no indirection) and added anomalyLog.Printf observability line before returning.

anomaly_test.go

  • anomalyScore comment reworded to correctly say "exported constants" (not "weight constants defined in the source"); helper body updated to reference the exported constants directly for compile-time sync.
  • TestAnalyze_NilResult: added t.Parallel(); added a second assertion block confirming isNew=true and a non-nil cluster are silently ignored when result is nil.
  • TestAnalyzeEvent: strengthened the sequential-guard comment to say "Do NOT add t.Parallel()..."; changed IsNewTemplate and AnomalyScore assertions in the first sub-test to require (gates for steps 2+3) with a clarifying comment covering both.

Note: thread resolution requires a maintainer to click "Resolve" — GraphQL mutations are blocked in the agent sandbox. CI must also be re-triggered (close/reopen or workflow_dispatch) after the agent's push.

@pelikhan pelikhan merged commit 2bae4d3 into main Jul 9, 2026
@pelikhan pelikhan deleted the copilot/testify-expert-improve-test-quality-again branch July 9, 2026 05:02
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.82.8

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

3 participants