Assert that Model() and Context() are only accessed on the UI thread

The bounce model requires that a worker never touch UI-thread-owned
state: it should capture what it needs on the UI thread and pass that
in. Guard the two central accessors -- Model() (the git model) and
Context() (the context manager, which owns the mutable
current-context/stack) -- with a debug-only panic when they're called
off the UI thread. Since the integration tests run with -debug, a stray
worker access now fails deterministically and points at itself, rather
than surfacing later as a probabilistic data race.

One supporting change make the assertion usable: the integration test
driver inspects gui state from the test goroutine, so
GuiDriver.CurrentContext reads the context manager directly rather than
through the now-guarded c.Context().

Contexts() (the registry of context objects) is deliberately left
unguarded: workers legitimately fetch a context to grab its mutex or
check identity, so a blanket assertion there would flag safe accesses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller 2026-07-14 13:11:07 +02:00
parent c23bcd6d94
commit e299de3270
2 changed files with 16 additions and 1 deletions

View file

@ -58,7 +58,18 @@ func (self *guiCommon) PauseBackgroundRefreshes(pause bool) {
self.gui.BackgroundRoutineMgr.PauseBackgroundRefreshes(pause)
}
// assertOnUIThread panics (in debug builds) if called from a worker goroutine.
// Use it to guard accessors for state that only the UI thread may touch, so
// that a stray worker access fails deterministically -- and points at itself --
// rather than surfacing later as a probabilistic data race.
func (self *guiCommon) assertOnUIThread(accessor string) {
if self.GetConfig().GetDebug() && !self.GocuiGui().IsUIThread() {
panic(accessor + " accessed from a worker")
}
}
func (self *guiCommon) Context() types.IContextMgr {
self.assertOnUIThread("Context()")
return self.gui.State.ContextMgr
}
@ -113,6 +124,7 @@ func (self *guiCommon) Modes() *types.Modes {
}
func (self *guiCommon) Model() *types.Model {
self.assertOnUIThread("Model()")
return self.gui.State.Model
}

View file

@ -92,7 +92,10 @@ func (self *GuiDriver) Keys() config.KeybindingConfig {
}
func (self *GuiDriver) CurrentContext() types.Context {
return self.gui.c.Context().Current()
// Read the context manager directly rather than through c.Context(): the
// driver runs on the test goroutine, not the UI thread, so it must bypass
// the UI-thread assertion that accessor carries.
return self.gui.State.ContextMgr.Current()
}
func (self *GuiDriver) ContextForView(viewName string) types.Context {