Skip to content

test: fix e2e coverage collection and expand device and rpc coverage - #315

Merged
gmegidish merged 9 commits into
mainfrom
test/e2e-coverage-and-protocol-assertions
Jul 30, 2026
Merged

test: fix e2e coverage collection and expand device and rpc coverage#315
gmegidish merged 9 commits into
mainfrom
test/e2e-coverage-and-protocol-assertions

Conversation

@gmegidish

@gmegidish gmegidish commented Jul 29, 2026

Copy link
Copy Markdown
Member

Why

E2E coverage was being collected incorrectly, so cover.out understated reality and Android contributed almost nothing. Fixing the harness first, then filling the gaps it revealed.

1. E2E coverage collection was broken

Four independent breaks, all fixed:

  • Makefileexport GOCOVERDIR=test/coverage sat on its own recipe line, so it ran in a shell that exited immediately and never reached the npm run test:* lines. Removed (collection is spec-side) and added mkdir -p after the rm -rf.
  • emulator.spec.ts:mobilecliJson() — passed env: {ANDROID_HOME}, dropping GOCOVERDIR. Every JSON-returning Android call emitted nothing.
  • emulator.spec.ts:mobilecli() — never set GOCOVERDIR at all.
  • Duplication — three specs each carried their own copy of the helper. Now one test/coverage.ts.

Net effect: five commands/fs.go functions that read 0.0% are now covered by the Android tests that were already exercising them.

2. Android was missing app-lifecycle coverage

Six tests added to emulator.spec.ts, mirroring what the iOS spec already had: list installed apps, launch + verify foreground, terminate + verify launcher, launch-twice idempotency, press HOME, and tap an element located via UI dump.

Android's dump ui returns a nested tree with text/identifier (iOS uses label/name), so this needed its own flatten + find helpers.

3. The JSON-RPC dispatch table was almost untested

server.spec.ts went 10 → 59 tests, split so CI gets value:

  • rpc method validation — no device required, so it runs on the bare CI runner where test:server executes. Drives 26 methods with an unresolvable device id, 9 missing-required-field cases, and the two handleFsPush branches (invalid base64, >1MB) that run before device lookup.
  • rpc methods against a live device — 11 happy-path tests covering handler success paths; skips cleanly without a device.

Measured contribution of the live group, in isolation: server 34.3% → 37.1%.

4. Protocol assertions

Tests asserted their own payload fields but nothing pinned the contract. Now enforced centrally:

  • rpc() checks jsonrpc: "2.0", that id is echoed, and that exactly one of result/error is present — on every call.
  • rpcExpectError() pins the error code and requires a non-empty message. It also rejects MethodNotFound, so a handler dropped from the registry fails instead of passing as "an error was returned".
  • mobilecli() in both device specs now parses stdout and asserts the {status, data} envelope. Previously launch, terminate, tap, button, url, screenshot and the fs commands checked only the exit code.
  • New test/shapes.ts defines each entity once (device, app, foreground app, UI element, fs entry) and is reused across all three specs.

Verified by renaming json:"status"json:"state" in commands/commands.go and confirming the suite fails.

5. Device selection

Specs now ask mobilecli to filter instead of scanning results:

  • Android: devices --platform android --type emulator
  • iOS: devices --platform ios --type simulator (replaces the simctl-based name lookup; findSimulatorByName deleted)
  • server.spec.ts excludes type === "real"

This is not cosmetic. With a physical Android phone attached, the unfiltered selection picked it and three tests failed while driving the real handset. Since devices omits offline entries by default, a shut-down simulator now skips instead of failing partway through.

6. Go unit tests

  • rpc/ had no tests → 52.6%
  • pkg/avc2mp4/ 25.6% → 76.0% (NAL parsing, access-unit grouping, Annex-B, Convert error paths)
  • commands/keys.go — added the two KeysCommand validation branches

Muxing itself is left to the e2e screenrecord tests rather than synthesizing a valid SPS.

Coverage

before after
e2e total 16.7% 30.1%
server 8.5% 37.1%
commands 26.0% 45.1%
devices 11.5% 23.7%

Test plan

  • go test ./... — all packages pass
  • golangci-lint run — 0 issues in changed files (329 pre-existing on main, unchanged)
  • Full e2e suite: 104 passed, 1 skipped, 0 failed (pre-existing test.skip on the device-lifecycle test)
  • Verified against a host with a real Android phone and a real iPhone attached — neither is selected
  • android_emulator_test remains if: false in CI, so the Android suite still does not run there

Not covered

webview (11 RPC methods) needs a page fixture and is untouched.

Summary by CodeRabbit

  • New Features
    • Improved end-to-end coverage output with merged Go coverage data, HTML report generation, and functional coverage summaries for Android emulator runs.
  • Tests
    • Added validation-focused unit tests for key combos, AVC→MP4 conversion, and Annex-B NAL parsing.
    • Expanded RPC, server, and end-to-end specs with stricter JSON shape assertions and emulator-focused agent lifecycle coverage.
  • Chores
    • Refined coverage collection/processing and updated CI/test commands to run the correct emulator vs Android Playwright projects.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR expands Go unit coverage across commands, RPC, and media codecs; introduces shared JSON shape validators; integrates coverage environment setup across test suites; refactors Android and iOS device discovery; validates server RPC dispatch and live-device methods; and updates E2E execution and CI wiring.

Changes

Go unit tests for commands, RPC, and media codec

Layer / File(s) Summary
Key validation and RPC behavior tests
commands/keys_test.go, rpc/rpc_test.go
Key command tests validate empty/invalid key combos before device interaction; RPC tests verify URL defaulting, error formatting, remarshal conversion, and connection failure handling.
NAL parsing and MP4 writer tests
pkg/avc2mp4/nalparser_test.go, pkg/avc2mp4/mp4writer_test.go
NAL parser tests validate 3-byte and 4-byte start codes, mixed lengths, nal_ref_idc masking, and empty inputs; MP4 writer tests verify access-unit grouping, timestamp SEI handling, Annex-B construction, and conversion errors.

Test shape contracts and environment setup

Layer / File(s) Summary
JSON shape validators and type contracts
test/shapes.ts, test/types.ts
Centralized assertions for device, app, UI element (with recursive child validation), filesystem entry, agent, and ok-envelope shapes; UIElement type extended with optional nested children.
Coverage environment and Playwright configuration
test/coverage.ts, test/fixtures.ts, test/playwright.config.ts, test/package.json
Coverage environment documentation clarified; DeviceTypeOptions fixture type enables worker-scoped device-type selection; Playwright config typed and split into emulator and real-device projects; npm scripts separate emulator and android test runs.

iOS and Android device workflows

Layer / File(s) Summary
iOS simulator discovery and shape-validated helpers
test/simulator.spec.ts, test/simctl.ts
Simulator discovery refactored to find booted device via mobilecli instead of name-based lookup; mobilecli helper enforces ok-envelope validation and uses coverageEnv() environment; device-list, device-info, app-list, foreground-app, UI-dump, and filesystem helpers use shape assertions instead of string containment; screenrecord helpers updated to use coverageEnv().
Android emulator discovery and comprehensive testing
test/android.spec.ts, test/emulator.spec.ts
Android tests discover emulator devices with platform/type filters; mobilecli and mobilecliJson enforce ok-envelope validation and coverageEnv; tests validate screenshots, screenrecord MP4 output via ffprobe, device info, app lifecycle, UI interaction with element tree traversal, filesystem operations in /sdcard and app containers, and agent install/uninstall/status lifecycle; helpers centralize command execution, video verification, and UI navigation.

Server RPC validation

Layer / File(s) Summary
Server dispatch and live-device RPC validation
test/server.spec.ts
New RPC method validation suite tests dispatch without hardware: server.info, devices.list filtering, unknown-device error consistency, missing-parameter errors, and device.fs.push base64/size constraints; conditional live-device suite validates device description, screenshots, app listing, launch/foreground/termination, UI dump tree structure, input methods, orientation, URL opening, and crashes; centralized RPC helpers enforce JSON-RPC 2.0 protocol and error-code constraints; startTestServer uses coverageEnv instead of manual coverage setup.

E2E execution wiring

Layer / File(s) Summary
Makefile and CI recipe updates
Makefile, .github/workflows/build.yml
Makefile test-e2e creates coverage directory, runs go test with atomic mode into that directory, generates coverage.out and coverage.html, and continues running server/simulator/android/emulator npm suites; CI emulator-runner invokes test:emulator instead of test:android.

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

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.98% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: fixing E2E coverage collection and expanding device/RPC coverage.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test/e2e-coverage-and-protocol-assertions

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

@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: 6

🧹 Nitpick comments (1)
test/emulator.spec.ts (1)

315-325: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

mobilecliJson loses failure diagnostics that mobilecli provides.

mobilecli (lines 298-313) logs the command args and stderr/stdout on failure before rethrowing; mobilecliJson doesn't, so a raw process failure (as opposed to a bad envelope, which expectOkEnvelope already reports clearly) surfaces with no command context, making local/CI debugging harder.

♻️ Proposed fix
 function mobilecliJson(args: string[]): any {
-	const result = execFileSync(mobilecliBinary, args, {
-		encoding: 'utf8',
-		timeout: 60000,
-		stdio: ['pipe', 'pipe', 'pipe'],
-		env: coverageEnv(),
-	});
-	const parsed = JSON.parse(result);
-	expectOkEnvelope(parsed);
-	return parsed;
+	try {
+		const result = execFileSync(mobilecliBinary, args, {
+			encoding: 'utf8',
+			timeout: 60000,
+			stdio: ['pipe', 'pipe', 'pipe'],
+			env: coverageEnv(),
+		});
+		const parsed = JSON.parse(result);
+		expectOkEnvelope(parsed);
+		return parsed;
+	} catch (error: any) {
+		console.log(`Command failed: ${mobilecliBinary} ${args.join(' ')}`);
+		if (error.stderr) console.log(`stderr: ${error.stderr}`);
+		if (error.stdout) console.log(`stdout: ${error.stdout}`);
+		throw error;
+	}
 }
🤖 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 `@test/emulator.spec.ts` around lines 315 - 325, Update mobilecliJson to catch
failures from execFileSync, log the command arguments and captured stdout/stderr
using the same diagnostics pattern as mobilecli, then rethrow the original
error; leave expectOkEnvelope handling unchanged for successfully executed
commands.
🤖 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 `@commands/keys_test.go`:
- Around line 57-63: Update TestKeysCommandRejectsEmptyKeyList to also assert
that the response error message equals "at least one key combo is required",
while retaining the existing status assertion, so the test specifically verifies
empty-key-list validation.

In `@pkg/avc2mp4/nalparser_test.go`:
- Around line 63-76: Update TestParseNALUnitsMasksRefIdcOutOfType to use a valid
H.264 header such as 0x45, which sets nal_ref_idc without setting
forbidden_zero_bit; keep the expected parsed type and original Data header
assertions aligned with the replacement header.

In `@rpc/rpc_test.go`:
- Around line 89-99: Replace the port-1 dependency in
TestCallReportsConnectionFailure with a deterministic injectable failing dialer
or transport supported by Call, ensuring the connection attempt reliably returns
the expected “failed to connect to fleet server” error without relying on an
unused local port.

In `@test/server.spec.ts`:
- Around line 299-309: Update the missing-required-field cases for
device.apps.path, device.crashes.get, and device.fs.* in
methodsMissingARequiredField to include deviceId: UNKNOWN_DEVICE_ID while still
omitting the field named by each test. Ensure these cases reach field-level
validation rather than failing during device lookup, matching the existing
device.io.swipe and device.webview.goto setup.
- Line 274: Update the `device.apps.uninstall` test request to pass the required
`bundleId` parameter instead of `packageName`, preserving the intended
unknown-device test path.

In `@test/shapes.ts`:
- Around line 32-36: Update expectForegroundAppShape to validate the required
version field from ForegroundAppResponse.data: assert foreground.version is a
string, alongside the existing packageName and appName checks.

---

Nitpick comments:
In `@test/emulator.spec.ts`:
- Around line 315-325: Update mobilecliJson to catch failures from execFileSync,
log the command arguments and captured stdout/stderr using the same diagnostics
pattern as mobilecli, then rethrow the original error; leave expectOkEnvelope
handling unchanged for successfully executed commands.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a6730140-52da-4199-8c23-52d6c41d7b88

📥 Commits

Reviewing files that changed from the base of the PR and between f1dba46 and ca5a58a.

⛔ Files ignored due to path filters (1)
  • test/README.md is excluded by !**/*.md
📒 Files selected for processing (12)
  • Makefile
  • commands/keys_test.go
  • pkg/avc2mp4/mp4writer_test.go
  • pkg/avc2mp4/nalparser_test.go
  • rpc/rpc_test.go
  • test/coverage.ts
  • test/emulator.spec.ts
  • test/server.spec.ts
  • test/shapes.ts
  • test/simctl.ts
  • test/simulator.spec.ts
  • test/types.ts
💤 Files with no reviewable changes (1)
  • test/simctl.ts

Comment thread commands/keys_test.go
Comment thread pkg/avc2mp4/nalparser_test.go Outdated
Comment thread rpc/rpc_test.go
Comment thread test/server.spec.ts Outdated
Comment thread test/server.spec.ts Outdated
Comment thread test/shapes.ts

@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 (2)
test/android.spec.ts (2)

54-64: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Log the underlying error before returning null.

The catch block discards the failure reason entirely. A missing/broken mobilecli binary, a malformed JSON response, or a non-zero exit code will all be reported identically to "no device found" (line 72), making CI failures harder to diagnose.

♻️ Proposed fix
 	} catch (error) {
+		console.error(`getFirstAndroidDevice(${deviceType}) failed:`, error);
 		return null;
 	}
🤖 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 `@test/android.spec.ts` around lines 54 - 64, Update the catch block in
getFirstAndroidDevice to log the caught error with sufficient context before
returning null, while preserving the existing null fallback for command or JSON
parsing failures.

76-304: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Repeated test.skip(!device, 'No Android device found') boilerplate across ~20+ tests.

Nearly every test in this describe block opens with the identical skip call. Since device is set once in beforeAll and doesn't vary per test, this can be centralized.

♻️ Proposed refactor
 test.describe('Android Tests', () => {
 	let device: Device | null;
 
 	test.beforeAll(({deviceType}) => {
 		device = getFirstAndroidDevice(deviceType);
 		if (!device) {
 			console.log(`No Android ${deviceType} device found. See test/README.md for setup instructions.`);
 		}
 	});
+
+	test.beforeEach(() => {
+		test.skip(!device, 'No Android device found');
+	});
 
 	test('should take screenshot', ({deviceType}) => {
-		test.skip(!device, 'No Android device found');
 		// ...
 	});

Note: nested test.describe blocks (e.g. the /sdcard/Download and playground-container groups) would need the same beforeEach added locally, or hoisted to the outer scope if applicable to all.

🤖 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 `@test/android.spec.ts` around lines 76 - 304, Centralize the repeated
missing-device skip by adding a shared `beforeEach` in the outer Android test
scope that skips when `device` is unavailable, then remove the redundant
`test.skip(!device, 'No Android device found')` calls from individual tests. Add
equivalent hooks within nested `test.describe` blocks such as the
`/sdcard/Download` and app-container groups if the outer hook does not cover
them, while preserving unrelated skips and test setup.
🤖 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 `@test/android.spec.ts`:
- Line 1: Update the recordThenInterruptWithCtrlC helper to bound its wait after
sending SIGINT: if mobilecli has not closed before the timeout, escalate by
sending SIGKILL or reject the operation, while preserving normal resolution when
the process exits promptly.

---

Nitpick comments:
In `@test/android.spec.ts`:
- Around line 54-64: Update the catch block in getFirstAndroidDevice to log the
caught error with sufficient context before returning null, while preserving the
existing null fallback for command or JSON parsing failures.
- Around line 76-304: Centralize the repeated missing-device skip by adding a
shared `beforeEach` in the outer Android test scope that skips when `device` is
unavailable, then remove the redundant `test.skip(!device, 'No Android device
found')` calls from individual tests. Add equivalent hooks within nested
`test.describe` blocks such as the `/sdcard/Download` and app-container groups
if the outer hook does not cover them, while preserving unrelated skips and test
setup.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b42b4846-18b3-4571-a9b9-c1b32dfa7bea

📥 Commits

Reviewing files that changed from the base of the PR and between ca5a58a and e4def4e.

⛔ Files ignored due to path filters (1)
  • test/README.md is excluded by !**/*.md
📒 Files selected for processing (6)
  • .github/workflows/build.yml
  • Makefile
  • test/android.spec.ts
  • test/fixtures.ts
  • test/package.json
  • test/playwright.config.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • Makefile

Comment thread test/android.spec.ts
@@ -0,0 +1,501 @@
import {test, expect} from './fixtures';

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:

ast-grep run --pattern 'spawn($$$)' --lang typescript test/android.spec.ts
rg -n -B2 -A15 'function mobilecli\b|spawn\(' test/android.spec.ts

Repository: mobile-next/mobilecli

Length of output: 1937


🏁 Script executed:

sed -n '430,520p' test/android.spec.ts
printf '\n--- usages ---\n'
rg -n 'recordThenInterruptWithCtrlC|screenrecord|mobilecliBinary|setTimeout\\(|timeout' test/android.spec.ts

Repository: mobile-next/mobilecli

Length of output: 3008


Add a force-kill fallback to the screenrecord helper
test/android.spec.ts:458-469recordThenInterruptWithCtrlC only sends SIGINT and waits for close; if mobilecli never exits, the test run can hang indefinitely. Add a timeout that escalates to SIGKILL (or rejects) if the process does not terminate.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 1-1: Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import {execFileSync, spawn} from 'child_process';
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 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 `@test/android.spec.ts` at line 1, Update the recordThenInterruptWithCtrlC
helper to bound its wait after sending SIGINT: if mobilecli has not closed
before the timeout, escalate by sending SIGKILL or reject the operation, while
preserving normal resolution when the process exits promptly.

@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

🤖 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 `@test/android.spec.ts`:
- Around line 253-315: Add an afterAll hook to the agent lifecycle suite that
attempts to uninstall the agent whenever a device is available, ensuring cleanup
runs even if a test fails or execution stops before the final uninstall test.
Reuse the existing runAgentCommand(device.id, 'uninstall') cleanup behavior from
beforeAll and keep the existing test assertions unchanged.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ded08c3e-0f96-4b7c-86ed-936555cf9219

📥 Commits

Reviewing files that changed from the base of the PR and between 725f1e1 and c40c828.

📒 Files selected for processing (2)
  • test/android.spec.ts
  • test/shapes.ts

Comment thread test/android.spec.ts
Comment on lines +253 to +315
test.describe('agent lifecycle', () => {
test.beforeAll(() => {
if (!device) return;
// the device may arrive with an agent from an earlier run, so start clean.
// the response is ignored: "not installed" is a perfectly good outcome here
runAgentCommand(device.id, 'uninstall');
});

test('status should report no agent before installation', () => {
test.skip(!device, 'No Android device found');

const response = runAgentCommand(device!.id, 'status');
expect(response.status).toBe('fail');
expect(response.data.message).toBe(AGENT_MISSING_MESSAGE);
});

test('uninstall should report no agent when none is installed', () => {
test.skip(!device, 'No Android device found');

const response = runAgentCommand(device!.id, 'uninstall');
expect(response.status).toBe('fail');
expect(response.data.message).toBe(AGENT_MISSING_MESSAGE);
});

test('install should report a successful installation', () => {
test.skip(!device, 'No Android device found');

const response = runAgentCommand(device!.id, 'install');
expect(response.status).toBe('ok');
expect(response.data.message).toBe('Agent installed successfully');
expectAgentShape(response.data.agent);
expect(response.data.agent.bundleId).toBe(AGENT_BUNDLE_ID);
});

test('installing again should report the agent is already installed', () => {
test.skip(!device, 'No Android device found');

const response = runAgentCommand(device!.id, 'install');
expect(response.status).toBe('ok');
expect(response.data.message).toBe('Agent is already installed');
expectAgentShape(response.data.agent);
expect(response.data.agent.bundleId).toBe(AGENT_BUNDLE_ID);
});

test('status should report the installed agent', () => {
test.skip(!device, 'No Android device found');

const response = runAgentCommand(device!.id, 'status');
expect(response.status).toBe('ok');
expectAgentShape(response.data.agent);
expect(response.data.agent.bundleId).toBe(AGENT_BUNDLE_ID);
// derived from the reported version rather than a literal, so bumping the
// pinned agent in cli/agent.go does not require editing this test
expect(response.data.message).toBe(`Agent version ${response.data.agent.version} is installed on device`);
});

test('uninstall should succeed once an agent is installed', () => {
test.skip(!device, 'No Android device found');

const response = runAgentCommand(device!.id, 'uninstall');
expect(response.status).toBe('ok');
expect(response.data.message).toBe('Agent uninstalled successfully');
});

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

Always clean up the installed agent.

Cleanup only occurs in the final test. A failure or early stop after installation can leave the emulator modified and affect later runs. Add an afterAll cleanup attempt.

Proposed fix
 	test('uninstall should succeed once an agent is installed', () => {
 		// ...
 	});
+
+	test.afterAll(() => {
+		if (device) runAgentCommand(device.id, 'uninstall');
+	});
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test.describe('agent lifecycle', () => {
test.beforeAll(() => {
if (!device) return;
// the device may arrive with an agent from an earlier run, so start clean.
// the response is ignored: "not installed" is a perfectly good outcome here
runAgentCommand(device.id, 'uninstall');
});
test('status should report no agent before installation', () => {
test.skip(!device, 'No Android device found');
const response = runAgentCommand(device!.id, 'status');
expect(response.status).toBe('fail');
expect(response.data.message).toBe(AGENT_MISSING_MESSAGE);
});
test('uninstall should report no agent when none is installed', () => {
test.skip(!device, 'No Android device found');
const response = runAgentCommand(device!.id, 'uninstall');
expect(response.status).toBe('fail');
expect(response.data.message).toBe(AGENT_MISSING_MESSAGE);
});
test('install should report a successful installation', () => {
test.skip(!device, 'No Android device found');
const response = runAgentCommand(device!.id, 'install');
expect(response.status).toBe('ok');
expect(response.data.message).toBe('Agent installed successfully');
expectAgentShape(response.data.agent);
expect(response.data.agent.bundleId).toBe(AGENT_BUNDLE_ID);
});
test('installing again should report the agent is already installed', () => {
test.skip(!device, 'No Android device found');
const response = runAgentCommand(device!.id, 'install');
expect(response.status).toBe('ok');
expect(response.data.message).toBe('Agent is already installed');
expectAgentShape(response.data.agent);
expect(response.data.agent.bundleId).toBe(AGENT_BUNDLE_ID);
});
test('status should report the installed agent', () => {
test.skip(!device, 'No Android device found');
const response = runAgentCommand(device!.id, 'status');
expect(response.status).toBe('ok');
expectAgentShape(response.data.agent);
expect(response.data.agent.bundleId).toBe(AGENT_BUNDLE_ID);
// derived from the reported version rather than a literal, so bumping the
// pinned agent in cli/agent.go does not require editing this test
expect(response.data.message).toBe(`Agent version ${response.data.agent.version} is installed on device`);
});
test('uninstall should succeed once an agent is installed', () => {
test.skip(!device, 'No Android device found');
const response = runAgentCommand(device!.id, 'uninstall');
expect(response.status).toBe('ok');
expect(response.data.message).toBe('Agent uninstalled successfully');
});
test.describe('agent lifecycle', () => {
test.beforeAll(() => {
if (!device) return;
// the device may arrive with an agent from an earlier run, so start clean.
// the response is ignored: "not installed" is a perfectly good outcome here
runAgentCommand(device.id, 'uninstall');
});
test('status should report no agent before installation', () => {
test.skip(!device, 'No Android device found');
const response = runAgentCommand(device!.id, 'status');
expect(response.status).toBe('fail');
expect(response.data.message).toBe(AGENT_MISSING_MESSAGE);
});
test('uninstall should report no agent when none is installed', () => {
test.skip(!device, 'No Android device found');
const response = runAgentCommand(device!.id, 'uninstall');
expect(response.status).toBe('fail');
expect(response.data.message).toBe(AGENT_MISSING_MESSAGE);
});
test('install should report a successful installation', () => {
test.skip(!device, 'No Android device found');
const response = runAgentCommand(device!.id, 'install');
expect(response.status).toBe('ok');
expect(response.data.message).toBe('Agent installed successfully');
expectAgentShape(response.data.agent);
expect(response.data.agent.bundleId).toBe(AGENT_BUNDLE_ID);
});
test('installing again should report the agent is already installed', () => {
test.skip(!device, 'No Android device found');
const response = runAgentCommand(device!.id, 'install');
expect(response.status).toBe('ok');
expect(response.data.message).toBe('Agent is already installed');
expectAgentShape(response.data.agent);
expect(response.data.agent.bundleId).toBe(AGENT_BUNDLE_ID);
});
test('status should report the installed agent', () => {
test.skip(!device, 'No Android device found');
const response = runAgentCommand(device!.id, 'status');
expect(response.status).toBe('ok');
expectAgentShape(response.data.agent);
expect(response.data.agent.bundleId).toBe(AGENT_BUNDLE_ID);
// derived from the reported version rather than a literal, so bumping the
// pinned agent in cli/agent.go does not require editing this test
expect(response.data.message).toBe(`Agent version ${response.data.agent.version} is installed on device`);
});
test('uninstall should succeed once an agent is installed', () => {
test.skip(!device, 'No Android device found');
const response = runAgentCommand(device!.id, 'uninstall');
expect(response.status).toBe('ok');
expect(response.data.message).toBe('Agent uninstalled successfully');
});
test.afterAll(() => {
if (device) runAgentCommand(device.id, 'uninstall');
});
🤖 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 `@test/android.spec.ts` around lines 253 - 315, Add an afterAll hook to the
agent lifecycle suite that attempts to uninstall the agent whenever a device is
available, ensuring cleanup runs even if a test fails or execution stops before
the final uninstall test. Reuse the existing runAgentCommand(device.id,
'uninstall') cleanup behavior from beforeAll and keep the existing test
assertions unchanged.

@gmegidish
gmegidish merged commit 167aa92 into main Jul 30, 2026
17 checks passed
@gmegidish
gmegidish deleted the test/e2e-coverage-and-protocol-assertions branch July 30, 2026 11:14
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.

1 participant