11import { DisplayType } from 'sentry/views/dashboards/types' ;
22
3- import { getWidgetQueryLLMHint } from './widgetLLMContext' ;
3+ import { getWidgetQueryLLMHint , readableConditions } from './widgetLLMContext' ;
44
55describe ( 'getWidgetQueryLLMHint' , ( ) => {
66 it . each ( [
@@ -26,3 +26,125 @@ describe('getWidgetQueryLLMHint', () => {
2626 expect ( getWidgetQueryLLMHint ( DisplayType . WHEEL ) ) . toContain ( 'table query' ) ;
2727 } ) ;
2828} ) ;
29+
30+ describe ( 'readableConditions' , ( ) => {
31+ it ( 'replaces Contains operator with readable label' , ( ) => {
32+ expect ( readableConditions ( 'span.name:\uf00dContains\uf00dfoo' ) ) . toBe (
33+ 'span.name: contains foo'
34+ ) ;
35+ } ) ;
36+
37+ it ( 'replaces Contains with IN list brackets' , ( ) => {
38+ expect ( readableConditions ( 'span.name:\uf00dContains\uf00d[a,b,c]' ) ) . toBe (
39+ 'span.name: contains [a,b,c]'
40+ ) ;
41+ } ) ;
42+
43+ it ( 'replaces DoesNotContain operator' , ( ) => {
44+ expect ( readableConditions ( 'key:\uf00dDoesNotContain\uf00dval' ) ) . toBe (
45+ 'key: does not contain val'
46+ ) ;
47+ } ) ;
48+
49+ it ( 'replaces StartsWith and EndsWith operators' , ( ) => {
50+ expect ( readableConditions ( 'key:\uf00dStartsWith\uf00d/api' ) ) . toBe (
51+ 'key: starts with /api'
52+ ) ;
53+ expect ( readableConditions ( 'key:\uf00dEndsWith\uf00d.json' ) ) . toBe (
54+ 'key: ends with .json'
55+ ) ;
56+ } ) ;
57+
58+ it ( 'replaces DoesNotStartWith and DoesNotEndWith operators' , ( ) => {
59+ expect ( readableConditions ( 'key:\uf00dDoesNotStartWith\uf00d/api' ) ) . toBe (
60+ 'key: does not start with /api'
61+ ) ;
62+ expect ( readableConditions ( 'key:\uf00dDoesNotEndWith\uf00d.json' ) ) . toBe (
63+ 'key: does not end with .json'
64+ ) ;
65+ } ) ;
66+
67+ it ( 'preserves negated filter prefix' , ( ) => {
68+ expect ( readableConditions ( '!path:\uf00dContains\uf00dfoo' ) ) . toBe (
69+ '!path: contains foo'
70+ ) ;
71+ } ) ;
72+
73+ it ( 'replaces multiple operators in one string' , ( ) => {
74+ const input =
75+ 'span.name:\uf00dContains\uf00dqueue.task !trigger_path:\uf00dContains\uf00dold_seer' ;
76+ expect ( readableConditions ( input ) ) . toBe (
77+ 'span.name: contains queue.task !trigger_path: contains old_seer'
78+ ) ;
79+ } ) ;
80+
81+ it ( 'passes through plain filters unchanged' , ( ) => {
82+ expect ( readableConditions ( 'browser.name:Firefox' ) ) . toBe ( 'browser.name:Firefox' ) ;
83+ } ) ;
84+
85+ it ( 'passes through free text unchanged' , ( ) => {
86+ expect ( readableConditions ( 'some free text' ) ) . toBe ( 'some free text' ) ;
87+ } ) ;
88+
89+ it ( 'passes through empty string' , ( ) => {
90+ expect ( readableConditions ( '' ) ) . toBe ( '' ) ;
91+ } ) ;
92+
93+ it ( 'preserves OR and parentheses' , ( ) => {
94+ expect ( readableConditions ( '(a:1 OR b:2) error' ) ) . toBe ( '(a:1 OR b:2) error' ) ;
95+ } ) ;
96+
97+ it ( 'preserves comparison operators' , ( ) => {
98+ expect ( readableConditions ( 'count():>100 duration:<=5s' ) ) . toBe (
99+ 'count():>100 duration:<=5s'
100+ ) ;
101+ } ) ;
102+
103+ it ( 'handles real-world widget query' , ( ) => {
104+ const input =
105+ 'span.description:\uf00dContains\uf00d[sentry.tasks.autofix.generate_issue_summary_only,sentry.tasks.autofix.run_automation_only_task] span.name:\uf00dContains\uf00dqueue.task.taskworker !trigger_path:\uf00dContains\uf00dold_seer_automation' ;
106+ expect ( readableConditions ( input ) ) . toBe (
107+ 'span.description: contains [sentry.tasks.autofix.generate_issue_summary_only,sentry.tasks.autofix.run_automation_only_task] span.name: contains queue.task.taskworker !trigger_path: contains old_seer_automation'
108+ ) ;
109+ } ) ;
110+
111+ it ( 'does not replace DoesNotContain partially as Contains' , ( ) => {
112+ // DoesNotContain must be replaced before Contains to avoid partial match
113+ expect ( readableConditions ( 'key:\uf00dDoesNotContain\uf00dval' ) ) . toBe (
114+ 'key: does not contain val'
115+ ) ;
116+ // Should NOT produce "key: does not contains val" or "key:DoesNot contains val"
117+ expect ( readableConditions ( 'key:\uf00dDoesNotContain\uf00dval' ) ) . not . toContain (
118+ '\uf00d'
119+ ) ;
120+ } ) ;
121+
122+ it ( 'handles mixed operator types in one query' , ( ) => {
123+ const input =
124+ 'url:\uf00dStartsWith\uf00d/api span.description:\uf00dContains\uf00dfoo !path:\uf00dDoesNotEndWith\uf00d.js' ;
125+ expect ( readableConditions ( input ) ) . toBe (
126+ 'url: starts with /api span.description: contains foo !path: does not end with .js'
127+ ) ;
128+ } ) ;
129+
130+ it ( 'handles the same operator appearing multiple times' , ( ) => {
131+ const input =
132+ 'a:\uf00dContains\uf00dfoo b:\uf00dContains\uf00dbar c:\uf00dContains\uf00dbaz' ;
133+ expect ( readableConditions ( input ) ) . toBe (
134+ 'a: contains foo b: contains bar c: contains baz'
135+ ) ;
136+ } ) ;
137+
138+ it ( 'preserves OR with wildcard operators inside parens' , ( ) => {
139+ const input =
140+ '(span.name:\uf00dContains\uf00dfoo OR span.name:\uf00dContains\uf00dbar)' ;
141+ expect ( readableConditions ( input ) ) . toBe (
142+ '(span.name: contains foo OR span.name: contains bar)'
143+ ) ;
144+ } ) ;
145+
146+ it ( 'does not replace literal "Contains" text without unicode markers' , ( ) => {
147+ // The word "Contains" in a value or free text should NOT be replaced
148+ expect ( readableConditions ( 'message:Contains error' ) ) . toBe ( 'message:Contains error' ) ;
149+ } ) ;
150+ } ) ;
0 commit comments