feat(lambdatest): Integrate TestMU hub connection with tunnel, mirroring SauceLabs/BrowserStack#116
feat(lambdatest): Integrate TestMU hub connection with tunnel, mirroring SauceLabs/BrowserStack#116Winify wants to merge 3 commits into
Conversation
Greptile SummaryThis PR adds TestMu (LambdaTest) as a third cloud provider alongside BrowserStack and SauceLabs, following the same
Confidence Score: 4/5Safe to merge after fixing the osVersion gap — no other correctness issues found. The src/providers/cloud/testmu.provider.ts — the browser-platform branch of buildCapabilities needs to forward osVersion into lt:options.platformVersion.
|
| Filename | Overview |
|---|---|
| src/providers/cloud/testmu.provider.ts | New TestMuProvider implementing SessionProvider. osVersion is accepted by the schema and shown in the README but never placed in lt:options.platformVersion for browser sessions, causing silent capability mismatch. |
| src/tools/cloud-provider.tool.ts | Adds TestMu to list_apps/upload_app with dual-platform fetch for Android+iOS. Error surfacing when both platforms fail is correct; one-platform-success gracefully drops the failed-platform error (intentional by design per PR description). |
| src/resources/testmu-local.resource.ts | New resource exposing TestMu tunnel binary download URL and setup steps, matching the BrowserStack/SauceLabs pattern. macOS always returns the Intel x64 binary (with a Rosetta 2 note for ARM64). |
| src/recording/code-generator.ts | Adds LambdaTest code-gen branch (try/catch/finally with REST status update or execute-based for mobile). Minor: the fallback tunnel name in generated code ('wdio-mcp-tunnel') differs from the one session.tool.ts generates at runtime, but this only triggers for edge-case histories where lt:options.tunnelName isn't recorded. |
| src/tools/session.tool.ts | Adds testmu to provider enum and testmuLocal legacy flag. Normalized effectiveTunnel correctly handles 'external' (does not auto-start tunnel; capabilities still carry lt:options.tunnel: true). |
| src/providers/cloud/saucelabs.provider.ts | Minor refactor: SauceLabs import changed from dynamic await import('saucelabs') to a static top-level import. No behavioral change; module now loads at startup rather than lazily. |
| tests/providers/testmu.provider.test.ts | Comprehensive test coverage for getConnectionConfig, buildCapabilities (browser, mobile browser, native app), getSessionType, and onSessionClose (REST PATCH + execute paths). All edge cases covered. |
| tests/tools/cloud-provider.tool.test.ts | Adds TestMu list_apps and upload_app tests covering happy-path, dual-platform fetch, credential errors, and HTTP errors. All verified with mock fetch. |
Sequence Diagram
sequenceDiagram
participant C as MCP Client
participant ST as session.tool.ts
participant TMP as TestMuProvider
participant LT as LambdaTest Hub
participant LTAPI as LambdaTest REST API
C->>ST: start_session(provider:'testmu', tunnel:true, ...)
ST->>TMP: getConnectionConfig(options)
TMP-->>ST: "{hostname:'hub.lambdatest.com', user, key}"
ST->>TMP: "buildCapabilities({...args, tunnelName})"
TMP-->>ST: "{browserName, platformName, lt:options:{tunnel:true,...}}"
ST->>TMP: "startTunnel({tunnelName})"
TMP->>LT: LambdaTunnel.start(user, key, tunnelName)
LT-->>TMP: tunnel handle
TMP-->>ST: tunnelHandle
ST->>LT: "remote({...config, capabilities})"
LT-->>ST: sessionId
C->>ST: close_session()
ST->>TMP: onSessionClose(sessionId, 'browser', result)
TMP->>LTAPI: "PATCH /automation/api/v1/sessions/{sessionId}"
LTAPI-->>TMP: 200 OK
ST->>LT: browser.deleteSession()
ST->>TMP: stopTunnel(tunnelHandle)
TMP->>LT: tunnel.stop()
Reviews (3): Last reviewed commit: "fix(testmu): Address code review finding..." | Re-trigger Greptile
…ing SauceLabs/BrowserStack - Adding LambdaTunnel declaration to types - Intorducing resources for tunnel setup
6ec8417 to
c76e4da
Compare
|
|
||
| // Mobile browser/emulator mode (e.g. Chrome on Android emulator) | ||
| if (mobileBrowser) { | ||
| ltOptions.appiumVersion = '2.11.0'; |
There was a problem hiding this comment.
Don't hardcode appiumVersion for the virtual mobile-browser path. TestMu AI rejects a pinned version here with The Device/Appium version combination is not supported, I got this while testing this PR locally. Both 2.11.0 and latest fail, while omitting it succeeds (hub auto-selects).
Though, native real-device mode below accepts latest.
There was a problem hiding this comment.
Thank you! Could not get a trial yet, so this code was "best effort guessing" based on the other 2
| const body = { status_ind: result.status === 'passed' ? 'passed' : 'failed' }; | ||
| const apiUrl = `https://api.lambdatest.com/automation/api/v1/sessions/${sessionId}`; |
There was a problem hiding this comment.
This REST PATCH only works for web sessions. For mobile, the WebDriver sessionId isn't addressable here - it 404s (Either resource not found or already deleted); mobile sessions live under mobile-api.lambdatest.com keyed by an internal test_id, so status is silently never recorded.
Branch by sessionType: web keeps this PATCH; mobile (ios/android) should use the live handle - await browser.execute('lambda-status=' + status) (the hub intercepts it server-side; verified). The hook doesn't work for web, hence the split.
| " const ltAuth = Buffer.from(`${process.env.TESTMU_USERNAME}:${process.env.TESTMU_ACCESS_KEY}`).toString('base64');", | ||
| " await fetch('https://api.lambdatest.com/automation/api/v1/sessions/' + browser.sessionId, {", | ||
| " method: 'PATCH',", | ||
| " headers: { Authorization: 'Basic ' + ltAuth, 'Content-Type': 'application/json' },", | ||
| ' body: JSON.stringify({ status_ind: ltStatus })', |
There was a problem hiding this comment.
Same issue as the provider's onSessionClose, in the generated script: this REST PATCH 404s for real-device mobile sessions. Branch on history.type - emit await browser.execute('lambda-status=' + ltStatus) for ios/android, and keep the REST PATCH only for browser.
Something like:
const isMobile = history.type !== 'browser';
const statusUpdate = isMobile
? " await browser.execute('lambda-status=' + ltStatus);"
: [
" const ltAuth = Buffer.from(`${process.env.TESTMU_USERNAME}:${process.env.TESTMU_ACCESS_KEY}`).toString('base64');",
" await fetch('https://api.lambdatest.com/automation/api/v1/sessions/' + browser.sessionId, {",
" method: 'PATCH',",
" headers: { Authorization: 'Basic ' + ltAuth, 'Content-Type': 'application/json' },",
' body: JSON.stringify({ status_ind: ltStatus })',
' });',
].join('\n');| if (process.env.TESTMU_USERNAME) ltOptions.username = process.env.TESTMU_USERNAME; | ||
| if (process.env.TESTMU_ACCESS_KEY) ltOptions.accessKey = process.env.TESTMU_ACCESS_KEY; |
There was a problem hiding this comment.
I think we can drop these 2 as getConnectionConfig() already passes user/key at the connection level, I verified that it works fine without passing them as caps and just having them as env vars.
It could also leak the creds wherever the capabilities is parsed.
…tatus, credential deduplication
Proposed changes
New Provider (src/providers/cloud/testmu.provider.ts)
TestMuProvider implements the SessionProvider interface with three capability modes (browser, mobile browser/emulator, mobile native app), tunnel lifecycle via
@lambdatest/node-tunnel, and session status reporting via LambdaTest REST API. UsesTESTMU_USERNAME/TESTMU_ACCESS_KEYenvironment variables, credentials never appear in tool call parameters.App Management (src/tools/cloud-provider.tool.ts)
Tunnel Binary Resource (src/resources/testmu-local.resource.ts)
wdio://testmu/local-binaryresource with platform-specific LambdaTest Tunnel binary download URLs and setup commands, matching the BrowserStack/Sauce Labs pattern.Closing #104
Types of changes
Checklist
Further comments
Reviewers: @webdriverio/project-committers