Compare explicit bundle-resource CLI and MCP results at the positive empty-inventory offset. Preserve a separate binary-bound MCP assertion with the exact fixture executable digest and complete metadata payload. Validation: reproduced the stale root-versus-binary parity failure on current main, then passed the macOS public CLI/MCP acceptance case across all five NIB fixtures. Focused lint, formatting and diff checks passed. No target code was executed.
159 lines
6.2 KiB
TypeScript
159 lines
6.2 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { expect } from "vitest";
|
|
import { buildBinary } from "plist";
|
|
|
|
import { encodeNibArchiveFixture } from "../../../src/artifacts/apple/NibArchive.fixture.js";
|
|
import { appleAssetCatalogResultSchema } from "../../../src/domain/apple/appleAssetCatalog.js";
|
|
import { interfaceBuilderAnalysisSchema } from "../../../src/domain/apple/interfaceBuilderGraph.js";
|
|
import { parseEvidence } from "../../../src/domain/evidence.js";
|
|
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { cliTest } from "../../support/cli/cliFixture.js";
|
|
|
|
// Exact rooted NIBArchive bytes from #2176; inert parser fixture, not ibtool output.
|
|
const reportedArchive = Buffer.from(
|
|
"TklCQXJjaGl2ZQEAAAAKAAAABAAAADIAAAAEAAAAPgAAAAQAAABgAAAABAAAAHwAAACAgIGBgYGCgoGDg4GGTlNSb290hWltYWdli05TSW1hZ2VOYW1liE5TLmJ5dGVzgAoBAAAAgQoCAAAAggoDAAAAgwiHVG9vbGJhco6ATlNJQk9iamVjdERhdGGLgE5TSW1hZ2VWaWV3h4BOU0ltYWdliIBOU1N0cmluZw==",
|
|
"base64",
|
|
);
|
|
const resourceArchive = (field: string) =>
|
|
encodeNibArchiveFixture({
|
|
classes: ["NSIBObjectData", "NSImageView", "NSImage", "NSString"],
|
|
objects: [
|
|
{ classIndex: 0, values: { NSRoot: { ref: 1 } } },
|
|
{ classIndex: 1, values: { image: { ref: 2 } } },
|
|
{ classIndex: 2, values: { [field]: { ref: 3 } } },
|
|
{ classIndex: 3, values: { "NS.bytes": "Toolbar" } },
|
|
],
|
|
});
|
|
|
|
const keyedArchive = (nested: boolean) =>
|
|
buildBinary({
|
|
$archiver: "NSKeyedArchiver",
|
|
$version: 100000,
|
|
$top: { root: { UID: 1 } },
|
|
$objects: [
|
|
"$null",
|
|
{
|
|
$class: { UID: 2 },
|
|
...(nested
|
|
? { nibValues: { NSImageName: "Toolbar" } }
|
|
: { NSImageName: "Toolbar" }),
|
|
},
|
|
{ $classname: "NSImage", $classes: ["NSImage", "NSObject"] },
|
|
],
|
|
});
|
|
|
|
cliTest.skipIf(process.platform !== "darwin")(
|
|
"retains resource keys and complete empty inventory through CLI and MCP at a positive offset",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-asset-resource-keys-");
|
|
const { call } = await connectLocalToolsMcp();
|
|
for (const [name, field, archive, recognized] of [
|
|
["reported", "NSImageName", reportedArchive, true],
|
|
["alias", "imageName", resourceArchive("imageName"), true],
|
|
["title", "title", resourceArchive("title"), false],
|
|
["keyed-direct", "NSImageName", keyedArchive(false), true],
|
|
["keyed-nested", "NSImageName", keyedArchive(true), false],
|
|
] as const) {
|
|
const app = join(root, `${name}.app`);
|
|
const contents = join(app, "Contents");
|
|
await mkdir(join(contents, "Resources"), { recursive: true });
|
|
await mkdir(join(contents, "MacOS"));
|
|
const mach = Buffer.alloc(32);
|
|
mach.writeUInt32LE(0xfeedfacf, 0);
|
|
mach.writeUInt32LE(0x0100000c, 4);
|
|
mach.writeUInt32LE(2, 12);
|
|
await writeFile(join(contents, "MacOS", "App"), mach);
|
|
await writeFile(
|
|
join(contents, "Info.plist"),
|
|
"<plist><dict><key>CFBundleExecutable</key><string>App</string></dict></plist>",
|
|
);
|
|
await writeFile(join(contents, "Resources", "Panel.nib"), archive);
|
|
const decoded = await cli.run({
|
|
arguments: ["decode-interface-builder", app, "--json"],
|
|
});
|
|
expect(decoded.exitCode, decoded.stderr).toBe(0);
|
|
const graph = interfaceBuilderAnalysisSchema.parse(
|
|
parseEvidence(decoded.json).normalized_result,
|
|
).graph;
|
|
expect(graph.truncated).toBe(false);
|
|
const resource = graph.nodes.find(
|
|
(node) =>
|
|
node.kind === "resource" && node.attributes.class_name === "NSImage",
|
|
);
|
|
expect(
|
|
name === "keyed-direct"
|
|
? resource?.attributes
|
|
: resource?.attributes.nibValues,
|
|
).toMatchObject({
|
|
[field]: "Toolbar",
|
|
});
|
|
const inspected = await cli.run({
|
|
arguments: ["inspect-asset-catalog", app, "--offset", "1", "--json"],
|
|
});
|
|
expect(inspected.exitCode, inspected.stderr).toBe(0);
|
|
const result = appleAssetCatalogResultSchema.parse(
|
|
parseEvidence(inspected.json).normalized_result,
|
|
);
|
|
const executableSha256 = createHash("sha256").update(mach).digest("hex");
|
|
expect(result.catalogs).toEqual([]);
|
|
expect(result).toMatchObject({
|
|
total_records: 0,
|
|
offset: 1,
|
|
records: [],
|
|
next_offset: null,
|
|
truncated: false,
|
|
});
|
|
expect(result.resource_key_matches).toEqual(
|
|
!recognized
|
|
? []
|
|
: [
|
|
{
|
|
source_node_id: resource?.id,
|
|
source_path: "Contents/Resources/Panel.nib",
|
|
source_archive_sha256: createHash("sha256")
|
|
.update(archive)
|
|
.digest("hex"),
|
|
field,
|
|
key: "Toolbar",
|
|
status: "unmatched",
|
|
renditions: [],
|
|
},
|
|
],
|
|
);
|
|
const opened = await call("open_bundle_resources", { path: app });
|
|
expect(opened.isError).not.toBe(true);
|
|
const mcp = await call("inspect_asset_catalog", { offset: 1 });
|
|
expect(mcp.isError).not.toBe(true);
|
|
expect(parseEvidence(mcp.structuredContent).normalized_result).toEqual(
|
|
result,
|
|
);
|
|
|
|
const binaryOpened = await call("open_binary", { path: app });
|
|
expect(binaryOpened.isError).not.toBe(true);
|
|
const binary = await call("inspect_asset_catalog", { offset: 1 });
|
|
expect(binary.isError).not.toBe(true);
|
|
const binaryResult = appleAssetCatalogResultSchema.parse(
|
|
parseEvidence(binary.structuredContent).normalized_result,
|
|
);
|
|
expect(binaryResult.target_sha256).toBe(executableSha256);
|
|
expect(binaryResult.resource_root).toBeUndefined();
|
|
expect(binaryResult).toEqual({
|
|
target_sha256: executableSha256,
|
|
catalogs: result.catalogs,
|
|
total_records: result.total_records,
|
|
offset: result.offset,
|
|
limit: result.limit,
|
|
records: result.records,
|
|
resource_key_matches: result.resource_key_matches,
|
|
next_offset: result.next_offset,
|
|
truncated: result.truncated,
|
|
limitations: result.limitations,
|
|
});
|
|
}
|
|
},
|
|
30_000,
|
|
);
|