internal/approvalgate/unanswered_ask_test.go
Rama main · réplica de lectura del repositorio original.
//go:build !nobundledharness
package approvalgate
import (
"context"
"strings"
"testing"
"github.com/yeomyeonggeori/blueclaw/internal/mcpserver"
"github.com/yeomyeonggeori/blueprotocol/agentcontract"
"github.com/yeomyeonggeori/blueprotocol/holdrecord"
)
type endingAsker struct{ status AskStatus }
func (asker endingAsker) AskPermission(context.Context, mcpserver.ApprovalRequest, PermissionQuestion) (ApprovalAnswer, AskStatus) {
return ApprovalAnswer{}, asker.status
}
func TestARequesterWhoCannotBeAskedRefusesTheCallLoudlyAndNeverParksTheRun(t *testing.T) {
gate, taskRunService, taskRun := gateFixture(t)
gate.UsePermissionAsker(endingAsker{status: AskUnreachable})
outcome, errorValue := gate.AwaitApproval(context.Background(), approvalRequestFixture(taskRun.TaskRunID))
if errorValue != nil || outcome.Decision != mcpserver.ApprovalDecisionUnanswerable {
t.Fatalf("a requester nobody can reach must see the call refused, got %q %v", outcome.Decision, errorValue)
}
if !carriesEvent(recordedEventNames(taskRunService, taskRun.TaskRunID), TaskEventApprovalUnreachable) {
t.Fatal("the refusal must leave an event naming why")
}
if status := taskRunStatus(t, taskRunService, taskRun.TaskRunID); status == agentcontract.TaskStatusWaitingApproval {
t.Fatalf("a call that can never be answered must not park the run, got %q", status)
}
if holds := holdrecord.Holds(taskRunService.ListTaskEvent(taskRun.TaskRunID)); len(holds) != 1 || holds[0].State != holdrecord.StateRejected {
t.Fatalf("the hold that could not be put to anyone must not stay pending, got %+v", holds)
}
}
func TestAGateWithNoAskerRefusesTheCallInsteadOfHoldingIt(t *testing.T) {
gate, taskRunService, taskRun := gateFixture(t)
gate.UsePermissionAsker(nil)
outcome, _ := gate.AwaitApproval(context.Background(), approvalRequestFixture(taskRun.TaskRunID))
if outcome.Decision != mcpserver.ApprovalDecisionUnanswerable {
t.Fatalf("with nobody to ask the call is refused, got %q", outcome.Decision)
}
if !carriesEvent(recordedEventNames(taskRunService, taskRun.TaskRunID), TaskEventApprovalUnreachable) {
t.Fatal("the refusal must leave an event naming why")
}
if len(holdrecord.Holds(taskRunService.ListTaskEvent(taskRun.TaskRunID))) != 0 {
t.Fatal("no hold is opened for a question nobody can be asked")
}
}
func TestAnExpiredQuestionRejectsTheHoldAndTellsTheRunWhy(t *testing.T) {
gate, taskRunService, taskRun := gateFixture(t)
gate.UsePermissionAsker(endingAsker{status: AskExpired})
outcome, _ := gate.AwaitApproval(context.Background(), approvalRequestFixture(taskRun.TaskRunID))
if outcome.Decision != mcpserver.ApprovalDecisionRejected || !strings.Contains(outcome.Notice, "24 hours") {
t.Fatalf("an unanswered question is rejected and says why, got %q %q", outcome.Decision, outcome.Notice)
}
if holds := holdrecord.Holds(taskRunService.ListTaskEvent(taskRun.TaskRunID)); len(holds) != 1 || holds[0].State != holdrecord.StateRejected {
t.Fatalf("an expired hold is rejected, got %+v", holds)
}
if !carriesEvent(recordedEventNames(taskRunService, taskRun.TaskRunID), TaskEventApprovalExpired) {
t.Fatal("the expiry must leave an event")
}
if status := taskRunStatus(t, taskRunService, taskRun.TaskRunID); status == agentcontract.TaskStatusWaitingApproval {
t.Fatalf("the run is told and continues, got %q", status)
}
}
func TestAnInterruptedQuestionLeavesTheHoldPendingForTheRestartToAwait(t *testing.T) {
gate, taskRunService, taskRun := gateFixture(t)
gate.AwaitApproval(context.Background(), approvalRequestFixture(taskRun.TaskRunID))
if holds := holdrecord.Holds(taskRunService.ListTaskEvent(taskRun.TaskRunID)); len(holds) != 1 || holds[0].State != holdrecord.StatePending {
t.Fatalf("an interrupted ask keeps its hold pending, got %+v", holds)
}
}