Files
artex/agent/deferred_test.go
dela 0335d572de
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
detections / detections (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
First Commit
2026-10-09 08:38:16 +08:00

90 lines
3.0 KiB
Go

package agent
import (
"encoding/json"
"strings"
"testing"
"github.com/Autumn-27/norma/llm"
actool "github.com/Autumn-27/norma/tool"
)
func TestDeferredSystem_NoGlobal(t *testing.T) {
// No global MCP names → plain single-segment system, no cache boundary.
sys, boundary := deferredSystem("SYS", DeferredInfo{})
if len(sys) != 1 || sys[0] != "SYS" || boundary != 0 {
t.Fatalf("expected [SYS],0 — got %v,%d", sys, boundary)
}
}
func TestDeferredSystem_WithGlobal(t *testing.T) {
def := DeferredInfo{
Deferred: []string{"mcp__browser__navigate", "mcp__browser__click"},
GlobalNames: []string{"mcp__browser__navigate", "mcp__browser__click"},
}
sys, boundary := deferredSystem("SYS", def)
if len(sys) != 2 || sys[0] != "SYS" {
t.Fatalf("expected [SYS, block], got %v", sys)
}
if !strings.Contains(sys[1], "<available-deferred-tools>") ||
!strings.Contains(sys[1], "mcp__browser__navigate") {
t.Fatalf("block missing names:\n%s", sys[1])
}
if boundary != len(sys) {
t.Fatalf("boundary=%d want %d (whole prompt cached)", boundary, len(sys))
}
}
func TestDeferredSystem_GatedNotInBlock(t *testing.T) {
// A skill-gated server's tools are deferred but NOT in the global block.
def := DeferredInfo{
Deferred: []string{"mcp__browser__navigate", "mcp__secret__do"},
GlobalNames: []string{"mcp__browser__navigate"}, // secret gated → excluded
}
sys, _ := deferredSystem("SYS", def)
if strings.Contains(sys[1], "mcp__secret__do") {
t.Fatalf("gated tool must not appear in global block:\n%s", sys[1])
}
if !strings.Contains(sys[1], "mcp__browser__navigate") {
t.Fatal("global tool should appear in block")
}
}
func TestSeedUnlockFromHistory(t *testing.T) {
skillCall := func(name string) llm.ContentBlock {
return llm.ContentBlock{Type: llm.BlockToolUse, Name: "Skill", Input: json.RawMessage(`{"name":"` + name + `"}`)}
}
msgs := []llm.Message{
{Role: llm.RoleAssistant, Content: []llm.ContentBlock{skillCall("browsing")}},
{Role: llm.RoleAssistant, Content: []llm.ContentBlock{
{Type: llm.BlockToolUse, Name: "Bash", Input: json.RawMessage(`{}`)}, // ignored
skillCall("recon"),
}},
}
var got []string
seedUnlockFromHistory(msgs, func(name string) { got = append(got, name) })
if strings.Join(got, ",") != "browsing,recon" {
t.Fatalf("unlocked=%v want [browsing recon]", got)
}
seedUnlockFromHistory(msgs, nil) // nil → no-op, no panic
}
// TestUnlockGatingFlow mirrors what OnInvoke / seedUnlockFromHistory do: a gated
// tool starts locked and becomes callable only after its skill unlocks it.
func TestUnlockGatingFlow(t *testing.T) {
serverTools := map[string][]string{"secret": {"mcp__secret__do"}}
unlock := actool.NewUnlockSet("mcp__browser__navigate") // global only
unlockSkill := func(name string) {
if name == "unlock-secret" {
unlock.Add(serverTools["secret"]...)
}
}
if unlock.Has("mcp__secret__do") {
t.Fatal("gated tool should start locked")
}
unlockSkill("unlock-secret")
if !unlock.Has("mcp__secret__do") {
t.Fatal("gated tool should be unlocked after skill load")
}
}