package main import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "strings" "testing" "gitcode.com/JianFeeeee/homeagent-sdk/sdk" ) func newPermissionTestPlugin(t *testing.T) *Plugin { t.Helper() instance, err := NewPluginFactory("qq", nil) if err != nil { t.Fatal(err) } return instance.(*Plugin) } func toolCallContext(name string, args map[string]interface{}) *sdk.StageContext { return &sdk.StageContext{ToolCalls: []sdk.ToolCall{{Name: name, Arguments: args}}} } func TestOwnerBypassesQQPermissionBoundary(t *testing.T) { p := newPermissionTestPlugin(t) p.auth = qqAuthContext{active: true, owner: true, userID: 2198972886} ctx := toolCallContext("calendar_list", nil) if err := p.beforeToolcall(ctx); err != nil { t.Fatal(err) } if ctx.Response != nil { t.Fatalf("owner call rejected: %s", *ctx.Response) } } func TestPrivateResourceCannotBeAllowlisted(t *testing.T) { p := newPermissionTestPlugin(t) p.privateToolAllowlist = append(p.privateToolAllowlist, "calendar_*") p.auth = qqAuthContext{active: true, userID: 10001} ctx := toolCallContext("calendar_list", nil) if err := p.beforeToolcall(ctx); err != nil { t.Fatal(err) } if ctx.Response == nil || !strings.Contains(*ctx.Response, "私人资源工具") { t.Fatalf("expected private-resource denial, got %#v", ctx.Response) } } func TestNonOwnerQQHistoryIsScopedToCurrentGroup(t *testing.T) { p := newPermissionTestPlugin(t) p.auth = qqAuthContext{active: true, messageID: 88, userID: 10001, groupID: 20002, isGroup: true} ctx := toolCallContext("qq_get_history", map[string]interface{}{"group_id": int64(20003)}) if err := p.beforeToolcall(ctx); err != nil { t.Fatal(err) } if ctx.Response == nil || !strings.Contains(*ctx.Response, "当前 QQ 会话") { t.Fatalf("cross-group history not rejected: %#v", ctx.Response) } ctx = toolCallContext("qq_get_history", map[string]interface{}{"group_id": int64(20002)}) if err := p.beforeToolcall(ctx); err != nil { t.Fatal(err) } if ctx.Response != nil { t.Fatalf("current-group history rejected: %s", *ctx.Response) } } func TestUnmatchedQQInputIsDowngraded(t *testing.T) { p := newPermissionTestPlugin(t) p.auth = qqAuthContext{active: true, owner: true, userID: 2198972886} ctx := &sdk.StageContext{ RawMessage: "来自未知事件(message_id=404)", Extra: map[string]interface{}{"input_source": "qq"}, } if err := p.onInputAuthContext(ctx); err != nil { t.Fatal(err) } if !p.auth.active || p.auth.owner || p.auth.userID != 0 { t.Fatalf("unmatched input reused prior privilege: %+v", p.auth) } } func TestDuplicateQQOutputIsStopped(t *testing.T) { p := newPermissionTestPlugin(t) p.maxDuplicateSend = 1 p.auth = qqAuthContext{active: true, owner: true, userID: 2198972886} args := map[string]interface{}{"payload": "same", "type": "text", "meta": `{"user_id":123}`} ctx := toolCallContext("output_send__qq", args) if err := p.beforeToolcall(ctx); err != nil { t.Fatal(err) } if ctx.Response != nil { t.Fatalf("first send rejected: %s", *ctx.Response) } ctx = toolCallContext("output_send__qq", args) if err := p.beforeToolcall(ctx); err != nil { t.Fatal(err) } if ctx.Response == nil || !strings.Contains(*ctx.Response, "循环保险") { t.Fatalf("duplicate send not stopped: %#v", ctx.Response) } } func TestGroupAndUserRouteAddsLeadingMention(t *testing.T) { var path string var request map[string]interface{} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { path = r.URL.Path if err := json.NewDecoder(r.Body).Decode(&request); err != nil { t.Errorf("decode request: %v", err) } w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"status":"ok","retcode":0,"data":{"message_id":1}}`)) })) defer server.Close() p := newPermissionTestPlugin(t) p.napcatURL = server.URL p.httpClient = server.Client() _, err := p.handleChannelOutput(map[string]interface{}{ "payload": "hello", "type": "text", "meta": `{"group_id":20002,"user_id":10001}`, }) if err != nil { t.Fatal(err) } if path != "/send_group_msg" { t.Fatalf("path=%q, want /send_group_msg", path) } segments, ok := request["message"].([]interface{}) if !ok || len(segments) < 2 { t.Fatalf("message is not a segment array: %#v", request["message"]) } mention, _ := segments[0].(map[string]interface{}) data, _ := mention["data"].(map[string]interface{}) if mention["type"] != "at" || data["qq"] != "10001" { t.Fatalf("leading mention=%#v", mention) } } // 回归:循环保险曾按“总数”拦截,导致参数不同且必需的调用被误杀。 // 现在只拦参数完全相同的重复调用。 func TestDistinctQQOutputsAreNotTreatedAsDuplicates(t *testing.T) { p := newPermissionTestPlugin(t) p.auth = qqAuthContext{active: true, owner: true, userID: 2198972886} // maxDuplicateSend 默认 1:同一条消息重复才会被拦,不同消息必须全部放行。 for i := 0; i < 5; i++ { ctx := toolCallContext("output_send__qq", map[string]interface{}{ "payload": fmt.Sprintf("message-%d", i), "type": "text", "meta": `{"user_id":123}`, }) if err := p.beforeToolcall(ctx); err != nil { t.Fatal(err) } if ctx.Response != nil { t.Fatalf("distinct message %d was blocked: %s", i, *ctx.Response) } } } func TestDistinctNecessaryToolCallsAreNotBlocked(t *testing.T) { p := newPermissionTestPlugin(t) p.auth = qqAuthContext{active: true, owner: true, userID: 2198972886} // 旧实现 maxQQToolCalls=32 会在第 33 个不同参数的必需调用处误拦。 for i := 0; i < 50; i++ { ctx := toolCallContext("cmd_run", map[string]interface{}{"command": fmt.Sprintf("cmd-%d", i)}) if err := p.beforeToolcall(ctx); err != nil { t.Fatal(err) } if ctx.Response != nil { t.Fatalf("necessary tool call %d was blocked: %s", i, *ctx.Response) } } } func TestZeroLimitsMeanUnlimited(t *testing.T) { p := newPermissionTestPlugin(t) p.maxQQOutputCalls = 0 p.maxDuplicateSend = 0 p.maxQQToolCalls = 0 p.auth = qqAuthContext{active: true, owner: true, userID: 2198972886} for i := 0; i < 30; i++ { ctx := toolCallContext("output_send__qq", map[string]interface{}{ "payload": "same-content", "type": "text", "meta": `{"user_id":123}`, }) if err := p.beforeToolcall(ctx); err != nil { t.Fatal(err) } if ctx.Response != nil { t.Fatalf("0 should mean unlimited, blocked at %d: %s", i, *ctx.Response) } } }