@@ -287,6 +287,46 @@ describe('GlobalKVStore', () => {
287287 } )
288288 } )
289289
290+ it ( 'returns entry with tags when token includes tags field' , async ( ) => {
291+ primeResolverWithOneOutput ( mockResolver )
292+
293+ const originalDecode = ( MockPushDrop as any ) . decode
294+ ; ( MockPushDrop as any ) . decode = jest . fn ( ) . mockReturnValue ( {
295+ fields : [
296+ Array . from ( Buffer . from ( JSON . stringify ( [ 1 , 'kvstore' ] ) ) ) , // protocolID
297+ Array . from ( Buffer . from ( TEST_KEY ) ) , // key
298+ Array . from ( Buffer . from ( TEST_VALUE ) ) , // value
299+ Array . from ( Buffer . from ( TEST_CONTROLLER , 'hex' ) ) , // controller
300+ // tags field as JSON string so Utils.toUTF8 returns it directly
301+ '["alpha","beta"]' ,
302+ Array . from ( Buffer . from ( 'signature' ) ) // signature
303+ ]
304+ } )
305+
306+ const result = await kvStore . get ( { key : TEST_KEY } )
307+
308+ expect ( Array . isArray ( result ) ) . toBe ( true )
309+ expect ( result ) . toHaveLength ( 1 )
310+ if ( Array . isArray ( result ) && result . length > 0 ) {
311+ expect ( result [ 0 ] . tags ) . toEqual ( [ 'alpha' , 'beta' ] )
312+ }
313+
314+ ; ( MockPushDrop as any ) . decode = originalDecode
315+ } )
316+
317+ it ( 'omits tags when token is in old-format (no tags field)' , async ( ) => {
318+ primeResolverWithOneOutput ( mockResolver )
319+
320+ // primePushDropDecodeToValidValue() already sets old-format (no tags)
321+ const result = await kvStore . get ( { key : TEST_KEY } )
322+
323+ expect ( Array . isArray ( result ) ) . toBe ( true )
324+ expect ( result ) . toHaveLength ( 1 )
325+ if ( Array . isArray ( result ) && result . length > 0 ) {
326+ expect ( result [ 0 ] . tags ) . toBeUndefined ( )
327+ }
328+ } )
329+
290330 it ( 'returns entry with history when history=true' , async ( ) => {
291331 primeResolverWithOneOutput ( mockResolver )
292332 mockHistorian . buildHistory . mockResolvedValue ( [ 'oldValue' , TEST_VALUE ] )
@@ -320,6 +360,67 @@ describe('GlobalKVStore', () => {
320360 } )
321361 } )
322362
363+ it ( 'forwards tags-only queries to the resolver' , async ( ) => {
364+ primeResolverEmpty ( mockResolver )
365+
366+ const tags = [ 'group:music' , 'env:prod' ]
367+ const result = await kvStore . get ( { tags } )
368+
369+ expect ( Array . isArray ( result ) ) . toBe ( true )
370+ expect ( mockResolver . query ) . toHaveBeenCalledWith ( {
371+ service : 'ls_kvstore' ,
372+ query : expect . objectContaining ( { tags } )
373+ } )
374+ } )
375+
376+ it ( 'forwards tagQueryMode "all" to the resolver (default)' , async ( ) => {
377+ primeResolverEmpty ( mockResolver )
378+
379+ const tags = [ 'music' , 'rock' ]
380+ const result = await kvStore . get ( { tags, tagQueryMode : 'all' } )
381+
382+ expect ( Array . isArray ( result ) ) . toBe ( true )
383+ expect ( mockResolver . query ) . toHaveBeenCalledWith ( {
384+ service : 'ls_kvstore' ,
385+ query : expect . objectContaining ( {
386+ tags,
387+ tagQueryMode : 'all'
388+ } )
389+ } )
390+ } )
391+
392+ it ( 'forwards tagQueryMode "any" to the resolver' , async ( ) => {
393+ primeResolverEmpty ( mockResolver )
394+
395+ const tags = [ 'music' , 'jazz' ]
396+ const result = await kvStore . get ( { tags, tagQueryMode : 'any' } )
397+
398+ expect ( Array . isArray ( result ) ) . toBe ( true )
399+ expect ( mockResolver . query ) . toHaveBeenCalledWith ( {
400+ service : 'ls_kvstore' ,
401+ query : expect . objectContaining ( {
402+ tags,
403+ tagQueryMode : 'any'
404+ } )
405+ } )
406+ } )
407+
408+ it ( 'defaults to tagQueryMode "all" when not specified' , async ( ) => {
409+ primeResolverEmpty ( mockResolver )
410+
411+ const tags = [ 'category:news' ]
412+ const result = await kvStore . get ( { tags } )
413+
414+ expect ( Array . isArray ( result ) ) . toBe ( true )
415+ expect ( mockResolver . query ) . toHaveBeenCalledWith ( {
416+ service : 'ls_kvstore' ,
417+ query : expect . objectContaining ( { tags } )
418+ } )
419+ // Verify tagQueryMode is not explicitly set (will default to 'all' on server side)
420+ const call = ( mockResolver . query as jest . Mock ) . mock . calls [ 0 ] [ 0 ]
421+ expect ( call . query . tagQueryMode ) . toBeUndefined ( )
422+ } )
423+
323424 it ( 'includes token data when includeToken=true for key queries' , async ( ) => {
324425 primeResolverWithOneOutput ( mockResolver )
325426
@@ -644,6 +745,34 @@ describe('GlobalKVStore', () => {
644745 expect ( mockBroadcaster . broadcast ) . toHaveBeenCalled ( )
645746 } )
646747
748+ it ( 'includes tags field in locking script when options.tags provided' , async ( ) => {
749+ primeResolverEmpty ( mockResolver )
750+
751+ // Override PushDrop to capture the instance used within set()
752+ const originalImpl = ( MockPushDrop as any ) . mockImplementation
753+ const mockLockingScript = { toHex : ( ) => 'mockLockingScriptHex' }
754+ const localPushDrop = {
755+ lock : jest . fn ( ) . mockResolvedValue ( mockLockingScript ) ,
756+ unlock : jest . fn ( ) . mockReturnValue ( {
757+ sign : jest . fn ( ) . mockResolvedValue ( { toHex : ( ) => 'mockUnlockingScript' } )
758+ } )
759+ }
760+ ; ( MockPushDrop as any ) . mockImplementation ( ( ) => localPushDrop as any )
761+
762+ const providedTags = [ 'primary' , 'news' ]
763+ await kvStore . set ( TEST_KEY , TEST_VALUE , { tags : providedTags } )
764+
765+ // Validate PushDrop.lock was called with 5 fields (protocolID, key, value, controller, tags)
766+ expect ( localPushDrop . lock ) . toHaveBeenCalled ( )
767+ const lockArgs = ( localPushDrop . lock as jest . Mock ) . mock . calls [ 0 ]
768+ const fields = lockArgs [ 0 ]
769+ expect ( Array . isArray ( fields ) ) . toBe ( true )
770+ expect ( fields . length ) . toBe ( 5 )
771+
772+ // Restore original implementation
773+ ; ( MockPushDrop as any ) . mockImplementation = originalImpl
774+ } )
775+
647776 it ( 'updates existing token when one exists' , async ( ) => {
648777 // Mock the queryOverlay to return an entry with a token
649778 const mockQueryOverlay = jest . spyOn ( kvStore as any , 'queryOverlay' )
0 commit comments