test(provision): swap to concurrent-safe broadcaster in 7-burst harness

CI Platform (Go) ran with -race and the concurrent test tripped the
detector: captureBroadcaster (sequential-test stub) writes lastData
unguarded; 7 fan-out goroutines call markProvisionFailed → that stub
concurrently. Local non-race run had hidden it.

Introduce concurrentSafeBroadcaster (mutex-counted) for this single
fan-out test. Sequential tests keep using captureBroadcaster — the
fix is local to the test that creates the goroutines.

Verified ./internal/handlers passes with -race.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hongming Wang 2026-05-01 20:03:11 -07:00
parent 7a19724194
commit 4f64c4366f

View File

@ -126,8 +126,13 @@ func TestProvisionWorkspaceCP_ConcurrentBurst_NoSilentDrop(t *testing.T) {
// goroutine entered AND reached the recorded Start() call.
rec := &recordingCPProv{startErr: fmt.Errorf("simulated CP rejection")}
cap := &captureBroadcaster{}
handler := NewWorkspaceHandler(cap, nil, "http://localhost:8080", t.TempDir())
// Concurrent-safe broadcaster — captureBroadcaster (used by sequential
// tests in workspace_provision_test.go) writes lastData unguarded.
// Under -race + 7 fan-out goroutines that's a real data race; this
// stub serializes via mutex and only counts (we don't need the
// payload for any assertion below).
bcast := &concurrentSafeBroadcaster{}
handler := NewWorkspaceHandler(bcast, nil, "http://localhost:8080", t.TempDir())
handler.SetCPProvisioner(rec)
var wg sync.WaitGroup
@ -203,6 +208,26 @@ type safeWriter struct {
mu *sync.Mutex
}
// concurrentSafeBroadcaster is a thread-safe events.EventEmitter stub
// for the 7-goroutine fan-out test. captureBroadcaster (the canonical
// sequential-test stub in workspace_provision_test.go) writes its
// lastData field without synchronization — under -race that's a true
// data race when 7 markProvisionFailed calls run concurrently. This
// stub only counts (no payload retention) and serializes via mutex.
type concurrentSafeBroadcaster struct {
mu sync.Mutex
count int
}
func (b *concurrentSafeBroadcaster) BroadcastOnly(_ string, _ string, _ interface{}) {}
func (b *concurrentSafeBroadcaster) RecordAndBroadcast(_ context.Context, _, _ string, _ interface{}) error {
b.mu.Lock()
b.count++
b.mu.Unlock()
return nil
}
func (w *safeWriter) Write(p []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()