Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/api/internal/openplatform/statistics_test.go
T

484 lines
20 KiB
Go

package openplatform
import (
"context"
"encoding/json"
"regexp"
"strings"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
)
func TestBuildHydrogenDailyStatsUsesDirectValidBoundaries(t *testing.T) {
base := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)
values := hydrogenActiveTestSegment("LTEST32960VIN0001", "source-a", base, 10.5, 9.5)
stats := BuildHydrogenDailyStats(values, "2026-07-01", 0.05, 20)
if len(stats) != 1 {
t.Fatalf("stats = %#v", stats)
}
stat := stats[0]
if stat.ConsumptionKg != 1 || stat.FirstMassKg != 10.5 || stat.LastMassKg != 9.5 {
t.Fatalf("stat = %#v", stat)
}
}
func TestBuildHydrogenDailyStatsMergesSequentialSources(t *testing.T) {
base := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)
values := hydrogenActiveTestSegment("LTEST32960VIN0001", "source-a", base, 10, 9.6)
for index := len(values) / 2; index < len(values); index++ {
values[index].Source = "source-b"
}
stats := BuildHydrogenDailyStats(values, "2026-07-01", 0.05, 5)
if len(stats) != 1 {
t.Fatalf("stats = %#v", stats)
}
if stats[0].Source != "source-a" || stats[0].ConsumptionKg != 0.4 || stats[0].SampleCount != 10 {
t.Fatalf("stat = %#v", stats[0])
}
}
func TestBuildHydrogenDailyStatsDeduplicatesSameEventAcrossSources(t *testing.T) {
base := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)
values := hydrogenActiveTestSegment("LTEST32960VIN0001", "source-a", base, 10, 9.6)
duplicates := append([]HydrogenObservation(nil), values...)
for index := range duplicates {
duplicates[index].Source = "source-b"
}
stats := BuildHydrogenDailyStats(append(values, duplicates...), "2026-07-01", 0.05, 5)
if len(stats) != 1 || stats[0].Source != "source-a" || stats[0].ConsumptionKg != 0.4 || stats[0].SampleCount != 10 {
t.Fatalf("stats = %#v", stats)
}
}
func TestBuildHydrogenDailyStatsOrderedMatchesUnorderedCalculation(t *testing.T) {
base := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)
values := hydrogenActiveTestSegment("LTEST32960VIN0001", "source-a", base, 10, 9.6)
duplicates := append([]HydrogenObservation(nil), values...)
for index := range duplicates {
duplicates[index].Source = "source-b"
}
ordered := make([]HydrogenObservation, 0, len(values)+len(duplicates))
for index := range values {
ordered = append(ordered, values[index], duplicates[index])
}
stats := BuildHydrogenDailyStatsOrdered(ordered, "2026-07-01", 0.05, 5)
if len(stats) != 1 || stats[0].ConsumptionKg != 0.4 || stats[0].SampleCount != 10 {
t.Fatalf("stats = %#v", stats)
}
}
func TestBuildHydrogenDailyStatsDoesNotReplaceBoundariesWithWholeDayMinimum(t *testing.T) {
base := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)
values := hydrogenActiveTestSegment("LTEST32960VIN0001", "source-a", base, 10, 9.8)
values = append(values[:5], append([]HydrogenObservation{{
VIN: "LTEST32960VIN0001", Source: "source-a", ObservedAt: base.Add(5 * time.Second),
MassKg: 9, NoiseKg: 0.05, RefuelThresholdKg: 1, FuelCellActive: true, FuelCellStateKnown: true,
SOCPercent: 50, SOCKnown: true, MileageKm: 10, MileageKnown: true,
VehicleState: 1, VehicleStateKnown: true, ChargeState: 3, ChargeStateKnown: true,
RunningMode: 2, RunningModeKnown: true,
}}, values[5:]...)...)
for index := 6; index < len(values); index++ {
values[index].ObservedAt = base.Add(time.Duration(index) * time.Second)
}
stats := BuildHydrogenDailyStats(values, "2026-07-01", 0.05, 5)
if len(stats) != 1 || stats[0].ConsumptionKg != 0.2 {
t.Fatalf("stats = %#v", stats)
}
}
func TestHydrogenSegmentAccumulatorKeepsOnlyBoundedEndpointWindows(t *testing.T) {
base := time.Now()
accumulator := newHydrogenSegmentAccumulator(0.05, 20)
for index := 0; index < 10000; index++ {
accumulator.Add(HydrogenObservation{
ObservedAt: base.Add(time.Duration(index) * time.Second), MassKg: 20 - float64(index)/10000,
NoiseKg: 0.05, RefuelThresholdKg: 1, FuelCellActive: true, FuelCellStateKnown: true,
})
}
if len(accumulator.segmentFirst) != hydrogenSegmentEndpointWindow || len(accumulator.segmentTail) != hydrogenSegmentEndpointWindow {
t.Fatalf("first=%d tail=%d", len(accumulator.segmentFirst), len(accumulator.segmentTail))
}
consumption, segments, _, _ := accumulator.Finalize()
if consumption <= 0 || segments != 1 {
t.Fatalf("consumption=%v segments=%d", consumption, segments)
}
}
func TestBuildHydrogenDailyStatsRejectsInactivePressureFall(t *testing.T) {
base := time.Date(2026, 8, 15, 0, 0, 0, 0, time.UTC)
stats := BuildHydrogenDailyStats([]HydrogenObservation{
{VIN: "LTEST32960VIN0001", ObservedAt: base, MassKg: 10, PressureMPa: 28.2, VehicleState: 2, VehicleStateKnown: true},
{VIN: "LTEST32960VIN0001", ObservedAt: base.Add(time.Minute), MassKg: 8, PressureMPa: 22, VehicleState: 2, VehicleStateKnown: true},
{VIN: "LTEST32960VIN0001", ObservedAt: base.Add(2 * time.Minute), MassKg: 5, PressureMPa: 12.8, VehicleState: 2, VehicleStateKnown: true},
}, "2026-08-15", 0.05, 20)
if len(stats) != 1 || stats[0].ConsumptionKg != 0 || stats[0].QualityStatus != "NO_DATA" {
t.Fatalf("stats = %#v", stats)
}
}
func hydrogenActiveTestSegment(vin, source string, start time.Time, startMass, endMass float64) []HydrogenObservation {
values := make([]HydrogenObservation, 0, hydrogenSegmentEndpointWindow*2)
for index := 0; index < hydrogenSegmentEndpointWindow*2; index++ {
mass := startMass
if index >= hydrogenSegmentEndpointWindow {
mass = endMass
}
values = append(values, HydrogenObservation{
VIN: vin, Source: source, ObservedAt: start.Add(time.Duration(index) * time.Second),
MassKg: mass, PressureMPa: mass, NoiseKg: 0.05, RefuelThresholdKg: 1,
FuelCellActive: true, FuelCellStateKnown: true,
SOCPercent: 50, SOCKnown: true,
MileageKm: float64(index) * 2, MileageKnown: true,
VehicleState: 1, VehicleStateKnown: true,
ChargeState: 3, ChargeStateKnown: true,
RunningMode: 2, RunningModeKnown: true,
})
}
return values
}
func TestExtractHydrogenMassSupportsCanonicalStringAndNumber(t *testing.T) {
for _, encoded := range []string{
`{"gb32960.gd_fc_vehicle_info.hydrogen_mass_kg":"12.3"}`,
`{"gb32960.gd_fc_vehicle_info.hydrogen_mass_kg":12.3}`,
} {
value, ok := ExtractHydrogenMass(encoded)
if !ok || value != 12.3 {
t.Fatalf("value=%v ok=%v for %s", value, ok, encoded)
}
}
}
func TestPressureHydrogenMassMatchesGuangdongVehicle(t *testing.T) {
mass, ok := PressureHydrogenMassKg(16.4, 34, 380)
if !ok || mass < 4.47 || mass > 4.49 {
t.Fatalf("mass=%.3f ok=%v", mass, ok)
}
pressure, temperature, ok := ExtractHydrogenPressureTemperature(`{
"gb32960.fuel_cell.max_hydrogen_pressure_mpa":"16.4",
"gb32960.fuel_cell.max_hydrogen_temperature_c":34,
"gb32960.gd_fc_vehicle_info.hydrogen_mass_kg":99.9
}`)
if !ok || pressure != 16.4 || temperature != 34 {
t.Fatalf("pressure=%v temperature=%v ok=%v", pressure, temperature, ok)
}
}
func TestExtractHydrogenPressureTemperatureRejectsPlaceholders(t *testing.T) {
for _, encoded := range []string{
`{"gb32960.fuel_cell.max_hydrogen_pressure_mpa":0,"gb32960.fuel_cell.max_hydrogen_temperature_c":30}`,
`{"gb32960.fuel_cell.max_hydrogen_pressure_mpa":22.8,"gb32960.fuel_cell.max_hydrogen_temperature_c":-40}`,
} {
if _, _, ok := ExtractHydrogenPressureTemperature(encoded); ok {
t.Fatalf("pressure/temperature placeholder must be rejected: %s", encoded)
}
}
}
func TestExtractHydrogenFuelCellState(t *testing.T) {
for _, test := range []struct {
encoded string
active bool
known bool
}{
{`{"gb32960.gd_fc_stack.engine_work_state":2}`, true, true},
{`{"gb32960.gd_fc_stack.engine_work_state":0}`, false, true},
{`{"gb32960.gd_fc_stack.engine_work_state":1}`, false, false},
{`{"gb32960.gd_fc_stack.engine_work_state":1,"gb32960.fuel_cell.fuel_cell_current_a":94.1}`, true, true},
{`{"gb32960.gd_fc_stack.engine_work_state":2,"gb32960.fuel_cell.fuel_cell_current_a":0}`, false, true},
{`{"gb32960.fuel_cell.fuel_cell_current_a":0.2}`, false, true},
{`{}`, false, false},
} {
active, known := ExtractHydrogenFuelCellState(test.encoded)
if active != test.active || known != test.known {
t.Fatalf("encoded=%s active=%v known=%v", test.encoded, active, known)
}
}
}
func TestExtractHydrogenTelemetryReadsPressureTemperatureAndStateTogether(t *testing.T) {
pressure, temperature, active, known, ok := ExtractHydrogenTelemetry(`{
"gb32960.fuel_cell.max_hydrogen_pressure_mpa":28.2,
"gb32960.fuel_cell.max_hydrogen_temperature_c":35,
"gb32960.gd_fc_stack.engine_work_state":2
}`)
if !ok || pressure != 28.2 || temperature != 35 || !active || !known {
t.Fatalf("pressure=%v temperature=%v active=%v known=%v ok=%v", pressure, temperature, active, known, ok)
}
}
func TestExtractHydrogenTelemetryFastMatchesJSONDecoder(t *testing.T) {
for _, encoded := range []string{
`{"gb32960.fuel_cell.max_hydrogen_pressure_mpa":28.2,"gb32960.fuel_cell.max_hydrogen_temperature_c":35,"gb32960.gd_fc_stack.engine_work_state":2}`,
`{ "gb32960.fuel_cell.fuel_cell_current_a" : "0.5", "gb32960.fuel_cell.max_hydrogen_temperature_c" : "35", "gb32960.fuel_cell.max_hydrogen_pressure_mpa" : "28.2" }`,
`{"unrelated":"gb32960.fuel_cell.max_hydrogen_pressure_mpa","gb32960.fuel_cell.max_hydrogen_pressure_mpa":28.2,"gb32960.fuel_cell.max_hydrogen_temperature_c":35}`,
} {
wantPressure, wantTemperature, wantActive, wantKnown, wantOK := ExtractHydrogenTelemetry(encoded)
pressure, temperature, active, known, ok := extractHydrogenTelemetryFast(encoded)
if pressure != wantPressure || temperature != wantTemperature || active != wantActive || known != wantKnown || ok != wantOK {
t.Fatalf("fast=(%v,%v,%v,%v,%v) decoded=(%v,%v,%v,%v,%v) input=%s",
pressure, temperature, active, known, ok,
wantPressure, wantTemperature, wantActive, wantKnown, wantOK, encoded)
}
}
}
func TestHydrogenObservationQueryUsesEventTimeAndScopedVIN(t *testing.T) {
loc := time.FixedZone("Asia/Shanghai", 8*3600)
start := time.Date(2026, 8, 12, 0, 0, 0, 0, loc)
query, err := buildHydrogenObservationQuery("lingniu_vehicle_ts", start, start.AddDate(0, 0, 1), "LB9A32A24R0LS1376")
if err != nil {
t.Fatal(err)
}
for _, want := range []string{
"CAST(event_time AS BIGINT)",
"ts>='2026-08-12T00:00:00+08:00'",
"ts<'2026-08-13T00:00:00+08:00'",
"event_time>='2026-08-12T00:00:00+08:00'",
"event_time<'2026-08-13T00:00:00+08:00'",
"vin='LB9A32A24R0LS1376'",
"protocol='GB32960'",
"ORDER BY vin,event_time,source_endpoint,ts",
} {
if !strings.Contains(query, want) {
t.Fatalf("query missing %q:\n%s", want, query)
}
}
if strings.Contains(query, "CAST(ts AS BIGINT)") {
t.Fatalf("received time must not become observation time:\n%s", query)
}
if strings.Contains(query, "YUTONG_MQTT") || strings.Contains(query, "JT808") {
t.Fatalf("hydrogen query must only use GB32960 frames:\n%s", query)
}
if strings.Contains(query, "2026-08-05") || strings.Contains(query, "2026-08-20") {
t.Fatalf("received-time window must not include delayed frames:\n%s", query)
}
}
func TestHydrogenObservationVINQueryUsesSameDayGB32960Watermark(t *testing.T) {
loc := time.FixedZone("Asia/Shanghai", 8*3600)
start := time.Date(2026, 8, 12, 0, 0, 0, 0, loc)
query, err := buildHydrogenObservationVINQuery("lingniu_vehicle_ts", start, start.AddDate(0, 0, 1))
if err != nil {
t.Fatal(err)
}
for _, want := range []string{
"SELECT DISTINCT vin", "protocol='GB32960'",
"ts>='2026-08-12T00:00:00+08:00'", "ts<'2026-08-13T00:00:00+08:00'",
"event_time>='2026-08-12T00:00:00+08:00'", "event_time<'2026-08-13T00:00:00+08:00'",
"ORDER BY vin",
} {
if !strings.Contains(query, want) {
t.Fatalf("query missing %q:\n%s", want, query)
}
}
if strings.Contains(query, "YUTONG_MQTT") || strings.Contains(query, "JT808") {
t.Fatalf("VIN query must only use GB32960:\n%s", query)
}
}
func TestLoadHydrogenObservationVINsFiltersMissingCapacity(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
defer db.Close()
mock.ExpectQuery("SELECT DISTINCT vin").WillReturnRows(sqlmock.NewRows([]string{"vin"}).
AddRow("lb9a32a25r0ls1452").
AddRow("LB9A32A24R0LS1376"))
loc := time.FixedZone("Asia/Shanghai", 8*3600)
start := time.Date(2026, 8, 12, 0, 0, 0, 0, loc)
vins, err := LoadHydrogenObservationVINs(context.Background(), db, "lingniu_vehicle_ts", start, start.AddDate(0, 0, 1), map[string]float64{
"LB9A32A25R0LS1452": 520,
})
if err != nil || len(vins) != 1 || vins[0] != "LB9A32A25R0LS1452" {
t.Fatalf("vins=%v err=%v", vins, err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestLoadHydrogenBrandVINsUsesExactBrandAndValidVINs(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
defer db.Close()
mock.ExpectQuery(regexp.QuoteMeta("SELECT UPPER(TRIM(vin))")).
WithArgs("现代").
WillReturnRows(sqlmock.NewRows([]string{"vin"}).
AddRow("lb9a32a25r0ls1452").
AddRow("invalid"))
vins, err := LoadHydrogenBrandVINs(context.Background(), db, " 现代 ")
if err != nil || len(vins) != 1 || vins[0] != "LB9A32A25R0LS1452" {
t.Fatalf("vins=%v err=%v", vins, err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestHydrogenObservationDateRangeForAllUsesBoundedQueries(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
defer db.Close()
first := time.Date(2026, 6, 21, 8, 0, 0, 0, time.FixedZone("Asia/Shanghai", 8*3600))
last := time.Date(2026, 8, 18, 9, 0, 0, 0, time.FixedZone("Asia/Shanghai", 8*3600))
mock.ExpectQuery("ORDER BY event_time ASC LIMIT 1").
WillReturnRows(sqlmock.NewRows([]string{"event_time"}).AddRow(first))
mock.ExpectQuery("ORDER BY event_time DESC LIMIT 1").
WillReturnRows(sqlmock.NewRows([]string{"event_time"}).AddRow(last))
gotFirst, gotLast, found, err := HydrogenObservationDateRangeForAll(context.Background(), db, "lingniu_vehicle_ts")
if err != nil || !found || !gotFirst.Equal(first) || !gotLast.Equal(last) {
t.Fatalf("range first=%v last=%v found=%v err=%v", gotFirst, gotLast, found, err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestReplaceHydrogenDailyStatsForVINDeletesOnlyScopedVehicle(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
defer db.Close()
vin := "LB9A32A24R0LS1376"
mock.ExpectBegin()
mock.ExpectExec(regexp.QuoteMeta("DELETE FROM vehicle_open_daily_energy WHERE stat_date=? AND energy_type='HYDROGEN' AND vin=?")).
WithArgs("2026-08-12", vin).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(regexp.QuoteMeta("INSERT INTO vehicle_open_daily_energy(")).
WithArgs(vin, "2026-08-12", "source-a", 8.691, 7.64, 10.515, 2803, 2, "OK", "",
8.691, sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), 0.0, 0.0,
sqlmock.AnyArg(), sqlmock.AnyArg(), 0, 0, 0, "", sqlmock.AnyArg(), sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
err = ReplaceHydrogenDailyStatsForVIN(context.Background(), db, "2026-08-12", vin, []HydrogenDailyStat{{
VIN: vin, Date: "2026-08-12", Source: "source-a", ConsumptionKg: 8.691,
FirstMassKg: 7.64, LastMassKg: 10.515, SampleCount: 2803, RefuelCount: 2, QualityStatus: "OK",
}})
if err != nil {
t.Fatal(err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestReplaceHydrogenDailyStatsForVINsDeletesOnlyScopedBrandVehicles(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
defer db.Close()
vins := []string{"LB9A32A24R0LS1376", "LB9A32A25R0LS1452"}
stat := HydrogenDailyStat{
VIN: vins[0], Date: "2026-08-12", Source: "source-a", ConsumptionKg: 2.5,
FirstMassKg: 12, LastMassKg: 9.5, SampleCount: 500, QualityStatus: "OK",
}
mock.ExpectBegin()
mock.ExpectExec(regexp.QuoteMeta("DELETE FROM vehicle_open_daily_energy WHERE stat_date=? AND energy_type='HYDROGEN' AND vin IN (?,?)")).
WithArgs(stat.Date, vins[0], vins[1]).
WillReturnResult(sqlmock.NewResult(0, 2))
mock.ExpectExec(regexp.QuoteMeta("INSERT INTO vehicle_open_daily_energy(")).
WithArgs(stat.VIN, stat.Date, stat.Source, stat.ConsumptionKg, stat.FirstMassKg, stat.LastMassKg,
stat.SampleCount, 0, stat.QualityStatus, "", stat.ConsumptionKg,
sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), 0.0, 0.0,
sqlmock.AnyArg(), sqlmock.AnyArg(), 0, 0, 0, "", sqlmock.AnyArg(), sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
if err := ReplaceHydrogenDailyStatsForVINs(context.Background(), db, stat.Date, vins, []HydrogenDailyStat{stat}); err != nil {
t.Fatal(err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestReplaceHydrogenDailyStatsAndSeedStreamIsAtomic(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
defer db.Close()
vin := "LB9A32A24R0LS1376"
loc := time.FixedZone("Asia/Shanghai", 8*3600)
first := HydrogenObservation{VIN: vin, Source: "source-a", ObservedAt: time.Date(2026, 8, 19, 8, 0, 0, 0, loc), MassKg: 12, TankCapacityLiter: 520, PressureMPa: 30, TemperatureC: 31, FuelCellActive: true, FuelCellStateKnown: true}
last := HydrogenObservation{VIN: vin, Source: "source-b", ObservedAt: time.Date(2026, 8, 19, 11, 0, 0, 0, loc), MassKg: 9.4, TankCapacityLiter: 520, PressureMPa: 22, TemperatureC: 35, FuelCellActive: true, FuelCellStateKnown: true}
stat := HydrogenDailyStat{
VIN: vin, Date: "2026-08-19", Source: "source-a", ConsumptionKg: 2.6,
FirstMassKg: 12, LastMassKg: 9.4, CycleMinimumMassKg: 9.4,
SampleCount: 500, RefuelCount: 1, EligibleIntervalCount: 460, QualifiedSegmentCount: 2,
FirstObservation: first, LastObservation: last, QualityStatus: "OK",
}
mock.ExpectBegin()
mock.ExpectExec(regexp.QuoteMeta("DELETE FROM vehicle_open_daily_energy WHERE stat_date=? AND energy_type='HYDROGEN'")).
WithArgs("2026-08-19").WillReturnResult(sqlmock.NewResult(0, 3))
mock.ExpectExec(regexp.QuoteMeta("DELETE FROM vehicle_open_hydrogen_segment_stream_state WHERE stat_date=?")).
WithArgs("2026-08-19").WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectExec(regexp.QuoteMeta("INSERT INTO vehicle_open_daily_energy(")).
WithArgs(vin, "2026-08-19", "source-a", 2.6, 12.0, 9.4, 500, 1, "OK", "",
2.6, sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), 0.0, 0.0,
sqlmock.AnyArg(), sqlmock.AnyArg(), 0, 2, 0, "", sqlmock.AnyArg(), sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec(regexp.QuoteMeta("INSERT INTO vehicle_open_hydrogen_segment_stream_state(")).
WithArgs(vin, "2026-08-19", "source-a", 2.6, 2.6, 500, 1, 0, 460, 2, 9.4, last.ObservedAt, "", sqlmock.AnyArg(), "OK", "").
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
if err := ReplaceHydrogenDailyStatsAndSeedStream(context.Background(), db, "2026-08-19", []HydrogenDailyStat{stat}); err != nil {
t.Fatal(err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
encoded, err := hydrogenSegmentStreamSeedJSON(stat)
if err != nil {
t.Fatal(err)
}
var state map[string]any
if err := json.Unmarshal(encoded, &state); err != nil {
t.Fatal(err)
}
if state["lastMassKg"] != 9.4 || state["finalizedHydrogenKg"] != 2.6 || state["lastEventTime"] == "" || state["algorithmVersion"] != trustedHydrogenAlgorithmVersion {
t.Fatalf("seed state=%s", encoded)
}
}
func TestReplaceHydrogenDailyStatsAndSeedStreamSkipsMissingWatermark(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
defer db.Close()
stat := HydrogenDailyStat{
VIN: "LB9A32A24R0LS1376", Date: "2026-09-04", Source: "source-a",
QualityStatus: "NO_DATA", QualityReason: "有效运行分界点不足2条",
}
mock.ExpectBegin()
mock.ExpectExec(regexp.QuoteMeta("DELETE FROM vehicle_open_daily_energy WHERE stat_date=? AND energy_type='HYDROGEN'")).
WithArgs("2026-09-04").WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(regexp.QuoteMeta("DELETE FROM vehicle_open_hydrogen_segment_stream_state WHERE stat_date=?")).
WithArgs("2026-09-04").WillReturnResult(sqlmock.NewResult(0, 0))
mock.ExpectExec(regexp.QuoteMeta("INSERT INTO vehicle_open_daily_energy(")).
WithArgs(stat.VIN, stat.Date, stat.Source, 0.0, 0.0, 0.0, 0, 0, stat.QualityStatus, stat.QualityReason,
0.0, sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), 0.0, 0.0,
sqlmock.AnyArg(), sqlmock.AnyArg(), 0, 0, 0, "", sqlmock.AnyArg(), sqlmock.AnyArg()).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
if err := ReplaceHydrogenDailyStatsAndSeedStream(context.Background(), db, stat.Date, []HydrogenDailyStat{stat}); err != nil {
t.Fatal(err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}