test(contracts): align plist and UI finalization regressions

Update public-boundary expectations after the explicit XML unknown-scalar
and native UI deadline provenance changes. Retain raw malformed literals,
independent exact integers, expanded-year ISO dates, and partial UI reports.
Assert successful cleanup releases its lease and failed cleanup retains it
until close retries. Keep malformed-input and true date-range coverage.

The shared number fixture owns CLI/filesystem/MCP expectations. Native
plutil controls confirm integer entities are invalid plist literals, even
though they are valid XML. No runtime behavior or CI gates change.

Validation: 187 source cases, 138 focused public cases, final 97-case
rerun after fixture typing/group changes, full static/architecture checks,
and test discovery (1591 files).
This commit is contained in:
morluto committed 2026-10-19 06:22:32 +08:00
1 parent 32f0e51209
commit d1adc60795
4 files changed
+97 -44

No files matched your search

@@ -1013,12 +1013,12 @@ describe("bounded Interface Builder archive decoding", () => {
);
});
it("decodes an archive whose date the decoder cannot represent", async () => {
it("decodes an archive with a date year beyond four digits", async () => {
const root = await createTestTempDirectory("rea-ib-test-");
const bundle = join(root, "Example.app");
const resources = join(bundle, "Contents", "Resources");
await mkdir(resources, { recursive: true });
// plutil writes years past 9999 unsigned, which the decoder rejects.
// XML property-list dates use an extended ISO year in the public schema.
await writeFile(
join(resources, "Far.nib"),
'<?xml version="1.0"?><plist version="1.0"><dict><key>$archiver</key><string>NSKeyedArchiver</string><key>$objects</key><array><string>$null</string><date>11507-08-17T05:20:00Z</date></array><key>$top</key><dict/></dict></plist>',
@@ -1030,7 +1030,10 @@ describe("bounded Interface Builder archive decoding", () => {
});
expect(result.documents).toHaveLength(1);
expect(result.limitations).toContainEqual(
expect(result.graph.coverage).toContainEqual(
expect.objectContaining({ facet: "archive_decode", status: "complete" }),
);
expect(result.limitations).not.toContainEqual(
expect.stringMatching(
/^Contents\/Resources\/Far\.nib: 1 date value\(s\)/u,
),
@@ -1039,7 +1042,9 @@ describe("bounded Interface Builder archive decoding", () => {
expect.stringContaining("could not be decoded"),
);
});
});
describe("Xcode-compiled Interface Builder archives", () => {
it.skipIf(!xcodeIbtoolAvailable)(
"decodes an Xcode-compiled storyboard NIB and recovers its UI routes",
async () => {
@@ -10,11 +10,11 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
cliTest(
"built CLI reports a date the decoder cannot represent as unknown",
"built CLI preserves dates with years beyond four digits",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-keyed-archive-date-");
const path = join(root, "archive.plist");
// plutil writes years past 9999 unsigned, which the decoder rejects.
// XML property-list dates use an extended ISO year in the public schema.
const xml = build({
$archiver: "NSKeyedArchiver",
$version: 100000,
@@ -37,15 +37,19 @@ cliTest(
result.objects.map(({ id, kind, value }) => ({ id, kind, value })),
).toEqual([
{ id: 0, kind: "nil", value: "$null" },
{ id: 1, kind: "scalar", value: { $plist_type: "date", iso8601: null } },
{
id: 1,
kind: "scalar",
value: { $plist_type: "date", iso8601: "+011507-08-17T05:20:00.000Z" },
},
{
id: 2,
kind: "scalar",
value: { $plist_type: "date", iso8601: "2020-01-01T00:00:00.000Z" },
},
]);
expect(result.limitations).toContainEqual(
expect.stringContaining("1 date value(s)"),
expect(result.limitations).not.toContainEqual(
expect.stringContaining("date value(s)"),
);
},
);
@@ -76,13 +76,15 @@ it.each([false, true])(
});
expect(parseMcpToolError(result)).toMatchObject({
error: {
code: fails ? "cleanup_incomplete" : "cancelled",
code: fails ? "cleanup_incomplete" : "provider_timeout",
details: {
cleanup: fails ? "incomplete" : "complete",
...(fails
? {
cleanup: "incomplete",
execution_failure: "execution_failure",
diagnostics: { primary_error: { code: "cancelled" } },
diagnostics: {
primary_error: { code: "provider_timeout" },
},
resources: ["controlled-ui-runtime"],
}
: {}),
@@ -94,6 +96,7 @@ it.each([false, true])(
},
},
});
expect(cleanupLease.released).toBe(!fails);
allowClose = true;
const closed = await client.callTool({
name: "close_binary",
+74 -33
View File
@@ -11,8 +11,17 @@ import { createTestTempDirectory } from "./temporaryDirectory.js";
const EXACT_INTEGER = { $plist_type: "integer", decimal: "9007199254740993" };
const ROUNDED = 9007199254740992;
const unknownInteger = (
literal: string,
reason: "invalid-literal" | "unsupported-markup",
) => ({
$plist_type: "unknown",
element: "integer",
literal,
reason,
});
/** Accepted decoder representations whose supplemental metadata needs care. */
/** Boundary cases that retain exact integers or explicit unknown literals. */
export const mixedArchiveNumberElements = [
"<integer>9007199254740992<!-- split -->3</integer>",
"<integer>9007199254740992<![CDATA[3]]></integer>",
@@ -23,8 +32,19 @@ export const mixedArchiveNumberElements = [
"<integer>9007199254740992x</integer>",
] as const;
interface MetadataCase {
readonly name: string;
readonly source: JsonValue;
readonly element: string;
readonly expected: JsonValue;
readonly knownIntegerCount?: number;
readonly unknownScalarCount?: number;
readonly malformedReference: boolean;
readonly integerElement?: string;
}
/** Ordinary dictionaries and malformed numeric references at the same boundary. */
export const archiveNumberMetadataCases = [
export const archiveNumberMetadataCases: readonly MetadataCase[] = [
...[
{
element: "<real>9007199254740992<![CDATA[0]]></real>",
@@ -39,15 +59,23 @@ export const archiveNumberMetadataCases = [
source: { first: "__INTEGER__", other: "__OTHER__" },
element,
expected: { first: EXACT_INTEGER, other: value },
incomplete: false,
malformedReference: false,
})),
...mixedArchiveNumberElements.map((element) => ({
name: element,
source: { first: "__INTEGER__", other: "__OTHER__" },
element,
expected: { first: ROUNDED, other: ROUNDED },
incomplete: true,
expected: {
first: EXACT_INTEGER,
other: unknownInteger(
element.slice("<integer>".length, -"</integer>".length),
element.endsWith("x</integer>")
? "invalid-literal"
: "unsupported-markup",
),
},
knownIntegerCount: 1,
unknownScalarCount: 1,
malformedReference: false,
})),
...(["UID", "CF$UID"] as const).flatMap((key) => [
@@ -56,7 +84,6 @@ export const archiveNumberMetadataCases = [
source: { [key]: { amount: "__INTEGER__" } },
element: "",
expected: { [key]: { amount: EXACT_INTEGER } },
incomplete: false,
malformedReference: false,
},
{
@@ -64,7 +91,6 @@ export const archiveNumberMetadataCases = [
source: { [key]: ["__INTEGER__"] },
element: "",
expected: { [key]: [EXACT_INTEGER] },
incomplete: false,
malformedReference: false,
},
{
@@ -72,30 +98,23 @@ export const archiveNumberMetadataCases = [
source: { [key]: "__INTEGER__" },
element: "",
expected: { [key]: ROUNDED },
incomplete: false,
malformedReference: true,
},
]),
{
name: "entity-encoded integer text",
// Native plist integer grammar reads raw text, unlike real elements.
name: "entity-encoded integer rejected by native plist grammar",
source: { amount: "__INTEGER__" },
element: "",
expected: { amount: EXACT_INTEGER },
incomplete: false,
expected: {
amount: unknownInteger("&#57;007199254740993", "invalid-literal"),
},
knownIntegerCount: 0,
unknownScalarCount: 1,
malformedReference: false,
integerElement: "<integer>&#57;007199254740993</integer>",
},
] satisfies readonly {
name: string;
source: JsonValue;
element: string;
expected: JsonValue;
incomplete: boolean;
malformedReference: boolean;
integerElement?: string;
}[];
type MetadataCase = (typeof archiveNumberMetadataCases)[number];
];
/** Write one inert archive and retain the original byte digest for assertions. */
export const keyedArchiveNumberMetadataFixture = async (item: MetadataCase) => {
@@ -108,9 +127,7 @@ export const keyedArchiveNumberMetadataFixture = async (item: MetadataCase) => {
})
.replace(
"<string>__INTEGER__</string>",
"integerElement" in item
? item.integerElement
: "<integer>9007199254740993</integer>",
item.integerElement ?? "<integer>9007199254740993</integer>",
)
.replace("<string>__OTHER__</string>", item.element);
const bytes = Buffer.from(xml);
@@ -145,14 +162,26 @@ export const expectKeyedArchiveNumberMetadata = (
status: "resolved",
raw: { CF$UID: 1 },
});
if (item.incomplete) {
if (item.unknownScalarCount !== undefined) {
expect(result.limitations).toContainEqual(
expect.stringMatching(
new RegExp(
`^${String(item.unknownScalarCount)} XML scalar value\\(s\\) could not be established`,
"u",
),
),
);
expect(result.limitations).not.toContainEqual(
expect.stringMatching(/metadata.*completely observed.*precision/u),
);
expect(
result.limitations.some((note) => note.includes("unambiguous integer")),
).toBe(false);
} else if (item.malformedReference) {
} else {
expect(result.limitations).not.toContainEqual(
expect.stringMatching(
/^\d+ XML scalar value\(s\) could not be established/u,
),
);
}
if (item.malformedReference) {
expect(result.references).toContainEqual({
source: 1,
path: [],
@@ -162,8 +191,20 @@ export const expectKeyedArchiveNumberMetadata = (
});
} else {
expect(result.references).toHaveLength(1);
expect(result.limitations).toContainEqual(
expect.stringMatching(/^1 unambiguous integer value/u),
);
const knownIntegerCount = item.knownIntegerCount ?? 1;
if (knownIntegerCount === 0) {
expect(result.limitations).not.toContainEqual(
expect.stringMatching(/^\d+ unambiguous integer value/u),
);
} else {
expect(result.limitations).toContainEqual(
expect.stringMatching(
new RegExp(
`^${String(knownIntegerCount)} unambiguous integer value`,
"u",
),
),
);
}
}
};