first commit

This commit is contained in:
2026-08-14 23:41:57 +08:00
commit 086803a8dd
471 changed files with 91938 additions and 0 deletions
@@ -0,0 +1,200 @@
package agenttool
import (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"github.com/smallnest/pigo/internal/agentcore"
)
// newTestBlackboard builds a BlackboardTool over a fresh temp dir and a helper
// to run an action and return the text result.
func newTestBlackboard(t *testing.T) (*BlackboardTool, func(args string) string) {
t.Helper()
root := t.TempDir()
tool := &BlackboardTool{Root: root}
run := func(args string) string {
t.Helper()
res, err := tool.Execute(context.Background(), "t1", json.RawMessage(args), nil)
if err != nil {
t.Fatalf("Execute: %v", err)
}
var sb strings.Builder
for _, c := range res.Content {
if txt, ok := c.(agentcore.TextContent); ok {
sb.WriteString(txt.Text)
}
}
return sb.String()
}
return tool, run
}
func TestBlackboardPostAndRead(t *testing.T) {
tool, run := newTestBlackboard(t)
got := run(`{"action":"post","file":"round-1-a.md","content":"hello from a"}`)
if !strings.Contains(got, "appended") {
t.Fatalf("post result = %q, want appended confirmation", got)
}
data, err := os.ReadFile(filepath.Join(tool.Root, "messages", "round-1-a.md"))
if err != nil {
t.Fatalf("message file not written: %v", err)
}
if string(data) != "hello from a" {
t.Fatalf("message content = %q, want %q", data, "hello from a")
}
// The snapshot lists the message; readFile returns its contents.
snap := run(`{"action":"read"}`)
if !strings.Contains(snap, "round-1-a.md") {
t.Fatalf("snapshot missing the message name:\n%s", snap)
}
if !strings.Contains(snap, "DONE") || !strings.Contains(snap, "not created") {
t.Fatalf("snapshot missing DONE status:\n%s", snap)
}
one := run(`{"action":"read","path":"messages/round-1-a.md"}`)
if !strings.Contains(one, "hello from a") {
t.Fatalf("readFile result missing content:\n%s", one)
}
}
func TestBlackboardPostRejectsTraversal(t *testing.T) {
tool, run := newTestBlackboard(t)
for _, bad := range []string{
`{"action":"post","file":"../escape.md","content":"x"}`,
`{"action":"post","file":"a/b.md","content":"x"}`,
`{"action":"post","file":"..","content":"x"}`,
`{"action":"post","file":"notes.txt","content":"x"}`,
`{"action":"post","file":"round.md","content":""}`,
} {
got := run(bad)
if strings.Contains(got, "appended") {
t.Fatalf("post with %s must be rejected, got %q", bad, got)
}
}
if _, err := os.Stat(filepath.Join(tool.Root, "escape.md")); !os.IsNotExist(err) {
t.Fatalf("traversal escaped the root: %v", err)
}
}
func TestBlackboardReadRejectsTraversal(t *testing.T) {
_, run := newTestBlackboard(t)
for _, bad := range []string{
`{"action":"read","path":"../outside.md"}`,
`{"action":"read","path":"/etc/passwd"}`,
`{"action":"read","path":"messages"}`,
} {
got := run(bad)
if !strings.Contains(got, "blackboard read:") {
t.Fatalf("read with %s must error, got %q", bad, got)
}
}
}
func TestBlackboardDoneIsExclusive(t *testing.T) {
tool, run := newTestBlackboard(t)
got := run(`{"action":"done","summary":"delivered: flag=abc"}`)
if !strings.Contains(got, "DONE marker created") {
t.Fatalf("first done failed: %q", got)
}
data, err := os.ReadFile(filepath.Join(tool.Root, "DONE"))
if err != nil {
t.Fatalf("DONE not written: %v", err)
}
if !strings.Contains(string(data), "flag=abc") {
t.Fatalf("DONE content missing summary: %q", data)
}
// A second done must report the existing marker, not overwrite it.
got2 := run(`{"action":"done","summary":"another summary"}`)
if !strings.Contains(got2, "already exists") {
t.Fatalf("second done must report existing marker, got %q", got2)
}
data2, _ := os.ReadFile(filepath.Join(tool.Root, "DONE"))
if strings.Contains(string(data2), "another summary") {
t.Fatalf("second done overwrote the marker: %q", data2)
}
// done with an empty summary is rejected.
if got := run(`{"action":"done","summary":""}`); !strings.Contains(got, "summary must not be empty") {
t.Fatalf("empty-summary done must error, got %q", got)
}
}
// TestBlackboardPostConcurrentAtomic verifies that parallel posts to the same
// message file never interleave or lose bytes: each message survives whole.
func TestBlackboardPostConcurrentAtomic(t *testing.T) {
tool, _ := newTestBlackboard(t)
const n = 16
msgs := make([]string, n)
for i := range msgs {
msgs[i] = strings.Repeat("M", 100) + string(rune('A'+i)) + strings.Repeat("N", 100)
}
var wg sync.WaitGroup
for i := 0; i < n; i++ {
i := i
wg.Add(1)
go func() {
defer wg.Done()
args := json.RawMessage(`{"action":"post","file":"round-1-a.md","content":"` + msgs[i] + `"}`)
if _, err := tool.Execute(context.Background(), "t", args, nil); err != nil {
t.Errorf("concurrent post %d: %v", i, err)
}
}()
}
wg.Wait()
data, err := os.ReadFile(filepath.Join(tool.Root, "messages", "round-1-a.md"))
if err != nil {
t.Fatalf("read messages: %v", err)
}
got := string(data)
for i, m := range msgs {
if !strings.Contains(got, m) {
t.Fatalf("message %d lost/interleaved in concurrent append:\n%s", i, got)
}
}
// Every message must appear exactly once (no duplication from re-read+write).
for _, m := range msgs {
if strings.Count(got, m) != 1 {
t.Fatalf("message %q appears %d times:\n%s", m, strings.Count(got, m), got)
}
}
}
func TestBlackboardPostSizeCap(t *testing.T) {
_, run := newTestBlackboard(t)
huge := strings.Repeat("x", maxBlackboardMessageBytes+1)
got := run(`{"action":"post","file":"big.md","content":"` + huge + `"}`)
if !strings.Contains(got, "too large") {
t.Fatalf("oversized post must error, got %q", got)
}
}
func TestBlackboardUnknownAction(t *testing.T) {
_, run := newTestBlackboard(t)
if got := run(`{"action":"bogus"}`); !strings.Contains(got, "unknown action") {
t.Fatalf("unknown action must error, got %q", got)
}
}
func TestBlackboardNoRoot(t *testing.T) {
tool := &BlackboardTool{}
res, err := tool.Execute(context.Background(), "t", json.RawMessage(`{"action":"read"}`), nil)
if err != nil {
t.Fatalf("Execute: %v", err)
}
txt := res.Content[0].(agentcore.TextContent).Text
if !strings.Contains(txt, "no blackboard root") {
t.Fatalf("no-root read must error, got %q", txt)
}
}