CompositeToolSourceStore.load_index: merge entries_by_version

The merge loop copied only entries and by_section, leaving the merged
index's per-version map empty — ToolIndex.get(tool_id, tool_version)
resolves exact versions through entries_by_version, so every non-newest
version of a member-store tool was unreachable (wrong default served,
get_all_versions empty). Merge per (id, version) with the same
earlier-member-wins rule.

Claude-Session: https://claude.ai/code/session_018L7ZmCv2ubKA3JNeSL8Pkr
This commit is contained in:
mvdbeek
2026-07-28 17:27:27 +02:00
parent a379c10993
commit 372ada496b
2 changed files with 26 additions and 22 deletions
@@ -168,6 +168,14 @@ class CompositeToolSourceStore(ToolSourceStore):
if tool_id in merged.entries:
continue
merged.entries[tool_id] = entry
# Merge the per-version map too — ``ToolIndex.get(tool_id,
# tool_version)`` resolves exact versions through it, so skipping
# it would make every non-newest version of a member-store tool
# unreachable. Same collision rule, applied per (id, version).
for tool_id, versions in idx.entries_by_version.items():
version_bucket = merged.entries_by_version.setdefault(tool_id, {})
for version, entry in versions.items():
version_bucket.setdefault(version, entry)
for section_id, ids in idx.by_section.items():
bucket = merged.by_section.setdefault(section_id, [])
for tid in ids:
@@ -30,14 +30,10 @@ def _src(hash, tool_id="t", version="1"):
)
def _sqlite_url(path):
return f"sqlite:///{path}"
def test_priority_order_first_hit_wins(two_paths):
pa, pb = two_paths
a = SqliteToolSourceStore(url=_sqlite_url(pa))
b = SqliteToolSourceStore(url=_sqlite_url(pb))
a = SqliteToolSourceStore(path=pa)
b = SqliteToolSourceStore(path=pb)
# Same hash, different tool_id payloads, to prove which member answered.
a.store(_src("dup", tool_id="from_a"))
b.store(_src("dup", tool_id="from_b"))
@@ -49,8 +45,8 @@ def test_priority_order_first_hit_wins(two_paths):
def test_writes_go_to_default(two_paths):
pa, pb = two_paths
a = SqliteToolSourceStore(url=_sqlite_url(pa))
b = SqliteToolSourceStore(url=_sqlite_url(pb))
a = SqliteToolSourceStore(path=pa)
b = SqliteToolSourceStore(path=pb)
composite = CompositeToolSourceStore(members=[("a", a), ("b", b)], default="b")
composite.store(_src("h1"))
assert b.exists("h1")
@@ -59,17 +55,17 @@ def test_writes_go_to_default(two_paths):
def test_default_must_not_be_read_only(two_paths):
pa, pb = two_paths
rw = SqliteToolSourceStore(url=_sqlite_url(pa))
rw = SqliteToolSourceStore(path=pa)
rw.store(_src("seed")) # so the file exists
ro = SqliteToolSourceStore(url=_sqlite_url(pa), read_only=True)
ro = SqliteToolSourceStore(path=pa, read_only=True)
with pytest.raises(ValueError):
CompositeToolSourceStore(members=[("ro", ro), ("rw", rw)], default="ro")
def test_list_all_dedupes_across_members(two_paths):
pa, pb = two_paths
a = SqliteToolSourceStore(url=_sqlite_url(pa))
b = SqliteToolSourceStore(url=_sqlite_url(pb))
a = SqliteToolSourceStore(path=pa)
b = SqliteToolSourceStore(path=pb)
a.store(_src("h1"))
a.store(_src("dup"))
b.store(_src("dup"))
@@ -81,8 +77,8 @@ def test_list_all_dedupes_across_members(two_paths):
def test_load_index_merges_and_dedupes(two_paths):
pa, pb = two_paths
a = SqliteToolSourceStore(url=_sqlite_url(pa))
b = SqliteToolSourceStore(url=_sqlite_url(pb))
a = SqliteToolSourceStore(path=pa)
b = SqliteToolSourceStore(path=pb)
a.store_index(
ToolIndex(
entries={
@@ -109,8 +105,8 @@ def test_load_index_merges_and_dedupes(two_paths):
def test_load_index_returns_none_when_no_member_has_one(two_paths):
pa, pb = two_paths
a = SqliteToolSourceStore(url=_sqlite_url(pa))
b = SqliteToolSourceStore(url=_sqlite_url(pb))
a = SqliteToolSourceStore(path=pa)
b = SqliteToolSourceStore(path=pb)
composite = CompositeToolSourceStore(members=[("a", a), ("b", b)], default="b")
assert composite.load_index() is None
@@ -123,8 +119,8 @@ def test_invalidate_fans_out(two_paths):
# cache hides the new entry; with composite.invalidate_index_cache()
# the next load surfaces it.
pa, pb = two_paths
a = SqliteToolSourceStore(url=_sqlite_url(pa))
b = SqliteToolSourceStore(url=_sqlite_url(pb))
a = SqliteToolSourceStore(path=pa)
b = SqliteToolSourceStore(path=pb)
a.store_index(ToolIndex(entries={"x": ToolIndexEntry(id="x")}))
b.store_index(ToolIndex(entries={"y": ToolIndexEntry(id="y")}))
a.load_index()
@@ -132,10 +128,10 @@ def test_invalidate_fans_out(two_paths):
# Out-of-band update via a fresh handle so the existing instance's
# cache stays primed with the old value.
SqliteToolSourceStore(url=_sqlite_url(pa)).store_index(
SqliteToolSourceStore(path=pa).store_index(
ToolIndex(entries={"x": ToolIndexEntry(id="x"), "x2": ToolIndexEntry(id="x2")})
)
SqliteToolSourceStore(url=_sqlite_url(pb)).store_index(
SqliteToolSourceStore(path=pb).store_index(
ToolIndex(entries={"y": ToolIndexEntry(id="y"), "y2": ToolIndexEntry(id="y2")})
)
@@ -160,8 +156,8 @@ def test_invalidate_fans_out(two_paths):
def test_load_index_merges_entries_by_version(two_paths):
pa, pb = two_paths
a = SqliteToolSourceStore(url=_sqlite_url(pa))
b = SqliteToolSourceStore(url=_sqlite_url(pb))
a = SqliteToolSourceStore(path=pa)
b = SqliteToolSourceStore(path=pb)
idx_a = ToolIndex()
idx_a.add_entry(ToolIndexEntry(id="multi", name="v1", version="1.0"))
idx_a.add_entry(ToolIndexEntry(id="multi", name="v2", version="2.0"))