Skip to content

Commit 2454798

Browse files
committed
test: add repeatable E2E model tests for deepseek-chat, v4-flash, v4-pro, and gpt-4o-mini
Creates cmd/kode/model_e2e_test.go with integration-tagged Go tests that exercise the full kode agent loop against real LLM APIs. - TestModelE2E_DeepSeek_Chat - TestModelE2E_DeepSeek_V4Flash - TestModelE2E_DeepSeek_V4Pro - TestModelE2E_OpenAI_GPT4oMini Each test: - Creates a real kode.Agent with the model config - Runs the prompt 'Respond with exactly the word ALIVE' - Verifies response contains ALIVE, non-zero token tracking - Uses 120s timeout per test, 15 max iterations - Gated by //go:build integration tag; skipped in -short mode Run: go test -tags=integration -v -count=1 -run TestModelE2E ./cmd/kode/ Keys: KODE_API_KEY_DEEPSEEK or KODE_API_KEY_OPENAI env vars
1 parent 3050a6c commit 2454798

1 file changed

Lines changed: 115 additions & 0 deletions

File tree

cmd/kode/model_e2e_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
//go:build integration
2+
3+
// E2E tests that exercise the full kode agent loop against real LLM APIs.
4+
//
5+
// These tests are gated by the "integration" build tag and require API keys
6+
// to be set via environment variables:
7+
//
8+
// export KODE_API_KEY_DEEPSEEK="sk-..." # required for DeepSeek models
9+
// export KODE_API_KEY_OPENAI="sk-..." # required for OpenAI models
10+
// go test -tags=integration -v -count=1 -run 'TestModelE2E' ./cmd/kode/
11+
//
12+
// Each test case creates a real kode.Agent with the given model config,
13+
// runs a simple task ("respond with ALIVE"), and verifies:
14+
// - The agent loop completes without error
15+
// - The response contains the expected substring
16+
// - Token tracking produces non-zero values
17+
// - The loop runs within a 120s timeout
18+
19+
package main
20+
21+
import (
22+
"context"
23+
"os"
24+
"strings"
25+
"testing"
26+
"time"
27+
28+
"github.com/BackendStack21/kode"
29+
"github.com/BackendStack21/kode/internal/render"
30+
)
31+
32+
type modelCase struct {
33+
Name string
34+
Model string
35+
BaseURL string
36+
KeyEnv string // env var name for the API key
37+
}
38+
39+
// ── DeepSeek models ────────────────────────────────────────────────────
40+
41+
func TestModelE2E_DeepSeek_Chat(t *testing.T) { testModel(t, modelCase{Name: "deepseek-chat", Model: "deepseek-chat", KeyEnv: "KODE_API_KEY_DEEPSEEK"}) }
42+
func TestModelE2E_DeepSeek_V4Flash(t *testing.T) { testModel(t, modelCase{Name: "deepseek-v4-flash", Model: "deepseek-v4-flash", KeyEnv: "KODE_API_KEY_DEEPSEEK"}) }
43+
func TestModelE2E_DeepSeek_V4Pro(t *testing.T) { testModel(t, modelCase{Name: "deepseek-v4-pro", Model: "deepseek-v4-pro", KeyEnv: "KODE_API_KEY_DEEPSEEK"}) }
44+
45+
// ── OpenAI models ──────────────────────────────────────────────────────
46+
47+
func TestModelE2E_OpenAI_GPT4oMini(t *testing.T) {
48+
testModel(t, modelCase{
49+
Name: "gpt-4o-mini",
50+
Model: "gpt-4o-mini",
51+
BaseURL: "https://api.openai.com/v1",
52+
KeyEnv: "KODE_API_KEY_OPENAI",
53+
})
54+
}
55+
56+
// testModel runs a single E2E model test: creates an agent, runs a prompt,
57+
// and verifies the response contains "ALIVE".
58+
func testModel(t *testing.T, mc modelCase) {
59+
t.Helper()
60+
61+
if testing.Short() {
62+
t.Skip("skipping E2E model test in short mode")
63+
}
64+
65+
apiKey := os.Getenv(mc.KeyEnv)
66+
if apiKey == "" {
67+
t.Skipf("env %s not set — skipping %s", mc.KeyEnv, mc.Name)
68+
}
69+
70+
baseURL := mc.BaseURL
71+
if baseURL == "" {
72+
baseURL = "https://api.deepseek.com/v1"
73+
}
74+
75+
agent, err := kode.New(kode.Config{
76+
Model: mc.Model,
77+
BaseURL: baseURL,
78+
APIKey: apiKey,
79+
MaxIterations: 15,
80+
SystemMessage: "You are a helpful assistant. Follow instructions precisely.",
81+
NoProjectFile: true,
82+
Renderer: render.New(os.Stderr, false),
83+
})
84+
if err != nil {
85+
t.Fatalf("kode.New: %v", err)
86+
}
87+
defer agent.Close()
88+
89+
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
90+
defer cancel()
91+
92+
start := time.Now()
93+
answer, err := agent.Run(ctx, "Respond with exactly the word 'ALIVE' and nothing else.")
94+
elapsed := time.Since(start)
95+
96+
if err != nil {
97+
t.Fatalf("Agent.Run: %v (after %v)", err, elapsed)
98+
}
99+
100+
inputTok := agent.TotalInputTokens()
101+
outputTok := agent.TotalOutputTokens()
102+
103+
t.Logf("✅ %-20s latency=%v input=%-5d output=%-5d total=%-5d",
104+
mc.Name, elapsed.Round(time.Millisecond), inputTok, outputTok, inputTok+outputTok)
105+
106+
if !strings.Contains(answer, "ALIVE") {
107+
t.Errorf("response = %q, want substring %q", strings.TrimSpace(answer), "ALIVE")
108+
}
109+
if inputTok <= 0 {
110+
t.Error("TotalInputTokens = 0, expected > 0")
111+
}
112+
if outputTok <= 0 {
113+
t.Error("TotalOutputTokens = 0, expected > 0")
114+
}
115+
}

0 commit comments

Comments
 (0)