Ctrl K
ComunidadGuías…

internal/approvalgate/permission_asker_test.go

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

Archivo original ↗Preguntar a KiBot
//go:build !nobundledharness

package approvalgate

import (
	"context"
	"testing"

	"github.com/yeomyeonggeori/blueclaw/internal/mcpserver"
	"github.com/yeomyeonggeori/blueclaw/internal/task"
	"github.com/yeomyeonggeori/blueprotocol/agentcontract"
	"github.com/yeomyeonggeori/blueprotocol/holdrecord"
)

type scriptedAsker struct {
	answer     agentcontract.ApprovalSignal
	isAnswered bool
	askedCount int
}

func (asker *scriptedAsker) AskPermission(context.Context, mcpserver.ApprovalRequest, PermissionQuestion) (ApprovalAnswer, AskStatus) {
	asker.askedCount++
	return ApprovalAnswer{Signal: asker.answer}, statusOfAnswered(asker.isAnswered)
}

func statusOfAnswered(isAnswered bool) AskStatus {
	if isAnswered {
		return AskAnswered
	}
	return AskInterrupted
}

func recordedEventNames(taskRunService *task.TaskRunService, taskRunID string) []string {
	names := []string{}
	for _, taskEvent := range taskRunService.ListTaskEvent(taskRunID) {
		names = append(names, taskEvent.Name)
	}
	return names
}

func carriesEvent(names []string, wanted string) bool {
	for _, name := range names {
		if name == wanted {
			return true
		}
	}
	return false
}

func taskRunStatus(t *testing.T, taskRunService *task.TaskRunService, taskRunID string) agentcontract.TaskStatus {
	t.Helper()
	taskRun, isFound := taskRunService.FindTaskRun(taskRunID)
	if !isFound {
		t.Fatalf("no task run %s", taskRunID)
	}
	return taskRun.Status
}

func TestAnAnsweredCallRunsInsideTheTurnAndReadsTheSameOnTheLedger(t *testing.T) {
	gate, taskRunService, taskRun := gateFixture(t)
	asker := &scriptedAsker{answer: agentcontract.ApprovalSignalApprove, isAnswered: true}
	gate.UsePermissionAsker(asker)

	outcome, errorValue := gate.AwaitApproval(context.Background(), approvalRequestFixture(taskRun.TaskRunID))
	if errorValue != nil {
		t.Fatalf("await approval: %v", errorValue)
	}
	if outcome.Decision != mcpserver.ApprovalDecisionApproved {
		t.Fatalf("the answered call decided %q, expected approved", outcome.Decision)
	}
	if asker.askedCount != 1 {
		t.Fatalf("the person was asked %d times, expected once", asker.askedCount)
	}
	names := recordedEventNames(taskRunService, taskRun.TaskRunID)
	for _, wanted := range []string{
		agentcontract.TaskEventApprovalHoldOpened,
		agentcontract.TaskEventConfirmationRequested,
		agentcontract.TaskEventApprovalDecided,
		agentcontract.TaskEventApprovalHoldSpent,
	} {
		if !carriesEvent(names, wanted) {
			t.Fatalf("the ledger carries %v and not %s, so an acp-answered turn reads differently from a chat-answered one", names, wanted)
		}
	}
	if taskRunStatus(t, taskRunService, taskRun.TaskRunID) == agentcontract.TaskStatusWaitingApproval {
		t.Fatal("the run was paused for an approval that had already been answered")
	}
	if holds := holdrecord.Holds(taskRunService.ListTaskEvent(taskRun.TaskRunID)); len(holds) != 1 || outcome.HoldID != holds[0].ID {
		t.Fatalf("the approved call is named by the hold that was answered, got %q for %+v", outcome.HoldID, holds)
	}
}

func TestACallNobodyCanBeAskedAboutIsStillHeld(t *testing.T) {
	gate, taskRunService, taskRun := gateFixture(t)
	gate.UsePermissionAsker(&scriptedAsker{isAnswered: false})

	outcome, errorValue := gate.AwaitApproval(context.Background(), approvalRequestFixture(taskRun.TaskRunID))
	if errorValue != nil {
		t.Fatalf("await approval: %v", errorValue)
	}
	if outcome.Decision != mcpserver.ApprovalDecisionHeld {
		t.Fatalf("the unanswerable call decided %q, expected held", outcome.Decision)
	}
	if taskRunStatus(t, taskRunService, taskRun.TaskRunID) != agentcontract.TaskStatusWaitingApproval {
		t.Fatal("a held call left the run running, so nobody can see it is waiting")
	}
}

func TestADeclinedCallIsRejectedAndNotRecordedAsExecuted(t *testing.T) {
	gate, taskRunService, taskRun := gateFixture(t)
	gate.UsePermissionAsker(&scriptedAsker{answer: agentcontract.ApprovalSignalReject, isAnswered: true})

	outcome, errorValue := gate.AwaitApproval(context.Background(), approvalRequestFixture(taskRun.TaskRunID))
	if errorValue != nil {
		t.Fatalf("await approval: %v", errorValue)
	}
	if outcome.Decision != mcpserver.ApprovalDecisionRejected {
		t.Fatalf("the declined call decided %q, expected rejected", outcome.Decision)
	}
	if carriesEvent(recordedEventNames(taskRunService, taskRun.TaskRunID), agentcontract.TaskEventApprovalHoldSpent) {
		t.Fatal("a declined call was recorded as executed")
	}
}

func TestAGateWithNoAskerHoldsTheCallItAlwaysHeld(t *testing.T) {
	gate, taskRunService, taskRun := gateFixture(t)

	outcome, errorValue := gate.AwaitApproval(context.Background(), approvalRequestFixture(taskRun.TaskRunID))
	if errorValue != nil {
		t.Fatalf("await approval: %v", errorValue)
	}
	if outcome.Decision != mcpserver.ApprovalDecisionHeld {
		t.Fatalf("the connectors path decided %q, expected held", outcome.Decision)
	}
	if taskRunStatus(t, taskRunService, taskRun.TaskRunID) != agentcontract.TaskStatusWaitingApproval {
		t.Fatal("the connectors path stopped pausing the run it holds")
	}
}

type inspectingAsker struct {
	inspect           func()
	statusWhileAsking agentcontract.TaskStatus
	eventsWhileAsking []string
}

func (asker *inspectingAsker) AskPermission(context.Context, mcpserver.ApprovalRequest, PermissionQuestion) (ApprovalAnswer, AskStatus) {
	asker.inspect()
	return ApprovalAnswer{Signal: agentcontract.ApprovalSignalApprove}, AskAnswered
}

func TestTheRunWaitsAndTheCallIsHeldWhileTheQuestionIsStillOutstanding(t *testing.T) {
	gate, taskRunService, taskRun := gateFixture(t)
	asker := &inspectingAsker{}
	asker.inspect = func() {
		asker.statusWhileAsking = taskRunStatus(t, taskRunService, taskRun.TaskRunID)
		asker.eventsWhileAsking = recordedEventNames(taskRunService, taskRun.TaskRunID)
	}
	gate.UsePermissionAsker(asker)

	if _, errorValue := gate.AwaitApproval(context.Background(), approvalRequestFixture(taskRun.TaskRunID)); errorValue != nil {
		t.Fatalf("await approval: %v", errorValue)
	}
	if asker.statusWhileAsking != agentcontract.TaskStatusWaitingApproval {
		t.Fatalf("the run read %q while the person was being asked, so a restart here finds nothing waiting", asker.statusWhileAsking)
	}
	if !carriesEvent(asker.eventsWhileAsking, agentcontract.TaskEventApprovalHoldOpened) {
		t.Fatalf("the ledger carried %v while the person was being asked, so a restart here cannot say which call was held", asker.eventsWhileAsking)
	}
}

type questionKeepingAsker struct {
	question PermissionQuestion
}

func (asker *questionKeepingAsker) AskPermission(_ context.Context, _ mcpserver.ApprovalRequest, question PermissionQuestion) (ApprovalAnswer, AskStatus) {
	asker.question = question
	return ApprovalAnswer{Signal: agentcontract.ApprovalSignalApprove}, AskAnswered
}

func TestAHandoffTheRefusalNamesIsAskedAndKeptAsThatHandoff(t *testing.T) {
	gate, taskRunService, taskRun := gateFixture(t)
	asker := &questionKeepingAsker{}
	gate.UsePermissionAsker(asker)
	named := ApprovalTarget{InputField: "handoff", ID: "handoff-1", Title: "https://portal.example.com", HandoffID: "handoff-1"}

	if _, errorValue := gate.AwaitApproval(WithRefusalTarget(context.Background(), named), approvalRequestFixture(taskRun.TaskRunID)); errorValue != nil {
		t.Fatalf("await approval: %v", errorValue)
	}

	if asker.question.HandoffID != "handoff-1" {
		t.Fatalf("the asker must know the question is a handoff, got %+v", asker.question)
	}
	holds := holdrecord.Holds(taskRunService.ListTaskEvent(taskRun.TaskRunID))
	if len(holds) != 1 || holds[0].Call.HandoffID != "handoff-1" {
		t.Fatalf("the ledger must keep the handoff so a restart can reopen it, got %+v", holds)
	}
}