Skip to content

Commit aa8c280

Browse files
authored
Merge pull request #1839 from entireio/fix/droid-trust-dialog-e2e
fix(e2e): dismiss Droid trust-folder dialog on interactive start
2 parents 3b483c3 + b269ecd commit aa8c280

2 files changed

Lines changed: 103 additions & 3 deletions

File tree

e2e/agents/droid.go

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,10 +191,33 @@ func (d *Droid) StartSession(ctx context.Context, dir string) (Session, error) {
191191
return nil, err
192192
}
193193

194-
// Wait for the interactive prompt indicator.
195-
if _, err := s.WaitFor(`>`, 30*time.Second); err != nil {
194+
// Dismiss startup dialogs (folder trust, etc.) then wait for the input
195+
// prompt. Droid v0.178.0 added a "Trust this folder?" dialog in interactive
196+
// mode for untrusted directories. Its "1. Trust this folder" option is
197+
// pre-selected, so Enter confirms it. The dialog renders its selected option
198+
// as "> 1. Trust this folder", so a bare ">" match cannot distinguish the
199+
// dialog from the real input box — we must key off the dialog chrome (see
200+
// isDroidStartupDialog) before treating ">" as ready.
201+
foundPrompt := false
202+
for range 5 {
203+
content, err := s.WaitFor(`>`, 30*time.Second)
204+
if err != nil {
205+
_ = s.Close()
206+
return nil, fmt.Errorf("waiting for startup prompt: %w", err)
207+
}
208+
if !isDroidStartupDialog(content) {
209+
foundPrompt = true
210+
break
211+
}
212+
if err := s.SendKeys("Enter"); err != nil {
213+
_ = s.Close()
214+
return nil, fmt.Errorf("dismissing startup dialog: %w", err)
215+
}
216+
time.Sleep(500 * time.Millisecond)
217+
}
218+
if !foundPrompt {
196219
_ = s.Close()
197-
return nil, fmt.Errorf("waiting for startup prompt: %w", err)
220+
return nil, errors.New("droid did not reach interactive prompt after dismissing startup dialogs")
198221
}
199222

200223
// Droid auto-generates a greeting on startup which fires a Stop hook.
@@ -209,3 +232,16 @@ func (d *Droid) StartSession(ctx context.Context, dir string) (Session, error) {
209232

210233
return s, nil
211234
}
235+
236+
// isDroidStartupDialog reports whether the captured pane is showing a Droid
237+
// startup dialog (currently the "Trust this folder?" prompt) rather than the
238+
// interactive input box. The dialog renders its pre-selected option as
239+
// "> 1. Trust this folder", so the presence of ">" alone cannot distinguish it
240+
// from the real prompt — we key off the dialog title ("trust this folder") and
241+
// its "exit without trusting" option label instead.
242+
// Matching is case-insensitive to stay resilient to Droid re-casing its copy.
243+
func isDroidStartupDialog(content string) bool {
244+
lower := strings.ToLower(content)
245+
return strings.Contains(lower, "trust this folder") ||
246+
strings.Contains(lower, "exit without trusting")
247+
}

e2e/agents/droid_trust_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package agents
2+
3+
import "testing"
4+
5+
// droidTrustDialogPane is a trimmed capture of Droid v0.178.0's interactive
6+
// "Trust this folder?" startup dialog. The pre-selected option renders as
7+
// "> 1. Trust this folder", i.e. it uses the same ">" as the real input box —
8+
// the exact shape that made the old bare-">" WaitFor mistake the dialog for the
9+
// prompt and swallow the first real prompt.
10+
const droidTrustDialogPane = `│ Trust this folder? │
11+
│ │
12+
│ /tmp/e2e-repo-4116086897 │
13+
│ │
14+
│ Droid will read, edit, and run files in this folder, and load any project │
15+
│ configuration it defines, including hooks and MCP servers that can execute │
16+
│ commands on your machine. │
17+
╰──────────────────────────────────────────────────────────────────────────────╯
18+
19+
> 1. Trust this folder
20+
2. Exit without trusting
21+
22+
Enter to confirm · Esc to exit`
23+
24+
// droidInteractivePromptPane is the real idle REPL after trust is granted: the
25+
// input box with a bare ">" and no dialog chrome. This must NOT be classified
26+
// as a startup dialog.
27+
const droidInteractivePromptPane = ` Auto (High) · allow all commands claude-haiku-custom (High) [custom]
28+
╭──────────────────────────────────────────────────────────────────────────────╮
29+
│ > │
30+
╰──────────────────────────────────────────────────────────────────────────────╯
31+
? for help TMUX ⧉`
32+
33+
// TestIsDroidStartupDialog_DetectsTrustDialog is the regression guard for the
34+
// Droid v0.178.0 break: the trust dialog must be recognized so StartSession
35+
// keeps sending Enter to confirm it instead of mistaking "> 1. Trust this
36+
// folder" for the interactive prompt and sending the first real prompt into a
37+
// dialog that swallows it.
38+
func TestIsDroidStartupDialog_DetectsTrustDialog(t *testing.T) {
39+
t.Parallel()
40+
if !isDroidStartupDialog(droidTrustDialogPane) {
41+
t.Fatal("trust dialog should be detected as a startup dialog")
42+
}
43+
}
44+
45+
// TestIsDroidStartupDialog_DetectsByOptionLabel confirms detection keys off the
46+
// "exit without trusting" option too, so a title-text change in a future Droid
47+
// release does not silently re-break the handshake.
48+
func TestIsDroidStartupDialog_DetectsByOptionLabel(t *testing.T) {
49+
t.Parallel()
50+
const optionOnly = "> 1. Trust folder\n 2. Exit without trusting"
51+
if !isDroidStartupDialog(optionOnly) {
52+
t.Fatal("the trust dialog's option labels alone should be enough to detect it")
53+
}
54+
}
55+
56+
// TestIsDroidStartupDialog_IgnoresInteractivePrompt ensures the real prompt is
57+
// not classified as a dialog — otherwise StartSession would loop dismissing a
58+
// dialog that isn't there and never hand back a usable session.
59+
func TestIsDroidStartupDialog_IgnoresInteractivePrompt(t *testing.T) {
60+
t.Parallel()
61+
if isDroidStartupDialog(droidInteractivePromptPane) {
62+
t.Fatal("bare interactive prompt should not be classified as a startup dialog")
63+
}
64+
}

0 commit comments

Comments
 (0)