okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
61c43ca
main
1package app 2 3// Controlled streams verify turn membership, retries, and cleanup after cancellation. 4import ( 5"albedo/cli/internal/daemon" 6"albedo/cli/internal/testwire" 7"context" 8"encoding/json" 9"errors" 10"net" 11"net/http" 12"net/http/httptest" 13"strings" 14"testing" 15"time" 16) 17 18func promptTestConnection (server * httptest.Server )* daemon.Connection { 19return daemon .NewConnection (daemon.ConnectionSnapshot {Port :server .Listener .Addr ().(* net.TCPAddr ).Port },nil ) 20} 21func TestPromptRequiresDurableInputCapabilityBeforeCreatingASession (t * testing.T ) { 22server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter ,r * http.Request ) { 23if r .URL .Path != "/server" { 24t .Errorf ("unsupported prompt issued %s" ,r .URL ) 25 } 26var resource map [string ]any 27_ = json .Unmarshal ([]byte (testwire .Server ),& resource ) 28resource ["capabilities" ]= map [string ]any {} 29_ = json .NewEncoder (w ).Encode (resource ) 30 })) 31defer server .Close () 32service := Service {Connect :func (ctx context.Context ) (* daemon.Connection ,error ) { 33return daemon .Attach (ctx , daemon.ConnectionSnapshot {Port :server .Listener .Addr ().(* net.TCPAddr ).Port ,Token :"token" },nil ) 34 }} 35_ ,err := service .RunPrompt (t .Context (),PromptOptions {Prompt :"hello" }) 36if _ ,ok := errors.AsType [* daemon.CompatibilityError ](err );! ok { 37t .Fatalf ("missing capability dispatched a submission: %v" ,err ) 38 } 39} 40func promptEntry (id ,text ,run string ,position int64 )map [string ]any { 41return map [string ]any {"id" :id ,"position" :position ,"kind" :"assistant" ,"created_at" :nil ,"input_id" :nil ,"turn_id" :run ,"checkpoint_id" :nil ,"content_complete" :true ,"content" : []any {map [string ]any {"kind" :"text" ,"text" :text }},"tool" :nil } 42} 43func TestPromptFollowsCombinedSubmissionThroughRetryAndCompletion (t * testing.T ) { 44submitted := make (chan string ,1 ) 45server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter ,r * http.Request ) { 46switch { 47case r .Method == "PUT" && strings .HasPrefix (r .URL .Path ,"/sessions/test/inputs/" ): 48id := strings .TrimPrefix (r .URL .Path ,"/sessions/test/inputs/" ) 49submitted <- id 50w .WriteHeader (202 ) 51_ = json .NewEncoder (w ).Encode (testwire .Input ("test" ,id ,"message" ,"pending" )) 52case r .URL .Path == "/sessions/test" : 53w .Header ().Set ("Content-Type" ,"text/event-stream" ) 54testwire .WriteBatch (w ,map [string ]any {"generation" :testwire .GenerationA ,"cursor" :0 ,"snapshot" :testwire .Session ("test" ,testwire .GenerationA ,0 ),"events" : []any {map [string ]any {"type" :"reset" ,"data" :map [string ]any {"reason" :"initial" }}}}) 55w .(http.Flusher ).Flush () 56id := <- submitted 57input := testwire .Input ("test" ,id ,"message" ,"committed" ) 58input ["turn" ]= map [string ]any {"id" :"combined" ,"state" :"running" ,"input_ids" : []string {id ,"other" },"input_count" :2 ,"truncated" :false ,"started_at" :"2026-10-03T00:00:00Z" ,"ended_at" :nil ,"outcome" :nil } 59events := []any {testwire .Event ("input" ,1 ,map [string ]any {"input" :input }),testwire .Event ("message" ,2 ,map [string ]any {"entry" :promptEntry ("m1" ,"before retry" ,"combined" ,1 )}),testwire .Event ("error" ,3 ,map [string ]any {"run_id" :"combined" ,"code" :"temporary" ,"message" :"temporary failure" }),testwire .Event ("retry" ,4 ,map [string ]any {"run_id" :"combined" ,"attempt" :2 ,"reason" :map [string ]any {"code" :"retry" ,"detail" :"retrying" },"delay_ms" :0 }),testwire .Event ("message" ,5 ,map [string ]any {"entry" :promptEntry ("m2" ,"combined final answer" ,"combined" ,2 )}),testwire .Event ("turn_completed" ,6 ,map [string ]any {"run_id" :"combined" ,"state" :"completed" ,"input_ids" : []string {id ,"other" },"input_count" :2 ,"truncated" :false })} 60testwire .WriteBatch (w ,map [string ]any {"generation" :testwire .GenerationA ,"cursor" :6 ,"events" :events }) 61w .(http.Flusher ).Flush () 62<- r .Context ().Done () 63default : 64t .Errorf ("unexpected %s %s" ,r .Method ,r .URL ) 65w .WriteHeader (404 ) 66 } 67 })) 68defer server .Close () 69ctx ,cancel := context .WithTimeout (t .Context (),time .Second ) 70defer cancel () 71answer ,err := awaitReply (ctx ,daemon .NewChatClient (promptTestConnection (server ),"test" ),"hello" ) 72if err != nil || answer != "combined final answer" { 73t .Fatalf ("combined reply: %q %v" ,answer ,err ) 74 } 75} 76func TestPromptCancellationReportsSharedWorkAndCleanupFailure (t * testing.T ) { 77for _ ,fixture := range []struct { 78name ,outcome ,want string 79status int 80 }{{"shared" ,"shared_running" ,"shared turn is still running" ,200 }, {"cleanup failure" ,"" ,"cancellation unconfirmed" ,503 }} { 81t .Run (fixture .name ,func (t * testing.T ) { 82submitted ,cancelled := make (chan struct {}),make (chan struct {}) 83ctx ,cancel := context .WithCancel (t .Context ()) 84defer cancel () 85server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter ,r * http.Request ) { 86switch { 87case r .URL .Path == "/sessions/test" : 88w .Header ().Set ("Content-Type" ,"text/event-stream" ) 89testwire .WriteBatch (w ,map [string ]any {"generation" :testwire .GenerationA ,"cursor" :0 ,"snapshot" :testwire .Session ("test" ,testwire .GenerationA ,0 ),"events" : []any {map [string ]any {"type" :"reset" ,"data" :map [string ]any {"reason" :"initial" }}}}) 90w .(http.Flusher ).Flush () 91<- r .Context ().Done () 92case r .Method == "PUT" && strings .HasPrefix (r .URL .Path ,"/sessions/test/inputs/" ): 93id := strings .TrimPrefix (r .URL .Path ,"/sessions/test/inputs/" ) 94w .WriteHeader (202 ) 95_ = json .NewEncoder (w ).Encode (testwire .Input ("test" ,id ,"message" ,"pending" )) 96close (submitted ) 97case r .Method == "POST" && strings .HasSuffix (r .URL .Path ,"/cancel" ): 98if r .Context ().Err ()!= nil { 99t .Error ("cleanup reused cancelled context" ) 100 } 101id := strings .TrimSuffix (strings .TrimPrefix (r .URL .Path ,"/sessions/test/inputs/" ),"/cancel" ) 102w .WriteHeader (fixture .status ) 103_ = json .NewEncoder (w ).Encode (map [string ]any {"input" :testwire .Input ("test" ,id ,"message" ,"committed" ),"result" :fixture .outcome }) 104close (cancelled ) 105default : 106t .Errorf ("unexpected operation %s %s" ,r .Method ,r .URL ) 107w .WriteHeader (404 ) 108 } 109 })) 110defer server .Close () 111go func () {<- submitted ;cancel () }() 112_ ,err := awaitReply (ctx ,daemon .NewChatClient (promptTestConnection (server ),"test" ),"hello" ) 113if err == nil || ! strings .Contains (err .Error (),fixture .want ) { 114t .Fatalf ("cancellation report %v" ,err ) 115 } 116select { 117case <- cancelled : 118default : 119t .Error ("cleanup not attempted" ) 120 } 121 }) 122 } 123}