Skip to content

Add macOS connect smoke test#8898

Open
atavism wants to merge 4 commits into
mainfrom
atavism/macos-connect
Open

Add macOS connect smoke test#8898
atavism wants to merge 4 commits into
mainfrom
atavism/macos-connect

Conversation

@atavism

@atavism atavism commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Resolves https://github.com/getlantern/engineering/issues/3670

Adds macOS connect test for the real VPN/System Extension flow: hidden app commands for extension status/activation, Flutter integration-test keys, a macOS connect smoke test, and manual CI path

Summary by CodeRabbit

  • New Features

    • Added macOS VPN connect/disconnect smoke testing to verify connection flow during builds and manual runs.
    • Expanded smoke test options to support public IP checks and full-tunnel validation.
    • Added better macOS system extension status handling during startup and testing.
  • Bug Fixes

    • Improved failure diagnostics with logs, screenshots, and system state capture when macOS smoke tests fail.
    • Added clearer error reporting for approval, reboot, timeout, and activation issues.

Copilot AI review requested due to automatic review settings July 8, 2026 03:53
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a macOS VPN connect/disconnect smoke test capability: a native Swift system-extension smoke CLI command, a bash orchestration script capturing diagnostics, CI workflow inputs/jobs to run it, and a Flutter integration test polling extension readiness before exercising connect/disconnect flow with stable UI keys.

Changes

macOS Connect/Disconnect Smoke Test

Layer / File(s) Summary
System extension smoke command
macos/Runner/VPN/SystemExtensionSmokeCommand.swift, macos/Runner/VPN/SystemExtensionManager.swift
Adds CLI parsing, JSON result serialization, and a Combine-based runner that drives SystemExtensionManager status/activation checks with timeout handling and exit-code mapping.
AppDelegate and Xcode project wiring
macos/Runner/AppDelegate.swift, macos/Runner.xcodeproj/project.pbxproj
Reorders startup to parse the smoke command after filesystem setup, adds a runner property, and registers the new Swift file in the Xcode build phases.
Swift unit tests
macos/RunnerTests/RunnerTests.swift
Adds tests covering command parsing (valid/invalid args, timeout formats) and result exit-code/JSON output.
macOS smoke suite script
.github/scripts/macos_smoke_suite.sh
New bash script resolving the app, running extension preflight and Flutter connect smoke test, capturing diagnostics/logs/screenshots, and verifying PacketTunnel exits.
CI workflow wiring
.github/workflows/app-smoke-tests.yml, .github/workflows/build-macos.yml
Adds a macos platform option, macos_connect_smoke input, a new macos job, and build-macos steps to run the smoke script and upload diagnostics artifacts.
Flutter integration test and UI keys
integration_test/vpn/macos_connect_smoke_test.dart, integration_test/vpn/connect_smoke_harness.dart, integration_test/vpn/vpn_smoke_helpers.dart, lib/features/macos_extension/macos_extension_dialog.dart
Adds the integration test polling extension readiness, a requireTrafficAfterConnect harness flag with post-connect IP check, debug key collection for macos_extension.*, and stable widget keys in the dialog.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

  • getlantern/engineering#3670: Directly implements the macOS connect/disconnect smoke test, extension approval handling, and diagnostics collection described in this issue.

Possibly related PRs

  • getlantern/lantern#8895: Both PRs modify .github/workflows/app-smoke-tests.yml to extend the workflow dispatch/job matrix and connect-smoke toggles for desktop smoke tests.

Suggested reviewers: myleshorton, jigar-f

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding a macOS connect smoke test.
Linked Issues check ✅ Passed The PR adds the macOS smoke test, real UI flow, traffic checks, failure artifacts, and a manual workflow path as requested.
Out of Scope Changes check ✅ Passed The changes are focused on enabling the macOS smoke test and its supporting app/workflow plumbing, with no obvious unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch atavism/macos-connect

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in macOS connect/disconnect smoke test that exercises the real VPN System Extension flow, including a hidden macOS app command for extension status/activation, Flutter integration-test hooks, and CI wiring to run the suite and upload diagnostics.

Changes:

  • Introduces a macOS “smoke command” CLI entry point (status/activate with timeout) and runner that reuses the existing SystemExtensionManager.
  • Adds a macOS integration test (macos_connect_smoke_test.dart) and UI debug keys to reliably detect System Extension readiness/failure states.
  • Adds an opt-in CI path (workflows + script) to run the macOS smoke suite and upload diagnostics artifacts.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
macos/RunnerTests/RunnerTests.swift Adds unit tests covering smoke-command parsing and structured output/exit codes.
macos/Runner/VPN/SystemExtensionSmokeCommand.swift Adds CLI parsing, JSON output helpers, and a runner around SystemExtensionManager status.
macos/Runner/VPN/SystemExtensionManager.swift Adds Combine import to support @Published usage consumed by the smoke runner.
macos/Runner/AppDelegate.swift Adds early smoke-command handling at launch (headless accessory mode + exit codes).
macos/Runner.xcodeproj/project.pbxproj Wires the new Swift source file into the macOS target build.
lib/features/macos_extension/macos_extension_dialog.dart Adds stable widget keys for integration tests to observe extension UI/status.
integration_test/vpn/vpn_smoke_helpers.dart Includes macos_extension.* keys in debug-key collection for diagnostics.
integration_test/vpn/macos_connect_smoke_test.dart Adds a macOS VPN connect/disconnect integration smoke test with extension readiness gating.
integration_test/vpn/connect_smoke_harness.dart Adds requireTrafficAfterConnect option to force a post-connect traffic check.
.github/workflows/build-macos.yml Adds inputs and an opt-in step to run the macOS smoke suite + upload diagnostics.
.github/workflows/app-smoke-tests.yml Adds a macOS platform option and opt-in macOS smoke inclusion for “all”.
.github/scripts/macos_smoke_suite.sh Adds the macOS smoke suite runner script (preflight sys-ext activation + Flutter test + diagnostics).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +83 to +95
let valueIndex = arguments.index(after: timeoutIndex)
guard valueIndex < arguments.endIndex else {
return .failure(
SystemExtensionSmokeParseError(message: "\(timeoutFlag) needs a value")
)
}

guard let parsed = TimeInterval(arguments[valueIndex]), parsed > 0 else {
return .failure(
SystemExtensionSmokeParseError(message: "\(timeoutFlag) must be a positive number")
)
}
timeout = parsed
@atavism atavism requested a review from jigar-f July 8, 2026 04:01

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
macos/RunnerTests/RunnerTests.swift (2)

594-631: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add missing timeout parsing edge-case tests

The error-case tests cover conflicting flags, non-positive values, and cross-form duplicates. Missing cases that exercise distinct code paths in parseTimeout:

  • --timeout-seconds as the last argument (no value) → should fail with "needs a value"
  • Non-numeric value (e.g., --timeout-seconds abc) → should fail with "positive number"
  • Same-form duplicates: --timeout-seconds 30 --timeout-seconds 45 (space-space) and --timeout-seconds=30 --timeout-seconds=45 (equals-equals)
🧪 Proposed additional test cases
     switch SystemExtensionSmokeCommand.parse(
       arguments: [
         "Lantern",
         "--smoke-system-extension-status",
         "--timeout-seconds",
         "30",
         "--timeout-seconds=45",
       ]
     ) {
     case .failure(let error):
       XCTAssertTrue(error.message.contains("can only be set once"))
     default:
       XCTFail("Expected duplicate timeout arguments to fail")
     }
+
+    // Missing value (flag is last argument)
+    switch SystemExtensionSmokeCommand.parse(
+      arguments: ["Lantern", "--smoke-system-extension-status", "--timeout-seconds"]
+    ) {
+    case .failure(let error):
+      XCTAssertTrue(error.message.contains("needs a value"))
+    default:
+      XCTFail("Expected missing timeout value to fail")
+    }
+
+    // Non-numeric value
+    switch SystemExtensionSmokeCommand.parse(
+      arguments: ["Lantern", "--smoke-system-extension-status", "--timeout-seconds", "abc"]
+    ) {
+    case .failure(let error):
+      XCTAssertTrue(error.message.contains("positive number"))
+    default:
+      XCTFail("Expected non-numeric timeout to fail")
+    }
+
+    // Same-form duplicate (space-separated)
+    switch SystemExtensionSmokeCommand.parse(
+      arguments: [
+        "Lantern",
+        "--smoke-system-extension-status",
+        "--timeout-seconds",
+        "30",
+        "--timeout-seconds",
+        "45",
+      ]
+    ) {
+    case .failure(let error):
+      XCTAssertTrue(error.message.contains("can only be set once"))
+    default:
+      XCTFail("Expected duplicate space-form timeout to fail")
+    }
+
+    // Same-form duplicate (equals-separated)
+    switch SystemExtensionSmokeCommand.parse(
+      arguments: [
+        "Lantern",
+        "--smoke-system-extension-status",
+        "--timeout-seconds=30",
+        "--timeout-seconds=45",
+      ]
+    ) {
+    case .failure(let error):
+      XCTAssertTrue(error.message.contains("can only be set once"))
+    default:
+      XCTFail("Expected duplicate equals-form timeout to fail")
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@macos/RunnerTests/RunnerTests.swift` around lines 594 - 631, The timeout
parsing tests in SystemExtensionSmokeCommand are missing edge cases that
exercise distinct parseTimeout paths. Extend
testSystemExtensionSmokeCommandRejectsInvalidArguments to cover a trailing
--timeout-seconds with no value, a non-numeric timeout value, and same-form
duplicate timeout arguments for both space-separated and equals-separated
styles. Verify each case matches the expected failure message so parseTimeout’s
validation and duplicate detection are fully covered.

633-666: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider testing more exit-code mappings and the timeoutSeconds JSON field

The exit-code tests cover .activated (0), .requiresApproval (20), .requiresReboot (21), and .timedOut (124) for the activate action. Missing: .installed (0), .error (1), and status-action exit codes. Additionally, the JSON test doesn't verify the timeoutSeconds field that's included when status == .timedOut.

🧪 Proposed additional assertions
     XCTAssertEqual(
       SystemExtensionSmokeResult(
         action: .activate,
         status: .timedOut,
         timeout: 120
       ).exitCode,
       124
     )
+
+    XCTAssertEqual(
+      SystemExtensionSmokeResult(
+        action: .activate,
+        status: .installed,
+        timeout: 120
+      ).exitCode,
+      0
+    )
+
+    XCTAssertEqual(
+      SystemExtensionSmokeResult(
+        action: .activate,
+        status: .error,
+        timeout: 120
+      ).exitCode,
+      1
+    )
+
+    // Status action exit codes
+    XCTAssertEqual(
+      SystemExtensionSmokeResult(
+        action: .status,
+        status: .notInstalled,
+        timeout: 120
+      ).exitCode,
+      0
+    )
+
+    XCTAssertEqual(
+      SystemExtensionSmokeResult(
+        action: .status,
+        status: .error,
+        timeout: 120
+      ).exitCode,
+      1
+    )
   }
+
+  func testSystemExtensionSmokeResultIncludesTimeoutSecondsWhenTimedOut() throws {
+    let result = SystemExtensionSmokeResult(
+      action: .activate,
+      status: .timedOut,
+      timeout: 45
+    )
+
+    let data = try XCTUnwrap(result.jsonLine.data(using: .utf8))
+    let payload = try XCTUnwrap(
+      JSONSerialization.jsonObject(with: data) as? [String: Any]
+    )
+
+    XCTAssertEqual(payload["timeoutSeconds"] as? Double, 45)
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@macos/RunnerTests/RunnerTests.swift` around lines 633 - 666, The current
`SystemExtensionSmokeResult` coverage only checks a subset of the `exitCode`
mappings for `.activate`; extend the
`testSystemExtensionSmokeResultUsesDistinctActivationExitCodes` style coverage
to include the missing `.installed` and `.error` cases as well as the
status/action-specific exit codes so the mapping is fully exercised. Also update
the JSON serialization test for `SystemExtensionSmokeResult` to assert the
`timeoutSeconds` field is present and correct when `status` is `.timedOut`,
using the existing `SystemExtensionSmokeResult` and related status/action
symbols to keep the assertions aligned with the model behavior.
.github/workflows/app-smoke-tests.yml (1)

114-127: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Consider explicit secret forwarding instead of secrets: inherit.

secrets: inherit passes all repository secrets to build-macos.yml. While the called workflow needs several Apple secrets, explicitly forwarding only the required secrets would follow least-privilege principles and prevent accidental exposure of unrelated secrets if the called workflow is modified in the future.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/app-smoke-tests.yml around lines 114 - 127, The macOS job
in app-smoke-tests is using broad secret inheritance, which gives the called
build-macos workflow access to every repository secret. Update the macos job to
forward only the specific secrets required by build-macos.yml instead of using
secrets: inherit, keeping the existing with inputs and the workflow call intact
while applying least-privilege secret handling.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@macos/Runner/VPN/SystemExtensionSmokeCommand.swift`:
- Around line 198-201: The smoke command can finish too early because
SystemExtensionSmokeCommand.handle(manager.status) is being called whenever
manager.initialized is already true, even if that status came from a cached
singleton query. Update the command submission flow in the logic around
commandSubmitted and handle(_:), and only complete based on the request-specific
result from checkInstallationStatus() or activateExtension() rather than the
current manager.status. Make sure the active request path waits for the fresh
status/activation callback before calling the completion logic so .status and
.activate cannot resolve from stale state.

---

Nitpick comments:
In @.github/workflows/app-smoke-tests.yml:
- Around line 114-127: The macOS job in app-smoke-tests is using broad secret
inheritance, which gives the called build-macos workflow access to every
repository secret. Update the macos job to forward only the specific secrets
required by build-macos.yml instead of using secrets: inherit, keeping the
existing with inputs and the workflow call intact while applying least-privilege
secret handling.

In `@macos/RunnerTests/RunnerTests.swift`:
- Around line 594-631: The timeout parsing tests in SystemExtensionSmokeCommand
are missing edge cases that exercise distinct parseTimeout paths. Extend
testSystemExtensionSmokeCommandRejectsInvalidArguments to cover a trailing
--timeout-seconds with no value, a non-numeric timeout value, and same-form
duplicate timeout arguments for both space-separated and equals-separated
styles. Verify each case matches the expected failure message so parseTimeout’s
validation and duplicate detection are fully covered.
- Around line 633-666: The current `SystemExtensionSmokeResult` coverage only
checks a subset of the `exitCode` mappings for `.activate`; extend the
`testSystemExtensionSmokeResultUsesDistinctActivationExitCodes` style coverage
to include the missing `.installed` and `.error` cases as well as the
status/action-specific exit codes so the mapping is fully exercised. Also update
the JSON serialization test for `SystemExtensionSmokeResult` to assert the
`timeoutSeconds` field is present and correct when `status` is `.timedOut`,
using the existing `SystemExtensionSmokeResult` and related status/action
symbols to keep the assertions aligned with the model behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 315c1b6b-aee5-4075-b30c-27ba785254d7

📥 Commits

Reviewing files that changed from the base of the PR and between f954364 and 1e342dd.

📒 Files selected for processing (12)
  • .github/scripts/macos_smoke_suite.sh
  • .github/workflows/app-smoke-tests.yml
  • .github/workflows/build-macos.yml
  • integration_test/vpn/connect_smoke_harness.dart
  • integration_test/vpn/macos_connect_smoke_test.dart
  • integration_test/vpn/vpn_smoke_helpers.dart
  • lib/features/macos_extension/macos_extension_dialog.dart
  • macos/Runner.xcodeproj/project.pbxproj
  • macos/Runner/AppDelegate.swift
  • macos/Runner/VPN/SystemExtensionManager.swift
  • macos/Runner/VPN/SystemExtensionSmokeCommand.swift
  • macos/RunnerTests/RunnerTests.swift

Comment on lines +198 to +201
commandSubmitted = true
if manager.initialized {
handle(manager.status)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how SystemExtensionManager.initialized is declared and set
rg -n 'initialized' macos/Runner/VPN/SystemExtensionManager.swift -C5

Repository: getlantern/lantern

Length of output: 1133


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- macos/Runner/VPN/SystemExtensionManager.swift (around init/status/updateStatus) ---'
sed -n '1,130p' macos/Runner/VPN/SystemExtensionManager.swift
echo
sed -n '360,430p' macos/Runner/VPN/SystemExtensionManager.swift
echo
echo '--- macos/Runner/VPN/SystemExtensionSmokeCommand.swift (around subscription/start logic) ---'
sed -n '120,240p' macos/Runner/VPN/SystemExtensionSmokeCommand.swift

Repository: getlantern/lantern

Length of output: 11131


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- checkInstallationStatus / activateExtension / related request methods ---'
rg -n 'checkInstallationStatus|activateExtension|submitPropertiesRequest|submitActivationRequest|submitDeactivationRequest|didFinishWithResult|didFailWithError|updateStatus\(' macos/Runner/VPN/SystemExtensionManager.swift -C4

Repository: getlantern/lantern

Length of output: 8863


Wait for the request-specific status before finishing
manager.initialized can already be true from the singleton’s earlier properties query, so handle(manager.status) may complete the smoke command from cached state before checkInstallationStatus() or activateExtension() returns. That can report a stale .status result or short-circuit .activate before the activation flow runs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@macos/Runner/VPN/SystemExtensionSmokeCommand.swift` around lines 198 - 201,
The smoke command can finish too early because
SystemExtensionSmokeCommand.handle(manager.status) is being called whenever
manager.initialized is already true, even if that status came from a cached
singleton query. Update the command submission flow in the logic around
commandSubmitted and handle(_:), and only complete based on the request-specific
result from checkInstallationStatus() or activateExtension() rather than the
current manager.status. Make sure the active request path waits for the fresh
status/activation callback before calling the completion logic so .status and
.activate cannot resolve from stale state.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants