summaryrefslogtreecommitdiff
path: root/internal/askcli/runlock_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/askcli/runlock_test.go')
-rw-r--r--internal/askcli/runlock_test.go30
1 files changed, 30 insertions, 0 deletions
diff --git a/internal/askcli/runlock_test.go b/internal/askcli/runlock_test.go
index 8ef8f2c..c8266f0 100644
--- a/internal/askcli/runlock_test.go
+++ b/internal/askcli/runlock_test.go
@@ -91,6 +91,36 @@ func TestAcquireAskRepoLock_StaleMetadataDoesNotRotateContendedLockFile(t *testi
}
}
+// TestAcquireAskRepoLock_ContextCancelledWhileBlocked verifies the retry loop
+// honors context cancellation while it is parked on the per-iteration timer.
+// This guards the time.After-based wait that replaced the unsafe timer reuse:
+// cancellation must win the select and return ctx.Err() promptly.
+func TestAcquireAskRepoLock_ContextCancelledWhileBlocked(t *testing.T) {
+ tmp := t.TempDir()
+ holder, _, _ := prepareContendedStaleLock(t, tmp)
+ defer releaseContendedLock(t, holder)
+
+ ctx, cancel := context.WithCancel(context.Background())
+ resultCh := acquireLockAsync(ctx, tmp)
+
+ // Let the contender enter the retry loop, then cancel while it waits.
+ time.Sleep(20 * time.Millisecond)
+ cancel()
+
+ select {
+ case result := <-resultCh:
+ if result.unlock != nil {
+ _ = result.unlock()
+ t.Fatal("lock acquired despite cancellation")
+ }
+ if result.err != context.Canceled {
+ t.Fatalf("err = %v, want context.Canceled", result.err)
+ }
+ case <-time.After(time.Second):
+ t.Fatal("acquireAskRepoLock did not return after cancellation")
+ }
+}
+
func prepareContendedStaleLock(t *testing.T, gitRoot string) (*os.File, string, os.FileInfo) {
t.Helper()
lockDir := filepath.Join(gitRoot, ".git")