fix(adb): preserve exact transport identity observations (#10062)

This commit is contained in:
morluto authored and GitHub committed 2026-11-11 18:18:35 +08:00
1 parent 3eb502ae5d
commit 913c9fe194
11 files changed
+542 -37

No files matched your search

+2
View File
@@ -61,6 +61,8 @@ export const listAdbDevicesOperation = async (
context.signal,
);
if (!ran.ok) return ran;
// Keep exact transport identities internally; the shared client boundary
// alone produces the legacy numeric projection.
const { devices, unparsedLines, daemonStarted } = parseAdbDevicesOutput(
ran.value.stdout,
ran.value.stderr,
+75 -5
View File
@@ -28,7 +28,13 @@ describe("parseAdbDevicesOutput", () => {
product: "sdk_gphone64_x86_64",
model: "sdk_gphone64_x86_64",
device: "emu64xa",
transport_id: 1,
transport_identity: {
status: "reported",
decimal: "1",
source_token: "1",
source_line:
"emulator-5554 device product:sdk_gphone64_x86_64 model:sdk_gphone64_x86_64 device:emu64xa transport_id:1",
},
kind: "emulator",
kind_basis: "emulator_serial_prefix",
},
@@ -39,7 +45,13 @@ describe("parseAdbDevicesOutput", () => {
product: "raven",
model: "Pixel_6_Pro",
device: "raven",
transport_id: 2,
transport_identity: {
status: "reported",
decimal: "2",
source_token: "2",
source_line:
"07ef9c2a device usb:1-1 product:raven model:Pixel_6_Pro device:raven transport_id:2",
},
kind: "physical",
kind_basis: "usb_transport",
},
@@ -50,7 +62,13 @@ describe("parseAdbDevicesOutput", () => {
product: "mock",
model: "mock",
device: null,
transport_id: 3,
transport_identity: {
status: "reported",
decimal: "3",
source_token: "3",
source_line:
"192.168.1.42:5555 device product:mock model:mock transport_id:3",
},
kind: "unknown",
kind_basis: "none",
},
@@ -196,7 +214,12 @@ describe("ADB device states", () => {
product: null,
model: null,
device: null,
transport_id: 4,
transport_identity: {
status: "reported",
decimal: "4",
source_token: "4",
source_line: `07ef9c2a ${stateText} usb:1-1 transport_id:4`,
},
kind: "physical",
});
});
@@ -229,7 +252,12 @@ describe("ADB device states", () => {
"",
].join("\n");
const { devices } = parseAdbDevicesOutput(output);
expect(devices[0]?.transport_id).toBeNull();
expect(devices[0]?.transport_identity).toEqual({
status: "invalid",
source_token: "not-a-number",
source_line: "emulator-5554 device transport_id:not-a-number",
reason: "invalid_decimal",
});
});
});
@@ -412,3 +440,45 @@ describe("parsePmPathOutput", () => {
).toEqual({ sample: [" package:/data/app/pkg/base.apk "], total: 1 });
});
});
it.each([
["1", "reported", "1"],
["2147483648", "reported", "2147483648"],
["9007199254740991", "reported", "9007199254740991"],
["9007199254740993", "reported", "9007199254740993"],
["18446744073709551615", "reported", "18446744073709551615"],
["0001", "reported", "1"],
["0", "invalid", "reserved_zero"],
["-1", "invalid", "invalid_decimal"],
["1e3", "invalid", "invalid_decimal"],
["", "invalid", "invalid_decimal"],
["18446744073709551616", "invalid", "outside_uint64"],
] as const)("retains exact transport identity %s", (token, status, fact) => {
const { devices } = parseAdbDevicesOutput(
`serial device model:Pixel transport_id:${token}\r\n`,
);
expect(devices[0]).toMatchObject({
serial: "serial",
model: "Pixel",
transport_identity: {
status,
source_token: token,
...(status === "reported" ? { decimal: fact } : { reason: fact }),
},
});
});
it("uses only the final transport field and distinguishes a missing field", () => {
const { devices } = parseAdbDevicesOutput(
[
"first device product:foo transport_id:7 model:Pixel transport_id:9",
"second device product:foo transport_id:7 model:Pixel",
].join("\n"),
);
expect(devices[0]).toMatchObject({
transport_identity: { status: "reported", decimal: "9", source_token: "9" },
});
expect(devices[1]).toMatchObject({
transport_identity: { status: "not_reported", source_token: null },
});
});
+13 -21
View File
@@ -16,21 +16,10 @@ import {
} from "./AdbUnparsedLines.js";
import {
ADB_DEVICE_STATES,
type AdbDeviceState,
type AdbDeviceObservation,
} from "../domain/adb/adbDeviceAnalysis.js";
/** One device exactly as `adb devices -l` reports it. */
export interface AdbDeviceEntry {
readonly serial: string;
readonly state: AdbDeviceState;
readonly transport: "usb" | "tcp" | "unknown";
readonly product: string | null;
readonly model: string | null;
readonly device: string | null;
readonly transport_id: number | null;
readonly kind: "emulator" | "physical" | "unknown";
readonly kind_basis: "emulator_serial_prefix" | "usb_transport" | "none";
}
import { parseAdbTransportIdentity } from "../domain/adb/adbTransportIdentity.js";
const DEVICE_LINE = /^(\S+)\s+(.*)$/u;
@@ -76,11 +65,11 @@ export const parseAdbDevicesOutput = (
stdout: string,
stderr = "",
): {
devices: readonly AdbDeviceEntry[];
devices: readonly AdbDeviceObservation[];
unparsedLines: AdbUnparsedLines;
daemonStarted: boolean;
} => {
const devices: AdbDeviceEntry[] = [];
const devices: AdbDeviceObservation[] = [];
const unparsed = collectUnparsedLines();
let daemonStarted = false;
for (const rawLine of stdout.split(/\r?\n/u)) {
@@ -113,11 +102,14 @@ export const parseAdbDevicesOutput = (
const serial = match[1]!;
const remainder = match[2]!.slice(state.length);
const usb = metadataValue(remainder, "usb");
const transportIdText = metadataValue(remainder, "transport_id");
const parsedTransportId =
transportIdText === null || !/^\d+$/u.test(transportIdText)
? null
: Number(transportIdText);
// AOSP appends the authoritative field last; earlier metadata may contain
// the same spelling. Keep the exact source token before decimal admission.
const transportToken =
/(?:^|\s)transport_id:(\S*)$/u.exec(remainder)?.[1] ?? null;
const transportIdentity = parseAdbTransportIdentity(
transportToken,
rawLine,
);
const isEmulator = /^emulator-\d+$/u.test(serial);
const kind = isEmulator
? ("emulator" as const)
@@ -136,7 +128,7 @@ export const parseAdbDevicesOutput = (
product: metadataValue(remainder, "product"),
model: metadataValue(remainder, "model"),
device: metadataValue(remainder, "device"),
transport_id: parsedTransportId,
transport_identity: transportIdentity,
kind,
kind_basis:
kind === "emulator"
+21 -3
View File
@@ -246,7 +246,13 @@ fi`,
product: "sdk_gphone64_x86_64",
model: "sdk_gphone64_x86_64",
device: "emu64xa",
transport_id: 1,
transport_identity: {
status: "reported",
decimal: "1",
source_token: "1",
source_line:
"emulator-5554 device product:sdk_gphone64_x86_64 model:sdk_gphone64_x86_64 device:emu64xa transport_id:1",
},
kind: "emulator",
kind_basis: "emulator_serial_prefix",
},
@@ -276,7 +282,13 @@ fi`,
product: "sdk_gphone64_x86_64",
model: "sdk_gphone64_x86_64",
device: "emu64xa",
transport_id: 1,
transport_identity: {
status: "reported",
decimal: "1",
source_token: "1",
source_line:
"emulator-5554 device product:sdk_gphone64_x86_64 model:sdk_gphone64_x86_64 device:emu64xa transport_id:1",
},
kind: "emulator",
kind_basis: "emulator_serial_prefix",
},
@@ -324,7 +336,13 @@ fi`,
product: null,
model: null,
device: null,
transport_id: 4,
transport_identity: {
status: "reported",
decimal: "4",
source_token: "4",
source_line:
"07ef9c2a no permissions; see [http://developer.android.com/tools/device.html] usb:1-1 transport_id:4",
},
kind: "physical",
kind_basis: "usb_transport",
},
@@ -1,4 +1,10 @@
import { expect, it } from "vitest";
import { chmod, readFile } from "node:fs/promises";
import { expect, it, onTestFinished } from "vitest";
import {
createTestWorkspace,
removeTestWorkspace,
} from "../../../tests/support/workspace/workspaceFixture.js";
import { AdbProvider } from "../../adb/AdbProvider.js";
import { AdbDeviceAnalysisService } from "./AdbDeviceAnalysisService.js";
import type { AdbDeviceAnalysisPort } from "./AdbDeviceAnalysisPort.js";
@@ -139,3 +145,125 @@ it("preserves cancellation before execution", async () => {
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(cancellationReason);
});
it.skipIf(process.platform === "win32").each([false, true])(
"projects full transport facts through the shared client boundary (late abort %s)",
async (lateAbort) => {
const workspace = await createTestWorkspace("rea-adb-transport-");
onTestFinished(() => removeTestWorkspace(workspace.root));
const lines = [
"safe device model:Pixel transport_id:2147483648",
"exact device model:Pixel transport_id:18446744073709551615",
"invalid device model:Pixel transport_id:0",
"missing device model:Pixel",
"suffix device product:foo transport_id:7 model:Pixel transport_id:9",
];
const logPath = workspace.path("commands.jsonl");
const binary = await workspace.write(
"adb",
`#!${process.execPath}\nimport {appendFileSync} from 'node:fs';\nconst args=process.argv.slice(2);appendFileSync(${JSON.stringify(logPath)},JSON.stringify(args)+'\\n');\nif(args[0]==='version')process.stdout.write('Android Debug Bridge version 1.0.41\\nVersion 35.0.2-inert\\n');\nelse if(args[0]==='devices'&&args[1]==='-l')process.stdout.write(${JSON.stringify(lines.join("\n") + "\n")});\nelse if(args[0]==='-s'&&args[1]==='exact'&&args[2]==='shell'&&args[3]==='getprop')process.stdout.write('[ro.build.version.sdk]: [34]\\n');\nelse throw Error('No other command authorized');\n`,
);
await chmod(binary, 0o755);
const provider = new AdbProvider({ environment: { REA_ADB_PATH: binary } });
onTestFinished(() => provider.close());
const controller = new AbortController();
const port: AdbDeviceAnalysisPort = {
inspectAvailability: provider.inspectAvailability.bind(provider),
close: provider.close.bind(provider),
async execute(request, options) {
const result = await provider.execute(request, options);
if (lateAbort) controller.abort("after listing");
return result.ok
? ok({
...result.value,
rawResult: { fixture_observation: "retained verbatim" },
})
: result;
},
};
const result = await new AdbDeviceAnalysisService(port).execute(
"list_adb_devices",
{},
{ signal: controller.signal },
);
const evidence = result.ok
? result.value
: parseEvidence(result.error.partialObservation);
expect(result.ok).toBe(!lateAbort);
expect(evidence.normalized_result).toMatchObject({
devices: [
{
serial: "safe",
transport_id: 2147483648,
transport_id_text: "2147483648",
transport_id_status: "reported",
},
{
serial: "exact",
transport_id: null,
transport_id_text: "18446744073709551615",
transport_id_status: "reported",
},
{
serial: "invalid",
transport_id: null,
transport_id_text: null,
transport_id_status: "invalid",
},
{
serial: "missing",
transport_id: null,
transport_id_text: null,
transport_id_status: "not_reported",
},
{
serial: "suffix",
transport_id: 9,
transport_id_text: "9",
transport_id_status: "reported",
},
],
});
expect(evidence.raw_result).toMatchObject({
provider_raw_result: { fixture_observation: "retained verbatim" },
transport_id_observations: [
{
row: 0,
serial: "safe",
source_token: "2147483648",
source_line: lines[0],
},
{
row: 1,
serial: "exact",
source_token: "18446744073709551615",
source_line: lines[1],
},
{
row: 2,
serial: "invalid",
source_token: "0",
reason: "reserved_zero",
},
{
row: 3,
serial: "missing",
source_token: null,
reason: "not_reported",
},
{ row: 4, serial: "suffix", source_token: "9" },
],
});
const inspected = await new AdbDeviceAnalysisService(provider).execute(
"inspect_adb_device",
{ serial: "exact" },
);
expect(inspected.ok).toBe(true);
expect(
(await readFile(logPath, "utf8"))
.split("\n")
.filter(Boolean)
.map((line) => JSON.parse(line)),
).toContainEqual(["-s", "exact", "shell", "getprop"]);
},
);
@@ -1,12 +1,18 @@
import type { AdbDeviceAnalysisPort } from "./AdbDeviceAnalysisPort.js";
import type { ExecutionOptions } from "../AnalysisProvider.js";
import type {
AnalysisExecution,
ExecutionOptions,
} from "../AnalysisProvider.js";
import {
adbRequestSchema,
adbDeviceListObservationSchema,
type AdbOperation,
} from "../../domain/adb/adbDeviceAnalysis.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import type { Evidence } from "../../domain/evidence.js";
import type { Result } from "../../domain/result.js";
import { err, ok, type Result } from "../../domain/result.js";
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
import { jsonValueSchema } from "../../domain/jsonValue.js";
import { executeProviderOperation } from "../ProviderOperationExecution.js";
/** Shared CLI/MCP admission, execution, and Evidence composition for ADB. */
@@ -24,8 +30,92 @@ export class AdbDeviceAnalysisService {
input,
options,
adbRequestSchema,
this.provider,
{
execute: async (request, options) => {
const result = await this.provider.execute(request, options);
return result.ok && request.operation === "list_adb_devices"
? projectDeviceList(result.value)
: result;
},
},
"observed",
);
}
}
/** Public number compatibility is a projection of exact provider facts, never identity admission. */
const projectDeviceList = (
execution: AnalysisExecution,
): Result<AnalysisExecution, AnalysisError> => {
const json = jsonValueSchema.safeParse(execution.result);
const parsed = json.success
? adbDeviceListObservationSchema.safeParse(json.data)
: json;
if (!parsed.success)
return err(
new AnalysisOutputError(
"list_adb_devices",
"ADB provider returned malformed device-list identity facts.",
{ cause: parsed.error },
),
);
let numericUnavailable = false;
let invalidIdentity = false;
const devices = parsed.data.devices.map(
({ transport_identity, ...device }) => {
const text =
transport_identity.status === "reported"
? transport_identity.decimal
: null;
const safe =
text !== null &&
(text.length < 16 ||
(text.length === 16 && text <= "9007199254740991"));
if (text !== null && !safe) numericUnavailable = true;
if (transport_identity.status === "invalid") invalidIdentity = true;
return {
...device,
transport_id: safe ? Number(text) : null,
transport_id_text: text,
transport_id_status: transport_identity.status,
};
},
);
return ok({
...execution,
result: { ...parsed.data, devices },
rawResult: {
...(execution.rawResult === null
? {}
: { provider_raw_result: execution.rawResult }),
transport_id_observations: parsed.data.devices.map(
({ serial, transport_identity }, row) => ({
row,
serial,
source_line: transport_identity.source_line,
source_token: transport_identity.source_token,
status: transport_identity.status,
reason:
transport_identity.status === "invalid"
? transport_identity.reason
: transport_identity.status === "not_reported"
? "not_reported"
: null,
}),
),
},
limitations: [
...execution.limitations,
...(numericUnavailable
? [
"A reported transport ID cannot be represented as a safe JSON integer. transport_id_text retains its exact identity; existing operations select the device by serial.",
]
: []),
...(invalidIdentity
? [
"Some transport ID fields were invalid. Known device facts remain available; raw_result.transport_id_observations retains source tokens and reasons.",
]
: []),
],
});
};
+1 -1
View File
@@ -22,7 +22,7 @@ export const ADB_TOOL_CONTRACTS = [
name: "list_adb_devices",
...toolContractMetadata("list_adb_devices"),
description:
"List attached devices exactly as `adb devices -l` reports them: serial, state, transport, model metadata, and an inferred emulator/physical kind with its evidence basis. Emulators and USB or network devices appear alike; REA neither starts nor manages them. May start the local adb server on first use.",
"List attached devices exactly as `adb devices -l` reports them: serial, state, transport, model metadata, and an inferred emulator/physical kind with its evidence basis. Emulators and USB or network devices appear alike; REA neither starts nor manages them. transport_id_text preserves exact full-range transport identity; transport_id is its safe numeric projection or null. May start the local adb server on first use.",
kind: "adb-provider",
inputSchema: adbInputSchemas.list_adb_devices,
outputSchema: evidenceResultOf(adbResultSchemas.list_adb_devices),
+33 -2
View File
@@ -1,3 +1,7 @@
import {
adbTransportIdentitySchema,
adbTransportIdSchema,
} from "./adbTransportIdentity.js";
import { z } from "zod";
import { localPathStringSchema } from "../localPath.js";
import { IDENTIFIER_PATTERN } from "../stringPatterns.js";
@@ -482,14 +486,14 @@ const client = z.strictObject({
installed_path: z.string().min(1).nullable(),
});
const device = z.strictObject({
export const adbDeviceObservationSchema = z.strictObject({
serial: z.string().min(1),
state: adbDeviceStateSchema,
transport: z.enum(["usb", "tcp", "unknown"]),
product: z.string().nullable(),
model: z.string().nullable(),
device: z.string().nullable(),
transport_id: z.int().positive().nullable(),
transport_identity: adbTransportIdentitySchema,
/**
* Inferred device kind with its evidence basis. The serial prefix and the
* `device:` metadata line are observations; the kind itself is derived.
@@ -498,6 +502,33 @@ const device = z.strictObject({
kind_basis: z.enum(["emulator_serial_prefix", "usb_transport", "none"]),
});
export type AdbDeviceObservation = z.output<typeof adbDeviceObservationSchema>;
/** Provider facts before the shared CLI/MCP numeric projection. */
export const adbDeviceListObservationSchema = z.strictObject({
client,
devices: z.array(adbDeviceObservationSchema),
may_have_started_adb_server: z.boolean(),
});
const device = adbDeviceObservationSchema
.omit({ transport_identity: true })
.extend({
transport_id: z
.int()
.positive()
.nullable()
.describe(
"Exact numeric projection when safely representable; otherwise null. transport_id_text retains the full reported identity.",
),
transport_id_text: adbTransportIdSchema
.nullable()
.describe(
"Canonical positive unsigned 64-bit transport ID as decimal text; null when invalid or not reported.",
),
transport_id_status: z.enum(["reported", "not_reported", "invalid"]),
});
const pulledArtifact = z.strictObject({
device_path: z.string().min(1),
file_name: z.string().min(1),
+69
View File
@@ -0,0 +1,69 @@
import { z } from "zod";
const MAX_TRANSPORT_ID = "18446744073709551615";
/** Canonical positive uint64 identity; independent of JSON number precision. */
export const adbTransportIdSchema = z
.string()
.regex(/^[1-9]\d*$/u)
.refine(
(value) =>
value.length < MAX_TRANSPORT_ID.length ||
(value.length === MAX_TRANSPORT_ID.length && value <= MAX_TRANSPORT_ID),
"Transport ID exceeds the unsigned 64-bit range",
)
.brand<"AdbTransportId">();
export const adbTransportIdentitySchema = z.discriminatedUnion("status", [
z.strictObject({
status: z.literal("reported"),
decimal: adbTransportIdSchema,
source_token: z.string(),
source_line: z.string(),
}),
z.strictObject({
status: z.literal("not_reported"),
source_token: z.null(),
source_line: z.string(),
}),
z.strictObject({
status: z.literal("invalid"),
source_token: z.string(),
source_line: z.string(),
reason: z.enum(["invalid_decimal", "reserved_zero", "outside_uint64"]),
}),
]);
export type AdbTransportIdentity = z.output<typeof adbTransportIdentitySchema>;
/** Admit source decimal text once; never round a transport identity through Number. */
export const parseAdbTransportIdentity = (
source_token: string | null,
source_line: string,
): AdbTransportIdentity => {
if (source_token === null)
return { status: "not_reported", source_token, source_line };
if (!/^\d+$/u.test(source_token))
return {
status: "invalid",
source_token,
source_line,
reason: "invalid_decimal",
};
const decimal = source_token.replace(/^0+(?=\d)/u, "");
if (decimal === "0")
return {
status: "invalid",
source_token,
source_line,
reason: "reserved_zero",
};
const admitted = adbTransportIdSchema.safeParse(decimal);
return admitted.success
? { status: "reported", decimal: admitted.data, source_token, source_line }
: {
status: "invalid",
source_token,
source_line,
reason: "outside_uint64",
};
};
+39
View File
@@ -791,3 +791,42 @@ exit 0
await expect(lstat(output)).rejects.toMatchObject({ code: "ENOENT" });
},
);
it.skipIf(process.platform === "win32")(
"preserves exact transport identity through CLI JSON projection",
async () => {
const workspace = await createTestWorkspace("rea-adb-identity-cli-");
onTestFinished(() => removeTestWorkspace(workspace.root));
const line =
"exact-serial device model:Pixel transport_id:18446744073709551615";
const binary = await workspace.write(
"adb",
`#!${process.execPath}\nif(process.argv[2]==='version')process.stdout.write('Android Debug Bridge version 1.0.41\\nVersion 35.0.2-inert\\n');\nelse if(process.argv[2]==='devices'&&process.argv[3]==='-l')process.stdout.write(${JSON.stringify(line + "\n")});\nelse throw Error('No other command authorized');\n`,
);
await chmod(binary, 0o755);
const result = await serve(["list-adb-devices", "--json"], {
REA_ADB_PATH: binary,
});
expect(result.exitCode).toBe(0);
const evidence = JSON.parse(result.stdout);
expect(evidence.normalized_result).toMatchObject({
devices: [
{
serial: "exact-serial",
model: "Pixel",
transport_id: null,
transport_id_text: "18446744073709551615",
transport_id_status: "reported",
},
],
});
expect(evidence.raw_result).toMatchObject({
transport_id_observations: [
{ source_line: line, source_token: "18446744073709551615" },
],
});
expect(
toolContract("list_adb_devices").outputSchema.safeParse(evidence).success,
).toBe(true);
},
);
+67 -1
View File
@@ -8,6 +8,7 @@ import { ok } from "../../../src/domain/result.js";
import { createServer } from "../../../src/server/createServer.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { createMcpSchemaValidation } from "../../fixtures/mcpSchemaValidation.js";
import { assertAdvertisedMcpContracts } from "./mcpContractHarness.js";
const identity = { id: "adb", name: "Android Debug Bridge", version: null };
@@ -34,10 +35,32 @@ const results: Readonly<Record<string, unknown>> = {
product: "sdk_gphone64_x86_64",
model: "sdk_gphone64_x86_64",
device: "emu64xa",
transport_id: 1,
transport_identity: {
status: "reported",
decimal: "1",
source_token: "1",
source_line: "emulator-5554 device transport_id:1",
},
kind: "emulator",
kind_basis: "emulator_serial_prefix",
},
{
serial: "exact-serial",
state: "device",
transport: "usb",
product: "product",
model: "Pixel",
device: "device",
transport_identity: {
status: "reported",
decimal: "18446744073709551615",
source_token: "18446744073709551615",
source_line:
"exact-serial device model:Pixel transport_id:18446744073709551615",
},
kind: "physical",
kind_basis: "usb_transport",
},
],
may_have_started_adb_server: false,
},
@@ -342,6 +365,49 @@ it("advertises exact valid ADB schemas and records inline evidence through MCP",
for (const name of operationNames) expect(names, name).toContain(name);
assertAdvertisedMcpContracts(operationNames, tools);
const listed = await client.callTool({
name: "list_adb_devices",
arguments: {},
});
expect(listed.isError).not.toBe(true);
const listing = parseEvidence(listed.structuredContent);
expect(listing.normalized_result).toMatchObject({
devices: [
{
serial: "emulator-5554",
transport_id: 1,
transport_id_text: "1",
transport_id_status: "reported",
},
{
serial: "exact-serial",
transport_id: null,
transport_id_text: "18446744073709551615",
transport_id_status: "reported",
},
],
});
expect(listing.raw_result).toMatchObject({
transport_id_observations: [
{ serial: "emulator-5554", source_token: "1" },
{ serial: "exact-serial", source_token: "18446744073709551615" },
],
});
const listingSchema = tools.find(
({ name }) => name === "list_adb_devices",
)?.outputSchema;
if (listingSchema === undefined)
throw new Error("Missing listing output schema");
const validation = createMcpSchemaValidation();
onTestFinished(() => validation.clear());
expect(validation.assertions.validateSchema(listingSchema)).toBe(true);
expect(validation.assertions.validate(listingSchema, listing)).toBe(true);
const malformed = structuredClone(listing);
const report = malformed.normalized_result as {
devices: { transport_id_text: string | null }[];
};
report.devices[1]!.transport_id_text = "not-a-decimal";
expect(validation.assertions.validate(listingSchema, malformed)).toBe(false);
const pulled = await client.callTool({
name: "pull_adb_package",