From 6c8cbbc78de7c7cb58417dcbe6661a53367ad773 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Fri, 25 Sep 2026 09:54:36 -0700 Subject: [PATCH] v2 plan: sticky-growth test appends turns instead of changing the first user message; note the finding Co-Authored-By: Claude Fable 5.1 --- docs/plans/v2/README.md | 6 ++++++ .../v2/_files/internal/proxy/ctxguard_test.go | 20 ++++++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/docs/plans/v2/README.md b/docs/plans/v2/README.md index 7d6dbe9..59c1606 100644 --- a/docs/plans/v2/README.md +++ b/docs/plans/v2/README.md @@ -61,3 +61,9 @@ reason. procedure. Owner's fault (T19 once more: a new task changed what an earlier given file measures, and the pre-handover walk missed it). `helpers_test.go` is now a v2 given file that answers `/props` without counting it; resumed. +- 2026-09-25, task 02: my `TestStickyLeaseSurvivesGrowthUntilItDoesNotFit` "grew" the conversation + by enlarging the *first user message*, which by the fingerprint spec makes it a different + conversation — so the test demanded `reused` for a new key. Ornith diagnosed it exactly ("turn + 2's fp differs from turn 1's, yet the test expects reuse") and the session ended on a + malformed tool call. Test fault (mine): later turns are now appended after the first user + message. Resumed from the working tree. diff --git a/docs/plans/v2/_files/internal/proxy/ctxguard_test.go b/docs/plans/v2/_files/internal/proxy/ctxguard_test.go index 65235f0..b56729b 100644 --- a/docs/plans/v2/_files/internal/proxy/ctxguard_test.go +++ b/docs/plans/v2/_files/internal/proxy/ctxguard_test.go @@ -110,6 +110,13 @@ func TestUnknownContextNeverBlocks(t *testing.T) { } } +// grow appends later turns to a conversation body without touching its system prompt or first +// user message, so the fingerprint — and therefore the lease — stays the same. +func grow(body string, words int) string { + turn := `,{"role":"assistant","content":"ok"},{"role":"user","content":"` + strings.Repeat("x ", words) + `"}` + return strings.Replace(body, `]}`, turn+`]}`, 1) +} + func TestStickyLeaseSurvivesGrowthUntilItDoesNotFit(t *testing.T) { small := ctxUpstream(t, "small", 8192, 2) big := ctxUpstream(t, "big", 131072, 1) @@ -120,19 +127,18 @@ func TestStickyLeaseSurvivesGrowthUntilItDoesNotFit(t *testing.T) { if resp.Header.Get(proxy.HostHeader) != "small" { t.Fatal("setup: first turn must be on small") } - // Same conversation (same first user message), later turn well under 4096: stays. - longer := strings.Replace(body, `"content":"`, `"content":"`+strings.Repeat("x ", 500), 1) - resp = r.post("/r/v1/chat/completions", longer) + // Same conversation, a later turn well under 4096 tokens: stays. + resp = r.post("/r/v1/chat/completions", grow(body, 500)) drain(resp) if resp.Header.Get(proxy.HostHeader) != "small" || resp.Header.Get(proxy.LeaseHeader) != "reused" { t.Errorf("turn 2: %q %q, want small reused", resp.Header.Get(proxy.HostHeader), resp.Header.Get(proxy.LeaseHeader)) } - // A turn that outgrows the slot moves the lease — once — and the move is recorded as an event. - huge := strings.Replace(body, `"content":"`, `"content":"`+strings.Repeat("x ", 30000), 1) + // A turn that outgrows the slot moves the lease — once — and the move is visible in the header. + huge := grow(body, 30000) resp = r.post("/r/v1/chat/completions", huge) drain(resp) - if resp.StatusCode != 200 || resp.Header.Get(proxy.HostHeader) != "big" { - t.Fatalf("outgrown turn: %d %q, want 200 from big", resp.StatusCode, resp.Header.Get(proxy.HostHeader)) + if resp.StatusCode != 200 || resp.Header.Get(proxy.HostHeader) != "big" || !strings.HasPrefix(resp.Header.Get(proxy.CtxHeader), "moved") { + t.Fatalf("outgrown turn: %d %q ctx=%q, want 200 from big with a moved header", resp.StatusCode, resp.Header.Get(proxy.HostHeader), resp.Header.Get(proxy.CtxHeader)) } resp = r.post("/r/v1/chat/completions", huge) drain(resp)