Files
rea/tests/acceptance/applications/assetCatalogResourceKeys.test.ts
morluto 9258df02cc test(apple): compare asset catalogs from matching root workflows
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.
2026-10-21 07:37:36 +08:00

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,
);