Files
summercms/modules/pier/reaper_smoke_test.go
Jakub Zych 2bf9dca603 fix(pier): reap stateful sessions whose store record expired
- a reaper started at Mount checks every local session against the store
  every min(idle_ttl/2, 1m) and closes each copy whose record is gone,
  logging the expired step (A-CR-01, A-IN-06)
- the GET stream re-check ends the stream when the record is gone (A-IN-01)
- tool calls served without a session on the stateful path log the
  session as ephemeral (A-IN-06)
- TestPierReaperSmoke and the ReapNow, LocalSessions, StopReaper seams
2026-10-23 15:50:04 +02:00

67 lines
2.3 KiB
Go

package pier_test
import (
"context"
"net/http"
"testing"
"time"
"git.golem15.com/golem15/summercms/modules/pact"
)
// TestPierReaperSmoke is the tracer of phase 19.7 plan 06 (A-CR-01): on the
// memory store, a stateful session whose record expired is closed by the
// reaper (it leaves the local map, so fan-out no longer reaches it), its id
// answers 404 next, and a session kept active by a request survives a pass.
func TestPierReaperSmoke(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
in := startInstance(t, newApp(t, appSpec{
mcp: "enabled: false\nsessions:\n store: memory\n idle_ttl: 30m\n",
guards: []pact.MCPGuard{&acmeGuard{}},
}), acmePlugin{tools: []pact.MCPTool{echoTool()}})
p := in.pier()
t.Cleanup(p.StopReaper)
// 1. An SDK client opens a session on this instance.
abandoned := sdkClient(t, ctx, in.url(), "oas_at_test", clientOpts{})
if n := p.LocalSessions(); n != 1 {
t.Fatalf("local sessions after connect = %d, want 1", n)
}
// 2. The client goes away without DELETE; past the idle TTL its record
// is gone and one reaper pass closes the local copy.
if !p.AdvanceStoreClock(31 * time.Minute) {
t.Fatal("the instance has no memory store")
}
if n := p.ReapNow(ctx); n != 1 {
t.Fatalf("reaped = %d, want 1", n)
}
if n := p.LocalSessions(); n != 0 {
t.Fatalf("local sessions after the reap = %d, want 0", n)
}
// 3. The old session id answers 404, so the client re-initialises.
if gone := rpc(t, ctx, in.url(), "oas_at_test", abandoned.ID(), callBody); gone.status != http.StatusNotFound {
t.Fatalf("call with the reaped session = %d %s", gone.status, gone.body)
}
// 4. A session touched by a request within the TTL survives a pass.
active := openSession(t, ctx, in, "oas_at_test")
if !p.AdvanceStoreClock(20 * time.Minute) {
t.Fatal("the instance has no memory store")
}
if call := rpc(t, ctx, in.url(), "oas_at_test", active, callBody); call.status != http.StatusOK {
t.Fatalf("call on the active session = %d %s", call.status, call.body)
}
if !p.AdvanceStoreClock(20 * time.Minute) {
t.Fatal("the instance has no memory store")
}
if n := p.ReapNow(ctx); n != 0 {
t.Fatalf("reaped the active session (%d closed)", n)
}
if n := p.LocalSessions(); n != 1 {
t.Fatalf("local sessions after the second pass = %d, want 1", n)
}
}