@@ -3,8 +3,10 @@ package websocket
33import (
44 "ambient-code-backend/types"
55 "encoding/json"
6+ "fmt"
67 "os"
78 "path/filepath"
9+ "strings"
810 "testing"
911)
1012
@@ -449,3 +451,215 @@ func TestLoadEventsForReplay(t *testing.T) {
449451 }
450452 })
451453}
454+
455+ func TestFastExtractType (t * testing.T ) {
456+ tests := []struct {
457+ name string
458+ input string
459+ expected string
460+ }{
461+ {"standard event" , `{"type":"RUN_STARTED","runId":"r1"}` , "RUN_STARTED" },
462+ {"type not first field" , `{"runId":"r1","type":"RUN_FINISHED","ts":123}` , "RUN_FINISHED" },
463+ {"messages snapshot" , `{"type":"MESSAGES_SNAPSHOT","messages":[]}` , "MESSAGES_SNAPSHOT" },
464+ {"no type field" , `{"runId":"r1","data":"hello"}` , "" },
465+ {"empty object" , `{}` , "" },
466+ {"empty string" , `` , "" },
467+ }
468+
469+ for _ , tt := range tests {
470+ t .Run (tt .name , func (t * testing.T ) {
471+ result := fastExtractType ([]byte (tt .input ))
472+ if result != tt .expected {
473+ t .Errorf ("fastExtractType(%q) = %q, want %q" , tt .input , result , tt .expected )
474+ }
475+ })
476+ }
477+ }
478+
479+ func writeLargeEventFile (t * testing.T , path string , headEvents []map [string ]interface {}, paddingCount int , tailEvents []map [string ]interface {}) {
480+ t .Helper ()
481+ f , err := os .Create (path )
482+ if err != nil {
483+ t .Fatalf ("Failed to create events file: %v" , err )
484+ }
485+ defer f .Close ()
486+
487+ for _ , evt := range headEvents {
488+ data , err := json .Marshal (evt )
489+ if err != nil {
490+ t .Fatalf ("Failed to marshal head event: %v" , err )
491+ }
492+ if _ , err := f .Write (append (data , '\n' )); err != nil {
493+ t .Fatalf ("Failed to write head event: %v" , err )
494+ }
495+ }
496+
497+ paddingContent := strings .Repeat ("x" , 200 )
498+ for i := 0 ; i < paddingCount ; i ++ {
499+ evt := map [string ]interface {}{
500+ "type" : types .EventTypeTextMessageContent ,
501+ "messageId" : fmt .Sprintf ("msg-pad-%d" , i ),
502+ "delta" : paddingContent ,
503+ "timestamp" : fmt .Sprintf ("2025-01-01T00:01:%02dZ" , i % 60 ),
504+ }
505+ data , err := json .Marshal (evt )
506+ if err != nil {
507+ t .Fatalf ("Failed to marshal padding event: %v" , err )
508+ }
509+ if _ , err := f .Write (append (data , '\n' )); err != nil {
510+ t .Fatalf ("Failed to write padding event: %v" , err )
511+ }
512+ }
513+
514+ for _ , evt := range tailEvents {
515+ data , err := json .Marshal (evt )
516+ if err != nil {
517+ t .Fatalf ("Failed to marshal tail event: %v" , err )
518+ }
519+ if _ , err := f .Write (append (data , '\n' )); err != nil {
520+ t .Fatalf ("Failed to write tail event: %v" , err )
521+ }
522+ }
523+ }
524+
525+ func TestLoadEventsHeadTailMerge (t * testing.T ) {
526+ tmpDir , err := os .MkdirTemp ("" , "agui-headtail-test-*" )
527+ if err != nil {
528+ t .Fatalf ("Failed to create temp dir: %v" , err )
529+ }
530+ defer os .RemoveAll (tmpDir )
531+
532+ origStateBaseDir := StateBaseDir
533+ StateBaseDir = tmpDir
534+ defer func () { StateBaseDir = origStateBaseDir }()
535+
536+ t .Run ("large file preserves head snapshot events" , func (t * testing.T ) {
537+ sessionID := "test-large-headtail"
538+ sessionsDir := filepath .Join (tmpDir , "sessions" , sessionID )
539+ if err := os .MkdirAll (sessionsDir , 0755 ); err != nil {
540+ t .Fatalf ("Failed to create sessions dir: %v" , err )
541+ }
542+
543+ eventsFile := filepath .Join (sessionsDir , "agui-events.jsonl" )
544+ writeLargeEventFile (t , eventsFile ,
545+ []map [string ]interface {}{
546+ {"type" : types .EventTypeRunStarted , "runId" : "r1" , "timestamp" : "2025-01-01T00:00:00Z" },
547+ {"type" : types .EventTypeMessagesSnapshot , "messages" : []interface {}{
548+ map [string ]interface {}{"id" : "msg1" , "role" : "user" , "content" : "Hello" },
549+ }, "timestamp" : "2025-01-01T00:00:01Z" },
550+ },
551+ 15000 ,
552+ []map [string ]interface {}{
553+ {"type" : types .EventTypeTextMessageContent , "messageId" : "msg-tail" , "delta" : "tail event" , "timestamp" : "2025-01-01T00:02:00Z" },
554+ },
555+ )
556+
557+ stat , err := os .Stat (eventsFile )
558+ if err != nil {
559+ t .Fatalf ("Failed to stat events file: %v" , err )
560+ }
561+ if stat .Size () <= replayMaxTailBytes {
562+ t .Fatalf ("Test file too small (%d bytes), need > %d to trigger head+tail path" , stat .Size (), replayMaxTailBytes )
563+ }
564+
565+ result := loadEvents (sessionID )
566+ if len (result ) == 0 {
567+ t .Fatal ("Expected events from loadEvents, got none" )
568+ }
569+
570+ hasRunStarted := false
571+ hasMessagesSnapshot := false
572+ for _ , evt := range result {
573+ evtType , _ := evt ["type" ].(string )
574+ if evtType == types .EventTypeRunStarted {
575+ hasRunStarted = true
576+ }
577+ if evtType == types .EventTypeMessagesSnapshot {
578+ hasMessagesSnapshot = true
579+ }
580+ }
581+
582+ if ! hasRunStarted {
583+ t .Error ("Expected RUN_STARTED from head scan to be present in merged result" )
584+ }
585+ if ! hasMessagesSnapshot {
586+ t .Error ("Expected MESSAGES_SNAPSHOT from head scan to be present in merged result" )
587+ }
588+
589+ if result [0 ]["type" ] != types .EventTypeRunStarted {
590+ t .Errorf ("Expected first event to be RUN_STARTED, got %v" , result [0 ]["type" ])
591+ }
592+ if result [1 ]["type" ] != types .EventTypeMessagesSnapshot {
593+ t .Errorf ("Expected second event to be MESSAGES_SNAPSHOT, got %v" , result [1 ]["type" ])
594+ }
595+ })
596+
597+ t .Run ("large file deduplicates overlapping events" , func (t * testing.T ) {
598+ sessionID := "test-large-dedup"
599+ sessionsDir := filepath .Join (tmpDir , "sessions" , sessionID )
600+ if err := os .MkdirAll (sessionsDir , 0755 ); err != nil {
601+ t .Fatalf ("Failed to create sessions dir: %v" , err )
602+ }
603+
604+ eventsFile := filepath .Join (sessionsDir , "agui-events.jsonl" )
605+ writeLargeEventFile (t , eventsFile ,
606+ []map [string ]interface {}{
607+ {"type" : types .EventTypeRunStarted , "runId" : "r1" , "timestamp" : "2025-01-01T00:00:00Z" },
608+ },
609+ 15000 ,
610+ []map [string ]interface {}{
611+ {"type" : types .EventTypeRunFinished , "runId" : "r1" , "timestamp" : "2025-01-01T00:03:00Z" },
612+ },
613+ )
614+
615+ stat , err := os .Stat (eventsFile )
616+ if err != nil {
617+ t .Fatalf ("Failed to stat events file: %v" , err )
618+ }
619+ if stat .Size () <= replayMaxTailBytes {
620+ t .Fatalf ("Test file too small (%d bytes), need > %d" , stat .Size (), replayMaxTailBytes )
621+ }
622+
623+ result := loadEvents (sessionID )
624+
625+ runStartedCount := 0
626+ for _ , evt := range result {
627+ if evt ["type" ] == types .EventTypeRunStarted {
628+ runStartedCount ++
629+ }
630+ }
631+ if runStartedCount != 1 {
632+ t .Errorf ("Expected exactly 1 RUN_STARTED (no duplicates), got %d" , runStartedCount )
633+ }
634+ })
635+
636+ t .Run ("large file with no head snapshots returns tail only" , func (t * testing.T ) {
637+ sessionID := "test-large-no-head-snapshot"
638+ sessionsDir := filepath .Join (tmpDir , "sessions" , sessionID )
639+ if err := os .MkdirAll (sessionsDir , 0755 ); err != nil {
640+ t .Fatalf ("Failed to create sessions dir: %v" , err )
641+ }
642+
643+ eventsFile := filepath .Join (sessionsDir , "agui-events.jsonl" )
644+ writeLargeEventFile (t , eventsFile , nil , 15000 , nil )
645+
646+ stat , err := os .Stat (eventsFile )
647+ if err != nil {
648+ t .Fatalf ("Failed to stat events file: %v" , err )
649+ }
650+ if stat .Size () <= replayMaxTailBytes {
651+ t .Fatalf ("Test file too small (%d bytes), need > %d" , stat .Size (), replayMaxTailBytes )
652+ }
653+
654+ result := loadEvents (sessionID )
655+ if len (result ) == 0 {
656+ t .Fatal ("Expected tail events, got none" )
657+ }
658+
659+ for _ , evt := range result {
660+ if evt ["type" ] != types .EventTypeTextMessageContent {
661+ t .Errorf ("Expected only TEXT_MESSAGE_CONTENT events, got %v" , evt ["type" ])
662+ }
663+ }
664+ })
665+ }
0 commit comments