542 lines
28 KiB
Go
542 lines
28 KiB
Go
package cache
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
homekv "github.com/router-for-me/CLIProxyAPI/v7/internal/home"
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
type fakeAntigravityReasoningReplayKVClient struct {
|
|
mu sync.Mutex
|
|
values map[string][]byte
|
|
expireCount int
|
|
casErr error
|
|
}
|
|
|
|
func newFakeAntigravityReasoningReplayKVClient() *fakeAntigravityReasoningReplayKVClient {
|
|
return &fakeAntigravityReasoningReplayKVClient{values: make(map[string][]byte)}
|
|
}
|
|
|
|
func (c *fakeAntigravityReasoningReplayKVClient) KVGet(_ context.Context, key string) ([]byte, bool, error) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
value, ok := c.values[key]
|
|
return append([]byte(nil), value...), ok, nil
|
|
}
|
|
|
|
func (c *fakeAntigravityReasoningReplayKVClient) KVSet(_ context.Context, key string, value []byte, _ homekv.KVSetOptions) (bool, error) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.values[key] = append([]byte(nil), value...)
|
|
return true, nil
|
|
}
|
|
|
|
func (c *fakeAntigravityReasoningReplayKVClient) KVCompareAndSwap(_ context.Context, key string, expected []byte, expectedExists bool, value []byte, _ time.Duration) (bool, error) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.casErr != nil {
|
|
return false, c.casErr
|
|
}
|
|
current, exists := c.values[key]
|
|
if exists != expectedExists || (exists && !bytes.Equal(current, expected)) {
|
|
return false, nil
|
|
}
|
|
c.values[key] = append([]byte(nil), value...)
|
|
return true, nil
|
|
}
|
|
|
|
func (c *fakeAntigravityReasoningReplayKVClient) KVDel(_ context.Context, keys ...string) (int64, error) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
var deleted int64
|
|
for _, key := range keys {
|
|
if _, ok := c.values[key]; ok {
|
|
delete(c.values, key)
|
|
deleted++
|
|
}
|
|
}
|
|
return deleted, nil
|
|
}
|
|
|
|
func (c *fakeAntigravityReasoningReplayKVClient) KVExpire(_ context.Context, _ string, _ time.Duration) (bool, error) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.expireCount++
|
|
return true, nil
|
|
}
|
|
|
|
func useFakeAntigravityReasoningReplayKVClient(t *testing.T, client *fakeAntigravityReasoningReplayKVClient, homeMode bool) {
|
|
t.Helper()
|
|
previous := currentAntigravityReasoningReplayKVClient
|
|
currentAntigravityReasoningReplayKVClient = func() (antigravityReasoningReplayKVClient, bool, error) {
|
|
return client, homeMode, nil
|
|
}
|
|
t.Cleanup(func() {
|
|
currentAntigravityReasoningReplayKVClient = previous
|
|
})
|
|
}
|
|
|
|
func antigravityReplayTestItem(signature string) []byte {
|
|
return []byte(`{"type":"thought_signature","contentIndex":1,"partIndex":0,"thoughtSignature":"` + signature + `"}`)
|
|
}
|
|
|
|
func TestAntigravityReasoningReplayConditionalMutationRejectsStaleLocalSnapshot(t *testing.T) {
|
|
ClearAntigravityReasoningReplayCache()
|
|
t.Cleanup(ClearAntigravityReasoningReplayCache)
|
|
const model, session = "gemini-3.6-flash-high", "stale-local"
|
|
oldItem := antigravityReplayTestItem("old-local-signature-123456")
|
|
newItem := antigravityReplayTestItem("new-local-signature-123456")
|
|
staleItem := antigravityReplayTestItem("stale-local-signature-123456")
|
|
if !CacheAntigravityReasoningReplayItems(model, session, [][]byte{oldItem}) {
|
|
t.Fatal("initial cache write failed")
|
|
}
|
|
_, snapshot, found, errGet := GetAntigravityReasoningReplayItemsWithSnapshotRequired(context.Background(), model, session)
|
|
if errGet != nil || !found {
|
|
t.Fatalf("snapshot read failed: found=%v err=%v", found, errGet)
|
|
}
|
|
if !CacheAntigravityReasoningReplayItems(model, session, [][]byte{newItem}) {
|
|
t.Fatal("newer cache write failed")
|
|
}
|
|
if swapped, errSwap := ReplaceAntigravityReasoningReplayItemsIfUnchanged(context.Background(), model, session, snapshot, [][]byte{staleItem}); errSwap != nil || swapped {
|
|
t.Fatalf("stale replace = %v, %v; want false, nil", swapped, errSwap)
|
|
}
|
|
if deleted, errDelete := DeleteAntigravityReasoningReplayItemsIfUnchanged(context.Background(), model, session, snapshot); errDelete != nil || deleted {
|
|
t.Fatalf("stale delete = %v, %v; want false, nil", deleted, errDelete)
|
|
}
|
|
items, ok := GetAntigravityReasoningReplayItems(model, session)
|
|
if !ok || len(items) != 1 || !bytes.Contains(items[0], []byte("new-local-signature")) {
|
|
t.Fatalf("newer state was lost: %q, found=%v", items, ok)
|
|
}
|
|
}
|
|
|
|
func TestAntigravityReasoningReplayNonPrefixReplaceRotatesLocalBranch(t *testing.T) {
|
|
ClearAntigravityReasoningReplayCache()
|
|
t.Cleanup(ClearAntigravityReasoningReplayCache)
|
|
const model, session = "gemini-3.6-flash-high", "non-prefix-local"
|
|
oldItem := antigravityReplayTestItem("non-prefix-old-signature-123456")
|
|
newItem := antigravityReplayTestItem("non-prefix-new-signature-123456")
|
|
latestItem := antigravityReplayTestItem("non-prefix-latest-signature-123456")
|
|
if !CacheAntigravityReasoningReplayItems(model, session, [][]byte{oldItem}) {
|
|
t.Fatal("old local write failed")
|
|
}
|
|
_, firstSnapshot, _, errFirstGet := GetAntigravityReasoningReplayItemsWithSnapshotRequired(context.Background(), model, session)
|
|
_, staleSnapshot, _, errStaleGet := GetAntigravityReasoningReplayItemsWithSnapshotRequired(context.Background(), model, session)
|
|
if errFirstGet != nil || errStaleGet != nil {
|
|
t.Fatalf("snapshot reads failed: %v, %v", errFirstGet, errStaleGet)
|
|
}
|
|
if swapped, errSwap := ReplaceAntigravityReasoningReplayItemsIfUnchanged(context.Background(), model, session, firstSnapshot, [][]byte{newItem}); errSwap != nil || !swapped {
|
|
t.Fatalf("non-prefix local replace = %v, %v", swapped, errSwap)
|
|
}
|
|
if swapped, errSwap := ReplaceAntigravityReasoningReplayItemsIfUnchanged(context.Background(), model, session, staleSnapshot, [][]byte{newItem, latestItem}); errSwap != nil || swapped {
|
|
t.Fatalf("stale local descendant crossed non-prefix reset: swapped=%v err=%v", swapped, errSwap)
|
|
}
|
|
}
|
|
|
|
func TestAntigravityReasoningReplayConditionalReplaceAcceptsDescendantLocalChain(t *testing.T) {
|
|
ClearAntigravityReasoningReplayCache()
|
|
t.Cleanup(ClearAntigravityReasoningReplayCache)
|
|
const model, session = "gemini-3.6-flash-high", "descendant-local"
|
|
prefix := antigravityReplayTestItem("descendant-prefix-signature-123456")
|
|
middle := antigravityReplayTestItem("descendant-middle-signature-123456")
|
|
latest := antigravityReplayTestItem("descendant-latest-signature-123456")
|
|
if !CacheAntigravityReasoningReplayItems(model, session, [][]byte{prefix}) {
|
|
t.Fatal("prefix write failed")
|
|
}
|
|
_, staleSnapshot, found, errGet := GetAntigravityReasoningReplayItemsWithSnapshotRequired(context.Background(), model, session)
|
|
if errGet != nil || !found {
|
|
t.Fatalf("prefix snapshot failed: found=%v err=%v", found, errGet)
|
|
}
|
|
_, firstSnapshot, _, errFirstGet := GetAntigravityReasoningReplayItemsWithSnapshotRequired(context.Background(), model, session)
|
|
if errFirstGet != nil {
|
|
t.Fatal(errFirstGet)
|
|
}
|
|
if swapped, errSwap := ReplaceAntigravityReasoningReplayItemsIfUnchanged(context.Background(), model, session, firstSnapshot, [][]byte{prefix, middle}); errSwap != nil || !swapped {
|
|
t.Fatalf("middle conditional write = %v, %v", swapped, errSwap)
|
|
}
|
|
if swapped, errSwap := ReplaceAntigravityReasoningReplayItemsIfUnchanged(context.Background(), model, session, staleSnapshot, [][]byte{prefix, middle, latest}); errSwap != nil || !swapped {
|
|
t.Fatalf("descendant local replace = %v, %v; want true, nil", swapped, errSwap)
|
|
}
|
|
items, ok := GetAntigravityReasoningReplayItems(model, session)
|
|
if !ok || len(items) != 3 {
|
|
t.Fatalf("descendant local chain = %d items, found=%v", len(items), ok)
|
|
}
|
|
}
|
|
|
|
func TestAntigravityReasoningReplayDescendantMergeRejectsResetBranchABA(t *testing.T) {
|
|
ClearAntigravityReasoningReplayCache()
|
|
t.Cleanup(ClearAntigravityReasoningReplayCache)
|
|
const model, session = "gemini-3.6-flash-high", "descendant-reset-aba"
|
|
prefix := antigravityReplayTestItem("reset-prefix-signature-123456")
|
|
middle := antigravityReplayTestItem("reset-middle-signature-123456")
|
|
staleLatest := antigravityReplayTestItem("reset-stale-signature-123456")
|
|
if !CacheAntigravityReasoningReplayItems(model, session, [][]byte{prefix}) {
|
|
t.Fatal("prefix write failed")
|
|
}
|
|
_, staleSnapshot, _, errStaleGet := GetAntigravityReasoningReplayItemsWithSnapshotRequired(context.Background(), model, session)
|
|
_, firstSnapshot, _, errFirstGet := GetAntigravityReasoningReplayItemsWithSnapshotRequired(context.Background(), model, session)
|
|
if errStaleGet != nil || errFirstGet != nil {
|
|
t.Fatalf("snapshot reads failed: %v, %v", errStaleGet, errFirstGet)
|
|
}
|
|
if swapped, errSwap := ReplaceAntigravityReasoningReplayItemsIfUnchanged(context.Background(), model, session, firstSnapshot, [][]byte{prefix, middle}); errSwap != nil || !swapped {
|
|
t.Fatalf("middle write = %v, %v", swapped, errSwap)
|
|
}
|
|
_, currentSnapshot, _, errCurrentGet := GetAntigravityReasoningReplayItemsWithSnapshotRequired(context.Background(), model, session)
|
|
if errCurrentGet != nil {
|
|
t.Fatal(errCurrentGet)
|
|
}
|
|
if deleted, errDelete := DeleteAntigravityReasoningReplayItemsIfUnchanged(context.Background(), model, session, currentSnapshot); errDelete != nil || !deleted {
|
|
t.Fatalf("branch reset = %v, %v", deleted, errDelete)
|
|
}
|
|
_, resetSnapshot, _, errResetGet := GetAntigravityReasoningReplayItemsWithSnapshotRequired(context.Background(), model, session)
|
|
if errResetGet != nil {
|
|
t.Fatal(errResetGet)
|
|
}
|
|
if swapped, errSwap := ReplaceAntigravityReasoningReplayItemsIfUnchanged(context.Background(), model, session, resetSnapshot, [][]byte{prefix}); errSwap != nil || !swapped {
|
|
t.Fatalf("new branch prefix write = %v, %v", swapped, errSwap)
|
|
}
|
|
if swapped, errSwap := ReplaceAntigravityReasoningReplayItemsIfUnchanged(context.Background(), model, session, staleSnapshot, [][]byte{prefix, staleLatest}); errSwap != nil || swapped {
|
|
t.Fatalf("stale descendant crossed reset branch: swapped=%v err=%v", swapped, errSwap)
|
|
}
|
|
}
|
|
|
|
func TestAntigravityReasoningReplayConditionalDeleteTombstoneBlocksStaleFirstWriter(t *testing.T) {
|
|
ClearAntigravityReasoningReplayCache()
|
|
t.Cleanup(ClearAntigravityReasoningReplayCache)
|
|
const model, session = "gemini-3.6-flash-high", "stale-first-writer"
|
|
_, staleSnapshot, found, errGet := GetAntigravityReasoningReplayItemsWithSnapshotRequired(context.Background(), model, session)
|
|
if errGet != nil || found {
|
|
t.Fatalf("initial absent snapshot = found %v, err %v", found, errGet)
|
|
}
|
|
_, clearSnapshot, _, errClearGet := GetAntigravityReasoningReplayItemsWithSnapshotRequired(context.Background(), model, session)
|
|
if errClearGet != nil {
|
|
t.Fatal(errClearGet)
|
|
}
|
|
if deleted, errDelete := DeleteAntigravityReasoningReplayItemsIfUnchanged(context.Background(), model, session, clearSnapshot); errDelete != nil || !deleted {
|
|
t.Fatalf("conditional empty clear = %v, %v; want true, nil", deleted, errDelete)
|
|
}
|
|
staleItem := antigravityReplayTestItem("stale-first-writer-signature-123456")
|
|
if swapped, errSwap := ReplaceAntigravityReasoningReplayItemsIfUnchanged(context.Background(), model, session, staleSnapshot, [][]byte{staleItem}); errSwap != nil || swapped {
|
|
t.Fatalf("stale first write = %v, %v; want false, nil", swapped, errSwap)
|
|
}
|
|
}
|
|
|
|
func TestAntigravityReasoningReplayEvictedTombstoneStillBlocksStaleFirstWriter(t *testing.T) {
|
|
ClearAntigravityReasoningReplayCache()
|
|
t.Cleanup(ClearAntigravityReasoningReplayCache)
|
|
const model, session = "gemini-3.6-flash-high", "evicted-stale-first-writer"
|
|
_, staleSnapshot, found, errGet := GetAntigravityReasoningReplayItemsWithSnapshotRequired(context.Background(), model, session)
|
|
if errGet != nil || found {
|
|
t.Fatalf("initial absent snapshot = found %v, err %v", found, errGet)
|
|
}
|
|
_, clearSnapshot, _, errClearGet := GetAntigravityReasoningReplayItemsWithSnapshotRequired(context.Background(), model, session)
|
|
if errClearGet != nil {
|
|
t.Fatal(errClearGet)
|
|
}
|
|
if deleted, errDelete := DeleteAntigravityReasoningReplayItemsIfUnchanged(context.Background(), model, session, clearSnapshot); errDelete != nil || !deleted {
|
|
t.Fatalf("conditional clear = %v, %v", deleted, errDelete)
|
|
}
|
|
antigravityReasoningReplayMu.Lock()
|
|
evictOldestAntigravityReasoningReplayEntries(1)
|
|
antigravityReasoningReplayMu.Unlock()
|
|
staleItem := antigravityReplayTestItem("evicted-stale-first-writer-signature-123456")
|
|
if swapped, errSwap := ReplaceAntigravityReasoningReplayItemsIfUnchanged(context.Background(), model, session, staleSnapshot, [][]byte{staleItem}); errSwap != nil || swapped {
|
|
t.Fatalf("stale first writer crossed tombstone eviction: swapped=%v err=%v", swapped, errSwap)
|
|
}
|
|
}
|
|
|
|
func TestAntigravityReasoningReplayUnrelatedEvictionDoesNotBlockAbsentSnapshot(t *testing.T) {
|
|
ClearAntigravityReasoningReplayCache()
|
|
t.Cleanup(ClearAntigravityReasoningReplayCache)
|
|
const model = "gemini-3.6-flash-high"
|
|
liveItem := antigravityReplayTestItem("evicted-live-signature-123456")
|
|
if !CacheAntigravityReasoningReplayItems(model, "older-live-entry", [][]byte{liveItem}) {
|
|
t.Fatal("live entry write failed")
|
|
}
|
|
_, snapshot, found, errGet := GetAntigravityReasoningReplayItemsWithSnapshotRequired(context.Background(), model, "untouched-absent-session")
|
|
if errGet != nil || found {
|
|
t.Fatalf("initial absent snapshot = found %v, err %v", found, errGet)
|
|
}
|
|
antigravityReasoningReplayMu.Lock()
|
|
evictOldestAntigravityReasoningReplayEntries(1)
|
|
antigravityReasoningReplayMu.Unlock()
|
|
firstItem := antigravityReplayTestItem("first-write-after-unrelated-eviction-123456")
|
|
if swapped, errSwap := ReplaceAntigravityReasoningReplayItemsIfUnchanged(context.Background(), model, "untouched-absent-session", snapshot, [][]byte{firstItem}); errSwap != nil || !swapped {
|
|
t.Fatalf("unrelated eviction blocked first write: swapped=%v err=%v", swapped, errSwap)
|
|
}
|
|
}
|
|
|
|
func TestAntigravityReasoningReplayHomeAbsentSnapshotIsFenced(t *testing.T) {
|
|
client := newFakeAntigravityReasoningReplayKVClient()
|
|
useFakeAntigravityReasoningReplayKVClient(t, client, true)
|
|
const model, session = "gemini-3.6-flash-high", "home-absent-fence"
|
|
_, snapshot, found, errGet := GetAntigravityReasoningReplayItemsWithSnapshotRequired(context.Background(), model, session)
|
|
if errGet != nil || found || !snapshot.found || len(snapshot.raw) == 0 {
|
|
t.Fatalf("fenced Home miss = found %v snapshotFound %v raw %d err %v", found, snapshot.found, len(snapshot.raw), errGet)
|
|
}
|
|
key := antigravityReasoningReplayKVKey(model, session)
|
|
client.mu.Lock()
|
|
client.values[key] = []byte(`[[123]]`)
|
|
delete(client.values, key)
|
|
client.mu.Unlock()
|
|
item := antigravityReplayTestItem("home-absent-stale-signature-123456")
|
|
if swapped, errSwap := ReplaceAntigravityReasoningReplayItemsIfUnchanged(context.Background(), model, session, snapshot, [][]byte{item}); errSwap != nil || swapped {
|
|
t.Fatalf("stale Home absent snapshot crossed value expiry: swapped=%v err=%v", swapped, errSwap)
|
|
}
|
|
}
|
|
|
|
func TestAntigravityReasoningReplayConditionalMutationRejectsStaleHomeSnapshot(t *testing.T) {
|
|
client := newFakeAntigravityReasoningReplayKVClient()
|
|
useFakeAntigravityReasoningReplayKVClient(t, client, true)
|
|
const model, session = "gemini-3.6-flash-high", "stale-home"
|
|
oldItem := antigravityReplayTestItem("old-home-signature-123456")
|
|
newItem := antigravityReplayTestItem("new-home-signature-123456")
|
|
staleItem := antigravityReplayTestItem("stale-home-signature-123456")
|
|
if !CacheAntigravityReasoningReplayItems(model, session, [][]byte{oldItem}) {
|
|
t.Fatal("initial Home write failed")
|
|
}
|
|
_, snapshot, found, errGet := GetAntigravityReasoningReplayItemsWithSnapshotRequired(context.Background(), model, session)
|
|
if errGet != nil || !found {
|
|
t.Fatalf("Home snapshot read failed: found=%v err=%v", found, errGet)
|
|
}
|
|
if !CacheAntigravityReasoningReplayItems(model, session, [][]byte{newItem}) {
|
|
t.Fatal("newer Home write failed")
|
|
}
|
|
if swapped, errSwap := ReplaceAntigravityReasoningReplayItemsIfUnchanged(context.Background(), model, session, snapshot, [][]byte{staleItem}); errSwap != nil || swapped {
|
|
t.Fatalf("stale Home replace = %v, %v; want false, nil", swapped, errSwap)
|
|
}
|
|
if deleted, errDelete := DeleteAntigravityReasoningReplayItemsIfUnchanged(context.Background(), model, session, snapshot); errDelete != nil || deleted {
|
|
t.Fatalf("stale Home delete = %v, %v; want false, nil", deleted, errDelete)
|
|
}
|
|
items, ok := GetAntigravityReasoningReplayItems(model, session)
|
|
if !ok || len(items) != 1 || !bytes.Contains(items[0], []byte("new-home-signature")) {
|
|
t.Fatalf("newer Home state was lost: %q, found=%v", items, ok)
|
|
}
|
|
}
|
|
|
|
func TestAntigravityReasoningReplayNonPrefixReplaceRotatesHomeBranch(t *testing.T) {
|
|
client := newFakeAntigravityReasoningReplayKVClient()
|
|
useFakeAntigravityReasoningReplayKVClient(t, client, true)
|
|
const model, session = "gemini-3.6-flash-high", "non-prefix-home"
|
|
oldItem := antigravityReplayTestItem("non-prefix-home-old-123456")
|
|
newItem := antigravityReplayTestItem("non-prefix-home-new-123456")
|
|
latestItem := antigravityReplayTestItem("non-prefix-home-latest-123456")
|
|
if !CacheAntigravityReasoningReplayItems(model, session, [][]byte{oldItem}) {
|
|
t.Fatal("old Home write failed")
|
|
}
|
|
_, firstSnapshot, _, errFirstGet := GetAntigravityReasoningReplayItemsWithSnapshotRequired(context.Background(), model, session)
|
|
_, staleSnapshot, _, errStaleGet := GetAntigravityReasoningReplayItemsWithSnapshotRequired(context.Background(), model, session)
|
|
if errFirstGet != nil || errStaleGet != nil {
|
|
t.Fatalf("Home snapshot reads failed: %v, %v", errFirstGet, errStaleGet)
|
|
}
|
|
if swapped, errSwap := ReplaceAntigravityReasoningReplayItemsIfUnchanged(context.Background(), model, session, firstSnapshot, [][]byte{newItem}); errSwap != nil || !swapped {
|
|
t.Fatalf("non-prefix Home replace = %v, %v", swapped, errSwap)
|
|
}
|
|
if swapped, errSwap := ReplaceAntigravityReasoningReplayItemsIfUnchanged(context.Background(), model, session, staleSnapshot, [][]byte{newItem, latestItem}); errSwap != nil || swapped {
|
|
t.Fatalf("stale Home descendant crossed non-prefix reset: swapped=%v err=%v", swapped, errSwap)
|
|
}
|
|
}
|
|
|
|
func TestAntigravityReasoningReplayConditionalReplaceAcceptsDescendantHomeChain(t *testing.T) {
|
|
client := newFakeAntigravityReasoningReplayKVClient()
|
|
useFakeAntigravityReasoningReplayKVClient(t, client, true)
|
|
const model, session = "gemini-3.6-flash-high", "descendant-home"
|
|
prefix := antigravityReplayTestItem("home-descendant-prefix-123456")
|
|
middle := antigravityReplayTestItem("home-descendant-middle-123456")
|
|
latest := antigravityReplayTestItem("home-descendant-latest-123456")
|
|
if !CacheAntigravityReasoningReplayItems(model, session, [][]byte{prefix}) {
|
|
t.Fatal("Home prefix write failed")
|
|
}
|
|
_, staleSnapshot, found, errGet := GetAntigravityReasoningReplayItemsWithSnapshotRequired(context.Background(), model, session)
|
|
if errGet != nil || !found {
|
|
t.Fatalf("Home prefix snapshot failed: found=%v err=%v", found, errGet)
|
|
}
|
|
_, firstSnapshot, _, errFirstGet := GetAntigravityReasoningReplayItemsWithSnapshotRequired(context.Background(), model, session)
|
|
if errFirstGet != nil {
|
|
t.Fatal(errFirstGet)
|
|
}
|
|
if swapped, errSwap := ReplaceAntigravityReasoningReplayItemsIfUnchanged(context.Background(), model, session, firstSnapshot, [][]byte{prefix, middle}); errSwap != nil || !swapped {
|
|
t.Fatalf("Home middle conditional write = %v, %v", swapped, errSwap)
|
|
}
|
|
if swapped, errSwap := ReplaceAntigravityReasoningReplayItemsIfUnchanged(context.Background(), model, session, staleSnapshot, [][]byte{prefix, middle, latest}); errSwap != nil || !swapped {
|
|
t.Fatalf("descendant Home replace = %v, %v; want true, nil", swapped, errSwap)
|
|
}
|
|
items, ok := GetAntigravityReasoningReplayItems(model, session)
|
|
if !ok || len(items) != 3 {
|
|
t.Fatalf("descendant Home chain = %d items, found=%v", len(items), ok)
|
|
}
|
|
}
|
|
|
|
func TestAntigravityReasoningReplayHomeGenerationRejectsSuccessfulValueABA(t *testing.T) {
|
|
client := newFakeAntigravityReasoningReplayKVClient()
|
|
useFakeAntigravityReasoningReplayKVClient(t, client, true)
|
|
const model, session = "gemini-3.6-flash-high", "home-aba"
|
|
itemA := antigravityReplayTestItem("home-aba-signature-a-123456")
|
|
itemB := antigravityReplayTestItem("home-aba-signature-b-123456")
|
|
if !CacheAntigravityReasoningReplayItems(model, session, [][]byte{itemA}) {
|
|
t.Fatal("initial A write failed")
|
|
}
|
|
_, staleSnapshot, found, errGet := GetAntigravityReasoningReplayItemsWithSnapshotRequired(context.Background(), model, session)
|
|
if errGet != nil || !found {
|
|
t.Fatalf("A snapshot read failed: found=%v err=%v", found, errGet)
|
|
}
|
|
if !CacheAntigravityReasoningReplayItems(model, session, [][]byte{itemB}) || !CacheAntigravityReasoningReplayItems(model, session, [][]byte{itemA}) {
|
|
t.Fatal("B to A rewrite failed")
|
|
}
|
|
if swapped, errSwap := ReplaceAntigravityReasoningReplayItemsIfUnchanged(context.Background(), model, session, staleSnapshot, [][]byte{itemB}); errSwap != nil || swapped {
|
|
t.Fatalf("stale A snapshot passed Home ABA guard: swapped=%v err=%v", swapped, errSwap)
|
|
}
|
|
}
|
|
|
|
func TestAntigravityReasoningReplayHomeReportsCASErrors(t *testing.T) {
|
|
// The cache layer keeps reporting CAS failures honestly. Deciding that a
|
|
// replay failure must not fail the request is the executor's job, so this
|
|
// layer must not start swallowing errors.
|
|
client := newFakeAntigravityReasoningReplayKVClient()
|
|
client.casErr = fmt.Errorf("ERR unknown command 'cas'")
|
|
useFakeAntigravityReasoningReplayKVClient(t, client, true)
|
|
const model, session = "gemini-3.6-flash-high", "home-cas-error"
|
|
|
|
_, _, found, errGet := GetAntigravityReasoningReplayItemsWithSnapshotRequired(context.Background(), model, session)
|
|
if errGet == nil {
|
|
t.Fatal("GetAntigravityReasoningReplayItemsWithSnapshotRequired() error = nil, want the CAS error")
|
|
}
|
|
if found {
|
|
t.Fatal("GetAntigravityReasoningReplayItemsWithSnapshotRequired() found = true, want false")
|
|
}
|
|
|
|
snapshot := AntigravityReasoningReplaySnapshot{loaded: true}
|
|
if _, errReplace := ReplaceAntigravityReasoningReplayItemsIfUnchanged(context.Background(), model, session, snapshot, [][]byte{antigravityReplayTestItem("home-cas-error-sig-1")}); errReplace == nil {
|
|
t.Fatal("ReplaceAntigravityReasoningReplayItemsIfUnchanged() error = nil, want the CAS error")
|
|
}
|
|
if _, errDelete := DeleteAntigravityReasoningReplayItemsIfUnchanged(context.Background(), model, session, snapshot); errDelete == nil {
|
|
t.Fatal("DeleteAntigravityReasoningReplayItemsIfUnchanged() error = nil, want the CAS error")
|
|
}
|
|
}
|
|
|
|
func TestAntigravityReasoningReplayHomeCASRetryRejectsOversizedValue(t *testing.T) {
|
|
client := newFakeAntigravityReasoningReplayKVClient()
|
|
useFakeAntigravityReasoningReplayKVClient(t, client, true)
|
|
const model, session = "gemini-3.6-flash-high", "oversized-home-cas"
|
|
prefix := antigravityReplayTestItem("oversized-home-prefix-123456")
|
|
latest := antigravityReplayTestItem("oversized-home-latest-123456")
|
|
if !CacheAntigravityReasoningReplayItems(model, session, [][]byte{prefix}) {
|
|
t.Fatal("Home prefix write failed")
|
|
}
|
|
_, snapshot, found, errGet := GetAntigravityReasoningReplayItemsWithSnapshotRequired(context.Background(), model, session)
|
|
if errGet != nil || !found {
|
|
t.Fatalf("Home snapshot read failed: found=%v err=%v", found, errGet)
|
|
}
|
|
oversized, errMarshal := marshalAntigravityReasoningReplayHomeValue([][]byte{prefix}, snapshot.branch)
|
|
if errMarshal != nil {
|
|
t.Fatal(errMarshal)
|
|
}
|
|
oversized = append(oversized, bytes.Repeat([]byte(" "), antigravityReasoningReplayCacheMaxSerializedBytes-len(oversized)+1)...)
|
|
key := antigravityReasoningReplayKVKey(model, session)
|
|
client.values[key] = oversized
|
|
if swapped, errSwap := ReplaceAntigravityReasoningReplayItemsIfUnchanged(context.Background(), model, session, snapshot, [][]byte{prefix, latest}); errSwap != nil || swapped {
|
|
t.Fatalf("oversized Home CAS retry = swapped %v, err %v; want false, nil", swapped, errSwap)
|
|
}
|
|
if got := len(client.values[key]); got <= antigravityReasoningReplayCacheMaxSerializedBytes {
|
|
t.Fatalf("oversized value was unexpectedly replaced: %d", got)
|
|
}
|
|
}
|
|
|
|
func TestAntigravityReasoningReplayLocalTombstonesStayWithinEntryBound(t *testing.T) {
|
|
ClearAntigravityReasoningReplayCache()
|
|
t.Cleanup(ClearAntigravityReasoningReplayCache)
|
|
for index := 0; index <= AntigravityReasoningReplayCacheMaxEntries; index++ {
|
|
if errDelete := DeleteAntigravityReasoningReplayItemRequired(context.Background(), "gemini-3.6-flash-high", fmt.Sprintf("tombstone-%d", index)); errDelete != nil {
|
|
t.Fatal(errDelete)
|
|
}
|
|
}
|
|
antigravityReasoningReplayMu.Lock()
|
|
entryCount := len(antigravityReasoningReplayEntries)
|
|
antigravityReasoningReplayMu.Unlock()
|
|
if entryCount > AntigravityReasoningReplayCacheMaxEntries {
|
|
t.Fatalf("local tombstone count = %d, max %d", entryCount, AntigravityReasoningReplayCacheMaxEntries)
|
|
}
|
|
}
|
|
|
|
func TestAntigravityReasoningReplayLocalAbsenceReservationsStayWithinEntryBound(t *testing.T) {
|
|
ClearAntigravityReasoningReplayCache()
|
|
t.Cleanup(ClearAntigravityReasoningReplayCache)
|
|
const model = "gemini-3.6-flash-high"
|
|
latestSession := ""
|
|
for index := 0; index <= AntigravityReasoningReplayCacheMaxEntries; index++ {
|
|
latestSession = fmt.Sprintf("absent-reservation-%d", index)
|
|
if _, _, found, errGet := GetAntigravityReasoningReplayItemsWithSnapshotRequired(context.Background(), model, latestSession); errGet != nil || found {
|
|
t.Fatalf("absence reservation %d = found %v, err %v", index, found, errGet)
|
|
}
|
|
}
|
|
latestKey := antigravityReasoningReplayCacheKey(model, latestSession)
|
|
antigravityReasoningReplayMu.Lock()
|
|
entryCount := len(antigravityReasoningReplayEntries)
|
|
latestEntry, latestFound := antigravityReasoningReplayEntries[latestKey]
|
|
antigravityReasoningReplayMu.Unlock()
|
|
if entryCount > AntigravityReasoningReplayCacheMaxEntries {
|
|
t.Fatalf("local absence reservation count = %d, max %d", entryCount, AntigravityReasoningReplayCacheMaxEntries)
|
|
}
|
|
if !latestFound || !latestEntry.Deleted {
|
|
t.Fatal("latest local absence reservation was evicted")
|
|
}
|
|
}
|
|
|
|
func TestAntigravityReasoningReplayHomeWritesRemainLegacyArrayReadable(t *testing.T) {
|
|
client := newFakeAntigravityReasoningReplayKVClient()
|
|
useFakeAntigravityReasoningReplayKVClient(t, client, true)
|
|
const model, session = "gemini-3.6-flash-high", "home-legacy-readable"
|
|
item := antigravityReplayTestItem("legacy-readable-signature-123456")
|
|
if !CacheAntigravityReasoningReplayItems(model, session, [][]byte{item}) {
|
|
t.Fatal("Home write failed")
|
|
}
|
|
raw := client.values[antigravityReasoningReplayKVKey(model, session)]
|
|
var legacyItems [][]byte
|
|
if errUnmarshal := json.Unmarshal(raw, &legacyItems); errUnmarshal != nil {
|
|
t.Fatalf("new Home value is not readable as legacy [][]byte: %v", errUnmarshal)
|
|
}
|
|
if len(legacyItems) != 2 || gjson.GetBytes(legacyItems[0], "type").String() != antigravityReasoningReplayGenerationItemType || !bytes.Contains(legacyItems[1], []byte("legacy-readable-signature")) {
|
|
t.Fatalf("legacy-readable Home array malformed: %q", legacyItems)
|
|
}
|
|
}
|
|
|
|
func TestAntigravityReasoningReplayHomeReadNormalizesAndRejectsMixedInvalidChain(t *testing.T) {
|
|
client := newFakeAntigravityReasoningReplayKVClient()
|
|
useFakeAntigravityReasoningReplayKVClient(t, client, true)
|
|
const model, session = "gemini-3.6-flash-high", "home-validation"
|
|
key := antigravityReasoningReplayKVKey(model, session)
|
|
valid := []byte(`{"type":"function_call_part","name":"run","args":{"b":2,"a":1},"targetOccurrence":1,"thoughtSignature":"valid-home-signature-123456"}`)
|
|
raw, errMarshal := json.Marshal([][]byte{valid})
|
|
if errMarshal != nil {
|
|
t.Fatal(errMarshal)
|
|
}
|
|
client.values[key] = raw
|
|
items, _, found, errGet := GetAntigravityReasoningReplayItemsWithSnapshotRequired(context.Background(), model, session)
|
|
if errGet != nil || !found || len(items) != 1 {
|
|
t.Fatalf("valid Home read = %q, found=%v err=%v", items, found, errGet)
|
|
}
|
|
if !bytes.Contains(items[0], []byte(`"targetOccurrence":1`)) {
|
|
t.Fatalf("target occurrence was not normalized: %s", items[0])
|
|
}
|
|
if client.expireCount != 1 {
|
|
t.Fatalf("valid Home read expire count = %d, want 1", client.expireCount)
|
|
}
|
|
|
|
invalidRaw, errInvalidMarshal := json.Marshal([][]byte{valid, []byte(`{"type":"unknown"}`)})
|
|
if errInvalidMarshal != nil {
|
|
t.Fatal(errInvalidMarshal)
|
|
}
|
|
client.values[key] = invalidRaw
|
|
if _, _, foundInvalid, errInvalid := GetAntigravityReasoningReplayItemsWithSnapshotRequired(context.Background(), model, session); errInvalid != nil || foundInvalid {
|
|
t.Fatalf("mixed invalid Home chain = found %v, err %v; want false, nil", foundInvalid, errInvalid)
|
|
}
|
|
if client.expireCount != 1 {
|
|
t.Fatalf("invalid Home read refreshed TTL: count=%d", client.expireCount)
|
|
}
|
|
}
|