Skip to content

[test] Add tests for logger.SlogHandler.Handle and related functions#3381

Merged
lpcox merged 1 commit intomainfrom
test-coverage/slog-adapter-handle-52cc30f7365d875a
Apr 11, 2026
Merged

[test] Add tests for logger.SlogHandler.Handle and related functions#3381
lpcox merged 1 commit intomainfrom
test-coverage/slog-adapter-handle-52cc30f7365d875a

Conversation

@github-actions
Copy link
Copy Markdown
Contributor

@github-actions github-actions bot commented Apr 8, 2026

Test Coverage Improvement: slog_adapter.go

Function Analyzed

  • Package: internal/logger
  • Functions: SlogHandler.Handle, formatSlogValue, NewSlogLoggerWithHandler
  • Previous Coverage: 0% (all three functions)
  • New Coverage: Handle → 95.2%, formatSlogValue → 100%, NewSlogLoggerWithHandler → 100%
  • Complexity: Medium (Handle has multiple branches: 4 log levels, attribute handling, disabled-logger early return)

Why These Functions?

These three functions in internal/logger/slog_adapter.go had 0% coverage because all existing tests guarded execution with:

if os.Getenv("DEBUG") == "" {
    t.Skip("Skipping test: DEBUG environment variable not set")
}
```

In CI environments where `DEBUG` is not set, the tests were always skipped — leaving `Handle`, `formatSlogValue`, and `NewSlogLoggerWithHandler` completely untested. These are important integration points since `SlogHandler` bridges the gh-aw debug logger with Go's standard `slog.Logger` interface.

### Tests Added

- ✅ `TestSlogHandler_Handle_WithDebugEnabled` — All 4 log level prefixes (`[DEBUG]`, `[INFO]`, `[WARN]`, `[ERROR]`) and unknown level fallback (no prefix)
-`TestSlogHandler_Handle_WhenDisabled`Early return path when logger is not enabled
-`TestSlogHandler_Handle_WithAttributes`String, integer, boolean, float attribute types; multiple attributes; empty message with attributes; no attributes
-`TestFormatSlogValue`Both `slog.Value` and non-`slog.Value` inputs (string, int, bool, float, nil)
-`TestNewSlogLoggerWithHandler_Enabled`Creates working logger, verifies output with namespace and attributes
-`TestNewSlogLoggerWithHandler_Disabled`Verifies no output when logger is disabled
-`TestNewSlogLoggerWithHandler_MultipleMessages`Verifies multiple messages through same logger
-`TestSlogHandler_Handle_AllLevelPrefixes`Focused table-driven test for all 4 standard slog levels
-`TestSlogHandler_Handle_NonStringKeyFallback`Exercises normal attribute key path

### Key Approach

Tests use `t.Setenv("DEBUG", "*")` to **force-enable** the logger rather than skipping. The logger's `enabled` field is computed at construction time via `computeEnabled()`, which reads the `DEBUG` env varso calling `t.Setenv` before `New()` correctly enables the logger and exercises all code paths.

### Coverage Report

```
Before:
  Handle                  0.0%
  formatSlogValue         0.0%
  NewSlogLoggerWithHandler 0.0%

After:
  Handle                  95.2%  (+95.2%)
  formatSlogValue        100.0%  (+100%)
  NewSlogLoggerWithHandler 100.0%  (+100%)
  
  logger package total:  90.7% → 96.0%  (+5.3%)
```

### Test Execution

```
=== RUN   TestSlogHandler_Handle_WithDebugEnabled
--- PASS: TestSlogHandler_Handle_WithDebugEnabled (0.00s)
=== RUN   TestSlogHandler_Handle_WhenDisabled
--- PASS: TestSlogHandler_Handle_WhenDisabled (0.00s)
=== RUN   TestSlogHandler_Handle_WithAttributes
--- PASS: TestSlogHandler_Handle_WithAttributes (0.00s)
=== RUN   TestFormatSlogValue
--- PASS: TestFormatSlogValue (0.00s)
=== RUN   TestNewSlogLoggerWithHandler_Enabled
--- PASS: TestNewSlogLoggerWithHandler_Enabled (0.00s)
=== RUN   TestNewSlogLoggerWithHandler_Disabled
--- PASS: TestNewSlogLoggerWithHandler_Disabled (0.00s)
=== RUN   TestSlogHandler_Handle_AllLevelPrefixes
--- PASS: TestSlogHandler_Handle_AllLevelPrefixes (0.00s)
ok  	github.com/github/gh-aw-mcpg/internal/logger	0.225s

Generated by Test Coverage Improver
Next run will target the next most complex under-tested function

Generated by Test Coverage Improver · ● 6M ·

- Add TestSlogHandler_Handle_WithDebugEnabled: covers all 4 log level
  prefixes (DEBUG/INFO/WARN/ERROR) and unknown level fallback
- Add TestSlogHandler_Handle_WhenDisabled: verifies early return when
  logger is not enabled
- Add TestSlogHandler_Handle_WithAttributes: covers all attribute types
  (string, int, bool, float) and multiple/empty attribute cases
- Add TestFormatSlogValue: covers both slog.Value and non-slog.Value
  inputs including nil, integer, boolean, and string
- Add TestNewSlogLoggerWithHandler_Enabled/Disabled/MultipleMessages:
  covers NewSlogLoggerWithHandler with enabled and disabled loggers
- Add TestSlogHandler_Handle_AllLevelPrefixes: targeted table-driven
  test for all 4 standard slog level prefix cases

Tests use t.Setenv("DEBUG", "*") instead of t.Skip() so they run
in all environments, bringing Handle from 0% → 95.2%, formatSlogValue
from 0% → 100%, and NewSlogLoggerWithHandler from 0% → 100%.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@lpcox lpcox marked this pull request as ready for review April 11, 2026 20:06
Copilot AI review requested due to automatic review settings April 11, 2026 20:06
@lpcox lpcox merged commit b547514 into main Apr 11, 2026
3 checks passed
@lpcox lpcox deleted the test-coverage/slog-adapter-handle-52cc30f7365d875a branch April 11, 2026 20:06
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds new unit tests to improve coverage for the internal/logger slog adapter, particularly around SlogHandler.Handle, formatSlogValue, and NewSlogLoggerWithHandler.

Changes:

  • Added table-driven tests that force-enable/disable logging via t.Setenv to exercise previously untested branches in SlogHandler.Handle.
  • Added direct tests for formatSlogValue and NewSlogLoggerWithHandler behavior in enabled/disabled scenarios.
  • Added additional prefix/attribute-focused test cases to expand branch coverage.
Show a summary per file
File Description
internal/logger/slog_adapter_test.go Adds new tests to cover slog adapter handler behavior, value formatting, and logger construction paths.

Copilot's findings

Tip

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

Comments suppressed due to low confidence (4)

internal/logger/slog_adapter_test.go:563

  • require.NoError is executed inside the captureStderr closure. If this fails, FailNow will prevent captureStderr from restoring os.Stderr, potentially impacting subsequent tests. Consider asserting outside the closure (e.g., capture error and check after), using assert.NoError inside, or making captureStderr restore os.Stderr with defer.

		r := slog.NewRecord(time.Now(), slog.LevelInfo, "should not appear", 0)
		err := handler.Handle(context.Background(), r)
		require.NoError(t, err)
	})

internal/logger/slog_adapter_test.go:658

  • Same pattern here: require.NoError runs within captureStderr's callback. On failure it can leave os.Stderr redirected because captureStderr doesn't defer its restoration. Prefer checking the error outside the callback or using assert.NoError inside it (or updating captureStderr to use defer).
					r.AddAttrs(attr)
				}

				err := handler.Handle(context.Background(), r)
				require.NoError(t, err)
			})

internal/logger/slog_adapter_test.go:804

  • require.NoError is called inside the captureStderr callback. If it fails, the callback will exit via FailNow and captureStderr won't restore os.Stderr. Recommend asserting the error outside the callback, using assert.NoError inside, or updating captureStderr to restore os.Stderr via defer.
				handler := NewSlogHandler(l)
				r := slog.NewRecord(time.Now(), lc.level, "test msg", 0)
				err := handler.Handle(context.Background(), r)
				require.NoError(t, err)
			})

internal/logger/slog_adapter_test.go:830

  • require.NoError is inside the captureStderr closure; if it fails, captureStderr won't reach its restoration logic and may leave os.Stderr redirected. Consider moving the assertion outside the closure, switching to assert.NoError within, or making captureStderr use defer for restoration.
		// verifying the normal path works (slog always provides string keys).
		r.AddAttrs(slog.String("normalkey", "val"))
		err := handler.Handle(context.Background(), r)
		require.NoError(t, err)
	})
  • Files reviewed: 1/1 changed files
  • Comments generated: 4

Comment on lines +536 to +540

r := slog.NewRecord(time.Now(), tt.level, tt.message, 0)
err := handler.Handle(context.Background(), r)
require.NoError(t, err)
})
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

require.NoError is called inside captureStderr's callback. If this assertion ever fails, require will FailNow and skip the cleanup logic inside captureStderr, leaving os.Stderr redirected for the rest of the test run. Prefer returning the error from the callback and asserting outside, or use assert.NoError inside the callback, or update captureStderr to restore os.Stderr via defer so it runs even on FailNow.

This issue also appears in the following locations of the same file:

  • line 559
  • line 653
  • line 800
  • line 826

Copilot uses AI. Check for mistakes.

assert.Contains(t, output, tt.message)
if tt.expectedPrefix != "" {
assert.Contains(t, output, tt.expectedPrefix)
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

The "unknown level produces no prefix" case isn't actually asserting the absence of a prefix; it only checks prefixes when expectedPrefix != "". This can let regressions slip through. Add an explicit negative assertion for the standard prefixes (or assert the logged line starts with the expected prefix) for the unknown-level case.

Suggested change
assert.Contains(t, output, tt.expectedPrefix)
assert.Contains(t, output, tt.expectedPrefix)
} else {
assert.NotContains(t, output, "[DEBUG] ")
assert.NotContains(t, output, "[INFO] ")
assert.NotContains(t, output, "[WARN] ")
assert.NotContains(t, output, "[ERROR] ")

Copilot uses AI. Check for mistakes.
Comment on lines +811 to +829
// TestSlogHandler_Handle_NonStringKeyFallback tests the defensive non-string key path.
// This exercises the fmt.Sprint fallback for non-string attribute keys.
func TestSlogHandler_Handle_NonStringKeyFallback(t *testing.T) {
t.Setenv("DEBUG", "*")

output := captureStderr(func() {
l := New("test:nonstring_key")
handler := NewSlogHandler(l)

r := slog.NewRecord(time.Now(), slog.LevelInfo, "test message", 0)

// Manually build an attrs slice that contains a non-string key
// by calling Handle with a crafted approach via direct field manipulation.
// Since the slog.Record.AddAttrs always uses string keys (a.Key is string),
// we test this path by calling the handler directly and adding a regular attr,
// verifying the normal path works (slog always provides string keys).
r.AddAttrs(slog.String("normalkey", "val"))
err := handler.Handle(context.Background(), r)
require.NoError(t, err)
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

This test is named/commented as if it exercises the non-string key fallback (fmt.Sprint), but slog.Attr.Key is always a string and this test only adds a normal slog.String attr. As written it doesn't validate the fallback branch in SlogHandler.Handle and is misleading. Either rename/rewrite the test to match what it actually covers, or remove the unreachable fallback logic in Handle (and adjust the PR description accordingly).

Copilot uses AI. Check for mistakes.
Comment on lines +530 to +555
// Force-enable the logger by setting DEBUG=* before creating logger
t.Setenv("DEBUG", "*")

output := captureStderr(func() {
l := New("test:handle_levels")
handler := NewSlogHandler(l)

r := slog.NewRecord(time.Now(), tt.level, tt.message, 0)
err := handler.Handle(context.Background(), r)
require.NoError(t, err)
})

assert.Contains(t, output, tt.message)
if tt.expectedPrefix != "" {
assert.Contains(t, output, tt.expectedPrefix)
}
})
}
}

// TestSlogHandler_Handle_WhenDisabled tests that Handle() returns nil without output
// when the logger is disabled.
func TestSlogHandler_Handle_WhenDisabled(t *testing.T) {
// Ensure DEBUG is unset so logger is disabled
t.Setenv("DEBUG", "")

Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

These tests use the hard-coded string "DEBUG" for environment configuration, while other tests in this file use the EnvDebug constant. Using EnvDebug here too avoids magic strings and keeps env-var naming consistent across the package.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants