package main import ( "context" "errors" "reflect" "testing" "time" "github.com/DATA-DOG/go-sqlmock" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/stats" ) func TestDurableGPSBoundariesSurviveIncompleteRawAndRepeatedBackfill(t *testing.T) { loc := time.FixedZone("Asia/Shanghai", 8*3600) previous := time.Date(2026, 10, 20, 23, 59, 0, 0, loc) last := time.Date(2026, 10, 21, 23, 59, 0, 0, loc) old := stats.SourceMileageSample{VIN: "VIN", StatDate: "2026-10-21", Protocol: envelope.ProtocolJT808, SourceKey: "JT808:phone@PLATFORM:g7s", SourceEndpoint: "1.2.3.4:1", FirstEventTime: previous, FirstTotalKM: 100, LatestEventTime: last, LatestTotalKM: 120, DailyKM: 20, SampleCount: 200, QualityStatus: stats.QualityOK, QualityReason: stats.QualityReasonHistorical} partial := old partial.FirstEventTime, partial.FirstTotalKM = previous.Add(-24*time.Hour), 90 partial.LatestEventTime, partial.LatestTotalKM = last.Add(-time.Hour), 115 partial.SampleCount = 100 for i := 0; i < 3; i++ { got := mergeProtectedGPSCandidate(old, partial) if !reflect.DeepEqual(got, old) { t.Fatalf("run %d replaced durable boundaries or double-counted: got=%+v old=%+v", i, got, old) } old = got } newer := partial newer.FirstEventTime, newer.FirstTotalKM = previous, 100 newer.LatestEventTime, newer.LatestTotalKM = last.Add(time.Minute), 119 got := mergeProtectedGPSCandidate(old, newer) if got.LatestTotalKM != 119 || got.DailyKM != 19 { t.Fatalf("event ordering incorrectly replaced with maximum odometer: %+v", got) } } func TestBackfillCurrentDayFirstBoundaryRetainsEarliestEvent(t *testing.T) { day := time.Date(2026, 10, 21, 8, 0, 0, 0, time.FixedZone("Asia/Shanghai", 8*3600)) if !preferDurableGPSFirst(day, day.Add(time.Hour), "2026-10-21") { t.Fatal("earliest current-day event lost") } if preferDurableGPSFirst(day.Add(-48*time.Hour), day.Add(-24*time.Hour), "2026-10-21") { t.Fatal("older previous-day baseline won") } } func gpsLockedRows() *sqlmock.Rows { return sqlmock.NewRows([]string{"source_key", "source_ip", "endpoint", "phone", "device", "platform", "first_km", "last_km", "daily_km", "hydrogen_km", "count", "hydrogen_count", "first_time", "last_time", "active", "known", "status", "reason", "selected"}) } func TestGPSBackfillKeepsManualFinalAndRollsBackPartialWrites(t *testing.T) { for _, fail := range []bool{false, true} { db, mock, err := sqlmock.New() if err != nil { t.Fatal(err) } stamp := time.Date(2026, 10, 21, 23, 59, 0, 0, time.FixedZone("Asia/Shanghai", 8*3600)) agg := &metricAgg{VIN: "VIN", Date: "2026-10-21", Protocol: envelope.ProtocolJT808, SourceKey: "JT808:phone@PLATFORM:g7s", SourceEndpoint: "1.2.3.4:123", FirstKM: 100, LatestKM: 120, FirstEventTime: stamp.Add(-24 * time.Hour), LatestEventTime: stamp, Count: 5, QualityStatus: stats.QualityOK, QualityReason: stats.QualityReasonHistorical} mock.ExpectBegin() mock.ExpectQuery("(?s)SELECT source_key.*FOR UPDATE").WithArgs("VIN", "2026-10-21").WillReturnRows(gpsLockedRows().AddRow("manual-report", "manual-import", "", "", "", "G7s", nil, nil, 88, 0, 1, 0, nil, nil, false, false, "OK", "manual_gps_report", true)) mock.ExpectExec("INSERT INTO vehicle_data_source").WillReturnResult(sqlmock.NewResult(0, 1)) if fail { mock.ExpectExec("INSERT INTO vehicle_daily_mileage_source").WillReturnError(errors.New("write failed")) mock.ExpectRollback() } else { mock.ExpectExec("(?s)INSERT INTO vehicle_daily_mileage_source.*sample_count=VALUES\\(sample_count\\)").WillReturnResult(sqlmock.NewResult(0, 1)) // No clearing other candidates, source-count increment or final // projection is permitted over a selected manual report. mock.ExpectCommit() } n, err := writeAggregates(context.Background(), db, map[string]*metricAgg{"target": agg}, 500) if fail && (err == nil || n != 0) { t.Fatalf("failed target committed: n=%d err=%v", n, err) } if !fail && (err != nil || n != 1) { t.Fatalf("manual override write: n=%d err=%v", n, err) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } db.Close() } } func TestGPSNormalizationDoesNotRewriteSelectedManualEvidence(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatal(err) } defer db.Close() mock.ExpectBegin() mock.ExpectQuery("(?s)SELECT source_key.*FOR UPDATE").WithArgs("VIN", "2026-07-01").WillReturnRows(gpsLockedRows().AddRow("manual-report", "manual-import", "", "", "", "G7s", nil, nil, 88, 0, 1, 0, nil, nil, false, false, "OK", "manual_gps_report", true)) // Do not normalize/delete the selected manual source, lose its selection // flag, or reproject over the manual final result. mock.ExpectCommit() if err := normalizeProtectedGPSDay(context.Background(), db, "VIN", "2026-07-01"); err != nil { t.Fatal(err) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } }