Files
DeepSeek-Reasonix/internal/frontend/tui/queue_slash_test.go
T
yhh 07d2d4bdd0 Show the cache hit rate after the cold start, and the output ratio
The session's first executor request pays for the prefix every later one
reuses, so the session-wide hit rate mixes "did the cache work once warm"
with "how much a cold start could reuse". usagereport already records the
cold start, but nothing a live session reads projected it.

The live figures come from the serve ledger, which restarts on /new,
/resume and /fork; usageTally does not, so its ColdStart can belong to an
earlier conversation in the same process. The broadcaster therefore takes
the cold start from the ledger's own population: the first request the
ledger files under the executor (pricing.LedgerSource, the rule Add
already used), split by the same CacheSplit the ledger applies, and reset
with it. SessionUsage carries it as coldStart.

- /status (TUI): "warm cache" row, ledger hit rate with the cold start
  taken out of hit, miss and unknown; "out/in" row, output ÷ input.
- Desktop session overview behind the cache meter: the same two figures,
  computed in state/overview beside cacheRate.
- wire-parity: ColdStartUsage mirrored in port/session.ts.
2026-11-07 09:06:37 +00:00

383 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package tui
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"slices"
"strings"
"sync"
"testing"
tea "charm.land/bubbletea/v2"
"github.com/charmbracelet/x/ansi"
)
// queueKernel answers the routes the local commands read and records every
// request; anything else is a 204.
type queueKernel struct {
mu sync.Mutex
calls []string
answers map[string]any
}
func (k *queueKernel) ServeHTTP(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
k.mu.Lock()
k.calls = append(k.calls, r.Method+" "+r.URL.Path+" "+strings.TrimSpace(string(body)))
k.mu.Unlock()
if v, ok := k.answers[r.Method+" "+r.URL.Path]; ok {
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(v)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (k *queueKernel) seen() string {
k.mu.Lock()
defer k.mu.Unlock()
return strings.Join(k.calls, "\n")
}
func queueModel(t *testing.T, answers map[string]any) (*model, *queueKernel) {
t.Helper()
t.Setenv("REASONIX_DISABLE_MOUSE", "0")
k := &queueKernel{answers: answers}
srv := httptest.NewServer(k)
t.Cleanup(srv.Close)
m := newModel(context.Background(), Options{Client: &Client{HTTP: srv.Client(), Base: srv.URL}})
closed := make(chan Update)
close(closed)
m.updates = closed
m.Update(tea.WindowSizeMsg{Width: 80, Height: 24})
return m, k
}
func queueLastNotice(m *model) string {
for _, it := range slices.Backward(m.tr.Items) {
if it.Kind == ItemNotice {
return it.Text
}
}
return ""
}
func queueSend(m *model, line string) {
m.composer.SetValue(line)
run(m, press(m, "enter"))
}
// While a turn runs, plain text becomes a queued follow-up; the three queue
// commands are answered by the kernel instead and leave no pending row.
func TestQueueCommandsAreNotQueuedAsProse(t *testing.T) {
m, k := queueModel(t, map[string]any{
"POST /inbox/items": map[string]string{"itemId": "abcdef1234567890", "disposition": "steer_accepted"},
"GET /inbox": map[string]any{"revision": 4, "items": []map[string]any{
{"id": "11112222333", "intent": "followup", "state": "queued", "preview": "write tests"},
}},
})
m.tr.Running = true
queueSend(m, "/steer use the small fix")
if got := queueLastNotice(m); got != "steer accepted #abcdef12" {
t.Fatalf("steer notice = %q", got)
}
if !strings.Contains(k.seen(), `POST /inbox/items {"input":"use the small fix","intent":"steer"}`) {
t.Fatalf("steer did not reach the inbox as a steer:\n%s", k.seen())
}
queueSend(m, "/queue")
if got := queueLastNotice(m); got != "inbox rev=4 items=1\n 1. [followup/queued] write tests #11112222" {
t.Fatalf("queue notice = %q", got)
}
for _, it := range m.tr.Items {
if it.Pending {
t.Fatalf("a local command left a pending queued row: %+v", it)
}
}
if strings.Count(k.seen(), "POST /inbox/items") != 1 {
t.Fatalf("/queue was sent as a follow-up:\n%s", k.seen())
}
}
func TestSteerWithoutTextShowsUsage(t *testing.T) {
m, k := queueModel(t, nil)
queueSend(m, "/steer")
if got := queueLastNotice(m); got != "usage: /steer <guidance>" || strings.Contains(k.seen(), "/inbox") {
t.Fatalf("notice = %q, calls:\n%s", got, k.seen())
}
}
func TestTakeoverNeedsATargetAndNeverStealsAHeldSession(t *testing.T) {
m, k := queueModel(t, map[string]any{
"GET /sessions": []map[string]any{
{"path": "/s/a.jsonl", "current": true}, {"path": "/s/b.jsonl"},
},
})
queueSend(m, "/takeover")
if got := queueLastNotice(m); !strings.HasPrefix(got, "takeover: no refused session") {
t.Fatalf("no-arg notice = %q", got)
}
queueSend(m, "/takeover 9")
if got := queueLastNotice(m); !strings.Contains(got, "1–2") {
t.Fatalf("out-of-range notice = %q", got)
}
queueSend(m, "/takeover 2")
if !strings.Contains(k.seen(), `POST /resume {"path":"/s/b.jsonl"}`) {
t.Fatalf("takeover did not resume the indexed session:\n%s", k.seen())
}
if !strings.Contains(queueLastNotice(m), "not taken over") {
t.Fatalf("notice = %q", queueLastNotice(m))
}
}
func TestStatusReportsTheFieldsTheFooterCompresses(t *testing.T) {
m, _ := queueModel(t, map[string]any{
"GET /status": map[string]any{
"label": "deepseek-chat", "toolApprovalMode": "auto", "effort": "high",
"used": 50000, "window": 100000, "cacheHit": 90, "cacheMiss": 10,
"jobs": []map[string]any{{"id": "j1"}, {"id": "j2"}},
},
})
queueSend(m, "/status")
got := queueLastNotice(m)
for _, want := range []string{"Session status", "mode Auto", "model deepseek-chat",
"context 50.0K / 100.0K ctx (50%)", "effort effort high", "cache ", "jobs ⚙ 2", "config "} {
if !strings.Contains(got, want) {
t.Fatalf("status lacks %q:\n%s", want, got)
}
}
}
// Both rows read the ledger /status sends: the warm rate leaves the cold start
// out of hit and of the split prompt, and the ratio is output over input.
func TestStatusReportsTheWarmCacheRateAndTheOutputRatio(t *testing.T) {
m, _ := queueModel(t, map[string]any{
"GET /status": map[string]any{
"label": "deepseek-chat", "cacheHit": 0, "cacheMiss": 0,
"sessionUsage": map[string]any{
"promptTokens": 20000, "completionTokens": 500, "cacheHit": 13000, "cacheMiss": 7000, "cacheUnknown": 2000,
"coldStart": map[string]any{"promptTokens": 5000, "cacheHit": 1000, "cacheMiss": 4000},
},
},
})
queueSend(m, "/status")
got := queueLastNotice(m)
for _, want := range []string{"warm cache 92.31% after the cold start", "out/in 2.5% (out 500 / in 20.0K)"} {
if !strings.Contains(got, want) {
t.Fatalf("status lacks %q:\n%s", want, got)
}
}
}
func TestStatusOwesNoWarmRateUntilARequestFollowsTheColdStart(t *testing.T) {
m, _ := queueModel(t, map[string]any{
"GET /status": map[string]any{
"label": "deepseek-chat",
"sessionUsage": map[string]any{"promptTokens": 5000, "cacheHit": 1000, "cacheMiss": 4000, "coldStart": map[string]any{"promptTokens": 5000, "cacheHit": 1000, "cacheMiss": 4000}},
},
})
queueSend(m, "/status")
if got := queueLastNotice(m); strings.Contains(got, "warm cache") {
t.Fatalf("a session of one request has no warm rate:\n%s", got)
}
}
// The kernel owns where the file goes; this screen only reports it.
func TestExportAsksTheKernelAndReportsItsPath(t *testing.T) {
m, k := queueModel(t, map[string]any{
"POST /sessions/export": map[string]any{"path": "/ws/session-1.md", "messages": 3},
})
queueSend(m, "/export")
if got := queueLastNotice(m); got != "session exported to /ws/session-1.md" {
t.Fatalf("notice = %q", got)
}
if strings.Contains(k.seen(), "GET /history") {
t.Fatalf("export read the history itself:\n%s", k.seen())
}
}
func TestExportOfAnEmptyConversationSaysSo(t *testing.T) {
m, _ := queueModel(t, map[string]any{"POST /sessions/export": map[string]any{"path": "", "messages": 0}})
queueSend(m, "/export")
if got := queueLastNotice(m); got != "no messages to export" {
t.Fatalf("notice = %q", got)
}
}
// Counting never crosses the last thing the person said, and 1 is the newest.
func TestCopyTakesTheNewestAssistantTextOfTheLatestTurn(t *testing.T) {
t.Setenv("SSH_CONNECTION", "1")
history := []HistoryMessage{
{Role: "user", Content: "old"}, {Role: "assistant", Content: "old answer"},
{Role: "user", Content: "now"}, {Role: "assistant", Content: "first"},
{Role: "assistant", Content: "..."}, {Role: "tool", Content: "out"},
{Role: "user", Content: "host", HostAuthored: true}, {Role: "assistant", Content: "second"},
}
if got := copyParts(history); len(got) != 2 || got[0] != "first" || got[1] != "second" {
t.Fatalf("parts = %q", got)
}
m, _ := queueModel(t, map[string]any{"GET /history": history})
queueSend(m, "/copy 2")
if got := queueLastNotice(m); got != "copied response to clipboard" {
t.Fatalf("notice = %q", got)
}
queueSend(m, "/copy 3")
if got := queueLastNotice(m); got != "no assistant response to copy" {
t.Fatalf("out-of-range notice = %q", got)
}
}
func TestCopyPickerChoosesWithTheArrowKeys(t *testing.T) {
t.Setenv("SSH_CONNECTION", "1")
m, _ := queueModel(t, map[string]any{"GET /history": []HistoryMessage{
{Role: "user", Content: "q"}, {Role: "assistant", Content: "alpha"}, {Role: "assistant", Content: "beta"},
}})
queueSend(m, "/copy")
if m.copying == nil || len(m.copying.parts) != 2 || m.copying.parts[0] != "beta" {
t.Fatalf("picker = %+v", m.copying)
}
if v := m.View().Content; !strings.Contains(v, "pick a response to copy") {
t.Fatalf("picker not drawn:\n%s", v)
}
m.Update(tea.KeyPressMsg{Code: tea.KeyDown})
run(m, press(m, "enter"))
if m.copying != nil || queueLastNotice(m) != "copied response to clipboard" {
t.Fatalf("picker = %+v, notice = %q", m.copying, queueLastNotice(m))
}
}
func TestLocalCommandsAreOfferedInTheMenu(t *testing.T) {
m, _ := queueModel(t, nil)
var labels []string
for _, c := range m.localCommands("/") {
labels = append(labels, c.Label)
}
got := strings.Join(labels, " ")
for _, want := range []string{"/queue", "/steer", "/takeover", "/status", "/export", "/copy"} {
if !strings.Contains(got, want) {
t.Fatalf("menu lacks %s: %s", want, got)
}
}
}
func TestQuitIsOfferedInTheMenuWhenTheHostDeclaresIt(t *testing.T) {
m, _ := queueModel(t, nil)
m.opts.QuitCommands = []string{"/quit", "/exit"}
var labels []string
for _, c := range m.localCommands("/qu") {
labels = append(labels, c.Label)
}
if got := strings.Join(labels, " "); got != "/queue /quit" {
t.Fatalf("/qu offers %q", got)
}
m.opts.QuitCommands = nil
for _, c := range m.localCommands("/qu") {
if c.Label == "/quit" {
t.Fatal("/quit offered although the host declares no quit command")
}
}
}
func TestQueueRefsRefuseWhatIsOutOfRangeOrAmbiguous(t *testing.T) {
snap := InboxSnapshot{Items: []InboxItem{{ID: "5aaa"}, {ID: "5bbb"}, {ID: "7ccc"}}}
for _, ref := range []string{"4", "0", "5", "9zz"} {
if id, err := resolveQueueRef(snap, []string{ref}); err == nil {
t.Fatalf("ref %q resolved to %q", ref, id)
}
}
if id, err := resolveQueueRef(snap, []string{"3"}); err != nil || id != "7ccc" {
t.Fatalf("a position resolves by position: %q, %v", id, err)
}
if id, err := resolveQueueRef(snap, []string{"5a"}); err != nil || id != "5aaa" {
t.Fatalf("unique prefix = %q, %v", id, err)
}
}
func TestUsageLevelComesFromTheCommandNotItsWording(t *testing.T) {
if got := queueCommand(context.Background(), nil, []string{"bogus"}); got.level != "warn" {
t.Fatalf("usage level = %q", got.level)
}
if got := steerCommand(context.Background(), nil, ""); got.level != "warn" {
t.Fatalf("steer usage level = %q", got.level)
}
}
// 1.x lists the work tree and an unset effort in /status too; the footer
// shows both, so the report must not drop what it compresses.
func TestStatusCarriesTheWorkTreeAndTheDefaultEffort(t *testing.T) {
m, _ := queueModel(t, map[string]any{
"GET /status": map[string]any{
"label": "deepseek-chat", "modelRef": "deepseek/deepseek-chat", "toolApprovalMode": "ask",
},
"GET /workspace/git": map[string]any{
"repo": true, "name": "proj", "branch": "main", "added": 2, "removed": 1, "untracked": 3,
},
})
queueSend(m, "/status")
got := ansi.Strip(queueLastNotice(m))
for _, want := range []string{"model deepseek/deepseek-chat", "effort effort auto", "git proj@main +2 -1 ?3"} {
if !strings.Contains(got, want) {
t.Fatalf("status lacks %q:\n%s", want, got)
}
}
}
// "/rename <n> <title>" names the nth saved session, as 1.x does; without a
// number the kernel renames the open one.
func TestRenameByIndexTitlesTheNthSavedSession(t *testing.T) {
m, k := queueModel(t, map[string]any{
"GET /sessions": []map[string]any{{"name": "a", "path": "/s/a.jsonl", "current": true}, {"name": "b", "path": "/s/b.jsonl"}},
})
queueSend(m, "/rename 2 Release notes")
if !strings.Contains(k.seen(), `POST /sessions/rename {"id":"b","title":"Release notes"}`) {
t.Fatalf("rename did not target the second session:\n%s", k.seen())
}
if got := queueLastNotice(m); got != `session renamed to "Release notes"` {
t.Fatalf("notice = %q", got)
}
queueSend(m, "/rename 9 x")
if got := queueLastNotice(m); !strings.Contains(got, "1–2") {
t.Fatalf("out-of-range notice = %q", got)
}
}
// "/resume <n>" continues the nth saved session in place, as 1.x does; a bare
// /resume still opens the picker.
func TestResumeByIndexContinuesTheNthSavedSession(t *testing.T) {
m, k := queueModel(t, map[string]any{
"GET /sessions": []map[string]any{{"name": "a", "path": "/s/a.jsonl", "current": true}, {"name": "b", "path": "/s/b.jsonl"}},
})
queueSend(m, "/resume 2")
if !strings.Contains(k.seen(), `POST /resume {"path":"/s/b.jsonl"}`) {
t.Fatalf("resume did not target the second session:\n%s", k.seen())
}
if m.picker != nil {
t.Fatal("an indexed /resume must not open the picker")
}
queueSend(m, "/resume 1")
if got := queueLastNotice(m); got != "already in that session" {
t.Fatalf("current-session notice = %q", got)
}
queueSend(m, "/resume 9")
if got := queueLastNotice(m); !strings.Contains(got, "1–2") {
t.Fatalf("out-of-range notice = %q", got)
}
m.tr.Running = true
queueSend(m, "/resume 2")
m.tr.Running = false
if got := queueLastNotice(m); got != "finish or cancel the current turn before resuming" {
t.Fatalf("mid-turn notice = %q", got)
}
if n := strings.Count(k.seen(), "POST /resume"); n != 1 {
t.Fatalf("refused indexes still reached the kernel (%d resumes):\n%s", n, k.seen())
}
queueSend(m, "/resume")
if m.picker == nil {
t.Fatal("a bare /resume must open the picker")
}
}