1package modes
2
3import (
4 "context"
5 "strings"
6 "sync"
7 "testing"
8
9 "github.com/bnema/zut/packages/agent/skills"
10 "github.com/bnema/zut/packages/core"
11 "github.com/bnema/zut/packages/provider"
12 "github.com/bnema/zut/packages/tui"
13)
14
15func newPinnedRecallInteractive(t *testing.T) *Interactive {
16 t.Helper()
17 return NewInteractive(InteractiveConfig{
18 Agent: core.NewAgent(nil, "test-model", "", nil), CWD: t.TempDir(),
19 LoadSkillPins: func(string) (skills.Pins, error) { return skills.Pins{Global: []string{"review"}}, nil },
20 SkillSnapshot: reviewSnapshot, PreloadPinnedSkills: true,
21 })
22}
23
24func TestPinnedQueuedDraftRecallKeepsEditorRawAndRearms(t *testing.T) {
25 for name, key := range map[string]tui.Key{"alt-up": {Kind: tui.KeyUp, Alt: true}, "escape": {Kind: tui.KeyEsc}} {
26 t.Run(name, func(t *testing.T) {
27 i := newPinnedRecallInteractive(t)
28 expanded := i.consumePinnedSkills("user draft")
29 i.agent.QueuePrompt(core.QueuedMessage{Text: expanded})
30 i.mu.Lock()
31 i.busy = true
32 i.cancelTurn = func() {}
33 i.mu.Unlock()
34 i.handleKey(context.Background(), key)
35 if text := i.ed.Value(); text != "user draft" {
36 t.Fatalf("recalled editor text=%q, want raw draft", text)
37 }
38 if next := i.consumePinnedSkills(i.ed.Value()); strings.Count(next, "Inspect carefully.") != 1 {
39 t.Fatalf("recall lost or doubled pins: %q", next)
40 }
41 })
42 }
43}
44
45func TestDiscardPinnedQueuedDraftRearmsNextUser(t *testing.T) {
46 for _, preserveHost := range []bool{false, true} {
47 i := newPinnedRecallInteractive(t)
48 expanded := i.consumePinnedSkills("discarded draft")
49 i.agent.QueuePrompt(core.QueuedMessage{Text: expanded})
50 i.agent.QueuePrompt(core.QueuedMessage{Text: "host report", HostEvent: true})
51 i.mu.Lock()
52 i.discardQueuedMessagesLocked(preserveHost)
53 i.mu.Unlock()
54 if next := i.consumePinnedSkills("new draft"); strings.Count(next, "Inspect carefully.") != 1 || !strings.HasSuffix(next, "new draft") {
55 t.Fatalf("discard lost pins: %q", next)
56 }
57 }
58}
59
60func TestCtrlCClearsPinnedQueueWithoutLosingPreload(t *testing.T) {
61 i := newPinnedRecallInteractive(t)
62 expanded := i.consumePinnedSkills("discarded draft")
63 i.agent.QueuePrompt(core.QueuedMessage{Text: expanded})
64 i.handleKey(context.Background(), tui.Key{Kind: tui.KeyCtrlC})
65 if next := i.consumePinnedSkills("next draft"); strings.Count(next, "Inspect carefully.") != 1 {
66 t.Fatalf("clearing queue lost pins: %q", next)
67 }
68}
69
70func TestPinnedRecallAndHistorySynchronizeWithHostArming(t *testing.T) {
71 i := newPinnedRecallInteractive(t)
72 expanded := i.consumePinnedSkills("user draft")
73 i.agent.SetMessages([]provider.Message{{Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: expanded}}}})
74 var wg sync.WaitGroup
75 wg.Add(1)
76 go func() {
77 defer wg.Done()
78 for range 100 {
79 i.SetPinnedSkillsPending(true)
80 }
81 }()
82 for range 100 {
83 i.restoreQueuedMessageToEditor(core.QueuedMessage{Text: expanded})
84 i.inputHistory()
85 }
86 wg.Wait()
87}
88
89func TestPinnedInputHistoryShowsRawUserText(t *testing.T) {
90 i := newPinnedRecallInteractive(t)
91 expanded := i.consumePinnedSkills("user draft")
92 i.agent.SetMessages([]provider.Message{{Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: expanded}}}})
93 if history := i.inputHistory(); len(history) != 1 || history[0] != "user draft" {
94 t.Fatalf("input history contains injected bodies: %#v", history)
95 }
96 if got := i.skillPins.rawPrompt("Pinned skills for this conversation.\nuser-written text"); !strings.Contains(got, "user-written text") {
97 t.Fatal("stripped unrelated user-written preamble")
98 }
99}