@@ -33,6 +33,14 @@ vi.mock('../../../src/trello/client.js', () => ({
3333 } ,
3434} ) ) ;
3535
36+ // Mock linear client
37+ vi . mock ( '../../../src/linear/client.js' , ( ) => ( {
38+ withLinearCredentials : vi . fn ( async ( _creds : unknown , fn : ( ) => Promise < unknown > ) => fn ( ) ) ,
39+ linearClient : {
40+ createReaction : vi . fn ( ) ,
41+ } ,
42+ } ) ) ;
43+
3644// Mock logger
3745vi . mock ( '../../../src/utils/logging.js' , ( ) => ( {
3846 logger : {
@@ -50,6 +58,7 @@ import {
5058 getIntegrationCredential ,
5159} from '../../../src/config/provider.js' ;
5260import type { PersonaIdentities } from '../../../src/github/personas.js' ;
61+ import { linearClient , withLinearCredentials } from '../../../src/linear/client.js' ;
5362import { _resetJiraCloudIdCache , sendAcknowledgeReaction } from '../../../src/router/reactions.js' ;
5463import { trelloClient , withTrelloCredentials } from '../../../src/trello/client.js' ;
5564import type { ProjectConfig } from '../../../src/types/index.js' ;
@@ -61,6 +70,8 @@ const mockFindProjectByRepo = vi.mocked(findProjectByRepo);
6170const mockFindProjectById = vi . mocked ( findProjectById ) ;
6271const mockAddActionReaction = vi . mocked ( trelloClient . addActionReaction ) ;
6372const mockWithTrelloCredentials = vi . mocked ( withTrelloCredentials ) ;
73+ const mockCreateReaction = vi . mocked ( linearClient . createReaction ) ;
74+ const mockWithLinearCredentials = vi . mocked ( withLinearCredentials ) ;
6475const mockLogger = vi . mocked ( logger ) ;
6576
6677// Mock global fetch
@@ -146,6 +157,9 @@ describe('sendAcknowledgeReaction', () => {
146157 mockAddActionReaction . mockReset ( ) ;
147158 mockWithTrelloCredentials . mockReset ( ) ;
148159 mockWithTrelloCredentials . mockImplementation ( async ( _creds , fn ) => fn ( ) ) ;
160+ mockCreateReaction . mockReset ( ) ;
161+ mockWithLinearCredentials . mockReset ( ) ;
162+ mockWithLinearCredentials . mockImplementation ( async ( _creds , fn ) => fn ( ) ) ;
149163 _resetJiraCloudIdCache ( ) ;
150164 mockLogger . info . mockReset ( ) ;
151165 mockLogger . warn . mockReset ( ) ;
@@ -601,6 +615,88 @@ describe('sendAcknowledgeReaction', () => {
601615 } ) ;
602616 } ) ;
603617
618+ // -------------------------------------------------------------------------
619+ // Linear
620+ // -------------------------------------------------------------------------
621+
622+ describe ( 'Linear reactions' , ( ) => {
623+ const LINEAR_COMMENT_PAYLOAD = {
624+ type : 'Comment' ,
625+ action : 'create' ,
626+ data : { id : 'comment-linear-123' } ,
627+ } ;
628+
629+ it ( 'sends 👀 reaction for Comment.create event' , async ( ) => {
630+ mockCreateReaction . mockResolvedValueOnce ( {
631+ id : 'reaction-1' ,
632+ emoji : '👀' ,
633+ user : null ,
634+ createdAt : '2026-01-01T00:00:00Z' ,
635+ } ) ;
636+
637+ await sendAcknowledgeReaction ( 'linear' , PROJECT_ID , LINEAR_COMMENT_PAYLOAD ) ;
638+
639+ expect ( mockCreateReaction ) . toHaveBeenCalledOnce ( ) ;
640+ expect ( mockCreateReaction ) . toHaveBeenCalledWith ( 'comment-linear-123' , '👀' ) ;
641+ } ) ;
642+
643+ it ( 'skips reaction for non-comment Linear events (e.g. Issue.update)' , async ( ) => {
644+ const payload = { type : 'Issue' , action : 'update' , data : { id : 'issue-abc' } } ;
645+
646+ await sendAcknowledgeReaction ( 'linear' , PROJECT_ID , payload ) ;
647+
648+ expect ( mockCreateReaction ) . not . toHaveBeenCalled ( ) ;
649+ } ) ;
650+
651+ it ( 'skips reaction for Comment events that are not create (e.g. Comment.update)' , async ( ) => {
652+ const payload = { type : 'Comment' , action : 'update' , data : { id : 'comment-xyz' } } ;
653+
654+ await sendAcknowledgeReaction ( 'linear' , PROJECT_ID , payload ) ;
655+
656+ expect ( mockCreateReaction ) . not . toHaveBeenCalled ( ) ;
657+ } ) ;
658+
659+ it ( 'skips reaction when Linear credentials are missing and logs warning' , async ( ) => {
660+ mockGetIntegrationCredential . mockRejectedValue ( new Error ( 'Credential not found' ) ) ;
661+
662+ await sendAcknowledgeReaction ( 'linear' , PROJECT_ID , LINEAR_COMMENT_PAYLOAD ) ;
663+
664+ expect ( mockCreateReaction ) . not . toHaveBeenCalled ( ) ;
665+ expect ( mockLogger . warn ) . toHaveBeenCalledWith (
666+ expect . stringContaining ( 'Missing Linear credentials' ) ,
667+ ) ;
668+ } ) ;
669+
670+ it ( 'does not throw when Linear credentials are missing' , async ( ) => {
671+ mockGetIntegrationCredential . mockRejectedValue ( new Error ( 'Credential not found' ) ) ;
672+
673+ await expect (
674+ sendAcknowledgeReaction ( 'linear' , PROJECT_ID , LINEAR_COMMENT_PAYLOAD ) ,
675+ ) . resolves . toBeUndefined ( ) ;
676+ } ) ;
677+
678+ it ( 'logs warning on Linear API error but does not throw' , async ( ) => {
679+ mockCreateReaction . mockRejectedValueOnce ( new Error ( 'Linear API error: 403' ) ) ;
680+
681+ await expect (
682+ sendAcknowledgeReaction ( 'linear' , PROJECT_ID , LINEAR_COMMENT_PAYLOAD ) ,
683+ ) . resolves . toBeUndefined ( ) ;
684+
685+ expect ( mockLogger . warn ) . toHaveBeenCalledWith (
686+ expect . stringContaining ( 'Linear reaction failed' ) ,
687+ expect . stringContaining ( '403' ) ,
688+ ) ;
689+ } ) ;
690+
691+ it ( 'skips reaction when comment id is missing from payload data' , async ( ) => {
692+ const payload = { type : 'Comment' , action : 'create' , data : { } } ;
693+
694+ await sendAcknowledgeReaction ( 'linear' , PROJECT_ID , payload ) ;
695+
696+ expect ( mockCreateReaction ) . not . toHaveBeenCalled ( ) ;
697+ } ) ;
698+ } ) ;
699+
604700 // -------------------------------------------------------------------------
605701 // Error handling (top-level)
606702 // -------------------------------------------------------------------------
0 commit comments