internal/approvalgate/continuation_test.go
Rama main · réplica de lectura del repositorio original.
package approvalgate
import (
"encoding/json"
"strings"
"testing"
"github.com/yeomyeonggeori/blueclaw/internal/task"
"github.com/yeomyeonggeori/blueprotocol/agentcontract"
"github.com/yeomyeonggeori/blueprotocol/holdrecord"
)
func taskRunWithHeldCall(t *testing.T) (*task.TaskRunService, string) {
t.Helper()
taskRunService := task.NewTaskRunService(task.NewTaskEventService())
taskRun := taskRunService.CreateTaskRun("person-1", "conversation-1", "내일 회의 지워줘")
taskRunService.AppendTaskEvent(taskRun.TaskRunID, agentcontract.TaskEventApprovalHoldOpened, `{"toolName":"event_delete","toolInput":{"eventHint":"내일 회의"},"confirmation":"지울까요?"}`)
return taskRunService, taskRun.TaskRunID
}
func TestAnApprovedCallIsHandedBackWithTheInputItWasApprovedWith(t *testing.T) {
taskRunService, taskRunID := taskRunWithHeldCall(t)
SettleLatest(taskRunService, taskRunID, "approve", "test")
approvedCall, isApproved := ApprovedPendingCall(taskRunService.ListTaskEvent(taskRunID))
if !isApproved || approvedCall.ToolName != "event_delete" {
t.Fatalf("expected the approved call to be found, got %+v", approvedCall)
}
if !strings.Contains(string(approvedCall.ToolInput), "내일 회의") {
t.Fatalf("expected the input the requester approved, got %q", approvedCall.ToolInput)
}
}
func TestACallThatAlreadyRanIsNotHandedBackAgain(t *testing.T) {
taskRunService, taskRunID := taskRunWithHeldCall(t)
SettleLatest(taskRunService, taskRunID, "approve", "test")
RecordApprovalSpent(taskRunService, taskRunID, "event_delete", json.RawMessage(`{"eventHint":"내일 회의"}`))
if _, isApproved := ApprovedPendingCall(taskRunService.ListTaskEvent(taskRunID)); isApproved {
t.Fatal("expected a call that already ran to stay carried out")
}
}
func TestANewHeldCallDoesNotInheritTheDecisionMadeAboutTheLastOne(t *testing.T) {
taskRunService, taskRunID := taskRunWithHeldCall(t)
SettleLatest(taskRunService, taskRunID, "approve", "test")
RecordApprovalSpent(taskRunService, taskRunID, "event_delete", json.RawMessage(`{"eventHint":"내일 회의"}`))
taskRunService.AppendTaskEvent(taskRunID, agentcontract.TaskEventApprovalHoldOpened, `{"toolName":"message_send","toolInput":{"message":"보냅니다"},"confirmation":"보낼까요?"}`)
if _, isApproved := ApprovedPendingCall(taskRunService.ListTaskEvent(taskRunID)); isApproved {
t.Fatal("expected a freshly held call to wait for its own decision")
}
}
func TestADeclinedCallIsReportedAsDeclinedRatherThanLeftSilent(t *testing.T) {
taskRunService, taskRunID := taskRunWithHeldCall(t)
SettleLatest(taskRunService, taskRunID, "reject", "test")
declinedCallNote := DeclinedCallNote(taskRunService.ListTaskEvent(taskRunID))
if !strings.Contains(declinedCallNote, "declined") {
t.Fatalf("expected the resumed turn to learn the requester said no, got %q", declinedCallNote)
}
if _, isApproved := ApprovedPendingCall(taskRunService.ListTaskEvent(taskRunID)); isApproved {
t.Fatal("expected a declined call to stay uncarried")
}
}
func TestATurnWithNothingPendingCarriesNoInstruction(t *testing.T) {
taskRunService := task.NewTaskRunService(task.NewTaskEventService())
taskRun := taskRunService.CreateTaskRun("person-1", "conversation-1", "안녕")
if declinedCallNote := DeclinedCallNote(taskRunService.ListTaskEvent(taskRun.TaskRunID)); declinedCallNote != "" {
t.Fatalf("expected an ordinary turn to be left alone, got %q", declinedCallNote)
}
}
func TestEverySurfaceRecordsTheRequesterDecisionUnderOneName(t *testing.T) {
testCases := []struct {
approvalSignal agentcontract.ApprovalSignal
expectedDecision string
}{
{agentcontract.ApprovalSignalApprove, "approve"},
{agentcontract.ApprovalSignalReject, "reject"},
}
for _, testCase := range testCases {
t.Run(string(testCase.approvalSignal), func(t *testing.T) {
taskRunService, taskRunID := taskRunWithHeldCall(t)
SettleSignal(taskRunService, taskRunID, &testCase.approvalSignal, "chat_reply")
taskEvents := taskRunService.ListTaskEvent(taskRunID)
decidedEvent := taskEvents[len(taskEvents)-1]
if decidedEvent.Name != "approval.decided" || !strings.Contains(decidedEvent.Body, `"decision":"`+testCase.expectedDecision+`"`) {
t.Fatalf("expected %q recorded as %q, got %s %s", testCase.approvalSignal, testCase.expectedDecision, decidedEvent.Name, decidedEvent.Body)
}
})
}
}
func TestAnUnclearReplyDecidesNothing(t *testing.T) {
taskRunService, taskRunID := taskRunWithHeldCall(t)
SettleSignal(taskRunService, taskRunID, nil, "chat_reply")
for _, taskEvent := range taskRunService.ListTaskEvent(taskRunID) {
if taskEvent.Name == "approval.decided" {
t.Fatalf("a reply the classifier could not read is not an answer, got %s", taskEvent.Body)
}
}
}
func eventNamesAfterDecision(t *testing.T, approvalSignal agentcontract.ApprovalSignal) []string {
t.Helper()
taskRunService, taskRunID := taskRunWithHeldCall(t)
heldEventCount := len(taskRunService.ListTaskEvent(taskRunID))
SettleSignal(taskRunService, taskRunID, &approvalSignal, "chat_reply")
names := []string{}
for _, taskEvent := range taskRunService.ListTaskEvent(taskRunID)[heldEventCount:] {
names = append(names, taskEvent.Name)
}
return names
}
func TestAnApprovalWritesOnlyTheEventsItAlwaysDid(t *testing.T) {
if names := eventNamesAfterDecision(t, agentcontract.ApprovalSignalApprove); strings.Join(names, ",") != "approval.decided" {
t.Fatalf("expected the decision alone, got %v", names)
}
if names := eventNamesAfterDecision(t, agentcontract.ApprovalSignalReject); strings.Join(names, ",") != "approval.decided" {
t.Fatalf("expected the decision alone, got %v", names)
}
}
func ApprovedPendingCall(taskEvents []agentcontract.TaskEvent) (ApprovedCall, bool) {
approvedHold, isApproved := holdrecord.LatestHold(holdrecord.Holds(taskEvents), holdrecord.StateApproved)
if !isApproved {
return ApprovedCall{}, false
}
return ApprovedCall{HoldID: approvedHold.ID, ToolName: approvedHold.Call.ToolName, ToolInput: approvedHold.Call.ApprovedInput()}, true
}