Files
rea/tests/acceptance/flutterAnalysis.test.ts
T
morluto 678c4cfa68 fix(flutter): preserve short GNU build IDs through public workflows
Reuse the reader correction from PR #6705, and share the byte projection budget with output validation. Verify actual APK parsing through compiled CLI and stdio MCP at 0, 1, 2, 3, 64 and 65 descriptor bytes.

Co-authored-by: Ezra <64943463+ezrabrilliant@users.noreply.github.com>

Fixes #6697
2026-10-24 00:11:41 +08:00

290 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { gnuBuildIdNote } from "../../src/native/elf/ElfNotes.fixture.js";
import { createHash } from "node:crypto";
import { expect } from "vitest";
import { toolContract } from "../../src/contracts/toolContracts.js";
import { parseEvidence } from "../../src/domain/evidence.js";
import { flutterResultSchemas } from "../../src/domain/flutter/flutterBuildAnalysis.js";
import {
aotFixtureImage,
engineFixtureImage,
REFERENCE_HASH,
appendElfRanges,
} from "../../src/flutter/FlutterBuild.fixture.js";
import {
createFlutterApk,
createDuplicateFlutterApk,
} from "../fixtures/flutterApk.js";
import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js";
import { parseMcpToolError } from "../fixtures/mcpToolError.js";
import { cliTest } from "../support/cli/cliFixture.js";
cliTest(
"CLI and stdio MCP retain Flutter identity, selected ABI and duplicate-member refusals",
async ({ cli }) => {
const image = aotFixtureImage();
Buffer.from("Login\0").copy(image, 0x2300);
const engine = engineFixtureImage({
notes: Buffer.from(
"040000000800000003000000474e55000123456789abcdef",
"hex",
),
prefix: Buffer.from(
"040000000800000003000000474e55001111111111111111",
"hex",
),
labels: ["Android (test) clang version 9.0.8"],
});
const path = await createFlutterApk("selected target.apk", [
{ path: "lib/arm64-v8a/libapp.so", bytes: image },
{
path: "lib/arm64-v8a/libflutter.so",
bytes: engine,
},
{ path: "lib/x86_64/libapp.so", bytes: image },
{
path: "lib/x86_64/libflutter.so",
bytes: engine,
},
]);
const { call } = await connectLocalToolsMcp();
for (const [name, abi] of [
["identify_flutter_build", undefined],
["inspect_dart_aot", undefined],
["inspect_dart_aot", "x86_64"],
] as const) {
const result = await cli.run({
arguments: [
name.replaceAll("_", "-"),
path,
"--json",
...(abi === undefined ? [] : ["--abi", abi]),
],
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = toolContract(name).outputSchema.parse(result.json);
const complete = flutterResultSchemas[name].parse(
evidence.normalized_result,
);
const response = await call(name, {
path,
...(abi === undefined ? {} : { abi }),
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
const mcp = parseEvidence(response.structuredContent);
expect(mcp.normalized_result).toEqual(evidence.normalized_result);
expect(mcp.subject).toEqual(evidence.subject);
expect(mcp.subject).toMatchObject({
local_path: path,
format: "apk",
digest: { sha256: complete.target.sha256 },
});
expect(mcp.evidence_id).toBe(evidence.evidence_id);
if (name === "inspect_dart_aot") {
expect(evidence.normalized_result).toMatchObject({
abi: abi ?? "arm64-v8a",
libapp: {
coverage: "complete",
snapshot_hash: REFERENCE_HASH,
sha256: createHash("sha256").update(image).digest("hex"),
},
string_pool: {
coverage: "complete",
class_like_tokens: expect.arrayContaining(["Login"]),
},
});
} else {
const identified = toolContract(
"identify_flutter_build",
).outputSchema.parse(result.json).normalized_result;
expect(identified.coverage).toBe("complete");
for (const abi of identified.abis) {
expect(abi.libflutter).toMatchObject({
build_id: "0123456789abcdef",
build_id_candidates: ["0123456789abcdef"],
});
}
}
}
const duplicate = await createDuplicateFlutterApk();
for (const name of [
"identify_flutter_build",
"inspect_dart_aot",
] as const) {
const result = await cli.run({
arguments: [name.replaceAll("_", "-"), duplicate, "--json"],
});
expect(result.exitCode).toBe(1);
const mcp = parseMcpToolError(await call(name, { path: duplicate }));
expect(mcp.error).toEqual(result.json);
expect(mcp.error).toMatchObject({
code: "invalid_request",
details: { issues: [{ message: expect.stringContaining(duplicate) }] },
});
}
},
);
cliTest(
"CLI and MCP retain unsupported snapshot kinds as partial evidence",
async ({ cli }) => {
const image = aotFixtureImage();
for (const offset of [0x1000, 0x2000])
image.writeBigUInt64LE(4n, offset + 12);
const path = await createFlutterApk("unsupported-kind.apk", [
{ path: "lib/arm64-v8a/libapp.so", bytes: image },
]);
const result = await cli.run({
arguments: ["inspect-dart-aot", path, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = toolContract("inspect_dart_aot").outputSchema.parse(
result.json,
);
const complete = flutterResultSchemas.inspect_dart_aot.parse(
evidence.normalized_result,
);
const { call } = await connectLocalToolsMcp();
const response = await call("inspect_dart_aot", { path });
expect(response.isError, JSON.stringify(response)).not.toBe(true);
expect(parseEvidence(response.structuredContent).normalized_result).toEqual(
evidence.normalized_result,
);
expect(evidence.normalized_result).toMatchObject({
coverage: "partial",
libapp: { coverage: "partial", snapshot_hash: REFERENCE_HASH },
string_pool: { coverage: "complete" },
});
expect(
complete.libapp.sections
.filter((section) => section.name.endsWith("Data"))
.map((section) => section.kind),
).toEqual([4, 4]);
},
);
cliTest(
"CLI and MCP qualify conflicting load mappings and bounded ELF notes",
async ({ cli }) => {
const note = Buffer.from(
"040000000800000003000000474e55000123456789abcdef",
"hex",
);
const engine = engineFixtureImage({
notes: Buffer.concat([note, Buffer.alloc(2000)]),
});
const denseEngine = appendElfRanges(
engine,
"program",
Array.from({ length: 2000 }, (_, index) => ({
type: 4,
offset: 0x400 + note.length + index,
size: 1,
})),
);
const { call } = await connectLocalToolsMcp();
for (const conflict of [false, true]) {
const image = appendElfRanges(aotFixtureImage(), "program", [
{
type: 1,
address: 0x2010,
offset: conflict ? 0x1010 : 0x2010,
size: 4,
},
]);
const path = await createFlutterApk(`load-${String(conflict)}.apk`, [
{ path: "lib/x86_64/libapp.so", bytes: image },
{
path: "lib/x86_64/libflutter.so",
bytes: conflict ? denseEngine : engine,
},
]);
for (const name of [
"inspect_dart_aot",
"identify_flutter_build",
] as const) {
const result = await cli.run({
arguments: [name.replaceAll("_", "-"), path, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = toolContract(name).outputSchema.parse(result.json);
const response = await call(name, { path });
expect(response.isError, JSON.stringify(response)).not.toBe(true);
const mcp = parseEvidence(response.structuredContent);
expect(mcp.normalized_result).toEqual(evidence.normalized_result);
expect(mcp.evidence_id).toBe(evidence.evidence_id);
expect(mcp.subject).toEqual(evidence.subject);
expect(mcp.limitations).toEqual(evidence.limitations);
expect(evidence.normalized_result.coverage).toBe(
conflict ? "partial" : "complete",
);
if (name === "inspect_dart_aot") {
const facts = flutterResultSchemas[name].parse(
evidence.normalized_result,
);
expect(facts.libapp.sections).toHaveLength(conflict ? 3 : 4);
expect(facts.string_pool.coverage).toBe(
conflict ? "partial" : "complete",
);
if (conflict) {
expect(facts.string_pool.package_uris).toEqual([]);
expect(evidence.limitations.join("\n")).toContain(
"conflicting PT_LOAD",
);
}
} else {
const facts = flutterResultSchemas[name].parse(
evidence.normalized_result,
);
expect(facts.abis[0]?.libflutter).toMatchObject({
build_id: conflict ? null : "0123456789abcdef",
build_id_candidates: ["0123456789abcdef"],
});
if (conflict)
expect(evidence.limitations.join("\n")).toContain("note-range");
}
}
}
},
);
cliTest(
"CLI and stdio MCP preserve byte-sized GNU build IDs and descriptor coverage",
async ({ cli }) => {
const { call } = await connectLocalToolsMcp();
for (const bytes of [0, 1, 2, 3, 64, 65]) {
const hex = "ab".repeat(bytes);
const path = await createFlutterApk(`engine-${bytes}.apk`, [
{ path: "lib/arm64-v8a/libapp.so", bytes: aotFixtureImage() },
{
path: "lib/arm64-v8a/libflutter.so",
bytes: engineFixtureImage({ notes: gnuBuildIdNote(hex) }),
},
]);
const output = await cli.run({
arguments: ["identify-flutter-build", path, "--json"],
});
expect(output.exitCode, output.stderr).toBe(0);
const cliEvidence = parseEvidence(output.json);
const response = await call("identify_flutter_build", { path });
expect(response.isError, JSON.stringify(response)).not.toBe(true);
const mcpEvidence = parseEvidence(response.structuredContent);
expect(mcpEvidence.normalized_result).toEqual(
cliEvidence.normalized_result,
);
const result = flutterResultSchemas.identify_flutter_build.parse(
cliEvidence.normalized_result,
);
const admitted = bytes > 0 && bytes <= 64;
expect(result.abis[0]?.libflutter).toMatchObject({
build_id: admitted ? hex : null,
build_id_candidates: admitted ? [hex] : [],
});
expect(result.coverage).toBe(admitted ? "complete" : "partial");
if (!admitted)
expect(cliEvidence.limitations.join("\n")).toContain(
"1–64 byte projection",
);
}
},
);