fix(hopper): preserve regex inventory facts

This commit is contained in:
morluto committed 2026-10-27 10:54:59 +08:00
1 parent 82d3ab7aa8
commit 1fdd3df7b4
2 files changed
+110 -4

No files matched your search

+101
View File
@@ -8,6 +8,10 @@ import {
HopperProcessError,
} from "../domain/hopperErrors.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { parseConfig } from "../config/parseConfig.js";
import { silentLogger } from "../logger.js";
import { HopperClient } from "./HopperClient.js";
import { HopperProvider } from "./HopperProvider.js";
const regexMatch = vi.hoisted(() => ({
afterSettle: undefined as (() => void) | undefined,
@@ -27,6 +31,103 @@ vi.mock("../process/RegexMatcher.js", async (importOriginal) => {
};
});
it.each(["search_strings", "search_procedures"] as const)(
"preserves selected source metadata through the provider's %s regex boundary",
async (operation) => {
const config = parseConfig({});
if (!config.ok) throw config.error;
const metadata =
operation === "search_strings"
? {
provider_value: "short display",
string: {
encoding: "utf-8",
encoding_status: "observed",
termination: "present_or_not_required",
byte_length: 7,
},
}
: {
procedure: { external: false, thunk: true, thunk_target: "0x2" },
};
const selected = {
address: "0x1",
value: "matched",
...metadata,
provider_metadata: { empty: "", absent: null, zero: 0, flag: false },
};
const source = [
{ address: "0x1", value: "different" },
selected,
{ ...selected, value: "matched alternative" },
];
const callTool = vi
.spyOn(HopperClient.prototype, "callTool")
.mockResolvedValue(ok(source));
const provider = new HopperProvider(
config.value,
silentLogger,
{},
"linux",
);
const client = provider.createClient({
path: "/tmp/fixture.hop",
sha256: "a".repeat(64),
kind: "database",
format: "analysis-database",
});
try {
const result = await client.execute(operation, {
pattern: "^matched$",
mode: "regex",
case_sensitive: true,
});
expect(result.ok).toBe(true);
if (result.ok) expect(result.value.result).toEqual([selected]);
expect(callTool).toHaveBeenCalledOnce();
expect(callTool.mock.calls[0]?.[0]).toBe(
operation === "search_strings" ? "list_strings" : "list_procedures",
);
expect(source[1]).toEqual(selected);
} finally {
await client.close();
callTool.mockRestore();
}
},
);
it.each(["search_strings", "search_procedures"] as const)(
"rejects malformed canonical metadata in the %s inventory",
async (operation) => {
const search = new HopperRegexSearch({
callTool: async () =>
ok([
{
address: "0x1",
value: "matched",
...(operation === "search_strings"
? { string: { encoding: false } }
: { procedure: { external: "unknown" } }),
},
]),
});
try {
expect(
await search.execute(operation, {
pattern: "matched",
mode: "regex",
case_sensitive: true,
}),
).toMatchObject({
ok: false,
error: { _tag: "AnalysisOutputError", operation },
});
} finally {
await search.close();
}
},
);
it.each(
(["search_strings", "search_procedures"] as const).flatMap((operation) =>
(["remote", "capability", "timeout", "process"] as const).map((family) => ({
+9 -4
View File
@@ -6,7 +6,7 @@ import {
AnalysisCancelledError,
AnalysisOutputError,
} from "../domain/analysisErrorCore.js";
import { analysisStringSchema } from "../domain/native/nativeString.js";
import { nativeInventoryRowsSchemas } from "../domain/native/nativeInventory.js";
import { HopperRemoteError } from "../domain/hopperErrors.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { err, ok, type Result } from "../domain/result.js";
@@ -111,8 +111,13 @@ export class HopperRegexSearch {
return err(
analysisClientFailureForOperation(inventory.error, "hopper", operation),
);
const parsed = z.array(analysisStringSchema).safeParse(inventory.value);
if (!parsed.success)
const rows = inventory.value;
const schema =
operation === "search_strings"
? nativeInventoryRowsSchemas.strings
: nativeInventoryRowsSchemas.names;
const parsed = schema.safeParse(rows);
if (!parsed.success || !Array.isArray(rows))
return err(
new AnalysisOutputError(
operation,
@@ -150,6 +155,6 @@ export class HopperRegexSearch {
new AnalysisCancelledError(operation, { cause: options.signal.reason }),
);
const selected = new Set(matched.value);
return ok(parsed.data.filter((_, index) => selected.has(index)));
return ok(rows.filter((_, index) => selected.has(index)));
}
}