Skip to content

Commit a085fc2

Browse files
committed
test(memory): harden local embedding init coverage
1 parent 81dbef0 commit a085fc2

File tree

2 files changed

+114
-0
lines changed

2 files changed

+114
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ Docs: https://docs.openclaw.ai
8282
- Agents/Compaction continuity: expand staged-summary merge instructions to preserve active task status, batch progress, latest user request, and follow-up commitments so compaction handoffs retain in-flight work context. (#8903) thanks @joetomasone.
8383
- Gateway/status self version reporting: make Gateway self version in `openclaw status` prefer runtime `VERSION` (while preserving explicit `OPENCLAW_VERSION` override), preventing stale post-upgrade app version output. (#32655) thanks @liuxiaopai-ai.
8484
- Memory/QMD index isolation: set `QMD_CONFIG_DIR` alongside `XDG_CONFIG_HOME` so QMD config state stays per-agent despite upstream XDG handling bugs, preventing cross-agent collection indexing and excess disk/CPU usage. (#27028) thanks @HenryLoenwind.
85+
- Memory/local embedding initialization hardening: add regression coverage for transient initialization retry and mixed `embedQuery` + `embedBatch` concurrent startup to lock single-flight initialization behavior. (#15639) thanks @SubtleSpark.
8586
- CLI/Coding-agent reliability: switch default `claude-cli` non-interactive args to `--permission-mode bypassPermissions`, auto-normalize legacy `--dangerously-skip-permissions` backend overrides to the modern permission-mode form, align coding-agent + live-test docs with the non-PTY Claude path, and emit session system-event heartbeat notices when CLI watchdog no-output timeouts terminate runs. Related to #28261. Landed from contributor PRs #28610 and #31149. Thanks @niceysam, @cryptomaltese and @vincentkoc.
8687
- ACP/ACPX session bootstrap: retry with `sessions new` when `sessions ensure` returns no session identifiers so ACP spawns avoid `NO_SESSION`/`ACP_TURN_FAILED` failures on affected agents. Related to #28786. Landed from contributor PR #31338. Thanks @Sid-Qin and @vincentkoc.
8788
- LINE/auth boundary hardening synthesis: enforce strict LINE webhook authn/z boundary semantics across pairing-store account scoping, DM/group allowlist separation, fail-closed webhook auth/runtime behavior, and replay/duplication controls (including in-flight replay reservation and post-success dedupe marking). (from #26701, #26683, #25978, #17593, #16619, #31990, #26047, #30584, #18777) Thanks @bmendonca3, @davidahmann, @harshang03, @haosenwang1018, @liuxiaopai-ai, @coygeek, and @Takhoffman.

src/memory/embeddings.test.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,119 @@ describe("local embedding ensureContext concurrency", () => {
537537
expect(loadModelSpy).toHaveBeenCalledTimes(1);
538538
expect(createContextSpy).toHaveBeenCalledTimes(1);
539539
});
540+
541+
it("retries initialization after a transient ensureContext failure", async () => {
542+
const getLlamaSpy = vi.fn();
543+
const loadModelSpy = vi.fn();
544+
const createContextSpy = vi.fn();
545+
546+
let failFirstGetLlama = true;
547+
const nodeLlamaModule = await import("./node-llama.js");
548+
vi.spyOn(nodeLlamaModule, "importNodeLlamaCpp").mockResolvedValue({
549+
getLlama: async (...args: unknown[]) => {
550+
getLlamaSpy(...args);
551+
if (failFirstGetLlama) {
552+
failFirstGetLlama = false;
553+
throw new Error("transient init failure");
554+
}
555+
return {
556+
loadModel: async (...modelArgs: unknown[]) => {
557+
loadModelSpy(...modelArgs);
558+
return {
559+
createEmbeddingContext: async () => {
560+
createContextSpy();
561+
return {
562+
getEmbeddingFor: vi.fn().mockResolvedValue({
563+
vector: new Float32Array([1, 0, 0, 0]),
564+
}),
565+
};
566+
},
567+
};
568+
},
569+
};
570+
},
571+
resolveModelFile: async () => "/fake/model.gguf",
572+
LlamaLogLevel: { error: 0 },
573+
} as never);
574+
575+
const { createEmbeddingProvider } = await import("./embeddings.js");
576+
577+
const result = await createEmbeddingProvider({
578+
config: {} as never,
579+
provider: "local",
580+
model: "",
581+
fallback: "none",
582+
});
583+
584+
const provider = requireProvider(result);
585+
await expect(provider.embedBatch(["first"])).rejects.toThrow("transient init failure");
586+
587+
const recovered = await provider.embedBatch(["second"]);
588+
expect(recovered).toHaveLength(1);
589+
expect(recovered[0]).toHaveLength(4);
590+
591+
expect(getLlamaSpy).toHaveBeenCalledTimes(2);
592+
expect(loadModelSpy).toHaveBeenCalledTimes(1);
593+
expect(createContextSpy).toHaveBeenCalledTimes(1);
594+
});
595+
596+
it("shares initialization when embedQuery and embedBatch start concurrently", async () => {
597+
const getLlamaSpy = vi.fn();
598+
const loadModelSpy = vi.fn();
599+
const createContextSpy = vi.fn();
600+
601+
const nodeLlamaModule = await import("./node-llama.js");
602+
vi.spyOn(nodeLlamaModule, "importNodeLlamaCpp").mockResolvedValue({
603+
getLlama: async (...args: unknown[]) => {
604+
getLlamaSpy(...args);
605+
await new Promise((r) => setTimeout(r, 50));
606+
return {
607+
loadModel: async (...modelArgs: unknown[]) => {
608+
loadModelSpy(...modelArgs);
609+
await new Promise((r) => setTimeout(r, 50));
610+
return {
611+
createEmbeddingContext: async () => {
612+
createContextSpy();
613+
return {
614+
getEmbeddingFor: vi.fn().mockResolvedValue({
615+
vector: new Float32Array([1, 0, 0, 0]),
616+
}),
617+
};
618+
},
619+
};
620+
},
621+
};
622+
},
623+
resolveModelFile: async () => "/fake/model.gguf",
624+
LlamaLogLevel: { error: 0 },
625+
} as never);
626+
627+
const { createEmbeddingProvider } = await import("./embeddings.js");
628+
629+
const result = await createEmbeddingProvider({
630+
config: {} as never,
631+
provider: "local",
632+
model: "",
633+
fallback: "none",
634+
});
635+
636+
const provider = requireProvider(result);
637+
const [queryA, batch, queryB] = await Promise.all([
638+
provider.embedQuery("query-a"),
639+
provider.embedBatch(["batch-a", "batch-b"]),
640+
provider.embedQuery("query-b"),
641+
]);
642+
643+
expect(queryA).toHaveLength(4);
644+
expect(batch).toHaveLength(2);
645+
expect(queryB).toHaveLength(4);
646+
expect(batch[0]).toHaveLength(4);
647+
expect(batch[1]).toHaveLength(4);
648+
649+
expect(getLlamaSpy).toHaveBeenCalledTimes(1);
650+
expect(loadModelSpy).toHaveBeenCalledTimes(1);
651+
expect(createContextSpy).toHaveBeenCalledTimes(1);
652+
});
540653
});
541654

542655
describe("FTS-only fallback when no provider available", () => {

0 commit comments

Comments
 (0)