mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-09 13:31:26 -10:00
Update the Go module path, imports, install scripts, docs, workflows, packaging and action defaults from Gitlawb/zero to Twigpine/zero. The npm scope (already @twigpine/zero on main, with its @gitlawb/zero migration notes), LICENSE, action author, the OpenGateway provider and historical CHANGELOG links are unchanged. The X-AIMLAPI-Integration-Repo header keeps Gitlawb/zero: it is what AIMLAPI sees for attribution, so changing it needs a maintainer's yes. docs/INSTALL.md keeps `go install github.com/Gitlawb/zero/...`, because every tagged release so far declares that module path. The Homebrew formula comment names the Twigpine/homebrew-tap tap. Co-authored-by: Claude Opus 5.5 <[email protected]>
190 lines
5.6 KiB
Go
190 lines
5.6 KiB
Go
package tui
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/Twigpine/zero/internal/agent"
|
|
)
|
|
|
|
func TestStaticRenderCacheHitMiss(t *testing.T) {
|
|
cache := newStaticRenderCache(4, 100)
|
|
renders := 0
|
|
|
|
first := cache.render("row:1", true, func() string {
|
|
renders++
|
|
return "rendered"
|
|
})
|
|
second := cache.render("row:1", true, func() string {
|
|
renders++
|
|
return "changed"
|
|
})
|
|
|
|
if first != "rendered" || second != "rendered" {
|
|
t.Fatalf("cached values = %q, %q; want first render reused", first, second)
|
|
}
|
|
if renders != 1 {
|
|
t.Fatalf("render func called %d times, want 1", renders)
|
|
}
|
|
stats := cache.stats()
|
|
if stats.Hits != 1 || stats.Misses != 1 {
|
|
t.Fatalf("stats = %#v, want 1 hit and 1 miss", stats)
|
|
}
|
|
}
|
|
|
|
func TestStaticRenderCacheEvictsLeastRecentlyUsedEntry(t *testing.T) {
|
|
cache := newStaticRenderCache(2, 100)
|
|
|
|
cache.render("a", true, func() string { return "A" })
|
|
cache.render("b", true, func() string { return "B" })
|
|
cache.render("a", true, func() string { return "A2" })
|
|
cache.render("c", true, func() string { return "C" })
|
|
|
|
renders := 0
|
|
got := cache.render("b", true, func() string {
|
|
renders++
|
|
return "B2"
|
|
})
|
|
|
|
if got != "B2" || renders != 1 {
|
|
t.Fatalf("b render = %q with %d calls, want evicted entry to rerender", got, renders)
|
|
}
|
|
stats := cache.stats()
|
|
if stats.Evictions != 2 {
|
|
t.Fatalf("stats = %#v, want 2 evictions (c insertion, b reinsertion)", stats)
|
|
}
|
|
}
|
|
|
|
func TestStaticRenderCacheEvictsToRetainedCharacterLimit(t *testing.T) {
|
|
cache := newStaticRenderCache(10, 6)
|
|
|
|
cache.render("a", true, func() string { return "aaa" })
|
|
cache.render("b", true, func() string { return "bbb" })
|
|
cache.render("a", true, func() string { return "aaa" })
|
|
cache.render("c", true, func() string { return "cc" })
|
|
|
|
if got := cache.retainedCharacters(); got != 5 {
|
|
t.Fatalf("retained characters = %d, want 5", got)
|
|
}
|
|
|
|
renders := 0
|
|
got := cache.render("b", true, func() string {
|
|
renders++
|
|
return "bbb"
|
|
})
|
|
if got != "bbb" || renders != 1 {
|
|
t.Fatalf("b render = %q with %d calls, want retained-char eviction to rerender", got, renders)
|
|
}
|
|
if stats := cache.stats(); stats.Evictions != 2 {
|
|
t.Fatalf("stats = %#v, want 2 evictions (char-limit insertion, b reinsertion)", stats)
|
|
}
|
|
}
|
|
|
|
func TestStaticRenderCacheSkipsOversizedOutputs(t *testing.T) {
|
|
cache := newStaticRenderCache(4, 5)
|
|
renders := 0
|
|
|
|
for range 2 {
|
|
got := cache.render("huge", true, func() string {
|
|
renders++
|
|
return "123456"
|
|
})
|
|
if got != "123456" {
|
|
t.Fatalf("render = %q, want oversized value returned without caching", got)
|
|
}
|
|
}
|
|
|
|
stats := cache.stats()
|
|
if renders != 2 || stats.SkippedOversized != 2 || stats.Hits != 0 {
|
|
t.Fatalf("renders=%d stats=%#v, want rerendered oversized output with no hits", renders, stats)
|
|
}
|
|
if cache.retainedCharacters() != 0 {
|
|
t.Fatalf("retained characters = %d, want 0", cache.retainedCharacters())
|
|
}
|
|
}
|
|
|
|
func TestStaticRenderCacheSkipsUnstableRequests(t *testing.T) {
|
|
cache := newStaticRenderCache(4, 100)
|
|
renders := 0
|
|
|
|
for range 2 {
|
|
cache.render("spinner", false, func() string {
|
|
renders++
|
|
return "tick"
|
|
})
|
|
}
|
|
|
|
if renders != 2 {
|
|
t.Fatalf("render func called %d times, want 2 for unstable requests", renders)
|
|
}
|
|
if stats := cache.stats(); stats != (renderCacheStats{}) {
|
|
t.Fatalf("stats = %#v, want unstable requests to bypass cache accounting", stats)
|
|
}
|
|
}
|
|
|
|
func TestRenderRowCacheSkipsRunningToolRows(t *testing.T) {
|
|
cache := replaceDefaultRenderCache(t, newStaticRenderCache(8, 1000))
|
|
m := newModel(context.Background(), Options{})
|
|
m.pending = true
|
|
m.activeRunID = 7
|
|
row := transcriptRow{kind: rowToolCall, id: "call_1", runID: 7, tool: "grep", detail: "internal/tui"}
|
|
|
|
for range 2 {
|
|
_ = m.renderRow(row, 80, buildRowContext([]transcriptRow{row}))
|
|
}
|
|
|
|
if stats := cache.stats(); stats != (renderCacheStats{}) {
|
|
t.Fatalf("stats = %#v, want running tool rows to bypass cache", stats)
|
|
}
|
|
}
|
|
|
|
func TestRenderRowCacheSkipsPendingPermissionPromptRows(t *testing.T) {
|
|
cache := replaceDefaultRenderCache(t, newStaticRenderCache(8, 1000))
|
|
m := newModel(context.Background(), Options{})
|
|
m.pending = true
|
|
m.activeRunID = 9
|
|
row := transcriptRow{kind: rowPermission, id: "call_1", runID: 9, permission: &agent.PermissionEvent{
|
|
ToolCallID: "call_1",
|
|
ToolName: "bash",
|
|
Action: agent.PermissionActionPrompt,
|
|
}}
|
|
|
|
for range 2 {
|
|
_ = m.renderRow(row, 80, buildRowContext([]transcriptRow{row}))
|
|
}
|
|
|
|
if stats := cache.stats(); stats != (renderCacheStats{}) {
|
|
t.Fatalf("stats = %#v, want pending permission prompts to bypass cache", stats)
|
|
}
|
|
}
|
|
|
|
func TestSelectableAssistantRowsUseRenderCacheWhenSelectionInactive(t *testing.T) {
|
|
cache := replaceDefaultRenderCache(t, newStaticRenderCache(8, 1000))
|
|
m := newModel(context.Background(), Options{})
|
|
row := transcriptRow{kind: rowAssistant, text: "Done with **markdown**.", final: true, turnTools: 1}
|
|
|
|
first, firstSelectable := m.renderSelectableAssistantRow(0, row, 80, 0)
|
|
second, secondSelectable := m.renderSelectableAssistantRow(0, row, 80, 0)
|
|
|
|
if first == "" || first != second {
|
|
t.Fatalf("rendered rows = %q and %q, want stable cached output", first, second)
|
|
}
|
|
if len(firstSelectable) == 0 || len(secondSelectable) != len(firstSelectable) {
|
|
t.Fatalf("selectable metadata = %#v then %#v, want stable selectable lines", firstSelectable, secondSelectable)
|
|
}
|
|
stats := cache.stats()
|
|
if stats.Misses != 1 || stats.Hits != 1 {
|
|
t.Fatalf("cache stats = %#v, want one miss and one hit", stats)
|
|
}
|
|
}
|
|
|
|
func replaceDefaultRenderCache(t *testing.T, cache *staticRenderCache) *staticRenderCache {
|
|
t.Helper()
|
|
previous := defaultRenderCache
|
|
defaultRenderCache = cache
|
|
t.Cleanup(func() {
|
|
defaultRenderCache = previous
|
|
})
|
|
return cache
|
|
}
|