refactor(snapshot): centralize resource cache eligibility

This commit is contained in:
morluto committed 2026-10-27 08:10:48 +08:00
1 parent 5d25aa4f52
commit 5b8e415483
4 files changed
+81 -15

No files matched your search

@@ -716,6 +716,26 @@ describe("analysis snapshot cache policy", () => {
effects: { changesPermissions: true },
cacheable: false,
},
{
policy: "snapshot",
operation: "decode_interface_builder",
cacheable: false,
},
{
policy: "snapshot",
operation: "inspect_asset_catalog",
cacheable: false,
},
{
policy: "snapshot",
operation: "inspect_keyed_archive",
cacheable: false,
},
{
policy: "snapshot",
operation: "trace_dylib_resolution",
cacheable: false,
},
{ policy: "snapshot", operation: "list_documents", cacheable: false },
{ policy: "snapshot", operation: "list_strings", cacheable: false },
{
@@ -51,6 +51,15 @@ const RELATIONSHIP_OPERATIONS: ReadonlySet<AnalysisOperation> = new Set([
"procedure_callees",
]);
// These reports depend on selected bundle resources or loader context beyond
// the immutable executable identity carried by a binary analysis snapshot.
const RESOURCE_SCOPED_OPERATIONS: ReadonlySet<AnalysisOperation> = new Set([
"decode_interface_builder",
"inspect_asset_catalog",
"inspect_keyed_archive",
"trace_dylib_resolution",
]);
const STATEFUL_OPERATIONS: ReadonlySet<AnalysisOperation> = new Set([
"health",
"current_address",
@@ -85,6 +94,7 @@ export const isSnapshotCacheable = (
parameters: Readonly<Record<string, JsonValue>>,
): descriptor is CapabilityDescriptor =>
!STATEFUL_OPERATIONS.has(operation) &&
!RESOURCE_SCOPED_OPERATIONS.has(operation) &&
(!DOCUMENT_SCOPED_OPERATIONS.has(operation) ||
typeof parameters.document === "string") &&
(!CURSOR_DEFAULT_OPERATIONS.has(operation) ||
+2 -15
View File
@@ -1224,12 +1224,7 @@ export class BinarySession
unboundOperationError: (operation, route) =>
this.#providerRouter.unboundOperationError(operation, route),
lookupSnapshot: (target, profile, operation, parameters) =>
operation === "decode_interface_builder" ||
operation === "inspect_asset_catalog" ||
operation === "inspect_keyed_archive" ||
operation === "trace_dylib_resolution"
? undefined
: this.lookupSnapshot(target, profile, operation, parameters),
this.lookupSnapshot(target, profile, operation, parameters),
});
if (!prepared.ok) return prepared;
const { active, capability, profile, cacheable, cached } = prepared.value;
@@ -1275,15 +1270,7 @@ export class BinarySession
);
if (!sourcesRecorded.ok) return sourcesRecorded;
}
if (
execution.ok &&
cacheable &&
profile !== undefined &&
name !== "decode_interface_builder" &&
name !== "inspect_asset_catalog" &&
name !== "inspect_keyed_archive" &&
name !== "trace_dylib_resolution"
) {
if (execution.ok && cacheable && profile !== undefined) {
const evidence = createAnalysisExecutionEvidence(
name,
arguments_,
@@ -968,6 +968,55 @@ describe("snapshot switch rollback", () => {
});
describe("snapshot cache eligibility", () => {
it.each([
"decode_interface_builder",
"inspect_asset_catalog",
"inspect_keyed_archive",
"trace_dylib_resolution",
] as const)(
"reobserves resource-scoped %s without executable snapshot Evidence",
async (operation) => {
const [target] = await createBinarySessionTargets();
const provider = createCacheProvider([]);
const [baseCapability] = provider.capabilities();
if (baseCapability === undefined)
throw new Error("Expected fixture capability");
provider.capabilities = () => [
{ ...baseCapability, operation, cachePolicy: "snapshot" },
];
let observations = 0;
provider.createClient = () => ({
execute: async (name) =>
resultOk(
createAnalysisExecution(
name === "health" ? null : ++observations,
provider.identity(),
),
),
close: async () => resultOk(null),
});
const session = createTestBinarySession(provider);
try {
expect((await session.open(target)).ok).toBe(true);
for (const observation of [1, 2]) {
const result = await session.execute(operation, {});
expect(result).toMatchObject({
ok: true,
value: { result: observation },
});
if (result.ok) expect(result.value.evidenceId).toBeUndefined();
}
expect(session.exportEvidenceBundle().records).toEqual([]);
expect(session.exportAnalysisSnapshot()).toMatchObject({
ok: true,
value: { entries: [] },
});
} finally {
await session.close();
}
},
);
it.each([false, true])(
"retains source Evidence independently of snapshot eligibility (live=%s)",
async (live) => {