Ctrl K
ComunidadGuías…

internal/approvalgate/continuation_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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
}