diff --git a/docs/testing.md b/docs/testing.md index 1685b361d5..76fa648b26 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -369,7 +369,17 @@ subsequent wire recovery and clean shutdown. Caller timeout does not interrupt Hopper's synchronous native operation. Socket boundary tests deterministically cover active and queued expiry, late replies and timer cleanup. -The Linux demo lane remains a separate `verify:hopper:linux` command. Its +The Linux demo lane remains a separate `verify:hopper:linux` command. Hopper +6.4.2's ELF loader did not recognize the original 4,119-byte literal or three +non-ASCII byte strings in the measured fixtures. The lane retains complete raw +byte checks for those literals through source-owned symbols and reports absent +recognition with decoded-object coverage explicitly unverified. Recognized +objects must still match their source bytes, encoding, filters and searches. +A separate 548-byte ASCII literal requires native fragmentation, shortened +provider display, complete decoded fragments and dossier provenance on Linux; +the macOS lane retains the original full-length recognition requirement. These +are observed fixture/build capabilities, not universal Hopper length limits. +Its lifecycle checks attempt competing CLI launches for the active target and a different target, requiring rejection before either can change the owning MCP session's documents or procedures. Lease boundary tests also preserve diff --git a/scripts/lib/real-hopper-search.mjs b/scripts/lib/real-hopper-search.mjs index 0d796f3d2d..5e37564e0e 100644 --- a/scripts/lib/real-hopper-search.mjs +++ b/scripts/lib/real-hopper-search.mjs @@ -1,13 +1,16 @@ import assert from "node:assert/strict"; import { mcpTextValue } from "./mcp-verifier-results.mjs"; +const fixtureSymbolPrefix = process.platform === "darwin" ? "_" : ""; + /** Exercise literal/regex search, Unicode identities and cache invalidation in Hopper. */ export async function verifyHopperSearch(call, invalid, procedure) { const address = procedure.address; const original = await call("address_name", { address }); + let stringCoverage; try { await verifySymbolNames(call, invalid, address); - await verifySearchModes(call, address); + stringCoverage = await verifySearchModes(call, address); } finally { await call("set_address_name", { address, name: original }); } @@ -26,7 +29,7 @@ export async function verifyHopperSearch(call, invalid, procedure) { symbolUtf16LimitValidatedBeforeMutation: true, regexAndCaseModes: true, annotationCacheInvalidation: true, - escapedUtf8AndByteStrings: true, + stringCoverage, }; } @@ -103,9 +106,23 @@ async function verifySearchModes(call, address) { [{ address, value: "REA_Search_Upper" }], ); const strings = await call("list_strings"); - const long = "REA_LONG_LITERAL_" + "x".repeat(4096) + "needle"; - const longItem = strings.find((item) => - item.value.startsWith("REA_LONG_LITERAL_"), + const linux = process.platform === "linux"; + const fullLiteral = "REA_LONG_LITERAL_" + "x".repeat(4096) + "needle"; + // Recognition is a provider/loader capability, not an adapter guarantee. + // Always retain the original size in raw-byte coverage; Linux additionally + // requires an extent observed to produce native fragmented string objects. + const fullBytes = Buffer.from(fullLiteral + "\0"); + const raw = await call("read_bytes", { + address: `${fixtureSymbolPrefix}rea_long_literal`, + length: fullBytes.length, + }); + assert.equal(raw.complete, true); + assert.equal(raw.bytes_hex, fullBytes.toString("hex")); + const long = linux + ? "REA_FRAGMENTED_LITERAL_" + "x".repeat(512) + "fragment_tail" + : fullLiteral; + const longItem = strings.find( + (item) => long.startsWith(item.value) && item.value.startsWith("REA_"), ); assert.ok(longItem, "fixture omitted its real long literal"); assert.ok(long.startsWith(longItem.value)); @@ -158,7 +175,7 @@ async function verifySearchModes(call, address) { } assert.deepEqual( await call("search_strings", { - pattern: "needle", + pattern: linux ? "fragment_tail" : "needle", mode: "literal", case_sensitive: true, }), @@ -176,31 +193,94 @@ async function verifySearchModes(call, address) { }), expected, ); - await verifyHopperStringObjects(call, [ - { value: "REA_UTF8_é_😀", encoding: "utf-8" }, + const objects = await verifyHopperStringObjects(call, [ + { + value: "REA_UTF8_é_😀", + encoding: "utf-8", + symbol: `${fixtureSymbolPrefix}rea_utf8_literal`, + recognitionOptional: linux, + }, { value: 'REA_ESCAPED_"\\line\nend\t\r', encoding: "utf-8" }, { value: "REA_LITERAL_BACKSLASH_\\n", encoding: "utf-8" }, - { value: "REA_LITERAL_…", encoding: "utf-8" }, - { value: "REA_LATIN1_ÿ", encoding: "latin-1" }, + { + value: "REA_LITERAL_…", + encoding: "utf-8", + symbol: `${fixtureSymbolPrefix}rea_ellipsis_literal`, + recognitionOptional: linux, + }, + { + value: "REA_LATIN1_ÿ", + encoding: "latin-1", + symbol: `${fixtureSymbolPrefix}rea_latin1_literal`, + recognitionOptional: linux, + }, ]); + return { + fullLiteralSourceBytes: fullBytes.length, + fragmentedLiteralSourceBytes: Buffer.byteLength(long), + fullLiteralRecognition: strings.some((item) => + item.value.startsWith("REA_LONG_LITERAL_"), + ) + ? "observed" + : "not_observed", + ...objects, + }; } /** Compare native string identities and values with the source fixture's bytes. */ export async function verifyHopperStringObjects(call, expectations) { const strings = await call("list_strings"); - for (const { value, encoding } of expectations) { - const item = strings.find((candidate) => candidate.value === value); - assert.ok( - item, - `Hopper omitted the decoded string ${JSON.stringify(value)}`, - ); - assert.equal(item.string.encoding, encoding); - assert.equal(item.string.encoding_status, "inferred"); - assert.equal(item.string.termination, "present_or_not_required"); + const recognition = []; + for (const { + value, + encoding, + symbol, + recognitionOptional = false, + } of expectations) { const expectedBytes = Buffer.from( value + "\0", encoding.replaceAll("-", ""), ); + let sourceAddress; + if (symbol !== undefined) { + const raw = await call("read_bytes", { + address: symbol, + length: expectedBytes.length, + }); + sourceAddress = raw.address; + assert.equal(raw.complete, true); + assert.equal(raw.bytes_hex, expectedBytes.toString("hex")); + } + const item = strings.find((candidate) => + sourceAddress === undefined + ? candidate.value === value + : candidate.address === sourceAddress, + ); + if (item === undefined && recognitionOptional) { + assert.ok( + symbol, + "optional recognition requires independent source-byte coverage", + ); + recognition.push({ + value, + state: "not_observed", + decodedObject: "unverified", + }); + continue; + } + assert.ok( + item, + `Hopper omitted the decoded string ${JSON.stringify(value)}`, + ); + assert.equal( + item.value, + value, + "native string decoding changed the source value", + ); + recognition.push({ value, state: "observed", decodedObject: "verified" }); + assert.equal(item.string.encoding, encoding); + assert.equal(item.string.encoding_status, "inferred"); + assert.equal(item.string.termination, "present_or_not_required"); assert.equal(item.string.byte_length, expectedBytes.length); const bytes = await call("read_bytes", { address: item.address, @@ -225,7 +305,7 @@ export async function verifyHopperStringObjects(call, expectations) { ); return { sourceBytesVerified: true, - inferredEncodingReported: true, + recognition, displayEscapesExcludedFromMatching: true, }; } diff --git a/tests/conformance/c/fixture.c b/tests/conformance/c/fixture.c index 6708ac9dd0..e1dff9a9cc 100644 --- a/tests/conformance/c/fixture.c +++ b/tests/conformance/c/fixture.c @@ -5,13 +5,24 @@ volatile int rea_c_global = 7; #define REA_LONG_CHUNK "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" #define REA_LONG_256 REA_LONG_CHUNK REA_LONG_CHUNK REA_LONG_CHUNK REA_LONG_CHUNK #define REA_LONG_1024 REA_LONG_256 REA_LONG_256 REA_LONG_256 REA_LONG_256 +/* Keep the full-size literal even when a loader does not type it as a string. */ +__attribute__((used)) const char rea_long_literal[] = + "REA_LONG_LITERAL_" REA_LONG_1024 REA_LONG_1024 REA_LONG_1024 REA_LONG_1024 "needle"; +/* Linux Hopper 6.4.2 recognizes this extent and splits it into native objects. */ +__attribute__((used)) const char rea_fragmented_literal[] = + "REA_FRAGMENTED_LITERAL_" REA_LONG_256 REA_LONG_256 "fragment_tail"; +__attribute__((used)) const char rea_utf8_literal[] = "REA_UTF8_é_😀"; +__attribute__((used)) const char rea_ellipsis_literal[] = "REA_LITERAL_…"; +__attribute__((used)) const char rea_latin1_literal[] = "REA_LATIN1_\xff"; + __attribute__((noinline, used)) void rea_long_literal_procedure(void) { - puts("REA_LONG_LITERAL_" REA_LONG_1024 REA_LONG_1024 REA_LONG_1024 REA_LONG_1024 "needle"); - puts("REA_UTF8_é_😀"); + puts(rea_long_literal); + puts(rea_fragmented_literal); + puts(rea_utf8_literal); puts("REA_ESCAPED_\"\\line\nend\t\r"); puts("REA_LITERAL_BACKSLASH_\\n"); - puts("REA_LITERAL_…"); - puts("REA_LATIN1_\xff"); + puts(rea_ellipsis_literal); + puts(rea_latin1_literal); } __attribute__((noinline, used)) int rea_leaf(int value) {