package logruntime import ( "context" "errors" "os" "path/filepath" "testing" "time" "tera-clone/services/tera-api/internal/domain" "tera-clone/services/tera-api/internal/serial" ) type fakeBackend struct { policy domain.LogRetentionSettings keepalives []domain.SystemLogEvent links []domain.LinkStateObservation } func (b *fakeBackend) LogRetention(context.Context) (domain.LogRetentionSettings, error) { return b.policy, nil } func (b *fakeBackend) RecordSystemLog(_ context.Context, event domain.SystemLogEvent) (domain.SystemLogEvent, error) { b.keepalives = append(b.keepalives, event) return event, nil } func (b *fakeBackend) RecordLinkState(_ context.Context, event domain.LinkStateObservation) (*domain.LinkLogEvent, error) { b.links = append(b.links, event) return nil, nil } type fakeObserver struct{ values []domain.LinkStateObservation } type partialObserver struct{ value domain.LinkStateObservation } func (o partialObserver) Observe(context.Context, time.Time) ([]domain.LinkStateObservation, error) { return []domain.LinkStateObservation{o.value}, errors.New("one source unavailable") } type fakeCRProbeController struct{ values []serial.CRProbeState } func (c fakeCRProbeController) CRProbeStates(context.Context) ([]serial.CRProbeState, error) { return append([]serial.CRProbeState(nil), c.values...), nil } func (o fakeObserver) Observe(_ context.Context, now time.Time) ([]domain.LinkStateObservation, error) { result := append([]domain.LinkStateObservation(nil), o.values...) for index := range result { result[index].ObservedAt = now } return result, nil } func TestSchedulerRunsConfiguredKeepaliveAndLinkCadences(t *testing.T) { backend := &fakeBackend{policy: domain.LogRetentionSettings{ KeepaliveEnabled: true, KeepaliveIntervalMinutes: 3, SinglePortStatusEnabled: true, SinglePortStatusIntervalMinutes: 1, AllPortStatusEnabled: true, AllPortStatusIntervalMinutes: 30, }} scheduler, err := NewScheduler(Options{Backend: backend, Observer: fakeObserver{values: []domain.LinkStateObservation{{Kind: "ethernet", Resource: "Interface-1", State: "up", Signal: "carrier"}}}}) if err != nil { t.Fatal(err) } start := time.Date(2026, 7, 29, 0, 0, 0, 0, time.UTC) scheduler.step(context.Background(), start) scheduler.step(context.Background(), start.Add(30*time.Second)) scheduler.step(context.Background(), start.Add(time.Minute)) scheduler.step(context.Background(), start.Add(3*time.Minute)) if len(backend.keepalives) != 2 || backend.keepalives[0].Category != "status" { t.Fatalf("keepalives = %+v", backend.keepalives) } if len(backend.links) != 3 { t.Fatalf("link observations = %d, want 3", len(backend.links)) } } func TestSchedulerUsesAllPortIntervalWhenSinglePortDisabled(t *testing.T) { backend := &fakeBackend{policy: domain.LogRetentionSettings{AllPortStatusEnabled: true, AllPortStatusIntervalMinutes: 30}} scheduler, _ := NewScheduler(Options{Backend: backend, Observer: fakeObserver{values: []domain.LinkStateObservation{{Kind: "serial", Resource: "CH01", State: "unknown", Signal: "unknown"}}}}) start := time.Date(2026, 7, 29, 0, 0, 0, 0, time.UTC) scheduler.step(context.Background(), start) scheduler.step(context.Background(), start.Add(29*time.Minute)) scheduler.step(context.Background(), start.Add(30*time.Minute)) if len(backend.links) != 2 { t.Fatalf("link observations = %d, want 2", len(backend.links)) } } func TestSchedulerContinuesObservationAndKeepaliveWithoutSnapshots(t *testing.T) { backend := &fakeBackend{policy: domain.LogRetentionSettings{ AllPortStatusEnabled: false, SinglePortStatusEnabled: true, SinglePortStatusIntervalMinutes: 1, KeepaliveEnabled: true, KeepaliveIntervalMinutes: 3, }} scheduler, err := NewScheduler(Options{Backend: backend, Observer: fakeObserver{values: []domain.LinkStateObservation{{ Kind: "ethernet", Resource: "Interface-1", State: "up", Signal: "carrier", }}}}) if err != nil { t.Fatal(err) } for minute := 0; minute <= 3; minute++ { scheduler.step(context.Background(), time.Date(2026, 9, 7, 0, minute, 0, 0, time.UTC)) } if len(backend.links) != 4 || len(backend.keepalives) != 2 { t.Fatalf("disabling snapshots stopped observation or keepalive: links=%d keepalives=%d", len(backend.links), len(backend.keepalives)) } } func TestEthernetObserverReadsCarrierWithoutGuessingMissingState(t *testing.T) { root := t.TempDir() for device, value := range map[string]string{"eth0": "1\n", "eth1": "0\n"} { path := filepath.Join(root, device) if err := os.Mkdir(path, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(path, "carrier"), []byte(value), 0o644); err != nil { t.Fatal(err) } } observer := EthernetObserver{SysfsRoot: root, Interfaces: map[string]string{"Interface-1": "eth0", "Interface-2": "eth1", "Interface-3": "missing"}} values, err := observer.Observe(context.Background(), time.Now()) if err != nil || len(values) != 3 { t.Fatalf("Observe = %+v, %v", values, err) } if values[0].State != "up" || values[1].State != "down" || values[2].State != "unknown" || values[2].Signal != "unknown" { t.Fatalf("carrier values = %+v", values) } } func TestParseEthernetInterfacesIsBoundedToDeviceNames(t *testing.T) { interfaces, err := ParseEthernetInterfaces("Interface-1=eth0,Interface-2=eth1") if err != nil || interfaces["Interface-2"] != "eth1" { t.Fatalf("ParseEthernetInterfaces = %+v, %v", interfaces, err) } if _, err := ParseEthernetInterfaces("Interface-1=../../secret"); err == nil { t.Fatal("ParseEthernetInterfaces accepted a path") } } func TestSerialObserverUsesCRProbeForUnifiedConnectionTransitions(t *testing.T) { now := time.Date(2026, 8, 22, 0, 0, 0, 0, time.UTC) observer := SerialObserver{Probes: fakeCRProbeController{values: []serial.CRProbeState{{ Channel: "CH06", Enabled: true, State: serial.CRProbeStateResponded, Up: true, EverUp: true, }}}} values, err := observer.Observe(context.Background(), now) if err != nil || len(values) != 1 || values[0].State != "up" || values[0].Signal != "cr" { t.Fatalf("CR up observations = %+v, %v", values, err) } observer.Probes = fakeCRProbeController{values: []serial.CRProbeState{{ Channel: "CH06", Enabled: true, State: serial.CRProbeStateNoResponse, Up: false, EverUp: true, }}} values, err = observer.Observe(context.Background(), now.Add(time.Second)) if err != nil || len(values) != 1 || values[0].State != "down" || values[0].Signal != "cr" { t.Fatalf("CR no response after UP must be DOWN = %+v, %v", values, err) } observer.Probes = fakeCRProbeController{values: []serial.CRProbeState{{ Channel: "CH06", Enabled: true, EverUp: true, LastResponseAt: &now, State: serial.CRProbeStateNoResponse, }}} values, err = observer.Observe(context.Background(), now.Add(4*time.Second)) if err != nil || len(values) != 1 || values[0].State != "up" { t.Fatalf("transient CR miss must use shared grace period = %+v, %v", values, err) } } func TestCompositeObserverKeepsPartialObservations(t *testing.T) { observer := CompositeObserver{partialObserver{value: domain.LinkStateObservation{ Kind: "serial", Resource: "CH06", State: "up", Signal: "cr", }}} values, err := observer.Observe(context.Background(), time.Now()) if err == nil || len(values) != 1 || values[0].Resource != "CH06" { t.Fatalf("partial observations = %+v, %v", values, err) } }