🤖 test: exercise sibling custody across document routes
Known-red regression evidence pending independent design approval. Search control passes; list, document and manual deletion routes reproduce the inherited release finding.
Co-Authored-By: Codex <noreply@openai.com>
1 file changed,
+150
−0
internal/memoriatest/custody_routes_test.go+150 −0
CI failed — run details
diff --git a/internal/memoriatest/custody_routes_test.go b/internal/memoriatest/custody_routes_test.go| new file mode 100644 |
| index 0000000..6e19678 |
| --- /dev/null |
| +++ b/internal/memoriatest/custody_routes_test.go |
| @@ -0,0 +1,150 @@ |
| +package memoriatest |
| + |
| +import ( |
| + "context" |
| + "fmt" |
| + "io" |
| + "log/slog" |
| + "net/http" |
| + "path/filepath" |
| + "strings" |
| + "testing" |
| + |
| + "github.com/carlosframework/rastrillo/db" |
| + |
| + memoria "amadan.net/oficina/memoria/internal/memoria" |
| + "amadan.net/oficina/memoria/internal/memoria/sibling" |
| + "amadan.net/oficina/suite" |
| +) |
| + |
| +type routeCustody struct { |
| + answer suite.Authorization |
| + asks []suite.Subject |
| + pairs []suite.ItemRevision |
| +} |
| + |
| +func (c *routeCustody) Sibling(source string) (sibling.Sibling, bool) { |
| + return sibling.Sibling{Name: "Docs"}, source == "Docs" |
| +} |
| + |
| +func (c *routeCustody) Rule(_ context.Context, who suite.Subject, candidates []sibling.Candidate) []sibling.Disposition { |
| + c.asks = append(c.asks, who) |
| + out := make([]sibling.Disposition, len(candidates)) |
| + for i, candidate := range candidates { |
| + c.pairs = append(c.pairs, candidate.Item) |
| + out[i] = sibling.Decide(c.answer) |
| + } |
| + return out |
| +} |
| + |
| +func custodyRouteApp(t *testing.T, custody memoria.Custody) (http.Handler, *memoria.Store, int64, int64) { |
| + t.Helper() |
| + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) |
| + d, err := db.Open(filepath.Join(t.TempDir(), "app.db"), logger) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Cleanup(func() { d.Close() }) |
| + h, app, err := memoria.AppForTest(d, testOrigin, testDEK(t), logger) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + app.AsPerson("reader@example.test") |
| + app.UseSiblings(custody) |
| + s := app.Store() |
| + siblingDoc, err := s.Ingest(context.Background(), memoria.IngestSpec{ |
| + Source: "Docs", Key: "route-sibling", Title: "SiblingTitleCanary", |
| + Body: "## SiblingHeadingCanary\n\nSiblingBodyCanary quarterly planning.", |
| + ItemID: "sibling-item-route", SiblingRevision: 19, |
| + }) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + localDoc, err := s.Ingest(context.Background(), memoria.IngestSpec{ |
| + Source: "upload", Key: "route-local", Title: "LocalTitleControl", |
| + Body: "LocalBodyControl quarterly planning.", |
| + }) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + return h, s, siblingDoc, localDoc |
| +} |
| + |
| +// Baseline regression evidence: these HTTP routes ingest actual sibling |
| +// coordinates, while retaining local content as a positive control. |
| +// Independent reviewer mutations will be recorded after design approval. |
| +func TestSiblingDocumentHTTPRequiresCustody(t *testing.T) { |
| + for _, answer := range []suite.Authorization{suite.NOT_VISIBLE, suite.STALE, suite.UNKNOWN, suite.VISIBLE} { |
| + for _, route := range []string{"/", "/?q=quarterly", "/search?q=quarterly", "/documents/%d"} { |
| + t.Run(string(answer)+route, func(t *testing.T) { |
| + custody := &routeCustody{answer: answer} |
| + h, _, id, localID := custodyRouteApp(t, custody) |
| + target := route |
| + if strings.Contains(route, "%d") { |
| + target = fmt.Sprintf(route, id) |
| + } |
| + r := get(t, h, target) |
| + body := r.Body.String() |
| + if answer == suite.VISIBLE { |
| + if r.Code != http.StatusOK || !strings.Contains(body, "SiblingTitleCanary") { |
| + t.Errorf("visible document not rendered: status %d", r.Code) |
| + } |
| + } else { |
| + for _, secret := range []string{"SiblingTitleCanary", "SiblingHeadingCanary", "SiblingBodyCanary"} { |
| + if strings.Contains(body, secret) { |
| + t.Errorf("unruled sibling field rendered: %s", secret) |
| + } |
| + } |
| + } |
| + if len(custody.asks) == 0 { |
| + t.Error("route did not obtain custody ruling") |
| + } |
| + for _, who := range custody.asks { |
| + if who.Kind != suite.SubjectAddress || who.Value != "reader@example.test" { |
| + t.Errorf("wrong requester: %#v", who) |
| + } |
| + } |
| + for _, pair := range custody.pairs { |
| + if pair.ItemID != "sibling-item-route" || pair.Revision != 19 { |
| + t.Errorf("wrong source coordinate: %#v", pair) |
| + } |
| + } |
| + if route != "/documents/%d" && !strings.Contains(body, "LocalTitleControl") { |
| + t.Error("local list control absent") |
| + } |
| + local := get(t, h, fmt.Sprintf("/documents/%d", localID)) |
| + if local.Code != http.StatusOK || !strings.Contains(local.Body.String(), "LocalBodyControl") { |
| + t.Error("local document control absent") |
| + } |
| + }) |
| + } |
| + } |
| +} |
| + |
| +func TestSiblingDocumentHTTPManualDeleteHasNoAuthority(t *testing.T) { |
| + custody := &routeCustody{answer: suite.NOT_VISIBLE} |
| + h, s, id, localID := custodyRouteApp(t, custody) |
| + before, err := s.Count() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + r := post(t, h, fmt.Sprintf("/documents/%d/delete", id), nil) |
| + if r.Code != http.StatusNotFound { |
| + t.Errorf("sibling delete status %d, want404", r.Code) |
| + } |
| + _, body, err := s.Body(context.Background(), id) |
| + if err != nil || !strings.Contains(body, "SiblingBodyCanary") { |
| + t.Errorf("sibling document removed: %v", err) |
| + } |
| + after, err := s.Count() |
| + if err != nil || before != after { |
| + t.Errorf("index changed on refused deletion: before%d after%d err%v", before, after, err) |
| + } |
| + local := post(t, h, fmt.Sprintf("/documents/%d/delete", localID), nil) |
| + if local.Code != http.StatusSeeOther { |
| + t.Errorf("local deletion status%d", local.Code) |
| + } |
| + if _, _, err := s.Body(context.Background(), localID); err == nil { |
| + t.Error("local deletion did not remove row") |
| + } |
| +} |