Skip to content

refactor: share offline player data loading helper#85

Merged
klauern merged 2 commits intomainfrom
codex/refactor-offline-player-data-loader
Apr 26, 2026
Merged

refactor: share offline player data loading helper#85
klauern merged 2 commits intomainfrom
codex/refactor-offline-player-data-loader

Conversation

@klauern
Copy link
Copy Markdown
Owner

@klauern klauern commented Apr 26, 2026

Summary

  • extract shared loadOfflineDeckPlayerData helper for offline analysis loading behavior
  • reuse the helper from both loadPlayerDataOffline and loadSuitePlayerDataFromAnalysis
  • add focused unit tests for default analysis-dir resolution, explicit analysis-file loading, and wrapped load errors

Testing

  • GOCACHE=/tmp/go-build-cache GOMODCACHE=/tmp/go-mod-cache go test ./cmd/cr-api -run 'TestLoadOnlinePlayerAnalysisPreservesEvolutionLevels|TestLoadSuitePlayerDataFromAPIUsesSharedDeckAnalysis|TestLoadOfflineDeckPlayerDataDefaultsDirAndFallbacks|TestLoadOfflineDeckPlayerDataUsesExplicitFile|TestLoadOfflineDeckPlayerDataWrapsLoadErrors'

Summary by CodeRabbit

  • Refactor

    • Consolidated offline deck/player analysis loading into a single helper, simplifying flow and reducing duplicated logic across runtime commands.
  • Tests

    • Added unit tests covering offline analysis loading: default behavior, explicit file/dir loading, source reporting, and error propagation.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 26, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Refactors offline player analysis loading by extracting shared logic into loadOfflineDeckPlayerData. Two existing call sites now delegate offline loading to that helper; a new offline loader interface and container are added along with unit tests for file vs latest-analysis selection and error propagation.

Changes

Cohort / File(s) Summary
Deck builder & suite callers
cmd/cr-api/deck_builder_config_runtime.go, cmd/cr-api/deck_compare_suite_runtime.go
Removed inline offline analysis loading logic and now delegate to loadOfflineDeckPlayerData; eliminated prior fallback behavior that sourced player name/tag from loaded analysis in callers.
Offline loader & data container
cmd/cr-api/player_analysis_runtime.go
Adds offlineAnalysisLoader interface, offlineDeckPlayerData type and loadOfflineDeckPlayerData implementation that selects between explicit analysis file or latest-analysis in an analysis directory, derives PlayerName/PlayerTag with fallbacks, records Source, and wraps loader errors with context.
Tests / fake loader
cmd/cr-api/player_analysis_runtime_test.go
Adds fakeOfflineAnalysisLoader and multiple tests (default latest-analysis path, explicit analysis dir, explicit analysis file, and error propagation), verifying loader call patterns, Source value, name/tag fallbacks, and error wrapping/identity.

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly Related PRs

Poem

🐰 Hopping through code with a hop and a cheer,
I pulled the loader close and made it clear.
Files and latest, one helper to call,
Fewer branches now—no more sprawl. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: extracting and sharing a reusable offline player data loading helper function across multiple callsites.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/refactor-offline-player-data-loader

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (5)
cmd/cr-api/player_analysis_runtime_test.go (2)

213-225: Strengthen the error-wrapping assertion.

The current check (got == "boom") only verifies the message is not the bare loader error. It would still pass if the helper returned, say, an unrelated wrapped error like "foo: bar". Since the helper uses %w, prefer errors.Is to verify proper unwrapping, and optionally assert the contextual prefix.

♻️ Suggested stronger assertion
+	sentinel := errors.New("boom")
 	loader := &fakeOfflineAnalysisLoader{
-		loadLatestErr: errors.New("boom"),
+		loadLatestErr: sentinel,
 	}
 
 	_, err := loadOfflineDeckPlayerData(loader, "#POFFLINE", "", "", "testdata")
 	if err == nil {
 		t.Fatal("expected error")
 	}
-	if got := err.Error(); got == "boom" {
-		t.Fatalf("expected wrapped error, got %q", got)
+	if !errors.Is(err, sentinel) {
+		t.Fatalf("expected wrapped sentinel error, got %v", err)
+	}
+	if !strings.Contains(err.Error(), "#POFFLINE") {
+		t.Fatalf("expected error to include player tag, got %q", err.Error())
 	}

(Add "strings" to the imports.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/cr-api/player_analysis_runtime_test.go` around lines 213 - 225, The test
TestLoadOfflineDeckPlayerDataWrapsLoadErrors should assert that the loader error
is properly wrapped using errors.Is rather than only checking the error message;
update the test to import "errors" (and add "strings" if you also assert the
contextual prefix) and replace the current string equality check with
errors.Is(err, loader.loadLatestErr) to confirm unwrapping, and optionally
assert that err.Error() has the expected contextual prefix (e.g., with
strings.HasPrefix) to verify the helper added context.

22-50: Nice test harness — good coverage of the new helper.

The fakeOfflineAnalysisLoader cleanly captures call counts, inputs, and configurable errors, and the three tests exercise the meaningful branches (default dir + tag fallback, explicit file with provided identity, and error propagation).

One minor gap worth considering: there's no test for the case where analysisDir is explicitly provided (non-empty) — i.e., that loadOfflineDeckPlayerData does not apply the filepath.Join(dataDir, "analysis") default and uses the caller-supplied directory verbatim. That branch is exercised by loadPlayerDataOffline in production but not directly covered here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/cr-api/player_analysis_runtime_test.go` around lines 22 - 50, Add a unit
test that verifies the explicit analysisDir path is used (i.e.,
loadOfflineDeckPlayerData does not fall back to filepath.Join(dataDir,
"analysis") when a non-empty analysisDir is provided): instantiate
fakeOfflineAnalysisLoader, call loadOfflineDeckPlayerData (or the wrapper
exercised by tests) with a non-empty analysisDir value, then assert
fakeOfflineAnalysisLoader.latestDir equals that exact provided directory and
that latestCalls was incremented; reference fakeOfflineAnalysisLoader,
LoadLatestAnalysis, and loadOfflineDeckPlayerData when implementing the test.
cmd/cr-api/player_analysis_runtime.go (1)

38-73: Consider guarding against (nil, nil) returns from the loader.

Lines 58 and 63 dereference loadedAnalysis immediately after a successful load. The real deck.Builder implementations probably never return (nil, nil), but the offlineAnalysisLoader interface doesn't enforce that contract — a misbehaving (or test-stub) loader returning a nil analysis with no error would panic here rather than producing a clean error. A small nil check would make this helper robust to interface misuse.

🛡️ Suggested defensive guard
 	if analysisFile != "" {
 		loadedAnalysis, err = loader.LoadAnalysis(analysisFile)
 		if err != nil {
 			return nil, fmt.Errorf("failed to load analysis file %s: %w", analysisFile, err)
 		}
 	} else {
 		loadedAnalysis, err = loader.LoadLatestAnalysis(tag, analysisDir)
 		if err != nil {
 			return nil, fmt.Errorf("failed to load analysis for player %s from %s: %w", tag, analysisDir, err)
 		}
 	}
+	if loadedAnalysis == nil {
+		return nil, fmt.Errorf("loader returned nil analysis for player %s", tag)
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/cr-api/player_analysis_runtime.go` around lines 38 - 73, The helper
loadOfflineDeckPlayerData should guard against a loader returning (nil, nil):
after calling loader.LoadAnalysis or loader.LoadLatestAnalysis check if
loadedAnalysis == nil and return a clear error (e.g. fmt.Errorf("loader returned
nil CardAnalysis for tag %s, file/dir %s", tag, analysisFileOrDir)) instead of
dereferencing it; update the function to validate loadedAnalysis before using
loadedAnalysis.PlayerName/PlayerTag so offlineAnalysisLoader implementations
(and test stubs) that return nil don't cause a panic.
cmd/cr-api/deck_compare_suite_runtime.go (1)

122-137: LGTM — clean delegation, behavior preserved.

The refactor correctly forwards to loadOfflineDeckPlayerData(builder, tag, "", "", dataDir), which preserves the prior filepath.Join(dataDir, "analysis") defaulting and the tag-based fallback for empty PlayerName/PlayerTag.

Optional follow-up: suitePlayerData (lines 95–99) and offlineDeckPlayerData (in player_analysis_runtime.go) now have identical fields. Consider consolidating to a single type or having suitePlayerData embed/alias offlineDeckPlayerData to remove the field-by-field copy at lines 132–136.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/cr-api/deck_compare_suite_runtime.go` around lines 122 - 137,
suitePlayerData duplicates offlineDeckPlayerData fields and
loadSuitePlayerDataFromAnalysis performs a field-by-field copy; change to reuse
the existing type by either making suitePlayerData an alias or embedding
offlineDeckPlayerData (or returning *offlineDeckPlayerData directly) so you can
return the loaded struct without manual copying; update the suitePlayerData type
declaration (or function return type) and adjust any callers of
loadSuitePlayerDataFromAnalysis or references to
suitePlayerData/offlineDeckPlayerData to match the new single combined type
(refer to suitePlayerData, offlineDeckPlayerData, and
loadSuitePlayerDataFromAnalysis for locations to change).
cmd/cr-api/deck_builder_config_runtime.go (1)

255-265: Default analysis-dir resolution is now duplicated.

The filepath.Join(dataDir, "analysis") fallback on lines 259–262 mirrors the same defaulting inside loadOfflineDeckPlayerData (player_analysis_runtime.go lines 39–41). If the default location ever changes, both call sites need updating, defeating part of the purpose of the helper.

Consider returning the resolved source/path from loadOfflineDeckPlayerData (e.g., add a Source string field on offlineDeckPlayerData) so the verbose logging can use it without re-deriving the default.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/cr-api/deck_builder_config_runtime.go` around lines 255 - 265, The code
duplicates the default analysis-dir resolution; modify loadOfflineDeckPlayerData
so it returns the resolved analysis path by adding a Source string field to the
offlineDeckPlayerData (or returning the source as a second return value) and set
it to the actual directory used (falling back to filepath.Join(dataDir,
"analysis") inside that function); then update callers (including where
offlineDeckPlayerData is consumed in the verbose block in
deck_builder_config_runtime.go) to use offlineDeckPlayerData.Source for the
"Loaded analysis" log instead of recomputing the default, and update any
function signatures/usages accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@cmd/cr-api/deck_builder_config_runtime.go`:
- Around line 255-265: The code duplicates the default analysis-dir resolution;
modify loadOfflineDeckPlayerData so it returns the resolved analysis path by
adding a Source string field to the offlineDeckPlayerData (or returning the
source as a second return value) and set it to the actual directory used
(falling back to filepath.Join(dataDir, "analysis") inside that function); then
update callers (including where offlineDeckPlayerData is consumed in the verbose
block in deck_builder_config_runtime.go) to use offlineDeckPlayerData.Source for
the "Loaded analysis" log instead of recomputing the default, and update any
function signatures/usages accordingly.

In `@cmd/cr-api/deck_compare_suite_runtime.go`:
- Around line 122-137: suitePlayerData duplicates offlineDeckPlayerData fields
and loadSuitePlayerDataFromAnalysis performs a field-by-field copy; change to
reuse the existing type by either making suitePlayerData an alias or embedding
offlineDeckPlayerData (or returning *offlineDeckPlayerData directly) so you can
return the loaded struct without manual copying; update the suitePlayerData type
declaration (or function return type) and adjust any callers of
loadSuitePlayerDataFromAnalysis or references to
suitePlayerData/offlineDeckPlayerData to match the new single combined type
(refer to suitePlayerData, offlineDeckPlayerData, and
loadSuitePlayerDataFromAnalysis for locations to change).

In `@cmd/cr-api/player_analysis_runtime_test.go`:
- Around line 213-225: The test TestLoadOfflineDeckPlayerDataWrapsLoadErrors
should assert that the loader error is properly wrapped using errors.Is rather
than only checking the error message; update the test to import "errors" (and
add "strings" if you also assert the contextual prefix) and replace the current
string equality check with errors.Is(err, loader.loadLatestErr) to confirm
unwrapping, and optionally assert that err.Error() has the expected contextual
prefix (e.g., with strings.HasPrefix) to verify the helper added context.
- Around line 22-50: Add a unit test that verifies the explicit analysisDir path
is used (i.e., loadOfflineDeckPlayerData does not fall back to
filepath.Join(dataDir, "analysis") when a non-empty analysisDir is provided):
instantiate fakeOfflineAnalysisLoader, call loadOfflineDeckPlayerData (or the
wrapper exercised by tests) with a non-empty analysisDir value, then assert
fakeOfflineAnalysisLoader.latestDir equals that exact provided directory and
that latestCalls was incremented; reference fakeOfflineAnalysisLoader,
LoadLatestAnalysis, and loadOfflineDeckPlayerData when implementing the test.

In `@cmd/cr-api/player_analysis_runtime.go`:
- Around line 38-73: The helper loadOfflineDeckPlayerData should guard against a
loader returning (nil, nil): after calling loader.LoadAnalysis or
loader.LoadLatestAnalysis check if loadedAnalysis == nil and return a clear
error (e.g. fmt.Errorf("loader returned nil CardAnalysis for tag %s, file/dir
%s", tag, analysisFileOrDir)) instead of dereferencing it; update the function
to validate loadedAnalysis before using loadedAnalysis.PlayerName/PlayerTag so
offlineAnalysisLoader implementations (and test stubs) that return nil don't
cause a panic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7666fbc6-2db0-4577-9461-212c9bf3550f

📥 Commits

Reviewing files that changed from the base of the PR and between f36d74a and d5f4adf.

📒 Files selected for processing (4)
  • cmd/cr-api/deck_builder_config_runtime.go
  • cmd/cr-api/deck_compare_suite_runtime.go
  • cmd/cr-api/player_analysis_runtime.go
  • cmd/cr-api/player_analysis_runtime_test.go

@klauern klauern merged commit 0121e61 into main Apr 26, 2026
8 of 9 checks passed
@klauern klauern deleted the codex/refactor-offline-player-data-loader branch April 26, 2026 15:09
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.

1 participant