@@ -19,6 +19,7 @@ jest.mock('../../src/integrations/yggdrasil/clients', () => ({
1919 yggdrasilSentimentClient : {
2020 getTimeSeries : jest . fn ( ) ,
2121 getHighlights : jest . fn ( ) ,
22+ getTopEntities : jest . fn ( ) ,
2223 } ,
2324} ) ) ;
2425
@@ -30,6 +31,10 @@ const getHighlightsMock =
3031 yggdrasilSentimentClient . getHighlights as jest . MockedFunction <
3132 typeof yggdrasilSentimentClient . getHighlights
3233 > ;
34+ const getTopEntitiesMock =
35+ yggdrasilSentimentClient . getTopEntities as jest . MockedFunction <
36+ typeof yggdrasilSentimentClient . getTopEntities
37+ > ;
3338
3439let con : DataSource ;
3540let state : GraphQLTestingState ;
@@ -87,6 +92,7 @@ describe('query sentimentTimeSeries', () => {
8792 scores
8893 volume
8994 scoreVariance
95+ dIndex
9096 }
9197 }
9298 }
@@ -103,6 +109,7 @@ describe('query sentimentTimeSeries', () => {
103109 s : [ 0.5 , - 0.2 ] ,
104110 v : [ 4 , 3 ] ,
105111 sv : [ 0.25 , 1 ] ,
112+ d : [ 1.1 , 0.8 ] ,
106113 } ,
107114 } ,
108115 } ) ;
@@ -134,6 +141,7 @@ describe('query sentimentTimeSeries', () => {
134141 scores : [ 0.5 , - 0.2 ] ,
135142 volume : [ 4 , 3 ] ,
136143 scoreVariance : [ 0.25 , 1 ] ,
144+ dIndex : [ 1.1 , 0.8 ] ,
137145 } ,
138146 ] ,
139147 } ,
@@ -604,3 +612,208 @@ describe('query sentimentGroup', () => {
604612 } ) ;
605613 } ) ;
606614} ) ;
615+
616+ describe ( 'query topSentimentEntities' , ( ) => {
617+ const QUERY = /* GraphQL */ `
618+ query TopSentimentEntities(
619+ $groupId: ID!
620+ $resolution: SentimentResolution!
621+ $lookback: String
622+ $limit: Int
623+ ) {
624+ topSentimentEntities(
625+ groupId: $groupId
626+ resolution: $resolution
627+ lookback: $lookback
628+ limit: $limit
629+ ) {
630+ dIndex
631+ score
632+ volume
633+ entity {
634+ entity
635+ name
636+ logo
637+ }
638+ }
639+ }
640+ ` ;
641+
642+ it ( 'should return top entities with sentiment metadata' , async ( ) => {
643+ await con . getRepository ( SentimentGroup ) . insert ( {
644+ id : '385404b4-f0f4-4e81-a338-bdca851eca31' ,
645+ name : 'Coding Agents' ,
646+ } ) ;
647+ await con . getRepository ( SentimentEntity ) . insert ( [
648+ {
649+ groupId : '385404b4-f0f4-4e81-a338-bdca851eca31' ,
650+ entity : 'cursor' ,
651+ name : 'Cursor' ,
652+ logo : 'https://media.daily.dev/image/upload/public/cursor' ,
653+ } ,
654+ {
655+ groupId : '385404b4-f0f4-4e81-a338-bdca851eca31' ,
656+ entity : 'copilot' ,
657+ name : 'Copilot' ,
658+ logo : 'https://media.daily.dev/image/upload/public/copilot' ,
659+ } ,
660+ ] ) ;
661+
662+ getTopEntitiesMock . mockResolvedValue ( {
663+ entities : [
664+ { entity : 'cursor' , d_index : 12.4 , score : 0.7 , volume : 20 } ,
665+ { entity : 'copilot' , d_index : 8.2 , score : 0.4 , volume : 18 } ,
666+ ] ,
667+ } ) ;
668+
669+ const res = await client . query ( QUERY , {
670+ variables : {
671+ groupId : '385404b4-f0f4-4e81-a338-bdca851eca31' ,
672+ resolution : 'HOUR' ,
673+ lookback : '24h' ,
674+ limit : 10 ,
675+ } ,
676+ } ) ;
677+
678+ expect ( res . errors ) . toBeFalsy ( ) ;
679+ expect ( getTopEntitiesMock ) . toHaveBeenCalledWith ( {
680+ groupId : '385404b4-f0f4-4e81-a338-bdca851eca31' ,
681+ resolution : '1h' ,
682+ lookback : '24h' ,
683+ limit : 10 ,
684+ } ) ;
685+ expect ( res . data . topSentimentEntities ) . toEqual ( [
686+ {
687+ dIndex : 12.4 ,
688+ score : 0.7 ,
689+ volume : 20 ,
690+ entity : {
691+ entity : 'cursor' ,
692+ name : 'Cursor' ,
693+ logo : 'https://media.daily.dev/image/upload/public/cursor' ,
694+ } ,
695+ } ,
696+ {
697+ dIndex : 8.2 ,
698+ score : 0.4 ,
699+ volume : 18 ,
700+ entity : {
701+ entity : 'copilot' ,
702+ name : 'Copilot' ,
703+ logo : 'https://media.daily.dev/image/upload/public/copilot' ,
704+ } ,
705+ } ,
706+ ] ) ;
707+ } ) ;
708+
709+ it ( 'should default limit to 20 and filter entities missing from DB' , async ( ) => {
710+ await con . getRepository ( SentimentGroup ) . insert ( {
711+ id : '385404b4-f0f4-4e81-a338-bdca851eca31' ,
712+ name : 'Coding Agents' ,
713+ } ) ;
714+ await con . getRepository ( SentimentEntity ) . insert ( {
715+ groupId : '385404b4-f0f4-4e81-a338-bdca851eca31' ,
716+ entity : 'cursor' ,
717+ name : 'Cursor' ,
718+ logo : 'https://media.daily.dev/image/upload/public/cursor' ,
719+ } ) ;
720+
721+ getTopEntitiesMock . mockResolvedValue ( {
722+ entities : [
723+ { entity : 'cursor' , d_index : 12.4 , score : 0.7 , volume : 20 } ,
724+ { entity : 'missing' , d_index : 8.2 , score : 0.4 , volume : 18 } ,
725+ ] ,
726+ } ) ;
727+
728+ const res = await client . query ( QUERY , {
729+ variables : {
730+ groupId : '385404b4-f0f4-4e81-a338-bdca851eca31' ,
731+ resolution : 'DAY' ,
732+ } ,
733+ } ) ;
734+
735+ expect ( res . errors ) . toBeFalsy ( ) ;
736+ expect ( getTopEntitiesMock ) . toHaveBeenCalledWith ( {
737+ groupId : '385404b4-f0f4-4e81-a338-bdca851eca31' ,
738+ resolution : '1d' ,
739+ lookback : undefined ,
740+ limit : 20 ,
741+ } ) ;
742+ expect ( res . data . topSentimentEntities ) . toEqual ( [
743+ {
744+ dIndex : 12.4 ,
745+ score : 0.7 ,
746+ volume : 20 ,
747+ entity : {
748+ entity : 'cursor' ,
749+ name : 'Cursor' ,
750+ logo : 'https://media.daily.dev/image/upload/public/cursor' ,
751+ } ,
752+ } ,
753+ ] ) ;
754+ } ) ;
755+
756+ it ( 'should validate limit range' , async ( ) => {
757+ await testQueryErrorCode (
758+ client ,
759+ {
760+ query : QUERY ,
761+ variables : {
762+ groupId : '385404b4-f0f4-4e81-a338-bdca851eca31' ,
763+ resolution : 'HOUR' ,
764+ limit : 0 ,
765+ } ,
766+ } ,
767+ 'GRAPHQL_VALIDATION_FAILED' ,
768+ 'limit must be between 1 and 50' ,
769+ ) ;
770+ } ) ;
771+
772+ it ( 'should map 404 to NOT_FOUND' , async ( ) => {
773+ getTopEntitiesMock . mockRejectedValue (
774+ new HttpError (
775+ 'http://localhost:3002/api/sentiment/top-entities' ,
776+ 404 ,
777+ 'not found' ,
778+ ) ,
779+ ) ;
780+
781+ await testQueryErrorCode (
782+ client ,
783+ {
784+ query : QUERY ,
785+ variables : {
786+ groupId : '385404b4-f0f4-4e81-a338-bdca851eca31' ,
787+ resolution : 'QUARTER_HOUR' ,
788+ } ,
789+ } ,
790+ 'NOT_FOUND' ,
791+ ) ;
792+ } ) ;
793+
794+ it ( 'should enforce shared 30/min rate limit for top entities query' , async ( ) => {
795+ getTopEntitiesMock . mockResolvedValue ( { entities : [ ] } ) ;
796+
797+ for ( let attempt = 0 ; attempt < 30 ; attempt += 1 ) {
798+ const response = await client . query ( QUERY , {
799+ variables : {
800+ groupId : '385404b4-f0f4-4e81-a338-bdca851eca31' ,
801+ resolution : 'HOUR' ,
802+ } ,
803+ } ) ;
804+ expect ( response . errors ) . toBeFalsy ( ) ;
805+ }
806+
807+ await testQueryErrorCode (
808+ client ,
809+ {
810+ query : QUERY ,
811+ variables : {
812+ groupId : '385404b4-f0f4-4e81-a338-bdca851eca31' ,
813+ resolution : 'HOUR' ,
814+ } ,
815+ } ,
816+ 'RATE_LIMITED' ,
817+ ) ;
818+ } ) ;
819+ } ) ;
0 commit comments