mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-19 09:40:12 +00:00
173 lines
4.7 KiB
Go
173 lines
4.7 KiB
Go
package rpc
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"google.golang.org/grpc/metadata"
|
|
|
|
"github.com/nezhahq/nezha/model"
|
|
pb "github.com/nezhahq/nezha/proto"
|
|
"github.com/nezhahq/nezha/service/singleton"
|
|
)
|
|
|
|
type fakeTaskStream struct {
|
|
sent chan *pb.Task
|
|
delay time.Duration
|
|
err error
|
|
}
|
|
|
|
func newFakeStream() *fakeTaskStream {
|
|
return &fakeTaskStream{sent: make(chan *pb.Task, 4)}
|
|
}
|
|
|
|
func (s *fakeTaskStream) Send(t *pb.Task) error {
|
|
if s.err != nil {
|
|
return s.err
|
|
}
|
|
s.sent <- t
|
|
return nil
|
|
}
|
|
func (s *fakeTaskStream) Recv() (*pb.TaskResult, error) { return nil, context.Canceled }
|
|
func (s *fakeTaskStream) SetHeader(metadata.MD) error { return nil }
|
|
func (s *fakeTaskStream) SendHeader(metadata.MD) error { return nil }
|
|
func (s *fakeTaskStream) SetTrailer(metadata.MD) {}
|
|
func (s *fakeTaskStream) Context() context.Context { return context.Background() }
|
|
func (s *fakeTaskStream) SendMsg(any) error { return nil }
|
|
func (s *fakeTaskStream) RecvMsg(any) error { return context.Canceled }
|
|
|
|
func installFakeServer(t *testing.T, id uint64, stream pb.NezhaService_RequestTaskServer) func() {
|
|
t.Helper()
|
|
original := singleton.ServerShared
|
|
sc := singleton.NewEmptyServerClassForTest()
|
|
srv := &model.Server{}
|
|
srv.ID = id
|
|
srv.SetTaskStream(stream)
|
|
sc.InsertForTest(srv)
|
|
singleton.ServerShared = sc
|
|
return func() { singleton.ServerShared = original }
|
|
}
|
|
|
|
func TestCallAgent_RejectsNonMCPType(t *testing.T) {
|
|
_, err := CallAgent(context.Background(), 1, model.TaskTypeCommand, struct{}{}, time.Second)
|
|
if err == nil {
|
|
t.Fatalf("expected error for non-MCP type")
|
|
}
|
|
}
|
|
|
|
func TestCallAgent_OfflineWhenNoStream(t *testing.T) {
|
|
original := singleton.ServerShared
|
|
sc := singleton.NewEmptyServerClassForTest()
|
|
srv := &model.Server{}
|
|
srv.ID = 7
|
|
sc.InsertForTest(srv)
|
|
singleton.ServerShared = sc
|
|
t.Cleanup(func() { singleton.ServerShared = original })
|
|
|
|
_, err := CallAgent(context.Background(), 7, model.TaskTypeExec, struct{}{}, time.Second)
|
|
if !errors.Is(err, ErrAgentOffline) {
|
|
t.Fatalf("expected ErrAgentOffline, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCallAgent_HappyPath_DelivlersResultByTaskID(t *testing.T) {
|
|
stream := newFakeStream()
|
|
cleanup := installFakeServer(t, 42, stream)
|
|
defer cleanup()
|
|
|
|
resultPayload, _ := json.Marshal(model.ExecResult{ExitCode: 0, Stdout: "hello"})
|
|
|
|
var captured atomic.Uint64
|
|
done := make(chan struct{})
|
|
go func() {
|
|
sent := <-stream.sent
|
|
captured.Store(sent.GetId())
|
|
deliverMCPResult(&pb.TaskResult{
|
|
Id: sent.GetId(),
|
|
Type: model.TaskTypeExec,
|
|
Data: string(resultPayload),
|
|
Successful: true,
|
|
})
|
|
close(done)
|
|
}()
|
|
|
|
raw, err := CallAgent(context.Background(), 42, model.TaskTypeExec, model.ExecRequest{Cmd: "x"}, 5*time.Second)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
<-done
|
|
if captured.Load() == 0 {
|
|
t.Fatalf("task id never captured")
|
|
}
|
|
var got model.ExecResult
|
|
if err := json.Unmarshal(raw, &got); err != nil {
|
|
t.Fatalf("bad result json: %v", err)
|
|
}
|
|
if got.Stdout != "hello" {
|
|
t.Fatalf("payload not propagated, got %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestCallAgent_Timeout(t *testing.T) {
|
|
stream := newFakeStream()
|
|
cleanup := installFakeServer(t, 43, stream)
|
|
defer cleanup()
|
|
|
|
go func() { <-stream.sent }()
|
|
|
|
_, err := CallAgent(context.Background(), 43, model.TaskTypeFsRead, model.FsReadRequest{Path: "/x"}, 50*time.Millisecond)
|
|
if !errors.Is(err, ErrAgentTimeout) {
|
|
t.Fatalf("expected ErrAgentTimeout, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCallAgent_LateResultIsDropped(t *testing.T) {
|
|
stream := newFakeStream()
|
|
cleanup := installFakeServer(t, 44, stream)
|
|
defer cleanup()
|
|
|
|
var taskID uint64
|
|
got := make(chan struct{})
|
|
go func() {
|
|
sent := <-stream.sent
|
|
taskID = sent.GetId()
|
|
close(got)
|
|
}()
|
|
|
|
_, err := CallAgent(context.Background(), 44, model.TaskTypeFsDelete, model.FsDeleteRequest{Path: "/x"}, 50*time.Millisecond)
|
|
if !errors.Is(err, ErrAgentTimeout) {
|
|
t.Fatalf("expected timeout")
|
|
}
|
|
<-got
|
|
|
|
deliverMCPResult(&pb.TaskResult{Id: taskID, Type: model.TaskTypeFsDelete, Successful: true, Data: "{}"})
|
|
if _, ok := mcpInflight.Load(taskID); ok {
|
|
t.Fatalf("inflight entry must be cleaned up after timeout")
|
|
}
|
|
}
|
|
|
|
func TestCallAgent_UnsuccessfulIsError(t *testing.T) {
|
|
stream := newFakeStream()
|
|
cleanup := installFakeServer(t, 45, stream)
|
|
defer cleanup()
|
|
|
|
go func() {
|
|
sent := <-stream.sent
|
|
deliverMCPResult(&pb.TaskResult{
|
|
Id: sent.GetId(),
|
|
Type: sent.GetType(),
|
|
Successful: false,
|
|
Data: "agent says nope",
|
|
})
|
|
}()
|
|
|
|
_, err := CallAgent(context.Background(), 45, model.TaskTypeFsWrite, model.FsWriteRequest{Path: "/x", Content: "y"}, time.Second)
|
|
if err == nil || err.Error() != "agent says nope" {
|
|
t.Fatalf("expected agent error message, got %v", err)
|
|
}
|
|
}
|