1package agent
2
3import (
4 "context"
5 "encoding/json"
6 "io"
7 "net/http"
8 "net/http/httptest"
9 "os"
10 "path/filepath"
11 "strings"
12 "testing"
13
14 "github.com/bnema/zut/packages/core"
15 "github.com/bnema/zut/packages/provider"
16)
17
18func TestHeadlessModesPreloadSkillPinsAndPersistOnce(t *testing.T) {
19 modes := []struct {
20 name string
21 run func(context.Context, Args, string) error
22 }{
23 {"print", runPrintMode}, {"stream", runStreamMode}, {"json", runJSONMode},
24 }
25 for _, mode := range modes {
26 t.Run(mode.name, func(t *testing.T) {
27 home, cwd := t.TempDir(), t.TempDir()
28 t.Setenv("ZUT_HOME", home)
29 t.Setenv("HOME", t.TempDir())
30 t.Setenv("ZUT_AGENT_SKILLS", "")
31 t.Setenv("ZUT_AGENT_PROFILES", "")
32 dir := filepath.Join(cwd, ".zut", "skills", "review")
33 if err := os.MkdirAll(dir, 0o700); err != nil {
34 t.Fatal(err)
35 }
36 if err := os.WriteFile(filepath.Join(dir, "SKILL.md"), []byte("---\nname: review\ndescription: Review things.\n---\nPINNED_INSTRUCTIONS\n"), 0o600); err != nil {
37 t.Fatal(err)
38 }
39 if err := toggleSkillPin(cwd, "review", true); err != nil {
40 t.Fatal(err)
41 }
42 requests := make(chan string, 3)
43 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
44 var request struct {
45 Messages []struct {
46 Role string `json:"role"`
47 Content json.RawMessage `json:"content"`
48 } `json:"messages"`
49 }
50 if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
51 t.Error(err)
52 http.Error(w, "bad request", http.StatusBadRequest)
53 return
54 }
55 for index := len(request.Messages) - 1; index >= 0; index-- {
56 if request.Messages[index].Role == "user" {
57 requests <- string(request.Messages[index].Content)
58 break
59 }
60 }
61 w.Header().Set("content-type", "text/event-stream")
62 _, _ = io.WriteString(w, "data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"done\"},\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n")
63 }))
64 defer server.Close()
65 stdin, err := os.Open(os.DevNull)
66 if err != nil {
67 t.Fatal(err)
68 }
69 oldStdin := os.Stdin
70 os.Stdin = stdin
71 t.Cleanup(func() { os.Stdin = oldStdin; _ = stdin.Close() })
72 args := Args{CWD: cwd, Provider: "openai", Model: "gpt-5", APIKey: "test-key", BaseURL: server.URL, Prompt: "first prompt", NoExt: true, WithSkills: true}
73 if _, err := captureTestStdout(t, func() error { return mode.run(context.Background(), args, "test") }); err != nil {
74 t.Fatal(err)
75 }
76 select {
77 case prompt := <-requests:
78 if !strings.Contains(prompt, "PINNED_INSTRUCTIONS") {
79 t.Fatalf("provider did not receive pinned body: %s", prompt)
80 }
81 default:
82 t.Fatal("no provider request")
83 }
84 path := core.LatestSession(home, cwd)
85 if path == "" {
86 t.Fatal("no persisted session")
87 }
88 args.Continue, args.Prompt = true, "second prompt"
89 if _, err := captureTestStdout(t, func() error { return mode.run(context.Background(), args, "test") }); err != nil {
90 t.Fatal(err)
91 }
92 select {
93 case prompt := <-requests:
94 if strings.Contains(prompt, "PINNED_INSTRUCTIONS") {
95 t.Fatalf("resumed prompt reloaded pins: %s", prompt)
96 }
97 default:
98 t.Fatal("no resumed provider request")
99 }
100 session, messages, err := core.OpenSession(path)
101 if err != nil {
102 t.Fatal(err)
103 }
104 defer session.Close()
105 count := 0
106 for _, message := range messages {
107 if message.Role != provider.RoleUser {
108 continue
109 }
110 for _, content := range message.Content {
111 if text, ok := content.(provider.TextBlock); ok {
112 count += strings.Count(text.Text, "PINNED_INSTRUCTIONS")
113 }
114 }
115 }
116 if count != 1 {
117 t.Fatalf("persisted pinned bodies=%d, want 1", count)
118 }
119 })
120 }
121}