Files
David Gageot 058f1523e8 feat(server): idempotency keys for follow-ups so retries are safe
A client whose POST /followup times out can't tell whether the
follow-up landed. Retrying risked delivering the prompt twice; not
retrying risked losing it.

Accept an optional Idempotency-Key header on /followup. A repeat with a
key already seen for the session is acknowledged (status "duplicate",
202) without delivering the message again, so retries are safe. The
reservation is per-session, bounded (recent keys only), made atomically
to settle concurrent retries to a single delivery, and rolled back if
delivery fails so a genuine failure stays retryable.
2026-06-15 11:01:08 +02:00

38 lines
1007 B
Go

package server
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIdempotencyCache_ReserveDedupes(t *testing.T) {
t.Parallel()
c := newIdempotencyCache(8)
assert.False(t, c.reserve("a"), "first sighting is not a duplicate")
assert.True(t, c.reserve("a"), "second sighting is a duplicate")
assert.False(t, c.reserve("b"), "a different key is not a duplicate")
}
func TestIdempotencyCache_ReleaseAllowsRetry(t *testing.T) {
t.Parallel()
c := newIdempotencyCache(8)
assert.False(t, c.reserve("a"))
c.release("a")
assert.False(t, c.reserve("a"), "after release the key is processable again")
}
func TestIdempotencyCache_EvictsOldestPastCapacity(t *testing.T) {
t.Parallel()
c := newIdempotencyCache(2)
assert.False(t, c.reserve("a"))
assert.False(t, c.reserve("b"))
assert.False(t, c.reserve("c")) // evicts "a"
assert.True(t, c.reserve("b"), "still-cached key remains a duplicate")
assert.False(t, c.reserve("a"), "evicted key is forgotten and processable again")
}