fix: validate resolved context engine contracts

This commit is contained in:
FullerStackDev
2026-04-08 09:25:20 -06:00
committed by Josh Lehman
parent 785b9b1bc0
commit f1737993b7
2 changed files with 112 additions and 1 deletions

View File

@@ -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().`,
);
});
});
// ═══════════════════════════════════════════════════════════════════════════

View File

@@ -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<string, unknown>;
const issues: string[] = [];
const info = candidate.info;
if (!info || typeof info !== "object") {
issues.push("missing info");
} else {
const infoRecord = info as Record<string, unknown>;
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<Con
);
}
return wrapContextEngineWithSessionKeyCompat(await entry.factory());
const engine = await entry.factory();
const contractError = describeResolvedContextEngineContractError(engineId, engine);
if (contractError) {
throw new Error(contractError);
}
return wrapContextEngineWithSessionKeyCompat(engine);
}