// relay_discovery_test.go: relay dispatch tests for the fs/list + fs/grep agent // discovery handlers (mirrors TestRelay_AgentFsRequest_Echoed). package acp_test import ( "encoding/json" "errors" "io" "os" "path/filepath" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/yan1h/agent-coding-workflow/internal/acp" ) func relayRoundTrip(t *testing.T, rig *relayTestRig, id int64, method string, params map[string]any) *acp.Message { t.Helper() agentDec := acp.NewDecoder(rig.agentReader, 0) agentEnc := acp.NewEncoder(rig.agentWriter) req, err := acp.NewRequestInt(id, method, params) require.NoError(t, err) require.NoError(t, agentEnc.Encode(req)) respCh := make(chan *acp.Message, 1) errCh := make(chan error, 1) go func() { m, derr := agentDec.DecodeMessage() if derr != nil { errCh <- derr return } respCh <- m }() select { case resp := <-respCh: return resp case err := <-errCh: if errors.Is(err, io.EOF) { t.Fatal("agent saw EOF before relay responded") } t.Fatalf("agent decoder error: %v", err) case <-time.After(3 * time.Second): t.Fatalf("relay did not respond to %s within 3s", method) } return nil } func TestRelay_AgentFsList_Dispatched(t *testing.T) { t.Parallel() rig := newRelayTestRig(t) cancel, doneCh := rig.runRelay(t) t.Cleanup(func() { rig.shutdown(t, cancel, doneCh) }) require.NoError(t, os.WriteFile(filepath.Join(rig.sess.CwdPath, "f.txt"), []byte("hi"), 0o644)) resp := relayRoundTrip(t, rig, 201, "fs/list", map[string]any{"path": ".", "sessionId": "a"}) require.True(t, resp.IsResponse()) require.Nil(t, resp.Error) var out struct { Entries []struct { Name string `json:"name"` } `json:"entries"` } require.NoError(t, json.Unmarshal(resp.Result, &out)) require.Len(t, out.Entries, 1) assert.Equal(t, "f.txt", out.Entries[0].Name) } func TestRelay_AgentFsGrep_Dispatched(t *testing.T) { t.Parallel() rig := newRelayTestRig(t) cancel, doneCh := rig.runRelay(t) t.Cleanup(func() { rig.shutdown(t, cancel, doneCh) }) require.NoError(t, os.WriteFile(filepath.Join(rig.sess.CwdPath, "code.go"), []byte("package x\nfunc Needle() {}\n"), 0o644)) resp := relayRoundTrip(t, rig, 202, "fs/grep", map[string]any{"pattern": "Needle", "sessionId": "a"}) require.True(t, resp.IsResponse()) require.Nil(t, resp.Error) var out struct { Matches []struct { File string `json:"file"` Line int `json:"line"` } `json:"matches"` } require.NoError(t, json.Unmarshal(resp.Result, &out)) require.Len(t, out.Matches, 1) assert.Equal(t, "code.go", out.Matches[0].File) assert.Equal(t, 2, out.Matches[0].Line) } func TestRelay_AgentFsGrep_PathEscapeRejected(t *testing.T) { t.Parallel() rig := newRelayTestRig(t) cancel, doneCh := rig.runRelay(t) t.Cleanup(func() { rig.shutdown(t, cancel, doneCh) }) outside := filepath.Join(filepath.VolumeName(rig.sess.CwdPath)+string(filepath.Separator), "etc") resp := relayRoundTrip(t, rig, 203, "fs/grep", map[string]any{"pattern": "x", "path": outside, "sessionId": "a"}) require.True(t, resp.IsResponse()) require.NotNil(t, resp.Error) assert.Equal(t, -32001, resp.Error.Code) }