From 3e2a6927e37d70d3e40a85db0b86d42c95809477 Mon Sep 17 00:00:00 2001 From: Dan Caseley Date: Sat, 12 Sep 2026 17:07:47 +0100 Subject: [PATCH 1/3] OF-3366: Add a basic e2e test for FAST --- build/ci/conversations/flows/README.md | 17 ++++ build/ci/conversations/flows/fast.yaml | 84 +++++++++++++++++++ .../flows/scripts/checkForLogs.js | 34 ++++++-- 3 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 build/ci/conversations/flows/fast.yaml diff --git a/build/ci/conversations/flows/README.md b/build/ci/conversations/flows/README.md index c085f78ae0..3bc4bd711b 100644 --- a/build/ci/conversations/flows/README.md +++ b/build/ci/conversations/flows/README.md @@ -33,6 +33,10 @@ The `demoboot` flows need no extra configuration beyond launching Openfire with For flows with a specific config, copy the matching file from `build/ci/conversations/configs/` into your distribution's `conf/` directory as `openfire-demoboot.xml` before starting demoboot mode. +Flows are tagged by which config they need, not by feature: `fast.yaml` is tagged `sasl2` +and reuses `configs/sasl2.xml`, since FAST (XEP-0484) is only advertised as a SASL2 inline +feature and needs no config of its own — `xmpp.fast.enabled` defaults to `true`. + ### 2. Start an Android emulator Launch an emulator with API 34 and x86_64 architecture. With Maestro installed, you can create or @@ -115,3 +119,16 @@ Use `checkForLogs.js` in a flow: The above returns `HTTP/200` if a log line containing "SMACK" followed later by "connected" exists. The matched lines are available in subsequent steps as `${checkForLogs.output.matchedLines}`. + +For a log line produced by work that happens asynchronously after the UI has already settled +(for example, a background reconnect), pass `MAX_ATTEMPTS`/`DELAY_MS` to poll the sidecar +instead of taking a single snapshot: + +```yaml +- runScript: + file: scripts/checkForLogs.js + env: + PATTERN: 'quick start with HT' + MAX_ATTEMPTS: 30 # default: 1 (single snapshot, no retry) + DELAY_MS: 1000 # default: 1000 +``` diff --git a/build/ci/conversations/flows/fast.yaml b/build/ci/conversations/flows/fast.yaml new file mode 100644 index 0000000000..09a36b0ff8 --- /dev/null +++ b/build/ci/conversations/flows/fast.yaml @@ -0,0 +1,84 @@ +appId: eu.siacs.conversations +name: FAST +tags: + - sasl2 +onFlowStart: + - runScript: scripts/checkHealth.js +--- + +- runScript: scripts/startSession.js + +- launchApp: + clearState: true + +- assertVisible: "I already have an account" + +- runScript: + file: scripts/checkForLogs.js + env: + PATTERN: initializing database.* + +- tapOn: "I already have an account" +- tapOn: "More options" +- tapOn: "Settings" +- tapOn: "Connection" +- tapOn: "Show extended connection settings when setting up an account" +- tapOn: "Navigate up" +- tapOn: "Navigate up" + +- tapOn: "XMPP address" +- inputText: "jane@example.org" +- tapOn: "Password" +- inputText: "secret" +- tapOn: "Hostname" +- inputText: "10.0.2.2" +- tapOn: "Next" + +- assertVisible: "Accept Unknown Certificate?" +- tapOn: "Always" + +- assertVisible: "Publish avatar" +- tapOn: "Skip" + +- runFlow: + when: + visible: "Battery optimizations enabled" + commands: + - tapOn: "Next" + - assertVisible: "Let app always run in background?" + - tapOn: "Allow" + +- assertVisible: "Once you start a new conversation.*" + +# The initial, password-based login should obtain and cache a FAST (XEP-0484) token for next time +- runScript: + file: scripts/checkForLogs.js + env: + PATTERN: 'jane@example\.org: storing hashed token Mechanism\[' + +# Restart the app (preserving state, unlike the clearState launch above) to force a fresh +# connection. This should authenticate using the cached FAST token via SASL2 quick start, +# rather than the password. +- runScript: scripts/startSession.js + +- launchApp + +- assertVisible: "Once you start a new conversation.*" + +# The XMPP reconnection happens in a background service after the UI above is already +# showing, so poll (rather than a single snapshot check) for these lines. + +# Quick start picks an HT-*/HT2-* (FAST) mechanism, not a password-based one +- runScript: + file: scripts/checkForLogs.js + env: + PATTERN: 'jane@example\.org.*quick start with HT' + MAX_ATTEMPTS: 30 + DELAY_MS: 1000 + +- runScript: + file: scripts/checkForLogs.js + env: + PATTERN: 'jane@example\.org.*logged in \(using SASL_2\)' + MAX_ATTEMPTS: 30 + DELAY_MS: 1000 diff --git a/build/ci/conversations/flows/scripts/checkForLogs.js b/build/ci/conversations/flows/scripts/checkForLogs.js index 41229cb990..14bab24a8b 100644 --- a/build/ci/conversations/flows/scripts/checkForLogs.js +++ b/build/ci/conversations/flows/scripts/checkForLogs.js @@ -1,10 +1,32 @@ +function sleep(ms) { + const end = Date.now() + ms; + while (Date.now() < end) { /* busy-wait */ } +} + +function findMatchingLines(pattern) { + const res = http.get('http://localhost:17777/assert?pattern=' + encodeURIComponent(pattern)); + if (res.status === 404) return null; + if (!res.ok) throw new Error('Sidecar error: ' + res.status); + return JSON.parse(res.body).lines; +} + +function pollForMatchingLines(pattern, maxAttempts, delayMs) { + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + const lines = findMatchingLines(pattern); + if (lines) return lines; + if (attempt < maxAttempts) sleep(delayMs); + } + return null; +} + const pattern = typeof PATTERN !== 'undefined' ? PATTERN : undefined; if (!pattern) throw new Error('PATTERN env var is required'); -const res = http.get('http://localhost:17777/assert?pattern=' + encodeURIComponent(pattern)); -if (res.status === 404) throw new Error('Expected log line not found: ' + pattern); -if (!res.ok) throw new Error('Sidecar error: ' + res.status); +const maxAttempts = typeof MAX_ATTEMPTS !== 'undefined' ? parseInt(MAX_ATTEMPTS, 10) : 1; +const delayMs = typeof DELAY_MS !== 'undefined' ? parseInt(DELAY_MS, 10) : 1000; + +const matchedLines = pollForMatchingLines(pattern, maxAttempts, delayMs); +if (!matchedLines) throw new Error('Expected log line not found: ' + pattern); -const body = JSON.parse(res.body); -output.matchedLines = body.lines; -body.lines.forEach(function(line) { console.log(line); }); +output.matchedLines = matchedLines; +matchedLines.forEach(function(line) { console.log(line); }); From 1f025cec4fccee5f58f6f5aae061de179d965a0f Mon Sep 17 00:00:00 2001 From: Dan Caseley Date: Sat, 12 Sep 2026 17:31:10 +0100 Subject: [PATCH 2/3] OF-3366: Refactor e2e flows to reduce duplication --- build/ci/conversations/flows/README.md | 14 +++++++ build/ci/conversations/flows/bind2.yaml | 41 ++---------------- build/ci/conversations/flows/cb.yaml | 30 +------------ build/ci/conversations/flows/fast.yaml | 40 ++---------------- build/ci/conversations/flows/sasl2.yaml | 42 ++----------------- build/ci/conversations/flows/simple.yaml | 40 ++---------------- .../subflows/dismiss-onboarding-dialogs.yaml | 12 ++++++ .../conversations/flows/subflows/login.yaml | 30 +++++++++++++ 8 files changed, 71 insertions(+), 178 deletions(-) create mode 100644 build/ci/conversations/flows/subflows/dismiss-onboarding-dialogs.yaml create mode 100644 build/ci/conversations/flows/subflows/login.yaml diff --git a/build/ci/conversations/flows/README.md b/build/ci/conversations/flows/README.md index 3bc4bd711b..e01cfd7064 100644 --- a/build/ci/conversations/flows/README.md +++ b/build/ci/conversations/flows/README.md @@ -105,6 +105,20 @@ for the full API reference. The `LOGCAT_TAGS` default filters to Conversations output only — override to capture additional tags if needed. +## Reusing steps across flows: subflows + +Common sequences (e.g. logging in) live in `subflows/` and are pulled into a flow with `runFlow`: + +```yaml +- runFlow: + file: subflows/login.yaml +``` + +Subflow files need an `appId` header and a `---` separator like any flow, but no `tags` - +that's what keeps them from being picked up as a standalone flow when Maestro scans the +`flows/` directory. A `runScript` path inside a subflow resolves relative to the subflow's +own directory, not the calling flow's, hence `../scripts/...` inside `subflows/*.yaml`. + ## Writing regex patterns Use `checkForLogs.js` in a flow: diff --git a/build/ci/conversations/flows/bind2.yaml b/build/ci/conversations/flows/bind2.yaml index b3e5f43b4d..733215219d 100644 --- a/build/ci/conversations/flows/bind2.yaml +++ b/build/ci/conversations/flows/bind2.yaml @@ -11,45 +11,11 @@ onFlowComplete: - runScript: scripts/startSession.js -- launchApp: - clearState: true - -- assertVisible: "I already have an account" - -- runScript: - file: scripts/checkForLogs.js - env: - PATTERN: initializing database.* - -- tapOn: "I already have an account" -- tapOn: "More options" -- tapOn: "Settings" -- tapOn: "Connection" -- tapOn: "Show extended connection settings when setting up an account" -- tapOn: "Navigate up" -- tapOn: "Navigate up" - -- tapOn: "XMPP address" -- inputText: "jane@example.org" -- tapOn: "Password" -- inputText: "secret" -- tapOn: "Hostname" -- inputText: "10.0.2.2" -- tapOn: "Next" - -- assertVisible: "Accept Unknown Certificate?" -- tapOn: "Always" - -- assertVisible: "Publish avatar" -- tapOn: "Skip" +- runFlow: + file: subflows/login.yaml - runFlow: - when: - visible: "Battery optimizations enabled" - commands: - - tapOn: "Next" - - assertVisible: "Let app always run in background?" - - tapOn: "Allow" + file: subflows/dismiss-onboarding-dialogs.yaml # Received Bind2 features - runScript: @@ -79,4 +45,3 @@ onFlowComplete: id: "server_info_bind2" text: "available" - takeScreenshot: server_info - diff --git a/build/ci/conversations/flows/cb.yaml b/build/ci/conversations/flows/cb.yaml index f19da4e37d..7cbc8c3549 100644 --- a/build/ci/conversations/flows/cb.yaml +++ b/build/ci/conversations/flows/cb.yaml @@ -8,34 +8,8 @@ onFlowStart: - runScript: scripts/startSession.js -- launchApp: - clearState: true - -- assertVisible: "I already have an account" - -- runScript: - file: scripts/checkForLogs.js - env: - PATTERN: initializing database.* - -- tapOn: "I already have an account" -- tapOn: "More options" -- tapOn: "Settings" -- tapOn: "Connection" -- tapOn: "Show extended connection settings when setting up an account" -- tapOn: "Navigate up" -- tapOn: "Navigate up" - -- tapOn: "XMPP address" -- inputText: "jane@example.org" -- tapOn: "Password" -- inputText: "secret" -- tapOn: "Hostname" -- inputText: "10.0.2.2" -- tapOn: "Next" - -- assertVisible: "Accept Unknown Certificate?" -- tapOn: "Always" +- runFlow: + file: subflows/login.yaml - runScript: file: scripts/checkForLogs.js diff --git a/build/ci/conversations/flows/fast.yaml b/build/ci/conversations/flows/fast.yaml index 09a36b0ff8..36b003a15b 100644 --- a/build/ci/conversations/flows/fast.yaml +++ b/build/ci/conversations/flows/fast.yaml @@ -8,45 +8,11 @@ onFlowStart: - runScript: scripts/startSession.js -- launchApp: - clearState: true - -- assertVisible: "I already have an account" - -- runScript: - file: scripts/checkForLogs.js - env: - PATTERN: initializing database.* - -- tapOn: "I already have an account" -- tapOn: "More options" -- tapOn: "Settings" -- tapOn: "Connection" -- tapOn: "Show extended connection settings when setting up an account" -- tapOn: "Navigate up" -- tapOn: "Navigate up" - -- tapOn: "XMPP address" -- inputText: "jane@example.org" -- tapOn: "Password" -- inputText: "secret" -- tapOn: "Hostname" -- inputText: "10.0.2.2" -- tapOn: "Next" - -- assertVisible: "Accept Unknown Certificate?" -- tapOn: "Always" - -- assertVisible: "Publish avatar" -- tapOn: "Skip" +- runFlow: + file: subflows/login.yaml - runFlow: - when: - visible: "Battery optimizations enabled" - commands: - - tapOn: "Next" - - assertVisible: "Let app always run in background?" - - tapOn: "Allow" + file: subflows/dismiss-onboarding-dialogs.yaml - assertVisible: "Once you start a new conversation.*" diff --git a/build/ci/conversations/flows/sasl2.yaml b/build/ci/conversations/flows/sasl2.yaml index 5f752e5ce7..8be38a6677 100644 --- a/build/ci/conversations/flows/sasl2.yaml +++ b/build/ci/conversations/flows/sasl2.yaml @@ -8,48 +8,14 @@ onFlowStart: - runScript: scripts/startSession.js -- launchApp: - clearState: true - -- assertVisible: "I already have an account" - -- runScript: - file: scripts/checkForLogs.js - env: - PATTERN: initializing database.* - -- tapOn: "I already have an account" -- tapOn: "More options" -- tapOn: "Settings" -- tapOn: "Connection" -- tapOn: "Show extended connection settings when setting up an account" -- tapOn: "Navigate up" -- tapOn: "Navigate up" - -- tapOn: "XMPP address" -- inputText: "jane@example.org" -- tapOn: "Password" -- inputText: "secret" -- tapOn: "Hostname" -- inputText: "10.0.2.2" -- tapOn: "Next" - -- assertVisible: "Accept Unknown Certificate?" -- tapOn: "Always" - -- assertVisible: "Publish avatar" -- tapOn: "Skip" +- runFlow: + file: subflows/login.yaml - runFlow: - when: - visible: "Battery optimizations enabled" - commands: - - tapOn: "Next" - - assertVisible: "Let app always run in background?" - - tapOn: "Allow" + file: subflows/dismiss-onboarding-dialogs.yaml # Using SASL2 -- runScript: +- runScript: file: scripts/checkForLogs.js env: PATTERN: .*SASL 2.0 authorization identifier was jane@example.org diff --git a/build/ci/conversations/flows/simple.yaml b/build/ci/conversations/flows/simple.yaml index 0f8125d6f7..6c04d0c12b 100644 --- a/build/ci/conversations/flows/simple.yaml +++ b/build/ci/conversations/flows/simple.yaml @@ -8,44 +8,10 @@ onFlowStart: - runScript: scripts/startSession.js -- launchApp: - clearState: true - -- assertVisible: "I already have an account" - -- runScript: - file: scripts/checkForLogs.js - env: - PATTERN: initializing database.* - -- tapOn: "I already have an account" -- tapOn: "More options" -- tapOn: "Settings" -- tapOn: "Connection" -- tapOn: "Show extended connection settings when setting up an account" -- tapOn: "Navigate up" -- tapOn: "Navigate up" - -- tapOn: "XMPP address" -- inputText: "jane@example.org" -- tapOn: "Password" -- inputText: "secret" -- tapOn: "Hostname" -- inputText: "10.0.2.2" -- tapOn: "Next" - -- assertVisible: "Accept Unknown Certificate?" -- tapOn: "Always" - -- assertVisible: "Publish avatar" -- tapOn: "Skip" +- runFlow: + file: subflows/login.yaml - runFlow: - when: - visible: "Battery optimizations enabled" - commands: - - tapOn: "Next" - - assertVisible: "Let app always run in background?" - - tapOn: "Allow" + file: subflows/dismiss-onboarding-dialogs.yaml - assertVisible: "Once you start a new conversation.*" diff --git a/build/ci/conversations/flows/subflows/dismiss-onboarding-dialogs.yaml b/build/ci/conversations/flows/subflows/dismiss-onboarding-dialogs.yaml new file mode 100644 index 0000000000..06ed0fbe6b --- /dev/null +++ b/build/ci/conversations/flows/subflows/dismiss-onboarding-dialogs.yaml @@ -0,0 +1,12 @@ +appId: eu.siacs.conversations +--- +- assertVisible: "Publish avatar" +- tapOn: "Skip" + +- runFlow: + when: + visible: "Battery optimizations enabled" + commands: + - tapOn: "Next" + - assertVisible: "Let app always run in background?" + - tapOn: "Allow" diff --git a/build/ci/conversations/flows/subflows/login.yaml b/build/ci/conversations/flows/subflows/login.yaml new file mode 100644 index 0000000000..beb3cce917 --- /dev/null +++ b/build/ci/conversations/flows/subflows/login.yaml @@ -0,0 +1,30 @@ +appId: eu.siacs.conversations +--- +- launchApp: + clearState: true + +- assertVisible: "I already have an account" + +- runScript: + file: ../scripts/checkForLogs.js + env: + PATTERN: initializing database.* + +- tapOn: "I already have an account" +- tapOn: "More options" +- tapOn: "Settings" +- tapOn: "Connection" +- tapOn: "Show extended connection settings when setting up an account" +- tapOn: "Navigate up" +- tapOn: "Navigate up" + +- tapOn: "XMPP address" +- inputText: "jane@example.org" +- tapOn: "Password" +- inputText: "secret" +- tapOn: "Hostname" +- inputText: "10.0.2.2" +- tapOn: "Next" + +- assertVisible: "Accept Unknown Certificate?" +- tapOn: "Always" From e9299f27ec113b93acf0c05b6a1386376c6cc3bc Mon Sep 17 00:00:00 2001 From: Dan Caseley Date: Sat, 12 Sep 2026 17:49:18 +0100 Subject: [PATCH 3/3] OF-3366: Add a test for FAST token rotation --- .../conversationstest-action/run-tests.sh | 2 +- .../continuous-integration-workflow.yml | 3 + .../conversations/configs/fast-rotation.xml | 96 +++++++++++++++++ build/ci/conversations/flows/README.md | 5 + .../ci/conversations/flows/fast-rotation.yaml | 100 ++++++++++++++++++ 5 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 build/ci/conversations/configs/fast-rotation.xml create mode 100644 build/ci/conversations/flows/fast-rotation.yaml diff --git a/.github/actions/conversationstest-action/run-tests.sh b/.github/actions/conversationstest-action/run-tests.sh index 590a12e90a..043ae026b6 100755 --- a/.github/actions/conversationstest-action/run-tests.sh +++ b/.github/actions/conversationstest-action/run-tests.sh @@ -5,7 +5,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" if [ -z "${1:-}" ]; then echo "Usage: run-tests.sh " >&2 - echo "Available tags: demoboot, sasl2" >&2 + echo "Available tags: demoboot, sasl2, fast-rotation" >&2 exit 1 fi INCLUDE_TAGS="$1" diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index 596f82b42f..a4b8e23033 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -354,6 +354,9 @@ jobs: - name: sasl2 maestro-tags: sasl2 config-file: build/ci/conversations/configs/sasl2.xml + - name: fast-rotation + maestro-tags: fast-rotation + config-file: build/ci/conversations/configs/fast-rotation.xml steps: - name: Checkout local actions and test flows # Do this _before_ untarring the distribution, as the checkout will empty the directory prior to the checkout! diff --git a/build/ci/conversations/configs/fast-rotation.xml b/build/ci/conversations/configs/fast-rotation.xml new file mode 100644 index 0000000000..69855e1491 --- /dev/null +++ b/build/ci/conversations/configs/fast-rotation.xml @@ -0,0 +1,96 @@ + + + + 9090 + 9091 + + + org.jivesoftware.database.EmbeddedConnectionProvider + + + true + en + + + true + + example.org + example.org + + + embedded + + + admin@example.com + admin + + + + john + secret + John Doe + john.doe@example.com + + + jane@example.org + Jane + + + juan@example.org + Juan + + + + + jane + secret + Jane Doe + jane.doe@example.com + + + john@example.org + John + + + juan@example.org + Juan + + + + + juan + secret + Juan Doe + juan.doe@example.com + + + john@example.org + John + + + jane@example.org + Jane + + + + + + + + + true + + + + + 1 + 48 + + + + diff --git a/build/ci/conversations/flows/README.md b/build/ci/conversations/flows/README.md index e01cfd7064..f70d85c1c4 100644 --- a/build/ci/conversations/flows/README.md +++ b/build/ci/conversations/flows/README.md @@ -37,6 +37,11 @@ Flows are tagged by which config they need, not by feature: `fast.yaml` is tagge and reuses `configs/sasl2.xml`, since FAST (XEP-0484) is only advertised as a SASL2 inline feature and needs no config of its own — `xmpp.fast.enabled` defaults to `true`. +`fast-rotation.yaml` needs its own config (`configs/fast-rotation.xml`): FAST token rotation +happens once the time left before expiry drops to or below the rotation threshold, but the +real defaults are multi-day, so the config sets the threshold >= the expiry to make rotation +happen on every reconnect instead of waiting on the clock. + ### 2. Start an Android emulator Launch an emulator with API 34 and x86_64 architecture. With Maestro installed, you can create or diff --git a/build/ci/conversations/flows/fast-rotation.yaml b/build/ci/conversations/flows/fast-rotation.yaml new file mode 100644 index 0000000000..8e1c714d08 --- /dev/null +++ b/build/ci/conversations/flows/fast-rotation.yaml @@ -0,0 +1,100 @@ +appId: eu.siacs.conversations +name: FAST token rotation +tags: + - fast-rotation +onFlowStart: + - runScript: scripts/checkHealth.js +--- + +- runScript: scripts/startSession.js + +- runFlow: + file: subflows/login.yaml + +- runFlow: + file: subflows/dismiss-onboarding-dialogs.yaml + +- assertVisible: "Once you start a new conversation.*" + +# The initial, password-based login should obtain and cache a FAST (XEP-0484) token +- runScript: + file: scripts/checkForLogs.js + env: + PATTERN: 'jane@example\.org: storing hashed token Mechanism\[' + +# A freshly issued token starts in a "new" slot, and is only promoted to "current" the first +# time it's actually used - the rotation-threshold check (see configs/fast-rotation.xml, which +# forces it to always be due) only applies to a "current" token. So it's this token's SECOND +# use, not its first, where the server should rotate it. + +# Reconnect #1: first use of the initial token - promotes it, no rotation expected yet. +- runScript: scripts/startSession.js + +- launchApp + +- assertVisible: "Once you start a new conversation.*" + +- runScript: + file: scripts/checkForLogs.js + env: + PATTERN: 'jane@example\.org.*quick start with HT' + MAX_ATTEMPTS: 30 + DELAY_MS: 1000 + +- runScript: + file: scripts/checkForLogs.js + env: + PATTERN: 'jane@example\.org.*logged in \(using SASL_2\)' + MAX_ATTEMPTS: 30 + DELAY_MS: 1000 + +# Reconnect #2: second use of that same token - now "current", so the server should rotate it. +- runScript: scripts/startSession.js + +- launchApp + +- assertVisible: "Once you start a new conversation.*" + +- runScript: + file: scripts/checkForLogs.js + env: + PATTERN: 'jane@example\.org.*quick start with HT' + MAX_ATTEMPTS: 30 + DELAY_MS: 1000 + +- runScript: + file: scripts/checkForLogs.js + env: + PATTERN: 'jane@example\.org.*logged in \(using SASL_2\)' + MAX_ATTEMPTS: 30 + DELAY_MS: 1000 + +- runScript: + file: scripts/checkForLogs.js + env: + PATTERN: 'jane@example\.org: storing hashed token Mechanism\[' + MAX_ATTEMPTS: 30 + DELAY_MS: 1000 + +# Reconnect #3: proves the rotated token was actually persisted and is usable - if rotation had +# silently broken (e.g. the replacement was never stored, or was stored but unusable), this +# reconnect would fail FAST and fall back to a password prompt instead. +- runScript: scripts/startSession.js + +- launchApp + +- assertVisible: "Once you start a new conversation.*" + +- runScript: + file: scripts/checkForLogs.js + env: + PATTERN: 'jane@example\.org.*quick start with HT' + MAX_ATTEMPTS: 30 + DELAY_MS: 1000 + +- runScript: + file: scripts/checkForLogs.js + env: + PATTERN: 'jane@example\.org.*logged in \(using SASL_2\)' + MAX_ATTEMPTS: 30 + DELAY_MS: 1000