Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 58 additions & 15 deletions pkg/actionpins/spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,12 @@ func (r *testSHAResolver) ResolveSHA(ctx context.Context, repo, version string)
// TestSpec_PublicAPI_FormatPinnedActionReference validates the documented format "repo@sha # version".
func TestSpec_PublicAPI_FormatPinnedActionReference(t *testing.T) {
tests := []struct {
name string
repo string
sha string
version string
expected string
name string
repo string
sha string
version string
expected string
wantPanic string
}{
{
name: "formats standard reference",
Expand All @@ -63,10 +64,32 @@ func TestSpec_PublicAPI_FormatPinnedActionReference(t *testing.T) {
version: "v5",
expected: "actions/setup-go@cdabf2d4679a00bef48b5a7c69a9b8d0b4f6e3c9 # v5",
},
{
name: "formats reference with empty version comment",
repo: "actions/checkout",
sha: "abc123",
version: "",
expected: "actions/checkout@abc123 # ",
},
{
name: "panics for empty sha",
repo: "actions/checkout",

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] wantPanic and expected are mutually exclusive fields, but the table loop doesn't enforce that — a row with both set will silently skip the expected assertion.

💡 Suggested guard

Add a quick sanity check at the top of the panic branch:

if tt.wantPanic != "" {
    assert.Empty(t, tt.expected, "test case %q: wantPanic and expected are mutually exclusive", tt.name)
    require.PanicsWithValue(t, tt.wantPanic, func() {
        actionpins.FormatPinnedActionReference(tt.repo, tt.sha, tt.version)
    })
    return
}

This makes the mutual-exclusion contract explicit and catches copy-paste mistakes early.

@copilot please address this.

sha: "",
version: "v4",
wantPanic: "FormatPinnedActionReference called with empty SHA for repo=actions/checkout version=v4 — this would produce invalid workflow YAML",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.wantPanic != "" {
assert.Empty(t, tt.expected, "test case %q: wantPanic and expected are mutually exclusive", tt.name)
require.PanicsWithValue(t, tt.wantPanic, func() {
actionpins.FormatPinnedActionReference(tt.repo, tt.sha, tt.version)
})
return
}

result := actionpins.FormatPinnedActionReference(tt.repo, tt.sha, tt.version)
assert.Equal(t, tt.expected, result, "FormatPinnedActionReference(%q, %q, %q) should match spec format", tt.repo, tt.sha, tt.version)
})
Expand Down Expand Up @@ -516,15 +539,17 @@ func TestSpec_Types_ActionPinsData(t *testing.T) {
// TestSpec_PublicAPI_ResolveActionPin_EmbeddedMatch validates embedded-only pin resolution returns
// a formatted reference for a known repository. Spec: "Embedded-only lookup from bundled pin data"
func TestSpec_PublicAPI_ResolveActionPin_EmbeddedMatch(t *testing.T) {
known := "actions/checkout"
latestPin, ok := actionpins.GetLatestActionPinByRepo(known)
require.True(t, ok, "prerequisite: known repo must be in embedded data")
t.Run("returns formatted reference for known embedded pin", func(t *testing.T) {
known := "actions/checkout"
latestPin, ok := actionpins.GetLatestActionPinByRepo(known)
require.True(t, ok, "prerequisite: known repo must be in embedded data")

ctx := &actionpins.PinContext{StrictMode: false, Warnings: make(map[string]bool)}
result, err := actionpins.ResolveActionPin(known, latestPin.Version, ctx)
require.NoError(t, err, "embedded-only ResolveActionPin should not error for known pin")
require.NotEmpty(t, result, "should return non-empty pinned reference for known embedded pin")
assert.Contains(t, result, latestPin.SHA, "resolved reference should contain the pin SHA")
ctx := &actionpins.PinContext{StrictMode: false, Warnings: make(map[string]bool)}
result, err := actionpins.ResolveActionPin(known, latestPin.Version, ctx)
require.NoError(t, err, "embedded-only ResolveActionPin should not error for known pin")
require.NotEmpty(t, result, "should return non-empty pinned reference for known embedded pin")
assert.Contains(t, result, latestPin.SHA, "resolved reference should contain the pin SHA")
})
}

// TestSpec_DynamicResolution_VersionCommentConsistency validates that when dynamic resolution
Expand Down Expand Up @@ -751,7 +776,7 @@ func TestSpec_PublicAPI_ResolveActionPin_NilCtxField(t *testing.T) {
}
require.NotPanics(t, func() {
result, err := actionpins.ResolveActionPin("actions/checkout", "v4", ctx)
assert.NoError(t, err)
require.NoError(t, err)
assert.NotEmpty(t, result)
}, "nil PinContext.Ctx should fall back to context.Background() without panicking")
require.NotNil(t, resolver.capturedCtx, "resolver must receive a non-nil context even when PinContext.Ctx is nil")
Expand Down Expand Up @@ -856,6 +881,24 @@ func TestSpec_PublicAPI_ResolveActionPin_AppliesMapping(t *testing.T) {
assert.True(t, ctx.Warnings["map:actions/setup-node@v4"], "mapping notification key should be set in warnings")
})

t.Run("self-mapping preserves the same resolved reference", func(t *testing.T) {
baseline, err := actionpins.ResolveActionPin("actions/checkout", "v4", &actionpins.PinContext{
Warnings: make(map[string]bool),
})
require.NoError(t, err)

ctx := &actionpins.PinContext{
Warnings: make(map[string]bool),
Mappings: map[string]string{
"actions/checkout@v4": "actions/checkout@v4",
},
}
result, err := actionpins.ResolveActionPin("actions/checkout", "v4", ctx)
require.NoError(t, err)
assert.Equal(t, baseline, result, "self-mapping should behave the same as the unmapped case")
assert.True(t, ctx.Warnings["map:actions/checkout@v4"], "self-mapping should still record the mapping notification key")
})

t.Run("no mapping leaves resolution unchanged", func(t *testing.T) {
ctx := &actionpins.PinContext{Warnings: make(map[string]bool)}

Expand Down Expand Up @@ -895,7 +938,7 @@ func TestSpec_PublicAPI_ResolveActionPin_MappingTargetUnknown(t *testing.T) {

require.NotPanics(t, func() {
result, err := actionpins.ResolveActionPin("actions/checkout", "v4", ctx)
assert.NoError(t, err)
require.NoError(t, err)
assert.Empty(t, result, "mapping to unknown repo should produce unresolved empty result")
})
}
Loading