fix herdr launch safety and task context

This commit is contained in:
kami
2026-07-28 13:20:28 +04:00
parent 636ed8a811
commit 58793a5aa3
8 changed files with 826 additions and 74 deletions
+187
View File
@@ -0,0 +1,187 @@
package herdr
import (
"bufio"
"context"
"encoding/json"
"net"
"reflect"
"regexp"
"testing"
"time"
)
func TestHarnessStartArgs(t *testing.T) {
if got, want := harnessStartArgs("claude"), []string{}; !reflect.DeepEqual(got, want) {
t.Errorf("Claude args = %q, want %q", got, want)
}
if got := harnessStartArgs("opencode"); len(got) != 0 {
t.Errorf("OpenCode args = %q, want none", got)
}
}
func TestStartAgentPassesEmptyHarnessArgs(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = ln.Close() })
requests := make(chan Request, 2)
go func() {
for i := 0; i < 2; i++ {
conn, err := ln.Accept()
if err != nil {
return
}
var req Request
if json.NewDecoder(bufio.NewReader(conn)).Decode(&req) == nil {
requests <- req
if i == 0 {
_ = json.NewEncoder(conn).Encode(Response{ID: req.ID, Result: json.RawMessage(`{}`)})
} else {
_ = json.NewEncoder(conn).Encode(Response{ID: req.ID, Result: json.RawMessage(`{"type":"pane_info","pane":{"agent":"claude","agent_status":"idle"}}`)})
}
}
_ = conn.Close()
}
}()
c := &Client{
Path: ln.Addr().String(),
panes: map[string]string{"/worktree": "w1:p1"},
dial: func() (net.Conn, error) {
return net.Dial("tcp", ln.Addr().String())
},
}
if _, err := c.StartAgent(context.Background(), "", "/worktree", "", "opencode", "t1"); err != nil {
t.Fatal(err)
}
start := <-requests
if start.Method != "agent.start" {
t.Fatalf("first method = %q, want agent.start", start.Method)
}
params, err := json.Marshal(start.Params)
if err != nil {
t.Fatal(err)
}
var got struct {
Args []string `json:"args"`
Kind string `json:"kind"`
Name string `json:"name"`
}
if err := json.Unmarshal(params, &got); err != nil {
t.Fatal(err)
}
if want := []string{}; !reflect.DeepEqual(got.Args, want) {
t.Errorf("agent.start args = %q, want %q", got.Args, want)
}
if got.Kind != "opencode" || got.Name != "oc-t1" {
t.Errorf("agent.start kind/name = %q/%q, want opencode/oc-t1", got.Kind, got.Name)
}
if get := <-requests; get.Method != "pane.get" {
t.Errorf("second method = %q, want pane.get", get.Method)
}
}
func TestAgentNameIsBoundedAndValid(t *testing.T) {
got := agentName("OpenCode", "TASK With spaces / and symbols !!! 0123456789")
if len(got) > 32 || !regexp.MustCompile(`^[a-z0-9_-]+$`).MatchString(got) {
t.Fatalf("invalid agent name %q", got)
}
}
func TestPromptDoesNotRetryAmbiguousDelivery(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
requests := make(chan Request, 3)
go func() {
for i := 0; i < 3; i++ {
conn, err := ln.Accept()
if err != nil {
return
}
var req Request
_ = json.NewDecoder(bufio.NewReader(conn)).Decode(&req)
requests <- req
switch req.Method {
case "pane.get":
_ = json.NewEncoder(conn).Encode(Response{ID: req.ID, Result: json.RawMessage(`{"pane":{"agent_status":"idle"}}`)})
case "pane.read":
_ = json.NewEncoder(conn).Encode(Response{ID: req.ID, Result: json.RawMessage(`{"read":{"text":""}}`)})
case "agent.prompt":
_ = conn.Close() // simulate post-write response loss
}
_ = conn.Close()
}
}()
c := &Client{Path: ln.Addr().String(), agents: map[string]string{"w:p": "oc-task"}, dial: func() (net.Conn, error) { return net.Dial("tcp", ln.Addr().String()) }}
if err := c.Prompt(context.Background(), "w:p", "do work", time.Millisecond); err == nil {
t.Fatal("expected uncertain delivery error")
}
for i, want := range []string{"pane.get", "pane.read", "agent.prompt"} {
select {
case req := <-requests:
if req.Method != want {
t.Fatalf("request %d = %s, want %s", i, req.Method, want)
}
if req.Method == "agent.prompt" {
p, _ := json.Marshal(req.Params)
var got struct {
Target string `json:"target"`
}
_ = json.Unmarshal(p, &got)
if got.Target != "oc-task" {
t.Fatalf("prompt target = %q, want unique agent name", got.Target)
}
}
case <-time.After(time.Second):
t.Fatal("missing request")
}
}
}
func TestClaudeWorkspaceTrustPrompt(t *testing.T) {
if !claudeWorkspaceTrustPrompt("Accessing workspace:\n 1. Yes, I trust this folder") {
t.Fatal("exact Claude trust prompt was not recognized")
}
if claudeWorkspaceTrustPrompt("WARNING: Claude Code running in Bypass Permissions mode\n 2. Yes, I accept") {
t.Fatal("bypass-permissions disclaimer must never be accepted automatically")
}
}
func TestPaneAgentAttachedParsesPaneGetEnvelope(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = ln.Close() })
go func() {
conn, err := ln.Accept()
if err != nil {
return
}
defer conn.Close()
var req Request
if err := json.NewDecoder(bufio.NewReader(conn)).Decode(&req); err != nil {
return
}
if req.Method != "pane.get" {
t.Errorf("method = %q, want pane.get", req.Method)
return
}
_ = json.NewEncoder(conn).Encode(Response{ID: req.ID, Result: json.RawMessage(`{"type":"pane_info","pane":{"agent":"opencode","agent_status":"idle"}}`)})
}()
c := &Client{Path: ln.Addr().String(), dial: func() (net.Conn, error) {
return net.Dial("tcp", ln.Addr().String())
}}
attached, err := c.paneAgentAttached(context.Background(), "w1:p1")
if err != nil {
t.Fatal(err)
}
if !attached {
t.Fatal("pane.get envelope with an idle agent was not recognized as attached")
}
}