Skip to content

Commit 6ff9d74

Browse files
ernadoclaude
andcommitted
test(conformance): guard against Bot API method drift
Add a conformance test that parses the published Bot API surface via the internal/botdoc extractor and asserts every method is implemented on *Bot, covered by other means (getMe -> Self), or explicitly categorized as deferred or not-applicable (getUpdates/webhooks, logOut/close). Fails on any uncategorized method (drift when Telegram ships a new one) and on stale allowlist entries that are no longer published. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1e2a46b commit 6ff9d74

2 files changed

Lines changed: 139 additions & 1 deletion

File tree

conformance_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package botapi
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"reflect"
7+
"testing"
8+
9+
"github.com/PuerkitoBio/goquery"
10+
11+
"github.com/gotd/botapi/internal/botdoc"
12+
)
13+
14+
// coveredByOtherMeans lists published methods the library satisfies through a
15+
// different API shape than a same-named method, mapped to where.
16+
var coveredByOtherMeans = map[string]string{
17+
"getMe": "Bot.Self()",
18+
}
19+
20+
// deferredMethods lists published methods that are planned but not yet
21+
// implemented. Each is an acknowledged gap tracked in docs/roadmap.md; the
22+
// conformance test allows them so it can still catch *unacknowledged* drift.
23+
var deferredMethods = map[string]string{
24+
"editMessageMedia": "Phase 3 deferred",
25+
"answerPreCheckoutQuery": "payments — deferred until payment updates land",
26+
"answerShippingQuery": "payments — deferred until payment updates land",
27+
"sendInvoice": "payments — deferred",
28+
"setPassportDataErrors": "Telegram Passport — deferred",
29+
"sendGame": "games — deferred",
30+
"setGameScore": "games — deferred",
31+
"getGameHighScores": "games — deferred",
32+
"getStickerSet": "stickers — deferred (needs Sticker[] conversion)",
33+
"setStickerSetThumb": "stickers — deferred",
34+
"getChat": "chat info — deferred",
35+
"getUserProfilePhotos": "deferred",
36+
"setChatStickerSet": "chat sticker set — deferred",
37+
"deleteChatStickerSet": "chat sticker set — deferred",
38+
"setChatAdministratorCustomTitle": "member management — deferred",
39+
"stopPoll": "deferred",
40+
}
41+
42+
// notApplicableMethods lists published methods that do not apply to the
43+
// MTProto-native model and are intentionally not implemented.
44+
var notApplicableMethods = map[string]string{
45+
"getUpdates": "MTProto-native: updates arrive on the persistent connection (Decision #2)",
46+
"setWebhook": "no webhook surface (Decision #2)",
47+
"deleteWebhook": "no webhook surface (Decision #2)",
48+
"getWebhookInfo": "no webhook surface (Decision #2)",
49+
"logOut": "HTTP Bot API server lifecycle method; not applicable over MTProto",
50+
"close": "HTTP Bot API server lifecycle method; not applicable over MTProto",
51+
}
52+
53+
// TestMethodConformance asserts that every method published in the Bot API docs
54+
// is either implemented on *Bot, covered by other means, or explicitly
55+
// acknowledged as deferred / not-applicable. It is a drift guard: when Telegram
56+
// ships a new method, this test fails until the method is implemented or
57+
// categorized. It also fails if an allowlist entry stops being a published
58+
// method (so the lists cannot rot).
59+
func TestMethodConformance(t *testing.T) {
60+
api := loadAPI(t)
61+
62+
implemented := botMethodSet()
63+
published := map[string]struct{}{}
64+
65+
var uncategorized []string
66+
for _, def := range api.Methods {
67+
name := def.Name
68+
published[name] = struct{}{}
69+
70+
switch {
71+
case implemented[goName(name)]:
72+
case coveredByOtherMeans[name] != "":
73+
case deferredMethods[name] != "":
74+
case notApplicableMethods[name] != "":
75+
default:
76+
uncategorized = append(uncategorized, name)
77+
}
78+
}
79+
80+
if len(uncategorized) > 0 {
81+
t.Errorf("API drift: %d published method(s) are neither implemented nor categorized: %v\n"+
82+
"Implement them, or add to deferredMethods/notApplicableMethods in conformance_test.go.",
83+
len(uncategorized), uncategorized)
84+
}
85+
86+
// Guard against stale allowlists: every acknowledged name must still be a
87+
// published method.
88+
for _, lists := range []map[string]string{coveredByOtherMeans, deferredMethods, notApplicableMethods} {
89+
for name := range lists {
90+
if _, ok := published[name]; !ok {
91+
t.Errorf("stale allowlist entry %q is no longer a published method; remove it", name)
92+
}
93+
}
94+
}
95+
}
96+
97+
// loadAPI extracts the Bot API surface from the committed documentation
98+
// snapshot.
99+
func loadAPI(t *testing.T) botdoc.API {
100+
t.Helper()
101+
path := filepath.Join("internal", "botdoc", "_testdata", "api.html")
102+
f, err := os.Open(path)
103+
if err != nil {
104+
t.Fatalf("open api docs: %v", err)
105+
}
106+
defer func() { _ = f.Close() }()
107+
doc, err := goquery.NewDocumentFromReader(f)
108+
if err != nil {
109+
t.Fatalf("parse api docs: %v", err)
110+
}
111+
return botdoc.Extract(doc)
112+
}
113+
114+
// botMethodSet returns the set of exported method names on *Bot.
115+
func botMethodSet() map[string]bool {
116+
t := reflect.TypeFor[*Bot]()
117+
set := make(map[string]bool, t.NumMethod())
118+
for i := 0; i < t.NumMethod(); i++ {
119+
set[t.Method(i).Name] = true
120+
}
121+
return set
122+
}
123+
124+
// goName maps a Bot API method name (lowerCamelCase) to the Go method name that
125+
// would implement it (UpperCamelCase): only the first letter changes.
126+
func goName(apiName string) string {
127+
if apiName == "" {
128+
return ""
129+
}
130+
b := []byte(apiName)
131+
if b[0] >= 'a' && b[0] <= 'z' {
132+
b[0] -= 'a' - 'A'
133+
}
134+
return string(b)
135+
}

docs/roadmap.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,10 @@ Deferred within Phase 5: payment answers
175175
`examples/media` (send by URL, echo incoming media by file_id, `GetFile`).
176176
- ☑ Allocation tests on hot paths (`bench_test.go`): entity/markup/user
177177
conversion and `file_unique_id`, with `-benchmem`.
178-
- ☐ Conformance test against kept `botdoc` extractor (API-version drift guard)
178+
- ☑ Conformance test against the `botdoc` extractor (`conformance_test.go`):
179+
every published method must be implemented on `*Bot`, covered by other means,
180+
or categorized as deferred/not-applicable — failing on uncategorized (drift)
181+
methods or stale allowlist entries.
179182
- ◐ Docs: package docs (`doc.go`) and README done; reference/migration guide
180183
still to write.
181184

0 commit comments

Comments
 (0)