package feichibridge import ( "context" "errors" "log/slog" "path/filepath" "sync" "testing" "time" ) type fakeSource struct { vehicles []Vehicle record Record } func (f *fakeSource) Vehicles(context.Context) ([]Vehicle, error) { return append([]Vehicle(nil), f.vehicles...), nil } func (f *fakeSource) Snapshot(context.Context, string) (Snapshot, error) { return Snapshot{DataItems: map[string]string(f.record)}, nil } func (f *fakeSource) History(context.Context, string, time.Time, time.Time) ([]Record, error) { return nil, nil } type fakeTarget struct { mu sync.Mutex frames [][]byte sendErr error lastACK time.Time } func (f *fakeTarget) Connect(context.Context) error { return nil } func (f *fakeTarget) Close() error { return nil } func (f *fakeTarget) LastACK() time.Time { return f.lastACK } func (f *fakeTarget) Send(_ context.Context, frame []byte) error { f.mu.Lock() defer f.mu.Unlock() if f.sendErr != nil { return f.sendErr } f.frames = append(f.frames, append([]byte(nil), frame...)) f.lastACK = time.Now() return nil } func TestRealtimeCommitsOnlyAfterACKAndDeduplicates(t *testing.T) { now := time.Now().In(shanghai).Truncate(time.Second) source := &fakeSource{ vehicles: []Vehicle{{ VehicleID: "id-1", VIN: "LTEST32960VIN0001", RuleTypeName: "GB_T32960", }}, record: Record{"2000": now.Format("2006-01-02 15:04:05"), "2201": "10"}, } target := &fakeTarget{} store, err := OpenStateStore(filepath.Join(t.TempDir(), "state.json")) if err != nil { t.Fatal(err) } service, err := NewService(ServiceConfig{ SourceStaleAfter: time.Minute, FetchConcurrency: 1, }, source, target, store, slog.Default(), nil) if err != nil { t.Fatal(err) } if err := service.discover(context.Background()); err != nil { t.Fatal(err) } service.pollRealtime(context.Background()) service.pollRealtime(context.Background()) if len(target.frames) != 1 { t.Fatalf("frames = %d, want 1", len(target.frames)) } if got := store.Vehicle("LTEST32960VIN0001"); !got.LastRealtimeTime.Equal(now) { t.Fatalf("committed state = %#v", got) } source.record = Record{"2000": now.Add(time.Second).Format("2006-01-02 15:04:05"), "2201": "11"} target.sendErr = errors.New("target down") service.pollRealtime(context.Background()) if got := store.Vehicle("LTEST32960VIN0001"); !got.LastRealtimeTime.Equal(now) { t.Fatalf("cursor advanced without ACK: %#v", got) } } func TestStaleSnapshotIsReissuedOnlyOnce(t *testing.T) { at := time.Now().In(shanghai).Add(-24 * time.Hour).Truncate(time.Second) vin := "LTEST32960VIN0002" source := &fakeSource{ vehicles: []Vehicle{{VehicleID: "id-2", VIN: vin, RuleTypeName: "GB_T32960"}}, record: Record{"2000": at.Format("2006-01-02 15:04:05"), "2201": "0"}, } target := &fakeTarget{} store, err := OpenStateStore(filepath.Join(t.TempDir(), "state.json")) if err != nil { t.Fatal(err) } service, err := NewService(ServiceConfig{ SourceStaleAfter: time.Minute, FetchConcurrency: 1, StaleReissueEnabled: true, }, source, target, store, slog.Default(), nil) if err != nil { t.Fatal(err) } if err := service.discover(context.Background()); err != nil { t.Fatal(err) } service.pollRealtime(context.Background()) service.pollRealtime(context.Background()) if len(target.frames) != 1 || target.frames[0][2] != CommandReissue { t.Fatalf("frames = %d command = %#v", len(target.frames), target.frames) } state := store.Vehicle(vin) if !state.LastSnapshotReissueTime.Equal(at) || state.LastSnapshotReissueACKAt.IsZero() { t.Fatalf("reissue state = %#v", state) } }