Why: the adjudicated preview plan needs server-side OG fetching for link cards, and the proposal's reserved-capability items call for the fs tool family to exist behind a default-off gate (implemented, not offered). Evidence: GET /api/url-preview fetches and parses og:title/description/ site_name/image/favicon with a 5s timeout, 256KB body cap, text/html content check, TTL cache, and SSRF guards enforced at dial time (loopback /private/link-local/multicast/reserved/CGNAT rejected per hop, anti DNS- rebind; AS_URL_PREVIEW_ALLOW_PRIVATE=1 exists for local testing only, default fail-closed; errors return sanitized cards, never internal addresses). fs_read_file/fs_write_file/fs_list_dir register as Hidden tools sandboxed under <root>/<owner>/<conversationId>: traversal, absolute paths, backslashes, and symlink escape rejected; 1MB/2MB size caps; writes register an artifact so workspace files stay visible in the panel. ToolDesc.Hidden gates both model visibility (runctx.Tools) and /api/tools listing on runtime.fsTools, and registry.Execute re-checks fail-closed. ToolExecCtx gains OwnerID/WorkspaceRoot/SettingBool. go test ./... green (fs negative matrix, SSRF negatives, endpoint fail-closed, cache). Impact: link previews work end to end once the frontend wires the fetcher; fs tools are inert until runtime.fsTools is enabled. Refs: task feature/file-preview-caps; core 4d29301.
76 lines
2.4 KiB
Go
76 lines
2.4 KiB
Go
package tools
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
core "agent-system-core"
|
|
)
|
|
|
|
type dummyCtx struct{ project string }
|
|
|
|
func (dummyCtx) ProjectID() string { return "" }
|
|
func (dummyCtx) ConversationID() string { return "" }
|
|
func (dummyCtx) Retrieve(q core.RetrievalQuery) ([]core.RetrievalHit, error) { return nil, nil }
|
|
func (dummyCtx) WriteArtifact(name, kind, mime string, content []byte) (core.Artifact, error) {
|
|
return core.Artifact{}, nil
|
|
}
|
|
func (dummyCtx) PeerConversations() ([]core.Conversation, error) { return nil, nil }
|
|
func (dummyCtx) PeerConversationMessages(id string, limit int) ([]core.Message, error) {
|
|
return nil, nil
|
|
}
|
|
func (dummyCtx) OwnerID() string { return "o" }
|
|
func (dummyCtx) WorkspaceRoot() string { return "" }
|
|
func (dummyCtx) SettingBool(string, bool) bool { return false }
|
|
|
|
// TestHTTPExecutorTemplateAndPath:模板变量注入、结果路径裁剪、错误状态。
|
|
func TestHTTPExecutorTemplateAndPath(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/translate" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{"data":[{"translated":"hello","src":"你好"}],"meta":{"n":1}}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
spec := core.HTTPToolSpec{
|
|
Method: "POST", URL: srv.URL + "/translate",
|
|
BodyTemplate: `{"q":"{{param.text}}"}`, ResultPath: "data.0.translated",
|
|
}
|
|
exec := HTTPExecutor(spec, nil)
|
|
out, err := exec(dummyCtx{}, []byte(`{"text":"你好"}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if out != `"hello"` && out != "hello" {
|
|
t.Errorf("result: %q", out)
|
|
}
|
|
|
|
// ResultPath 不存在 → 显式错误。
|
|
specBad := spec
|
|
specBad.ResultPath = "missing.path"
|
|
if _, err := HTTPExecutor(specBad, nil)(dummyCtx{}, []byte(`{}`)); err == nil {
|
|
t.Error("missing path should error")
|
|
}
|
|
|
|
// 404 → 错误带状态码。
|
|
spec404 := spec
|
|
spec404.URL = srv.URL + "/nope"
|
|
spec404.ResultPath = ""
|
|
if _, err := HTTPExecutor(spec404, nil)(dummyCtx{}, nil); err == nil {
|
|
t.Error("404 should error")
|
|
}
|
|
}
|
|
|
|
func TestExpandTemplate(t *testing.T) {
|
|
got := expandTemplate(`{{param.a}}/{{param.b.c}}/{{param.unknown}}`, map[string]any{
|
|
"a": "x", "b": map[string]any{"c": "y"},
|
|
})
|
|
if got != "x/y/" {
|
|
t.Errorf("expand: %q", got)
|
|
}
|
|
}
|