package m7gate import ( "encoding/json" "os" "path/filepath" "testing" "time" "github.com/itworx/pulse/internal/host" "github.com/itworx/pulse/internal/network" "github.com/itworx/pulse/internal/probe" "github.com/itworx/pulse/internal/service" ) type outageFixture struct { ID string Timeline []struct { Action string Payload struct { Probe string ProbeGroup string Success bool } } ExpectedOutcomes []struct { Assertion string } } func TestDNSOutageFixtureProjectsOneUpstreamAndSuppressedDownstreamInputs(t *testing.T) { content, err := os.ReadFile(filepath.Join("..", "..", "fixtures", "scenarios", "dns-outage-suppression.json")) if err != nil { t.Fatal(err) } var fixture outageFixture if err := json.Unmarshal(content, &fixture); err != nil { t.Fatal(err) } if fixture.ID != "dns-outage-suppression" || len(fixture.Timeline) != 2 || len(fixture.ExpectedOutcomes) != 2 { t.Fatalf("unexpected DNS outage fixture: %+v", fixture) } if fixture.Timeline[0].Payload.Success || fixture.Timeline[0].Payload.Probe != "dns-primary" || fixture.Timeline[1].Payload.Success || fixture.Timeline[1].Payload.ProbeGroup != "dependent-http" { t.Fatalf("fixture does not describe the expected upstream/downstream outage: %+v", fixture.Timeline) } now := time.Date(2026, 8, 2, 12, 0, 0, 0, time.UTC) dns := service.ServiceStatus{ID: "dns-primary", State: service.StateDown, Probes: []service.ProbeConfig{{ID: "dns-probe", Type: probe.TypeDNS}}, History: []probe.Result{{ProbeID: "dns-probe", State: service.StateDown, ErrorClass: "timeout", ObservedAt: now}}} dependencies := make([]service.Dependency, 20) services := make([]service.ServiceStatus, 20) for index := range services { serviceID := "dependent-" + string(rune('a'+index)) services[index] = service.ServiceStatus{ID: serviceID, State: service.StateDown, Reason: "dns"} dependencies[index] = service.Dependency{ID: "dependency-" + serviceID, ServiceID: serviceID, DependsOnServiceID: dns.ID, RelationType: service.RelationDependsOn, SourceID: "scenario", Confidence: 1, Confirmed: true} } serviceSnapshot := service.Snapshot{ObservedAt: now, Services: append([]service.ServiceStatus{dns}, services...), Total: 21} networkSnapshot, err := network.BuildSnapshot(now, host.Snapshot{Source: host.Source{ID: "fixture-host", Freshness: host.Fresh, State: host.StatusHealthy, ObservedAt: now}, Status: host.Status{State: host.StatusHealthy}, ObservedAt: now}, serviceSnapshot, nil, nil, network.Limits{}) if err != nil { t.Fatal(err) } var dnsHealth network.Health for _, health := range networkSnapshot.Health { if health.Scope == network.ScopeDNS { dnsHealth = health } } if dnsHealth.State != network.StateDown || dnsHealth.Reason != "dns_probe_failed" { t.Fatalf("DNS outage was not projected independently: %+v", dnsHealth) } states := map[string]string{dns.ID: service.StateDown} for _, item := range services { states[item.ID] = item.State } inputs, err := service.BuildSuppressionInputs(dependencies, states) if err != nil { t.Fatal(err) } if len(inputs) != 20 { t.Fatalf("expected 20 downstream suppression inputs, got %d", len(inputs)) } for _, input := range inputs { if !input.Suppress || input.UpstreamState != service.StateDown || input.Reason != "confirmed_dependency_failure" { t.Fatalf("downstream failure was not grouped/suppressed by DNS dependency: %+v", input) } } }