package mock import ( "context" "path/filepath" "testing" "time" "tera-clone/services/tera-api/internal/domain" ) // Exercise the production retention switch at the storage boundary, not a UI // filter. Existing snapshots remain readable while new observations only append // transitions, including after the saved configuration is loaded again. func TestLinkChangesOnlyPreservesHistoryAndSurvivesReload(t *testing.T) { for _, journalMode := range []bool{false, true} { name := "jsonl" if journalMode { name = "journal" } t.Run(name, func(t *testing.T) { ctx := context.Background() dir := t.TempDir() options := StoreOptions{ConfigPath: filepath.Join(dir, "config.json")} if journalMode { options.LogJournal = &memoryJournal{} } else { options.LinkLogPath = filepath.Join(dir, "links.jsonl") } store, err := NewStoreWithOptions(fixedClock, options) if err != nil { t.Fatal(err) } defer store.Close() observation := domain.LinkStateObservation{ Kind: "serial", Resource: "CH01", Signal: "cr", State: "up", ObservedAt: fixedClock(), } first, err := store.RecordLinkState(ctx, observation) if err != nil || first == nil || first.EventType != "snapshot" { t.Fatalf("historical snapshot = %+v, %v", first, err) } before, err := store.LogRetention(ctx) if err != nil { t.Fatal(err) } disabled := false if _, err := store.UpdateLogRetention(ctx, domain.LogRetentionUpdate{AllPortStatusEnabled: &disabled}); err != nil { t.Fatal(err) } if _, err := store.SaveAndApplyConfig(ctx); err != nil { t.Fatal(err) } want := before want.AllPortStatusEnabled = false if after, err := store.LogRetention(ctx); err != nil || after != want { t.Fatalf("only snapshot setting should change: got %+v, err=%v", after, err) } for index, state := range []string{"up", "down", "down", "up", "up"} { observation.State = state observation.ObservedAt = fixedClock().Add(time.Duration(index+1) * time.Hour) event, err := store.RecordLinkState(ctx, observation) if err != nil { t.Fatal(err) } changed := index == 1 || index == 3 if !changed && event != nil { t.Fatalf("unchanged state stored after snapshot interval: %+v", event) } if changed && (event == nil || event.EventType != "transition" || event.PreviousState == event.State) { t.Fatalf("state change was not recorded: %+v", event) } } // An unseen, unavailable port establishes a baseline without logging. baseline := domain.LinkStateObservation{ Kind: "serial", Resource: "CH02", Signal: "unknown", State: "unknown", ObservedAt: fixedClock(), } if event, err := store.RecordLinkState(ctx, baseline); err != nil || event != nil { t.Fatalf("new baseline must not be stored: %+v, %v", event, err) } reloaded, err := NewStoreWithOptions(fixedClock, options) if err != nil { t.Fatal(err) } defer reloaded.Close() if after, err := reloaded.LogRetention(ctx); err != nil || after != want { t.Fatalf("saved retention not preserved: %+v, %v", after, err) } observation.ObservedAt = fixedClock().Add(6 * time.Hour) if event, err := reloaded.RecordLinkState(ctx, observation); err != nil || event != nil { t.Fatalf("reload must not resume same-state snapshots: %+v, %v", event, err) } for eventType, count := range map[string]int{"snapshot": 1, "transition": 2} { events, err := reloaded.LinkLogEvents(ctx, domain.LinkLogFilter{EventType: eventType}) if err != nil || len(events) != count { t.Fatalf("%s records = %d, want %d, err=%v", eventType, len(events), count, err) } } }) } }