初始化仓库
This commit is contained in:
@@ -0,0 +1,450 @@
|
||||
//go:build integration
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/chenhg5/cc-connect/config"
|
||||
"github.com/chenhg5/cc-connect/core"
|
||||
)
|
||||
|
||||
// testConfigPath returns the path to config.test.toml co-located with this
|
||||
// test file. Override via CC_TEST_CONFIG env var.
|
||||
func testConfigPath(t *testing.T) string {
|
||||
t.Helper()
|
||||
if p := os.Getenv("CC_TEST_CONFIG"); p != "" {
|
||||
return p
|
||||
}
|
||||
_, thisFile, _, _ := runtime.Caller(0)
|
||||
return filepath.Join(filepath.Dir(thisFile), "config.test.toml")
|
||||
}
|
||||
|
||||
// setupE2E loads config.test.toml, finds the named project, creates a real
|
||||
// agent with provider wiring, and returns Engine + mockPlatform. All work_dir
|
||||
// and session state use t.TempDir() for full isolation.
|
||||
func setupE2E(t *testing.T, projectName string) (*core.Engine, *mockPlatform, func()) {
|
||||
t.Helper()
|
||||
|
||||
cfgPath := testConfigPath(t)
|
||||
if _, err := os.Stat(cfgPath); err != nil {
|
||||
t.Skipf("skip: test config %s not found (copy config.test.toml.example and fill in credentials)", cfgPath)
|
||||
}
|
||||
|
||||
cfg, err := config.Load(cfgPath)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot load test config: %v", err)
|
||||
}
|
||||
|
||||
var proj *config.ProjectConfig
|
||||
for i := range cfg.Projects {
|
||||
if cfg.Projects[i].Name == projectName {
|
||||
proj = &cfg.Projects[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if proj == nil {
|
||||
t.Fatalf("project %q not found in test config", projectName)
|
||||
}
|
||||
|
||||
agentType := proj.Agent.Type
|
||||
bin, err := findAgentBin(agentType)
|
||||
if err != nil {
|
||||
t.Skipf("skip: %v", err)
|
||||
}
|
||||
if _, err := exec.LookPath(bin); err != nil {
|
||||
t.Skipf("skip: %s binary not in PATH", bin)
|
||||
}
|
||||
|
||||
workDir := t.TempDir()
|
||||
opts := make(map[string]any)
|
||||
for k, v := range proj.Agent.Options {
|
||||
opts[k] = v
|
||||
}
|
||||
opts["work_dir"] = workDir
|
||||
|
||||
agent, err := core.CreateAgent(agentType, opts)
|
||||
if err != nil {
|
||||
t.Skipf("skip: cannot create agent: %v", err)
|
||||
}
|
||||
|
||||
if ps, ok := agent.(core.ProviderSwitcher); ok {
|
||||
var providers []core.ProviderConfig
|
||||
for _, ref := range proj.Agent.ProviderRefs {
|
||||
for _, gp := range cfg.Providers {
|
||||
if gp.Name == ref {
|
||||
providers = append(providers, configProviderToCore(gp))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(providers) > 0 {
|
||||
ps.SetProviders(providers)
|
||||
if provName, _ := opts["provider"].(string); provName != "" {
|
||||
ps.SetActiveProvider(provName)
|
||||
} else {
|
||||
ps.SetActiveProvider(providers[0].Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mp := &mockPlatform{agent: agent}
|
||||
sessPath := filepath.Join(workDir, "sessions.json")
|
||||
e := core.NewEngine("e2e-test", agent, []core.Platform{mp}, sessPath, core.LangEnglish)
|
||||
|
||||
return e, mp, func() {
|
||||
agent.Stop()
|
||||
e.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
type e2eHelper struct {
|
||||
t *testing.T
|
||||
e *core.Engine
|
||||
mp *mockPlatform
|
||||
uk string
|
||||
}
|
||||
|
||||
func (h *e2eHelper) send(content string) {
|
||||
h.mp.clear()
|
||||
h.e.ReceiveMessage(h.mp, &core.Message{
|
||||
SessionKey: h.uk, Platform: "mock", UserID: "e2e-user",
|
||||
UserName: "tester", Content: content, ReplyCtx: "ctx",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *e2eHelper) waitReply(timeout time.Duration) string {
|
||||
msgs, ok := waitForMessages(h.mp, 1, timeout)
|
||||
if !ok {
|
||||
h.t.Fatalf("timeout waiting for reply; sent so far: %v", h.mp.getSent())
|
||||
}
|
||||
reply := joinMsgContent(msgs)
|
||||
if strings.Contains(strings.ToLower(reply), "auth") && strings.Contains(strings.ToLower(reply), "balance") {
|
||||
h.t.Skipf("skip: provider auth/balance error: %s", reply)
|
||||
}
|
||||
return reply
|
||||
}
|
||||
|
||||
func (h *e2eHelper) waitCmd(timeout time.Duration) string {
|
||||
return h.waitReply(timeout)
|
||||
}
|
||||
|
||||
func (h *e2eHelper) sendAndWait(content string, timeout time.Duration) string {
|
||||
h.send(content)
|
||||
return h.waitReply(timeout)
|
||||
}
|
||||
|
||||
// countSessions counts the "msgs" markers in /list output (each session line
|
||||
// contains "N msgs"), giving us the session count.
|
||||
func countSessions(listOutput string) int {
|
||||
return strings.Count(listOutput, "msgs")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Comprehensive E2E: covers /list /new /name /switch /delete /current /stop
|
||||
// Each test function runs against ONE agent type. We define two entry points
|
||||
// (Codex, ClaudeCode) so both are exercised. Skips gracefully if binary or
|
||||
// config is missing.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func runE2E_FullSessionCommands(t *testing.T, project string) {
|
||||
e, mp, cleanup := setupE2E(t, project)
|
||||
defer cleanup()
|
||||
|
||||
h := &e2eHelper{t: t, e: e, mp: mp, uk: sessionKey("e2e-cmds")}
|
||||
|
||||
// ────── 1. First message → agent replies ──────
|
||||
t.Log("[1] sending first message")
|
||||
reply := h.sendAndWait("respond with exactly: E2E_PING", 90*time.Second)
|
||||
t.Logf("[1] reply: %.120s", reply)
|
||||
|
||||
// ────── 2. /list → 1 session ──────
|
||||
t.Log("[2] /list")
|
||||
list1 := h.sendAndWait("/list", 10*time.Second)
|
||||
c1 := countSessions(list1)
|
||||
if c1 < 1 {
|
||||
t.Fatalf("[2] /list should show >= 1 session, got %d\n%s", c1, list1)
|
||||
}
|
||||
t.Logf("[2] /list → %d session(s)", c1)
|
||||
|
||||
// ────── 3. /current ──────
|
||||
t.Log("[3] /current")
|
||||
cur := h.sendAndWait("/current", 10*time.Second)
|
||||
if !strings.Contains(strings.ToLower(cur), "session") && !strings.Contains(strings.ToLower(cur), "s1") {
|
||||
t.Logf("[3] /current output: %s", cur)
|
||||
}
|
||||
t.Logf("[3] /current OK")
|
||||
|
||||
// ────── 4. /new with custom name ──────
|
||||
t.Log("[4] /new my-named-session")
|
||||
h.sendAndWait("/new my-named-session", 10*time.Second)
|
||||
t.Log("[4] /new done")
|
||||
|
||||
// ────── 5. Chat in new session → agent replies ──────
|
||||
t.Log("[5] message in new session")
|
||||
reply5 := h.sendAndWait("respond with exactly: E2E_PONG", 90*time.Second)
|
||||
t.Logf("[5] reply: %.120s", reply5)
|
||||
|
||||
// ────── 6. /list → 2 sessions, name visible ──────
|
||||
t.Log("[6] /list (should show 2 sessions)")
|
||||
list2 := h.sendAndWait("/list", 10*time.Second)
|
||||
c2 := countSessions(list2)
|
||||
if c2 < 2 {
|
||||
t.Fatalf("[6] /list should show >= 2 sessions, got %d\n%s", c2, list2)
|
||||
}
|
||||
if !strings.Contains(list2, "my-named-session") {
|
||||
t.Errorf("[6] /list should show session name 'my-named-session'\n%s", list2)
|
||||
}
|
||||
t.Logf("[6] /list → %d sessions, name OK", c2)
|
||||
|
||||
// ────── 7. /name rename current session ──────
|
||||
t.Log("[7] /name renamed-session")
|
||||
nameReply := h.sendAndWait("/name renamed-session", 10*time.Second)
|
||||
t.Logf("[7] /name reply: %.80s", nameReply)
|
||||
|
||||
// ────── 8. /list → verify renamed ──────
|
||||
t.Log("[8] /list after rename")
|
||||
list3 := h.sendAndWait("/list", 10*time.Second)
|
||||
if !strings.Contains(list3, "renamed-session") {
|
||||
t.Errorf("[8] /list should show 'renamed-session' after rename\n%s", list3)
|
||||
}
|
||||
t.Log("[8] rename confirmed in /list")
|
||||
|
||||
// ────── 9. /switch back to session 1 ──────
|
||||
t.Log("[9] /switch 1")
|
||||
switchReply := h.sendAndWait("/switch 1", 10*time.Second)
|
||||
t.Logf("[9] /switch: %.80s", switchReply)
|
||||
|
||||
// ────── 10. Send message in session 1 → verify context ──────
|
||||
t.Log("[10] message in session 1")
|
||||
reply10 := h.sendAndWait("respond with exactly: BACK_TO_S1", 90*time.Second)
|
||||
t.Logf("[10] reply: %.120s", reply10)
|
||||
|
||||
// ────── 11. /list → still 2 sessions ──────
|
||||
t.Log("[11] /list (still 2 sessions after /switch)")
|
||||
list4 := h.sendAndWait("/list", 10*time.Second)
|
||||
c4 := countSessions(list4)
|
||||
if c4 < 2 {
|
||||
t.Errorf("[11] /list should still show >= 2 sessions, got %d\n%s", c4, list4)
|
||||
}
|
||||
t.Logf("[11] /list → %d sessions", c4)
|
||||
|
||||
// ────── 12. /new → third session ──────
|
||||
t.Log("[12] /new third-session")
|
||||
h.sendAndWait("/new third-session", 10*time.Second)
|
||||
t.Log("[12] /new done")
|
||||
|
||||
// ────── 13. Chat in third session ──────
|
||||
t.Log("[13] message in third session")
|
||||
h.sendAndWait("respond with exactly: THIRD_OK", 90*time.Second)
|
||||
t.Log("[13] reply received")
|
||||
|
||||
// ────── 14. /delete third session ──────
|
||||
t.Log("[14] /delete 3")
|
||||
delReply := h.sendAndWait("/delete 3", 10*time.Second)
|
||||
t.Logf("[14] /delete: %.80s", delReply)
|
||||
|
||||
// ────── 15. /list → session deleted ──────
|
||||
t.Log("[15] /list after /delete")
|
||||
list5 := h.sendAndWait("/list", 10*time.Second)
|
||||
c5 := countSessions(list5)
|
||||
if c5 >= c4+1 {
|
||||
t.Errorf("[15] /list should have fewer sessions after delete, got %d (was %d+1)\n%s", c5, c4, list5)
|
||||
}
|
||||
t.Logf("[15] /list → %d sessions (after delete)", c5)
|
||||
|
||||
// ────── 16. /stop ──────
|
||||
t.Log("[16] /stop")
|
||||
h.send("/stop")
|
||||
time.Sleep(2 * time.Second)
|
||||
t.Log("[16] /stop sent, no crash")
|
||||
|
||||
// ────── 17. /status ──────
|
||||
t.Log("[17] /status")
|
||||
statusReply := h.sendAndWait("/status", 10*time.Second)
|
||||
t.Logf("[17] /status: %.120s", statusReply)
|
||||
|
||||
t.Log("=== ALL STEPS PASSED ===")
|
||||
}
|
||||
|
||||
func TestE2E_Codex_FullSessionCommands(t *testing.T) {
|
||||
runE2E_FullSessionCommands(t, "e2e-codex")
|
||||
}
|
||||
|
||||
func TestE2E_ClaudeCode_FullSessionCommands(t *testing.T) {
|
||||
runE2E_FullSessionCommands(t, "e2e-claudecode")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// E2E: /provider switch (requires multiple providers in config)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func runE2E_ProviderSwitch(t *testing.T, project string) {
|
||||
e, mp, cleanup := setupE2E(t, project)
|
||||
defer cleanup()
|
||||
|
||||
h := &e2eHelper{t: t, e: e, mp: mp, uk: sessionKey("e2e-provider")}
|
||||
|
||||
// Start a session
|
||||
t.Log("[1] initial message")
|
||||
h.sendAndWait("respond with exactly: PROV_INIT", 90*time.Second)
|
||||
|
||||
// /provider list
|
||||
t.Log("[2] /provider list")
|
||||
provList := h.sendAndWait("/provider list", 10*time.Second)
|
||||
t.Logf("[2] providers: %.200s", provList)
|
||||
|
||||
// /provider switch to a different provider
|
||||
t.Log("[3] /provider switch shengsuanyun")
|
||||
switchReply := h.sendAndWait("/provider switch shengsuanyun", 10*time.Second)
|
||||
t.Logf("[3] switch: %.120s", switchReply)
|
||||
|
||||
// Verify new provider works
|
||||
t.Log("[4] message after switch")
|
||||
reply4 := h.sendAndWait("respond with exactly: PROV_SWITCHED", 90*time.Second)
|
||||
t.Logf("[4] reply: %.120s", reply4)
|
||||
|
||||
// /list should still work
|
||||
t.Log("[5] /list after provider switch")
|
||||
list := h.sendAndWait("/list", 10*time.Second)
|
||||
if countSessions(list) < 1 {
|
||||
t.Errorf("[5] /list broken after provider switch\n%s", list)
|
||||
}
|
||||
t.Log("[5] /list OK after provider switch")
|
||||
|
||||
t.Log("=== PROVIDER SWITCH PASSED ===")
|
||||
}
|
||||
|
||||
func TestE2E_Codex_ProviderSwitch(t *testing.T) {
|
||||
runE2E_ProviderSwitch(t, "e2e-codex")
|
||||
}
|
||||
|
||||
func TestE2E_ClaudeCode_ProviderSwitch(t *testing.T) {
|
||||
runE2E_ProviderSwitch(t, "e2e-claudecode")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// E2E: Session persistence across restart (simulate by recreating engine)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func runE2E_SessionPersistence(t *testing.T, project string) {
|
||||
cfgPath := testConfigPath(t)
|
||||
if _, err := os.Stat(cfgPath); err != nil {
|
||||
t.Skipf("skip: test config not found")
|
||||
}
|
||||
cfg, err := config.Load(cfgPath)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot load config: %v", err)
|
||||
}
|
||||
|
||||
var proj *config.ProjectConfig
|
||||
for i := range cfg.Projects {
|
||||
if cfg.Projects[i].Name == project {
|
||||
proj = &cfg.Projects[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if proj == nil {
|
||||
t.Fatalf("project %q not found", project)
|
||||
}
|
||||
|
||||
agentType := proj.Agent.Type
|
||||
bin, _ := findAgentBin(agentType)
|
||||
if _, err := exec.LookPath(bin); err != nil {
|
||||
t.Skipf("skip: %s not in PATH", bin)
|
||||
}
|
||||
|
||||
workDir := t.TempDir()
|
||||
sessPath := filepath.Join(workDir, "sessions.json")
|
||||
|
||||
createAgent := func() core.Agent {
|
||||
opts := make(map[string]any)
|
||||
for k, v := range proj.Agent.Options {
|
||||
opts[k] = v
|
||||
}
|
||||
opts["work_dir"] = workDir
|
||||
agent, err := core.CreateAgent(agentType, opts)
|
||||
if err != nil {
|
||||
t.Skipf("skip: %v", err)
|
||||
}
|
||||
if ps, ok := agent.(core.ProviderSwitcher); ok {
|
||||
var providers []core.ProviderConfig
|
||||
for _, ref := range proj.Agent.ProviderRefs {
|
||||
for _, gp := range cfg.Providers {
|
||||
if gp.Name == ref {
|
||||
providers = append(providers, configProviderToCore(gp))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(providers) > 0 {
|
||||
ps.SetProviders(providers)
|
||||
if pn, _ := opts["provider"].(string); pn != "" {
|
||||
ps.SetActiveProvider(pn)
|
||||
} else {
|
||||
ps.SetActiveProvider(providers[0].Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return agent
|
||||
}
|
||||
|
||||
// Phase 1: create session, send message, /new, send message
|
||||
agent1 := createAgent()
|
||||
mp1 := &mockPlatform{agent: agent1}
|
||||
e1 := core.NewEngine("persist-test", agent1, []core.Platform{mp1}, sessPath, core.LangEnglish)
|
||||
h1 := &e2eHelper{t: t, e: e1, mp: mp1, uk: sessionKey("persist-user")}
|
||||
|
||||
t.Log("[phase1] sending first message")
|
||||
h1.sendAndWait("respond with exactly: PERSIST_S1", 90*time.Second)
|
||||
|
||||
t.Log("[phase1] /new persist-session-2")
|
||||
h1.sendAndWait("/new persist-session-2", 10*time.Second)
|
||||
|
||||
t.Log("[phase1] message in session 2")
|
||||
h1.sendAndWait("respond with exactly: PERSIST_S2", 90*time.Second)
|
||||
|
||||
t.Log("[phase1] /list")
|
||||
list1 := h1.sendAndWait("/list", 10*time.Second)
|
||||
c1 := countSessions(list1)
|
||||
t.Logf("[phase1] %d sessions", c1)
|
||||
|
||||
// Simulate restart
|
||||
agent1.Stop()
|
||||
e1.Stop()
|
||||
|
||||
// Phase 2: new engine, same sessPath
|
||||
agent2 := createAgent()
|
||||
mp2 := &mockPlatform{agent: agent2}
|
||||
e2 := core.NewEngine("persist-test", agent2, []core.Platform{mp2}, sessPath, core.LangEnglish)
|
||||
defer func() { agent2.Stop(); e2.Stop() }()
|
||||
|
||||
h2 := &e2eHelper{t: t, e: e2, mp: mp2, uk: sessionKey("persist-user")}
|
||||
|
||||
t.Log("[phase2] /list after restart")
|
||||
list2 := h2.sendAndWait("/list", 10*time.Second)
|
||||
c2 := countSessions(list2)
|
||||
if c2 < c1 {
|
||||
t.Errorf("[phase2] sessions lost after restart: had %d, now %d\n%s", c1, c2, list2)
|
||||
}
|
||||
if !strings.Contains(list2, "persist-session-2") {
|
||||
t.Errorf("[phase2] session name 'persist-session-2' lost after restart\n%s", list2)
|
||||
}
|
||||
t.Logf("[phase2] %d sessions survived restart, name preserved", c2)
|
||||
|
||||
t.Log("=== PERSISTENCE PASSED ===")
|
||||
}
|
||||
|
||||
func TestE2E_Codex_SessionPersistence(t *testing.T) {
|
||||
runE2E_SessionPersistence(t, "e2e-codex")
|
||||
}
|
||||
|
||||
func TestE2E_ClaudeCode_SessionPersistence(t *testing.T) {
|
||||
runE2E_SessionPersistence(t, "e2e-claudecode")
|
||||
}
|
||||
Reference in New Issue
Block a user