Files
rohitg00--agentmemory/test/index-persistence.test.ts
wehub-resource-sync 979fb22d7c
CI / test (20, macos-latest) (push) Has been cancelled
CI / test (20, ubuntu-latest) (push) Has been cancelled
CI / test (22, macos-latest) (push) Has been cancelled
CI / test (22, ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:18 +08:00

794 lines
25 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { IndexPersistence } from "../src/state/index-persistence.js";
import { SearchIndex } from "../src/state/search-index.js";
import { VectorIndex } from "../src/state/vector-index.js";
import type { CompressedObservation } from "../src/types.js";
const BM25_SCOPE = "mem:index:bm25";
const BM25_LEGACY_KEY = "data";
const BM25_MANIFEST_KEY = "data:manifest";
const VECTOR_LEGACY_KEY = "vectors";
const VECTOR_MANIFEST_KEY = "vectors:manifest";
type TestIndexShardManifest = {
v: 1;
generation?: string;
shards: Array<{ scope: string; key: string; chars: number }>;
chars: number;
};
function mockKV() {
const store = new Map<string, Map<string, unknown>>();
return {
get: async <T>(scope: string, key: string): Promise<T | null> => {
return (store.get(scope)?.get(key) as T) ?? null;
},
set: async <T>(scope: string, key: string, data: T): Promise<T> => {
if (!store.has(scope)) store.set(scope, new Map());
store.get(scope)!.set(key, data);
return data;
},
delete: async (scope: string, key: string): Promise<void> => {
store.get(scope)?.delete(key);
},
list: async <T>(scope: string): Promise<T[]> => {
const entries = store.get(scope);
return entries ? (Array.from(entries.values()) as T[]) : [];
},
};
}
type MockKV = ReturnType<typeof mockKV>;
function makeObs(
overrides: Partial<CompressedObservation> = {},
): CompressedObservation {
return {
id: "obs_1",
sessionId: "ses_1",
timestamp: new Date().toISOString(),
type: "file_edit",
title: "Edit auth middleware",
subtitle: "JWT validation",
facts: ["Added token check"],
narrative: "Modified the auth middleware to validate JWT tokens",
concepts: ["authentication", "jwt"],
files: ["src/middleware/auth.ts"],
importance: 7,
...overrides,
};
}
function makeBm25(id: string, title: string): SearchIndex {
const bm25 = new SearchIndex();
bm25.add(makeObs({ id, title, narrative: `${title} narrative` }));
return bm25;
}
function makeVector(id = "obs_1"): VectorIndex {
const vector = new VectorIndex();
vector.add(id, "ses_1", new Float32Array([0.1, 0.2, 0.3]));
return vector;
}
async function getBm25Manifest(kv: MockKV): Promise<TestIndexShardManifest> {
const manifest = await kv.get<TestIndexShardManifest>(
BM25_SCOPE,
BM25_MANIFEST_KEY,
);
expect(manifest).not.toBeNull();
return manifest!;
}
describe("IndexPersistence", () => {
let kv: ReturnType<typeof mockKV>;
beforeEach(() => {
vi.useFakeTimers();
kv = mockKV();
});
afterEach(() => {
vi.useRealTimers();
});
it("saves and loads BM25 index round-trip", async () => {
const bm25 = new SearchIndex();
bm25.add(makeObs({ id: "obs_1", title: "auth handler" }));
const persistence = new IndexPersistence(kv as never, bm25, null);
await persistence.save();
const loaded = await persistence.load();
expect(loaded.bm25).not.toBeNull();
expect(loaded.bm25!.size).toBe(1);
const results = loaded.bm25!.search("auth");
expect(results.length).toBe(1);
});
it("saves BM25 index shards outside the BM25 metadata scope", async () => {
const bm25 = new SearchIndex();
bm25.add(
makeObs({
id: "obs_1",
title: "auth handler ".repeat(40),
narrative: "JWT middleware validation ".repeat(40),
}),
);
const persistence = new IndexPersistence(kv as never, bm25, null, {
shardChars: 80,
createGeneration: () => "gen_bm25",
});
await persistence.save();
const manifest = await getBm25Manifest(kv);
expect(manifest.generation).toBe("gen_bm25");
expect(manifest.shards.length).toBeGreaterThan(1);
expect(manifest.shards[0].scope).toContain(":gen_bm25:");
await expect(kv.get(BM25_SCOPE, BM25_LEGACY_KEY)).resolves.toBeNull();
await expect(
kv.get(manifest.shards[0].scope, manifest.shards[0].key),
).resolves.toEqual(expect.any(String));
const loaded = await persistence.load();
expect(loaded.bm25).not.toBeNull();
expect(loaded.bm25!.search("auth").length).toBe(1);
});
it("loads legacy monolithic BM25 and vector snapshots", async () => {
const bm25 = makeBm25("obs_1", "legacy auth handler");
const vector = makeVector("obs_1");
await kv.set(BM25_SCOPE, BM25_LEGACY_KEY, bm25.serialize());
await kv.set(BM25_SCOPE, VECTOR_LEGACY_KEY, vector.serialize());
const loaded = await new IndexPersistence(
kv as never,
new SearchIndex(),
null,
).load();
expect(loaded.bm25).not.toBeNull();
expect(loaded.bm25!.search("legacy").length).toBe(1);
expect(loaded.vector).not.toBeNull();
expect(loaded.vector!.size).toBe(1);
});
it("fails closed instead of falling back when manifest reads fail", async () => {
const legacy = makeBm25("obs_legacy", "legacy stale snapshot");
await kv.set(BM25_SCOPE, BM25_LEGACY_KEY, legacy.serialize());
const failingKv = {
...kv,
get: vi.fn(async <T>(scope: string, key: string): Promise<T | null> => {
if (scope === BM25_SCOPE && key === BM25_MANIFEST_KEY) {
throw new Error("manifest backend unavailable");
}
return kv.get(scope, key);
}),
};
const loaded = await new IndexPersistence(
failingKv as never,
new SearchIndex(),
null,
).load();
expect(loaded.bm25).toBeNull();
});
it("fails closed when legacy snapshot reads fail", async () => {
const failingKv = {
...kv,
get: vi.fn(async <T>(scope: string, key: string): Promise<T | null> => {
if (scope === BM25_SCOPE && key === BM25_LEGACY_KEY) {
throw new Error("legacy backend unavailable");
}
return kv.get(scope, key);
}),
};
const loaded = await new IndexPersistence(
failingKv as never,
new SearchIndex(),
null,
).load();
expect(loaded.bm25).toBeNull();
});
it("loads sharded manifests that omit optional generation metadata", async () => {
const bm25 = makeBm25("obs_1", "deterministic shard auth");
const serialized = bm25.serialize();
const chunks = [serialized.slice(0, 50), serialized.slice(50)];
await kv.set("mem:index:bm25:bm25:00000", "data", chunks[0]);
await kv.set("mem:index:bm25:bm25:00001", "data", chunks[1]);
await kv.set<TestIndexShardManifest>(BM25_SCOPE, BM25_MANIFEST_KEY, {
v: 1,
chars: serialized.length,
shards: [
{
scope: "mem:index:bm25:bm25:00000",
key: "data",
chars: chunks[0].length,
},
{
scope: "mem:index:bm25:bm25:00001",
key: "data",
chars: chunks[1].length,
},
],
});
const loaded = await new IndexPersistence(
kv as never,
new SearchIndex(),
null,
).load();
expect(loaded.bm25).not.toBeNull();
expect(loaded.bm25!.search("deterministic").length).toBe(1);
});
it("saves and loads vector index round-trip", async () => {
const bm25 = new SearchIndex();
const vector = makeVector();
const persistence = new IndexPersistence(kv as never, bm25, vector);
await persistence.save();
const loaded = await persistence.load();
expect(loaded.vector).not.toBeNull();
expect(loaded.vector!.size).toBe(1);
});
it("saves vector index shards outside the BM25 scope", async () => {
const bm25 = new SearchIndex();
const vector = new VectorIndex();
vector.add(
"obs_1",
"ses_1",
new Float32Array(Array.from({ length: 32 }, (_, i) => i)),
);
const persistence = new IndexPersistence(kv as never, bm25, vector, {
shardChars: 40,
createGeneration: () => "gen_vector",
});
await persistence.save();
const manifest = await kv.get<TestIndexShardManifest>(
BM25_SCOPE,
VECTOR_MANIFEST_KEY,
);
expect(manifest).not.toBeNull();
expect(manifest!.generation).toBe("gen_vector");
expect(manifest!.shards.length).toBeGreaterThan(1);
expect(manifest!.shards[0].scope).toContain(":gen_vector:");
await expect(kv.get(BM25_SCOPE, VECTOR_LEGACY_KEY)).resolves.toBeNull();
await expect(
kv.get(manifest!.shards[0].scope, manifest!.shards[0].key),
).resolves.toEqual(expect.any(String));
const loaded = await persistence.load();
expect(loaded.vector).not.toBeNull();
expect(loaded.vector!.size).toBe(1);
});
it("persists empty vector snapshots so cleared vectors do not reload", async () => {
const previousBm25 = makeBm25("obs_old", "alpha previous snapshot");
const previousVector = makeVector("obs_old");
await new IndexPersistence(kv as never, previousBm25, previousVector, {
shardChars: 80,
createGeneration: () => "gen_old",
}).save();
const nextBm25 = makeBm25("obs_new", "bravo new snapshot");
const emptyVector = new VectorIndex();
await new IndexPersistence(kv as never, nextBm25, emptyVector, {
shardChars: 80,
createGeneration: () => "gen_empty",
}).save();
const vectorManifest = await kv.get<TestIndexShardManifest>(
BM25_SCOPE,
VECTOR_MANIFEST_KEY,
);
expect(vectorManifest).not.toBeNull();
expect(vectorManifest!.generation).toBe("gen_empty");
const loaded = await new IndexPersistence(
kv as never,
new SearchIndex(),
null,
).load();
expect(loaded.bm25!.search("bravo").length).toBe(1);
expect(loaded.vector).not.toBeNull();
expect(loaded.vector!.size).toBe(0);
});
it("avoids one oversized state::set string payload for persisted indexes", async () => {
const maxStringPayloadChars = 80;
const bm25 = new SearchIndex();
bm25.add(
makeObs({
id: "obs_1",
title: "large persisted snapshot ".repeat(40),
narrative: "oversized state set reproduction ".repeat(40),
}),
);
const vector = new VectorIndex();
vector.add(
"obs_1",
"ses_1",
new Float32Array(Array.from({ length: 64 }, (_, i) => i / 10)),
);
const guardedKv = {
...kv,
set: vi.fn(async <T>(scope: string, key: string, data: T): Promise<T> => {
if (
typeof data === "string" &&
data.length > maxStringPayloadChars
) {
throw new Error(`oversized state::set payload: ${scope}/${key}`);
}
return kv.set(scope, key, data);
}),
};
await new IndexPersistence(guardedKv as never, bm25, vector, {
shardChars: maxStringPayloadChars,
createGeneration: () => "gen_payload_limit",
}).save();
const loaded = await new IndexPersistence(
kv as never,
new SearchIndex(),
null,
).load();
expect(loaded.bm25!.search("oversized").length).toBe(1);
expect(loaded.vector!.size).toBe(1);
});
it("falls back to the default shard size for fractional values below one", async () => {
const bm25 = makeBm25("obs_fraction", "fractional shard config");
let newShardWrites = 0;
const guardedKv = {
...kv,
set: vi.fn(async <T>(scope: string, key: string, data: T): Promise<T> => {
if (scope.includes(":gen_fraction:")) {
newShardWrites += 1;
if (newShardWrites > 3) {
throw new Error("fractional shard size caused zero-width shards");
}
}
return kv.set(scope, key, data);
}),
};
await new IndexPersistence(guardedKv as never, bm25, null, {
shardChars: 0.5,
createGeneration: () => "gen_fraction",
}).save();
const manifest = await getBm25Manifest(kv);
expect(manifest.generation).toBe("gen_fraction");
expect(manifest.shards.length).toBe(1);
expect(newShardWrites).toBe(1);
});
it("keeps the previous generation when a shard write fails before manifest commit", async () => {
const previous = makeBm25("obs_old", "alpha previous snapshot");
await new IndexPersistence(kv as never, previous, null, {
shardChars: 80,
createGeneration: () => "gen_old",
}).save();
const previousManifest = await getBm25Manifest(kv);
let newShardWrites = 0;
const failingKv = {
...kv,
set: vi.fn(async <T>(scope: string, key: string, data: T): Promise<T> => {
if (scope.includes(":gen_new:")) {
newShardWrites += 1;
if (newShardWrites === 2) throw new Error("shard write failed");
}
return kv.set(scope, key, data);
}),
};
const next = makeBm25("obs_new", "bravo new snapshot");
await new IndexPersistence(failingKv as never, next, null, {
shardChars: 80,
createGeneration: () => "gen_new",
}).save();
await expect(kv.get(BM25_SCOPE, BM25_MANIFEST_KEY)).resolves.toEqual(
previousManifest,
);
await expect(
kv.get("mem:index:bm25:bm25:gen_new:00000", "data"),
).resolves.toBeNull();
const loaded = await new IndexPersistence(
kv as never,
new SearchIndex(),
null,
).load();
expect(loaded.bm25!.search("alpha").length).toBe(1);
expect(loaded.bm25!.search("bravo").length).toBe(0);
});
it("keeps the previous generation when manifest set rejects before commit", async () => {
const previous = makeBm25("obs_old", "alpha previous snapshot");
await new IndexPersistence(kv as never, previous, null, {
shardChars: 80,
createGeneration: () => "gen_old",
}).save();
const previousManifest = await getBm25Manifest(kv);
const failingKv = {
...kv,
set: vi.fn(async <T>(scope: string, key: string, data: T): Promise<T> => {
if (scope === BM25_SCOPE && key === BM25_MANIFEST_KEY) {
throw new Error("manifest write failed");
}
return kv.set(scope, key, data);
}),
};
const next = makeBm25("obs_new", "bravo new snapshot");
await new IndexPersistence(failingKv as never, next, null, {
shardChars: 80,
createGeneration: () => "gen_new",
}).save();
await expect(kv.get(BM25_SCOPE, BM25_MANIFEST_KEY)).resolves.toEqual(
previousManifest,
);
await expect(
kv.get("mem:index:bm25:bm25:gen_new:00000", "data"),
).resolves.toBeNull();
const loaded = await new IndexPersistence(
kv as never,
new SearchIndex(),
null,
).load();
expect(loaded.bm25!.search("alpha").length).toBe(1);
expect(loaded.bm25!.search("bravo").length).toBe(0);
});
it("keeps a generation loadable when manifest set commits before rejecting", async () => {
const previous = makeBm25("obs_old", "alpha previous snapshot");
await new IndexPersistence(kv as never, previous, null, {
shardChars: 80,
createGeneration: () => "gen_old",
}).save();
const failingKv = {
...kv,
set: vi.fn(async <T>(scope: string, key: string, data: T): Promise<T> => {
if (scope === BM25_SCOPE && key === BM25_MANIFEST_KEY) {
await kv.set(scope, key, data);
throw new Error("manifest write timed out after commit");
}
return kv.set(scope, key, data);
}),
};
const next = makeBm25("obs_new", "bravo new snapshot");
await new IndexPersistence(failingKv as never, next, null, {
shardChars: 80,
createGeneration: () => "gen_new",
}).save();
const manifest = await getBm25Manifest(kv);
expect(manifest.generation).toBe("gen_new");
await expect(
kv.get("mem:index:bm25:bm25:gen_new:00000", "data"),
).resolves.toEqual(expect.any(String));
const loaded = await new IndexPersistence(
kv as never,
new SearchIndex(),
null,
).load();
expect(loaded.bm25!.search("bravo").length).toBe(1);
});
it("deletes a shard that committed before set rejected", async () => {
const previous = makeBm25("obs_old", "alpha previous snapshot");
await new IndexPersistence(kv as never, previous, null, {
shardChars: 80,
createGeneration: () => "gen_old",
}).save();
const previousManifest = await getBm25Manifest(kv);
const failingKv = {
...kv,
set: vi.fn(async <T>(scope: string, key: string, data: T): Promise<T> => {
if (scope === "mem:index:bm25:bm25:gen_new:00000") {
await kv.set(scope, key, data);
throw new Error("state::set timed out after commit");
}
return kv.set(scope, key, data);
}),
};
const next = makeBm25("obs_new", "bravo new snapshot");
await new IndexPersistence(failingKv as never, next, null, {
shardChars: 80,
createGeneration: () => "gen_new",
}).save();
await expect(kv.get(BM25_SCOPE, BM25_MANIFEST_KEY)).resolves.toEqual(
previousManifest,
);
await expect(
kv.get("mem:index:bm25:bm25:gen_new:00000", "data"),
).resolves.toBeNull();
const loaded = await new IndexPersistence(
kv as never,
new SearchIndex(),
null,
).load();
expect(loaded.bm25!.search("alpha").length).toBe(1);
expect(loaded.bm25!.search("bravo").length).toBe(0);
});
it("loads the new generation even when old generation cleanup fails", async () => {
const previous = makeBm25("obs_old", "alpha previous snapshot");
await new IndexPersistence(kv as never, previous, null, {
shardChars: 80,
createGeneration: () => "gen_old",
}).save();
const cleanupKv = {
...kv,
delete: vi.fn(async () => {
throw new Error("cleanup failed");
}),
};
const next = makeBm25("obs_new", "bravo new snapshot");
await new IndexPersistence(cleanupKv as never, next, null, {
shardChars: 80,
createGeneration: () => "gen_new",
}).save();
const manifest = await getBm25Manifest(kv);
expect(manifest.generation).toBe("gen_new");
expect(cleanupKv.delete).toHaveBeenCalled();
const loaded = await new IndexPersistence(
kv as never,
new SearchIndex(),
null,
).load();
expect(loaded.bm25!.search("bravo").length).toBe(1);
expect(loaded.bm25!.search("alpha").length).toBe(0);
});
it("keeps the previous vector generation when vector save fails after BM25 publish", async () => {
const previousBm25 = makeBm25("obs_old", "alpha previous snapshot");
const previousVector = makeVector("obs_old");
await new IndexPersistence(kv as never, previousBm25, previousVector, {
shardChars: 80,
createGeneration: () => "gen_old",
}).save();
const failingKv = {
...kv,
set: vi.fn(async <T>(scope: string, key: string, data: T): Promise<T> => {
if (scope === BM25_SCOPE && key === VECTOR_MANIFEST_KEY) {
throw new Error("vector manifest write failed");
}
return kv.set(scope, key, data);
}),
};
const nextBm25 = makeBm25("obs_new", "bravo new snapshot");
const nextVector = new VectorIndex();
nextVector.add("obs_new", "ses_1", new Float32Array([0.4, 0.5, 0.6]));
await new IndexPersistence(failingKv as never, nextBm25, nextVector, {
shardChars: 80,
createGeneration: () => "gen_new",
}).save();
await expect(
kv.get("mem:index:bm25:vectors:gen_new:00000", "data"),
).resolves.toBeNull();
const loaded = await new IndexPersistence(
kv as never,
new SearchIndex(),
null,
).load();
expect(loaded.bm25!.search("bravo").length).toBe(1);
expect(loaded.vector!.size).toBe(1);
expect(
loaded.vector!.search(new Float32Array([0.1, 0.2, 0.3]))[0]?.obsId,
).toBe("obs_old");
});
it("fails closed when a manifest shard is missing", async () => {
const bm25 = makeBm25("obs_1", "alpha sharded snapshot");
await new IndexPersistence(kv as never, bm25, null, {
shardChars: 80,
createGeneration: () => "gen_missing",
}).save();
const manifest = await getBm25Manifest(kv);
await kv.delete(manifest.shards[0].scope, manifest.shards[0].key);
const loaded = await new IndexPersistence(
kv as never,
new SearchIndex(),
null,
).load();
expect(loaded.bm25).toBeNull();
});
it("fails closed when a manifest shard length mismatches", async () => {
const bm25 = makeBm25("obs_1", "alpha sharded snapshot");
await new IndexPersistence(kv as never, bm25, null, {
shardChars: 80,
createGeneration: () => "gen_mismatch",
}).save();
const manifest = await getBm25Manifest(kv);
const firstShard = manifest.shards[0];
const chunk = await kv.get<string>(firstShard.scope, firstShard.key);
await kv.set(firstShard.scope, firstShard.key, `${chunk}x`);
const loaded = await new IndexPersistence(
kv as never,
new SearchIndex(),
null,
).load();
expect(loaded.bm25).toBeNull();
});
it("fails closed before reading invalid shard descriptors", async () => {
await kv.set<TestIndexShardManifest>(BM25_SCOPE, BM25_MANIFEST_KEY, {
v: 1,
chars: 10,
shards: [{ scope: "", key: "data", chars: 10 }],
});
const guardedKv = {
...kv,
get: vi.fn(async <T>(scope: string, key: string): Promise<T | null> => {
if (scope === "") {
throw new Error("invalid shard descriptor was read");
}
return kv.get(scope, key);
}),
};
const loaded = await new IndexPersistence(
guardedKv as never,
new SearchIndex(),
null,
).load();
expect(loaded.bm25).toBeNull();
expect(guardedKv.get).not.toHaveBeenCalledWith("", "data");
});
it("scheduleSave debounces multiple calls", async () => {
const bm25 = new SearchIndex();
const persistence = new IndexPersistence(kv as never, bm25, null);
persistence.scheduleSave();
persistence.scheduleSave();
persistence.scheduleSave();
await expect(kv.get(BM25_SCOPE, BM25_MANIFEST_KEY)).resolves.toBeNull();
vi.advanceTimersByTime(5000);
await vi.runAllTimersAsync();
const saved = await kv.get<string>(BM25_SCOPE, BM25_MANIFEST_KEY);
expect(saved).not.toBeNull();
});
it("stop clears the pending timer", async () => {
const bm25 = new SearchIndex();
bm25.add(makeObs({ id: "obs_1", title: "auth handler" }));
const persistence = new IndexPersistence(kv as never, bm25, null);
persistence.scheduleSave();
persistence.stop();
vi.advanceTimersByTime(10000);
const saved = await kv.get<string>(BM25_SCOPE, BM25_MANIFEST_KEY);
expect(saved).toBeNull();
});
it("returns null indexes when nothing has been saved", async () => {
const bm25 = new SearchIndex();
const persistence = new IndexPersistence(kv as never, bm25, null);
const loaded = await persistence.load();
expect(loaded.bm25).toBeNull();
expect(loaded.vector).toBeNull();
});
it("scheduled save swallows kv.set rejection without unhandledRejection (#204)", async () => {
const failingKv = {
...mockKV(),
set: vi.fn(async () => {
const err = new Error(
"TIMEOUT: invocation timed out after 30000ms",
) as Error & { code?: string; function_id?: string };
err.code = "TIMEOUT";
err.function_id = "state::set";
throw err;
}),
};
const bm25 = new SearchIndex();
bm25.add(makeObs({ id: "obs_1", title: "auth handler" }));
const persistence = new IndexPersistence(failingKv as never, bm25, null);
let unhandled = false;
const onUnhandled = () => {
unhandled = true;
};
process.on("unhandledRejection", onUnhandled);
try {
persistence.scheduleSave();
vi.advanceTimersByTime(5000);
await vi.runAllTimersAsync();
// give microtasks a chance to flush
await Promise.resolve();
expect(failingKv.set).toHaveBeenCalled();
expect(unhandled).toBe(false);
} finally {
process.off("unhandledRejection", onUnhandled);
}
});
it("save() does not throw when kv.set rejects (#204)", async () => {
const failingKv = {
...mockKV(),
set: vi.fn(async () => {
throw new Error("TIMEOUT");
}),
};
const bm25 = new SearchIndex();
bm25.add(makeObs({ id: "obs_1", title: "auth handler" }));
const persistence = new IndexPersistence(failingKv as never, bm25, null);
await expect(persistence.save()).resolves.toBeUndefined();
});
// #797: first run after upgrading to 0.9.25 crashed with
// 'TypeError: Cannot read properties of undefined (reading "v")'
// because some iii-state adapters return `undefined` (not `null`)
// for a missing key. The load path's `value !== null` check passed
// undefined to loadManifestData, which then read `undefined.v`.
it("load() returns null instead of crashing when kv.get returns undefined for the manifest (#797)", async () => {
const undefinedKv = {
...mockKV(),
get: vi.fn(async () => undefined),
};
const persistence = new IndexPersistence(
undefinedKv as never,
new SearchIndex(),
null,
);
const loaded = await persistence.load();
expect(loaded.bm25).toBeNull();
expect(loaded.vector).toBeNull();
});
it("load() does not crash when a manifest row value is the wrong shape (#797)", async () => {
const wrongShapeKv = {
...mockKV(),
get: vi.fn(async () => "not-a-manifest"),
};
const persistence = new IndexPersistence(
wrongShapeKv as never,
new SearchIndex(),
null,
);
await expect(persistence.load()).resolves.toBeDefined();
});
});