Ctrl K
ComunidadGuías…

internal/approvalgate/unanswered_ask_test.go

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

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