Skip to content

Commit b180764

Browse files
test(runtime): fix ask_user CI regressions
Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: Cai-Tang-www <106404101+Cai-Tang-www@users.noreply.github.com>
1 parent 048d08a commit b180764

5 files changed

Lines changed: 207 additions & 0 deletions

File tree

internal/app/bootstrap_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2087,6 +2087,10 @@ func (s *stubRemoteRuntimeForBootstrap) ResolvePermission(context.Context, servi
20872087
return nil
20882088
}
20892089

2090+
func (s *stubRemoteRuntimeForBootstrap) ResolveUserQuestion(context.Context, services.UserQuestionResolutionInput) error {
2091+
return nil
2092+
}
2093+
20902094
func (s *stubRemoteRuntimeForBootstrap) CancelActiveRun() bool {
20912095
return false
20922096
}

internal/gateway/multi_workspace_runtime_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type recordingPort struct {
2424
runCalls atomic.Int32
2525
listSessionsCalls atomic.Int32
2626
executeSysCalls atomic.Int32
27+
resolveUserCalls atomic.Int32
2728
cancelCalls atomic.Int32
2829
closed atomic.Int32
2930
closeOnce sync.Once
@@ -81,6 +82,7 @@ func (p *recordingPort) ResolvePermission(_ context.Context, _ PermissionResolut
8182
}
8283

8384
func (p *recordingPort) ResolveUserQuestion(_ context.Context, _ UserQuestionAnswerInput) error {
85+
p.resolveUserCalls.Add(1)
8486
return nil
8587
}
8688

@@ -467,6 +469,28 @@ func TestMultiWorkspaceRuntime_SwitchWorkspaceValidates(t *testing.T) {
467469
}
468470
}
469471

472+
func TestMultiWorkspaceRuntime_ResolveUserQuestionRoutesByWorkspace(t *testing.T) {
473+
idx, alpha, beta := setupIndex(t)
474+
builder := newTestBuilder()
475+
mw := NewMultiWorkspaceRuntime(idx, alpha.Hash, builder.build)
476+
t.Cleanup(func() { _ = mw.Close() })
477+
478+
if err := mw.ResolveUserQuestion(ctxWithHash(t, beta.Hash), UserQuestionAnswerInput{
479+
RequestID: "ask-1",
480+
Status: "answered",
481+
}); err != nil {
482+
t.Fatalf("ResolveUserQuestion: %v", err)
483+
}
484+
485+
betaPort := builder.portFor(beta.Path)
486+
if betaPort == nil {
487+
t.Fatalf("beta port should be built")
488+
}
489+
if got := betaPort.resolveUserCalls.Load(); got != 1 {
490+
t.Fatalf("beta resolve user calls = %d, want 1", got)
491+
}
492+
}
493+
470494
func TestMultiWorkspaceRuntime_CreatePersistsIndex(t *testing.T) {
471495
idx, alpha, _ := setupIndex(t)
472496
builder := newTestBuilder()

internal/gateway/validate_additional_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,46 @@ func TestValidateResolvePermissionInvalidPayloadType(t *testing.T) {
137137
t.Fatalf("error code = %q, want %q", err.Code, ErrorCodeInvalidAction.String())
138138
}
139139
}
140+
141+
func TestDecodeUserQuestionAnswerPayloadAdditionalBranches(t *testing.T) {
142+
t.Parallel()
143+
144+
t.Run("direct struct", func(t *testing.T) {
145+
input, err := decodeUserQuestionAnswerPayload(UserQuestionAnswerInput{
146+
RequestID: "ask-1",
147+
Status: "answered",
148+
})
149+
if err != nil {
150+
t.Fatalf("unexpected error: %v", err)
151+
}
152+
if input.RequestID != "ask-1" {
153+
t.Fatalf("request id = %q, want ask-1", input.RequestID)
154+
}
155+
})
156+
157+
t.Run("nil pointer", func(t *testing.T) {
158+
var input *UserQuestionAnswerInput
159+
decoded, err := decodeUserQuestionAnswerPayload(input)
160+
if err != nil {
161+
t.Fatalf("unexpected error: %v", err)
162+
}
163+
if decoded.RequestID != "" || decoded.Status != "" {
164+
t.Fatalf("expected zero value from nil pointer, got %#v", decoded)
165+
}
166+
})
167+
168+
t.Run("marshal error", func(t *testing.T) {
169+
payload := map[string]any{"bad": func() {}}
170+
_, err := decodeUserQuestionAnswerPayload(payload)
171+
if err == nil {
172+
t.Fatal("expected marshal error")
173+
}
174+
})
175+
176+
t.Run("unmarshal error", func(t *testing.T) {
177+
_, err := decodeUserQuestionAnswerPayload([]byte("not-json-object"))
178+
if err == nil {
179+
t.Fatal("expected unmarshal error")
180+
}
181+
})
182+
}

internal/runtime/ask_user_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,61 @@ func TestResolveUserQuestionSkip(t *testing.T) {
172172
}
173173
}
174174

175+
func TestResolveUserQuestionDefaultsStatusAndTrimsMessage(t *testing.T) {
176+
t.Parallel()
177+
178+
service := NewWithFactory(
179+
newRuntimeConfigManager(t),
180+
&stubToolManager{},
181+
newMemoryStore(),
182+
&scriptedProviderFactory{provider: &scriptedProvider{}},
183+
nil,
184+
)
185+
186+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
187+
defer cancel()
188+
189+
var openResult askuser.Result
190+
var openErr error
191+
192+
var wg sync.WaitGroup
193+
wg.Add(1)
194+
go func() {
195+
defer wg.Done()
196+
_, openResult, openErr = service.askUserBroker.Open(ctx, askuser.Request{
197+
QuestionID: "q1",
198+
TimeoutSec: 10,
199+
})
200+
}()
201+
202+
time.Sleep(50 * time.Millisecond)
203+
204+
ids := service.askUserBroker.PendingIDs()
205+
if len(ids) != 1 {
206+
t.Fatalf("expected 1 pending request, got %d", len(ids))
207+
}
208+
209+
resolveErr := service.ResolveUserQuestion(context.Background(), UserQuestionResolutionInput{
210+
RequestID: ids[0],
211+
Message: " keep this trimmed ",
212+
})
213+
if resolveErr != nil {
214+
t.Fatalf("ResolveUserQuestion default status error: %v", resolveErr)
215+
}
216+
217+
wg.Wait()
218+
219+
if openErr != nil {
220+
t.Fatalf("broker Open error: %v", openErr)
221+
}
222+
if openResult.Status != askuser.StatusAnswered {
223+
t.Fatalf("expected default answered status, got %q", openResult.Status)
224+
}
225+
if openResult.Message != "keep this trimmed" {
226+
t.Fatalf("expected trimmed message, got %q", openResult.Message)
227+
}
228+
}
229+
175230
func TestResolveUserQuestionContextCanceled(t *testing.T) {
176231
t.Parallel()
177232

@@ -195,6 +250,24 @@ func TestResolveUserQuestionContextCanceled(t *testing.T) {
195250
}
196251
}
197252

253+
func TestEventTypeFromAskUserEvent(t *testing.T) {
254+
t.Parallel()
255+
256+
tests := map[string]EventType{
257+
"user_question_requested": EventUserQuestionRequested,
258+
"user_question_answered": EventUserQuestionAnswered,
259+
"user_question_skipped": EventUserQuestionSkipped,
260+
"user_question_timeout": EventUserQuestionTimeout,
261+
"unknown": EventError,
262+
}
263+
264+
for name, want := range tests {
265+
if got := eventTypeFromAskUserEvent(name); got != want {
266+
t.Fatalf("eventTypeFromAskUserEvent(%q) = %q, want %q", name, got, want)
267+
}
268+
}
269+
}
270+
198271
func TestAskUserBrokerAdapterConversion(t *testing.T) {
199272
t.Parallel()
200273

@@ -255,3 +328,38 @@ func TestAskUserBrokerAdapterConversion(t *testing.T) {
255328

256329
wg.Wait()
257330
}
331+
332+
func TestAskUserBrokerAdapterErrorAndOptionConversion(t *testing.T) {
333+
t.Parallel()
334+
335+
broker := askuser.NewBroker()
336+
adapter := newAskUserBrokerAdapter(broker)
337+
338+
if got := convertAskUserOptions(nil); got != nil {
339+
t.Fatalf("expected nil options conversion, got %#v", got)
340+
}
341+
342+
ctx, cancel := context.WithCancel(context.Background())
343+
cancel()
344+
345+
requestID, result, err := adapter.Open(ctx, tools.AskUserRequest{
346+
QuestionID: "adapter-q2",
347+
Title: "Choose",
348+
Kind: "single_choice",
349+
Options: []tools.AskUserOption{
350+
{Label: "A", Description: "first"},
351+
},
352+
})
353+
if err == nil {
354+
t.Fatal("expected adapter Open error when context is canceled")
355+
}
356+
if !strings.Contains(err.Error(), "canceled") {
357+
t.Fatalf("expected canceled error, got %v", err)
358+
}
359+
if requestID == "" {
360+
t.Fatal("expected generated request_id on canceled adapter Open")
361+
}
362+
if result.Status != askuser.StatusTimeout {
363+
t.Fatalf("expected timeout status on canceled adapter Open, got %q", result.Status)
364+
}
365+
}

internal/tools/ask_user_tool_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"errors"
77
"strings"
88
"testing"
9+
10+
"neo-code/internal/security"
911
)
1012

1113
// stubAskUserBroker implements AskUserBroker for tests.
@@ -481,3 +483,29 @@ func TestAskUserToolVisibleInReadOnlyMode(t *testing.T) {
481483
t.Fatal("expected ask_user to be visible in read-only plan mode")
482484
}
483485
}
486+
487+
func TestIsReadOnlyActionAllowedIncludesInteractionAndTodoWrite(t *testing.T) {
488+
t.Parallel()
489+
490+
if !isReadOnlyActionAllowed(security.Action{Type: security.ActionTypeInteraction}) {
491+
t.Fatal("expected interaction action to be allowed in read-only mode")
492+
}
493+
494+
if !isReadOnlyActionAllowed(security.Action{
495+
Type: security.ActionTypeWrite,
496+
Payload: security.ActionPayload{
497+
Operation: " " + ToolNameTodoWrite + " ",
498+
},
499+
}) {
500+
t.Fatal("expected todo_write action to be allowed in read-only mode")
501+
}
502+
503+
if isReadOnlyActionAllowed(security.Action{
504+
Type: security.ActionTypeWrite,
505+
Payload: security.ActionPayload{
506+
Operation: ToolNameBash,
507+
},
508+
}) {
509+
t.Fatal("expected non-todo write action to be blocked in read-only mode")
510+
}
511+
}

0 commit comments

Comments
 (0)