Skip to content

Commit da1c96e

Browse files
committed
address review comments
1 parent 6753263 commit da1c96e

2 files changed

Lines changed: 73 additions & 3 deletions

File tree

cmd/checkout.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,9 +248,11 @@ func checkoutRemoteStack(cfg *config.Config, sf *stack.StackFile, gitDir string,
248248
// Case A: branch is in a local stack — check composition
249249
if stackCompositionMatches(localStack, remoteStack.PRNumbers()) {
250250
// Composition matches — checkout
251-
if localStack.ID == "" {
252-
localStack.ID = remoteStackID
253-
}
251+
// remoteStack is authoritative for both identifiers here, so
252+
// refresh them together. Updating only one (e.g. the number while
253+
// keeping a stale ID) breaks later ID-based discovery when the old
254+
// remote stack was replaced by a new one holding the same PRs.
255+
localStack.ID = remoteStackID
254256
localStack.Number = remoteStack.Number
255257
if err := stack.Save(gitDir, sf); err != nil {
256258
return nil, "", handleSaveError(cfg, err)

internal/github/github_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package github
22

33
import (
4+
"encoding/json"
45
"testing"
56

67
graphql "github.com/cli/shurcooL-graphql"
78
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
810
)
911

1012
func TestPRURL(t *testing.T) {
@@ -80,3 +82,69 @@ func TestToGraphQLInt(t *testing.T) {
8082
assert.Error(t, err)
8183
})
8284
}
85+
86+
// TestRemoteStack_UnmarshalJSON verifies the custom decoder that flattens the
87+
// Stacks REST API's pull_requests object array into ordered PullRequests
88+
// numbers while preserving the full entries in PRDetails. This is the only path
89+
// that populates those fields from the wire, so a shape regression here would
90+
// silently break every stack endpoint.
91+
func TestRemoteStack_UnmarshalJSON(t *testing.T) {
92+
payload := `{
93+
"id": 6154,
94+
"number": 360,
95+
"node_id": "S_kwABCD",
96+
"url": "https://api.github.com/repos/o/r/stacks/360",
97+
"base": {"ref": "main", "sha": "basesha"},
98+
"open": true,
99+
"created_at": "2026-01-01T00:00:00Z",
100+
"pull_requests": [
101+
{"number": 12, "state": "open", "draft": true, "merged_at": null, "head": {"ref": "feat-1", "sha": "sha1"}},
102+
{"number": 15, "state": "closed", "draft": false, "merged_at": "2026-01-02T00:00:00Z", "head": {"ref": "feat-2", "sha": "sha2"}}
103+
]
104+
}`
105+
106+
var s RemoteStack
107+
require.NoError(t, json.Unmarshal([]byte(payload), &s))
108+
109+
// Top-level metadata.
110+
assert.Equal(t, 6154, s.ID)
111+
assert.Equal(t, 360, s.Number)
112+
assert.Equal(t, "S_kwABCD", s.NodeID)
113+
assert.Equal(t, "https://api.github.com/repos/o/r/stacks/360", s.URL)
114+
assert.Equal(t, "main", s.Base.Ref)
115+
assert.Equal(t, "basesha", s.Base.Sha)
116+
assert.True(t, s.Open)
117+
assert.Equal(t, "2026-01-01T00:00:00Z", s.CreatedAt)
118+
119+
// Ordered PR numbers (bottom to top) derived from the object array.
120+
assert.Equal(t, []int{12, 15}, s.PullRequests)
121+
assert.Equal(t, []int{12, 15}, s.PRNumbers())
122+
123+
// Full PR entries preserved in PRDetails, including nullable merged_at.
124+
require.Len(t, s.PRDetails, 2)
125+
assert.Equal(t, 12, s.PRDetails[0].Number)
126+
assert.Equal(t, "open", s.PRDetails[0].State)
127+
assert.True(t, s.PRDetails[0].Draft)
128+
assert.Nil(t, s.PRDetails[0].MergedAt)
129+
assert.False(t, s.PRDetails[0].IsMerged())
130+
assert.Equal(t, "feat-1", s.PRDetails[0].Head.Ref)
131+
assert.Equal(t, "sha1", s.PRDetails[0].Head.Sha)
132+
133+
assert.Equal(t, 15, s.PRDetails[1].Number)
134+
assert.Equal(t, "closed", s.PRDetails[1].State)
135+
require.NotNil(t, s.PRDetails[1].MergedAt)
136+
assert.Equal(t, "2026-01-02T00:00:00Z", *s.PRDetails[1].MergedAt)
137+
assert.True(t, s.PRDetails[1].IsMerged())
138+
assert.Equal(t, "feat-2", s.PRDetails[1].Head.Ref)
139+
}
140+
141+
// TestRemoteStack_UnmarshalJSON_EmptyPRs ensures a stack with no pull_requests
142+
// decodes to empty (non-nil) slices rather than panicking.
143+
func TestRemoteStack_UnmarshalJSON_EmptyPRs(t *testing.T) {
144+
var s RemoteStack
145+
require.NoError(t, json.Unmarshal([]byte(`{"id": 1, "number": 2, "pull_requests": []}`), &s))
146+
assert.Equal(t, 1, s.ID)
147+
assert.Equal(t, 2, s.Number)
148+
assert.Empty(t, s.PullRequests)
149+
assert.Empty(t, s.PRDetails)
150+
}

0 commit comments

Comments
 (0)