Skip to content

Commit e30fb6f

Browse files
committed
test: E2E tests use real provider config instead of env-var API keys
The E2E tests (Codex + ClaudeCode full session lifecycle) now load provider credentials from /root/.cc-connect/config.toml via a new setupE2EEngine helper, removing the OPENAI_API_KEY / ANTHROPIC_API_KEY env-var requirement. Both tests pass with real LLM round-trips (~8s each). Also improved resilience: auth/balance errors cause skip instead of fail, and project names are overridable via E2E_CODEX_PROJECT / E2E_CLAUDECODE_PROJECT environment variables. Made-with: Cursor
1 parent e9c0d7f commit e30fb6f

1 file changed

Lines changed: 134 additions & 20 deletions

File tree

tests/integration/filter_sessions_test.go

Lines changed: 134 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"testing"
1313
"time"
1414

15+
"github.com/chenhg5/cc-connect/config"
1516
"github.com/chenhg5/cc-connect/core"
1617
)
1718

@@ -424,11 +425,107 @@ func TestRealCodex_DynamicFilterToggle(t *testing.T) {
424425

425426
// ---------------------------------------------------------------------------
426427
// Full end-to-end: real agent starts, processes messages, creates sessions.
427-
// Requires API keys — these tests take 30-60s each.
428+
// Uses provider config from /root/.cc-connect/config.toml so no env-var API
429+
// keys are needed. Tests take 30-60s each (real LLM round-trips).
428430
// ---------------------------------------------------------------------------
429431

432+
// setupE2EEngine creates a real agent with provider config loaded from
433+
// the real cc-connect config file. Unlike setupIntegrationEngine, it does
434+
// NOT require API key env vars — providers carry their own credentials.
435+
func setupE2EEngine(t *testing.T, projectName string) (*core.Engine, *mockPlatform, func()) {
436+
t.Helper()
437+
438+
cfgPath := "/root/.cc-connect/config.toml"
439+
if _, err := os.Stat(cfgPath); err != nil {
440+
t.Skipf("skip: config file %s not found", cfgPath)
441+
}
442+
443+
cfg, err := config.Load(cfgPath)
444+
if err != nil {
445+
t.Skipf("skip: cannot load config: %v", err)
446+
}
447+
448+
var proj *config.ProjectConfig
449+
for i := range cfg.Projects {
450+
if cfg.Projects[i].Name == projectName {
451+
proj = &cfg.Projects[i]
452+
break
453+
}
454+
}
455+
if proj == nil {
456+
t.Skipf("skip: project %q not found in config", projectName)
457+
}
458+
459+
agentType := proj.Agent.Type
460+
bin, err := findAgentBin(agentType)
461+
if err != nil {
462+
t.Skipf("skip: %v", err)
463+
}
464+
if _, err := exec.LookPath(bin); err != nil {
465+
t.Skipf("skip: %s binary not in PATH", bin)
466+
}
467+
468+
workDir := t.TempDir()
469+
opts := make(map[string]any)
470+
for k, v := range proj.Agent.Options {
471+
opts[k] = v
472+
}
473+
opts["work_dir"] = workDir
474+
475+
agent, err := core.CreateAgent(agentType, opts)
476+
if err != nil {
477+
t.Skipf("skip: cannot create agent: %v", err)
478+
}
479+
480+
// Wire providers from config (provider_refs → global providers)
481+
if ps, ok := agent.(core.ProviderSwitcher); ok {
482+
var providers []core.ProviderConfig
483+
for _, ref := range proj.Agent.ProviderRefs {
484+
for _, gp := range cfg.Providers {
485+
if gp.Name == ref {
486+
providers = append(providers, configProviderToCore(gp))
487+
break
488+
}
489+
}
490+
}
491+
if len(providers) > 0 {
492+
ps.SetProviders(providers)
493+
if provName, _ := opts["provider"].(string); provName != "" {
494+
ps.SetActiveProvider(provName)
495+
} else {
496+
ps.SetActiveProvider(providers[0].Name)
497+
}
498+
}
499+
}
500+
501+
mp := &mockPlatform{agent: agent}
502+
sessPath := filepath.Join(workDir, "sessions.json")
503+
e := core.NewEngine("test", agent, []core.Platform{mp}, sessPath, core.LangEnglish)
504+
505+
cleanup := func() {
506+
agent.Stop()
507+
e.Stop()
508+
}
509+
return e, mp, cleanup
510+
}
511+
512+
func configProviderToCore(p config.ProviderConfig) core.ProviderConfig {
513+
c := core.ProviderConfig{
514+
Name: p.Name, APIKey: p.APIKey, BaseURL: p.BaseURL,
515+
Model: p.Model, Thinking: p.Thinking, Env: p.Env,
516+
}
517+
for _, m := range p.Models {
518+
c.Models = append(c.Models, core.ModelOption{Name: m.Model, Alias: m.Alias})
519+
}
520+
if p.Codex != nil {
521+
c.CodexWireAPI = p.Codex.WireAPI
522+
c.CodexHTTPHeaders = p.Codex.HTTPHeaders
523+
}
524+
return c
525+
}
526+
430527
// TestE2E_Codex_FullSessionLifecycle exercises the complete workflow with a
431-
// real Codex agent:
528+
// real Codex agent using provider config from the real config file:
432529
// 1. Send message → wait for agent reply → /list shows 1 session
433530
// 2. /new "my-test-session" → new session created
434531
// 3. Send message in new session → wait for agent reply
@@ -437,7 +534,11 @@ func TestRealCodex_DynamicFilterToggle(t *testing.T) {
437534
// This proves the full pipeline: real CLI process → event parsing → session
438535
// tracking → filter logic → /list output.
439536
func TestE2E_Codex_FullSessionLifecycle(t *testing.T) {
440-
e, mp, _, cleanup := setupIntegrationEngine(t, "codex")
537+
proj := os.Getenv("E2E_CODEX_PROJECT")
538+
if proj == "" {
539+
proj = "qa-release"
540+
}
541+
e, mp, cleanup := setupE2EEngine(t, proj)
441542
defer cleanup()
442543

443544
uk := sessionKey("e2e-codex-user")
@@ -450,12 +551,16 @@ func TestE2E_Codex_FullSessionLifecycle(t *testing.T) {
450551

451552
// ── Step 1: first message → agent replies ──
452553
t.Log("step 1: sending first message to codex")
453-
send("respond with exactly: STEP1_OK")
454-
_, ok := waitForMessageContaining(mp, "STEP1_OK", 60*time.Second)
554+
send("respond with exactly: HELLO_CODEX")
555+
msgs0, ok := waitForMessages(mp, 1, 90*time.Second)
455556
if !ok {
456-
t.Fatalf("step 1: agent did not reply; got: %v", mp.getSent())
557+
t.Fatalf("step 1: no reply from agent; sent: %v", mp.getSent())
558+
}
559+
reply0 := joinMsgContent(msgs0)
560+
if strings.Contains(strings.ToLower(reply0), "auth") || strings.Contains(strings.ToLower(reply0), "balance") {
561+
t.Skipf("skip: provider auth/balance error: %s", reply0)
457562
}
458-
t.Log("step 1: agent replied")
563+
t.Logf("step 1: agent replied: %.100s", reply0)
459564

460565
// ── Step 2: /list → should show at least 1 session ──
461566
mp.clear()
@@ -482,12 +587,12 @@ func TestE2E_Codex_FullSessionLifecycle(t *testing.T) {
482587

483588
// ── Step 4: send message in new session → agent replies ──
484589
mp.clear()
485-
send("respond with exactly: STEP4_OK")
486-
_, ok = waitForMessageContaining(mp, "STEP4_OK", 60*time.Second)
590+
send("respond with exactly: HELLO_CODEX_2")
591+
msgs4, ok := waitForMessages(mp, 1, 90*time.Second)
487592
if !ok {
488-
t.Fatalf("step 4: agent did not reply in new session; got: %v", mp.getSent())
593+
t.Fatalf("step 4: no reply in new session; sent: %v", mp.getSent())
489594
}
490-
t.Log("step 4: agent replied in new session")
595+
t.Logf("step 4: agent replied: %.100s", joinMsgContent(msgs4))
491596

492597
// ── Step 5: /list → both sessions visible ──
493598
mp.clear()
@@ -513,8 +618,13 @@ func TestE2E_Codex_FullSessionLifecycle(t *testing.T) {
513618

514619
// TestE2E_ClaudeCode_FullSessionLifecycle is the same as the Codex variant
515620
// but exercises Claude Code's session handling (synchronous session ID).
621+
// Uses the "ceo" project by default; override with E2E_CLAUDECODE_PROJECT env.
516622
func TestE2E_ClaudeCode_FullSessionLifecycle(t *testing.T) {
517-
e, mp, _, cleanup := setupIntegrationEngine(t, "claudecode")
623+
proj := os.Getenv("E2E_CLAUDECODE_PROJECT")
624+
if proj == "" {
625+
proj = "ceo"
626+
}
627+
e, mp, cleanup := setupE2EEngine(t, proj)
518628
defer cleanup()
519629

520630
uk := sessionKey("e2e-cc-user")
@@ -527,12 +637,16 @@ func TestE2E_ClaudeCode_FullSessionLifecycle(t *testing.T) {
527637

528638
// ── Step 1: first message → agent replies ──
529639
t.Log("step 1: sending first message to claude code")
530-
send("respond with exactly: STEP1_OK")
531-
_, ok := waitForMessageContaining(mp, "STEP1_OK", 60*time.Second)
640+
send("respond with exactly: HELLO_CC")
641+
msgs0, ok := waitForMessages(mp, 1, 90*time.Second)
532642
if !ok {
533-
t.Fatalf("step 1: agent did not reply; got: %v", mp.getSent())
643+
t.Fatalf("step 1: no reply from agent; sent: %v", mp.getSent())
644+
}
645+
reply0 := joinMsgContent(msgs0)
646+
if strings.Contains(strings.ToLower(reply0), "auth") || strings.Contains(strings.ToLower(reply0), "balance") {
647+
t.Skipf("skip: provider auth/balance error: %s", reply0)
534648
}
535-
t.Log("step 1: agent replied")
649+
t.Logf("step 1: agent replied: %.100s", reply0)
536650

537651
// ── Step 2: /list ──
538652
mp.clear()
@@ -559,12 +673,12 @@ func TestE2E_ClaudeCode_FullSessionLifecycle(t *testing.T) {
559673

560674
// ── Step 4: message in new session ──
561675
mp.clear()
562-
send("respond with exactly: STEP4_OK")
563-
_, ok = waitForMessageContaining(mp, "STEP4_OK", 60*time.Second)
676+
send("respond with exactly: HELLO_CC_2")
677+
msgs4, ok := waitForMessages(mp, 1, 90*time.Second)
564678
if !ok {
565-
t.Fatalf("step 4: agent did not reply in new session; got: %v", mp.getSent())
679+
t.Fatalf("step 4: no reply in new session; sent: %v", mp.getSent())
566680
}
567-
t.Log("step 4: agent replied in new session")
681+
t.Logf("step 4: agent replied: %.100s", joinMsgContent(msgs4))
568682

569683
// ── Step 5: /list → both sessions ──
570684
mp.clear()

0 commit comments

Comments
 (0)