Skip to content

Track speed limit state independently of lookahead - #103

Merged
pfeiferj merged 3 commits into
pfeiferj:mainfrom
FrogAi:codex/track-speed-limit-state
Sep 7, 2026
Merged

pfeiferj merged 3 commits into
pfeiferj:mainfrom
FrogAi:codex/track-speed-limit-state

Conversation

@FrogAi

@FrogAi FrogAi commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Upstream updates the current-limit tracker only when a lookahead limit is present. This change tracks the selected current speed limit on every update, including when there is no lookahead. This preserves the last valid observation across missing data and lets upcoming limits be classified against that held value, with the configured source priority and offset.

Keep observation and acceptance timing separate. The raw Limit tracker describes the selected current source; Suggestion describes the value currently offered for acceptance, which can change because of lookahead or an offset while the raw current limit stays constant. Use the suggestion event for both the acceptance timeout and cruise-adjustment freshness.

Driver/update sequence Verified behavior with this change
Accept current 30 m/s by changing cruise 35→36, then offer lower 20 or higher 40 with unchanged cruise The new offer does not reuse the earlier cruise adjustment
Observe current 30 for a minute, then offer new 20 with a 10-second timeout The new offer gets its own acceptance window
Cruise input arrives in the same tick as first detection First-detection input is discarded; later input is required
Held current observation is lost/reacquired without changing the offer The pending offer keeps its own elapsed acceptance window

This belongs in the existing acceptance owner. It adds no tracker or state machine, and preserves source selection, held observations, offsets, accepted-limit retention and gas override policy.

Event ordering and complete real-path validation

State.UpdateCarState updates the car, scans upcoming ways, then updates speed-limit state. Acceptance is evaluated before the suggestion is recalculated. When a newly calculated suggestion changes, it resets acceptance and the remembered confirmation speed. Keeping that order discards first-detection input. On following updates, only a cruise timestamp later than the pending suggestion is fresh; timeout measures elapsed time since that same event. Gas remains the existing level-based input.

Tested source and validation scope

  • Upstream comparison: 7201c6b4b4ec1b0b9ea21daa8c05b80fdd7e01ee.
  • PR head: 20e7c25, tree 9fa74d269b8f25dd4d09a009d1f0a887bedf4e4b.

Checks described here used Go 1.25.1 on Linux amd64 with isolated fixtures and networking disabled during behavior tests. Existing repository tests remain unchanged; the extra reproductions below are deliberately outside the committed source. These are local execution results, not physical-device validation. The PR's Checks tab provides the published workflow result.

The tests below cover lower/higher lookahead with zero/10-second timeouts, stale/fresh/same-tick cruise changes, repeated updates, gas acceptance/override, offsets, first detection and held loss/reacquisition. The actual car message/upcoming/state path executes; the cruise timestamp is not fabricated in stale-confirmation cases. Timeout fixtures explicitly age trackers to model elapsed time without a minute-long sleep. The generic expired-offer fixture ages both observation and suggestion so it models the pending offer's timeout.

In the higher-limit test, cruise remains 36 m/s when a 40 m/s limit is offered. Without a fresh cruise adjustment, accepted limit and final suggestion remain 30 m/s. This is state/output behavior, not measured vehicle actuation. Ordinary tests, race checks, vet and Linux amd64 build passed.

Use an exact checkout with Linux Go 1.25.1 and its pinned go.mod dependencies already cached. To populate a separate module cache, create a temporary directory, set GOMODCACHE to a modules subdirectory there, and run go mod download from that checkout. The commands below then run offline, archive source into another temporary directory, and write no tests into the source checkout. They invoke tests, not the daemon.

ref=20e7c25b054b6399360676a7f539a39b4fbf855c # Validated PR head.
repository=$(pwd)
scratch=$(mktemp -d)
module_cache=$(go env GOMODCACHE)
mkdir -p "$scratch/source" "$scratch/tmp" "$scratch/gocache" "$scratch/gopath"
git -c core.autocrlf=false archive "$ref" | tar -x -C "$scratch/source"
cat > "$scratch/source/audit_speed_limit_test.go" <<'GO'
package main

import (
	"testing"
	"time"

	"capnproto.org/go/capnp/v3"
	"pfeifer.dev/mapd/cereal/custom"
	"pfeifer.dev/mapd/cereal/log"
	"pfeifer.dev/mapd/cereal/offline"
	"pfeifer.dev/mapd/maps"
	ms "pfeifer.dev/mapd/settings"
)

func speedLimitSettings(t *testing.T) {
	t.Helper()
	previous := ms.Settings
	t.Cleanup(func() { ms.Settings = previous })
	ms.Settings = ms.MapdSettings{SpeedLimitControlEnabled: true}
	ms.Settings.SpeedLimitSettings.HoldLastSeenSpeedLimit = true
	ms.Settings.Personalities.Standard = ms.PersonalitySettings{
		TargetSpeedAccel:          1,
		TargetSpeedJerk:           0.5,
		SlowDownForNextSpeedLimit: true,
	}
	ms.Settings.SetPersonality(log.LongitudinalPersonality_standard)
}

func speedLimitWay(t *testing.T, speed float32) CurrentWay {
	t.Helper()
	_, segment, err := capnp.NewMessage(capnp.SingleSegment(nil))
	if err != nil {
		t.Fatal(err)
	}
	way, err := offline.NewRootWay(segment)
	if err != nil {
		t.Fatal(err)
	}
	way.SetMaxSpeed(float64(speed))
	return CurrentWay{Way: maps.NewWay(way)}
}

func TestSpeedLimitHistoryWithoutLookahead(t *testing.T) {
	for _, hold := range []bool{false, true} {
		for _, offset := range []float32{-2, 0, 3} {
			speedLimitSettings(t)
			ms.Settings.SpeedLimitSettings.HoldLastSeenSpeedLimit = hold
			ms.Settings.SpeedLimitSettings.SpeedLimitOffset = offset
			state := SpeedLimitState{}
			state.Init()
			for _, observed := range []float32{20, 25, 0, 0} {
				state.Update(speedLimitWay(t, observed), CarState{})
				want := observed
				if want == 0 && hold {
					want = 25
				}
				if want > 0 {
					want += offset
				}
				if state.AcceptedLimit != want {
					t.Errorf("hold=%t offset=%v observation=%v: accepted %v, want %v", hold, offset, observed, state.AcceptedLimit, want)
				}
				if state.Limit.Value != observed {
					t.Errorf("current observation = %v, want %v", state.Limit.Value, observed)
				}
			}
		}
	}
}

func TestHeldSpeedLimitClassifiesUpcomingLimit(t *testing.T) {
	speedLimitSettings(t)
	ms.Settings.SpeedLimitSettings.SpeedLimitOffset = 2
	state := SpeedLimitState{}
	state.Init()
	state.Update(speedLimitWay(t, 30), CarState{VEgo: 30})
	state.NextLimit.Value = 20
	state.NextLimit.Distance = 1
	state.Update(speedLimitWay(t, 0), CarState{VEgo: 30})
	if state.AcceptedLimit != 22 {
		t.Fatalf("upcoming slower limit = %v, want 22", state.AcceptedLimit)
	}
}

func TestSpeedLimitHistoryUsesSelectedSource(t *testing.T) {
	speedLimitSettings(t)
	ms.Settings.ExternalSpeedLimitControlEnabled = true
	_, segment, err := capnp.NewMessage(capnp.SingleSegment(nil))
	if err != nil {
		t.Fatal(err)
	}
	input, err := custom.NewRootMapdIn(segment)
	if err != nil {
		t.Fatal(err)
	}
	input.SetType(custom.MapdInputType_setExternalSpeedLimit)
	input.SetFloat(30)
	ms.Settings.Handle(input)
	for priority, want := range map[ms.SpeedLimitPriority]float32{
		ms.PRIORITY_MAP: 20, ms.PRIORITY_EXTERNAL: 30, ms.PRIORITY_HIGHEST: 30, ms.PRIORITY_LOWEST: 20,
	} {
		ms.Settings.SpeedLimitSettings.SpeedLimitPriority = string(priority)
		state := SpeedLimitState{}
		state.Init()
		state.Update(speedLimitWay(t, 20), CarState{})
		if state.Limit.Value != want || state.AcceptedLimit != want {
			t.Errorf("%s: observed %v, accepted %v, want %v", priority, state.Limit.Value, state.AcceptedLimit, want)
		}
	}
}

func TestSpeedLimitAcceptanceWithoutLookahead(t *testing.T) {
	speedLimitSettings(t)
	ms.Settings.SpeedLimitSettings.SpeedLimitChangeRequiresAccept = true
	ms.Settings.SpeedLimitSettings.PressGasToAcceptSpeedLimit = true
	ms.Settings.SpeedLimitSettings.AcceptSpeedLimitTimeout = 10
	state := SpeedLimitState{}
	state.Init()
	way := speedLimitWay(t, 25)
	state.Update(way, CarState{})
	state.Update(way, CarState{GasPressed: true})
	if state.AcceptedLimit != 25 || !ms.Settings.SpeedLimitAccepted() {
		t.Fatal("gas did not accept the observed limit within its acceptance window")
	}
	missing := speedLimitWay(t, 0)
	state.Update(missing, CarState{})
	missingTime := state.Limit.UpdatedTime
	state.Update(missing, CarState{})
	if state.Limit.UpdatedTime != missingTime || state.AcceptedLimit != 25 {
		t.Fatal("repeated missing observation refreshed the window or lost the accepted held limit")
	}
	state.Update(way, CarState{})
	if state.Limit.Value != 25 || !state.Limit.UpdatedTime.After(missingTime) {
		t.Fatal("reacquiring the current limit did not update its observation")
	}
	ms.Settings.ResetSpeedLimitAccepted()
	state.Limit.UpdatedTime = time.Now().Add(-time.Minute)
	state.Suggestion.UpdatedTime = time.Now().Add(-time.Minute)
	state.Update(way, CarState{GasPressed: true})
	if ms.Settings.SpeedLimitAccepted() {
		t.Fatal("gas accepted a limit after the acceptance window expired")
	}
	ms.Settings.SpeedLimitSettings.AdjustSetSpeedToAcceptSpeedLimit = true
	state.Update(speedLimitWay(t, 20), CarState{})
	car := CarState{}
	car.SetSpeed.Update(20)
	state.Update(speedLimitWay(t, 20), car)
	if state.AcceptedLimit != 20 || !ms.Settings.SpeedLimitAccepted() {
		t.Fatal("set-speed change did not accept the new current limit")
	}
}
GO
cat > "$scratch/source/audit_real_speed_update_test.go" <<'GO'
package main

import (
  "testing"
  "time"

  "capnproto.org/go/capnp/v3"
  "pfeifer.dev/mapd/cereal/car"
  "pfeifer.dev/mapd/maps"
  m "pfeifer.dev/mapd/math"
  ms "pfeifer.dev/mapd/settings"
)

func auditCar(t *testing.T, cruise, ego float32, gas bool) car.CarState {
  t.Helper()
  _, segment, err := capnp.NewMessage(capnp.SingleSegment(nil))
  if err != nil { t.Fatal(err) }
  result, err := car.NewRootCarState(segment)
  if err != nil { t.Fatal(err) }
  result.SetVCruise(cruise * ms.MS_TO_KPH)
  result.SetVEgo(ego)
  result.SetGasPressed(gas)
  return result
}

func auditState(t *testing.T, speed float32) State {
  t.Helper()
  speedLimitSettings(t)
  state := State{}
  state.Init()
  state.CurrentWay = speedLimitWay(t, speed)
  return state
}

func TestAuditRealUpdateAcceptanceAndOverride(t *testing.T) {
  s := auditState(t, 25)
  ms.Settings.SpeedLimitSettings.SpeedLimitChangeRequiresAccept = true
  ms.Settings.SpeedLimitSettings.PressGasToAcceptSpeedLimit = true
  ms.Settings.SpeedLimitSettings.PressGasToOverrideSpeedLimit = true
  ms.Settings.SpeedLimitSettings.AcceptSpeedLimitTimeout = 10
  s.UpdateCarState(auditCar(t, 35, 25, false))
  if s.SpeedLimit.Limit.Value != 25 || s.SpeedLimit.AcceptedLimit != 0 { t.Fatalf("first observation %+v", s.SpeedLimit) }
  s.UpdateCarState(auditCar(t, 35, 25, true))
  if s.SpeedLimit.AcceptedLimit != 25 { t.Fatalf("gas acceptance %+v", s.SpeedLimit) }
  s.Car.SetSpeed.UpdatedTime = time.Now().Add(-2*time.Second)
  s.UpdateCarState(auditCar(t, 35, 30, true))
  if s.SpeedLimit.OverrideSpeed != 30 || s.SpeedLimit.SpeedLimitFinalSuggestion(true, false, 30) != 30 { t.Fatalf("gas override %+v", s.SpeedLimit) }
  s.UpdateCarState(auditCar(t, 36, 30, false))
  if s.SpeedLimit.OverrideSpeed != 0 { t.Fatalf("set speed override clear %+v", s.SpeedLimit) }
}

func TestAuditRealUpdateSetSpeedAcceptance(t *testing.T) {
  s := auditState(t, 25)
  ms.Settings.SpeedLimitSettings.SpeedLimitChangeRequiresAccept = true
  ms.Settings.SpeedLimitSettings.AdjustSetSpeedToAcceptSpeedLimit = true
  ms.Settings.SpeedLimitSettings.AcceptSpeedLimitTimeout = 10
  s.UpdateCarState(auditCar(t, 35, 25, false))
  s.UpdateCarState(auditCar(t, 35, 25, false))
  if ms.Settings.SpeedLimitAccepted() { t.Fatal("stale initial set speed accepted") }
  s.UpdateCarState(auditCar(t, 36, 25, false))
  if !ms.Settings.SpeedLimitAccepted() || s.SpeedLimit.AcceptedLimit != 25 { t.Fatalf("new set speed did not accept %+v", s.SpeedLimit) }
  s.UpdateCarState(auditCar(t, 37, 25, false))
  if ms.Settings.SpeedLimitAccepted() { t.Fatal("second set speed failed to reset acceptance") }
  t.Logf("after rejection accepted flag=%v retained accepted limit=%v final suggestion=%v", ms.Settings.SpeedLimitAccepted(), s.SpeedLimit.AcceptedLimit, s.SuggestedSpeed())
}

func TestAuditExpiredCurrentNewLookahead(t *testing.T) {
  s := auditState(t, 30)
  ms.Settings.SpeedLimitSettings.SpeedLimitChangeRequiresAccept = true
  ms.Settings.SpeedLimitSettings.PressGasToAcceptSpeedLimit = true
  ms.Settings.SpeedLimitSettings.AcceptSpeedLimitTimeout = 10
  s.UpdateCarState(auditCar(t, 35, 30, false))
  s.UpdateCarState(auditCar(t, 35, 30, true))
  s.SpeedLimit.Limit.UpdatedTime = time.Now().Add(-time.Minute)
  next := speedLimitWay(t, 20)
  s.NextWays = []maps.NextWayResult{{Way: next.Way, StartPosition: m.NewPosition(1, 1), IsForward: true}}
  s.UpdateCarState(auditCar(t, 35, 30, false))
  if s.SpeedLimit.Suggestion.Value != 20 { t.Fatalf("lookahead not reached %+v", s.SpeedLimit) }
  s.UpdateCarState(auditCar(t, 35, 30, true))
  t.Logf("new pending lookahead after old current observation: current=%v suggestion=%v accepted=%v flag=%v current_timestamp_age=%v suggestion_timestamp_age=%v", s.SpeedLimit.Limit.Value, s.SpeedLimit.Suggestion.Value, s.SpeedLimit.AcceptedLimit, ms.Settings.SpeedLimitAccepted(), time.Since(s.SpeedLimit.Limit.UpdatedTime), time.Since(s.SpeedLimit.Suggestion.UpdatedTime))
  if s.SpeedLimit.AcceptedLimit != 20 { t.Error("gas cannot accept newly pending lookahead within its own detection window") }
}

func TestAuditPriorSetSpeedCannotAcceptNewLookahead(t *testing.T) {
  s := auditState(t, 30)
  ms.Settings.SpeedLimitSettings.SpeedLimitChangeRequiresAccept = true
  ms.Settings.SpeedLimitSettings.AdjustSetSpeedToAcceptSpeedLimit = true
  ms.Settings.SpeedLimitSettings.AcceptSpeedLimitTimeout = 0
  s.UpdateCarState(auditCar(t, 35, 30, false))
  s.UpdateCarState(auditCar(t, 36, 30, false))
  if s.SpeedLimit.AcceptedLimit != 30 { t.Fatalf("could not establish accepted current30 %+v", s.SpeedLimit) }
  setSpeedChangeTime := s.Car.SetSpeed.UpdatedTime
  next := speedLimitWay(t, 20)
  s.NextWays = []maps.NextWayResult{{Way: next.Way, StartPosition: m.NewPosition(1, 1), IsForward: true}}
  s.UpdateCarState(auditCar(t, 36, 30, false))
  if ms.Settings.SpeedLimitAccepted() { t.Fatal("new next limit initially marked accepted") }
  s.UpdateCarState(auditCar(t, 36, 30, false))
  if s.Car.SetSpeed.UpdatedTime != setSpeedChangeTime { t.Fatal("fixture unexpectedly changed set speed") }
  t.Logf("unchanged set speed after newly pending lookahead: current=%v suggestion=%v accepted=%v flag=%v set_speed_time=%v observation_time=%v suggestion_time=%v", s.SpeedLimit.Limit.Value, s.SpeedLimit.Suggestion.Value, s.SpeedLimit.AcceptedLimit, ms.Settings.SpeedLimitAccepted(), s.Car.SetSpeed.UpdatedTime, s.SpeedLimit.Limit.UpdatedTime, s.SpeedLimit.Suggestion.UpdatedTime)
  if ms.Settings.SpeedLimitAccepted() || s.SpeedLimit.AcceptedLimit != 30 { t.Error("old set-speed adjustment accepted newly pending next20 without a new driver input") }
}
func TestAuditPriorSetSpeedCannotAcceptHigherLookahead(t *testing.T) {
  s := auditState(t, 30)
  ms.Settings.SpeedLimitSettings.SpeedLimitChangeRequiresAccept = true
  ms.Settings.SpeedLimitSettings.AdjustSetSpeedToAcceptSpeedLimit = true
  ms.Settings.SpeedLimitSettings.AcceptSpeedLimitTimeout = 0
  s.UpdateCarState(auditCar(t, 35, 30, false))
  s.UpdateCarState(auditCar(t, 36, 30, false))
  if s.SpeedLimit.AcceptedLimit != 30 { t.Fatalf("could not establish accepted current30 %+v", s.SpeedLimit) }
  setSpeedChangeTime := s.Car.SetSpeed.UpdatedTime
  ms.Settings.Personalities.Standard.SpeedUpForNextSpeedLimit = true
  next := speedLimitWay(t, 40)
  s.NextWays = []maps.NextWayResult{{Way: next.Way, StartPosition: m.NewPosition(1, 1), IsForward: true}}
  s.UpdateCarState(auditCar(t, 36, 30, false))
  if ms.Settings.SpeedLimitAccepted() { t.Fatal("new next limit initially marked accepted") }
  s.UpdateCarState(auditCar(t, 36, 30, false))
  if s.Car.SetSpeed.UpdatedTime != setSpeedChangeTime { t.Fatal("fixture unexpectedly changed set speed") }
  t.Logf("unchanged set speed after newly pending lookahead: current=%v suggestion=%v accepted=%v flag=%v set_speed_time=%v observation_time=%v suggestion_time=%v", s.SpeedLimit.Limit.Value, s.SpeedLimit.Suggestion.Value, s.SpeedLimit.AcceptedLimit, ms.Settings.SpeedLimitAccepted(), s.Car.SetSpeed.UpdatedTime, s.SpeedLimit.Limit.UpdatedTime, s.SpeedLimit.Suggestion.UpdatedTime)
  t.Logf("final suggested speed=%v", s.SuggestedSpeed())
  if ms.Settings.SpeedLimitAccepted() || s.SpeedLimit.AcceptedLimit != 30 { t.Error("old set-speed adjustment accepted newly pending next40 without a new driver input") }
}
GO
cat > "$scratch/source/acceptance_event_test.go" <<'GO'
package main

import (
  "fmt"
  "testing"
  "time"

  "pfeifer.dev/mapd/maps"
  m "pfeifer.dev/mapd/math"
  ms "pfeifer.dev/mapd/settings"
)

func TestSpeedLimitFreshLookaheadConfirmation(t *testing.T) {
  for _, nextSpeed := range []float32{20, 40} {
    for _, timeout := range []float32{0, 10} {
      t.Run(fmt.Sprintf("next%g_timeout%g", nextSpeed, timeout), func(t *testing.T) {
        state := auditState(t, 30)
        ms.Settings.SpeedLimitSettings.SpeedLimitChangeRequiresAccept = true
        ms.Settings.SpeedLimitSettings.AdjustSetSpeedToAcceptSpeedLimit = true
        ms.Settings.SpeedLimitSettings.AcceptSpeedLimitTimeout = timeout
        ms.Settings.Personalities.Standard.SpeedUpForNextSpeedLimit = true
        state.UpdateCarState(auditCar(t, 35, 30, false))
        state.UpdateCarState(auditCar(t, 36, 30, false))
        if state.SpeedLimit.AcceptedLimit != 30 { t.Fatal("initial fresh confirmation failed") }
        next := speedLimitWay(t, nextSpeed)
        state.NextWays = []maps.NextWayResult{{Way: next.Way, StartPosition: m.NewPosition(1, 1), IsForward: true}}
        // A fresh cruise adjustment in the first-detection tick must be discarded too.
        state.UpdateCarState(auditCar(t, 37, 30, false))
        pendingTime := state.SpeedLimit.Suggestion.UpdatedTime
        for index := 0; index < 3; index++ {
          state.UpdateCarState(auditCar(t, 37, 30, false))
          if ms.Settings.SpeedLimitAccepted() || state.SpeedLimit.AcceptedLimit != 30 {
            t.Fatal("first-detection cruise input accepted new lookahead")
          }
          if state.SpeedLimit.Suggestion.UpdatedTime != pendingTime { t.Fatal("repeated pending value refreshed event") }
        }
        state.UpdateCarState(auditCar(t, 38, 30, false))
        if !ms.Settings.SpeedLimitAccepted() || state.SpeedLimit.AcceptedLimit != nextSpeed { t.Fatal("later fresh input failed") }
      })
    }
  }
}

func TestSpeedLimitFirstDetectionAndOffsets(t *testing.T) {
  state := auditState(t, 30)
  ms.Settings.SpeedLimitSettings.SpeedLimitChangeRequiresAccept = true
  ms.Settings.SpeedLimitSettings.PressGasToAcceptSpeedLimit = true
  ms.Settings.SpeedLimitSettings.AdjustSetSpeedToAcceptSpeedLimit = true
  ms.Settings.SpeedLimitSettings.AcceptSpeedLimitTimeout = 10
  state.UpdateCarState(auditCar(t, 35, 30, true))
  if ms.Settings.SpeedLimitAccepted() || state.SpeedLimit.AcceptedLimit != 0 { t.Fatal("first detection accepted") }
  state.UpdateCarState(auditCar(t, 35, 30, false))
  if ms.Settings.SpeedLimitAccepted() { t.Fatal("initial cruise replay accepted") }
  state.UpdateCarState(auditCar(t, 36, 30, false))
  if state.SpeedLimit.AcceptedLimit != 30 { t.Fatal("fresh initial input failed") }
  observationTime := state.SpeedLimit.Limit.UpdatedTime
  ms.Settings.SpeedLimitSettings.SpeedLimitOffset = 2
  state.UpdateCarState(auditCar(t, 36, 30, false))
  state.UpdateCarState(auditCar(t, 36, 30, false))
  if state.SpeedLimit.Suggestion.Value != 32 || state.SpeedLimit.AcceptedLimit != 30 || ms.Settings.SpeedLimitAccepted() {
    t.Fatal("offset suggestion reused earlier confirmation")
  }
  if state.SpeedLimit.Limit.UpdatedTime != observationTime { t.Fatal("offset changed raw observation") }
  state.UpdateCarState(auditCar(t, 36, 30, true))
  if state.SpeedLimit.AcceptedLimit != 32 { t.Fatal("fresh gas failed to accept offset suggestion") }
}

func TestSpeedLimitHeldLossDoesNotRenewExpiredSuggestion(t *testing.T) {
  for _, gas := range []bool{false, true} {
    t.Run(fmt.Sprintf("gas%t", gas), func(t *testing.T) {
      state := auditState(t, 30)
      ms.Settings.SpeedLimitSettings.SpeedLimitChangeRequiresAccept = true
      ms.Settings.SpeedLimitSettings.PressGasToAcceptSpeedLimit = true
      ms.Settings.SpeedLimitSettings.AdjustSetSpeedToAcceptSpeedLimit = true
      ms.Settings.SpeedLimitSettings.AcceptSpeedLimitTimeout = 10
      state.UpdateCarState(auditCar(t, 35, 30, false))
      // Model an unchanged pending offer whose 10-second window has elapsed.
      expired := time.Now().Add(-11*time.Second)
      state.SpeedLimit.Suggestion.UpdatedTime = expired
      state.SpeedLimit.Limit.UpdatedTime = expired
      state.CurrentWay = speedLimitWay(t, 0)
      state.UpdateCarState(auditCar(t, 35, 30, false))
      missingTime := state.SpeedLimit.Limit.UpdatedTime
      state.UpdateCarState(auditCar(t, 35, 30, false))
      if state.SpeedLimit.Limit.UpdatedTime != missingTime || state.SpeedLimit.Suggestion.UpdatedTime != expired {
        t.Fatal("held/repeated loss refreshed wrong event")
      }
      state.CurrentWay = speedLimitWay(t, 30)
      state.UpdateCarState(auditCar(t, 35, 30, false))
      if !state.SpeedLimit.Limit.UpdatedTime.After(missingTime) || state.SpeedLimit.Suggestion.UpdatedTime != expired {
        t.Fatal("reacquisition failed to preserve distinct clocks")
      }
      state.UpdateCarState(auditCar(t, 36, 30, gas))
      if ms.Settings.SpeedLimitAccepted() || state.SpeedLimit.AcceptedLimit != 0 { t.Fatal("expired held suggestion revived") }
      ms.Settings.SpeedLimitSettings.SpeedLimitOffset = 2
      state.UpdateCarState(auditCar(t, 36, 30, false))
      state.UpdateCarState(auditCar(t, 37, 30, gas))
      if state.SpeedLimit.AcceptedLimit != 32 { t.Fatal("new offer did not get fresh acceptance window") }
    })
  }
}
GO
cd "$scratch/source"
GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off GOMODCACHE="$module_cache" \
  GOCACHE="$scratch/gocache" GOPATH="$scratch/gopath" TMPDIR="$scratch/tmp" \
  go test -mod=readonly -buildvcs=false -count=1 -timeout=120s -v ./...

Combined validation: exact source tree a4c306906627db3ac7a8ab768651c8628d55465a combines #101 9d61f06a1288ec4ea6f74f7d56a3057444316a13, #103 20e7c25b054b6399360676a7f539a39b4fbf855c, #105 bfcfe77be066634e36054327b20cfa6541063b54, #107 8e5e677d1196838069e9665d4e9d962bcc1e116b, #116 6fd5bbd6cf617c24a7fefd5e302fd36688a1a63b, #136 30e8ce98ea7a4c8401dbb5bfc62120c84fc689e4. The only overlapping file is settings/download.go; the resolution retains #136's selected-row loop and #107's progress publication inside it.

Combined Linux amd64 tests (including the scratch regression fixtures), race checks, vet and build passed. Under ARM64 emulation, the existing Makefile build stage (make GO_CAPNP_PATH=/usr/local/go-capnp/std), committed repository tests, vet and both CLI help commands passed with Go1.25.1; go.mod/go.sum stayed unchanged and the resulting executable is AArch64. The ARM64 run does not include the extra amd64 scratch tests. It used an isolated retained build image, not a new dependency-install/image rebuild or physical device. No archive payload or live params were accessed. #105 still requires runtime-first rollout before regenerated tiles are distributed.

@FrogAi
FrogAi force-pushed the codex/track-speed-limit-state branch 2 times, most recently from 5838f3a to 87e3cf2 Compare August 10, 2026 03:09
@FrogAi
FrogAi force-pushed the codex/track-speed-limit-state branch from 87e3cf2 to 65ff1b1 Compare August 10, 2026 03:21
@FrogAi
FrogAi force-pushed the codex/track-speed-limit-state branch from 65ff1b1 to f666308 Compare September 4, 2026 21:28
FrogAi added a commit to FrogAi/mapd that referenced this pull request Sep 4, 2026
Retain the original PR commits and the tested rewrite. The resulting
file tree is identical to f666308.
Replace the earlier implementation with the simplified version.
@FrogAi
FrogAi force-pushed the codex/track-speed-limit-state branch from 9cde270 to ce6ef71 Compare September 4, 2026 21:50
@pfeiferj
pfeiferj merged commit 7fbd3e9 into pfeiferj:main Sep 7, 2026
1 check passed
@FrogAi
FrogAi deleted the codex/track-speed-limit-state branch September 8, 2026 00:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants