fix(snapshot): count newly retained workflow bindings on import (#4428)

AnalysisSnapshotCache.stage counted new direct entries but never
incremented for new workflow entries, so a snapshot carrying only a
trace_feature binding reported 0 new entries while retaining it.

Count a workflow binding when its query ID is new, matching direct
entries; replacements and capacity-refused bindings stay uncounted.

Fixes #4377
This commit is contained in:
Jiwon | 지원 authored and GitHub committed 2026-10-22 03:51:32 +08:00
1 parent cc4ed5b904
commit 3220e17b8b
2 files changed
+75 -9

No files matched your search

@@ -118,6 +118,68 @@ it("exports independent nested query, workflow and Evidence payloads", () => {
expect(serializeAnalysisSnapshot(fresh.value)).toBe(expected);
});
it("counts newly retained direct and workflow bindings when staging", () => {
const source = new AnalysisSnapshotCache();
const target = ANALYSIS_SNAPSHOT_TARGET;
const profile = ANALYSIS_SNAPSHOT_PROFILE;
const parameters = { address: "0x1000" };
const workflowProfile = workflowAnalysisProfile(profile, "trace_feature");
source.record({
target,
profile,
operation: "procedure_info",
parameters,
execution: createAnalysisExecution("direct", ANALYSIS_SNAPSHOT_PROVIDER, {
subject: target,
analysisProfile: profile,
}),
});
source.recordWorkflow({
target,
profile,
operation: "trace_feature",
parameters: { query: "feature" },
execution: {
...createAnalysisExecution("workflow", REA_WORKFLOW_PROVIDER, {
subject: target,
}),
analysisProfile: workflowProfile,
},
});
const bundle = createEvidenceBundle([
createEvidence(target, ANALYSIS_SNAPSHOT_PROVIDER, {
operation: "procedure_info",
parameters,
result: "direct",
analysisProfile: profile,
}),
createEvidence(target, REA_WORKFLOW_PROVIDER, {
operation: "trace_feature",
confidence: "derived",
parameters: { query: "feature" },
result: "workflow",
analysisProfile: workflowProfile,
}),
]);
const exported = source.export(target, profile, bundle);
if (!exported.ok) throw exported.error;
const snapshot = parseAnalysisSnapshot(
JSON.parse(serializeAnalysisSnapshot(exported.value)),
);
expect(snapshot.entries).toHaveLength(1);
expect(snapshot.workflow_entries).toHaveLength(1);
const workflowOnly = new AnalysisSnapshotCache();
expect(workflowOnly.stage({ ...snapshot, entries: [] })).toBe(1);
expect(workflowOnly.workflowEntries()).toHaveLength(1);
const staged = new AnalysisSnapshotCache();
expect(staged.stage(snapshot)).toBe(2);
expect(staged.entries()).toHaveLength(1);
expect(staged.workflowEntries()).toHaveLength(1);
expect(staged.stage(snapshot), "replacements are not new entries").toBe(0);
});
it("rebinds replayed target subjects while preserving child artifact subjects", () => {
const cache = new AnalysisSnapshotCache();
const target = ANALYSIS_SNAPSHOT_TARGET;
@@ -333,15 +395,18 @@ describe("analysis snapshot cache capacity", () => {
limitations: execution.limitations,
locations: execution.locations,
});
cache.stage(
parseAnalysisSnapshot({
target: snapshotTarget(target),
binding: snapshotBinding(profile),
entries: [],
workflow_entries: [incoming],
evidence_bundle: createEvidenceBundle([evidence]),
}),
);
expect(
cache.stage(
parseAnalysisSnapshot({
target: snapshotTarget(target),
binding: snapshotBinding(profile),
entries: [],
workflow_entries: [incoming],
evidence_bundle: createEvidenceBundle([evidence]),
}),
),
"a capacity-refused workflow binding is not counted",
).toBe(0);
expect(cache.workflowEntries()).toHaveLength(
SNAPSHOT_CACHE_ENTRY_CEILING - 1,
);
@@ -142,6 +142,7 @@ export class AnalysisSnapshotCache {
for (const entry of snapshot.workflow_entries) {
if (!this.#workflowEntries.has(entry.query_id) && !this.#hasCapacity())
continue;
if (!this.#workflowEntries.has(entry.query_id)) imported += 1;
this.#workflowEntries.set(entry.query_id, entry);
}
return imported;