From 4f5c71e81173d6022c539f376497fa4f3755cd9e Mon Sep 17 00:00:00 2001 From: Jan Willem Mannaerts Date: Tue, 3 Mar 2026 11:20:22 +0100 Subject: [PATCH] Fix infinite loop when saving 0-point estimate Saving an estimate of 0 caused advanceToNext to re-poker the same issue repeatedly: !0 is true and 0 === 0 matches the "unestimated" check. Track pokered issues with an `estimated` flag so they are skipped regardless of estimate value. Also guard against empty session index entries in createScopedSession (NATS KV tombstones after delete can return empty values, producing an invalid trailing-dot key that crashes Bucket.get). Co-Authored-By: Claude Opus 4.6 --- backend/src/services/pokerService.js | 19 ++++++++++++------- frontend/src/components/Room.jsx | 6 +++--- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/backend/src/services/pokerService.js b/backend/src/services/pokerService.js index 4a9c4cf..b947a2b 100644 --- a/backend/src/services/pokerService.js +++ b/backend/src/services/pokerService.js @@ -105,14 +105,19 @@ export async function createScopedSession({ issueKey, issueId, issueTitle, roomI const indexEntry = await kvSessionIndex.get(issueIndexKey(tenantCloudId, issueKey)); if (indexEntry) { const existingId = indexEntry.string(); - const existing = await getSession(tenantCloudId, existingId); - if (existing && existing.tenantCloudId === tenantCloudId) { - if (existing.state === 'VOTING') { - return getSnapshot(existing); - } - // Clean up stale revealed/saved sessions - await kvSessions.delete(sessionKey(tenantCloudId, existingId)); + if (!existingId) { + // Corrupted index entry (empty value) — clean up and continue await kvSessionIndex.delete(issueIndexKey(tenantCloudId, issueKey)); + } else { + const existing = await getSession(tenantCloudId, existingId); + if (existing && existing.tenantCloudId === tenantCloudId) { + if (existing.state === 'VOTING') { + return getSnapshot(existing); + } + // Clean up stale revealed/saved sessions + await kvSessions.delete(sessionKey(tenantCloudId, existingId)); + await kvSessionIndex.delete(issueIndexKey(tenantCloudId, issueKey)); + } } } diff --git a/frontend/src/components/Room.jsx b/frontend/src/components/Room.jsx index 1f128c8..a2c1f0a 100644 --- a/frontend/src/components/Room.jsx +++ b/frontend/src/components/Room.jsx @@ -75,13 +75,13 @@ export default function Room({ room, user, dark, toggleDark, onBack }) { const currentKey = activeSessionRef.current.issue.key; const updated = issuesRef.current.map((i) => - i.key === currentKey ? { ...i, estimate } : i + i.key === currentKey ? { ...i, estimate, estimated: true } : i ); issuesRef.current = updated; setIssues(updated); setActiveSession(null); - const next = updated.find((i) => !i.estimate || i.estimate === 0); + const next = updated.find((i) => !i.estimated && (!i.estimate || i.estimate === 0)); if (!next) { finishSession(); return; @@ -90,7 +90,7 @@ export default function Room({ room, user, dark, toggleDark, onBack }) { setTimeout(() => startSessionForIssue(next), 300); }, []); - const estimatedCount = issues.filter((i) => i.estimate && i.estimate > 0).length; + const estimatedCount = issues.filter((i) => i.estimated || (i.estimate && i.estimate > 0)).length; const totalCount = issues.length; if (loading) {