初始化仓库
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
//go:build blackbox
|
||||
|
||||
package p1
|
||||
|
||||
import (
|
||||
_ "github.com/chenhg5/cc-connect/agent/claudecode"
|
||||
_ "github.com/chenhg5/cc-connect/agent/codex"
|
||||
_ "github.com/chenhg5/cc-connect/agent/cursor"
|
||||
_ "github.com/chenhg5/cc-connect/agent/gemini"
|
||||
_ "github.com/chenhg5/cc-connect/agent/opencode"
|
||||
_ "github.com/chenhg5/cc-connect/agent/qoder"
|
||||
)
|
||||
@@ -0,0 +1,209 @@
|
||||
//go:build blackbox
|
||||
|
||||
// Package p1 contains P1 blackbox tests.
|
||||
//
|
||||
// Session commands (/help, /current, /name, /switch, /delete, /status, /version)
|
||||
// are dispatched by cc-connect's engine directly, so they respond within
|
||||
// seconds regardless of agent speed.
|
||||
//
|
||||
// Run:
|
||||
//
|
||||
// CC_BLACKBOX_CLAUDECODE_API_KEY=xxx \
|
||||
// CC_BLACKBOX_CLAUDECODE_BASE_URL=https://... \
|
||||
// go test -tags blackbox ./tests/blackbox/p1/... -timeout 1200s -v
|
||||
package p1
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/chenhg5/cc-connect/core"
|
||||
"github.com/chenhg5/cc-connect/tests/blackbox/helper"
|
||||
bbplatform "github.com/chenhg5/cc-connect/tests/blackbox/platform"
|
||||
)
|
||||
|
||||
const cmdTimeout = 30 * time.Second // engine-handled commands are near-instant
|
||||
|
||||
// ── P1-1: /help ──────────────────────────────────────────────────────────────
|
||||
|
||||
func TestP1_1_Help_ClaudeCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
env := helper.NewEnv(t, "claudecode")
|
||||
reply := env.SendWithTimeout("/help", cmdTimeout)
|
||||
assertContainsAny(t, "P1-1 /help", reply.Text(),
|
||||
"/new", "/list", "/stop", "/help", "Available", "命令", "Command")
|
||||
t.Logf("P1-1 OK: %q", truncate(reply.Text(), 200))
|
||||
}
|
||||
|
||||
// ── P1-5: /current ───────────────────────────────────────────────────────────
|
||||
|
||||
func TestP1_5_Current_ClaudeCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
env := helper.NewEnv(t, "claudecode")
|
||||
env.Send("say hi briefly")
|
||||
|
||||
reply := env.SendWithTimeout("/current", cmdTimeout)
|
||||
assertContainsAny(t, "P1-5 /current", strings.ToLower(reply.Text()),
|
||||
"session", "会话", "#1", "s1")
|
||||
t.Logf("P1-5 OK: %q", truncate(reply.Text(), 200))
|
||||
}
|
||||
|
||||
// ── P1-7: /name ──────────────────────────────────────────────────────────────
|
||||
|
||||
func TestP1_7_Name_ClaudeCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
env := helper.NewEnv(t, "claudecode")
|
||||
env.Send("say hi briefly")
|
||||
|
||||
const newName = "blackbox-rename-test"
|
||||
env.SendWithTimeout("/name "+newName, cmdTimeout)
|
||||
listReply := env.SendWithTimeout("/list", cmdTimeout)
|
||||
|
||||
if !strings.Contains(listReply.Text(), newName) {
|
||||
t.Errorf("P1-7 /name: %q not in /list\n%q", newName, listReply.Text())
|
||||
}
|
||||
t.Logf("P1-7 OK: /list shows renamed session %q", newName)
|
||||
}
|
||||
|
||||
// ── P1-4: /switch ────────────────────────────────────────────────────────────
|
||||
|
||||
func TestP1_4_Switch_ClaudeCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
env := helper.NewEnv(t, "claudecode")
|
||||
|
||||
env.Send("say hi briefly")
|
||||
env.SendWithTimeout("/new", cmdTimeout)
|
||||
|
||||
listReply := env.SendWithTimeout("/list", cmdTimeout)
|
||||
if strings.Count(listReply.Text(), "msgs") < 2 {
|
||||
t.Skipf("could not establish 2 sessions; /list: %q", listReply.Text())
|
||||
}
|
||||
|
||||
switchReply := env.SendWithTimeout("/switch 1", cmdTimeout)
|
||||
t.Logf("/switch 1: %q", truncate(switchReply.Text(), 150))
|
||||
|
||||
currentReply := env.SendWithTimeout("/current", cmdTimeout)
|
||||
t.Logf("P1-4 OK: /current after switch: %q", truncate(currentReply.Text(), 150))
|
||||
}
|
||||
|
||||
// ── P1-6: /delete ────────────────────────────────────────────────────────────
|
||||
|
||||
func TestP1_6_Delete_ClaudeCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
env := helper.NewEnv(t, "claudecode")
|
||||
|
||||
env.Send("say hi briefly")
|
||||
env.SendWithTimeout("/new", cmdTimeout)
|
||||
env.Send("say hello briefly")
|
||||
env.SendWithTimeout("/new", cmdTimeout)
|
||||
|
||||
env.SendWithTimeout("/delete 2", cmdTimeout)
|
||||
|
||||
listReply := env.SendWithTimeout("/list", cmdTimeout)
|
||||
remaining := strings.Count(listReply.Text(), "msgs")
|
||||
if remaining != 2 {
|
||||
t.Errorf("P1-6 /delete: expected 2 sessions, got %d\n/list: %q", remaining, listReply.Text())
|
||||
}
|
||||
t.Logf("P1-6 OK: 2 sessions remain after deleting #2")
|
||||
}
|
||||
|
||||
// ── P1-9: /status ────────────────────────────────────────────────────────────
|
||||
|
||||
func TestP1_9_Status_ClaudeCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
env := helper.NewEnv(t, "claudecode")
|
||||
reply := env.SendWithTimeout("/status", cmdTimeout)
|
||||
assertContainsAny(t, "P1-9 /status", strings.ToLower(reply.Text()),
|
||||
"agent", "project", "session", "claudecode", "状态")
|
||||
t.Logf("P1-9 OK: %q", truncate(reply.Text(), 200))
|
||||
}
|
||||
|
||||
// ── P1-10: /version ──────────────────────────────────────────────────────────
|
||||
|
||||
func TestP1_10_Version_ClaudeCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
env := helper.NewEnv(t, "claudecode")
|
||||
reply := env.SendWithTimeout("/version", cmdTimeout)
|
||||
// VersionInfo is set by main at startup; in tests it's empty string "".
|
||||
// The command must still produce a reply (even if content is empty in tests),
|
||||
// confirming the command is dispatched without error.
|
||||
if reply == nil {
|
||||
t.Fatal("P1-10 /version: no reply received")
|
||||
}
|
||||
// In production, VersionInfo contains a version string like "v1.3.x".
|
||||
// In test environment it's empty — this is expected.
|
||||
t.Logf("P1-10 OK: /version replied (content: %q)", truncate(reply.Text(), 100))
|
||||
}
|
||||
|
||||
// ── P1-14: 消息排队 ───────────────────────────────────────────────────────────
|
||||
|
||||
func TestP1_14_MessageQueuing_ClaudeCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
env := helper.NewEnv(t, "claudecode")
|
||||
|
||||
// Fire 3 messages rapidly while the agent may still be starting.
|
||||
env.Platform.InjectMessage(helper.DefaultUser, helper.DefaultChat, "reply with only the word: FIRST")
|
||||
env.Platform.InjectMessage(helper.DefaultUser, helper.DefaultChat, "reply with only the word: SECOND")
|
||||
env.Platform.InjectMessage(helper.DefaultUser, helper.DefaultChat, "reply with only the word: THIRD")
|
||||
|
||||
// Wait until all 3 keywords appear in the combined output.
|
||||
// We can't predict exact message count due to thinking/queue notifications.
|
||||
deadline := time.Now().Add(3 * helper.DefaultReplyTimeout)
|
||||
for {
|
||||
combined := strings.ToUpper(env.Platform.AllText())
|
||||
allFound := strings.Contains(combined, "FIRST") &&
|
||||
strings.Contains(combined, "SECOND") &&
|
||||
strings.Contains(combined, "THIRD")
|
||||
if allFound {
|
||||
break
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatalf("P1-14: timeout waiting for FIRST/SECOND/THIRD\nall messages:\n%s",
|
||||
env.Platform.AllText())
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
t.Logf("P1-14 OK: all 3 queued messages processed\nall messages:\n%s", env.Platform.AllText())
|
||||
}
|
||||
|
||||
// ── shared helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
func assertContainsAny(t *testing.T, label, text string, candidates ...string) {
|
||||
t.Helper()
|
||||
lower := strings.ToLower(text)
|
||||
for _, c := range candidates {
|
||||
if strings.Contains(lower, strings.ToLower(c)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Errorf("%s: none of %v found\ngot: %q", label, candidates, text)
|
||||
}
|
||||
|
||||
func truncate(s string, max int) string {
|
||||
if len(s) <= max {
|
||||
return s
|
||||
}
|
||||
return s[:max] + "…"
|
||||
}
|
||||
|
||||
// restartedEngine stops env.Engine and returns a new Engine + MockPlatform
|
||||
// that share the same session store — simulating a service restart.
|
||||
func restartedEngine(t *testing.T, agentType, dataDir string) (*core.Engine, *bbplatform.MockPlatform) {
|
||||
t.Helper()
|
||||
opts := map[string]any{"work_dir": t.TempDir()}
|
||||
agent, err := core.CreateAgent(agentType, opts)
|
||||
if err != nil {
|
||||
t.Skipf("restart skip: cannot create %s agent: %v", agentType, err)
|
||||
}
|
||||
|
||||
mp := bbplatform.New(agentType + "-restarted")
|
||||
sessPath := filepath.Join(dataDir, "sessions.json")
|
||||
engine := core.NewEngine("blackbox-restarted", agent, []core.Platform{mp}, sessPath, core.LangEnglish)
|
||||
if err := engine.Start(); err != nil {
|
||||
t.Fatalf("restarted engine Start failed: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { engine.Stop(); agent.Stop() })
|
||||
return engine, mp
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
//go:build blackbox
|
||||
|
||||
package p1
|
||||
|
||||
// Session visibility regression suite — P1-30 to P1-40.
|
||||
//
|
||||
// These tests reproduce bugs from v1.3.1:
|
||||
// - /list lost sessions after /new
|
||||
// - /new xxx names didn't persist
|
||||
// - /switch caused sessions to disappear
|
||||
//
|
||||
// Per checklist: ALL P1-30..40 tests must run on BOTH claudecode AND codex.
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/chenhg5/cc-connect/tests/blackbox/helper"
|
||||
)
|
||||
|
||||
// ── P1-30: 历史会话全部可见 ───────────────────────────────────────────────────
|
||||
|
||||
func TestP1_30_HistorySessionsVisible_ClaudeCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
testHistorySessionsVisible(t, "claudecode")
|
||||
}
|
||||
|
||||
func TestP1_30_HistorySessionsVisible_Codex(t *testing.T) {
|
||||
t.Parallel()
|
||||
testHistorySessionsVisible(t, "codex")
|
||||
}
|
||||
|
||||
func testHistorySessionsVisible(t *testing.T, agentType string) {
|
||||
t.Helper()
|
||||
env := helper.NewEnv(t, agentType)
|
||||
|
||||
env.Send("say ALPHA briefly")
|
||||
env.SendWithTimeout("/new", cmdTimeout)
|
||||
env.Send("say BETA briefly")
|
||||
env.SendWithTimeout("/new", cmdTimeout)
|
||||
env.Send("say GAMMA briefly")
|
||||
|
||||
listReply := env.SendWithTimeout("/list", cmdTimeout)
|
||||
count := strings.Count(listReply.Text(), "msgs")
|
||||
if count < 3 {
|
||||
t.Errorf("P1-30: expected ≥3 sessions, got %d\n/list: %q", count, listReply.Text())
|
||||
}
|
||||
t.Logf("P1-30 OK: %d sessions visible in /list", count)
|
||||
}
|
||||
|
||||
// ── P1-31: /new 后 /list 完整 ─────────────────────────────────────────────────
|
||||
|
||||
func TestP1_31_NewThenListComplete_ClaudeCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
testNewThenListComplete(t, "claudecode")
|
||||
}
|
||||
|
||||
func TestP1_31_NewThenListComplete_Codex(t *testing.T) {
|
||||
t.Parallel()
|
||||
testNewThenListComplete(t, "codex")
|
||||
}
|
||||
|
||||
func testNewThenListComplete(t *testing.T, agentType string) {
|
||||
t.Helper()
|
||||
env := helper.NewEnv(t, agentType)
|
||||
|
||||
// Session 1: wait for full agent turn before creating session 2.
|
||||
env.SendComplete("say hi briefly")
|
||||
|
||||
env.SendWithTimeout("/new", cmdTimeout)
|
||||
env.SendComplete("say hello briefly")
|
||||
|
||||
listReply := env.SendWithTimeout("/list", cmdTimeout)
|
||||
count := strings.Count(listReply.Text(), "msgs")
|
||||
if count < 2 {
|
||||
t.Errorf("P1-31: expected ≥2 sessions after /new, got %d\n/list: %q", count, listReply.Text())
|
||||
}
|
||||
t.Logf("P1-31 OK: %d sessions after /new + message", count)
|
||||
}
|
||||
|
||||
// ── P1-32: /new xxx 命名生效 ─────────────────────────────────────────────────
|
||||
|
||||
func TestP1_32_NewWithNaming_ClaudeCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
testNewWithNaming(t, "claudecode")
|
||||
}
|
||||
|
||||
func TestP1_32_NewWithNaming_Codex(t *testing.T) {
|
||||
t.Parallel()
|
||||
testNewWithNaming(t, "codex")
|
||||
}
|
||||
|
||||
func testNewWithNaming(t *testing.T, agentType string) {
|
||||
t.Helper()
|
||||
env := helper.NewEnv(t, agentType)
|
||||
|
||||
const sessionName = "regression-name-test"
|
||||
env.SendWithTimeout("/new "+sessionName, cmdTimeout)
|
||||
env.SendComplete("say ok briefly")
|
||||
|
||||
listReply := env.SendWithTimeout("/list", cmdTimeout)
|
||||
if !strings.Contains(listReply.Text(), sessionName) {
|
||||
t.Errorf("P1-32: /new %q name missing in /list\n/list: %q", sessionName, listReply.Text())
|
||||
}
|
||||
t.Logf("P1-32 OK: session name %q appears in /list", sessionName)
|
||||
}
|
||||
|
||||
// ── P1-33: /name 重命名 ──────────────────────────────────────────────────────
|
||||
|
||||
func TestP1_33_NameRename_ClaudeCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
testNameRename(t, "claudecode")
|
||||
}
|
||||
|
||||
func TestP1_33_NameRename_Codex(t *testing.T) {
|
||||
t.Parallel()
|
||||
testNameRename(t, "codex")
|
||||
}
|
||||
|
||||
func testNameRename(t *testing.T, agentType string) {
|
||||
t.Helper()
|
||||
env := helper.NewEnv(t, agentType)
|
||||
|
||||
env.SendComplete("say hi briefly")
|
||||
|
||||
const newName = "renamed-42"
|
||||
env.SendWithTimeout("/name "+newName, cmdTimeout)
|
||||
|
||||
listReply := env.SendWithTimeout("/list", cmdTimeout)
|
||||
if !strings.Contains(listReply.Text(), newName) {
|
||||
t.Errorf("P1-33: /name %q not in /list\n/list: %q", newName, listReply.Text())
|
||||
}
|
||||
t.Logf("P1-33 OK: renamed session %q in /list", newName)
|
||||
}
|
||||
|
||||
// ── P1-34: /switch 后 /list 完整 ─────────────────────────────────────────────
|
||||
|
||||
func TestP1_34_SwitchPreservesAllSessions_ClaudeCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
testSwitchPreservesAllSessions(t, "claudecode")
|
||||
}
|
||||
|
||||
func TestP1_34_SwitchPreservesAllSessions_Codex(t *testing.T) {
|
||||
t.Parallel()
|
||||
testSwitchPreservesAllSessions(t, "codex")
|
||||
}
|
||||
|
||||
func testSwitchPreservesAllSessions(t *testing.T, agentType string) {
|
||||
t.Helper()
|
||||
env := helper.NewEnv(t, agentType)
|
||||
|
||||
env.SendComplete("say hi briefly")
|
||||
env.SendWithTimeout("/new", cmdTimeout)
|
||||
env.SendComplete("say hello briefly")
|
||||
|
||||
env.SendWithTimeout("/switch 1", cmdTimeout)
|
||||
|
||||
listReply := env.SendWithTimeout("/list", cmdTimeout)
|
||||
count := strings.Count(listReply.Text(), "msgs")
|
||||
if count < 2 {
|
||||
t.Errorf("P1-34: /list after /switch shows %d session(s), expected ≥2\n/list: %q",
|
||||
count, listReply.Text())
|
||||
}
|
||||
t.Logf("P1-34 OK: %d sessions preserved after /switch", count)
|
||||
}
|
||||
|
||||
// ── P1-35: /delete 只删目标 ──────────────────────────────────────────────────
|
||||
|
||||
func TestP1_35_DeleteOnlyRemovesTarget_ClaudeCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
testDeleteOnlyRemovesTarget(t, "claudecode")
|
||||
}
|
||||
|
||||
func TestP1_35_DeleteOnlyRemovesTarget_Codex(t *testing.T) {
|
||||
t.Parallel()
|
||||
testDeleteOnlyRemovesTarget(t, "codex")
|
||||
}
|
||||
|
||||
func testDeleteOnlyRemovesTarget(t *testing.T, agentType string) {
|
||||
t.Helper()
|
||||
env := helper.NewEnv(t, agentType)
|
||||
|
||||
env.SendComplete("say one briefly")
|
||||
env.SendWithTimeout("/new", cmdTimeout)
|
||||
env.SendComplete("say two briefly")
|
||||
env.SendWithTimeout("/new", cmdTimeout)
|
||||
env.SendComplete("say three briefly")
|
||||
|
||||
pre := env.SendWithTimeout("/list", cmdTimeout)
|
||||
if strings.Count(pre.Text(), "msgs") < 3 {
|
||||
t.Skipf("P1-35: could not establish 3 sessions; /list: %q", pre.Text())
|
||||
}
|
||||
|
||||
env.SendWithTimeout("/delete 2", cmdTimeout)
|
||||
|
||||
post := env.SendWithTimeout("/list", cmdTimeout)
|
||||
remaining := strings.Count(post.Text(), "msgs")
|
||||
if remaining != 2 {
|
||||
t.Errorf("P1-35: expected 2 sessions after /delete 2, got %d\n/list: %q",
|
||||
remaining, post.Text())
|
||||
}
|
||||
t.Logf("P1-35 OK: 2 sessions remain after deleting #2")
|
||||
}
|
||||
|
||||
// ── P1-37: Agent 回复后 /list 不减少 ─────────────────────────────────────────
|
||||
|
||||
func TestP1_37_ListAfterReplyStable_ClaudeCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
testListAfterReplyStable(t, "claudecode")
|
||||
}
|
||||
|
||||
func TestP1_37_ListAfterReplyStable_Codex(t *testing.T) {
|
||||
t.Parallel()
|
||||
testListAfterReplyStable(t, "codex")
|
||||
}
|
||||
|
||||
func testListAfterReplyStable(t *testing.T, agentType string) {
|
||||
t.Helper()
|
||||
env := helper.NewEnv(t, agentType)
|
||||
|
||||
env.SendComplete("say hi briefly")
|
||||
env.SendWithTimeout("/new", cmdTimeout)
|
||||
env.SendComplete("say hello briefly")
|
||||
|
||||
pre := env.SendWithTimeout("/list", cmdTimeout)
|
||||
preCnt := strings.Count(pre.Text(), "msgs")
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
post := env.SendWithTimeout("/list", cmdTimeout)
|
||||
postCnt := strings.Count(post.Text(), "msgs")
|
||||
|
||||
if postCnt < preCnt {
|
||||
t.Errorf("P1-37: session count dropped %d → %d after agent reply\npre: %q\npost: %q",
|
||||
preCnt, postCnt, pre.Text(), post.Text())
|
||||
}
|
||||
t.Logf("P1-37 OK: session count stable (%d → %d)", preCnt, postCnt)
|
||||
}
|
||||
|
||||
// ── P1-39: 重启后名称保留 ─────────────────────────────────────────────────────
|
||||
|
||||
func TestP1_39_RestartPreservesNames_ClaudeCode(t *testing.T) {
|
||||
// Not parallel — involves engine stop/restart.
|
||||
testRestartPreservesNames(t, "claudecode")
|
||||
}
|
||||
|
||||
func testRestartPreservesNames(t *testing.T, agentType string) {
|
||||
t.Helper()
|
||||
env := helper.NewEnv(t, agentType)
|
||||
|
||||
const sessionName = "persisted-name-99"
|
||||
env.SendWithTimeout("/new "+sessionName, cmdTimeout)
|
||||
env.SendComplete("say ok briefly")
|
||||
|
||||
pre := env.SendWithTimeout("/list", cmdTimeout)
|
||||
if !strings.Contains(pre.Text(), sessionName) {
|
||||
t.Skipf("P1-39: name %q not established pre-restart (%q); skipping", sessionName, pre.Text())
|
||||
}
|
||||
|
||||
// Simulate restart: stop engine, start fresh engine with same session store.
|
||||
env.Engine.Stop()
|
||||
|
||||
_, newMP := restartedEngine(t, agentType, env.DataDir)
|
||||
|
||||
before := newMP.MessageCount()
|
||||
newMP.InjectMessage(helper.DefaultUser, helper.DefaultChat, "/list")
|
||||
listReply := newMP.WaitForReply(before, cmdTimeout)
|
||||
if listReply == nil {
|
||||
t.Fatal("P1-39: no /list response after restart")
|
||||
}
|
||||
if !strings.Contains(listReply.Text(), sessionName) {
|
||||
t.Errorf("P1-39: name %q lost after restart\n/list: %q", sessionName, listReply.Text())
|
||||
}
|
||||
t.Logf("P1-39 OK: name %q preserved after restart", sessionName)
|
||||
}
|
||||
Reference in New Issue
Block a user