package lifecycle import ( "fmt" "strings" "testing" "time" ) type stubEventBus struct { failUntil int attempts int txHash string } func (s *stubEventBus) Publish(event ModuleEvent) error { s.attempts++ if s.attempts <= s.failUntil { return fmt.Errorf("publish failure %d for tx %s", s.attempts, s.txHash) } return nil } func (s *stubEventBus) Subscribe(eventType EventType, handler EventHandler) error { return nil } func TestModuleRegistryPublishEventWithRetrySuccess(t *testing.T) { registry := NewModuleRegistry(RegistryConfig{ EventPublishRetries: 3, EventPublishDelay: time.Nanosecond, }) registry.logger = nil bus := &stubEventBus{failUntil: 2, txHash: "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"} registry.eventBus = bus err := registry.publishEventWithRetry(ModuleEvent{ModuleID: "module-A", Type: EventModuleStarted}, "publish failed") if err != nil { t.Fatalf("expected publish to eventually succeed, got %v", err) } if bus.attempts != 3 { t.Fatalf("expected 3 attempts, got %d", bus.attempts) } if err := registry.aggregatedErrors(); err != nil { t.Fatalf("expected no aggregated errors, got %v", err) } } func TestModuleRegistryPublishEventWithRetryFailure(t *testing.T) { registry := NewModuleRegistry(RegistryConfig{ EventPublishRetries: 2, EventPublishDelay: time.Nanosecond, }) registry.logger = nil txHash := "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" bus := &stubEventBus{failUntil: 3, txHash: txHash} registry.eventBus = bus err := registry.publishEventWithRetry(ModuleEvent{ModuleID: "module-B", Type: EventModuleStopped}, "publish failed") if err == nil { t.Fatal("expected publish to fail after retries") } if bus.attempts != 2 { t.Fatalf("expected 2 attempts, got %d", bus.attempts) } exported := registry.RegistryErrors() if len(exported) != 1 { t.Fatalf("expected 1 recorded registry error, got %d", len(exported)) } if exported[0] == nil { t.Fatal("expected recorded error to be non-nil") } if copyErrs := registry.RegistryErrors(); copyErrs[0] == nil { t.Fatal("copy of registry errors should preserve values") } if got := exported[0].Error(); !strings.Contains(got, txHash) { t.Fatalf("recorded registry error should include tx hash, got %q", got) } details := registry.RegistryErrorDetails() if len(details) != 1 { t.Fatalf("expected registry error details to include entry, got %d", len(details)) } if details[0].TxHash != txHash { t.Fatalf("expected registry error detail to track tx hash %s, got %s", txHash, details[0].TxHash) } agg := registry.aggregatedErrors() if agg == nil { t.Fatal("expected aggregated error to be returned") } if got := agg.Error(); !strings.Contains(got, "publish failed") || !strings.Contains(got, "publish failure 1") || !strings.Contains(got, txHash) { t.Fatalf("aggregated error should include failure details and tx hash, got %q", got) } }