iop/apps/edge/internal/service/single_request_test.go
toki d7a150c7fe feat(agent): 단일 요청 실행 경로를 완성한다
Claude의 단일 Anthropic 요청 안에서 IOP가 Plan, Work, Review와 workspace 도구 실행을 끝내고 실제 dev smoke로 계약을 검증할 수 있어야 한다.\n\n완료 task evidence와 마일스톤 검토 상태도 같은 변경에 고정한다.
2026-08-08 23:35:13 +09:00

668 lines
25 KiB
Go

package service
import (
"context"
"errors"
"sync"
"testing"
"time"
)
type channelFakeExecutor struct {
fn func(context.Context, SingleRequestRequest, SingleRequestController) error
}
func (f *channelFakeExecutor) ExecuteSingleRequest(ctx context.Context, req SingleRequestRequest, ctrl SingleRequestController) error {
if f.fn != nil {
return f.fn(ctx, req, ctrl)
}
return nil
}
func createTestBinding(t *testing.T) *SingleRequestBinding {
t.Helper()
binding, err := NewSingleRequestBinding(
"test-model", "workspace-ref-123",
SingleRequestStageBinding{Model: "gemini-3.6-flash", Options: map[string]any{"reasoning_effort": "high"}},
SingleRequestStageBinding{Model: "ornith-fast"},
SingleRequestStageBinding{Model: "gemini-3.6-flash", Options: map[string]any{"reasoning_effort": "high"}},
SingleRequestLimits{WallClockMS: 60000, StageTimeoutMS: 10000, MaxToolIterations: 10, MaxOutputBytes: 1048576},
)
if err != nil {
t.Fatalf("NewSingleRequestBinding: %v", err)
}
return binding
}
func testEnvelope(requestID string, sequence uint64, stage SingleRequestState) SingleRequestEnvelope {
return SingleRequestEnvelope{RequestID: requestID, Sequence: sequence, Stage: stage}
}
func submitToFinalizing(req SingleRequestRequest, ctrl SingleRequestController, result *SingleRequestResult) error {
for sequence, stage := range []SingleRequestState{
SingleRequestStatePlanning,
SingleRequestStateWorking,
SingleRequestStateReviewing,
SingleRequestStateFinalizing,
} {
env := testEnvelope(req.RequestID, uint64(sequence+1), stage)
if stage == SingleRequestStateFinalizing {
env.Result = result
}
if err := ctrl.SubmitEnvelope(env); err != nil {
return err
}
}
return nil
}
func waitForState(t *testing.T, handle SingleRequestExecution, want SingleRequestState) {
t.Helper()
deadline := time.After(2 * time.Second)
ticker := time.NewTicker(time.Millisecond)
defer ticker.Stop()
for {
if handle.State() == want {
return
}
select {
case <-deadline:
t.Fatalf("state=%s, want %s", handle.State(), want)
case <-ticker.C:
}
}
}
func waitForExecution(t *testing.T, handle SingleRequestExecution) (SingleRequestResult, error) {
t.Helper()
type outcome struct {
result SingleRequestResult
err error
}
done := make(chan outcome, 1)
go func() {
result, err := handle.Wait()
done <- outcome{result: result, err: err}
}()
select {
case outcome := <-done:
return outcome.result, outcome.err
case <-time.After(2 * time.Second):
t.Fatal("Wait did not return")
return SingleRequestResult{}, nil
}
}
func startTestExecution(t *testing.T, executor SingleRequestExecutor) SingleRequestExecution {
t.Helper()
handle, err := startSingleRequest(context.Background(), executor, SingleRequestRequest{
RequestID: "request-test",
Binding: createTestBinding(t),
Prompt: "complete the private task",
})
if err != nil {
t.Fatalf("StartSingleRequest: %v", err)
}
return handle
}
func TestSingleRequestExecutorUnavailable(t *testing.T) {
_, err := (&Service{}).StartSingleRequest(context.Background(), SingleRequestRequest{
RequestID: "request-unavailable",
Binding: createTestBinding(t),
})
if !errors.Is(err, ErrSingleRequestExecutorUnavailable) {
t.Fatalf("error=%v, want ErrSingleRequestExecutorUnavailable", err)
}
}
func TestSingleRequestExecutorCannotMutateAdmission(t *testing.T) {
result := &SingleRequestResult{Output: "accepted result"}
executor := &channelFakeExecutor{fn: func(_ context.Context, req SingleRequestRequest, ctrl SingleRequestController) error {
req.Binding.Plan.Options["reasoning_effort"] = "low"
ctrl.Binding().Review.Options["reasoning_effort"] = "low"
if err := submitToFinalizing(req, ctrl, result); err != nil {
return err
}
result.Output = "executor-mutated result"
return nil
}}
callerBinding := createTestBinding(t)
handle, err := startSingleRequest(context.Background(), executor, SingleRequestRequest{
RequestID: "request-test",
Binding: callerBinding,
Prompt: "complete the private task",
})
if err != nil {
t.Fatalf("StartSingleRequest: %v", err)
}
callerBinding.Plan.Options["reasoning_effort"] = "caller-mutated"
waitForState(t, handle, SingleRequestStateFinalizing)
if got := handle.Binding().Plan.Options["reasoning_effort"]; got != "high" {
t.Fatalf("executor mutated retained binding: %v", got)
}
if err := handle.AcknowledgeTerminal(true); err != nil {
t.Fatalf("AcknowledgeTerminal: %v", err)
}
got, err := waitForExecution(t, handle)
if err != nil || got.Output != "accepted result" {
t.Fatalf("Wait=(%q, %v), want accepted immutable result", got.Output, err)
}
}
func TestSingleRequestRejectsExecutorCompletedEnvelope(t *testing.T) {
executor := &channelFakeExecutor{fn: func(_ context.Context, req SingleRequestRequest, ctrl SingleRequestController) error {
if err := submitToFinalizing(req, ctrl, &SingleRequestResult{Output: "candidate"}); err != nil {
return err
}
return ctrl.SubmitEnvelope(testEnvelope(req.RequestID, 5, SingleRequestStateCompleted))
}}
handle := startTestExecution(t, executor)
_, err := waitForExecution(t, handle)
if !errors.Is(err, ErrSingleRequestInvalidState) {
t.Fatalf("Wait error=%v, want invalid state", err)
}
if got := handle.State(); got != SingleRequestStateFailed {
t.Fatalf("state=%s, want failed", got)
}
}
func TestSingleRequestExecutorExitFailsClosed(t *testing.T) {
for name, executor := range map[string]SingleRequestExecutor{
"no envelopes": &channelFakeExecutor{},
"mid-stage": &channelFakeExecutor{fn: func(_ context.Context, req SingleRequestRequest, ctrl SingleRequestController) error {
return ctrl.SubmitEnvelope(testEnvelope(req.RequestID, 1, SingleRequestStatePlanning))
}},
} {
t.Run(name, func(t *testing.T) {
handle := startTestExecution(t, executor)
_, err := waitForExecution(t, handle)
if !errors.Is(err, ErrSingleRequestFailed) {
t.Fatalf("Wait error=%v, want ErrSingleRequestFailed", err)
}
if got := handle.State(); got != SingleRequestStateFailed {
t.Fatalf("state=%s, want failed", got)
}
})
}
}
func TestSingleRequestEnvelopeOrderingFailsClosed(t *testing.T) {
tests := map[string]func(SingleRequestRequest, SingleRequestController) error{
"missing sequence": func(req SingleRequestRequest, ctrl SingleRequestController) error {
return ctrl.SubmitEnvelope(SingleRequestEnvelope{RequestID: req.RequestID, Stage: SingleRequestStatePlanning})
},
"duplicate sequence": func(req SingleRequestRequest, ctrl SingleRequestController) error {
if err := ctrl.SubmitEnvelope(testEnvelope(req.RequestID, 1, SingleRequestStatePlanning)); err != nil {
return err
}
return ctrl.SubmitEnvelope(testEnvelope(req.RequestID, 1, SingleRequestStateWorking))
},
"reordered sequence": func(req SingleRequestRequest, ctrl SingleRequestController) error {
if err := ctrl.SubmitEnvelope(testEnvelope(req.RequestID, 2, SingleRequestStatePlanning)); err != nil {
return err
}
return ctrl.SubmitEnvelope(testEnvelope(req.RequestID, 1, SingleRequestStateWorking))
},
"mismatched saved stage": func(req SingleRequestRequest, ctrl SingleRequestController) error {
if err := ctrl.SubmitEnvelope(testEnvelope(req.RequestID, 1, SingleRequestStatePlanning)); err != nil {
return err
}
tool := testEnvelope(req.RequestID, 2, SingleRequestStateInternalTool)
tool.SavedStage = SingleRequestStateWorking
return ctrl.SubmitEnvelope(tool)
},
"duplicate internal tool": func(req SingleRequestRequest, ctrl SingleRequestController) error {
if err := ctrl.SubmitEnvelope(testEnvelope(req.RequestID, 1, SingleRequestStatePlanning)); err != nil {
return err
}
tool := testEnvelope(req.RequestID, 2, SingleRequestStateInternalTool)
tool.SavedStage = SingleRequestStatePlanning
if err := ctrl.SubmitEnvelope(tool); err != nil {
return err
}
tool.Sequence = 3
return ctrl.SubmitEnvelope(tool)
},
}
for name, submit := range tests {
t.Run(name, func(t *testing.T) {
handle := startTestExecution(t, &channelFakeExecutor{fn: func(_ context.Context, req SingleRequestRequest, ctrl SingleRequestController) error {
return submit(req, ctrl)
}})
_, err := waitForExecution(t, handle)
if !errors.Is(err, ErrSingleRequestInvalidSequence) && !errors.Is(err, ErrSingleRequestInvalidState) {
t.Fatalf("Wait error=%v, want sequence or state failure", err)
}
})
}
}
func TestSingleRequestFinalCandidateRequired(t *testing.T) {
tests := map[string]func(SingleRequestRequest, SingleRequestController) error{
"nil finalizing candidate": func(req SingleRequestRequest, ctrl SingleRequestController) error {
for sequence, stage := range []SingleRequestState{
SingleRequestStatePlanning,
SingleRequestStateWorking,
SingleRequestStateReviewing,
SingleRequestStateFinalizing,
} {
if err := ctrl.SubmitEnvelope(testEnvelope(req.RequestID, uint64(sequence+1), stage)); err != nil {
return err
}
}
return nil
},
"stale earlier-stage candidate": func(req SingleRequestRequest, ctrl SingleRequestController) error {
env := testEnvelope(req.RequestID, 1, SingleRequestStatePlanning)
env.Result = &SingleRequestResult{Output: "stale candidate"}
return ctrl.SubmitEnvelope(env)
},
}
for name, submit := range tests {
t.Run(name, func(t *testing.T) {
handle := startTestExecution(t, &channelFakeExecutor{fn: func(_ context.Context, req SingleRequestRequest, ctrl SingleRequestController) error {
return submit(req, ctrl)
}})
_, err := waitForExecution(t, handle)
if !errors.Is(err, ErrSingleRequestInvalidState) {
t.Fatalf("Wait error=%v, want ErrSingleRequestInvalidState", err)
}
if got := handle.State(); got != SingleRequestStateFailed {
t.Fatalf("state=%s, want failed", got)
}
})
}
}
func TestSingleRequestAcknowledgementRequiresFinalCandidate(t *testing.T) {
executor := &channelFakeExecutor{fn: func(ctx context.Context, _ SingleRequestRequest, _ SingleRequestController) error {
<-ctx.Done()
return ctx.Err()
}}
handle := startTestExecution(t, executor)
internal := handle.(*singleRequestHandle)
internal.mu.Lock()
internal.state = SingleRequestStateFinalizing
internal.mu.Unlock()
err := handle.AcknowledgeTerminal(true)
if !errors.Is(err, ErrSingleRequestInvalidState) {
t.Fatalf("AcknowledgeTerminal error=%v, want ErrSingleRequestInvalidState", err)
}
if got := handle.State(); got != SingleRequestStateFailed {
t.Fatalf("state=%s, want failed", got)
}
if _, err := waitForExecution(t, handle); !errors.Is(err, ErrSingleRequestInvalidState) {
t.Fatalf("Wait error=%v, want ErrSingleRequestInvalidState", err)
}
}
func TestSingleRequestProgressRedactionAndFinalCandidateDelivery(t *testing.T) {
ready := make(chan struct{})
release := make(chan struct{})
executor := &channelFakeExecutor{fn: func(_ context.Context, req SingleRequestRequest, ctrl SingleRequestController) error {
planning := testEnvelope(req.RequestID, 1, SingleRequestStatePlanning)
planning.Message = "raw executor secret"
if err := ctrl.SubmitEnvelope(planning); err != nil {
return err
}
close(ready)
<-release
if err := ctrl.SubmitEnvelope(testEnvelope(req.RequestID, 2, SingleRequestStateWorking)); err != nil {
return err
}
if err := ctrl.SubmitEnvelope(testEnvelope(req.RequestID, 3, SingleRequestStateReviewing)); err != nil {
return err
}
final := testEnvelope(req.RequestID, 4, SingleRequestStateFinalizing)
final.Message = "raw executor secret"
final.Result = &SingleRequestResult{Output: "raw executor result"}
return ctrl.SubmitEnvelope(final)
}}
handle := startTestExecution(t, executor)
<-ready
// Saturate the ordinary lane before the final candidate is emitted.
internal := handle.(*singleRequestHandle)
internal.mu.Lock()
for range 100 {
internal.notifyProgressLocked(SingleRequestProgress{RequestID: internal.req.RequestID, Message: "filler"}, false)
}
internal.mu.Unlock()
close(release)
waitForState(t, handle, SingleRequestStateFinalizing)
seenFinalizingCandidate := false
for len(internal.progressCh) > 0 {
progress := <-internal.progressCh
if progress.Message == "raw executor secret" || progress.Err != nil {
t.Fatalf("progress leaked executor-controlled data: %#v", progress)
}
if progress.Result == nil {
if progress.Stage == SingleRequestStateFinalizing {
t.Fatal("finalizing progress did not include a final candidate")
}
continue
}
if progress.Stage != SingleRequestStateFinalizing {
t.Fatalf("non-finalizing progress exposed a result: %#v", progress)
}
if progress.Result.Output != "raw executor result" {
t.Fatalf("finalizing result=%q, want final candidate", progress.Result.Output)
}
progress.Result.Output = "surface-mutated result"
seenFinalizingCandidate = true
}
if !seenFinalizingCandidate {
t.Fatal("finalizing candidate was dropped after ordinary progress saturation")
}
if err := handle.AcknowledgeTerminal(true); err != nil {
t.Fatalf("AcknowledgeTerminal: %v", err)
}
result, err := waitForExecution(t, handle)
if err != nil {
t.Fatalf("Wait: %v", err)
}
if result.Output != "raw executor result" {
t.Fatalf("Wait result=%q, want immutable final candidate", result.Output)
}
}
func TestSingleRequestTerminalRaces(t *testing.T) {
for i := 0; i < 20; i++ {
release := make(chan struct{})
executor := &channelFakeExecutor{fn: func(ctx context.Context, req SingleRequestRequest, ctrl SingleRequestController) error {
if err := submitToFinalizing(req, ctrl, &SingleRequestResult{Output: "candidate"}); err != nil {
return err
}
select {
case <-release:
return nil
case <-ctx.Done():
return ctx.Err()
}
}}
handle := startTestExecution(t, executor)
waitForState(t, handle, SingleRequestStateFinalizing)
var wg sync.WaitGroup
wg.Add(4)
go func() { defer wg.Done(); _ = handle.AcknowledgeTerminal(true) }()
go func() { defer wg.Done(); _ = handle.AcknowledgeTerminal(false) }()
go func() { defer wg.Done(); handle.Cancel() }()
go func() {
defer wg.Done()
_ = handle.SubmitEnvelope(SingleRequestEnvelope{RequestID: "request-test", Sequence: 5, Stage: SingleRequestStateFailed, Err: errors.New("executor failure")})
}()
wg.Wait()
close(release)
if _, err := waitForExecution(t, handle); err == nil && handle.State() != SingleRequestStateCompleted {
t.Fatalf("non-completed terminal state must retain an error")
}
terminalCount := 0
for progress := range handle.Progress() {
if progress.Terminal != nil {
terminalCount++
}
}
if terminalCount != 1 {
t.Fatalf("terminal disposition progress count=%d, want exactly one", terminalCount)
}
}
}
func TestSingleRequestRequestWallClockBudgetDisposition(t *testing.T) {
for iteration := 0; iteration < 40; iteration++ {
binding := createTestBinding(t)
binding.Limits.WallClockMS = 10
binding.Limits.StageTimeoutMS = 10
executorReturned := make(chan struct{})
executor := &channelFakeExecutor{fn: func(ctx context.Context, _ SingleRequestRequest, _ SingleRequestController) error {
defer close(executorReturned)
<-ctx.Done()
return ctx.Err()
}}
handle, err := startSingleRequest(context.Background(), executor, SingleRequestRequest{
RequestID: "request-budget",
Binding: binding,
Prompt: "exercise the immutable wall-clock budget",
})
if err != nil {
t.Fatalf("iteration=%d StartSingleRequest: %v", iteration, err)
}
terminalCount := 0
var terminal SingleRequestTerminalDisposition
for progress := range handle.Progress() {
if progress.Terminal != nil {
terminalCount++
terminal = *progress.Terminal
}
}
result, waitErr := waitForExecution(t, handle)
want := SingleRequestTerminalDisposition{Kind: SingleRequestTerminalError, ErrorClass: SingleRequestTerminalErrorBudget}
if !errors.Is(waitErr, ErrSingleRequestInternalToolBudget) || result.Output != "" || terminal != want || terminalCount != 1 {
t.Fatalf("iteration=%d Wait=(%+v, %v) terminal=%+v count=%d, want budget/1", iteration, result, waitErr, terminal, terminalCount)
}
if handle.State() != SingleRequestStateFailed {
t.Fatalf("iteration=%d state=%s, want failed", iteration, handle.State())
}
select {
case <-executorReturned:
default:
t.Fatalf("iteration=%d executor remained active after Wait", iteration)
}
}
}
func TestSingleRequestTerminalDispositionValidationAndLegacyNormalization(t *testing.T) {
for _, invalid := range []SingleRequestTerminalDisposition{
{},
{Kind: SingleRequestTerminalEndTurn, ErrorClass: SingleRequestTerminalErrorProvider},
{Kind: SingleRequestTerminalError},
{Kind: SingleRequestTerminalError, ErrorClass: "raw-private-value"},
{Kind: "unknown"},
} {
if invalid.Validate() == nil {
t.Fatalf("invalid disposition accepted: %+v", invalid)
}
}
handle := startTestExecution(t, &channelFakeExecutor{fn: func(_ context.Context, req SingleRequestRequest, ctrl SingleRequestController) error {
return submitToFinalizing(req, ctrl, &SingleRequestResult{Output: "legacy output"})
}})
waitForState(t, handle, SingleRequestStateFinalizing)
var terminal *SingleRequestTerminalDisposition
for terminal == nil {
progress := <-handle.Progress()
if progress.Stage == SingleRequestStateFinalizing {
terminal = progress.Terminal
if progress.Result == nil || progress.Result.Terminal.Kind != SingleRequestTerminalEndTurn {
t.Fatalf("legacy result was not normalized: %+v", progress)
}
}
}
terminal.Kind = SingleRequestTerminalLength
if err := handle.AcknowledgeTerminal(true); err != nil {
t.Fatalf("AcknowledgeTerminal: %v", err)
}
result, err := waitForExecution(t, handle)
if err != nil || result.Terminal.Kind != SingleRequestTerminalEndTurn {
t.Fatalf("Wait=(%+v, %v), want immutable end_turn", result, err)
}
}
func TestSingleRequestTerminalDispositionEarlyLengthOnly(t *testing.T) {
t.Run("length from planning", func(t *testing.T) {
handle := startTestExecution(t, &channelFakeExecutor{fn: func(_ context.Context, req SingleRequestRequest, ctrl SingleRequestController) error {
if err := ctrl.SubmitEnvelope(testEnvelope(req.RequestID, 1, SingleRequestStatePlanning)); err != nil {
return err
}
return ctrl.SubmitEnvelope(SingleRequestEnvelope{
RequestID: req.RequestID,
Sequence: 2,
Stage: SingleRequestStateFinalizing,
Result: &SingleRequestResult{Terminal: SingleRequestTerminalDisposition{Kind: SingleRequestTerminalLength}},
})
}})
terminalCount := 0
for progress := range handle.Progress() {
if progress.Terminal != nil {
terminalCount++
if progress.Stage != SingleRequestStateFinalizing || progress.Terminal.Kind != SingleRequestTerminalLength {
t.Fatalf("progress=%+v, want planning length final", progress)
}
if err := handle.AcknowledgeTerminal(true); err != nil {
t.Fatalf("AcknowledgeTerminal: %v", err)
}
}
}
result, err := waitForExecution(t, handle)
if err != nil || result.Terminal.Kind != SingleRequestTerminalLength || terminalCount != 1 {
t.Fatalf("Wait=(%+v, %v), terminals=%d", result, err, terminalCount)
}
})
t.Run("end turn cannot skip work and review", func(t *testing.T) {
handle := startTestExecution(t, &channelFakeExecutor{fn: func(_ context.Context, req SingleRequestRequest, ctrl SingleRequestController) error {
if err := ctrl.SubmitEnvelope(testEnvelope(req.RequestID, 1, SingleRequestStatePlanning)); err != nil {
return err
}
return ctrl.SubmitEnvelope(SingleRequestEnvelope{
RequestID: req.RequestID,
Sequence: 2,
Stage: SingleRequestStateFinalizing,
Result: &SingleRequestResult{Output: "invalid early success", Terminal: SingleRequestTerminalDisposition{Kind: SingleRequestTerminalEndTurn}},
})
}})
var terminal *SingleRequestTerminalDisposition
for progress := range handle.Progress() {
if progress.Terminal != nil {
terminal = progress.Terminal
}
}
result, err := waitForExecution(t, handle)
want := SingleRequestTerminalDisposition{Kind: SingleRequestTerminalError, ErrorClass: SingleRequestTerminalErrorValidation}
if !errors.Is(err, ErrSingleRequestInvalidState) || result.Output != "" || terminal == nil || *terminal != want {
t.Fatalf("Wait=(%+v, %v), terminal=%+v, want validation failure", result, err, terminal)
}
})
}
func TestSingleRequestTerminalDispositionFailureAndCancelPropagation(t *testing.T) {
tests := []struct {
name string
stage SingleRequestState
terminal SingleRequestTerminalDisposition
}{
{name: "provider", stage: SingleRequestStateFailed, terminal: SingleRequestTerminalDisposition{Kind: SingleRequestTerminalError, ErrorClass: SingleRequestTerminalErrorProvider}},
{name: "timeout", stage: SingleRequestStateFailed, terminal: SingleRequestTerminalDisposition{Kind: SingleRequestTerminalError, ErrorClass: SingleRequestTerminalErrorTimeout}},
{name: "budget", stage: SingleRequestStateFailed, terminal: SingleRequestTerminalDisposition{Kind: SingleRequestTerminalError, ErrorClass: SingleRequestTerminalErrorBudget}},
{name: "repetition", stage: SingleRequestStateFailed, terminal: SingleRequestTerminalDisposition{Kind: SingleRequestTerminalError, ErrorClass: SingleRequestTerminalErrorRepetition}},
{name: "malformed", stage: SingleRequestStateFailed, terminal: SingleRequestTerminalDisposition{Kind: SingleRequestTerminalError, ErrorClass: SingleRequestTerminalErrorMalformed}},
{name: "context", stage: SingleRequestStateFailed, terminal: SingleRequestTerminalDisposition{Kind: SingleRequestTerminalError, ErrorClass: SingleRequestTerminalErrorContext}},
{name: "cancel", stage: SingleRequestStateCancelled, terminal: SingleRequestTerminalDisposition{Kind: SingleRequestTerminalCancelled}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
handle := startTestExecution(t, &channelFakeExecutor{fn: func(_ context.Context, req SingleRequestRequest, ctrl SingleRequestController) error {
if err := ctrl.SubmitEnvelope(testEnvelope(req.RequestID, 1, SingleRequestStatePlanning)); err != nil {
return err
}
env := testEnvelope(req.RequestID, 2, test.stage)
env.Terminal = &test.terminal
if test.stage == SingleRequestStateFailed {
env.Err = errors.New("private provider detail")
}
return ctrl.SubmitEnvelope(env)
}})
_, _ = waitForExecution(t, handle)
var got *SingleRequestTerminalDisposition
for progress := range handle.Progress() {
if progress.Stage == test.stage {
got = progress.Terminal
if progress.Err != nil || progress.Result != nil {
t.Fatalf("terminal progress leaked internal data: %+v", progress)
}
}
}
if got == nil || *got != test.terminal {
t.Fatalf("terminal=%+v, want %+v", got, test.terminal)
}
})
}
}
type terminalDispositionCleanupFailure struct{}
func (terminalDispositionCleanupFailure) CleanupWorkspace(context.Context, *SingleRequestWorkspaceBinding, string) error {
return errors.New("private cleanup detail")
}
func TestSingleRequestTerminalDispositionCleanupConversionBeforeFreeze(t *testing.T) {
handle := startTestExecution(t, &channelFakeExecutor{fn: func(_ context.Context, req SingleRequestRequest, ctrl SingleRequestController) error {
internal := ctrl.(*singleRequestHandle)
internal.mu.Lock()
internal.toolLoop.opened = true
internal.toolLoop.lifecycle = terminalDispositionCleanupFailure{}
internal.mu.Unlock()
return submitToFinalizing(req, ctrl, &SingleRequestResult{
Output: "must not escape",
Terminal: SingleRequestTerminalDisposition{Kind: SingleRequestTerminalLength},
})
}})
result, err := waitForExecution(t, handle)
if !errors.Is(err, ErrSingleRequestWorkspaceCleanup) || result.Output != "" {
t.Fatalf("Wait=(%+v, %v), want cleanup failure without partial result", result, err)
}
var got *SingleRequestTerminalDisposition
for progress := range handle.Progress() {
if progress.Stage == SingleRequestStateFailed {
got = progress.Terminal
}
}
want := SingleRequestTerminalDisposition{Kind: SingleRequestTerminalError, ErrorClass: SingleRequestTerminalErrorWorkspaceCleanup}
if got == nil || *got != want {
t.Fatalf("cleanup terminal=%+v, want %+v", got, want)
}
}
func TestSingleRequestTerminalDispositionPostFreezeWinnerStability(t *testing.T) {
handle := startTestExecution(t, &channelFakeExecutor{fn: func(_ context.Context, req SingleRequestRequest, ctrl SingleRequestController) error {
return submitToFinalizing(req, ctrl, &SingleRequestResult{Output: "candidate", Terminal: SingleRequestTerminalDisposition{Kind: SingleRequestTerminalEndTurn}})
}})
waitForState(t, handle, SingleRequestStateFinalizing)
for {
progress := <-handle.Progress()
if progress.Stage != SingleRequestStateFinalizing {
continue
}
if progress.Terminal == nil || progress.Terminal.Kind != SingleRequestTerminalEndTurn {
t.Fatalf("finalizing terminal=%+v", progress.Terminal)
}
progress.Terminal.Kind = SingleRequestTerminalLength
break
}
if err := handle.AcknowledgeTerminal(false); err != nil {
t.Fatalf("AcknowledgeTerminal(false): %v", err)
}
_, err := waitForExecution(t, handle)
if !errors.Is(err, ErrSingleRequestFailed) {
t.Fatalf("Wait error=%v, want endpoint failure", err)
}
internal := handle.(*singleRequestHandle)
internal.mu.Lock()
frozen := cloneSingleRequestTerminal(internal.terminal)
internal.mu.Unlock()
if frozen == nil || frozen.Kind != SingleRequestTerminalEndTurn {
t.Fatalf("frozen terminal changed after acknowledgement failure: %+v", frozen)
}
for progress := range handle.Progress() {
if progress.Terminal != nil {
t.Fatalf("post-freeze acknowledgement emitted a second terminal: %+v", progress)
}
}
}