From 3225661d093f77c6385ea79658819f94d922d554 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 22 Jun 2026 11:03:29 +0000 Subject: [PATCH] fix(linear): use oauth auth header for idempotent creates Co-authored-by: Kev Glynn --- internal/linear/client.go | 6 +- internal/linear/idempotency_test.go | 108 ++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/internal/linear/client.go b/internal/linear/client.go index 075b84a3b2..e5e3c9d32a 100644 --- a/internal/linear/client.go +++ b/internal/linear/client.go @@ -748,7 +748,11 @@ func (c *Client) createIssueSingleAttempt(ctx context.Context, title, descriptio } httpReq.Header.Set("Content-Type", "application/json") - httpReq.Header.Set("Authorization", c.APIKey) + authValue, err := c.authHeader() + if err != nil { + return nil, err + } + httpReq.Header.Set("Authorization", authValue) resp, err := c.HTTPClient.Do(httpReq) if err != nil { diff --git a/internal/linear/idempotency_test.go b/internal/linear/idempotency_test.go index 6d41d4ca1c..3f7a071fcf 100644 --- a/internal/linear/idempotency_test.go +++ b/internal/linear/idempotency_test.go @@ -7,6 +7,7 @@ import ( "net/http/httptest" "strings" "testing" + "time" ) func TestGenerateIdempotencyMarker(t *testing.T) { @@ -376,3 +377,110 @@ func TestCreateIssueIdempotentRecoverAfterAmbiguousFailure(t *testing.T) { t.Errorf("find calls = %d, want 2 (initial check + recovery check)", handler.findCallCount) } } + +type oauthAuthHeaderHandler struct { + t *testing.T + searchCalls int + createCalls int +} + +func (h *oauthAuthHeaderHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if got := r.Header.Get("Authorization"); got != "Bearer oauth-token" { + h.t.Fatalf("Authorization header = %q, want %q", got, "Bearer oauth-token") + } + + var req GraphQLRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + h.t.Fatalf("failed to decode request: %v", err) + } + + w.Header().Set("Content-Type", "application/json") + + if strings.Contains(req.Query, "FindByDescription") { + h.searchCalls++ + resp := map[string]interface{}{ + "data": map[string]interface{}{ + "issues": map[string]interface{}{ + "nodes": []Issue{}, + "pageInfo": map[string]interface{}{"hasNextPage": false, "endCursor": ""}, + }, + }, + } + _ = json.NewEncoder(w).Encode(resp) + return + } + + if strings.Contains(req.Query, "issueCreate") { + h.createCalls++ + input, _ := req.Variables["input"].(map[string]interface{}) + resp := map[string]interface{}{ + "data": map[string]interface{}{ + "issueCreate": map[string]interface{}{ + "success": true, + "issue": map[string]interface{}{ + "id": "oauth-created-uuid", + "identifier": "TEAM-401", + "title": input["title"], + "description": input["description"], + "url": "https://linear.app/team/issue/TEAM-401", + "priority": input["priority"], + "state": map[string]interface{}{ + "id": "state-1", + "name": "Todo", + "type": "unstarted", + }, + "createdAt": "2026-05-01T10:00:00Z", + "updatedAt": "2026-05-01T10:00:00Z", + }, + }, + }, + } + _ = json.NewEncoder(w).Encode(resp) + return + } + + h.t.Fatalf("unexpected query: %s", req.Query) +} + +func TestCreateIssueIdempotentOAuthUsesBearerAuth(t *testing.T) { + handler := &oauthAuthHeaderHandler{t: t} + server := httptest.NewServer(handler) + defer server.Close() + + client := &Client{ + TeamID: "team-1", + Endpoint: server.URL, + HTTPClient: server.Client(), + AuthMode: AuthModeOAuth, + TokenManager: &OAuthTokenManager{ + token: "oauth-token", + expiresAt: time.Now().Add(time.Hour), + nowFunc: time.Now, + client: server.Client(), + }, + } + + marker := GenerateIdempotencyMarker("bead-oauth", "ci@test.com", 123) + issue, deduped, err := client.CreateIssueIdempotent( + context.Background(), + "OAuth Created Issue", + "description", + 2, "", nil, + marker, + ) + if err != nil { + t.Fatalf("CreateIssueIdempotent failed in OAuth mode: %v", err) + } + if deduped { + t.Error("expected deduped=false for fresh OAuth create") + } + if issue == nil || issue.Identifier != "TEAM-401" { + t.Fatalf("unexpected issue result: %+v", issue) + } + if handler.searchCalls != 1 { + t.Errorf("search calls = %d, want 1", handler.searchCalls) + } + if handler.createCalls != 1 { + t.Errorf("create calls = %d, want 1", handler.createCalls) + } +}