Files
zero/internal/tui/command_polish_test.go
Anandan f401c66e81 polish: cut per-turn overhead, stabilize cache, add /new (#478)
* polish: cut per-turn overhead, stabilize cache, add /new

Context economy
- Compaction: preserveLast 8->6, triggerRatio 0.8->0.7 (both copies in sync).
- System prompt: kill the "ELABORATE summary" directive; scale summary to
  the work. Comment discipline moved into the core prompt for all model
  families (was Anthropic-only); Minimax/DeepSeek/Qwen/etc. now get it.
- Token-budget regression test ratchets the fixed per-turn overhead (base
  prompt + eager tool schemas) using the runtime's own estimator, so silent
  prompt/schema growth fails CI.

Response quality
- defaultResponseStyle balanced->concise. Reversible via /style.

Tool discovery
- <available_skills> block in the system prompt lists installed skills
  (name + one-line description) so the model loads the right one on the
  first call instead of guessing and reading the failure. Budget-capped at
  640 bytes with an "...and N more" fallback; renders nothing when no
  skills are installed (byte-identical to prior prompt).

Session lifecycle
- /new starts a fresh session in place; previous session stays on disk and
  is named in the confirmation for /resume. Guards against mid-run reset.
- /clear now notes that context persists and points to /new.

Performance & caching
- partitionTools no longer alpha-sorts the whole tools array every turn,
  which inserted a loaded deferred tool mid-array and shifted the cached
  prefix on every load. It now builds append-only regions
  [eager sorted] + [tool_search] + [loaded deferred sorted] so a
  mid-session load grows the tail instead of busting the eager prefix.
  Regression test asserts the eager block precedes any loaded tool.
- /context now shows cache hit rate (flags low rate over >5 turns as a
  possible unstable prefix) and rolling average turn latency with
  time-to-first-token. Both reset on /new.

Measured baselines (auto-mode, no workspace): base prompt 3162 tokens,
eager tool schemas 2433 across 12 tools. Budget ceilings set ~10% above.

Deliberately deferred (need a decision): inverted safety-policy rewrite
(sandbox doesn't catch ~half the B-tier rules), core-tool/web_fetch
deferral (activation-semantics blocker), aggressive prompt slim.

* fix(tui): clear staged attachments + queued message on /new

CodeRabbit: startNewSession reset conversation/usage/latency state but not
the staged input (pendingImages, pendingImageLabels, pendingDocuments,
queuedMessage), which is only consumed at prompt-submit. Without clearing it,
the fresh session's first prompt silently inherited the previous session's
images/documents/queued text. Test now stages all four and asserts they clear.
2026-07-04 20:56:52 +08:00

405 lines
11 KiB
Go

package tui
import (
"context"
"errors"
"path/filepath"
"strings"
"testing"
tea "charm.land/bubbletea/v2"
"github.com/Gitlawb/zero/internal/agent"
"github.com/Gitlawb/zero/internal/config"
"github.com/Gitlawb/zero/internal/sandbox"
"github.com/Gitlawb/zero/internal/tools"
)
func TestHelpCommandRendersGroupedSections(t *testing.T) {
m := newModel(context.Background(), Options{})
m.input.SetValue("/help")
updated, cmd := m.Update(testKey(tea.KeyEnter))
next := updated.(model)
if cmd != nil {
t.Fatal("expected /help to be handled without starting an agent run")
}
text := transcriptText(next.transcript)
for _, want := range []string{
"Commands",
"Model",
"Session",
"Runtime",
"Tools",
"Meta",
" /model [list|id]",
" /permissions",
"hint: submit plain text to ask Zero",
} {
assertContains(t, text, want)
}
assertNotContains(t, text, "Commands:\n/provider")
}
func TestProviderAndConfigCommandsUseStableStatusOutput(t *testing.T) {
m := newModel(context.Background(), Options{
ProviderName: "openai",
ModelName: "gpt-4.1",
ProviderProfile: config.ProviderProfile{
Name: "openai",
ProviderKind: config.ProviderKindOpenAI,
BaseURL: config.OpenAIBaseURL,
APIKey: "sk-sensitive",
Model: "gpt-4.1",
},
AgentOptions: agent.Options{MaxTurns: 42},
RecapsEnabled: true,
})
m.input.SetValue("/provider status")
updated, cmd := m.Update(testKey(tea.KeyEnter))
next := updated.(model)
if cmd != nil {
t.Fatal("expected /provider to be handled without starting an agent run")
}
providerText := transcriptText(next.transcript)
for _, want := range []string{"Provider", "status: ok", "provider: openai", "model: gpt-4.1", "api key: set"} {
assertContains(t, providerText, want)
}
assertNotContains(t, providerText, "sk-sensitive")
next.input.SetValue("/config")
updated, cmd = next.Update(testKey(tea.KeyEnter))
next = updated.(model)
if cmd != nil {
t.Fatal("expected /config to be handled without starting an agent run")
}
configText := transcriptText(next.transcript)
for _, want := range []string{"Config", "status: ok", "runtime: go", "max turns: 42", "permission mode:", "recaps: on"} {
assertContains(t, configText, want)
}
assertNotContains(t, configText, "sk-sensitive")
}
func TestProviderCommandRedactsCredentialBearingBaseURL(t *testing.T) {
m := newModel(context.Background(), Options{
ProviderName: "openai",
ModelName: "gpt-4.1",
ProviderProfile: config.ProviderProfile{
Name: "openai",
ProviderKind: config.ProviderKindOpenAI,
BaseURL: "https://user:[email protected]/v1?api_key=query-secret",
APIKey: "query-secret",
Model: "gpt-4.1",
},
})
m.input.SetValue("/provider status")
updated, cmd := m.Update(testKey(tea.KeyEnter))
next := updated.(model)
if cmd != nil {
t.Fatal("expected /provider to be handled without starting an agent run")
}
text := transcriptText(next.transcript)
for _, unwanted := range []string{"super-secret", "query-secret", "user:super-secret@"} {
assertNotContains(t, text, unwanted)
}
assertContains(t, text, "base url: https://proxy.local/v1?api_key=[REDACTED]")
}
func TestToolsCommandRendersCommandCard(t *testing.T) {
m := newModel(context.Background(), Options{
Registry: tools.NewRegistry(),
})
m.input.SetValue("/tools")
updated, cmd := m.Update(testKey(tea.KeyEnter))
next := updated.(model)
if cmd != nil {
t.Fatal("expected /tools to be handled without starting an agent run")
}
emptyText := transcriptText(next.transcript)
for _, want := range []string{
"Tools",
"0 registered | no tools available",
"Registry",
"registered 0",
"actions: /mcp manage servers | /permissions manage access",
} {
assertContains(t, emptyText, want)
}
assertNotContains(t, emptyText, "status: warning")
assertNotContains(t, emptyText, "registered tools:")
registry := tools.NewRegistry()
registry.Register(tools.NewReadFileTool("."))
m = newModel(context.Background(), Options{
Registry: registry,
})
m.input.SetValue("/tools")
updated, cmd = m.Update(testKey(tea.KeyEnter))
next = updated.(model)
if cmd != nil {
t.Fatal("expected /tools to be handled without starting an agent run")
}
toolsText := transcriptText(next.transcript)
for _, want := range []string{
"Tools",
"1 registered | registered catalog",
"Registry",
"registered 1",
"Available",
"- read_file",
"actions: /mcp manage servers | /permissions manage access",
} {
assertContains(t, toolsText, want)
}
assertNotContains(t, toolsText, "status: ok")
assertNotContains(t, toolsText, "registered tools:")
}
func TestToolsCommandCardHandlesNilRegistry(t *testing.T) {
text := model{}.toolsText()
for _, want := range []string{
"Tools",
"0 registered | no tools available",
"registered 0",
} {
assertContains(t, text, want)
}
}
func TestToolsCommandShowsFullSortedCatalog(t *testing.T) {
registry := tools.NewRegistry()
for _, name := range []string{
"write_file",
"read_file",
"grep",
"glob",
"edit_file",
"apply_patch",
"bash",
"web_search",
"web_fetch",
} {
registry.Register(commandTestMCPTool{name: name, description: name + " tool"})
}
m := newModel(context.Background(), Options{
Registry: registry,
})
m.input.SetValue("/tools")
updated, cmd := m.Update(testKey(tea.KeyEnter))
next := updated.(model)
if cmd != nil {
t.Fatal("expected /tools to be handled without starting an agent run")
}
text := transcriptText(next.transcript)
for _, want := range []string{
"9 registered | registered catalog",
"- apply_patch",
"- bash",
"- edit_file",
"- glob",
"- grep",
"- read_file",
"- web_fetch",
"- web_search",
"- write_file",
} {
assertContains(t, text, want)
}
assertNotContains(t, text, "... 1 more")
previous := -1
for _, want := range []string{
"- apply_patch",
"- bash",
"- edit_file",
"- glob",
"- grep",
"- read_file",
"- web_fetch",
"- web_search",
"- write_file",
} {
current := strings.Index(text, want)
if current < 0 {
t.Fatalf("expected tools output to contain %q, got:\n%s", want, text)
}
if current <= previous {
t.Fatalf("expected tools output to keep sorted order at %q, got:\n%s", want, text)
}
previous = current
}
}
func TestContextAndPermissionsCommandsRenderProductState(t *testing.T) {
registry := tools.NewRegistry()
registry.Register(tools.NewReadFileTool("."))
store, err := sandbox.NewGrantStore(sandbox.StoreOptions{FilePath: filepath.Join(t.TempDir(), "sandbox-grants.json")})
if err != nil {
t.Fatalf("NewGrantStore returned error: %v", err)
}
if _, err := store.Grant(sandbox.GrantInput{
ToolName: "bash",
Decision: sandbox.GrantAllow,
Reason: "sk-proj-sensitive approved shell",
}); err != nil {
t.Fatalf("Grant returned error: %v", err)
}
m := newModel(context.Background(), Options{
Cwd: `D:\codings\Opensource\Zero`,
ProviderName: "openai",
ModelName: "gpt-4.1",
Registry: registry,
SandboxStore: store,
PermissionMode: agent.PermissionModeAsk,
})
m.input.SetValue("/context")
updated, cmd := m.Update(testKey(tea.KeyEnter))
next := updated.(model)
if cmd != nil {
t.Fatal("expected /context to be handled without starting an agent run")
}
contextText := transcriptText(next.transcript)
for _, want := range []string{
"Context",
"go runtime | ask permissions | 1 tool",
"Runtime",
"cwd D:\\codings\\Opensource\\Zero",
"provider openai",
"model gpt-4.1",
"Session",
"active none",
"compaction idle",
"Tools",
"registered 1",
"actions: /permissions manage access | /tools inspect catalog",
} {
assertContains(t, contextText, want)
}
assertNotContains(t, contextText, "status: ok")
assertNotContains(t, contextText, "permission mode:")
next.input.SetValue("/permissions")
updated, cmd = next.Update(testKey(tea.KeyEnter))
next = updated.(model)
if cmd != nil {
t.Fatal("expected /permissions to be handled without starting an agent run")
}
permissionText := transcriptText(next.transcript)
for _, want := range []string{
"Permissions",
"ask permissions",
"1 persistent grant",
"State",
"mode ask",
"Grants",
"bash [allow]",
"[REDACTED]",
} {
assertContains(t, permissionText, want)
}
assertNotContains(t, permissionText, "sk-proj-sensitive")
assertNotContains(t, permissionText, "status: ok")
assertNotContains(t, permissionText, "Permission mode:")
}
// stubGrantStore is a minimal test double for sandbox.GrantStore. It only
// implements List, which is enough to exercise the error path of
// permissionsText().
type stubGrantStore struct {
listErr error
}
func (store *stubGrantStore) List() ([]sandbox.Grant, error) {
return nil, store.listErr
}
func TestPermissionsCommandCardHandlesNilStoreAndEmptyGrants(t *testing.T) {
nilStore := model{permissionMode: agent.PermissionModeAuto}.permissionsText()
for _, want := range []string{
"Permissions",
"auto permissions",
"grants unavailable",
"mode auto",
"persistent grants: unavailable",
} {
assertContains(t, nilStore, want)
}
assertNotContains(t, nilStore, "status: warning")
store, err := sandbox.NewGrantStore(sandbox.StoreOptions{FilePath: filepath.Join(t.TempDir(), "empty-grants.json")})
if err != nil {
t.Fatalf("NewGrantStore returned error: %v", err)
}
emptyText := model{permissionMode: agent.PermissionModeAsk, sandboxStore: store}.permissionsText()
for _, want := range []string{
"Permissions",
"ask permissions",
"no persistent grants",
"mode ask",
"none",
} {
assertContains(t, emptyText, want)
}
assertNotContains(t, emptyText, "status: info")
errStore := &stubGrantStore{listErr: errors.New("storage failure")}
errText := model{permissionMode: agent.PermissionModeAsk}.permissionsTextWithStore(errStore)
for _, want := range []string{
"Permissions",
"ask permissions",
"grants error",
"mode ask",
"error: storage failure",
} {
assertContains(t, errText, want)
}
assertNotContains(t, errText, "status: blocked")
}
func TestContextCommandCardHandlesNilRegistryAndStableStyle(t *testing.T) {
text := model{}.contextText()
for _, want := range []string{
"Context",
"0 tools",
"style concise",
"root unknown",
} {
assertContains(t, text, want)
}
}
func TestCompactCommandAvoidsShellOnlyPlaceholder(t *testing.T) {
m := newModel(context.Background(), Options{})
m.input.SetValue("/compact")
updated, cmd := m.Update(testKey(tea.KeyEnter))
next := updated.(model)
if cmd != nil {
t.Fatal("expected /compact to be handled without starting an agent run")
}
text := transcriptText(next.transcript)
for _, want := range []string{"Compact", "status: warning", "requested: yes", "visible transcript rows:"} {
assertContains(t, text, want)
}
if strings.Contains(text, "not wired") || strings.Contains(text, "future compaction backend") {
t.Fatalf("compact output should describe product state, got %q", text)
}
}