Implements US1, US2, and US3 of spec 016 by layering a marketplace service on top of existing primitives rather than reinventing them: - Capability manifests (US2) reuse the wiki subsystem. Each agent publishes a per-agent article at slug "agent-<name>" and gets versioning, revision history, and FTS search for free. - Auction channels (US1) reuse the existing auction channel type, swarm service, and task/bid store. post_auction / bid / award wrap post_task / bid_task / accept_bid and attach marketplace metadata (max_budget_tokens, domains, estimated_tokens, confidence, approach) in the task.requirements and bid.capabilities JSON blobs. Award converts the auction into a claim by DM'ing the winner at priority 8 with task_id metadata, so the existing claim/process/done lifecycle takes over with zero new machinery. - Reputation ledger (US3) adds migration 018_agent_marketplace.sql with a new agent_reputation table keyed by (agent_name, domain). mark_task_done completes the task via the swarm service and writes one ledger row per declared domain using the reported actual_tokens and success_score. query_reputation returns a rolled-up summary plus recent entries for a given (agent, domain) pair — reputation is always a vector, never a global score (FR-013). Also: - Adds the "awarded" reaction type (FR-008) alongside existing approve/ reject/in_progress/done/published. Migration 018 widens the reactions CHECK constraint via a table rebuild. - 6 new actions added to the action registry (post_auction, bid, award, mark_task_done, read_skill_card, query_reputation) so the search tool can discover them and the execute tool can dispatch them. - New internal/marketplace package (store.go + service.go). - New internal/mcp/marketplace.go bridge handlers. - New internal/mcp/marketplace_test.go covers the full auction lifecycle, capability manifest publish/read/update, self-bid rejection, non-auction channel rejection, and reputation summary aggregation. Out of scope for MVP (deferred per spec prompt): US4 reflection loop, tombstoning FR-020a/b, multi-owner quorums, auto-escalation on zero bids, bootstrap exploration credit, epsilon-greedy selection, and the hard-stop budget enforcement daemon (only soft recording of estimated vs actual is included). All existing tests pass; new marketplace tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
147 lines
3.4 KiB
Go
147 lines
3.4 KiB
Go
package actions
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestRegistryHasAllActions(t *testing.T) {
|
|
r := NewRegistry()
|
|
got := len(r.List())
|
|
const want = 41 // 35 original + 6 marketplace (spec 016)
|
|
if got != want {
|
|
t.Errorf("expected %d actions, got %d", want, got)
|
|
}
|
|
}
|
|
|
|
func TestRegistryCategories(t *testing.T) {
|
|
r := NewRegistry()
|
|
|
|
tests := []struct {
|
|
category string
|
|
want int
|
|
}{
|
|
{"messaging", 7},
|
|
{"channels", 9},
|
|
{"swarm", 5},
|
|
{"attachments", 2},
|
|
{"reactions", 4},
|
|
{"threads", 1},
|
|
{"trust", 1},
|
|
{"data", 1},
|
|
{"wiki", 5},
|
|
{"marketplace", 6},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.category, func(t *testing.T) {
|
|
got := len(r.ListByCategory(tt.category))
|
|
if got != tt.want {
|
|
t.Errorf("category %q: expected %d actions, got %d", tt.category, tt.want, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRegistryGetByName(t *testing.T) {
|
|
r := NewRegistry()
|
|
|
|
allNames := []string{
|
|
// messaging
|
|
"my_status", "send_message", "read_inbox", "claim_messages", "mark_done", "search_messages", "discover_agents",
|
|
// channels
|
|
"create_channel", "join_channel", "leave_channel", "list_channels",
|
|
"invite_to_channel", "kick_from_channel", "get_channel_messages",
|
|
"send_channel_message", "update_channel",
|
|
// swarm
|
|
"post_task", "bid_task", "accept_bid", "complete_task", "list_tasks",
|
|
// attachments
|
|
"upload_attachment", "download_attachment",
|
|
// reactions
|
|
"react", "unreact", "get_reactions", "list_by_state",
|
|
// threads
|
|
"get_replies",
|
|
// trust
|
|
"get_trust",
|
|
// data
|
|
"query",
|
|
// wiki
|
|
"create_article", "get_article", "update_article", "list_articles", "get_backlinks",
|
|
// marketplace (spec 016)
|
|
"post_auction", "bid", "award", "mark_task_done", "read_skill_card", "query_reputation",
|
|
}
|
|
|
|
for _, name := range allNames {
|
|
t.Run(name, func(t *testing.T) {
|
|
a, ok := r.Get(name)
|
|
if !ok {
|
|
t.Fatalf("action %q not found in registry", name)
|
|
}
|
|
if a.Name != name {
|
|
t.Errorf("expected name %q, got %q", name, a.Name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRegistryGetNotFound(t *testing.T) {
|
|
r := NewRegistry()
|
|
_, ok := r.Get("nonexistent_action")
|
|
if ok {
|
|
t.Error("expected Get to return false for nonexistent action")
|
|
}
|
|
}
|
|
|
|
func TestRegistryActionsHaveExamples(t *testing.T) {
|
|
r := NewRegistry()
|
|
for _, a := range r.List() {
|
|
t.Run(a.Name, func(t *testing.T) {
|
|
if len(a.Examples) == 0 {
|
|
t.Errorf("action %q has no examples", a.Name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRegistryActionsHaveDescriptions(t *testing.T) {
|
|
r := NewRegistry()
|
|
for _, a := range r.List() {
|
|
t.Run(a.Name, func(t *testing.T) {
|
|
if a.Description == "" {
|
|
t.Errorf("action %q has empty description", a.Name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRegistryActionsHaveReturns(t *testing.T) {
|
|
r := NewRegistry()
|
|
for _, a := range r.List() {
|
|
t.Run(a.Name, func(t *testing.T) {
|
|
if a.Returns == "" {
|
|
t.Errorf("action %q has empty Returns field", a.Name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRegistryListByUnknownCategory(t *testing.T) {
|
|
r := NewRegistry()
|
|
got := r.ListByCategory("nonexistent")
|
|
if len(got) != 0 {
|
|
t.Errorf("expected 0 actions for unknown category, got %d", len(got))
|
|
}
|
|
}
|
|
|
|
func TestRegistryListReturnsCopy(t *testing.T) {
|
|
r := NewRegistry()
|
|
list1 := r.List()
|
|
list2 := r.List()
|
|
// Mutating the first list should not affect the second.
|
|
if len(list1) > 0 {
|
|
list1[0].Name = "mutated"
|
|
if list2[0].Name == "mutated" {
|
|
t.Error("List() should return a copy, not a reference to internal slice")
|
|
}
|
|
}
|
|
}
|