eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1package bot
2
3// The sidecar half of the views contract (docs/views.md): normalizeViewSpec
4// fails closed with errors the model can fix. The client fold's half is
5// pinned by test/event-vectors/views.json + test/views.test.mjs — keep the
6// caps here and there in step.
7
8import (
9 "strings"
10 "testing"
11)
12
13func viewInput(mut func(map[string]any)) map[string]any {
14 in := map[string]any{
15 "title": "Top GitHub issues",
16 "glyph": "⬢",
17 "items": []any{
18 map[string]any{"title": "Crash on login", "subtitle": "#412 · amy", "note": "urgent", "url": "https://github.com/o/r/issues/412", "accent": true},
19 map[string]any{"title": "Dark mode flicker"},
20 },
21 }
22 if mut != nil {
23 mut(in)
24 }
25 return in
26}
27
28func TestNormalizeViewSpec(t *testing.T) {
29 spec, err := normalizeViewSpec(viewInput(nil))
30 if err != nil {
31 t.Fatalf("valid spec refused: %v", err)
32 }
33 items := spec["items"].([]map[string]any)
34 if len(items) != 2 || items[0]["title"] != "Crash on login" || items[0]["accent"] != true {
35 t.Fatalf("items: %+v", items)
36 }
37 // Positional ids keep the schema's keyed list stable.
38 if items[0]["id"] != "i0" || items[1]["id"] != "i1" {
39 t.Errorf("ids: %v %v", items[0]["id"], items[1]["id"])
40 }
41 // Absent optionals are absent, not "".
42 if _, present := items[1]["url"]; present {
43 t.Errorf("empty url should be omitted: %+v", items[1])
44 }
45}
46
47func TestNormalizeViewSpecRefusals(t *testing.T) {
48 cases := []struct {
49 name string
50 mut func(map[string]any)
51 want string
52 }{
53 {"no title", func(m map[string]any) { m["title"] = "" }, "title"},
54 {"title too long", func(m map[string]any) { m["title"] = strings.Repeat("x", 81) }, "title"},
55 {"no items", func(m map[string]any) { m["items"] = []any{} }, "items"},
56 {"items not array", func(m map[string]any) { m["items"] = "nope" }, "items"},
57 {"item missing title", func(m map[string]any) { m["items"] = []any{map[string]any{"subtitle": "s"}} }, "items[0].title"},
58 {"http url", func(m map[string]any) { m["items"] = []any{map[string]any{"title": "t", "url": "http://x.example"}} }, "https"},
59 {"javascript url", func(m map[string]any) { m["items"] = []any{map[string]any{"title": "t", "url": "javascript:alert(1)"}} }, "https"},
60 {"long glyph", func(m map[string]any) { m["glyph"] = "abcdef" }, "glyph"},
61 }
62 for _, c := range cases {
63 t.Run(c.name, func(t *testing.T) {
64 if _, err := normalizeViewSpec(viewInput(c.mut)); err == nil || !strings.Contains(err.Error(), c.want) {
65 t.Errorf("want refusal mentioning %q, got %v", c.want, err)
66 }
67 })
68 }
69
70 // 51 items refuse (the model trims — unlike the fold, which clamps).
71 many := make([]any, 51)
72 for i := range many {
73 many[i] = map[string]any{"title": "row"}
74 }
75 if _, err := normalizeViewSpec(viewInput(func(m map[string]any) { m["items"] = many })); err == nil || !strings.Contains(err.Error(), "items") {
76 t.Errorf("51 items: %v", err)
77 }
78
79 // Over the byte cap refuses honestly.
80 big := make([]any, 50)
81 for i := range big {
82 big[i] = map[string]any{"title": strings.Repeat("t", 300), "note": strings.Repeat("n", 500)}
83 }
84 if _, err := normalizeViewSpec(viewInput(func(m map[string]any) { m["items"] = big })); err == nil || !strings.Contains(err.Error(), "large") {
85 t.Errorf("oversized view: %v", err)
86 }
87}
88
89func TestCreateViewToolDeclared(t *testing.T) {
90 // The tool must be in both palettes: the conversational brain's and the
91 // research loop's (where it is the one non-service tool).
92 has := func(tools []anthTool) bool {
93 for _, tl := range tools {
94 if tl.Name == "create_view" {
95 return true
96 }
97 }
98 return false
99 }
100 r := &Runtime{}
101 if !has(r.replyTools(&ThreadCtx{Kind: "dm"})) {
102 t.Error("create_view missing from the conversational palette")
103 }
104 if !has(researchTools([]string{"github"})) {
105 t.Error("create_view missing from the research palette")
106 }
107}
108