okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

Aly Raffaufdocs(test): explain coverage and manual checks2f6697a

main
10.3 KiB228 linesraw
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) {
18	var delivered atomic.Int32
19	conn := controlledConnection(t, func(w http.ResponseWriter, r *http.Request) {
20		delivered.Add(1)
21		_, _ = w.Write([]byte(`{}`))
22	})
23	for _, 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	} {
28		var action PageAction
29		if err := json.Unmarshal([]byte(`{"method":"POST","path_template":"/extensions/custom/items","body":`+body+`}`), &action.Operation); err != nil {
30			t.Fatal(err)
31		}
32		if _, err := ExecutePageAction(t.Context(), conn, PageActionRequest{Action: action}); err == nil {
33			t.Errorf("overlapping body was accepted: %s", body)
34		}
35	}
36	if count := delivered.Load(); count != 0 {
37		t.Fatalf("ambiguous actions reached the daemon %d times", count)
38	}
39}
40
41func TestDeclaredActionDeliversSiblingAndEscapedBodyKeys(t *testing.T) {
42	var delivered atomic.Int32
43	conn := controlledConnection(t, func(w http.ResponseWriter, r *http.Request) {
44		var body map[string]any
45		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
46			t.Error(err)
47			return
48		}
49		nested, ok := body["a/b"].(map[string]any)
50		if !ok || nested["~name"] != "one" || nested["second"] != "two" || body["a"] != "separate" {
51			t.Errorf("body bindings changed: %#v", body)
52		}
53		delivered.Add(1)
54		_, _ = w.Write([]byte(`{}`))
55	})
56	var action PageAction
57	if 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 {
58		t.Fatal(err)
59	}
60	if _, err := ExecutePageAction(t.Context(), conn, PageActionRequest{Action: action}); err != nil {
61		t.Fatal(err)
62	}
63	if delivered.Load() != 1 {
64		t.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) {
71	for _, scenario := range []struct {
72		name      string
73		declared  *int64
74		response  int
75		wantError bool
76		wantCalls 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	} {
84		t.Run(scenario.name, func(t *testing.T) {
85			var calls atomic.Int32
86			conn := controlledConnection(t, func(w http.ResponseWriter, r *http.Request) {
87				calls.Add(1)
88				w.WriteHeader(scenario.response)
89				_, _ = io.WriteString(w, `{}`)
90			})
91			operation := protocol.ActionOperation{Method: "POST", PathTemplate: "/extensions/custom/items", SuccessStatus: scenario.declared, ResultSchema: json.RawMessage(`{}`)}
92			_, err := executeBoundOperation(t.Context(), conn, operation, nil, nil, nil)
93			if (err != nil) != scenario.wantError || calls.Load() != scenario.wantCalls {
94				t.Fatalf("status handling: %v, deliveries=%d", err, calls.Load())
95			}
96		})
97	}
98}
99
100func TestDeclaredActionCapturesRowBindingsAndValidatesResult(t *testing.T) {
101	var row PageRow
102	if 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 {
103		t.Fatal(err)
104	}
105	var 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)
107	resolved, err := ResolveFormField(field, &row, nil)
108	if err != nil || string(resolved.Default) != `"displayed title"` {
109		t.Fatalf("row prefill was lost: %s %v", resolved.Default, err)
110	}
111	resolved.Required = false
112	if cleared, err := ParseActionField(resolved, ""); err != nil || string(cleared) != `""` {
113		t.Fatalf("clearing a prefilled text field restored its old value: %s %v", cleared, err)
114	}
115	field.DefaultBinding.Pointer = "/resource"
116	if _, err := ResolveFormField(field, &row, nil); err == nil {
117		t.Fatal("object prefill accepted for text field")
118	}
119	for _, scenario := range []string{"valid", "bad result", "bad schema"} {
120		t.Run(scenario, func(t *testing.T) {
121			var calls atomic.Int32
122			conn := controlledConnection(t, func(w http.ResponseWriter, r *http.Request) {
123				calls.Add(1)
124				if r.Method != http.MethodPatch || r.URL.String() != "/extensions/custom/items/row-a" || r.Header.Get("If-Match") != `"seen"` {
125					t.Errorf("displayed binding not captured: %s %s %v", r.Method, r.URL, r.Header)
126				}
127				var body map[string]any
128				_ = json.NewDecoder(r.Body).Decode(&body)
129				if len(body) != 1 || body["title"] != "edited title" {
130					t.Errorf("optional form field became a write: %v", body)
131				}
132				if scenario == "bad result" {
133					_, _ = io.WriteString(w, `{"saved":"yes"}`)
134				} else {
135					_, _ = io.WriteString(w, `{"saved":true}`)
136				}
137			})
138			var operation protocol.ActionOperation
139			if 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 {
140				t.Fatal(err)
141			}
142			if scenario == "bad schema" {
143				operation.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)
146			switch scenario {
147			case "valid":
148				if err != nil || calls.Load() != 1 {
149					t.Fatalf("valid action failed: %v count=%d", err, calls.Load())
150				}
151			case "bad result":
152				if _, uncertain := errors.AsType[*UncertainOutcomeError](err); !uncertain || calls.Load() != 1 {
153					t.Fatalf("invalid result lost uncertainty or replayed: %v count=%d", err, calls.Load())
154				}
155			case "bad schema":
156				if err == nil || calls.Load() != 0 {
157					t.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) {
168	command, 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"}`))
169	if err != nil {
170		t.Fatal(err)
171	}
172	conn := controlledConnection(t, func(w http.ResponseWriter, r *http.Request) {
173		if r.Method != http.MethodPut || !strings.HasPrefix(r.URL.Path, "/sessions/s/inputs/") {
174			t.Errorf("input command used another contract: %s %s", r.Method, r.URL)
175		}
176		var body struct {
177			Kind      string                     `json:"kind"`
178			CommandID string                     `json:"command_id"`
179			Arguments map[string]json.RawMessage `json:"arguments"`
180		}
181		if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Kind != "command" || body.CommandID != "extension.command" || string(body.Arguments["structured"]) != `{"enabled":true}` {
182			t.Errorf("structured input intent changed: %#v %v", body, err)
183		}
184		id := strings.TrimPrefix(r.URL.Path, "/sessions/s/inputs/")
185		w.WriteHeader(http.StatusAccepted)
186		_ = json.NewEncoder(w).Encode(canonicalInput("s", id, "command", "pending"))
187	})
188	result, err := InvokeDeclaredCommand(t.Context(), conn, Session{ID: "s"}, command, map[string]json.RawMessage{"structured": json.RawMessage(`{"enabled":true}`)})
189	if err != nil || !result.Submitted {
190		t.Fatalf("declared input was not admitted: %#v %v", result, err)
191	}
192}
193
194func TestDeclaredReadActionKeepsItsCapturedFilterOnRefresh(t *testing.T) {
195	reads := 0
196	conn := controlledConnection(t, func(w http.ResponseWriter, r *http.Request) {
197		if r.Method != http.MethodGet {
198			t.Errorf("read caused a mutation: %s", r.Method)
199		}
200		if r.URL.Path == "/sessions/s" {
201			_ = json.NewEncoder(w).Encode(canonicalSession("s", generationA, 0))
202			return
203		}
204		if r.URL.String() != "/extensions/custom/items?scope=all" {
205			t.Errorf("captured filter was lost: %s", r.URL)
206		}
207		reads++
208		_ = json.NewEncoder(w).Encode(map[string]any{"items": []any{map[string]any{"version": reads, "exact": json.Number("9007199254740993")}}})
209	})
210	var action PageAction
211	if 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 {
212		t.Fatal(err)
213	}
214	action.Label = "Custom items"
215	form := map[string]json.RawMessage{"scope": json.RawMessage(`"all"`)}
216	result, err := ExecutePageAction(t.Context(), conn, PageActionRequest{Action: action, Form: form, Session: &Session{ID: "s"}})
217	if err != nil || result.Page == nil || !strings.Contains(result.Page.Rows[0].Detail, `"version": 1`) || !strings.Contains(result.Page.Rows[0].Detail, `9007199254740993`) {
218		t.Fatalf("read result was discarded: %#v %v", result, err)
219	}
220	form["scope"] = json.RawMessage(`"edited"`)
221	refreshed, err := RefreshPage(t.Context(), conn, "s", "/custom", result.Page)
222	if err != nil || reads != 2 || !strings.Contains(refreshed.Rows[0].Detail, `"version": 2`) {
223		t.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.