fix(apple): keep empty asset catalog pages complete
This commit is contained in:
1 parent
3a9176098d
commit
9db5f1b276
4 files changed
+141
-31
No files matched your search
@@ -64,33 +64,42 @@ describe("Apple asset catalog application workflow", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("reports an app without compiled catalogs as an empty observed inventory", async () => {
|
||||
const bundle = await createTestTempDirectory("rea-asset-catalog-absent-");
|
||||
await mkdir(join(bundle, "Contents", "Resources"), { recursive: true });
|
||||
await writeFile(join(bundle, "Contents", "Resources", "icon.icns"), "");
|
||||
// Names that merely end in Assets.car are ordinary resources.
|
||||
await mkdir(join(bundle, "Contents", "Resources", "NotAssets.car"));
|
||||
await writeFile(join(bundle, "Contents", "Resources", "OldAssets.car"), "");
|
||||
const result = await analyzeAppleAssetCatalogs({
|
||||
environment: {},
|
||||
bundlePath: bundle,
|
||||
targetSha256: "a".repeat(64),
|
||||
runAssetUtil: () => {
|
||||
throw new Error("assetutil must not run without a catalog");
|
||||
},
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
catalogs: [],
|
||||
total_records: 0,
|
||||
records: [],
|
||||
resource_key_matches: [],
|
||||
next_offset: null,
|
||||
truncated: false,
|
||||
});
|
||||
expect(result.limitations).toContain(
|
||||
"No compiled Assets.car catalog was found among the app bundle's regular files (symlinks are not followed); any resource keys are reported as unmatched.",
|
||||
);
|
||||
});
|
||||
it.each([0, 1, 5])(
|
||||
"reports an app without compiled catalogs as empty at offset %i",
|
||||
async (offset) => {
|
||||
const bundle = await createTestTempDirectory("rea-asset-catalog-absent-");
|
||||
await mkdir(join(bundle, "Contents", "Resources"), { recursive: true });
|
||||
await writeFile(join(bundle, "Contents", "Resources", "icon.icns"), "");
|
||||
// Names that merely end in Assets.car are ordinary resources.
|
||||
await mkdir(join(bundle, "Contents", "Resources", "NotAssets.car"));
|
||||
await writeFile(
|
||||
join(bundle, "Contents", "Resources", "OldAssets.car"),
|
||||
"",
|
||||
);
|
||||
const result = await analyzeAppleAssetCatalogs({
|
||||
environment: {},
|
||||
bundlePath: bundle,
|
||||
targetSha256: "a".repeat(64),
|
||||
page: { offset },
|
||||
runAssetUtil: () => {
|
||||
throw new Error("assetutil must not run without a catalog");
|
||||
},
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
catalogs: [],
|
||||
total_records: 0,
|
||||
offset,
|
||||
limit: 1_000,
|
||||
records: [],
|
||||
resource_key_matches: [],
|
||||
next_offset: null,
|
||||
truncated: false,
|
||||
});
|
||||
expect(result.limitations).toContain(
|
||||
"No compiled Assets.car catalog was found among the app bundle's regular files (symlinks are not followed); any resource keys are reported as unmatched.",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("does not report absence when the only catalog entry is a symlink", async () => {
|
||||
const bundle = await createTestTempDirectory("rea-asset-catalog-link-");
|
||||
@@ -137,6 +146,31 @@ describe("Apple asset catalog application workflow", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each([0, 1, 5])(
|
||||
"keeps an empty assetutil inventory complete at offset %i",
|
||||
async (offset) => {
|
||||
const bundle = await createTestTempDirectory("rea-asset-catalog-empty-");
|
||||
const resources = join(bundle, "Contents", "Resources");
|
||||
await mkdir(resources, { recursive: true });
|
||||
await writeFile(join(resources, "Assets.car"), "inert catalog fixture");
|
||||
const result = await analyzeAppleAssetCatalogs({
|
||||
environment: {},
|
||||
bundlePath: bundle,
|
||||
targetSha256: "a".repeat(64),
|
||||
page: { offset, limit: 2 },
|
||||
runAssetUtil: () => Promise.resolve("[]"),
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
total_records: 0,
|
||||
offset,
|
||||
limit: 2,
|
||||
records: [],
|
||||
next_offset: null,
|
||||
truncated: false,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([1, 2])(
|
||||
"joins NIBArchive keys against %i supplied assetutil renditions",
|
||||
async (count) => {
|
||||
|
||||
@@ -104,3 +104,72 @@ describe("compiled Apple asset catalog projection", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("reports truncation only when a nonempty inventory has omitted records", () => {
|
||||
const empty = [{ path: "Assets.car", sha256: "a".repeat(64), records: [] }];
|
||||
for (const offset of [0, 1, 5]) {
|
||||
expect(
|
||||
projectAppleAssetCatalogPage({
|
||||
targetSha256: "c".repeat(64),
|
||||
catalogs: empty,
|
||||
offset,
|
||||
limit: 1,
|
||||
}),
|
||||
).toMatchObject({
|
||||
total_records: 0,
|
||||
records: [],
|
||||
next_offset: null,
|
||||
truncated: false,
|
||||
});
|
||||
}
|
||||
|
||||
const records = parseAppleAssetCatalogRecords([
|
||||
{ Name: "One" },
|
||||
{ Name: "Two" },
|
||||
{ Name: "Three" },
|
||||
]);
|
||||
const catalogs = [{ path: "Assets.car", sha256: "a".repeat(64), records }];
|
||||
expect(
|
||||
projectAppleAssetCatalogPage({
|
||||
targetSha256: "c".repeat(64),
|
||||
catalogs,
|
||||
offset: 0,
|
||||
limit: 3,
|
||||
}),
|
||||
).toMatchObject({
|
||||
records: records.map((metadata, index) => ({ index, metadata })),
|
||||
next_offset: null,
|
||||
truncated: false,
|
||||
});
|
||||
expect(
|
||||
projectAppleAssetCatalogPage({
|
||||
targetSha256: "c".repeat(64),
|
||||
catalogs,
|
||||
offset: 0,
|
||||
limit: 2,
|
||||
}),
|
||||
).toMatchObject({
|
||||
records: expect.arrayContaining([
|
||||
expect.objectContaining({ index: 0 }),
|
||||
expect.objectContaining({ index: 1 }),
|
||||
]),
|
||||
next_offset: 2,
|
||||
truncated: true,
|
||||
});
|
||||
expect(
|
||||
projectAppleAssetCatalogPage({
|
||||
targetSha256: "c".repeat(64),
|
||||
catalogs,
|
||||
offset: 1,
|
||||
limit: 2,
|
||||
}),
|
||||
).toMatchObject({ next_offset: null, truncated: true });
|
||||
expect(
|
||||
projectAppleAssetCatalogPage({
|
||||
targetSha256: "c".repeat(64),
|
||||
catalogs,
|
||||
offset: 3,
|
||||
limit: 2,
|
||||
}),
|
||||
).toMatchObject({ records: [], next_offset: null, truncated: true });
|
||||
});
|
||||
@@ -188,7 +188,7 @@ export const projectAppleAssetCatalogPage = (input: {
|
||||
records,
|
||||
resource_key_matches: resourceKeyMatches,
|
||||
next_offset: next < all.length ? next : null,
|
||||
truncated: input.offset > 0 || next < all.length,
|
||||
truncated: records.length !== all.length,
|
||||
limitations: [
|
||||
"Rendition metadata is reported as emitted by the installed assetutil version; private fields and platform-specific interpretations are not normalized.",
|
||||
"Asset catalog inspection does not decode or extract rendition image bytes.",
|
||||
|
||||
@@ -46,7 +46,7 @@ const keyedArchive = (nested: boolean) =>
|
||||
});
|
||||
|
||||
cliTest.skipIf(process.platform !== "darwin")(
|
||||
"retains recognized NIBArchive resource keys and provenance through CLI and MCP without a catalog",
|
||||
"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();
|
||||
@@ -91,13 +91,20 @@ cliTest.skipIf(process.platform !== "darwin")(
|
||||
[field]: "Toolbar",
|
||||
});
|
||||
const inspected = await cli.run({
|
||||
arguments: ["inspect-asset-catalog", app, "--json"],
|
||||
arguments: ["inspect-asset-catalog", app, "--offset", "1", "--json"],
|
||||
});
|
||||
expect(inspected.exitCode, inspected.stderr).toBe(0);
|
||||
const result = appleAssetCatalogResultSchema.parse(
|
||||
parseEvidence(inspected.json).normalized_result,
|
||||
);
|
||||
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
|
||||
? []
|
||||
@@ -117,7 +124,7 @@ cliTest.skipIf(process.platform !== "darwin")(
|
||||
);
|
||||
const opened = await call("open_binary", { path: app });
|
||||
expect(opened.isError).not.toBe(true);
|
||||
const mcp = await call("inspect_asset_catalog", {});
|
||||
const mcp = await call("inspect_asset_catalog", { offset: 1 });
|
||||
expect(mcp.isError).not.toBe(true);
|
||||
expect(parseEvidence(mcp.structuredContent).normalized_result).toEqual(
|
||||
result,
|
||||
|
||||
Reference in new issue
Block a user