From f1737993b7e6fd8760cd0552963aaefd2772e65a Mon Sep 17 00:00:00 2001 From: FullerStackDev <263060202+fuller-stack-dev@users.noreply.github.com> Date: Wed, 8 Apr 2026 09:25:20 -0600 Subject: [PATCH] fix: validate resolved context engine contracts --- src/context-engine/context-engine.test.ts | 65 +++++++++++++++++++++++ src/context-engine/registry.ts | 48 ++++++++++++++++- 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/src/context-engine/context-engine.test.ts b/src/context-engine/context-engine.test.ts index 5fe7cc37c4d..51fd031ba1a 100644 --- a/src/context-engine/context-engine.test.ts +++ b/src/context-engine/context-engine.test.ts @@ -709,6 +709,71 @@ describe("Invalid engine fallback", () => { expect(message).toContain("legacy"); } }); + + it("rejects resolved engines that omit info metadata", async () => { + const engineId = `invalid-info-${Date.now().toString(36)}`; + registerContextEngine( + engineId, + () => + ({ + async ingest() { + return { ingested: false }; + }, + async assemble({ messages }: { messages: AgentMessage[] }) { + return { messages, estimatedTokens: 0 }; + }, + async compact() { + return { ok: true, compacted: false }; + }, + }) as unknown as ContextEngine, + ); + + await expect(resolveContextEngine(configWithSlot(engineId))).rejects.toThrow( + `Context engine "${engineId}" factory returned an invalid ContextEngine: missing info.`, + ); + }); + + it("rejects resolved engines that omit required info fields", async () => { + const engineId = `invalid-info-fields-${Date.now().toString(36)}`; + registerContextEngine( + engineId, + () => + ({ + info: { id: engineId }, + async ingest() { + return { ingested: false }; + }, + async assemble({ messages }: { messages: AgentMessage[] }) { + return { messages, estimatedTokens: 0 }; + }, + async compact() { + return { ok: true, compacted: false }; + }, + }) as unknown as ContextEngine, + ); + + await expect(resolveContextEngine(configWithSlot(engineId))).rejects.toThrow( + `Context engine "${engineId}" factory returned an invalid ContextEngine: missing info.name.`, + ); + }); + + it("rejects resolved engines that omit required lifecycle methods", async () => { + const engineId = `invalid-methods-${Date.now().toString(36)}`; + registerContextEngine( + engineId, + () => + ({ + info: { id: engineId, name: "Broken Engine" }, + async ingest() { + return { ingested: false }; + }, + }) as unknown as ContextEngine, + ); + + await expect(resolveContextEngine(configWithSlot(engineId))).rejects.toThrow( + `Context engine "${engineId}" factory returned an invalid ContextEngine: missing assemble(), missing compact().`, + ); + }); }); // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/context-engine/registry.ts b/src/context-engine/registry.ts index 23e5c783c30..ec8ffc146f7 100644 --- a/src/context-engine/registry.ts +++ b/src/context-engine/registry.ts @@ -395,6 +395,46 @@ export function listContextEngineIds(): string[] { return [...getContextEngineRegistryState().engines.keys()]; } +function describeResolvedContextEngineContractError( + engineId: string, + engine: unknown, +): string | null { + if (!engine || typeof engine !== "object") { + return `Context engine "${engineId}" factory returned ${JSON.stringify(engine)} instead of a ContextEngine object.`; + } + + const candidate = engine as Record; + const issues: string[] = []; + const info = candidate.info; + if (!info || typeof info !== "object") { + issues.push("missing info"); + } else { + const infoRecord = info as Record; + if (typeof infoRecord.id !== "string" || !infoRecord.id.trim()) { + issues.push("missing info.id"); + } + if (typeof infoRecord.name !== "string" || !infoRecord.name.trim()) { + issues.push("missing info.name"); + } + } + + if (typeof candidate.ingest !== "function") { + issues.push("missing ingest()"); + } + if (typeof candidate.assemble !== "function") { + issues.push("missing assemble()"); + } + if (typeof candidate.compact !== "function") { + issues.push("missing compact()"); + } + + if (issues.length === 0) { + return null; + } + + return `Context engine "${engineId}" factory returned an invalid ContextEngine: ${issues.join(", ")}.`; +} + // --------------------------------------------------------------------------- // Resolution // --------------------------------------------------------------------------- @@ -423,5 +463,11 @@ export async function resolveContextEngine(config?: OpenClawConfig): Promise