mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(apple): retain unsupported NIB connectors in connection coverage
Recognize AppKit binding connectors as explicit unknown connection facts and report partial semantic coverage rather than silently omitting them. Share format-aware connector admission across keyed archives and NIBArchive. Make the keyed archive table own parsed class and superclass facts for names, array hierarchy traversal, inherited connectors, and class-table projection. Preserve malformed class evidence and exclude unrelated custom class names. Validation: 84 focused tests passed, including compiled CLI, filesystem, MCP summary, and retained views; check:fast and test discovery passed. Independent source review found no blocker. Production CLI and stdio MCP replayed a read-only copy of Terminal 2.15's picker NIB: 13 connections with partial unsupported-type coverage, versus baseline 9 with complete coverage. Advertised schemas and retained coverage results validated with the SDK/Ajv. No app, analysis target, or optional deep provider was executed. Fixes #5590
This commit is contained in:
1 parent
a35623707e
commit
14cd78c322
7 files changed
+415
-43
No files matched your search
@@ -4,6 +4,7 @@ import { parseBinary } from "plist";
|
||||
import { appleResourceKeyFields } from "../../domain/apple/appleAssetCatalog.js";
|
||||
import {
|
||||
interfaceBuilderConnectionFieldNames,
|
||||
isInterfaceBuilderConnectionClass,
|
||||
isUIKitRuntimeConnectionClass,
|
||||
} from "../../domain/apple/interfaceBuilderKeyedArchive.js";
|
||||
import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js";
|
||||
@@ -576,7 +577,7 @@ const projectNibArchive = (
|
||||
// AppKit connectors use NS-prefixed keys; UIKit's runtime outlet and
|
||||
// event connections use UI-prefixed keys with the same roles.
|
||||
const uiKit = isUIKitRuntimeConnectionClass(className);
|
||||
if (!uiKit && !className.includes("Connector")) continue;
|
||||
if (!isInterfaceBuilderConnectionClass(className)) continue;
|
||||
const values = object.values;
|
||||
const connectionFields = interfaceBuilderConnectionFieldNames(className);
|
||||
const source = reference(values[connectionFields.source]);
|
||||
|
||||
@@ -77,6 +77,23 @@ describe("NIBArchive decoder", () => {
|
||||
expect(decoded.coder_version).toBe(10);
|
||||
});
|
||||
|
||||
it("counts AppKit outlet, action, and binding connector objects", () => {
|
||||
const archive = encodeArchive({
|
||||
classes: [
|
||||
"NSNibOutletConnector",
|
||||
"NSNibControlConnector",
|
||||
"NSNibBindingConnector",
|
||||
],
|
||||
objects: [
|
||||
{ classIndex: 0, values: {} },
|
||||
{ classIndex: 1, values: {} },
|
||||
{ classIndex: 2, values: {} },
|
||||
],
|
||||
});
|
||||
|
||||
expect(decodeNibArchive(archive).connection_object_count).toBe(3);
|
||||
});
|
||||
|
||||
it("rejects malformed references and unsupported format versions", () => {
|
||||
const valid = encodeArchive({
|
||||
classes: ["NSObject"],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { JsonValue } from "../../domain/jsonValue.js";
|
||||
import { hasNibArchiveSignature } from "../../domain/apple/appleArchiveFormat.js";
|
||||
import { isInterfaceBuilderConnectionClass } from "../../domain/apple/interfaceBuilderKeyedArchive.js";
|
||||
import { TextDecoder } from "node:util";
|
||||
import {
|
||||
InterfaceBuilderDecodeBudget,
|
||||
@@ -147,9 +148,7 @@ export const decodeNibArchive = (
|
||||
});
|
||||
let connectionObjectCount = 0;
|
||||
for (const object of objects)
|
||||
if (
|
||||
/(?:Outlet|Connection|Segue|ActionConnection)/iu.test(object.class_name)
|
||||
)
|
||||
if (isInterfaceBuilderConnectionClass(object.class_name))
|
||||
connectionObjectCount += 1;
|
||||
|
||||
return {
|
||||
|
||||
@@ -707,7 +707,170 @@ describe("AppKit keyed archive connectors", () => {
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("unsupported keyed archive connections", () => {
|
||||
it("keeps unsupported binding connectors in connection coverage", () => {
|
||||
const raw = {
|
||||
$archiver: "NSKeyedArchiver",
|
||||
$objects: [
|
||||
"$null",
|
||||
{ $class: { UID: 6 } },
|
||||
{ $class: { UID: 6 } },
|
||||
{
|
||||
$class: { UID: 7 },
|
||||
NSSource: { UID: 1 },
|
||||
NSDestination: { UID: 2 },
|
||||
NSLabel: "outlet",
|
||||
},
|
||||
{
|
||||
$class: { UID: 8 },
|
||||
NSSource: { UID: 1 },
|
||||
NSDestination: { UID: 2 },
|
||||
NSLabel: "performAction:",
|
||||
},
|
||||
{
|
||||
$class: { UID: 9 },
|
||||
NSSource: { UID: 1 },
|
||||
NSDestination: { UID: 2 },
|
||||
NSLabel: "value",
|
||||
},
|
||||
{ $classname: "NSView", $classes: ["NSView", "NSObject"] },
|
||||
{
|
||||
$classname: "NSNibOutletConnector",
|
||||
$classes: ["NSNibOutletConnector", "NSNibConnector", "NSObject"],
|
||||
},
|
||||
{
|
||||
$classname: "NSNibControlConnector",
|
||||
$classes: ["NSNibControlConnector", "NSNibConnector", "NSObject"],
|
||||
},
|
||||
{
|
||||
$classname: "NSNibBindingConnector",
|
||||
$classes: ["NSNibBindingConnector", "NSNibConnector", "NSObject"],
|
||||
},
|
||||
],
|
||||
$top: {},
|
||||
};
|
||||
|
||||
const parsed = parseInterfaceBuilderRecords(raw);
|
||||
expect(parsed.connections.map(({ kind }) => kind)).toEqual([
|
||||
"outlet",
|
||||
"action",
|
||||
"other",
|
||||
]);
|
||||
|
||||
const analysis = buildInterfaceBuilderAnalysis({
|
||||
targetSha256: hash,
|
||||
toolVersion: "test",
|
||||
documents: [
|
||||
{
|
||||
relativePath: "MainMenu.nib",
|
||||
archiveSha256: hash,
|
||||
documentKind: "nib",
|
||||
raw,
|
||||
},
|
||||
],
|
||||
limits: interfaceBuilderLimitsSchema.parse({}),
|
||||
});
|
||||
expect(analysis.documents[0]?.connection_count).toBe(3);
|
||||
expect(analysis.graph.coverage).toContainEqual(
|
||||
expect.objectContaining({
|
||||
facet: "connections:MainMenu.nib",
|
||||
status: "partial",
|
||||
reason: "unsupported_connection_type",
|
||||
examined: 3,
|
||||
omitted: 0,
|
||||
}),
|
||||
);
|
||||
expect(analysis.graph.edges).toContainEqual(
|
||||
expect.objectContaining({
|
||||
relation: "contains",
|
||||
reason: "unsupported_connection_type",
|
||||
evidence: [
|
||||
expect.objectContaining({ description: "connection type other" }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("keyed archive class identity", () => {
|
||||
it("does not classify a custom class containing Connector as a connection", () => {
|
||||
const parsed = parseInterfaceBuilderRecords({
|
||||
$archiver: "NSKeyedArchiver",
|
||||
$objects: [
|
||||
"$null",
|
||||
{
|
||||
$class: { UID: 2 },
|
||||
NSSource: { UID: 1 },
|
||||
NSDestination: { UID: 1 },
|
||||
},
|
||||
{
|
||||
$classname: "TelemetryConnectorState",
|
||||
$classes: ["TelemetryConnectorState", "NSObject"],
|
||||
},
|
||||
],
|
||||
$top: {},
|
||||
});
|
||||
|
||||
expect(parsed.objects).toContainEqual(
|
||||
expect.objectContaining({
|
||||
class_name: "TelemetryConnectorState",
|
||||
id: "1",
|
||||
}),
|
||||
);
|
||||
expect(parsed.connections).toEqual([]);
|
||||
expect(parsed.unresolvedConnections).toBe(0);
|
||||
|
||||
const inheritedConnector = parseInterfaceBuilderRecords({
|
||||
$archiver: "NSKeyedArchiver",
|
||||
$objects: [
|
||||
"$null",
|
||||
{
|
||||
$class: { UID: 3 },
|
||||
NSSource: { UID: 1 },
|
||||
NSDestination: { UID: 1 },
|
||||
},
|
||||
{ $classname: "NSNibConnector", $classes: ["NSNibConnector"] },
|
||||
{
|
||||
$classname: "CustomBindingConnector",
|
||||
$classes: ["CustomBindingConnector", "NSNibConnector", "NSObject"],
|
||||
},
|
||||
],
|
||||
$top: {},
|
||||
});
|
||||
expect(inheritedConnector.connections).toMatchObject([
|
||||
{ kind: "other", source_id: "1", destination_id: "1" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves a malformed class UID without inferring a connector", () => {
|
||||
const parsed = parseInterfaceBuilderRecords({
|
||||
$archiver: "NSKeyedArchiver",
|
||||
$objects: [
|
||||
"$null",
|
||||
{
|
||||
$class: { UID: 99 },
|
||||
NSSource: { UID: 1 },
|
||||
NSDestination: { UID: 1 },
|
||||
},
|
||||
],
|
||||
$top: {},
|
||||
});
|
||||
|
||||
expect(parsed.objects).toContainEqual(
|
||||
expect.objectContaining({
|
||||
class_name: null,
|
||||
id: "1",
|
||||
attributes: expect.objectContaining({ $class: { UID: 99 } }),
|
||||
}),
|
||||
);
|
||||
expect(parsed.connections).toEqual([]);
|
||||
expect(parsed.unresolvedConnections).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("AppKit control connectors", () => {
|
||||
it("projects an AppKit action from the control through the selector to its target", () => {
|
||||
const analysis = buildInterfaceBuilderAnalysis({
|
||||
targetSha256: hash,
|
||||
|
||||
@@ -453,6 +453,9 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
// no connection record, so its coverage is partial with its own reason
|
||||
// rather than absent or truncated.
|
||||
const connectionsUnresolved = parsed.unresolvedConnections > 0;
|
||||
const unsupportedConnections = parsed.connections.some(
|
||||
({ kind }) => kind === "other",
|
||||
);
|
||||
omittedObjects +=
|
||||
parsed.omittedObjects +
|
||||
Math.max(0, parsed.objects.length - documentObjectCount);
|
||||
@@ -500,7 +503,8 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
status:
|
||||
connectionsTruncated ||
|
||||
documentOmittedConnections > 0 ||
|
||||
connectionsUnresolved
|
||||
connectionsUnresolved ||
|
||||
unsupportedConnections
|
||||
? "partial"
|
||||
: "complete",
|
||||
reason:
|
||||
@@ -508,7 +512,9 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
? "connection_limit_reached"
|
||||
: connectionsUnresolved
|
||||
? "connection_unresolved"
|
||||
: null,
|
||||
: unsupportedConnections
|
||||
? "unsupported_connection_type"
|
||||
: null,
|
||||
examined: Math.min(
|
||||
parsed.connections.length,
|
||||
input.limits.max_connections,
|
||||
|
||||
@@ -38,6 +38,13 @@ type InterfaceBuilderConnection = z.infer<typeof connection>;
|
||||
export const isUIKitRuntimeConnectionClass = (className: string): boolean =>
|
||||
/^UIRuntime\w*Connection$/u.test(className);
|
||||
|
||||
/** AppKit NIB connector classes and UIKit runtime connection records. */
|
||||
export const isInterfaceBuilderConnectionClass = (className: string): boolean =>
|
||||
isUIKitRuntimeConnectionClass(className) ||
|
||||
/^NSNib(?:Connector|BindingConnector|ControlConnector|OutletConnector)$/u.test(
|
||||
className,
|
||||
);
|
||||
|
||||
/** The connection class selects the source archive field namespace. */
|
||||
export const interfaceBuilderConnectionFieldNames = (className: string) =>
|
||||
isUIKitRuntimeConnectionClass(className)
|
||||
@@ -167,13 +174,51 @@ const HIERARCHY_KEY =
|
||||
* names the referenced object; the hierarchy walk visits each object once.
|
||||
*/
|
||||
const keyedArchiveTable = (objectTable: readonly unknown[]) => {
|
||||
const classNameOf = (value: unknown): string | null => {
|
||||
const classReference = record(value).$class;
|
||||
const uid = archiveUid(classReference);
|
||||
return firstString(
|
||||
record(uid === undefined ? classReference : objectTable[uid]).$classname,
|
||||
);
|
||||
type ClassFacts = {
|
||||
readonly name: string | null;
|
||||
readonly rawSuperclasses: unknown[];
|
||||
readonly superclasses: readonly string[];
|
||||
};
|
||||
const classFactsByUid = new Map<number, ClassFacts>();
|
||||
const parseClassFacts = (value: unknown): ClassFacts | null => {
|
||||
const descriptor = record(value);
|
||||
const rawSuperclasses = Array.isArray(descriptor.$classes)
|
||||
? descriptor.$classes
|
||||
: [];
|
||||
if (
|
||||
!Object.hasOwn(descriptor, "$classname") &&
|
||||
rawSuperclasses.length === 0
|
||||
)
|
||||
return null;
|
||||
return {
|
||||
name: firstString(descriptor.$classname),
|
||||
rawSuperclasses,
|
||||
superclasses: rawSuperclasses.filter(
|
||||
(name): name is string => typeof name === "string",
|
||||
),
|
||||
};
|
||||
};
|
||||
const classFactsForUid = (uid: number): ClassFacts | null => {
|
||||
const cached = classFactsByUid.get(uid);
|
||||
if (cached !== undefined) return cached;
|
||||
if (!Number.isInteger(uid) || uid < 0 || uid >= objectTable.length)
|
||||
return null;
|
||||
const facts = parseClassFacts(objectTable[uid]);
|
||||
if (facts !== null) classFactsByUid.set(uid, facts);
|
||||
return facts;
|
||||
};
|
||||
const classFactsOfReference = (reference: unknown): ClassFacts | null => {
|
||||
const uid = archiveUid(reference);
|
||||
return uid === undefined
|
||||
? parseClassFacts(reference)
|
||||
: classFactsForUid(uid);
|
||||
};
|
||||
const classFactsOfObject = (value: unknown) =>
|
||||
classFactsOfReference(record(value).$class);
|
||||
const classNameOf = (value: unknown): string | null =>
|
||||
classFactsOfObject(value)?.name ?? null;
|
||||
const hasClassSuperclass = (value: unknown, superclass: string): boolean =>
|
||||
classFactsOfObject(value)?.superclasses.includes(superclass) ?? false;
|
||||
/** The string an archived string object holds, if it is one. */
|
||||
const archivedString = (target: unknown): string | undefined => {
|
||||
if (typeof target === "string") return target;
|
||||
@@ -294,20 +339,23 @@ const keyedArchiveTable = (objectTable: readonly unknown[]) => {
|
||||
return { items: root, omittedReferences };
|
||||
};
|
||||
const archivedArrayClass = (classReference: unknown): boolean => {
|
||||
const uid = archiveUid(classReference);
|
||||
const descriptor = record(
|
||||
uid === undefined ? classReference : objectTable[uid],
|
||||
);
|
||||
const classNames = Array.isArray(descriptor.$classes)
|
||||
? descriptor.$classes.filter(
|
||||
(name): name is string => typeof name === "string",
|
||||
)
|
||||
: [];
|
||||
const superclasses =
|
||||
classFactsOfReference(classReference)?.superclasses ?? [];
|
||||
return (
|
||||
classNames.includes("NSArray") || classNames.includes("NSMutableArray")
|
||||
superclasses.includes("NSArray") ||
|
||||
superclasses.includes("NSMutableArray")
|
||||
);
|
||||
};
|
||||
return { resolve, hierarchy };
|
||||
const classTable = () =>
|
||||
Object.fromEntries(
|
||||
objectTable.flatMap((_, uid) => {
|
||||
const facts = classFactsForUid(uid);
|
||||
return facts === null || facts.name === null
|
||||
? []
|
||||
: [[facts.name, { superclasses: facts.rawSuperclasses }]];
|
||||
}),
|
||||
);
|
||||
return { resolve, hierarchy, hasClassSuperclass, classTable };
|
||||
};
|
||||
|
||||
/** Project class and connection objects from an NSKeyedArchiver object table. */
|
||||
@@ -317,7 +365,12 @@ const parseKeyedArchive = (
|
||||
if (root.$archiver !== "NSKeyedArchiver" || !Array.isArray(root.$objects))
|
||||
return null;
|
||||
const objectTable: readonly unknown[] = root.$objects;
|
||||
const { resolve, hierarchy: toHierarchy } = keyedArchiveTable(objectTable);
|
||||
const {
|
||||
resolve,
|
||||
hierarchy: toHierarchy,
|
||||
hasClassSuperclass,
|
||||
classTable,
|
||||
} = keyedArchiveTable(objectTable);
|
||||
const objects: InterfaceBuilderObject[] = [];
|
||||
const connections: InterfaceBuilderConnection[] = [];
|
||||
let objectCount = 0;
|
||||
@@ -363,9 +416,8 @@ const parseKeyedArchive = (
|
||||
);
|
||||
if (
|
||||
className !== null &&
|
||||
/(?:Outlet|Connection|Segue|ActionConnection|ControlConnector)/iu.test(
|
||||
className,
|
||||
)
|
||||
(isInterfaceBuilderConnectionClass(className) ||
|
||||
hasClassSuperclass(raw, "NSNibConnector"))
|
||||
) {
|
||||
const controlAction = /ControlConnector/iu.test(className);
|
||||
const uiKit = isUIKitRuntimeConnectionClass(className);
|
||||
@@ -417,22 +469,7 @@ const parseKeyedArchive = (
|
||||
}
|
||||
const top = record(root.$top);
|
||||
const hierarchy = toHierarchy(top.root ?? top.UITopLevelObjectsKey);
|
||||
const classes = Object.fromEntries(
|
||||
objectTable.flatMap((raw) => {
|
||||
const item = record(raw);
|
||||
const name = firstString(item.$classname);
|
||||
return name === null
|
||||
? []
|
||||
: [
|
||||
[
|
||||
name,
|
||||
{
|
||||
superclasses: Array.isArray(item.$classes) ? item.$classes : [],
|
||||
},
|
||||
],
|
||||
];
|
||||
}),
|
||||
);
|
||||
const classes = classTable();
|
||||
return {
|
||||
objects,
|
||||
connections,
|
||||
|
||||
@@ -745,6 +745,155 @@ cliTest(
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"compiled CLI reports unsupported keyed archive bindings in connection coverage",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-ib-binding-coverage-");
|
||||
const bundle = join(root, "Example.app");
|
||||
const contents = join(bundle, "Contents");
|
||||
const resources = join(contents, "Resources");
|
||||
await mkdir(resources, { recursive: true });
|
||||
await mkdir(join(contents, "MacOS"), { recursive: true });
|
||||
await writeFile(
|
||||
join(contents, "MacOS", "App"),
|
||||
thinMach(0xfeedfacf, 0x0100000c),
|
||||
);
|
||||
await writeFile(
|
||||
join(contents, "Info.plist"),
|
||||
"<plist><dict><key>CFBundleExecutable</key><string>App</string></dict></plist>",
|
||||
);
|
||||
await writeFile(
|
||||
join(resources, "MainMenu.nib"),
|
||||
buildBinary({
|
||||
$archiver: "NSKeyedArchiver",
|
||||
$version: 100000,
|
||||
$objects: [
|
||||
"$null",
|
||||
{ $class: { UID: 6 } },
|
||||
{ $class: { UID: 6 } },
|
||||
{
|
||||
$class: { UID: 7 },
|
||||
NSSource: { UID: 1 },
|
||||
NSDestination: { UID: 2 },
|
||||
NSLabel: "outlet",
|
||||
},
|
||||
{
|
||||
$class: { UID: 8 },
|
||||
NSSource: { UID: 1 },
|
||||
NSDestination: { UID: 2 },
|
||||
NSLabel: "performAction:",
|
||||
},
|
||||
{
|
||||
$class: { UID: 9 },
|
||||
NSSource: { UID: 1 },
|
||||
NSDestination: { UID: 2 },
|
||||
NSLabel: "value",
|
||||
},
|
||||
{ $classname: "NSView", $classes: ["NSView", "NSObject"] },
|
||||
{
|
||||
$classname: "NSNibOutletConnector",
|
||||
$classes: ["NSNibOutletConnector", "NSNibConnector", "NSObject"],
|
||||
},
|
||||
{
|
||||
$classname: "NSNibControlConnector",
|
||||
$classes: ["NSNibControlConnector", "NSNibConnector", "NSObject"],
|
||||
},
|
||||
{
|
||||
$classname: "NSNibBindingConnector",
|
||||
$classes: ["NSNibBindingConnector", "NSNibConnector", "NSObject"],
|
||||
},
|
||||
],
|
||||
$top: { root: { UID: 1 } },
|
||||
}),
|
||||
);
|
||||
|
||||
const output = await cli.run({
|
||||
arguments: ["decode-interface-builder", bundle, "--json"],
|
||||
environment: { REA_LOG_LEVEL: "silent", REA_ANALYSIS_PROVIDER: "auto" },
|
||||
});
|
||||
expect(output.exitCode, output.stderr).toBe(0);
|
||||
const result = interfaceBuilderAnalysisSchema.parse(
|
||||
parseEvidence(output.json).normalized_result,
|
||||
);
|
||||
expect(result.documents[0]?.connection_count).toBe(3);
|
||||
expect(result.graph.coverage).toContainEqual(
|
||||
expect.objectContaining({
|
||||
facet: "connections:Contents/Resources/MainMenu.nib",
|
||||
status: "partial",
|
||||
reason: "unsupported_connection_type",
|
||||
examined: 3,
|
||||
omitted: 0,
|
||||
}),
|
||||
);
|
||||
expect(result.graph.edges).toContainEqual(
|
||||
expect.objectContaining({
|
||||
relation: "contains",
|
||||
reason: "unsupported_connection_type",
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("projects an NIBArchive binding as an unsupported partial connection", async () => {
|
||||
const root = await createTestTempDirectory("rea-ib-nibarchive-binding-");
|
||||
const bundle = join(root, "Example.app");
|
||||
const resources = join(bundle, "Contents", "Resources");
|
||||
await mkdir(resources, { recursive: true });
|
||||
await writeFile(
|
||||
join(resources, "Main.nib"),
|
||||
encodeNibArchiveFixture({
|
||||
classes: [
|
||||
"NSView",
|
||||
"NSNibBindingConnector",
|
||||
"NSString",
|
||||
"TelemetryConnectorState",
|
||||
],
|
||||
objects: [
|
||||
{ classIndex: 0, values: {} },
|
||||
{ classIndex: 0, values: {} },
|
||||
{
|
||||
classIndex: 1,
|
||||
values: {
|
||||
NSSource: { ref: 0 },
|
||||
NSDestination: { ref: 1 },
|
||||
NSLabel: { ref: 3 },
|
||||
},
|
||||
},
|
||||
{ classIndex: 2, values: { "NS.bytes": "value" } },
|
||||
{
|
||||
classIndex: 3,
|
||||
values: { NSSource: { ref: 0 }, NSDestination: { ref: 1 } },
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await analyzeInterfaceBuilderBundle({
|
||||
bundlePath: bundle,
|
||||
targetSha256: "e".repeat(64),
|
||||
});
|
||||
|
||||
expect(result.documents[0]?.connection_count).toBe(1);
|
||||
expect(result.graph.nodes).toContainEqual(
|
||||
expect.objectContaining({ name: "TelemetryConnectorState" }),
|
||||
);
|
||||
expect(result.graph.coverage).toContainEqual(
|
||||
expect.objectContaining({
|
||||
facet: "connections:Contents/Resources/Main.nib",
|
||||
status: "partial",
|
||||
reason: "unsupported_connection_type",
|
||||
examined: 1,
|
||||
omitted: 0,
|
||||
}),
|
||||
);
|
||||
expect(result.graph.edges).toContainEqual(
|
||||
expect.objectContaining({
|
||||
relation: "contains",
|
||||
reason: "unsupported_connection_type",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
cliTest(
|
||||
"compiled CLI preserves a good archive and reports repeated key and data omissions",
|
||||
async ({ cli }) => {
|
||||
|
||||
Reference in new issue
Block a user