Files
cc-connect/tests/blackbox/p2/commands_test.go
T
2026-06-02 23:14:41 +08:00

200 lines
8.2 KiB
Go

//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)
}