Files
orbit/pulse/service/recovery_test.go

77 lines
3.4 KiB
Go

package service
import (
"context"
"encoding/json"
"strings"
"testing"
"time"
"git.yuntuo.hk/k2safe/orbit/internal/fault"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/platform/auth"
pulse "git.yuntuo.hk/k2safe/orbit/pulse/model"
)
type recoveryValidationStore struct {
Store
reviews, retries int
retry ManualRetryInput
}
func (s *recoveryValidationStore) ReviewTask(context.Context, auth.Token, core.ID, string, ReviewInput) (ReviewResult, error) {
s.reviews++
return ReviewResult{}, nil
}
func (s *recoveryValidationStore) RetryTask(_ context.Context, _ auth.Token, _ core.ID, _ string, in ManualRetryInput) (ManualRetryResult, error) {
s.retries++
s.retry = in
return ManualRetryResult{}, nil
}
func TestRecoveryRequiresExplicitEvidenceAndConfirmation(t *testing.T) {
id := core.ID(strings.Repeat("1", 32))
s := &recoveryValidationStore{}
api := New(s)
base := ReviewInput{ExpectedRevision: 7, Reason: "verified externally", EvidenceRef: "evidence:record-1", ExecutionSettled: true, Outcome: pulse.TaskFailed, Result: json.RawMessage(`{"n":9007199254740993}`)}
for name, change := range map[string]func(*ReviewInput){
"no revision": func(v *ReviewInput) { v.ExpectedRevision = 0 },
"blank reason": func(v *ReviewInput) { v.Reason = " " },
"large reason": func(v *ReviewInput) { v.Reason = strings.Repeat("x", 1025) },
"empty evidence": func(v *ReviewInput) { v.EvidenceRef = "" },
"secret whitespace": func(v *ReviewInput) { v.EvidenceRef = "receipt:\nvalue" },
"too long evidence": func(v *ReviewInput) { v.EvidenceRef = strings.Repeat("x", 513) },
"unsettled": func(v *ReviewInput) { v.ExecutionSettled = false },
"reopen": func(v *ReviewInput) { v.Outcome = pulse.TaskWaiting },
"cancel": func(v *ReviewInput) { v.Outcome = pulse.TaskCancelled },
"null": func(v *ReviewInput) { v.Result = json.RawMessage(`null`) },
"duplicate fields": func(v *ReviewInput) { v.Result = json.RawMessage(`{"n":1,"n":2}`) },
"large result": func(v *ReviewInput) { v.Result = json.RawMessage(`"` + strings.Repeat("a", 256<<10) + `"`) },
} {
t.Run(name, func(t *testing.T) {
in := base
change(&in)
if _, err := api.ReviewTask(context.Background(), auth.Token{}, id, "review", in); fault.Code(err) != "INVALID_ARGUMENT" {
t.Fatal(err)
}
})
}
if s.reviews != 0 {
t.Fatal("invalid review reached persistence")
}
if _, err := api.ReviewTask(context.Background(), auth.Token{}, id, "review", base); err != nil || s.reviews != 1 {
t.Fatal(err)
}
in := ManualRetryInput{ExpectedRevision: 7, Reason: "verified retry", EvidenceRef: "evidence:retry", SafeToRetry: true, TaskTimeoutMS: 30000}
for _, bad := range []ManualRetryInput{{ExpectedRevision: 7, Reason: in.Reason, EvidenceRef: in.EvidenceRef, TaskTimeoutMS: 30000}, {ExpectedRevision: 7, Reason: in.Reason, EvidenceRef: in.EvidenceRef, SafeToRetry: true}, {ExpectedRevision: 7, Reason: in.Reason, EvidenceRef: in.EvidenceRef, SafeToRetry: true, TaskTimeoutMS: 30*86400000 + 1}} {
if _, err := api.RetryTask(context.Background(), auth.Token{}, id, "retry", bad); fault.Code(err) != "INVALID_ARGUMENT" {
t.Fatal(err)
}
}
tm := time.Date(2026, 9, 17, 12, 0, 0, 0, time.FixedZone("example", 8*60*60))
in.AvailableAt = &tm
if _, err := api.RetryTask(context.Background(), auth.Token{}, id, "retry", in); err != nil || s.retries != 1 || s.retry.AvailableAt.Location() != time.UTC {
t.Fatal(err)
}
}