Files
software-mansion__argent/packages/tool-server/test/android-perfetto/profiler-query-android-trace-gate.test.ts
Ignacy Łątka d34ac458e4 fix(profiler): enable native stack-query and combined-report on Android (#343)
## Problem

`profiler-stack-query` and `profiler-combined-report` reject Android
devices with:

> Tool '…' is not supported on android emulator (no android support
declared)

even though **both tools already contain complete, working Android
implementations**:

- `profiler-stack-query` has `executeAndroid` (queries the Perfetto
`.pftrace` via `runAndroidStackQuery`) and an `api.platform ===
"android"` dispatch branch.
- `profiler-combined-report` has an `api.platform === "android"` branch
using `loadAndroidCombinedData` and `buildPerfettoAnchor`.

The only thing gating Android was the `capability` declaration: `{
apple: { simulator: true, device: true } }` with no `android` block.
`assertSupported` (`packages/tool-server/src/utils/capability.ts`)
throws `UnsupportedOperationError` for any platform without a declared
block — so it rejected the device *before* the Android code path could
run.

This was a broken workflow: the sibling `native-profiler-analyze` (which
**does** declare Android support) and `profiler-load`'s `load_native`
output both tell Android users to drill down with `profiler-stack-query`
— which then refused.

## Fix

Add `android: { emulator: true, device: true, unknown: true }` to the
`capability` of both tools, matching `native-profiler-analyze`. The
existing `apple` block is unchanged. No code in the handlers needed to
change — the Android paths were already there.

### `leak_stacks` on Android

`leak_stacks` mode is iOS-only. `executeAndroid` already guards it: it
returns a clean guidance message (`"Memory leak detection is not yet
supported on Android. … adb shell am dumpheap"`) *before* touching the
trace, so enabling the capability does not let `leak_stacks` reach any
crashing code path. A test pins this behavior.

## Tests

New
`packages/tool-server/test/android-perfetto/profiler-query-android-capability.test.ts`:
- both tools are supported on Android emulator / device / unknown,
- both tools are still supported on Apple simulator + device,
- `profiler-stack-query` `leak_stacks` on Android returns the iOS-only
guidance message without querying the trace.

## Verification

- `npm test -w @argent/tool-server` — 131 files, 1158 tests passed.
- `npm test --workspaces` — all package suites green.
- `npm run build` (`tsc --build`) — clean.
- `prettier --write` on changed files — no changes.
2026-06-16 16:19:08 +02:00

81 lines
3.2 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
// Both gates short-circuit BEFORE any .pftrace query, so the Android pipeline
// helpers are stubbed to throw: if a gate were missing, the tool would reach
// loadAndroidCombinedData / runAndroidStackQuery and we'd see that explosion
// instead of the expected "No Android trace loaded" error.
vi.mock("../../src/utils/android-profiler/pipeline/index", async (importActual) => ({
...(await importActual<typeof import("../../src/utils/android-profiler/pipeline/index")>()),
loadAndroidCombinedData: vi.fn(async () => {
throw new Error("loadAndroidCombinedData should not be reached when the trace gate trips");
}),
runAndroidStackQuery: vi.fn(async () => {
throw new Error("runAndroidStackQuery should not be reached when the trace gate trips");
}),
}));
import { profilerStackQueryTool } from "../../src/tools/profiler/query/profiler-stack-query";
import { profilerCombinedReportTool } from "../../src/tools/profiler/combined/profiler-combined-report";
import type { NativeProfilerSessionApi } from "../../src/blueprints/native-profiler-session";
const DEVICE_ID = "emulator-5554";
// traceFile is set at native-profiler-start; exportedFiles.pftrace is only set
// once native-profiler-stop has exported the trace. A session that started but
// never stopped/analyzed therefore has traceFile WITHOUT exportedFiles.pftrace —
// the exact state both gates guard against (so the tools error clearly instead
// of rendering an empty "0 hangs" report off an unexported trace).
const startedButNotStopped: Partial<NativeProfilerSessionApi> = {
platform: "android",
traceFile: "/fake.pftrace",
exportedFiles: undefined,
appProcess: "com.example.app",
wallClockStartMs: 1_700_000_000_000,
};
const GATE_MESSAGE =
"No Android trace loaded. Run native-profiler-stop → native-profiler-analyze first.";
describe("profiler-combined-report Android trace gate", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("throws the trace-not-loaded error when exportedFiles.pftrace is absent (does not render an empty report)", async () => {
await expect(
profilerCombinedReportTool.execute({ nativeSession: startedButNotStopped } as never, {
port: 8081,
device_id: DEVICE_ID,
})
).rejects.toThrow(GATE_MESSAGE);
});
});
describe("profiler-stack-query Android trace gate", () => {
beforeEach(() => {
vi.clearAllMocks();
});
// thread_breakdown is a non-leak Android mode, so it would normally fall
// through to runAndroidStackQuery — the gate at executeAndroid must trip first.
it("throws the trace-not-loaded error for thread_breakdown when exportedFiles.pftrace is absent", async () => {
await expect(
profilerStackQueryTool.execute({ session: startedButNotStopped } as never, {
device_id: DEVICE_ID,
mode: "thread_breakdown",
top_n: 15,
})
).rejects.toThrow(GATE_MESSAGE);
});
it("also gates leak_stacks (which would otherwise short-circuit to the iOS-only message)", async () => {
await expect(
profilerStackQueryTool.execute({ session: startedButNotStopped } as never, {
device_id: DEVICE_ID,
mode: "leak_stacks",
top_n: 15,
})
).rejects.toThrow(GATE_MESSAGE);
});
});