okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
d9c1a13
main
1// Discovery must distinguish an unsafe live endpoint from an absent daemon. 2// Malformed records and deliberate health failures require controlled peers. 3package localdaemon 4 5import ( 6"albedo/cli/internal/daemon" 7"context" 8"encoding/json" 9"errors" 10"fmt" 11"net" 12"net/http" 13"net/http/httptest" 14"os" 15"path/filepath" 16"strings" 17"testing" 18"time" 19) 20 21const readyServer = `{"instance_id":"instance-a","protocol":3,"state":"ready","capabilities":{"durable_inputs":1,"session_replay":2,"collection_invalidation":1,"tool_progress":1},"build":"verified-build","digest":"verified-digest","extensions":[],"quota":[],"notices":[]}` 22 23func writeDiscovery (t * testing.T ,home string ,snapshot daemon.ConnectionSnapshot ) { 24t .Helper () 25data ,err := json .Marshal (snapshot ) 26if err != nil { 27t .Fatal (err ) 28 } 29if err := os .WriteFile (filepath .Join (home ,"daemon.json" ),data ,0600 );err != nil { 30t .Fatal (err ) 31 } 32} 33 34// A real daemon normally exits before cancellation can reach the stop wait. 35func TestUpgradeCancellationAfterShutdownDoesNotLaunchReplacement (t * testing.T ) { 36if processAlive (0 ) { 37t .Skip ("local process inspection is unsupported" ) 38 } 39home := t .TempDir () 40executable := filepath .Join (t .TempDir (),"replacement" ) 41if err := os .WriteFile (executable , []byte ("#!/bin/sh\n: > \"$ALBEDO_HOME/replacement-started\"\n" ),0700 );err != nil { 42t .Fatal (err ) 43 } 44t .Setenv ("ALBEDO_DAEMON" ,executable ) 45shutdownAcknowledged := make (chan struct {}) 46server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter ,r * http.Request ) { 47switch r .URL .Path { 48case "/server" : 49_ ,_ = fmt .Fprint (w ,readyServer ) 50case "/server/shutdown" : 51w .WriteHeader (202 ) 52_ ,_ = fmt .Fprint (w ,`{"instance_id":"instance-a","state":"draining"}` ) 53w .(http.Flusher ).Flush () 54close (shutdownAcknowledged ) 55default : 56http .NotFound (w ,r ) 57 } 58 })) 59defer server .Close () 60snapshot := daemon.ConnectionSnapshot {Port :server .Listener .Addr ().(* net.TCPAddr ).Port ,Pid :os .Getpid (),Token :"token" ,Version :daemon .ProtocolVersion ,Build :"verified-build" ,Digest :"verified-digest" ,InstanceID :"instance-a" } 61writeDiscovery (t ,home ,snapshot ) 62ctx ,cancel := context .WithCancel (t .Context ()) 63defer cancel () 64finished := make (chan error ,1 ) 65go func () { 66_ ,err := Upgrade (ctx ,LocalOptions {HomeDir :home },snapshot ) 67finished <- err 68 }() 69select { 70case <- shutdownAcknowledged : 71case <- time .After (time .Second ): 72t .Fatal ("upgrade did not request shutdown" ) 73 } 74// The acknowledged process deliberately remains alive, keeping Upgrade in 75// its exit wait until the caller cancels. 76select { 77case err := <- finished : 78t .Fatalf ("upgrade returned before the live process exited: %v" ,err ) 79case <- time .After (50 * time .Millisecond ): 80 } 81cancel () 82select { 83case err := <- finished : 84if ! errors .Is (err ,context .Canceled ) { 85t .Fatalf ("upgrade lost cancellation: %v" ,err ) 86 } 87case <- time .After (time .Second ): 88t .Fatal ("upgrade did not cancel its exit wait" ) 89 } 90if _ ,err := os .Stat (filepath .Join (home ,"replacement-started" ));! errors .Is (err ,os .ErrNotExist ) { 91t .Fatalf ("canceled upgrade launched a replacement: %v" ,err ) 92 } 93} 94 95func TestDiscoveryPreservesUnsafeRecordAndLiveHealthFailures (t * testing.T ) { 96t .Run ("loaded healthy daemon" ,func (t * testing.T ) { 97server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter ,r * http.Request ) { 98// Owner observations may queue behind work on an already ready daemon. 99timer := time .NewTimer (750 * time .Millisecond ) 100defer timer .Stop () 101select { 102case <- timer .C : 103_ ,_ = fmt .Fprint (w ,readyServer ) 104case <- r .Context ().Done (): 105 } 106 })) 107defer server .Close () 108home := t .TempDir () 109snapshot := daemon.ConnectionSnapshot {Port :server .Listener .Addr ().(* net.TCPAddr ).Port ,Pid :os .Getpid (),Token :"token" ,Version :daemon .ProtocolVersion ,Build :"verified-build" ,InstanceID :"instance-a" } 110writeDiscovery (t ,home ,snapshot ) 111// The live resource supplies verified build metadata missing from the record. 112snapshot .Digest = "verified-digest" 113discovery ,err := Discover (t .Context (),home ) 114if err != nil || discovery .Kind != Running || discovery .Snapshot != snapshot || discovery .Server .State != "ready" { 115t .Fatalf ("loaded healthy daemon was not usable: %+v, %v" ,discovery ,err ) 116 } 117 }) 118for _ ,status := range []int {http .StatusOK ,http .StatusNotFound ,http .StatusUnauthorized ,http .StatusServiceUnavailable } { 119t .Run (http .StatusText (status ),func (t * testing.T ) { 120server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter ,r * http.Request ) { 121w .WriteHeader (status ) 122if status == http .StatusOK { 123_ ,_ = w .Write ([]byte (`{` )) 124return 125 } 126_ ,_ = w .Write ([]byte (`{"type":"about:blank","title":"Unauthorized","status":401,"code":"authentication_required","detail":"health refused"}` )) 127 })) 128defer server .Close () 129home := t .TempDir () 130writeDiscovery (t ,home , daemon.ConnectionSnapshot {Port :server .Listener .Addr ().(* net.TCPAddr ).Port ,Pid :os .Getpid (),Token :"token" ,Version :daemon .ProtocolVersion }) 131_ ,err := Discover (t .Context (),home ) 132failure ,ok := errors.AsType [* LocalError ](err ) 133expected := UnhealthyDaemon 134if status == http .StatusUnauthorized { 135expected = AuthenticationFailed 136 } 137if ! ok || failure .Kind != expected { 138t .Fatalf ("health failure was treated as absence: %v" ,err ) 139 } 140if status == http .StatusOK { 141if _ ,ok := errors.AsType [* daemon.ProtocolError ](err );! ok { 142t .Fatalf ("malformed health lost its protocol failure: %v" ,err ) 143 } 144 }else if api ,ok := errors.AsType [* daemon.APIError ](err );! ok || api .StatusCode != status { 145t .Fatalf ("health failure lost its HTTP status: %v" ,err ) 146 } 147 }) 148 } 149home := t .TempDir () 150if err := os .WriteFile (filepath .Join (home ,"daemon.json" ), []byte (`{` ),0600 );err != nil { 151t .Fatal (err ) 152 } 153_ ,err := Discover (t .Context (),home ) 154failure ,ok := errors.AsType [* LocalError ](err ) 155if ! ok || failure .Kind != InvalidDiscovery { 156t .Fatalf ("malformed record was treated as absence: %v" ,err ) 157 } 158} 159 160func TestDiscoveryRejectsChangedBuildDigest (t * testing.T ) { 161server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter ,_ * http.Request ) { 162_ ,_ = fmt .Fprint (w ,readyServer ) 163 })) 164t .Cleanup (server .Close ) 165home := t .TempDir () 166writeDiscovery (t ,home , daemon.ConnectionSnapshot {Port :server .Listener .Addr ().(* net.TCPAddr ).Port ,Pid :os .Getpid (),Token :"token" ,Version :daemon .ProtocolVersion ,Build :"verified-build" ,Digest :"different-digest" }) 167found ,err := Discover (t .Context (),home ) 168local ,ok := errors.AsType [* LocalError ](err ) 169if ! ok || local .Kind != InvalidDiscovery || found .Kind == Running { 170t .Fatalf ("changed executable identity accepted: %+v, %v" ,found ,err ) 171 } 172} 173 174// A protocol 2 daemon has no /server route; the current daemon cannot produce 175// this response, so a controlled peer checks migration diagnostics and refusal. 176func TestDiscoveryExplainsOlderProtocolWithoutTreatingItAsReady (t * testing.T ) { 177for _ ,status := range []int {http .StatusNotFound ,http .StatusUnauthorized ,http .StatusServiceUnavailable } { 178t .Run (http .StatusText (status ),func (t * testing.T ) { 179server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter ,r * http.Request ) { 180if r .Method != http .MethodGet || r .URL .Path != "/server" || r .Header .Get ("Authorization" )!= "Bearer token" { 181t .Errorf ("unexpected request while diagnosing an old daemon: %s %s" ,r .Method ,r .URL .Path ) 182 } 183w .WriteHeader (status ) 184 })) 185t .Cleanup (server .Close ) 186home := t .TempDir () 187snapshot := daemon.ConnectionSnapshot {Port :server .Listener .Addr ().(* net.TCPAddr ).Port ,Pid :os .Getpid (),Token :"token" ,Version :2 } 188writeDiscovery (t ,home ,snapshot ) 189found ,err := Discover (t .Context (),home ) 190failure ,ok := errors.AsType [* LocalError ](err ) 191expected := UnhealthyDaemon 192switch status { 193case http .StatusNotFound : 194expected = IncompatibleDaemon 195case http .StatusUnauthorized : 196expected = AuthenticationFailed 197 } 198if ! ok || failure .Kind != expected || found .Kind == Running { 199t .Fatalf ("old discovery record authorized attachment or lost its failure: %+v, %v" ,found ,err ) 200 } 201if status == http .StatusNotFound { 202compatibility ,ok := errors.AsType [* daemon.CompatibilityError ](err ) 203if ! ok || compatibility .Version != 2 || ! strings .Contains (err .Error (),"matching client (albedo daemon --stop)" ) { 204t .Fatalf ("missing actionable protocol mismatch: %v" ,err ) 205 } 206 } 207data ,readErr := os .ReadFile (filepath .Join (home ,"daemon.json" )) 208var retained daemon.ConnectionSnapshot 209if readErr != nil || json .Unmarshal (data ,& retained )!= nil || retained != snapshot { 210t .Fatal ("discovery changed the old daemon record" ) 211 } 212 }) 213 } 214} 215 216func TestDiscoveryAndLauncherWaitHonorCancellation (t * testing.T ) { 217for _ ,health := range []bool {false ,true } { 218t .Run (fmt .Sprintf ("stalled_health_%t" ,health ),func (t * testing.T ) { 219home := t .TempDir () 220var operation func (context.Context )error 221if health { 222server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter ,r * http.Request ) {<- r .Context ().Done () })) 223defer server .Close () 224writeDiscovery (t ,home , daemon.ConnectionSnapshot {Port :server .Listener .Addr ().(* net.TCPAddr ).Port ,Pid :os .Getpid (),Token :"t" ,Version :daemon .ProtocolVersion }) 225operation = func (ctx context.Context )error {_ ,err := Discover (ctx ,home );return err } 226 }else { 227if processAlive (0 ) { 228t .Skip ("advisory locking requires Unix" ) 229 } 230executable := filepath .Join (t .TempDir (),"unused-daemon" ) 231if err := os .WriteFile (executable , []byte ("#!/bin/sh\nexit 99\n" ),0700 );err != nil { 232t .Fatal (err ) 233 } 234t .Setenv ("ALBEDO_DAEMON" ,executable ) 235lock ,err := acquireLauncher (t .Context (),home ) 236if err != nil { 237t .Fatal (err ) 238 } 239defer lock .Close () 240operation = func (ctx context.Context )error {_ ,err := Launch (ctx ,LocalOptions {HomeDir :home });return err } 241 } 242ctx ,cancel := context .WithTimeout (t .Context (),30 * time .Millisecond ) 243defer cancel () 244started := time .Now () 245if err := operation (ctx );! errors .Is (err ,context .DeadlineExceeded ) { 246t .Fatalf ("deadline lost: %v" ,err ) 247 } 248if time .Since (started )> 300 * time .Millisecond { 249t .Fatal ("discovery or launcher wait ignored caller deadline" ) 250 } 251 }) 252 } 253}