fix(verification): distinguish Hopper string recognition from byte fidelity

This commit is contained in:
Luis Taffelli committed 2026-10-11 18:31:29 -03:00
1 parent b9d9a474cf
commit 8ea0432ffc
3 files changed
+126 -25

No files matched your search

+11 -1
View File
@@ -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
+100 -20
View File
@@ -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,
};
}
+15 -4
View File
@@ -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) {