Skip to content

Commit 0024a9c

Browse files
committed
test(e2e): add mock backend tests for /v1/detokenize
Add Detokenize to the mock gRPC backend and wire up two e2e tests in the MockBackend suite: one that posts known token IDs and asserts a non-empty content response, and a round-trip that tokenizes first then detokenizes the returned IDs. Addresses reviewer feedback on #9620. Assisted-by: Claude:claude-sonnet-4-6
1 parent b2f4b7e commit 0024a9c

2 files changed

Lines changed: 83 additions & 0 deletions

File tree

tests/e2e/mock-backend/main.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,17 @@ func (m *MockBackend) TokenizeString(ctx context.Context, in *pb.PredictOptions)
489489
}, nil
490490
}
491491

492+
func (m *MockBackend) Detokenize(ctx context.Context, in *pb.DetokenizeRequest) (*pb.DetokenizeResponse, error) {
493+
xlog.Debug("Detokenize called", "tokens", in.Tokens)
494+
parts := make([]string, len(in.Tokens))
495+
for i, t := range in.Tokens {
496+
parts[i] = strconv.Itoa(int(t))
497+
}
498+
return &pb.DetokenizeResponse{
499+
Content: "detokenized: " + strings.Join(parts, " "),
500+
}, nil
501+
}
502+
492503
func (m *MockBackend) Status(ctx context.Context, in *pb.HealthMessage) (*pb.StatusResponse, error) {
493504
xlog.Debug("Status called")
494505
return &pb.StatusResponse{

tests/e2e/mock_backend_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,78 @@ var _ = Describe("Mock Backend E2E Tests", Label("MockBackend"), func() {
267267
})
268268
})
269269

270+
Describe("Detokenization API", func() {
271+
It("should return content for known token IDs", func() {
272+
body := `{"model":"mock-model","tokens":[101,2023,2003,1037,3231,1012]}`
273+
req, err := http.NewRequest("POST", apiURL+"/detokenize", strings.NewReader(body))
274+
Expect(err).ToNot(HaveOccurred())
275+
req.Header.Set("Content-Type", "application/json")
276+
277+
httpClient := &http.Client{Timeout: 30 * time.Second}
278+
resp, err := httpClient.Do(req)
279+
Expect(err).ToNot(HaveOccurred())
280+
defer resp.Body.Close()
281+
Expect(resp.StatusCode).To(Equal(200))
282+
283+
data, err := io.ReadAll(resp.Body)
284+
Expect(err).ToNot(HaveOccurred())
285+
var result map[string]any
286+
Expect(json.Unmarshal(data, &result)).To(Succeed())
287+
content, ok := result["content"].(string)
288+
Expect(ok).To(BeTrue(), "response missing 'content' field: %s", string(data))
289+
Expect(content).ToNot(BeEmpty())
290+
})
291+
292+
It("should round-trip tokenize then detokenize", func() {
293+
httpClient := &http.Client{Timeout: 30 * time.Second}
294+
295+
// Step 1: tokenize
296+
tokenizeReq, err := http.NewRequest("POST", apiURL+"/tokenize",
297+
strings.NewReader(`{"model":"mock-model","content":"Hello world"}`))
298+
Expect(err).ToNot(HaveOccurred())
299+
tokenizeReq.Header.Set("Content-Type", "application/json")
300+
301+
tokenizeResp, err := httpClient.Do(tokenizeReq)
302+
Expect(err).ToNot(HaveOccurred())
303+
defer tokenizeResp.Body.Close()
304+
Expect(tokenizeResp.StatusCode).To(Equal(200))
305+
306+
tokenizeData, err := io.ReadAll(tokenizeResp.Body)
307+
Expect(err).ToNot(HaveOccurred())
308+
var tokenizeResult map[string]any
309+
Expect(json.Unmarshal(tokenizeData, &tokenizeResult)).To(Succeed())
310+
311+
tokensRaw, ok := tokenizeResult["tokens"].([]any)
312+
Expect(ok).To(BeTrue(), "tokenize response missing 'tokens': %s", string(tokenizeData))
313+
Expect(tokensRaw).ToNot(BeEmpty())
314+
315+
// Step 2: detokenize the returned token IDs
316+
tokens := make([]int, len(tokensRaw))
317+
for i, t := range tokensRaw {
318+
tokens[i] = int(t.(float64))
319+
}
320+
tokenJSON, err := json.Marshal(map[string]any{"model": "mock-model", "tokens": tokens})
321+
Expect(err).ToNot(HaveOccurred())
322+
323+
detokenizeReq, err := http.NewRequest("POST", apiURL+"/detokenize", strings.NewReader(string(tokenJSON)))
324+
Expect(err).ToNot(HaveOccurred())
325+
detokenizeReq.Header.Set("Content-Type", "application/json")
326+
327+
detokenizeResp, err := httpClient.Do(detokenizeReq)
328+
Expect(err).ToNot(HaveOccurred())
329+
defer detokenizeResp.Body.Close()
330+
Expect(detokenizeResp.StatusCode).To(Equal(200))
331+
332+
detokenizeData, err := io.ReadAll(detokenizeResp.Body)
333+
Expect(err).ToNot(HaveOccurred())
334+
var detokenizeResult map[string]any
335+
Expect(json.Unmarshal(detokenizeData, &detokenizeResult)).To(Succeed())
336+
content, ok := detokenizeResult["content"].(string)
337+
Expect(ok).To(BeTrue(), "detokenize response missing 'content': %s", string(detokenizeData))
338+
Expect(content).ToNot(BeEmpty())
339+
})
340+
})
341+
270342
Describe("Autoparser ChatDelta Streaming", Label("Autoparser"), func() {
271343
// These tests verify that when the C++ autoparser handles tool calls
272344
// and content via ChatDeltas (with empty raw message), the streaming

0 commit comments

Comments
 (0)