Skip to content

Commit bafd5b5

Browse files
michaelmcneesclaude
andcommitted
fix: make integration tests CI-resilient — install playwright inline, retry IMAP connections
- browser/run: change JS/TS and Python container commands to install the playwright npm/pip package inline before executing the script. The Microsoft Playwright Docker images ship browsers but not the Node API package, causing MODULE_NOT_FOUND errors in CI. - TestIMAPDial: add a 5-attempt retry loop (2s sleep) around imapDial. GreenMail may not accept logins immediately after the "Starting GreenMail" log line appears in CI. - fetchFirstUID: replace the single-shot call with a 5-attempt retry loop (2s sleep) so that SMTP-to-IMAP visibility delay in CI doesn't cause TestEmailDelete_DeletesMessage and TestEmailMove_MovesMessage to fail with "no messages found". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3aa38d7 commit bafd5b5

3 files changed

Lines changed: 62 additions & 30 deletions

File tree

internal/connector/browser.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,18 +107,21 @@ func (c *BrowserRunConnector) Execute(ctx context.Context, params map[string]any
107107
case "javascript":
108108
containerImage = playwrightNodeImage
109109
wrapperScript = buildJSWrapper(script)
110-
// Pass the wrapper via stdin; `node` reads from stdin when no file argument is given.
111-
containerCmd = []string{"node"}
110+
// The Playwright Docker image ships browsers but not the npm package.
111+
// Install it silently before running so the script can require('playwright').
112+
// `node` with no file argument reads the script from stdin.
113+
containerCmd = []string{"sh", "-c", "cd /tmp && npm init -y --silent 2>/dev/null && npm install --silent playwright 2>/dev/null && node"}
112114
case "typescript":
113115
containerImage = playwrightNodeImage
114116
wrapperScript = buildJSWrapper(script)
115-
// Use --experimental-strip-types so Node can execute TypeScript directly.
116-
containerCmd = []string{"node", "--experimental-strip-types"}
117+
// Same npm-install preamble; use --experimental-strip-types for TS syntax.
118+
containerCmd = []string{"sh", "-c", "cd /tmp && npm init -y --silent 2>/dev/null && npm install --silent playwright 2>/dev/null && node --experimental-strip-types"}
117119
case "python":
118120
containerImage = playwrightPythonImage
119121
wrapperScript = buildPythonWrapper(script)
120-
// Pass the wrapper via stdin; `python3 -` reads from stdin.
121-
containerCmd = []string{"python3", "-"}
122+
// Install the playwright Python package if not already present, then run
123+
// the script via stdin (`python3 -`).
124+
containerCmd = []string{"sh", "-c", "pip install --quiet playwright 2>/dev/null && python3 -"}
122125
}
123126

124127
// --- build docker/run params ---

internal/connector/email_move_test.go

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"net/smtp"
77
"testing"
8+
"time"
89
)
910

1011
// TestEmailMoveParamValidation verifies that required parameters are enforced.
@@ -150,31 +151,45 @@ func createTargetFolder(t *testing.T, host, imapPort, username, password, folder
150151

151152
// fetchFirstUID retrieves the first UID from the given IMAP folder using the
152153
// EmailReceiveConnector, to avoid duplicating IMAP client logic in tests.
154+
// It retries up to 5 times with a 2-second pause to accommodate the brief
155+
// delay between SMTP delivery and IMAP visibility in CI.
153156
func fetchFirstUID(t *testing.T, host, imapPort, username, password, folder string) uint32 {
154157
t.Helper()
155158
recv := &EmailReceiveConnector{}
156-
result, err := recv.Execute(context.Background(), map[string]any{
157-
"folder": folder,
158-
"filter": "all",
159-
"limit": 1,
160-
"_credential": map[string]string{
161-
"host": host,
162-
"port": imapPort,
163-
"username": username,
164-
"password": password,
165-
"use_tls": "false",
166-
},
167-
})
168-
if err != nil {
169-
t.Fatalf("fetchFirstUID: receive error: %v", err)
170-
}
171-
msgs, ok := result["messages"].([]map[string]any)
172-
if !ok || len(msgs) == 0 {
173-
t.Fatal("fetchFirstUID: no messages found")
159+
cred := map[string]string{
160+
"host": host,
161+
"port": imapPort,
162+
"username": username,
163+
"password": password,
164+
"use_tls": "false",
174165
}
175-
uid, _ := msgs[0]["uid"].(uint32)
176-
if uid == 0 {
177-
t.Fatal("fetchFirstUID: uid is zero")
166+
167+
for attempt := 0; attempt < 5; attempt++ {
168+
if attempt > 0 {
169+
time.Sleep(2 * time.Second)
170+
}
171+
result, err := recv.Execute(context.Background(), map[string]any{
172+
"folder": folder,
173+
"filter": "all",
174+
"limit": 1,
175+
"_credential": cred,
176+
})
177+
if err != nil {
178+
t.Logf("fetchFirstUID attempt %d/5: receive error: %v", attempt+1, err)
179+
continue
180+
}
181+
msgs, ok := result["messages"].([]map[string]any)
182+
if !ok || len(msgs) == 0 {
183+
t.Logf("fetchFirstUID attempt %d/5: no messages yet", attempt+1)
184+
continue
185+
}
186+
uid, _ := msgs[0]["uid"].(uint32)
187+
if uid == 0 {
188+
t.Logf("fetchFirstUID attempt %d/5: uid is zero", attempt+1)
189+
continue
190+
}
191+
return uid
178192
}
179-
return uid
193+
t.Fatal("fetchFirstUID: no messages found after retries")
194+
return 0
180195
}

internal/connector/imap_test.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"testing"
88
"time"
99

10+
"github.com/emersion/go-imap/v2/imapclient"
1011
"github.com/testcontainers/testcontainers-go"
1112
"github.com/testcontainers/testcontainers-go/wait"
1213
)
@@ -72,9 +73,22 @@ func TestIMAPDial(t *testing.T) {
7273
UseTLS: false,
7374
}
7475

75-
client, err := imapDial(cfg)
76+
// GreenMail may not accept logins immediately after the startup log line.
77+
// Retry with a short backoff to handle this CI timing window.
78+
var client *imapclient.Client
79+
var err error
80+
for attempt := 0; attempt < 5; attempt++ {
81+
if attempt > 0 {
82+
time.Sleep(2 * time.Second)
83+
}
84+
client, err = imapDial(cfg)
85+
if err == nil {
86+
break
87+
}
88+
t.Logf("imapDial attempt %d/5 failed: %v", attempt+1, err)
89+
}
7690
if err != nil {
77-
t.Fatalf("imapDial() error: %v", err)
91+
t.Fatalf("imapDial() error after retries: %v", err)
7892
}
7993
defer func() {
8094
if err := client.Close(); err != nil {

0 commit comments

Comments
 (0)