okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
2f6697a
main
1// Advertised actions must honor bindings, routes, validators, and success statuses. 2// A controlled peer supplies declarations the real daemon does not publish. 3package daemon 4 5import ( 6"encoding/json" 7"errors" 8"io" 9"net/http" 10"strings" 11"sync/atomic" 12"testing" 13 14"albedo/cli/internal/daemon/protocol" 15) 16 17func TestDeclaredActionRejectsOverlappingBodyBindingsBeforeDelivery (t * testing.T ) { 18var delivered atomic.Int32 19conn := controlledConnection (t ,func (w http.ResponseWriter ,r * http.Request ) { 20delivered .Add (1 ) 21_ ,_ = w .Write ([]byte (`{}` )) 22 }) 23for _ ,body := range []string { 24`{"/a":{"source":"literal","value":{}},"/a/b":{"source":"literal","value":1}}` , 25`{"/a/b":{"source":"literal","value":1},"/a":{"source":"literal","value":{}}}` , 26`{"/a~1b":{"source":"literal","value":{}},"/a~1b/c":{"source":"literal","value":1}}` , 27 } { 28var action PageAction 29if err := json .Unmarshal ([]byte (`{"method":"POST","path_template":"/extensions/custom/items","body":` + body + `}` ),& action .Operation );err != nil { 30t .Fatal (err ) 31 } 32if _ ,err := ExecutePageAction (t .Context (),conn ,PageActionRequest {Action :action });err == nil { 33t .Errorf ("overlapping body was accepted: %s" ,body ) 34 } 35 } 36if count := delivered .Load ();count != 0 { 37t .Fatalf ("ambiguous actions reached the daemon %d times" ,count ) 38 } 39} 40 41func TestDeclaredActionDeliversSiblingAndEscapedBodyKeys (t * testing.T ) { 42var delivered atomic.Int32 43conn := controlledConnection (t ,func (w http.ResponseWriter ,r * http.Request ) { 44var body map [string ]any 45if err := json .NewDecoder (r .Body ).Decode (& body );err != nil { 46t .Error (err ) 47return 48 } 49nested ,ok := body ["a/b" ].(map [string ]any ) 50if ! ok || nested ["~name" ]!= "one" || nested ["second" ]!= "two" || body ["a" ]!= "separate" { 51t .Errorf ("body bindings changed: %#v" ,body ) 52 } 53delivered .Add (1 ) 54_ ,_ = w .Write ([]byte (`{}` )) 55 }) 56var action PageAction 57if err := json .Unmarshal ([]byte (`{"method":"POST","path_template":"/extensions/custom/items","body":{"/a~1b/~0name":{"source":"literal","value":"one"},"/a~1b/second":{"source":"literal","value":"two"},"/a":{"source":"literal","value":"separate"}}}` ),& action .Operation );err != nil { 58t .Fatal (err ) 59 } 60if _ ,err := ExecutePageAction (t .Context (),conn ,PageActionRequest {Action :action });err != nil { 61t .Fatal (err ) 62 } 63if delivered .Load ()!= 1 { 64t .Fatal ("valid action was not delivered" ) 65 } 66} 67 68// Extension responses can choose either success status without the client 69// knowing their URL. An explicit declaration must still detect a lost result. 70func TestDeclaredActionHonorsSuccessStatusWithoutRouteGuessing (t * testing.T ) { 71for _ ,scenario := range []struct { 72name string 73declared * int64 74response int 75wantError bool 76wantCalls int32 77 }{ 78 {"default ok" ,nil ,200 ,false ,1 }, 79 {"default created" ,nil ,201 ,false ,1 }, 80 {"explicit created" ,new (int64 (201 )),201 ,false ,1 }, 81 {"wrong success" ,new (int64 (201 )),200 ,true ,1 }, 82 {"unsupported success" ,new (int64 (204 )),204 ,true ,0 }, 83 } { 84t .Run (scenario .name ,func (t * testing.T ) { 85var calls atomic.Int32 86conn := controlledConnection (t ,func (w http.ResponseWriter ,r * http.Request ) { 87calls .Add (1 ) 88w .WriteHeader (scenario .response ) 89_ ,_ = io .WriteString (w ,`{}` ) 90 }) 91operation := protocol.ActionOperation {Method :"POST" ,PathTemplate :"/extensions/custom/items" ,SuccessStatus :scenario .declared ,ResultSchema :json .RawMessage (`{}` )} 92_ ,err := executeBoundOperation (t .Context (),conn ,operation ,nil ,nil ,nil ) 93if (err != nil )!= scenario .wantError || calls .Load ()!= scenario .wantCalls { 94t .Fatalf ("status handling: %v, deliveries=%d" ,err ,calls .Load ()) 95 } 96 }) 97 } 98} 99 100func TestDeclaredActionCapturesRowBindingsAndValidatesResult (t * testing.T ) { 101var row PageRow 102if err := json .Unmarshal ([]byte (`{"id":"row-a","text":"Task","badge":null,"tone":"plain","detail":null,"resource":{"url":"/extensions/custom/items/row-a","etag":"\"seen\"","value":{"title":"displayed title"}}}` ),& row .wire );err != nil { 103t .Fatal (err ) 104 } 105var field FormField 106_ = json .Unmarshal ([]byte (`{"name":"title","label":"Title","type":"text","required":true,"default":"literal","default_binding":{"source":"row","pointer":"/resource/value/title"}}` ),& field ) 107resolved ,err := ResolveFormField (field ,& row ,nil ) 108if err != nil || string (resolved .Default )!= `"displayed title"` { 109t .Fatalf ("row prefill was lost: %s %v" ,resolved .Default ,err ) 110 } 111resolved .Required = false 112if cleared ,err := ParseActionField (resolved ,"" );err != nil || string (cleared )!= `""` { 113t .Fatalf ("clearing a prefilled text field restored its old value: %s %v" ,cleared ,err ) 114 } 115field .DefaultBinding .Pointer = "/resource" 116if _ ,err := ResolveFormField (field ,& row ,nil );err == nil { 117t .Fatal ("object prefill accepted for text field" ) 118 } 119for _ ,scenario := range []string {"valid" ,"bad result" ,"bad schema" } { 120t .Run (scenario ,func (t * testing.T ) { 121var calls atomic.Int32 122conn := controlledConnection (t ,func (w http.ResponseWriter ,r * http.Request ) { 123calls .Add (1 ) 124if r .Method != http .MethodPatch || r .URL .String ()!= "/extensions/custom/items/row-a" || r .Header .Get ("If-Match" )!= `"seen"` { 125t .Errorf ("displayed binding not captured: %s %s %v" ,r .Method ,r .URL ,r .Header ) 126 } 127var body map [string ]any 128_ = json .NewDecoder (r .Body ).Decode (& body ) 129if len (body )!= 1 || body ["title" ]!= "edited title" { 130t .Errorf ("optional form field became a write: %v" ,body ) 131 } 132if scenario == "bad result" { 133_ ,_ = io .WriteString (w ,`{"saved":"yes"}` ) 134 }else { 135_ ,_ = io .WriteString (w ,`{"saved":true}` ) 136 } 137 }) 138var operation protocol.ActionOperation 139if err := json .Unmarshal ([]byte (`{"operation_id":"editCustomItem","method":"PATCH","path_template":"/extensions/custom/items/{id}","path":{"id":{"source":"row","pointer":"/id"}},"query":{},"headers":{"If-Match":{"source":"row","pointer":"/resource/etag"}},"body":{"/title":{"source":"form","pointer":"/title"},"/notes":{"source":"form","pointer":"/notes"}},"result_schema":{"type":"object","required":["saved"],"properties":{"saved":{"type":"boolean"}}}}` ),& operation );err != nil { 140t .Fatal (err ) 141 } 142if scenario == "bad schema" { 143operation .ResultSchema = json .RawMessage (`{"$ref":"https://untrusted.example/schema"}` ) 144 } 145_ ,err := executeBoundOperation (t .Context (),conn ,operation ,& row ,map [string ]json.RawMessage {"title" :json .RawMessage (`"edited title"` )},nil ) 146switch scenario { 147case "valid" : 148if err != nil || calls .Load ()!= 1 { 149t .Fatalf ("valid action failed: %v count=%d" ,err ,calls .Load ()) 150 } 151case "bad result" : 152if _ ,uncertain := errors.AsType [* UncertainOutcomeError ](err );! uncertain || calls .Load ()!= 1 { 153t .Fatalf ("invalid result lost uncertainty or replayed: %v count=%d" ,err ,calls .Load ()) 154 } 155case "bad schema" : 156if err == nil || calls .Load ()!= 0 { 157t .Fatalf ("unresolvable schema caused an effect: %v count=%d" ,err ,calls .Load ()) 158 } 159 } 160 }) 161 } 162} 163 164// Input commands and HTTP operations are different catalog variants. An input 165// must not require a fabricated operation descriptor or use a public dispatcher. 166 167func TestAdvertisedInputCommandUsesChosenInputResource (t * testing.T ) { 168command ,err := decodeSessionCommand (json .RawMessage (`{"id":"declared","slash_name":"/declared","description":"Run a declared input","arguments":[],"caller_permissions":["client","model"],"delivery":"input","command_id":"extension.command"}` )) 169if err != nil { 170t .Fatal (err ) 171 } 172conn := controlledConnection (t ,func (w http.ResponseWriter ,r * http.Request ) { 173if r .Method != http .MethodPut || ! strings .HasPrefix (r .URL .Path ,"/sessions/s/inputs/" ) { 174t .Errorf ("input command used another contract: %s %s" ,r .Method ,r .URL ) 175 } 176var body struct { 177Kind string `json:"kind"` 178CommandID string `json:"command_id"` 179Arguments map [string ]json.RawMessage `json:"arguments"` 180 } 181if err := json .NewDecoder (r .Body ).Decode (& body );err != nil || body .Kind != "command" || body .CommandID != "extension.command" || string (body .Arguments ["structured" ])!= `{"enabled":true}` { 182t .Errorf ("structured input intent changed: %#v %v" ,body ,err ) 183 } 184id := strings .TrimPrefix (r .URL .Path ,"/sessions/s/inputs/" ) 185w .WriteHeader (http .StatusAccepted ) 186_ = json .NewEncoder (w ).Encode (canonicalInput ("s" ,id ,"command" ,"pending" )) 187 }) 188result ,err := InvokeDeclaredCommand (t .Context (),conn ,Session {ID :"s" },command ,map [string ]json.RawMessage {"structured" :json .RawMessage (`{"enabled":true}` )}) 189if err != nil || ! result .Submitted { 190t .Fatalf ("declared input was not admitted: %#v %v" ,result ,err ) 191 } 192} 193 194func TestDeclaredReadActionKeepsItsCapturedFilterOnRefresh (t * testing.T ) { 195reads := 0 196conn := controlledConnection (t ,func (w http.ResponseWriter ,r * http.Request ) { 197if r .Method != http .MethodGet { 198t .Errorf ("read caused a mutation: %s" ,r .Method ) 199 } 200if r .URL .Path == "/sessions/s" { 201_ = json .NewEncoder (w ).Encode (canonicalSession ("s" ,generationA ,0 )) 202return 203 } 204if r .URL .String ()!= "/extensions/custom/items?scope=all" { 205t .Errorf ("captured filter was lost: %s" ,r .URL ) 206 } 207reads ++ 208_ = json .NewEncoder (w ).Encode (map [string ]any {"items" : []any {map [string ]any {"version" :reads ,"exact" :json .Number ("9007199254740993" )}}}) 209 }) 210var action PageAction 211if err := json .Unmarshal ([]byte (`{"operation_id":"readCustomItems","method":"GET","path_template":"/extensions/custom/items","path":{},"query":{"scope":{"source":"form","pointer":"/scope"}},"headers":{},"body":{},"result_schema":{}}` ),& action .Operation );err != nil { 212t .Fatal (err ) 213 } 214action .Label = "Custom items" 215form := map [string ]json.RawMessage {"scope" :json .RawMessage (`"all"` )} 216result ,err := ExecutePageAction (t .Context (),conn ,PageActionRequest {Action :action ,Form :form ,Session :& Session {ID :"s" }}) 217if err != nil || result .Page == nil || ! strings .Contains (result .Page .Rows [0 ].Detail ,`"version": 1` )|| ! strings .Contains (result .Page .Rows [0 ].Detail ,`9007199254740993` ) { 218t .Fatalf ("read result was discarded: %#v %v" ,result ,err ) 219 } 220form ["scope" ]= json .RawMessage (`"edited"` ) 221refreshed ,err := RefreshPage (t .Context (),conn ,"s" ,"/custom" ,result .Page ) 222if err != nil || reads != 2 || ! strings .Contains (refreshed .Rows [0 ].Detail ,`"version": 2` ) { 223t .Fatalf ("read refresh changed its intent: %#v %v" ,refreshed ,err ) 224 } 225} 226 227// Adjacent arbitrary JSON integers can identify different domain choices and 228// tool arguments. A float64 intermediate silently changes the selected value.