@@ -51,4 +51,262 @@ public void BuildSupportBundle_DefaultIncludesStoredActivityWindow()
5151 Assert . Contains ( "bundle-item-399" , bundle ) ;
5252 Assert . Contains ( "bundle-item-000" , bundle ) ;
5353 }
54+
55+ // ── Add: field defaults ──
56+
57+ [ Fact ]
58+ public void Add_WithAllOptionalFields_StoresFieldValues ( )
59+ {
60+ ActivityStreamService . Add (
61+ "sessions" ,
62+ "My Session" ,
63+ details : "some detail" ,
64+ icon : "🔥" ,
65+ dashboardPath : "/dashboard/abc" ,
66+ sessionKey : "sk-123" ,
67+ nodeId : "node-xyz" ) ;
68+
69+ var items = ActivityStreamService . GetItems ( ) ;
70+
71+ Assert . Single ( items ) ;
72+ var item = items [ 0 ] ;
73+ Assert . Equal ( "sessions" , item . Category ) ;
74+ Assert . Equal ( "My Session" , item . Title ) ;
75+ Assert . Equal ( "some detail" , item . Details ) ;
76+ Assert . Equal ( "🔥" , item . Icon ) ;
77+ Assert . Equal ( "/dashboard/abc" , item . DashboardPath ) ;
78+ Assert . Equal ( "sk-123" , item . SessionKey ) ;
79+ Assert . Equal ( "node-xyz" , item . NodeId ) ;
80+ }
81+
82+ [ Fact ]
83+ public void Add_WithNullCategory_DefaultsToGeneral ( )
84+ {
85+ ActivityStreamService . Add ( null ! , "Title" ) ;
86+
87+ var items = ActivityStreamService . GetItems ( ) ;
88+
89+ Assert . Equal ( "general" , items [ 0 ] . Category ) ;
90+ }
91+
92+ [ Fact ]
93+ public void Add_WithWhitespaceCategory_DefaultsToGeneral ( )
94+ {
95+ ActivityStreamService . Add ( " " , "Title" ) ;
96+
97+ var items = ActivityStreamService . GetItems ( ) ;
98+
99+ Assert . Equal ( "general" , items [ 0 ] . Category ) ;
100+ }
101+
102+ [ Fact ]
103+ public void Add_WithEmptyTitle_DoesNotAddItem ( )
104+ {
105+ ActivityStreamService . Add ( "test" , "" ) ;
106+
107+ var items = ActivityStreamService . GetItems ( ) ;
108+
109+ Assert . Empty ( items ) ;
110+ }
111+
112+ [ Fact ]
113+ public void Add_WithWhitespaceTitle_DoesNotAddItem ( )
114+ {
115+ ActivityStreamService . Add ( "test" , " " ) ;
116+
117+ var items = ActivityStreamService . GetItems ( ) ;
118+
119+ Assert . Empty ( items ) ;
120+ }
121+
122+ [ Fact ]
123+ public void Add_StoresItemsNewestFirst ( )
124+ {
125+ ActivityStreamService . Add ( "test" , "first" ) ;
126+ ActivityStreamService . Add ( "test" , "second" ) ;
127+
128+ var items = ActivityStreamService . GetItems ( ) ;
129+
130+ Assert . Equal ( "second" , items [ 0 ] . Title ) ;
131+ Assert . Equal ( "first" , items [ 1 ] . Title ) ;
132+ }
133+
134+ // ── GetItems: filtering and limiting ──
135+
136+ [ Fact ]
137+ public void GetItems_WithMaxItemsLessThanStored_ReturnsOnlyNewest ( )
138+ {
139+ ActivityStreamService . Add ( "test" , "old" ) ;
140+ ActivityStreamService . Add ( "test" , "newer" ) ;
141+ ActivityStreamService . Add ( "test" , "newest" ) ;
142+
143+ var items = ActivityStreamService . GetItems ( maxItems : 2 ) ;
144+
145+ Assert . Equal ( 2 , items . Count ) ;
146+ Assert . Equal ( "newest" , items [ 0 ] . Title ) ;
147+ Assert . Equal ( "newer" , items [ 1 ] . Title ) ;
148+ }
149+
150+ [ Fact ]
151+ public void GetItems_WithMaxItemsZero_ReturnsEmpty ( )
152+ {
153+ ActivityStreamService . Add ( "test" , "item" ) ;
154+
155+ var items = ActivityStreamService . GetItems ( maxItems : 0 ) ;
156+
157+ Assert . Empty ( items ) ;
158+ }
159+
160+ [ Fact ]
161+ public void GetItems_WithCategoryFilter_ReturnsOnlyMatchingCategory ( )
162+ {
163+ ActivityStreamService . Add ( "sessions" , "session item" ) ;
164+ ActivityStreamService . Add ( "nodes" , "node item" ) ;
165+ ActivityStreamService . Add ( "sessions" , "another session" ) ;
166+
167+ var items = ActivityStreamService . GetItems ( category : "sessions" ) ;
168+
169+ Assert . Equal ( 2 , items . Count ) ;
170+ Assert . All ( items , item => Assert . Equal ( "sessions" , item . Category ) ) ;
171+ }
172+
173+ [ Fact ]
174+ public void GetItems_WithCategoryAll_ReturnsEverything ( )
175+ {
176+ ActivityStreamService . Add ( "sessions" , "s" ) ;
177+ ActivityStreamService . Add ( "nodes" , "n" ) ;
178+
179+ var items = ActivityStreamService . GetItems ( category : "all" ) ;
180+
181+ Assert . Equal ( 2 , items . Count ) ;
182+ }
183+
184+ [ Fact ]
185+ public void GetItems_CategoryFilter_IsCaseInsensitive ( )
186+ {
187+ ActivityStreamService . Add ( "Sessions" , "uppercase S" ) ;
188+
189+ var items = ActivityStreamService . GetItems ( category : "sessions" ) ;
190+
191+ Assert . Single ( items ) ;
192+ }
193+
194+ [ Fact ]
195+ public void GetItems_CategoryFilter_MatchesDotPrefixSubcategories ( )
196+ {
197+ ActivityStreamService . Add ( "sessions.chat" , "chat session" ) ;
198+ ActivityStreamService . Add ( "sessions.mcp" , "mcp session" ) ;
199+ ActivityStreamService . Add ( "nodes" , "unrelated" ) ;
200+
201+ var items = ActivityStreamService . GetItems ( category : "sessions" ) ;
202+
203+ Assert . Equal ( 2 , items . Count ) ;
204+ Assert . DoesNotContain ( items , item => item . Category == "nodes" ) ;
205+ }
206+
207+ [ Fact ]
208+ public void GetItems_CategoryFilter_DoesNotMatchPartialPrefix ( )
209+ {
210+ ActivityStreamService . Add ( "sessionsExtra" , "should not match" ) ;
211+ ActivityStreamService . Add ( "sessions.sub" , "should match" ) ;
212+
213+ var items = ActivityStreamService . GetItems ( category : "sessions" ) ;
214+
215+ Assert . Single ( items ) ;
216+ Assert . Equal ( "sessions.sub" , items [ 0 ] . Category ) ;
217+ }
218+
219+ // ── Clear ──
220+
221+ [ Fact ]
222+ public void Clear_RemovesAllItems ( )
223+ {
224+ ActivityStreamService . Add ( "test" , "item1" ) ;
225+ ActivityStreamService . Add ( "test" , "item2" ) ;
226+
227+ ActivityStreamService . Clear ( ) ;
228+
229+ Assert . Empty ( ActivityStreamService . GetItems ( ) ) ;
230+ }
231+
232+ [ Fact ]
233+ public void Clear_RaisesUpdatedEvent ( )
234+ {
235+ var raised = false ;
236+ EventHandler handler = ( _ , _ ) => raised = true ;
237+ ActivityStreamService . Updated += handler ;
238+ try
239+ {
240+ ActivityStreamService . Clear ( ) ;
241+ Assert . True ( raised ) ;
242+ }
243+ finally
244+ {
245+ ActivityStreamService . Updated -= handler ;
246+ }
247+ }
248+
249+ [ Fact ]
250+ public void Add_RaisesUpdatedEvent ( )
251+ {
252+ var raised = false ;
253+ EventHandler handler = ( _ , _ ) => raised = true ;
254+ ActivityStreamService . Updated += handler ;
255+ try
256+ {
257+ ActivityStreamService . Add ( "test" , "item" ) ;
258+ Assert . True ( raised ) ;
259+ }
260+ finally
261+ {
262+ ActivityStreamService . Updated -= handler ;
263+ }
264+ }
265+
266+ // ── BuildSupportBundle ──
267+
268+ [ Fact ]
269+ public void BuildSupportBundle_WithMaxItemsParam_LimitsOutput ( )
270+ {
271+ for ( var i = 0 ; i < 10 ; i ++ )
272+ ActivityStreamService . Add ( "test" , $ "item-{ i } ") ;
273+
274+ var bundle = ActivityStreamService . BuildSupportBundle ( maxItems : 3 ) ;
275+
276+ Assert . Contains ( "Items: 3" , bundle ) ;
277+ }
278+
279+ [ Fact ]
280+ public void BuildSupportBundle_IncludesSessionKeyAndNodeIdWhenPresent ( )
281+ {
282+ ActivityStreamService . Add ( "test" , "title" , sessionKey : "sk-abc" , nodeId : "nd-xyz" ) ;
283+
284+ var bundle = ActivityStreamService . BuildSupportBundle ( ) ;
285+
286+ Assert . Contains ( "session=sk-abc" , bundle ) ;
287+ Assert . Contains ( "node=nd-xyz" , bundle ) ;
288+ }
289+
290+ [ Fact ]
291+ public void BuildSupportBundle_TruncatesLongNodeId ( )
292+ {
293+ var longNodeId = new string ( 'a' , 32 ) ;
294+ ActivityStreamService . Add ( "test" , "title" , nodeId : longNodeId ) ;
295+
296+ var bundle = ActivityStreamService . BuildSupportBundle ( ) ;
297+
298+ Assert . Contains ( "node=" + new string ( 'a' , 16 ) + "..." , bundle ) ;
299+ Assert . DoesNotContain ( "node=" + longNodeId , bundle ) ;
300+ }
301+
302+ [ Fact ]
303+ public void BuildSupportBundle_OmitsSessionAndNodeFieldsWhenAbsent ( )
304+ {
305+ ActivityStreamService . Add ( "test" , "plain item" ) ;
306+
307+ var bundle = ActivityStreamService . BuildSupportBundle ( ) ;
308+
309+ Assert . DoesNotContain ( "session=" , bundle ) ;
310+ Assert . DoesNotContain ( "node=" , bundle ) ;
311+ }
54312}
0 commit comments