202 lines
8.7 KiB
Go
202 lines
8.7 KiB
Go
package rpc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
agentpb "git.ipao.vip/rogee/go-sip/gen/agent"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
func authorizedMockRequest(now time.Time) *agentpb.ExecuteAuthorizedRequest {
|
|
return &agentpb.ExecuteAuthorizedRequest{
|
|
SchemaVersion: "agent-authorized-origination.v0.1",
|
|
Meta: testMeta("authorized-1", "authorized-key-1", 1),
|
|
Binding: &agentpb.ExecutionBinding{
|
|
TenantId: "tenant-1", TenantKey: "tenant-original", ExecutionId: "execution-authorized-1",
|
|
TaskId: "task-1", TaskItemId: "command-1", TaskRevision: 2,
|
|
AgentVersionId: "agent-version-1", RoutePolicyId: "route-1", CallerProfileId: "caller-1",
|
|
},
|
|
SelectedTrunkId: "trunk-1",
|
|
CallerId: "BD93205882",
|
|
Callee: "15003164745",
|
|
RingTimeoutMs: 12000,
|
|
MaxCallDurationMs: 30000,
|
|
DialBeforeUnixMs: now.Add(time.Minute).UnixMilli(),
|
|
BoundSnapshotSha256: strings.Repeat("a", 64),
|
|
}
|
|
}
|
|
|
|
func TestExecuteAuthorizedMockOneShotAndIdentity(t *testing.T) {
|
|
now := time.Date(2026, 9, 21, 1, 30, 0, 0, time.UTC)
|
|
attempts := 0
|
|
server := NewServer(ServerOptions{Mode: "mock", Now: func() time.Time { return now }, MockAuthorizedOriginate: func(_ context.Context, req *agentpb.ExecuteAuthorizedRequest) error {
|
|
attempts++
|
|
if req.SelectedTrunkId != "trunk-1" || req.CallerId != "BD93205882" || req.MaxCallDurationMs != 30000 {
|
|
t.Fatalf("Agent received modified Dispatcher decision: %+v", req)
|
|
}
|
|
return nil
|
|
}})
|
|
activateTestServer(t, server)
|
|
req := authorizedMockRequest(now)
|
|
first, err := server.ExecuteAuthorized(context.Background(), req)
|
|
if err != nil || first.GetReceipt().GetResult() != agentpb.ResultCode_RESULT_CODE_APPLIED || first.GetState() != agentpb.ExecutionState_EXECUTION_STATE_TERMINAL || attempts != 1 {
|
|
t.Fatalf("first mock execution=%+v attempts=%d err=%v", first, attempts, err)
|
|
}
|
|
replay, err := server.ExecuteAuthorized(context.Background(), req)
|
|
if err != nil || replay.GetReceipt().GetMeta().GetOperationId() != first.GetReceipt().GetMeta().GetOperationId() || attempts != 1 {
|
|
t.Fatalf("replay caused duplicate mock dial: %+v attempts=%d err=%v", replay, attempts, err)
|
|
}
|
|
changed := proto.Clone(req).(*agentpb.ExecuteAuthorizedRequest)
|
|
changed.Callee = "15830461047"
|
|
conflict, err := server.ExecuteAuthorized(context.Background(), changed)
|
|
if err != nil || conflict.GetReceipt().GetResult() != agentpb.ResultCode_RESULT_CODE_CONFLICT || attempts != 1 {
|
|
t.Fatalf("same key/different body: %+v attempts=%d err=%v", conflict, attempts, err)
|
|
}
|
|
newKey := proto.Clone(req).(*agentpb.ExecuteAuthorizedRequest)
|
|
newKey.Meta = testMeta("authorized-2", "authorized-key-2", 1)
|
|
conflict, err = server.ExecuteAuthorized(context.Background(), newKey)
|
|
if err != nil || conflict.GetReceipt().GetResult() != agentpb.ResultCode_RESULT_CODE_CONFLICT || attempts != 1 {
|
|
t.Fatalf("same execution/new operation: %+v attempts=%d err=%v", conflict, attempts, err)
|
|
}
|
|
}
|
|
|
|
func TestExecuteAuthorizedRejectsExpiredAndInvalidMock(t *testing.T) {
|
|
now := time.Date(2026, 9, 21, 1, 30, 0, 0, time.UTC)
|
|
attempts := 0
|
|
server := NewServer(ServerOptions{Mode: "mock", Now: func() time.Time { return now }, MockAuthorizedOriginate: func(context.Context, *agentpb.ExecuteAuthorizedRequest) error {
|
|
attempts++
|
|
return nil
|
|
}})
|
|
activateTestServer(t, server)
|
|
for _, tc := range []struct {
|
|
name string
|
|
change func(*agentpb.ExecuteAuthorizedRequest)
|
|
}{
|
|
{"expired at Agent", func(r *agentpb.ExecuteAuthorizedRequest) { r.DialBeforeUnixMs = now.UnixMilli() }},
|
|
{"missing selected trunk", func(r *agentpb.ExecuteAuthorizedRequest) { r.SelectedTrunkId = "" }},
|
|
{"invalid snapshot digest", func(r *agentpb.ExecuteAuthorizedRequest) { r.BoundSnapshotSha256 = "bad" }},
|
|
{"unknown contract version", func(r *agentpb.ExecuteAuthorizedRequest) { r.SchemaVersion = "other" }},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
req := authorizedMockRequest(now)
|
|
tc.change(req)
|
|
response, err := server.ExecuteAuthorized(context.Background(), req)
|
|
if response != nil || status.Code(err) != codes.InvalidArgument && status.Code(err) != codes.FailedPrecondition {
|
|
t.Fatalf("invalid decision response=%+v err=%v", response, err)
|
|
}
|
|
if attempts != 0 {
|
|
t.Fatalf("invalid decision reached mock dial %d times", attempts)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExecuteAuthorizedMockFailureIsUnknownAndNeverRetried(t *testing.T) {
|
|
now := time.Date(2026, 9, 21, 1, 30, 0, 0, time.UTC)
|
|
attempts := 0
|
|
server := NewServer(ServerOptions{Mode: "mock", Now: func() time.Time { return now }, MockAuthorizedOriginate: func(context.Context, *agentpb.ExecuteAuthorizedRequest) error {
|
|
attempts++
|
|
return errors.New("mock transport failed")
|
|
}})
|
|
activateTestServer(t, server)
|
|
req := authorizedMockRequest(now)
|
|
first, err := server.ExecuteAuthorized(context.Background(), req)
|
|
if err != nil || first.GetReceipt().GetResult() != agentpb.ResultCode_RESULT_CODE_UNKNOWN || attempts != 1 {
|
|
t.Fatalf("uncertain mock execution=%+v attempts=%d err=%v", first, attempts, err)
|
|
}
|
|
replay, err := server.ExecuteAuthorized(context.Background(), req)
|
|
if err != nil || replay.GetReceipt().GetResult() != agentpb.ResultCode_RESULT_CODE_UNKNOWN || attempts != 1 {
|
|
t.Fatalf("uncertain execution redialed: %+v attempts=%d err=%v", replay, attempts, err)
|
|
}
|
|
}
|
|
|
|
func TestExecuteAuthorizedJournalPreventsRedialAfterAgentRestart(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
fail bool
|
|
}{
|
|
{"mock completed", false},
|
|
{"mock outcome unknown", true},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
now := time.Date(2026, 9, 21, 1, 30, 0, 0, time.UTC)
|
|
path := filepath.Join(t.TempDir(), "agent-session.json")
|
|
attempts := 0
|
|
start := func(generation uint64, bootID string) *Server {
|
|
server := NewServer(ServerOptions{Mode: "mock", Now: func() time.Time { return now }, StatePath: path,
|
|
Status: &agentpb.AgentStatus{AgentId: "agent-1", CellId: "cell-1", BootId: bootID},
|
|
MockAuthorizedOriginate: func(context.Context, *agentpb.ExecuteAuthorizedRequest) error {
|
|
attempts++
|
|
if tc.fail {
|
|
return errors.New("mock adapter failed")
|
|
}
|
|
return nil
|
|
},
|
|
})
|
|
meta := testMeta("activate", "", 0)
|
|
meta.BootId = bootID
|
|
if _, err := server.ActivateAgent(context.Background(), &agentpb.ActivateAgentRequest{
|
|
Meta: meta, Binding: &agentpb.AgentBinding{AgentId: "agent-1", CellId: "cell-1", ExpectedBootId: bootID,
|
|
DispatcherEpoch: "epoch-1", SessionGeneration: generation}, ActivationOperationId: "activate",
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return server
|
|
}
|
|
first := start(1, "boot-1")
|
|
req := authorizedMockRequest(now)
|
|
response, err := first.ExecuteAuthorized(context.Background(), req)
|
|
if err != nil || response == nil || attempts != 1 {
|
|
t.Fatalf("first mock call: response=%+v attempts=%d err=%v", response, attempts, err)
|
|
}
|
|
terminalObservedAt := now.UnixMilli()
|
|
now = now.Add(time.Hour)
|
|
recovered := start(2, "boot-2")
|
|
replay := proto.Clone(req).(*agentpb.ExecuteAuthorizedRequest)
|
|
replay.Meta = testMeta("authorized-new-boot", "authorized-key-new-boot", 2)
|
|
replay.Meta.BootId = "boot-2"
|
|
retry, err := recovered.ExecuteAuthorized(context.Background(), replay)
|
|
if err != nil || retry.GetReceipt().GetResult() == agentpb.ResultCode_RESULT_CODE_APPLIED || attempts != 1 {
|
|
t.Fatalf("restart redialed execution: response=%+v attempts=%d err=%v", retry, attempts, err)
|
|
}
|
|
queryMeta := testMeta("query-recovered", "query-recovered-key", 2)
|
|
queryMeta.BootId = "boot-2"
|
|
query, err := recovered.QueryExecution(context.Background(), &agentpb.QueryExecutionRequest{Meta: queryMeta, Binding: req.Binding})
|
|
if err != nil || query.Snapshot == nil || query.Snapshot.Unknown != tc.fail {
|
|
t.Fatalf("recovered state failed to retain uncertainty: %+v err=%v", query, err)
|
|
}
|
|
if !tc.fail && query.Snapshot.ObservedAtUnixMs != terminalObservedAt {
|
|
t.Fatalf("terminal observation drifted after restart: got=%d want=%d", query.Snapshot.ObservedAtUnixMs, terminalObservedAt)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExecuteAuthorizedCannotDialOutsideMockOrWithoutAdapter(t *testing.T) {
|
|
now := time.Date(2026, 9, 21, 1, 30, 0, 0, time.UTC)
|
|
for _, tc := range []struct {
|
|
name string
|
|
mode string
|
|
}{
|
|
{"mixed disabled", "mixed"},
|
|
{"real disabled", "real"},
|
|
{"mock adapter missing", "mock"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
server := NewServer(ServerOptions{Mode: tc.mode, Now: func() time.Time { return now }})
|
|
activateTestServer(t, server)
|
|
response, err := server.ExecuteAuthorized(context.Background(), authorizedMockRequest(now))
|
|
if response != nil || status.Code(err) != codes.FailedPrecondition {
|
|
t.Fatalf("unavailable mode/adapter response=%+v err=%v", response, err)
|
|
}
|
|
})
|
|
}
|
|
}
|