feat: implement control history and mutation APIs

This commit is contained in:
2026-09-06 10:18:37 +00:00
parent ef43e9592c
commit 8df56263fb
11 changed files with 1515 additions and 3 deletions
+170
View File
@@ -15,10 +15,12 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/grpc/test/bufconn"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
)
func TestControlListAndGetViews_HP_CONTROL_01(t *testing.T) {
@@ -147,6 +149,174 @@ func TestControlGRPCRoundTrip_HP_CONTROL_06(t *testing.T) {
}
}
func TestFollowCommandStreamsDurableEvents_HP_CONTROL_10(t *testing.T) {
service, persistence := newTestService(t)
defer persistence.Close()
registerControlClient(t, persistence, "win-a", rvboxv1.Platform_PLATFORM_WINDOWS, rvboxv1.ShellType_SHELL_CMD, 6)
issue := fixedIssue(0xa6)
now := time.Now().UTC()
specBytes, err := proto.Marshal(&rvboxv1.ExecutionSpec{ShellType: rvboxv1.ShellType_SHELL_CMD, Source: &rvboxv1.ExecutionSpec_CommandText{CommandText: "echo"}})
if err != nil {
t.Fatal(err)
}
if _, err := persistence.QueueCommand(context.Background(), store.QueueCommandInput{IssueUUID: issue, ClientID: "win-a", IssueTime: now, ReceiptTime: now, ImmutableSHA256: sha256.Sum256([]byte("follow")), ExecutionSpec: specBytes}); err != nil {
t.Fatal(err)
}
if _, err := persistence.ClaimNextDispatch(context.Background(), "win-a", 1, now); err != nil {
t.Fatal(err)
}
if _, err := persistence.RecordCommandAcceptance(context.Background(), issue, "win-a", 1, 1, true, now); err != nil {
t.Fatal(err)
}
for index, lifecycle := range []rvboxv1.CommandLifecycle{rvboxv1.CommandLifecycle_COMMAND_RUNNING, rvboxv1.CommandLifecycle_COMMAND_SUCCEEDED} {
event := &rvboxv1.CommandEvent{IssueUuid: issue.String(), EventSeq: uint64(index + 1), ObservedAt: timestamppb.New(now.Add(time.Duration(index) * time.Millisecond)), Payload: &rvboxv1.CommandEvent_Lifecycle{Lifecycle: &rvboxv1.LifecycleChange{Lifecycle: lifecycle, CommandRevision: 1}}}
digest, digestErr := agentproto.CommandEventDigest(event)
if digestErr != nil {
t.Fatal(digestErr)
}
event.ImmutableEventSha256 = digest[:]
payload, marshalErr := proto.MarshalOptions{Deterministic: true}.Marshal(event)
if marshalErr != nil {
t.Fatal(marshalErr)
}
if _, appendErr := persistence.AppendCommandEvent(context.Background(), store.EventAppend{IssueUUID: [16]byte(issue), ClientID: "win-a", SessionGeneration: 1, EventSeq: uint64(index + 1), ObservedUnixNano: event.GetObservedAt().AsTime().UnixNano(), ReceiptUnixNano: now.Add(time.Duration(index) * time.Millisecond).UnixNano(), EventType: 4, Compression: 1, RawLength: uint64(len(payload)), Payload: payload, ImmutableSHA256: digest, Lifecycle: &lifecycle, LifecycleRevision: 1}); appendErr != nil {
t.Fatal(appendErr)
}
}
stream := &testFollowStream{ctx: context.Background()}
if err := service.FollowCommand(&rvboxv1.FollowCommandRequest{ClientId: "win-a", IssueUuid: issue.String(), IncludeExisting: true}, stream); err != nil {
t.Fatal(err)
}
if len(stream.responses) != 2 || stream.responses[0].GetEvent().GetEventSeq() != 1 || stream.responses[1].GetEvent().GetLifecycle().GetLifecycle() != rvboxv1.CommandLifecycle_COMMAND_SUCCEEDED {
t.Fatalf("follow responses = %#v", stream.responses)
}
}
func TestGetOutputSlicesWithAuthenticatedCursor_HP_CONTROL_12(t *testing.T) {
service, persistence := newTestService(t)
defer persistence.Close()
registerControlClient(t, persistence, "win-a", rvboxv1.Platform_PLATFORM_WINDOWS, rvboxv1.ShellType_SHELL_CMD, 7)
issue := fixedIssue(0xa7)
now := time.Now().UTC()
specBytes, _ := proto.Marshal(&rvboxv1.ExecutionSpec{ShellType: rvboxv1.ShellType_SHELL_CMD, Source: &rvboxv1.ExecutionSpec_CommandText{CommandText: "echo"}})
if _, err := persistence.QueueCommand(context.Background(), store.QueueCommandInput{IssueUUID: issue, ClientID: "win-a", IssueTime: now, ReceiptTime: now, ImmutableSHA256: sha256.Sum256([]byte("output")), ExecutionSpec: specBytes}); err != nil {
t.Fatal(err)
}
if _, err := persistence.ClaimNextDispatch(context.Background(), "win-a", 1, now); err != nil {
t.Fatal(err)
}
if _, err := persistence.RecordCommandAcceptance(context.Background(), issue, "win-a", 1, 1, true, now); err != nil {
t.Fatal(err)
}
event := &rvboxv1.CommandEvent{IssueUuid: issue.String(), EventSeq: 1, ObservedAt: timestamppb.New(now), Payload: &rvboxv1.CommandEvent_Output{Output: &rvboxv1.OutputChunk{Stream: rvboxv1.StreamKind_STREAM_STDOUT, Compression: rvboxv1.Compression_COMPRESSION_NONE, Data: []byte("abcdef"), UncompressedSize: 6, CompressedSize: 6}}}
digest, err := agentproto.CommandEventDigest(event)
if err != nil {
t.Fatal(err)
}
event.ImmutableEventSha256 = digest[:]
payload, err := proto.MarshalOptions{Deterministic: true}.Marshal(event)
if err != nil {
t.Fatal(err)
}
if _, err := persistence.AppendCommandEvent(context.Background(), store.EventAppend{IssueUUID: [16]byte(issue), ClientID: "win-a", SessionGeneration: 1, EventSeq: 1, ObservedUnixNano: now.UnixNano(), ReceiptUnixNano: now.UnixNano(), EventType: 5, Stream: 1, Compression: 1, RawLength: uint64(len(payload)), Payload: payload, ImmutableSHA256: digest, Output: true}); err != nil {
t.Fatal(err)
}
first, err := service.GetOutput(context.Background(), &rvboxv1.GetOutputRequest{ClientId: "win-a", IssueUuid: issue.String(), Streams: []rvboxv1.StreamKind{rvboxv1.StreamKind_STREAM_STDOUT}, MaxBytes: 3})
if err != nil || len(first.GetOutput()) != 1 || string(first.GetOutput()[0].GetData()) != "abc" || first.GetNextPageToken() == "" {
t.Fatalf("first output page = %#v, %v", first, err)
}
second, err := service.GetOutput(context.Background(), &rvboxv1.GetOutputRequest{ClientId: "win-a", IssueUuid: issue.String(), Streams: []rvboxv1.StreamKind{rvboxv1.StreamKind_STREAM_STDOUT}, MaxBytes: 3, PageToken: first.GetNextPageToken()})
if err != nil || len(second.GetOutput()) != 1 || string(second.GetOutput()[0].GetData()) != "def" || !second.GetOutput()[0].GetEndOfEvent() {
t.Fatalf("second output page = %#v, %v", second, err)
}
if _, err := service.GetOutput(context.Background(), &rvboxv1.GetOutputRequest{ClientId: "win-a", IssueUuid: issue.String(), Streams: []rvboxv1.StreamKind{rvboxv1.StreamKind_STREAM_STDERR}, MaxBytes: 3, PageToken: first.GetNextPageToken()}); status.Code(err) != codes.InvalidArgument {
t.Fatalf("changed output filter code = %v", status.Code(err))
}
}
func TestControlMutationIdempotencyAndQueuedCancellation_BH_CONTROL_13(t *testing.T) {
service, persistence := newTestService(t)
defer persistence.Close()
registerControlClient(t, persistence, "win-a", rvboxv1.Platform_PLATFORM_WINDOWS, rvboxv1.ShellType_SHELL_CMD, 8)
issue := fixedIssue(0xa8)
if _, err := service.RunCommand(context.Background(), &rvboxv1.RunCommandRequest{TargetClientId: "win-a", RequestId: issue.String(), Spec: &rvboxv1.ExecutionSpec{ShellType: rvboxv1.ShellType_SHELL_CMD, Source: &rvboxv1.ExecutionSpec_CommandText{CommandText: "wait"}}}); err != nil {
t.Fatal(err)
}
stdinID := fixedIssue(0xa9)
appendRequest := &rvboxv1.AppendStdinRequest{ClientId: "win-a", IssueUuid: issue.String(), RequestId: stdinID.String(), Data: []byte("hello"), AppendNewline: true}
first, err := service.AppendStdin(context.Background(), appendRequest)
if err != nil || first.GetWriteSeq() != 1 {
t.Fatalf("append stdin = %#v, %v", first, err)
}
second, err := service.AppendStdin(context.Background(), proto.Clone(appendRequest).(*rvboxv1.AppendStdinRequest))
if err != nil || second.GetWriteSeq() != first.GetWriteSeq() {
t.Fatalf("append replay = %#v, %v", second, err)
}
conflict := proto.Clone(appendRequest).(*rvboxv1.AppendStdinRequest)
conflict.Data = []byte("different")
if _, err := service.AppendStdin(context.Background(), conflict); status.Code(err) != codes.AlreadyExists {
t.Fatalf("append conflict code = %v", status.Code(err))
}
closeID := fixedIssue(0xaa)
closed, err := service.CloseStdin(context.Background(), &rvboxv1.CloseStdinRequest{ClientId: "win-a", IssueUuid: issue.String(), RequestId: closeID.String()})
if err != nil || closed.GetWriteSeq() != 2 {
t.Fatalf("close stdin = %#v, %v", closed, err)
}
signalID := fixedIssue(0xab)
cancelled, err := service.SignalCommand(context.Background(), &rvboxv1.ControlSignalCommandRequest{ClientId: "win-a", IssueUuid: issue.String(), RequestId: signalID.String(), Signal: rvboxv1.SignalKind_SIGNAL_TERM})
if err != nil || cancelled.GetCommandRevision() != 2 {
t.Fatalf("queued cancellation = %#v, %v", cancelled, err)
}
replay, err := service.SignalCommand(context.Background(), &rvboxv1.ControlSignalCommandRequest{ClientId: "win-a", IssueUuid: issue.String(), RequestId: signalID.String(), Signal: rvboxv1.SignalKind_SIGNAL_TERM})
if err != nil || replay.GetCommandRevision() != cancelled.GetCommandRevision() {
t.Fatalf("cancel replay = %#v, %v", replay, err)
}
command, err := service.GetCommand(context.Background(), &rvboxv1.GetCommandRequest{ClientId: "win-a", IssueUuid: issue.String()})
if err != nil || command.GetCommand().GetLifecycle() != rvboxv1.CommandLifecycle_COMMAND_CANCELLED {
t.Fatalf("cancelled command = %#v, %v", command, err)
}
}
func TestStorageIncidentControlLifecycle_HP_CONTROL_14(t *testing.T) {
service, persistence := newTestService(t)
defer persistence.Close()
incidentID := fixedIssue(0xac)
if _, err := persistence.RecordIncident(context.Background(), store.IncidentInput{IncidentUUID: [16]byte(incidentID), DetectedAt: time.Now().UTC(), Kind: store.IncidentChecksumMismatch, Scope: store.IncidentScopeGlobal, ScopeKey: "segments", Summary: "checksum mismatch", Evidence: []byte("evidence"), AutomaticallyRepairable: true}); err != nil {
t.Fatal(err)
}
listed, err := service.ListStorageIncidents(context.Background(), &rvboxv1.ListStorageIncidentsRequest{PageSize: 1})
if err != nil || len(listed.GetIncidents()) != 1 || listed.GetIncidents()[0].GetState() != rvboxv1.StorageIncidentState_STORAGE_INCIDENT_STATE_OPEN {
t.Fatalf("incident list = %#v, %v", listed, err)
}
repaired, err := service.RepairStorageIncident(context.Background(), &rvboxv1.RepairStorageIncidentRequest{IncidentId: incidentID.String(), RequestId: fixedIssue(0xad).String()})
if err != nil || repaired.GetIncident().GetState() != rvboxv1.StorageIncidentState_STORAGE_INCIDENT_STATE_REPAIRED {
t.Fatalf("incident repair = %#v, %v", repaired, err)
}
if repeat, err := service.RepairStorageIncident(context.Background(), &rvboxv1.RepairStorageIncidentRequest{IncidentId: incidentID.String(), RequestId: fixedIssue(0xae).String()}); err != nil || repeat.GetIncident().GetState() != rvboxv1.StorageIncidentState_STORAGE_INCIDENT_STATE_REPAIRED {
t.Fatalf("second repair = %#v, %v", repeat, err)
}
resolved, err := service.ListStorageIncidents(context.Background(), &rvboxv1.ListStorageIncidentsRequest{IncludeResolved: true})
if err != nil || len(resolved.GetIncidents()) != 1 || resolved.GetIncidents()[0].GetResolvedAt() == nil {
t.Fatalf("resolved incident list = %#v, %v", resolved, err)
}
}
type testFollowStream struct {
ctx context.Context
responses []*rvboxv1.FollowCommandResponse
}
func (stream *testFollowStream) Send(response *rvboxv1.FollowCommandResponse) error {
stream.responses = append(stream.responses, response)
return nil
}
func (stream *testFollowStream) SetHeader(metadata.MD) error { return nil }
func (stream *testFollowStream) SendHeader(metadata.MD) error { return nil }
func (stream *testFollowStream) SetTrailer(metadata.MD) {}
func (stream *testFollowStream) Context() context.Context { return stream.ctx }
func (stream *testFollowStream) SendMsg(any) error { return nil }
func (stream *testFollowStream) RecvMsg(any) error { return nil }
func newTestService(t *testing.T) (*Service, *store.Store) {
t.Helper()
persistence, err := store.Open(context.Background(), store.Options{DataDir: filepath.Join(t.TempDir(), "state"), BusyTimeout: time.Second})