First Commit
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
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
detections / detections (push) Canceled after 0s
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
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
detections / detections (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"iter"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Autumn-27/artex/db"
|
||||
"github.com/Autumn-27/artex/sidequestion"
|
||||
"github.com/Autumn-27/norma/llm"
|
||||
"github.com/Autumn-27/norma/transcript"
|
||||
)
|
||||
|
||||
type sideAgentProvider struct {
|
||||
calls int
|
||||
path string
|
||||
}
|
||||
|
||||
func (p *sideAgentProvider) Stream(ctx context.Context, req llm.CompletionRequest) iter.Seq2[llm.StreamEvent, error] {
|
||||
return func(y func(llm.StreamEvent, error) bool) {
|
||||
msg, stop, _, err := p.Complete(ctx, req)
|
||||
if err != nil {
|
||||
y(llm.StreamEvent{}, err)
|
||||
return
|
||||
}
|
||||
for _, b := range msg.Content {
|
||||
if b.Type == llm.BlockToolUse {
|
||||
if !y(llm.StreamEvent{Type: llm.SEToolUseStart, ToolID: b.ID, ToolName: b.Name}, nil) {
|
||||
return
|
||||
}
|
||||
if !y(llm.StreamEvent{Type: llm.SEToolInputJSON, Text: string(b.Input)}, nil) {
|
||||
return
|
||||
}
|
||||
} else if !y(llm.StreamEvent{Type: llm.SETextDelta, Text: b.Text}, nil) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if !y(llm.StreamEvent{Type: llm.SEMessageDelta, StopReason: stop}, nil) {
|
||||
return
|
||||
}
|
||||
y(llm.StreamEvent{Type: llm.SEMessageStop}, nil)
|
||||
}
|
||||
}
|
||||
func (p *sideAgentProvider) Complete(_ context.Context, req llm.CompletionRequest) (llm.Message, string, llm.Usage, error) {
|
||||
p.calls++
|
||||
if p.calls == 1 {
|
||||
input, _ := json.Marshal(map[string]string{"file_path": p.path})
|
||||
return llm.Message{Role: llm.RoleAssistant, Content: []llm.ContentBlock{{Type: llm.BlockToolUse, ID: "fixture-read", Name: "Read", Input: input}}}, "tool_use", llm.Usage{}, nil
|
||||
}
|
||||
return llm.Message{Role: llm.RoleAssistant, Content: []llm.ContentBlock{llm.TextBlock("main finished")}}, "end_turn", llm.Usage{}, nil
|
||||
}
|
||||
|
||||
func TestSideActualChatCheckpointToolResultAndTranscriptIsolation(t *testing.T) {
|
||||
for _, streaming := range []bool{false, true} {
|
||||
t.Run(map[bool]string{true: "stream", false: "atomic"}[streaming], func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "asset.txt")
|
||||
if err := os.WriteFile(file, []byte("controlled-homepage-result"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p := &sideAgentProvider{path: file}
|
||||
bound := sidequestion.Bind(p, sidequestion.Model{Model: "fixture", Streaming: streaming})
|
||||
store := transcript.NewStore(filepath.Join(dir, "transcripts"))
|
||||
chat := NewChatAgent(bound, "fixture", dir, store, 100000)
|
||||
chat.SetNonStreaming(func() bool { return !streaming })
|
||||
var snapshots []sidequestion.Snapshot
|
||||
var activities []db.Activity
|
||||
ctx := sidequestion.WithPublisher(t.Context(), func(s sidequestion.Snapshot) { snapshots = append(snapshots, s) })
|
||||
if _, err := chat.Chat(ctx, "mainagent", "conv-987654", "Read the local fixture", 5, time.Minute, false, func(a db.Activity) { activities = append(activities, a) }); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(snapshots) < 3 {
|
||||
t.Fatalf("actual Chat missed capture hooks: %d", len(snapshots))
|
||||
}
|
||||
last := snapshots[len(snapshots)-1]
|
||||
raw, _ := json.Marshal(last)
|
||||
if last.Parent.ConversationID != 987654 || !strings.Contains(string(raw), "controlled-homepage-result") || !strings.Contains(string(raw), "main finished") {
|
||||
t.Fatalf("missing real tool result/final reply: %s", raw)
|
||||
}
|
||||
before, err := os.ReadFile(store.MainPath("conv-987654"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
count := len(activities)
|
||||
tools := 0
|
||||
for _, a := range activities {
|
||||
if a.Kind == "tool_use" {
|
||||
tools++
|
||||
}
|
||||
}
|
||||
if tools != 1 {
|
||||
t.Fatalf("main fixture tool executions: %d", tools)
|
||||
}
|
||||
req, err := sidequestion.BuildRequest(last, nil, "side-only-question")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Reset the fake to request another Read; the side executor cannot run it.
|
||||
p.calls = 0
|
||||
p.path = filepath.Join(dir, "nonexistent")
|
||||
answer, err := (sidequestion.SideQuestionService{Provider: bound}).Answer(t.Context(), req, streaming, nil)
|
||||
if err != nil || !answer.ToolUse || p.calls != 1 || !strings.Contains(answer.Text, "도구 작업을 실행할 수 없습니다") {
|
||||
t.Fatalf("tool denial %+v %v", answer, err)
|
||||
}
|
||||
after, err := os.ReadFile(store.MainPath("conv-987654"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(before) != string(after) || len(activities) != count {
|
||||
t.Fatal("side question modified main transcript/activity")
|
||||
}
|
||||
if len(snapshots) == 0 || snapshots[len(snapshots)-1].Version != last.Version {
|
||||
t.Fatal("side request replaced main checkpoint")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user