package agent import ( "crypto/sha256" "encoding/hex" "errors" "os" "path/filepath" "testing" "time" agentpb "git.ipao.vip/rogee/go-sip/gen/agent" "github.com/google/uuid" "google.golang.org/protobuf/proto" ) func TestMockRecordingFailureFactSurvivesRestartWithoutAnotherPUT(t *testing.T) { root := t.TempDir() spool, err := NewSpool(root, nil) if err != nil { t.Fatal(err) } binding := &agentpb.ExecutionBinding{TenantId: "tenant-a", TenantKey: "tenant-a", ExecutionId: "execution-a"} asset := &agentpb.AssetDescriptor{AssetId: "recording-a"} requestID := uuid.NewString() if err := spool.ClaimUpload(UploadAttempt{ RequestID: requestID, UploadID: "upload-a", Identity: "identity-a", State: "attempted", Binding: binding, Asset: asset, }); err != nil { t.Fatal(err) } payload := []byte(`{"schema_version":"local-mock-recording-failure.v0.1","upload_id":"upload-a","recording_id":"recording-a","error_code":"upload_failed"}`) sum := sha256.Sum256(payload) fact := &agentpb.ExecutionFact{ FactId: uuid.NewString(), Binding: binding, Kind: agentpb.FactKind_FACT_KIND_RECORDING_PROGRESS, PayloadJson: payload, ContentSha256: hex.EncodeToString(sum[:]), ObservedAtUnixMs: time.Now().UnixMilli(), SourceBootId: uuid.NewString(), SourceSequence: 1, } if err := spool.RecordUploadFailureFact("upload-a", fact); err != nil { t.Fatal(err) } restarted, err := NewSpool(root, nil) if err != nil { t.Fatal(err) } pending, err := restarted.PendingUploadFailures() if err != nil || len(pending) != 1 || pending[0].FailureFact.GetFactId() != fact.FactId { t.Fatalf("failure fact lost across restart: pending=%+v err=%v", pending, err) } if err := restarted.RecordUploadFailureFact("upload-a", fact); err != nil { t.Fatalf("same fact must be idempotent: %v", err) } conflict := proto.Clone(fact).(*agentpb.ExecutionFact) conflict.FactId = uuid.NewString() if err := restarted.RecordUploadFailureFact("upload-a", conflict); err == nil { t.Fatal("a second failure identity replaced the original") } if err := restarted.ReserveUploadRetry("upload-a", uuid.NewString()); err == nil { t.Fatal("final failure still permitted an explicit new PUT") } if err := restarted.RecordUploadResult("upload-a", UploadResult{SizeBytes: 4, SHA256: "checksum", StatusCode: 200}); err == nil { t.Fatal("failed upload was replaced by a success") } if err := restarted.CompleteUploadFailureReport("upload-a", fact.FactId); err != nil { t.Fatal(err) } pending, err = restarted.PendingUploadFailures() if err != nil || len(pending) != 0 { t.Fatalf("acknowledged fact was redelivered: pending=%d err=%v", len(pending), err) } if err := restarted.CompleteUploadFailureReport("upload-a", fact.FactId); err != nil { t.Fatalf("same acknowledgement must be idempotent: %v", err) } if err := restarted.CompleteUploadFailureReport("upload-a", uuid.NewString()); err == nil { t.Fatal("unrelated acknowledgement closed failure fact") } persisted, err := restarted.LoadUploadAttempt("upload-a") if err != nil || persisted.State != "attempted" || !persisted.FailureDelivered { t.Fatalf("failed PUT attempt regressed: %+v err=%v", persisted, err) } data, err := os.ReadFile(filepath.Join(root, ".uploads", "upload-a", "state.json")) if err != nil { t.Fatal(err) } if len(data) == 0 || errors.Is(err, os.ErrNotExist) { t.Fatal("failure fact was not durably journaled") } }