Skip to content

Commit c168bcb

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 Signed-off-by: Adira Denis Muhando <dennisadira@gmail.com>
1 parent d1e01cc commit c168bcb

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
@@ -549,6 +549,17 @@ func (m *MockBackend) TokenizeString(ctx context.Context, in *pb.PredictOptions)
549549
}, nil
550550
}
551551

552+
func (m *MockBackend) Detokenize(ctx context.Context, in *pb.DetokenizeRequest) (*pb.DetokenizeResponse, error) {
553+
xlog.Debug("Detokenize called", "tokens", in.Tokens)
554+
parts := make([]string, len(in.Tokens))
555+
for i, t := range in.Tokens {
556+
parts[i] = strconv.Itoa(int(t))
557+
}
558+
return &pb.DetokenizeResponse{
559+
Content: "detokenized: " + strings.Join(parts, " "),
560+
}, nil
561+
}
562+
552563
func (m *MockBackend) Status(ctx context.Context, in *pb.HealthMessage) (*pb.StatusResponse, error) {
553564
xlog.Debug("Status called")
554565
return &pb.StatusResponse{

tests/e2e/mock_backend_test.go

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

390+
Describe("Detokenization API", func() {
391+
It("should return content for known token IDs", func() {
392+
body := `{"model":"mock-model","tokens":[101,2023,2003,1037,3231,1012]}`
393+
req, err := http.NewRequest("POST", apiURL+"/detokenize", strings.NewReader(body))
394+
Expect(err).ToNot(HaveOccurred())
395+
req.Header.Set("Content-Type", "application/json")
396+
397+
httpClient := &http.Client{Timeout: 30 * time.Second}
398+
resp, err := httpClient.Do(req)
399+
Expect(err).ToNot(HaveOccurred())
400+
defer resp.Body.Close()
401+
Expect(resp.StatusCode).To(Equal(200))
402+
403+
data, err := io.ReadAll(resp.Body)
404+
Expect(err).ToNot(HaveOccurred())
405+
var result map[string]any
406+
Expect(json.Unmarshal(data, &result)).To(Succeed())
407+
content, ok := result["content"].(string)
408+
Expect(ok).To(BeTrue(), "response missing 'content' field: %s", string(data))
409+
Expect(content).ToNot(BeEmpty())
410+
})
411+
412+
It("should round-trip tokenize then detokenize", func() {
413+
httpClient := &http.Client{Timeout: 30 * time.Second}
414+
415+
// Step 1: tokenize
416+
tokenizeReq, err := http.NewRequest("POST", apiURL+"/tokenize",
417+
strings.NewReader(`{"model":"mock-model","content":"Hello world"}`))
418+
Expect(err).ToNot(HaveOccurred())
419+
tokenizeReq.Header.Set("Content-Type", "application/json")
420+
421+
tokenizeResp, err := httpClient.Do(tokenizeReq)
422+
Expect(err).ToNot(HaveOccurred())
423+
defer tokenizeResp.Body.Close()
424+
Expect(tokenizeResp.StatusCode).To(Equal(200))
425+
426+
tokenizeData, err := io.ReadAll(tokenizeResp.Body)
427+
Expect(err).ToNot(HaveOccurred())
428+
var tokenizeResult map[string]any
429+
Expect(json.Unmarshal(tokenizeData, &tokenizeResult)).To(Succeed())
430+
431+
tokensRaw, ok := tokenizeResult["tokens"].([]any)
432+
Expect(ok).To(BeTrue(), "tokenize response missing 'tokens': %s", string(tokenizeData))
433+
Expect(tokensRaw).ToNot(BeEmpty())
434+
435+
// Step 2: detokenize the returned token IDs
436+
tokens := make([]int, len(tokensRaw))
437+
for i, t := range tokensRaw {
438+
tokens[i] = int(t.(float64))
439+
}
440+
tokenJSON, err := json.Marshal(map[string]any{"model": "mock-model", "tokens": tokens})
441+
Expect(err).ToNot(HaveOccurred())
442+
443+
detokenizeReq, err := http.NewRequest("POST", apiURL+"/detokenize", strings.NewReader(string(tokenJSON)))
444+
Expect(err).ToNot(HaveOccurred())
445+
detokenizeReq.Header.Set("Content-Type", "application/json")
446+
447+
detokenizeResp, err := httpClient.Do(detokenizeReq)
448+
Expect(err).ToNot(HaveOccurred())
449+
defer detokenizeResp.Body.Close()
450+
Expect(detokenizeResp.StatusCode).To(Equal(200))
451+
452+
detokenizeData, err := io.ReadAll(detokenizeResp.Body)
453+
Expect(err).ToNot(HaveOccurred())
454+
var detokenizeResult map[string]any
455+
Expect(json.Unmarshal(detokenizeData, &detokenizeResult)).To(Succeed())
456+
content, ok := detokenizeResult["content"].(string)
457+
Expect(ok).To(BeTrue(), "detokenize response missing 'content': %s", string(detokenizeData))
458+
Expect(content).ToNot(BeEmpty())
459+
})
460+
})
461+
390462
Describe("Autoparser ChatDelta Streaming", Label("Autoparser"), func() {
391463
// These tests verify that when the C++ autoparser handles tool calls
392464
// and content via ChatDeltas (with empty raw message), the streaming

0 commit comments

Comments
 (0)