Overview
File: pkg/actionpins/spec_test.go
Source pair: pkg/actionpins/actionpins.go
Test count: 29 top-level Test* functions (820 LOC)
Internal tests: actionpins_internal_test.go — 24 functions (562 LOC)
Strengths
- Excellent coverage of happy paths and error modes for
ResolveActionPin and ResolveLatestActionPin.
- Good use of
require.* for prerequisites/fatal guards and assert.* for non-fatal checks.
- Table-driven tests where appropriate.
- Concurrency test covers the
sync.Once guarantee.
- Context propagation test is precise and well-structured.
Prioritized Improvements
1. Missing / High-Value Tests
Details
a) EnforcePinned=true + resolver succeeds
The TestSpec_PublicAPI_ResolveActionPin_EnforcePinned table covers failure scenarios but never tests the path where EnforcePinned=true and the dynamic resolver returns a valid SHA — the most important production path for enforce mode.
// After: add a success case to the existing table
{
name: "dynamic resolver succeeds with EnforcePinned=true returns pinned reference",
resolver: &testSHAResolver{sha: testResolvedSHA},
wantErr: false,
},
b) Dynamic resolver returns empty SHA ("", nil)
The dynamic path silently falls through when the resolver returns an empty SHA with no error. This edge case is not covered:
func TestSpec_DynamicResolution_EmptySHAFallsThrough(t *testing.T) {
ctx := &actionpins.PinContext{
Resolver: &testSHAResolver{sha: "", err: nil},
Warnings: make(map[string]bool),
}
result, err := actionpins.ResolveActionPin("actions/checkout", "v4", ctx)
require.NoError(t, err)
assert.NotEmpty(t, result, "empty SHA from resolver should fall back to hardcoded pins")
}
c) GetContainerPin — missing image@sha256: format assertion
TestSpec_PublicAPI_GetContainerPin verifies that pin.PinnedImage is non-empty but does not check the image@sha256:digest format:
// Add after the existing NotEmpty check:
assert.Truef(t, strings.Contains(pin.PinnedImage, "`@sha256`:"),
"PinnedImage should be in image@sha256:digest format, got %q", pin.PinnedImage)
d) ctx.Ctx == nil falls back to context.Background()
TestSpec_PublicAPI_ResolveActionPin_UsesProvidedContext verifies an explicit context but no test verifies that a nil ctx.Ctx still calls the resolver without panicking.
2. Testify Assertion Upgrades
Details
Inconsistent assert.Truef vs assert.True usage (lines 473-478):
// Before:
assert.Truef(t, strings.HasPrefix(cacheKey, repo+"@"), "cache key should be repo@version, got %q", cacheKey)
assert.Truef(t, strings.HasPrefix(reference, repo+"@"), "reference should start with repo@sha, got %q", reference)
assert.Contains(t, cacheKey, version, "cache key should contain version")
// After (consistent — use f-variants only when the message has format verbs):
assert.Truef(t, strings.HasPrefix(cacheKey, repo+"@"), "cache key should be repo@version, got %q", cacheKey)
assert.Truef(t, strings.HasPrefix(reference, repo+"@"), "reference should start with repo@sha, got %q", reference)
assert.Containsf(t, cacheKey, version, "cache key should contain version")
assert.Containsf(t, reference, sha, "reference should contain sha")
assert.Containsf(t, reference, version, "reference should contain version comment")
Prefer assert.ErrorIs over assert.Contains(t, err.Error(), ...):
In TestSpec_PublicAPI_ResolveActionPin_EnforcePinned, errors are matched by substring. If the error type becomes a sentinel, prefer assert.ErrorIs; at minimum add a code comment explaining why string matching is used.
3. Table-Driven Refactors
Details
TestSpec_PublicAPI_RecordResolutionFailure and TestSpec_PublicAPI_RecordResolutionFailure_DynamicFailed share identical fixture structure (failures slice, PinContext, resolve call, length+type assertions). Merge into one table:
tests := []struct {
name string
resolver actionpins.SHAResolver
wantErrorType actionpins.ResolutionErrorType
}{
{
name: "no resolver classified as pin_not_found",
resolver: nil,
wantErrorType: actionpins.ResolutionErrorTypePinNotFound,
},
{
name: "failing resolver classified as dynamic_resolution_failed",
resolver: &testSHAResolver{err: errors.New("network error")},
wantErrorType: actionpins.ResolutionErrorTypeDynamicResolutionFailed,
},
}
TestSpec_PublicAPI_ResolveActionPin_SkipHardcodedFallback — the three sub-tests share the same PinContext + ResolveActionPin + assertion pattern and are candidates for a struct table.
4. Organisation / Readability
Details
testSHAResolver lacks a doc comment explaining its captured fields (capturedCtx, capturedRepo, capturedRef). Add one to clarify their role in context-propagation assertions.
testContextPropagationKey and testResolvedSHA constants are only referenced in two tests. Add a grouping comment or move them adjacent to their consumers.
TestSpec_Types_ActionPinsData is a struct-construction smoke test, not a behaviour test. Rename to TestSpec_Types_ActionPinsData_ConstructsCorrectly and add a one-line comment stating intent.
Acceptance Checklist
Generated by 🧪 Daily Testify Uber Super Expert · 47.6 AIC · ⌖ 11.6 AIC · ⊞ 5.2K · ◷
Overview
File:
pkg/actionpins/spec_test.goSource pair:
pkg/actionpins/actionpins.goTest count: 29 top-level
Test*functions (820 LOC)Internal tests:
actionpins_internal_test.go— 24 functions (562 LOC)Strengths
ResolveActionPinandResolveLatestActionPin.require.*for prerequisites/fatal guards andassert.*for non-fatal checks.sync.Onceguarantee.Prioritized Improvements
1. Missing / High-Value Tests
Details
a)
EnforcePinned=true+ resolver succeedsThe
TestSpec_PublicAPI_ResolveActionPin_EnforcePinnedtable covers failure scenarios but never tests the path whereEnforcePinned=trueand the dynamic resolver returns a valid SHA — the most important production path for enforce mode.b) Dynamic resolver returns empty SHA (
"", nil)The dynamic path silently falls through when the resolver returns an empty SHA with no error. This edge case is not covered:
c)
GetContainerPin— missingimage@sha256:format assertionTestSpec_PublicAPI_GetContainerPinverifies thatpin.PinnedImageis non-empty but does not check theimage@sha256:digestformat:d)
ctx.Ctx == nilfalls back tocontext.Background()TestSpec_PublicAPI_ResolveActionPin_UsesProvidedContextverifies an explicit context but no test verifies that a nilctx.Ctxstill calls the resolver without panicking.2. Testify Assertion Upgrades
Details
Inconsistent
assert.Truefvsassert.Trueusage (lines 473-478):Prefer
assert.ErrorIsoverassert.Contains(t, err.Error(), ...):In
TestSpec_PublicAPI_ResolveActionPin_EnforcePinned, errors are matched by substring. If the error type becomes a sentinel, preferassert.ErrorIs; at minimum add a code comment explaining why string matching is used.3. Table-Driven Refactors
Details
TestSpec_PublicAPI_RecordResolutionFailureandTestSpec_PublicAPI_RecordResolutionFailure_DynamicFailedshare identical fixture structure (failures slice, PinContext, resolve call, length+type assertions). Merge into one table:TestSpec_PublicAPI_ResolveActionPin_SkipHardcodedFallback— the three sub-tests share the samePinContext+ResolveActionPin+ assertion pattern and are candidates for a struct table.4. Organisation / Readability
Details
testSHAResolverlacks a doc comment explaining its captured fields (capturedCtx,capturedRepo,capturedRef). Add one to clarify their role in context-propagation assertions.testContextPropagationKeyandtestResolvedSHAconstants are only referenced in two tests. Add a grouping comment or move them adjacent to their consumers.TestSpec_Types_ActionPinsDatais a struct-construction smoke test, not a behaviour test. Rename toTestSpec_Types_ActionPinsData_ConstructsCorrectlyand add a one-line comment stating intent.Acceptance Checklist
EnforcePinned=true+ resolver-succeeds case toTestSpec_PublicAPI_ResolveActionPin_EnforcePinnedTestSpec_DynamicResolution_EmptySHAFallsThroughfor empty-SHA resolver path@sha256:format assertion toTestSpec_PublicAPI_GetContainerPinctx.Ctxresolver test (default context fallback without panic)assert.Truef/assert.ContainsfinTestSpec_DesignDecision_FormatConsistencyTestSpec_PublicAPI_RecordResolutionFailure*pair into a table-driven testtestSHAResolvermake test-unit