//go:build blackbox // Package p2 contains P2 blackbox tests — important features that don't block // release but failures must be recorded. // // This file covers engine-dispatched slash commands: // // /whoami, /agent-sid, /skills, /cron list, /quiet, /effort, /search // // and security guard: non-authorized user rejection. // // Run: // // CC_BLACKBOX_CLAUDECODE_API_KEY=xxx \ // go test -tags blackbox ./tests/blackbox/p2/... -timeout 600s -v package p2 import ( "fmt" "strings" "testing" "time" "github.com/chenhg5/cc-connect/tests/blackbox/helper" bbplatform "github.com/chenhg5/cc-connect/tests/blackbox/platform" ) const p2CmdTimeout = 30 * time.Second // ── P2-41: /whoami ──────────────────────────────────────────────────────────── func TestP2_41_Whoami_ClaudeCode(t *testing.T) { t.Parallel() env := helper.NewEnv(t, "claudecode") reply := env.SendWithTimeout("/whoami", p2CmdTimeout) // Should contain the user ID we injected ("user1") or user name. assertContainsAny(t, "P2-41 /whoami", reply.Text(), "user1", "test-user", "user", "身份", "ID") t.Logf("P2-41 OK: %q", truncate(reply.Text(), 150)) } // ── P2-45: /agent-sid ──────────────────────────────────────────────────────── func TestP2_45_AgentSid_ClaudeCode(t *testing.T) { t.Parallel() env := helper.NewEnv(t, "claudecode") // /agent-sid requires an active session first. env.Send("say hi briefly") reply := env.SendWithTimeout("/agent-sid", p2CmdTimeout) // Should contain a session ID of some kind. text := reply.Text() hasSessionID := strings.ContainsAny(text, "-") && len(text) > 5 if !hasSessionID && !strings.Contains(strings.ToLower(text), "session") && !strings.Contains(strings.ToLower(text), "no session") { t.Errorf("P2-45 /agent-sid: response doesn't look like a session ID\ngot: %q", text) } t.Logf("P2-45 OK: %q", truncate(text, 150)) } // ── P2-38: /skills ─────────────────────────────────────────────────────────── func TestP2_38_Skills_ClaudeCode(t *testing.T) { t.Parallel() env := helper.NewEnv(t, "claudecode") reply := env.SendWithTimeout("/skills", p2CmdTimeout) assertContainsAny(t, "P2-38 /skills", strings.ToLower(reply.Text()), "skill", "no skill", "没有", "available", "技能") t.Logf("P2-38 OK: %q", truncate(reply.Text(), 200)) } // ── P2-56 + P2-57: /cron list and /cron add ────────────────────────────────── func TestP2_56_CronList_ClaudeCode(t *testing.T) { t.Parallel() env := helper.NewEnv(t, "claudecode") reply := env.SendWithTimeout("/cron list", p2CmdTimeout) assertContainsAny(t, "P2-56 /cron list", strings.ToLower(reply.Text()), "cron", "job", "task", "schedule", "no cron", "没有", "定时") t.Logf("P2-56 OK: %q", truncate(reply.Text(), 200)) } func TestP2_57_CronAdd_ClaudeCode(t *testing.T) { t.Parallel() env := helper.NewEnv(t, "claudecode") // Add a cron job. addReply := env.SendWithTimeout( `/cron add --cron "0 9 * * 1" --prompt "Weekly status summary" --desc "weekly-test"`, p2CmdTimeout, ) t.Logf("/cron add reply: %q", truncate(addReply.Text(), 200)) // /cron list should now show the job. listReply := env.SendWithTimeout("/cron list", p2CmdTimeout) hasEntry := strings.Contains(listReply.Text(), "weekly-test") || strings.Contains(listReply.Text(), "Weekly") || strings.Contains(listReply.Text(), "0 9 * * 1") if !hasEntry { t.Errorf("P2-57 /cron add: job not found in /cron list\n/cron list: %q", listReply.Text()) } t.Logf("P2-57 OK: cron job appears in /cron list") } // ── P2-40: /quiet ──────────────────────────────────────────────────────────── func TestP2_40_Quiet_ClaudeCode(t *testing.T) { t.Parallel() env := helper.NewEnv(t, "claudecode") reply := env.SendWithTimeout("/quiet", p2CmdTimeout) assertContainsAny(t, "P2-40 /quiet", strings.ToLower(reply.Text()), "quiet", "silent", "mode", "安静", "模式", "enabled", "disabled", "on", "off") t.Logf("P2-40 OK: %q", truncate(reply.Text(), 150)) } // ── P2-39: /effort ─────────────────────────────────────────────────────────── func TestP2_39_Effort_ClaudeCode(t *testing.T) { t.Parallel() env := helper.NewEnv(t, "claudecode") reply := env.SendWithTimeout("/effort high", p2CmdTimeout) assertContainsAny(t, "P2-39 /effort high", strings.ToLower(reply.Text()), "effort", "high", "reasoning", "思考", "努力", "switched", "changed", "not supported", "支持") t.Logf("P2-39 OK: %q", truncate(reply.Text(), 150)) } // ── P2-46: /search ─────────────────────────────────────────────────────────── func TestP2_46_Search_ClaudeCode(t *testing.T) { t.Parallel() env := helper.NewEnv(t, "claudecode") // Create a session with a known keyword. env.Send("the keyword is XYZZY42") // Search for the keyword. reply := env.SendWithTimeout("/search XYZZY42", p2CmdTimeout) assertContainsAny(t, "P2-46 /search", strings.ToLower(reply.Text()), "xyzzy42", "session", "result", "found", "no result", "没有", "搜索") t.Logf("P2-46 OK: %q", truncate(reply.Text(), 200)) } // ── P2-61: 非授权用户被拒绝 ────────────────────────────────────────────────── // TestP2_61_UnauthorizedUserIgnored verifies that when a message arrives from // a user ID not in allow_from, cc-connect does NOT send a reply. // // The engine's allow_from filter is configured per-project. This test uses a // separate MockPlatform that injects a message from a user NOT in any // allow_from list. The platform should receive no reply. // // NOTE: This test requires allow_from to be configured. Since the blackbox // test engine is created without allow_from config, we CANNOT test rejection // here — this is a configuration-level feature. Marking as informational. func TestP2_61_AllowFromDocumented(t *testing.T) { t.Log("P2-61 (allow_from): requires platform-level allow_from config.") t.Log("Verified manually via IM with non-whitelisted Telegram user ID.") t.Log("Cannot automate without modifying engine config per-test.") // Not a failure — documenting the gap. } // ── P2-47: /bind ───────────────────────────────────────────────────────────── func TestP2_47_Bind_ClaudeCode(t *testing.T) { t.Parallel() env := helper.NewEnv(t, "claudecode") reply := env.SendWithTimeout("/bind setup", p2CmdTimeout) assertContainsAny(t, "P2-47 /bind setup", strings.ToLower(reply.Text()), "bind", "relay", "setup", "绑定", "不支持", "not supported", "project") t.Logf("P2-47 OK: %q", truncate(reply.Text(), 200)) } // ── 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] + "…" } // sendToNewPlatform injects a message from a fresh user to the existing engine // via a second mock platform. Used for multi-user test scenarios. func sendToNewPlatform(_ *bbplatform.MockPlatform, userID, content string) { // placeholder — used in custom command tests below _ = fmt.Sprintf("user %s: %s", userID, content) }