返回 DeepSeek-Reasonix
schema_test.go
根目录 / internal / extension / protocol / schema_test.go
1 package protocol
2
3 import (
4 "bytes"
5 "crypto/sha256"
6 "encoding/hex"
7 "encoding/json"
8 "regexp"
9 "strings"
10 "testing"
11 )
12
13 var schemaHashPattern = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`)
14
15 func TestSchemaHashMatchesCanonicalBytes(t *testing.T) {
16 if !schemaHashPattern.MatchString(SchemaHash()) {
17 t.Fatalf("SchemaHash = %q, want sha256:<64 lowercase hex>", SchemaHash())
18 }
19 canonical, err := CanonicalSchemaBytes()
20 if err != nil {
21 t.Fatal(err)
22 }
23 digest := sha256.Sum256(canonical)
24 recomputed := "sha256:" + hex.EncodeToString(digest[:])
25 if recomputed != SchemaHash() {
26 t.Fatalf("committed GeneratedSchemaHash %s does not match recomputed %s (run go run ./cmd/extension-protocol-gen -root .)",
27 SchemaHash(), recomputed)
28 }
29 }
30
31 func TestCanonicalSchemaBytesAreDeterministic(t *testing.T) {
32 first, err := CanonicalSchemaBytes()
33 if err != nil {
34 t.Fatal(err)
35 }
36 second, err := CanonicalSchemaBytes()
37 if err != nil {
38 t.Fatal(err)
39 }
40 if !bytes.Equal(first, second) {
41 t.Fatal("CanonicalSchemaBytes differ across calls")
42 }
43 // A fresh document build must marshal to the identical bytes, proving
44 // determinism is a property of the builder, not of the sync.Once cache.
45 fresh, err := BuildSchemaDocument()
46 if err != nil {
47 t.Fatal(err)
48 }
49 encoded, err := json.Marshal(fresh)
50 if err != nil {
51 t.Fatal(err)
52 }
53 if !bytes.Equal(first, encoded) {
54 t.Fatal("freshly built schema document does not match canonical bytes")
55 }
56 }
57
58 func TestSchemaDocumentStructure(t *testing.T) {
59 document, err := BuildSchemaDocument()
60 if err != nil {
61 t.Fatal(err)
62 }
63 if document["$schema"] != SchemaDraft202012 {
64 t.Fatalf("$schema = %v", document["$schema"])
65 }
66 if document["$id"] != ProtocolID || document["protocol"] != ProtocolID || document["protocolID"] != ProtocolID {
67 t.Fatal("schema identity fields do not carry the protocol ID")
68 }
69 if document["protocolMajor"] != ProtocolMajor {
70 t.Fatalf("protocolMajor = %v", document["protocolMajor"])
71 }
72
73 defs, ok := document["$defs"].(map[string]any)
74 if !ok || len(defs) == 0 {
75 t.Fatal("$defs is missing or empty")
76 }
77 methods, ok := document["methods"].(map[string]any)
78 if !ok {
79 t.Fatal("methods is missing")
80 }
81 if len(methods) != 16 {
82 t.Fatalf("methods has %d entries, want 16", len(methods))
83 }
84
85 for _, spec := range Registry() {
86 entry, ok := methods[string(spec.Name)].(map[string]any)
87 if !ok {
88 t.Fatalf("methods.%s missing", spec.Name)
89 }
90 if entry["direction"] != string(spec.Direction) || entry["class"] != string(spec.Class) {
91 t.Fatalf("methods.%s metadata = %v/%v", spec.Name, entry["direction"], entry["class"])
92 }
93 if entry["notification"] != spec.Notification() {
94 t.Fatalf("methods.%s notification flag wrong", spec.Name)
95 }
96 paramsRef, ok := entry["params"].(map[string]any)
97 if !ok {
98 t.Fatalf("methods.%s params is not a $ref", spec.Name)
99 }
100 assertRefResolves(t, defs, paramsRef["$ref"], string(spec.Name)+" params")
101 if spec.Notification() {
102 if entry["result"] != nil {
103 t.Fatalf("methods.%s notification result = %v, want null", spec.Name, entry["result"])
104 }
105 } else {
106 resultRef, ok := entry["result"].(map[string]any)
107 if !ok {
108 t.Fatalf("methods.%s result is not a $ref", spec.Name)
109 }
110 assertRefResolves(t, defs, resultRef["$ref"], string(spec.Name)+" result")
111 }
112 }
113
114 // Every $defs entry must be a closed object schema.
115 for name, raw := range defs {
116 object, ok := raw.(map[string]any)
117 if !ok {
118 t.Fatalf("$defs.%s is not an object schema", name)
119 }
120 if object["type"] != "object" || object["additionalProperties"] != false {
121 t.Fatalf("$defs.%s = type %v additionalProperties %v, want closed object", name, object["type"], object["additionalProperties"])
122 }
123 }
124
125 // Frozen enums, events, limits, and errors land in the document.
126 events, ok := document["interceptEvents"].([]any)
127 if !ok || len(events) != 17 {
128 t.Fatalf("interceptEvents = %v", document["interceptEvents"])
129 }
130 eventDef := defs["EventParams"].(map[string]any)["properties"].(map[string]any)["event"].(map[string]any)
131 if enum, ok := eventDef["enum"].([]any); !ok || len(enum) != 17 {
132 t.Fatalf("EventParams.event enum = %v", eventDef["enum"])
133 }
134 limits, ok := document["limits"].(map[string]any)
135 if !ok || limits["frameBytes"] != FrameBytes || limits["externalizeFieldBytes"] != ExternalizeFieldBytes ||
136 limits["contentRefChunkBytes"] != ContentRefChunkBytes || limits["contentRefObjectBytes"] != ContentRefObjectBytes {
137 t.Fatalf("limits = %v", document["limits"])
138 }
139 if errs, ok := document["errors"].([]any); !ok || len(errs) != len(frozenErrorSpecs) {
140 t.Fatalf("errors = %v", document["errors"])
141 }
142
143 // Tag-derived constraints: minLength from nonempty, minimum from min=,
144 // x-externalizable from the externalizable tag.
145 contentRef := defs["ContentReadParams"].(map[string]any)["properties"].(map[string]any)["contentRef"].(map[string]any)
146 if contentRef["minLength"] != 1 {
147 t.Fatalf("contentRef schema = %v, want minLength 1", contentRef)
148 }
149 offset := defs["ContentReadParams"].(map[string]any)["properties"].(map[string]any)["offset"].(map[string]any)
150 if offset["minimum"] != float64(0) {
151 t.Fatalf("offset schema = %v, want minimum 0", offset)
152 }
153 payload := defs["EventParams"].(map[string]any)["properties"].(map[string]any)["payload"]
154 if payloadMap, ok := payload.(map[string]any); !ok || payloadMap["x-externalizable"] != true {
155 t.Fatalf("payload schema = %v, want x-externalizable annotation", payload)
156 }
157 }
158
159 func assertRefResolves(t *testing.T, defs map[string]any, ref any, at string) {
160 t.Helper()
161 name, ok := ref.(string)
162 if !ok || !strings.HasPrefix(name, "#/$defs/") {
163 t.Fatalf("%s ref = %v, want #/$defs/<name>", at, ref)
164 }
165 if _, ok := defs[strings.TrimPrefix(name, "#/$defs/")]; !ok {
166 t.Fatalf("%s ref %s does not resolve in $defs", at, name)
167 }
168 }
169
169 lines GO