1717 * SOFTWARE.
1818 */
1919
20- const getLinearSecretMock = jest . fn ( ) ;
21- jest . mock ( '../../../src/handlers/shared/linear-verify ' , ( ) => ( {
22- getLinearSecret : ( ...args : unknown [ ] ) => getLinearSecretMock ( ...args ) ,
20+ const resolveLinearOauthTokenMock = jest . fn ( ) ;
21+ jest . mock ( '../../../src/handlers/shared/linear-oauth-resolver ' , ( ) => ( {
22+ resolveLinearOauthToken : ( ...args : unknown [ ] ) => resolveLinearOauthTokenMock ( ...args ) ,
2323} ) ) ;
2424
2525const fetchMock = jest . fn ( ) ;
@@ -28,13 +28,17 @@ const fetchMock = jest.fn();
2828
2929import {
3030 addIssueReaction ,
31+ type LinearFeedbackContext ,
3132 postIssueComment ,
3233 reportIssueFailure ,
3334} from '../../../src/handlers/shared/linear-feedback' ;
3435
35- const SECRET_ARN = 'arn:aws:secretsmanager:us-east-1:123:secret:bgagent/linear/api-token-XYZ' ;
36+ const CTX : LinearFeedbackContext = {
37+ linearWorkspaceId : 'ws-uuid-1' ,
38+ registryTableName : 'TestLinearWorkspaceRegistry' ,
39+ } ;
3640const ISSUE_ID = 'issue-1' ;
37- const TOKEN = 'lin_api_TESTTOKEN ' ;
41+ const TOKEN = 'lin_oauth_TESTTOKEN ' ;
3842
3943function jsonResponse ( body : unknown , status : number = 200 ) : Response {
4044 return {
@@ -46,34 +50,40 @@ function jsonResponse(body: unknown, status: number = 200): Response {
4650
4751describe ( 'linear-feedback' , ( ) => {
4852 beforeEach ( ( ) => {
49- getLinearSecretMock . mockReset ( ) ;
53+ resolveLinearOauthTokenMock . mockReset ( ) ;
5054 fetchMock . mockReset ( ) ;
51- getLinearSecretMock . mockResolvedValue ( TOKEN ) ;
55+ resolveLinearOauthTokenMock . mockResolvedValue ( {
56+ accessToken : TOKEN ,
57+ scope : 'read write' ,
58+ workspaceSlug : 'acme' ,
59+ oauthSecretArn : 'arn:secret:acme' ,
60+ } ) ;
5261 fetchMock . mockResolvedValue ( jsonResponse ( { data : { commentCreate : { success : true } } } ) ) ;
5362 } ) ;
5463
5564 describe ( 'postIssueComment' , ( ) => {
5665 test ( 'POSTs the commentCreate mutation with the issue id and body' , async ( ) => {
57- const ok = await postIssueComment ( SECRET_ARN , ISSUE_ID , '❌ blocked' ) ;
66+ const ok = await postIssueComment ( CTX , ISSUE_ID , '❌ blocked' ) ;
5867
5968 expect ( ok ) . toBe ( true ) ;
6069 expect ( fetchMock ) . toHaveBeenCalledTimes ( 1 ) ;
6170 const [ url , init ] = fetchMock . mock . calls [ 0 ] ;
6271 expect ( url ) . toBe ( 'https://api.linear.app/graphql' ) ;
6372 expect ( init . method ) . toBe ( 'POST' ) ;
6473 expect ( init . headers ) . toMatchObject ( {
65- 'Authorization' : TOKEN ,
74+ // OAuth tokens use Bearer prefix per Phase 2.0b-O2.
75+ 'Authorization' : `Bearer ${ TOKEN } ` ,
6676 'Content-Type' : 'application/json' ,
6777 } ) ;
6878 const body = JSON . parse ( init . body as string ) as { query : string ; variables : Record < string , string > } ;
6979 expect ( body . query ) . toContain ( 'commentCreate' ) ;
7080 expect ( body . variables ) . toEqual ( { issueId : ISSUE_ID , body : '❌ blocked' } ) ;
7181 } ) ;
7282
73- test ( 'returns false (and logs warn) when the secret cannot be resolved' , async ( ) => {
74- getLinearSecretMock . mockResolvedValueOnce ( null ) ;
83+ test ( 'returns false (and logs warn) when the token cannot be resolved' , async ( ) => {
84+ resolveLinearOauthTokenMock . mockResolvedValueOnce ( null ) ;
7585
76- const ok = await postIssueComment ( SECRET_ARN , ISSUE_ID , 'msg' ) ;
86+ const ok = await postIssueComment ( CTX , ISSUE_ID , 'msg' ) ;
7787
7888 expect ( ok ) . toBe ( false ) ;
7989 expect ( fetchMock ) . not . toHaveBeenCalled ( ) ;
@@ -82,31 +92,31 @@ describe('linear-feedback', () => {
8292 test ( 'returns false on non-2xx response (no throw)' , async ( ) => {
8393 fetchMock . mockResolvedValueOnce ( jsonResponse ( { } , 500 ) ) ;
8494
85- const ok = await postIssueComment ( SECRET_ARN , ISSUE_ID , 'msg' ) ;
95+ const ok = await postIssueComment ( CTX , ISSUE_ID , 'msg' ) ;
8696
8797 expect ( ok ) . toBe ( false ) ;
8898 } ) ;
8999
90100 test ( 'returns false on GraphQL errors (no throw)' , async ( ) => {
91101 fetchMock . mockResolvedValueOnce ( jsonResponse ( { errors : [ { message : 'auth' } ] } ) ) ;
92102
93- const ok = await postIssueComment ( SECRET_ARN , ISSUE_ID , 'msg' ) ;
103+ const ok = await postIssueComment ( CTX , ISSUE_ID , 'msg' ) ;
94104
95105 expect ( ok ) . toBe ( false ) ;
96106 } ) ;
97107
98108 test ( 'returns false on network failure (swallowed)' , async ( ) => {
99109 fetchMock . mockRejectedValueOnce ( new Error ( 'ECONNRESET' ) ) ;
100110
101- const ok = await postIssueComment ( SECRET_ARN , ISSUE_ID , 'msg' ) ;
111+ const ok = await postIssueComment ( CTX , ISSUE_ID , 'msg' ) ;
102112
103113 expect ( ok ) . toBe ( false ) ;
104114 } ) ;
105115
106- test ( 'returns false when getLinearSecret throws (swallowed at resolveToken layer)' , async ( ) => {
107- getLinearSecretMock . mockRejectedValueOnce ( new Error ( 'AccessDenied' ) ) ;
116+ test ( 'returns false when resolveLinearOauthToken throws (swallowed at resolveToken layer)' , async ( ) => {
117+ resolveLinearOauthTokenMock . mockRejectedValueOnce ( new Error ( 'AccessDenied' ) ) ;
108118
109- const ok = await postIssueComment ( SECRET_ARN , ISSUE_ID , 'msg' ) ;
119+ const ok = await postIssueComment ( CTX , ISSUE_ID , 'msg' ) ;
110120
111121 expect ( ok ) . toBe ( false ) ;
112122 expect ( fetchMock ) . not . toHaveBeenCalled ( ) ;
@@ -115,7 +125,7 @@ describe('linear-feedback', () => {
115125
116126 describe ( 'addIssueReaction' , ( ) => {
117127 test ( 'defaults to ❌ (emoji short-code "x")' , async ( ) => {
118- await addIssueReaction ( SECRET_ARN , ISSUE_ID ) ;
128+ await addIssueReaction ( CTX , ISSUE_ID ) ;
119129
120130 const init = fetchMock . mock . calls [ 0 ] [ 1 ] ;
121131 const body = JSON . parse ( init . body as string ) as { query : string ; variables : { emoji : string } } ;
@@ -124,7 +134,7 @@ describe('linear-feedback', () => {
124134 } ) ;
125135
126136 test ( 'honours an explicit emoji argument' , async ( ) => {
127- await addIssueReaction ( SECRET_ARN , ISSUE_ID , 'eyes' ) ;
137+ await addIssueReaction ( CTX , ISSUE_ID , 'eyes' ) ;
128138
129139 const init = fetchMock . mock . calls [ 0 ] [ 1 ] ;
130140 const body = JSON . parse ( init . body as string ) as { variables : { emoji : string } } ;
@@ -134,7 +144,7 @@ describe('linear-feedback', () => {
134144
135145 describe ( 'reportIssueFailure' , ( ) => {
136146 test ( 'posts comment + ❌ in parallel via Promise.allSettled' , async ( ) => {
137- await reportIssueFailure ( SECRET_ARN , ISSUE_ID , '❌ failed' ) ;
147+ await reportIssueFailure ( CTX , ISSUE_ID , '❌ failed' ) ;
138148
139149 expect ( fetchMock ) . toHaveBeenCalledTimes ( 2 ) ;
140150 const queries = fetchMock . mock . calls . map ( ( c ) => {
@@ -151,13 +161,13 @@ describe('linear-feedback', () => {
151161 . mockResolvedValueOnce ( jsonResponse ( { } , 500 ) )
152162 . mockResolvedValueOnce ( jsonResponse ( { data : { reactionCreate : { success : true } } } ) ) ;
153163
154- await expect ( reportIssueFailure ( SECRET_ARN , ISSUE_ID , 'msg' ) ) . resolves . toBeUndefined ( ) ;
164+ await expect ( reportIssueFailure ( CTX , ISSUE_ID , 'msg' ) ) . resolves . toBeUndefined ( ) ;
155165 } ) ;
156166
157167 test ( 'does not throw when both legs fail' , async ( ) => {
158168 fetchMock . mockRejectedValue ( new Error ( 'ECONNRESET' ) ) ;
159169
160- await expect ( reportIssueFailure ( SECRET_ARN , ISSUE_ID , 'msg' ) ) . resolves . toBeUndefined ( ) ;
170+ await expect ( reportIssueFailure ( CTX , ISSUE_ID , 'msg' ) ) . resolves . toBeUndefined ( ) ;
161171 } ) ;
162172 } ) ;
163173} ) ;
0 commit comments