updates on the ai agents and time series prediction and updates on the api to

This commit is contained in:
2026-10-09 13:50:30 +05:30
parent 29690c56f2
commit 153be40e5c
42 changed files with 4889 additions and 186 deletions

View File

@@ -0,0 +1,284 @@
package prediction
import (
"testing"
"time"
"doormile/utils"
)
func ptr(n int) *int { return &n }
// A calibration that is deliberately coarse-to-fine, so the fallback ladder is
// observable: the zone_hour_weekday cell has a different factor from the
// zone_weekday one, which differs from zone, which differs from global.
func fixture() []ETACalibration {
return []ETACalibration{
{Scope: "zone_hour_weekday", Zone: "641", Hourbucket: ptr(3), Weekday: ptr(1), Factor: 1.5, Samples: 100},
{Scope: "zone_weekday", Zone: "641", Weekday: ptr(1), Factor: 2.0, Samples: 100},
{Scope: "zone", Zone: "641", Factor: 2.5, Samples: 100},
{Scope: "global", Factor: 3.0, Samples: 100},
}
}
// 2026-10-05 is a Monday. 10:00 IST is hour bucket 3 (10/3). The database
// stores IST digits labelled UTC, so the fixture time is built that way on
// purpose — it is the shape a real column read produces.
func mondayTenAM() time.Time {
return time.Date(2026, 10, 5, 10, 0, 0, 0, time.UTC)
}
// ─── The floor rule: these are the cases that must return false ────────────
func TestNoRoutedDurationFallsBack(t *testing.T) {
Load(fixture())
defer Reset()
// The dominant case in production today: ROUTE_OPTIMIZER_URL is unset, so
// internal/routing never runs and etaminutes is 0.
if _, ok := ETAMinutes(Input{RoutedMinutes: 0, DeliveryPincode: "641001", At: mondayTenAM()}); ok {
t.Fatal("ETAMinutes answered with no routed duration; the promise table must keep answering")
}
if _, ok := ETAMinutes(Input{RoutedMinutes: -5, DeliveryPincode: "641001", At: mondayTenAM()}); ok {
t.Fatal("ETAMinutes answered on a negative routed duration")
}
}
func TestNoCalibrationFallsBack(t *testing.T) {
Reset()
if _, ok := ETAMinutes(Input{RoutedMinutes: 40, DeliveryPincode: "641001", At: mondayTenAM()}); ok {
t.Fatal("ETAMinutes answered with no calibration loaded")
}
}
func TestBelowSampleFloorFallsBack(t *testing.T) {
Load([]ETACalibration{
{Scope: "zone", Zone: "641", Factor: 2.0, Samples: minSamples - 1},
})
defer Reset()
if _, ok := ETAMinutes(Input{RoutedMinutes: 40, DeliveryPincode: "641001", At: mondayTenAM()}); ok {
t.Fatalf("ETAMinutes trusted a cell with %d samples (floor is %d)", minSamples-1, minSamples)
}
}
func TestOutOfBoundsFactorFallsBack(t *testing.T) {
for _, f := range []float64{minFactor - 0.1, maxFactor + 0.1, 0, -1} {
Load([]ETACalibration{{Scope: "zone", Zone: "641", Factor: f, Samples: 100}})
if _, ok := ETAMinutes(Input{RoutedMinutes: 40, DeliveryPincode: "641001", At: mondayTenAM()}); ok {
t.Errorf("ETAMinutes served an out-of-bounds factor %v; an absurd ETA is worse than a flat constant", f)
}
Reset()
}
}
// This is also the H1 regression test, verified by mutation: Refreshedat is
// written through utils.DBNow (IST wall-clock digits labelled UTC), so reading
// it as a raw instant makes it appear 5h30m NEWER than it is. A calibration
// 73h old then measures as 67.5h and slips under the 72h staleAfter bound.
// Removing the utils.IST call in Load fails exactly this test.
//
// The margin matters: with staleAfter at 72h, the 5.5h error is a 7.6% window
// in which a stale calibration keeps answering. Narrow staleAfter and the bug
// gets proportionally worse, which is why the correction belongs in Load rather
// than in a wider bound here.
func TestStaleCalibrationFallsBack(t *testing.T) {
old := utils.DBNow().Add(-(staleAfter + time.Hour))
Load([]ETACalibration{
{Scope: "zone", Zone: "641", Factor: 2.0, Samples: 100, Refreshedat: old},
})
defer Reset()
if _, ok := ETAMinutes(Input{RoutedMinutes: 40, DeliveryPincode: "641001", At: mondayTenAM()}); ok {
t.Fatal("ETAMinutes trusted a calibration older than staleAfter; refreshes have been failing")
}
}
// The companion case: a just-refreshed calibration must answer. Note this one
// passes with or without the IST correction (the raw read errs toward "newer",
// not "older"), so it documents intent rather than guarding the bug — the guard
// above is what fails on mutation.
func TestFreshCalibrationIsNotMisreadAsStale(t *testing.T) {
Load([]ETACalibration{
{Scope: "zone", Zone: "641", Factor: 2.0, Samples: 100, Refreshedat: utils.DBNow()},
})
defer Reset()
if _, ok := ETAMinutes(Input{RoutedMinutes: 40, DeliveryPincode: "641001", At: mondayTenAM()}); !ok {
t.Fatal("a just-refreshed calibration was treated as stale — the DBNow/IST correction in Load is wrong")
}
}
// Regression: loading month-old rows from the store at boot must stay stale.
// Stamping builtAt with time.Now() here would re-arm a dead calibration on
// every deploy.
func TestBootLoadDoesNotRearmStaleRows(t *testing.T) {
old := utils.DBNow().Add(-(staleAfter + 24*time.Hour))
Load([]ETACalibration{
{Scope: "zone", Zone: "641", Factor: 2.0, Samples: 100, Refreshedat: old},
{Scope: "global", Factor: 2.2, Samples: 100, Refreshedat: old},
})
defer Reset()
if _, ok := ETAMinutes(Input{RoutedMinutes: 40, DeliveryPincode: "641001", At: mondayTenAM()}); ok {
t.Fatal("a restart re-armed a stale stored calibration")
}
}
// ─── The ladder: most specific cell wins, then progressively coarser ───────
func TestMostSpecificCellWins(t *testing.T) {
Load(fixture())
defer Reset()
r, ok := ETAMinutes(Input{RoutedMinutes: 40, DeliveryPincode: "641001", At: mondayTenAM()})
if !ok {
t.Fatal("expected an estimate")
}
if r.Source != "zone_hour_weekday" {
t.Errorf("source = %q, want zone_hour_weekday", r.Source)
}
if r.Minutes != 60 { // 40 × 1.5
t.Errorf("minutes = %d, want 60 (40 × 1.5)", r.Minutes)
}
}
func TestFallsToZoneWeekdayThenZoneThenGlobal(t *testing.T) {
full := fixture()
defer Reset()
// Drop the finest cell: 13:00 IST is bucket 4, for which there is no
// zone_hour_weekday row, so zone_weekday answers.
Load(full)
r, ok := ETAMinutes(Input{RoutedMinutes: 40, DeliveryPincode: "641001",
At: time.Date(2026, 10, 5, 13, 0, 0, 0, time.UTC)})
if !ok || r.Source != "zone_weekday" || r.Minutes != 80 { // 40 × 2.0
t.Errorf("got %+v ok=%v, want zone_weekday 80", r, ok)
}
// A Tuesday has no weekday cell for this zone, so zone answers.
r, ok = ETAMinutes(Input{RoutedMinutes: 40, DeliveryPincode: "641001",
At: time.Date(2026, 10, 6, 13, 0, 0, 0, time.UTC)})
if !ok || r.Source != "zone" || r.Minutes != 100 { // 40 × 2.5
t.Errorf("got %+v ok=%v, want zone 100", r, ok)
}
// An unknown zone falls all the way to global.
r, ok = ETAMinutes(Input{RoutedMinutes: 40, DeliveryPincode: "500081", At: mondayTenAM()})
if !ok || r.Source != "global" || r.Minutes != 120 { // 40 × 3.0
t.Errorf("got %+v ok=%v, want global 120", r, ok)
}
}
func TestShortOrEmptyPincodeUsesGlobalOnly(t *testing.T) {
Load(fixture())
defer Reset()
for _, p := range []string{"", "6", "64", " "} {
r, ok := ETAMinutes(Input{RoutedMinutes: 40, DeliveryPincode: p, At: mondayTenAM()})
if !ok || r.Source != "global" {
t.Errorf("pincode %q: got %+v ok=%v, want global", p, r, ok)
}
}
}
// ─── Bucket arithmetic must match the refresh SQL, or the lookup misses ────
func TestHourBucketMatchesRefreshSQL(t *testing.T) {
// refreshSQL does EXTRACT(HOUR FROM createdat)::int / hourBucketSize.
// hourBucket must agree exactly, or every finest-grain lookup misses.
for hour := 0; hour < 24; hour++ {
got := hourBucket(time.Date(2026, 10, 5, hour, 30, 0, 0, time.UTC))
if want := hour / hourBucketSize; got != want {
t.Errorf("hour %d: bucket = %d, want %d", hour, got, want)
}
}
}
func TestWeekdayMatchesPostgresISODOW(t *testing.T) {
// EXTRACT(ISODOW) is Monday=1 … Sunday=7. Go's time.Weekday is Sunday=0.
// 2026-10-05 is a Monday.
want := map[int]int{5: 1, 6: 2, 7: 3, 8: 4, 9: 5, 10: 6, 11: 7}
for day, w := range want {
got := weekdayOf(time.Date(2026, 10, day, 12, 0, 0, 0, time.UTC))
if got != w {
t.Errorf("2026-10-%02d: weekday = %d, want %d (ISODOW)", day, got, w)
}
}
}
func TestZoneIsPincodePrefix(t *testing.T) {
cases := map[string]string{
"641001": "641", "641 ": "641", "500081": "500",
"": "", "64": "", " ": "",
}
for in, want := range cases {
if got := zoneOf(in); got != want {
t.Errorf("zoneOf(%q) = %q, want %q", in, got, want)
}
}
}
// ─── ETAAt and handling term ───────────────────────────────────────────────
func TestETAAtAddsToTheGivenTime(t *testing.T) {
Load(fixture())
defer Reset()
from := mondayTenAM()
at, r, ok := ETAAt(Input{RoutedMinutes: 40, DeliveryPincode: "641001", At: from}, from)
if !ok {
t.Fatal("expected an estimate")
}
if want := from.Add(60 * time.Minute); !at.Equal(want) {
t.Errorf("ETAAt = %v, want %v", at, want)
}
// The returned time keeps the caller's location, so it round-trips into the
// database in the same shape it came out.
if at.Location() != from.Location() {
t.Errorf("ETAAt changed the location from %v to %v", from.Location(), at.Location())
}
if r.Minutes != 60 {
t.Errorf("result minutes = %d, want 60", r.Minutes)
}
}
func TestETAAtFallsBackWithoutCalibration(t *testing.T) {
Reset()
from := mondayTenAM()
if _, _, ok := ETAAt(Input{RoutedMinutes: 40, DeliveryPincode: "641001", At: from}, from); ok {
t.Fatal("ETAAt answered with no calibration")
}
}
func TestHandlingTermIsAddedAndBounded(t *testing.T) {
Load([]ETACalibration{
{Scope: "zone", Zone: "641", Factor: 2.0, Handling: 15, Samples: 100},
})
r, ok := ETAMinutes(Input{RoutedMinutes: 40, DeliveryPincode: "641001", At: mondayTenAM()})
if !ok || r.Minutes != 95 { // 40 × 2.0 + 15
t.Errorf("got %+v ok=%v, want 95", r, ok)
}
Reset()
Load([]ETACalibration{
{Scope: "zone", Zone: "641", Factor: 2.0, Handling: maxHandlingMinutes + 1, Samples: 100},
})
if _, ok := ETAMinutes(Input{RoutedMinutes: 40, DeliveryPincode: "641001", At: mondayTenAM()}); ok {
t.Error("served a handling term above the cap")
}
Reset()
}
func TestLoadedReportsState(t *testing.T) {
Reset()
if ok, _, _ := Loaded(); ok {
t.Error("Loaded() true after Reset")
}
Load(fixture())
defer Reset()
ok, _, cells := Loaded()
if !ok || cells != 3 { // 3 keyed cells; global is held separately
t.Errorf("Loaded() = %v, cells %d; want true, 3", ok, cells)
}
}