openclaw / src /context-engine /context-engine.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
76289e7 verified
Raw
History Blame Contribute Delete
77.2 kB
// Context engine tests cover context extraction and prompt context assembly.
import type { AgentMessage } from "openclaw/plugin-sdk/agent-core";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { createDeferred } from "../../test/helpers/promise.js";
import {
createContextEngineLogicalTurnLease,
selectContextEngineForTranscriptHost,
} from "../agents/harness/context-engine-logical-turn.js";
import { createAgentCleanupScope } from "../agents/run-cleanup-timeout.js";
import { SessionTranscriptReadFenceError } from "../config/sessions/session-transcript-read-fence.js";
import type { MemoryCitationsMode } from "../config/types.memory.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import {
clearMemoryPluginState,
registerMemoryPromptPreparation,
registerTestMemoryPromptBuilder,
} from "../plugins/memory-state.test-fixtures.js";
import { createEmptyPluginRegistry } from "../plugins/registry-empty.js";
import {
requireActivePluginRegistry,
setActivePluginRegistry,
withPluginRegistrationContext,
} from "../plugins/runtime.js";
import type { UserTurnTranscriptAdmissionReceipt } from "../sessions/user-turn-transcript.types.js";
import { escapeRegExp } from "../shared/regexp.js";
// ---------------------------------------------------------------------------
// We dynamically import the registry so we can get a fresh module per test
// group when needed. For most groups we use the shared singleton directly.
// ---------------------------------------------------------------------------
import {
buildMemorySystemPromptAddition,
delegateCompactionToRuntime,
isRuntimeCompactionDelegate,
prepareMemorySystemPromptAddition,
} from "./delegate.js";
import { LegacyContextEngine } from "./legacy.js";
import { registerLegacyContextEngine } from "./legacy.registration.js";
import {
activateContextEngineRegistrations,
getContextEngineRegistration,
listContextEngineQuarantines,
registerContextEngineForOwner,
registerContextEngineInRegistry,
resolveContextEngine,
resolveContextEngineOwnerPluginId,
resolveLogicalTurnContextEngines,
} from "./registry.js";
import {
captureContextEngineRegistryStateForTests,
resetContextEngineRuntimeQuarantineForTests,
} from "./registry.test-support.js";
import type {
ContextEngine,
ContextEngineInfo,
ContextEngineSessionTarget,
AssembleResult,
CompactResult,
IngestResult,
} from "./types.js";
type ContextEngineFactory = Parameters<typeof registerContextEngineForOwner>[1];
type ContextEngineFactoryContext = Parameters<ContextEngineFactory>[0];
function registerTestContextEngine(id: string, factory: ContextEngineFactory) {
return registerContextEngineForOwner(id, factory, `test:${id}`, {
allowSameOwnerRefresh: true,
});
}
const { compactEmbeddedAgentSessionOnDemandMock } = vi.hoisted(() => ({
compactEmbeddedAgentSessionOnDemandMock: vi.fn(),
}));
vi.mock("../agents/embedded-agent-runner/compact.runtime.js", () => ({
compactEmbeddedAgentSessionOnDemand: compactEmbeddedAgentSessionOnDemandMock,
}));
function installCompactRuntimeSpy(sessionTarget?: ContextEngineSessionTarget) {
return compactEmbeddedAgentSessionOnDemandMock.mockResolvedValue({
ok: true,
compacted: false,
reason: "mock compaction",
result: {
summary: "",
firstKeptEntryId: "",
tokensBefore: 0,
tokensAfter: 0,
details: undefined,
...(sessionTarget ? { sessionTarget } : {}),
},
});
}
function requireCompactRuntimeParams(callIndex: number): Record<string, unknown> {
const params = compactEmbeddedAgentSessionOnDemandMock.mock.calls[callIndex]?.[0] as
| Record<string, unknown>
| undefined;
if (!params) {
throw new Error(`missing compact runtime call ${callIndex}`);
}
return params;
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Build a config object with a contextEngine slot for testing. */
function configWithSlot(engineId: string): OpenClawConfig {
return { plugins: { slots: { contextEngine: engineId } } };
}
function testAdmissionReceipt(): UserTurnTranscriptAdmissionReceipt {
return {
agentId: "main",
sessionId: "session",
sessionKey: "agent:main:session",
storePath: "sqlite://session",
generation: "generation",
entryId: "user-entry",
rawSeq: 1,
effectiveParentId: null,
activeMessagePosition: 0,
logicalTurnId: "logical-turn",
role: "user",
};
}
function makeMockMessage(role: "user" | "assistant" = "user", text = "hello"): AgentMessage {
return { role, content: text, timestamp: Date.now() } as AgentMessage;
}
let restoreContextEngineRegistry = () => {};
beforeAll(() => {
restoreContextEngineRegistry = captureContextEngineRegistryStateForTests();
});
afterAll(() => {
restoreContextEngineRegistry();
});
let uniqueEngineIdCounter = 0;
function uniqueEngineId(prefix: string): string {
uniqueEngineIdCounter += 1;
return `${prefix}-${uniqueEngineIdCounter}`;
}
async function withCompactionDelegateFixture(
acceptSessionKey: boolean,
run: (engine: ContextEngine) => Promise<void>,
) {
registerLegacyContextEngine();
const engineId = uniqueEngineId("compaction-projection");
const compact = vi.fn<ContextEngine["compact"]>(delegateCompactionToRuntime);
registerTestContextEngine(engineId, () => ({
info: {
id: engineId,
name: "Compaction projection",
acceptedHostParams: acceptSessionKey
? ["sessionKey", "runtimeContext", "sessionTarget"]
: ["runtimeContext", "sessionTarget"],
},
async ingest() {
return { ingested: false };
},
async assemble({ messages }) {
return { messages, estimatedTokens: 0 };
},
compact,
}));
const resolution = await resolveLogicalTurnContextEngines(configWithSlot(engineId));
try {
await run(resolution.configured.engine);
expect(compact).toHaveBeenCalledOnce();
if (!acceptSessionKey) {
// The registry, not an invalid typed call, owns host-field omission.
expect(compact.mock.calls[0]?.[0]).not.toHaveProperty("sessionKey");
}
} finally {
await Promise.allSettled([
resolution.configured.engine.dispose?.(),
resolution.fallback.engine.dispose?.(),
]);
}
}
function registerPromptTrackingEngine(engineId: string) {
const calls: Array<Record<string, unknown>> = [];
registerTestContextEngine(engineId, () => ({
info: {
id: engineId,
name: "Prompt Tracker",
version: "0.0.0",
acceptedHostParams: ["prompt"],
},
async ingest() {
return { ingested: false };
},
async assemble(params) {
calls.push({ ...params });
return { messages: params.messages, estimatedTokens: 0 };
},
async compact() {
return { ok: true, compacted: false };
},
}));
return calls;
}
function createPassthroughEngineMethods(): Pick<ContextEngine, "ingest" | "assemble" | "compact"> {
return {
async ingest() {
return { ingested: true };
},
async assemble({ messages }) {
return { messages, estimatedTokens: 0 };
},
async compact() {
return { ok: true, compacted: false };
},
};
}
function requireFactoryContext(
context: ContextEngineFactoryContext | undefined,
): ContextEngineFactoryContext {
if (!context) {
throw new Error("expected context engine factory context");
}
return context;
}
function requireRegistryState() {
return { engines: requireActivePluginRegistry().contextEngines };
}
/** A minimal mock engine that satisfies the ContextEngine interface. */
class MockContextEngine implements ContextEngine {
readonly info: ContextEngineInfo = {
id: "mock",
name: "Mock Engine",
version: "0.0.1",
};
async ingest(_params: {
sessionId: string;
sessionKey?: string;
message: AgentMessage;
isHeartbeat?: boolean;
}): Promise<IngestResult> {
return { ingested: true };
}
async assemble(params: {
sessionId: string;
sessionKey?: string;
messages: AgentMessage[];
tokenBudget?: number;
availableTools?: Set<string>;
citationsMode?: MemoryCitationsMode;
}): Promise<AssembleResult> {
return {
messages: params.messages,
estimatedTokens: 42,
systemPromptAddition: "mock system addition",
};
}
async compact(_params: {
sessionId: string;
sessionKey: string;
agentId?: string;
sessionTarget?: ContextEngineSessionTarget;
tokenBudget?: number;
compactionTarget?: "budget" | "threshold";
customInstructions?: string;
runtimeContext?: Record<string, unknown>;
}): Promise<CompactResult> {
return {
ok: true,
compacted: true,
reason: "mock compaction",
result: {
summary: "mock summary",
tokensBefore: 100,
tokensAfter: 50,
},
};
}
async dispose(): Promise<void> {
// no-op
}
}
// ═══════════════════════════════════════════════════════════════════════════
// 1. Engine contract tests
// ═══════════════════════════════════════════════════════════════════════════
describe("Engine contract tests", () => {
beforeEach(() => {
vi.restoreAllMocks();
compactEmbeddedAgentSessionOnDemandMock.mockReset();
clearMemoryPluginState();
});
it("a mock engine implementing ContextEngine can be registered and resolved", async () => {
const factory = () => new MockContextEngine();
registerTestContextEngine("mock", factory);
const engine = await resolveContextEngine(configWithSlot("mock"));
expect(engine).toBeInstanceOf(MockContextEngine);
expect(engine.info.id).toBe("mock");
});
it("legacy compact preserves runtimeContext currentTokenCount when top-level value is absent", async () => {
const compactRuntimeSpy = installCompactRuntimeSpy();
const engine = new LegacyContextEngine();
await engine.compact({
sessionId: "s1",
sessionKey: "agent:main:s1",
sessionTarget: { agentId: "main", sessionId: "s1", sessionKey: "agent:main:s1" },
runtimeContext: {
workspaceDir: "/tmp/workspace",
currentTokenCount: 277403,
},
});
expect(compactRuntimeSpy).toHaveBeenCalledTimes(1);
expect(requireCompactRuntimeParams(0).currentTokenCount).toBe(277403);
});
it("delegateCompactionToRuntime reuses the legacy runtime bridge", async () => {
const sessionTarget = {
agentId: "main",
sessionId: "s2",
sessionKey: "agent:main:s2",
storePath: "/tmp/openclaw-agent.sqlite",
};
const compactRuntimeSpy = installCompactRuntimeSpy(sessionTarget);
const runtimeContext = {
workspaceDir: "/tmp/workspace",
currentTokenCount: 12345,
};
const result = await delegateCompactionToRuntime({
sessionId: "s2",
sessionKey: "agent:main:s2",
sessionTarget,
tokenBudget: 4096,
runtimeContext,
});
expect(compactRuntimeSpy).toHaveBeenCalledTimes(1);
const compactRuntimeParams = requireCompactRuntimeParams(0);
expect(compactRuntimeParams.sessionId).toBe("s2");
expect(compactRuntimeParams.sessionKey).toBe("agent:main:s2");
expect(compactRuntimeParams.sessionTarget).toEqual(sessionTarget);
expect(compactRuntimeParams).not.toHaveProperty("sessionFile");
expect(compactRuntimeParams.tokenBudget).toBe(4096);
expect(compactRuntimeParams.currentTokenCount).toBe(12345);
expect(compactRuntimeParams.workspaceDir).toBe("/tmp/workspace");
expect(compactRuntimeParams.contextEngineRuntimeContext).toBe(runtimeContext);
expect(result).toEqual({
ok: true,
compacted: false,
reason: "mock compaction",
result: {
summary: "",
firstKeptEntryId: "",
tokensBefore: 0,
tokensAfter: 0,
details: undefined,
sessionTarget,
},
});
});
it("rejects a structured successor key from another agent", async () => {
installCompactRuntimeSpy();
await expect(
delegateCompactionToRuntime({
sessionId: "s-agent-conflict",
sessionKey: "agent:main:s-agent-conflict",
sessionTarget: {
agentId: "worker",
sessionId: "s-agent-conflict",
sessionKey: "agent:main:s-agent-conflict",
storePath: "/tmp/openclaw-agent.sqlite",
},
tokenBudget: 4096,
}),
).rejects.toThrow("successor target conflicts with the caller session identity");
expect(compactEmbeddedAgentSessionOnDemandMock).not.toHaveBeenCalled();
});
it("rejects an internally consistent successor for another caller agent", async () => {
installCompactRuntimeSpy();
await expect(
delegateCompactionToRuntime({
agentId: "main",
sessionId: "s-agent-redirect",
sessionKey: "agent:main:s-agent-redirect",
sessionTarget: {
agentId: "worker",
sessionId: "s-agent-redirect",
sessionKey: "agent:worker:s-agent-redirect",
},
tokenBudget: 4096,
}),
).rejects.toThrow("successor target conflicts with the caller session identity");
expect(compactEmbeddedAgentSessionOnDemandMock).not.toHaveBeenCalled();
});
it.each([
{ name: "caller agent", agentId: "worker" },
{ name: "physical session", sessionId: "another-session" },
{ name: "caller key", sessionKey: "agent:main:another-key" },
{
name: "runtime fallback agent",
agentId: undefined,
runtimeContext: { agentId: "worker" },
},
{
name: "runtime fallback key",
projectSessionKey: true,
runtimeContext: { sessionKey: "agent:main:another-key" },
},
{
name: "runtime fallback target",
sessionTarget: undefined,
runtimeContext: { sessionTarget: { sessionId: "another-session" } },
},
{
name: "target-only parsed key",
agentId: undefined,
projectSessionKey: true,
sessionTarget: { agentId: "worker", sessionKey: "agent:main:session" },
},
{
name: "agent and key without target",
agentId: "worker",
sessionTarget: undefined,
},
])(
"rejects $name before a backend with no nested result can run",
async ({ name: _name, projectSessionKey, ...input }) => {
compactEmbeddedAgentSessionOnDemandMock.mockResolvedValue({ ok: true, compacted: true });
await withCompactionDelegateFixture(!projectSessionKey, async (engine) => {
await expect(
engine.compact({
agentId: "main",
sessionId: "session",
sessionKey: "agent:main:session",
sessionTarget: {
agentId: "main",
sessionId: "session",
sessionKey: "agent:main:session",
},
...input,
}),
).rejects.toThrow(/conflicts with/);
expect(compactEmbeddedAgentSessionOnDemandMock).not.toHaveBeenCalled();
});
},
);
it.each([false, true])(
"selects inputs before normalized comparisons (runtime fallback=%s)",
async (runtimeFallback) => {
const sessionTarget = {
agentId: " main ",
sessionId: " session ",
sessionKey: " agent:main:session ",
};
const runtimeContext = {
agentId: runtimeFallback ? "main" : "ignored",
sessionId: "ignored",
sessionKey: runtimeFallback ? "agent:main:session" : "agent:ignored:ignored",
sessionTarget: runtimeFallback ? sessionTarget : { agentId: "ignored" },
};
compactEmbeddedAgentSessionOnDemandMock.mockResolvedValue({ ok: true, compacted: true });
await withCompactionDelegateFixture(!runtimeFallback, async (engine) => {
const result = await engine.compact({
sessionId: "session",
sessionKey: "agent:main:session",
...(runtimeFallback ? {} : { agentId: "main", sessionTarget }),
runtimeContext,
});
expect(result).toEqual({ ok: true, compacted: true, reason: undefined, result: undefined });
expect(requireCompactRuntimeParams(0)).toMatchObject({
agentId: "main",
sessionId: "session",
sessionKey: "agent:main:session",
sessionTarget,
});
expect(requireCompactRuntimeParams(0).contextEngineRuntimeContext).toBe(runtimeContext);
});
},
);
it("does not restore runtime identity over explicitly empty top-level inputs", async () => {
compactEmbeddedAgentSessionOnDemandMock.mockResolvedValue({ ok: true, compacted: false });
await delegateCompactionToRuntime({
agentId: "",
sessionId: "session",
sessionKey: "",
sessionTarget: {},
runtimeContext: {
agentId: "worker",
sessionId: "other-session",
sessionKey: "agent:worker:other",
sessionTarget: { agentId: "worker", sessionId: "other-session" },
},
});
expect(requireCompactRuntimeParams(0)).toMatchObject({
agentId: "",
sessionId: "session",
sessionKey: "",
sessionTarget: {},
});
});
it("delegateCompactionToRuntime forwards the caller abortSignal to the runtime (#89868)", async () => {
installCompactRuntimeSpy();
const controller = new AbortController();
await delegateCompactionToRuntime({
sessionId: "s-abort",
sessionKey: "agent:main:s-abort",
tokenBudget: 4096,
abortSignal: controller.signal,
});
const compactRuntimeParams = requireCompactRuntimeParams(0);
expect(compactRuntimeParams.abortSignal).toBe(controller.signal);
});
it("delegateCompactionToRuntime passes undefined abortSignal when none supplied", async () => {
installCompactRuntimeSpy();
await delegateCompactionToRuntime({
sessionId: "s-no-abort",
sessionKey: "agent:main:s-no-abort",
tokenBudget: 4096,
});
const compactRuntimeParams = requireCompactRuntimeParams(0);
expect(compactRuntimeParams.abortSignal).toBeUndefined();
});
it("builds a normalized memory system prompt addition from the active memory prompt path", () => {
registerTestMemoryPromptBuilder(({ citationsMode }) => [
"## Memory Recall",
`citations=${citationsMode ?? "auto"}`,
"",
]);
expect(
buildMemorySystemPromptAddition({
availableTools: new Set(["memory_search"]),
citationsMode: "off",
}),
).toBe("## Memory Recall\ncitations=off");
});
it("passes agent context through delegated memory prompt assembly", () => {
registerTestMemoryPromptBuilder(({ agentId, agentSessionKey, sandboxed }) => [
"## Agent Memory",
`agent=${agentId} session=${agentSessionKey} sandboxed=${sandboxed}`,
"",
]);
expect(
buildMemorySystemPromptAddition({
availableTools: new Set(["memory_search", "memory_get"]),
agentId: "marketing-agent",
agentSessionKey: "agent:marketing-agent:main",
sandboxed: true,
}),
).toBe(
"## Agent Memory\nagent=marketing-agent session=agent:marketing-agent:main sandboxed=true",
);
});
it("returns undefined when the active memory prompt path contributes nothing", () => {
expect(
buildMemorySystemPromptAddition({
availableTools: new Set(["memory_search"]),
}),
).toBeUndefined();
});
it("prepares async memory state before context-engine prompt rendering", async () => {
const prepare = vi.fn(async () => ["## Prepared Memory", "loaded from sqlite", ""]);
registerMemoryPromptPreparation("memory-wiki", prepare);
await expect(
prepareMemorySystemPromptAddition({
availableTools: new Set(["wiki_search"]),
agentId: "main",
agentSessionKey: "agent:main:main",
}),
).resolves.toBe("## Prepared Memory\nloaded from sqlite");
expect(prepare).toHaveBeenCalledTimes(1);
});
});
// ═══════════════════════════════════════════════════════════════════════════
// 2. Registry tests
// ═══════════════════════════════════════════════════════════════════════════
describe("Registry tests", () => {
it("registering the same id with the same owner refreshes the factory", () => {
const factory1 = () => new MockContextEngine();
const factory2 = () => new MockContextEngine();
expect(
registerContextEngineForOwner("reg-overwrite", factory1, "owner-a", {
allowSameOwnerRefresh: true,
}),
).toEqual({ ok: true });
expect(getContextEngineRegistration("reg-overwrite")?.factory).toBe(factory1);
expect(
registerContextEngineForOwner("reg-overwrite", factory2, "owner-a", {
allowSameOwnerRefresh: true,
}),
).toEqual({ ok: true });
expect(getContextEngineRegistration("reg-overwrite")?.factory).toBe(factory2);
expect(getContextEngineRegistration("reg-overwrite")?.factory).not.toBe(factory1);
});
it("rejects context engine registrations from a different owner", () => {
const factory1 = () => new MockContextEngine();
const factory2 = () => new MockContextEngine();
expect(
registerContextEngineForOwner("reg-owner-guard", factory1, "owner-a", {
allowSameOwnerRefresh: true,
}),
).toEqual({ ok: true });
expect(registerContextEngineForOwner("reg-owner-guard", factory2, "owner-b")).toEqual({
ok: false,
existingOwner: "owner-a",
});
expect(getContextEngineRegistration("reg-owner-guard")?.factory).toBe(factory1);
});
it("reserves the default engine id even in an empty builder registry", () => {
const building = createEmptyPluginRegistry();
expect(
registerContextEngineInRegistry(
building,
"legacy",
() => new MockContextEngine(),
"plugin:shadow",
),
).toEqual({ ok: false, existingOwner: "core" });
expect(building.contextEngines.size).toBe(0);
});
it("exposes the trusted plugin owner for a resolved registered engine", async () => {
const engineId = `owner-policy-${Date.now().toString(36)}`;
registerContextEngineForOwner(engineId, () => new MockContextEngine(), "plugin:lossless-claw", {
allowSameOwnerRefresh: true,
});
const engine = await resolveContextEngine(configWithSlot(engineId));
expect(resolveContextEngineOwnerPluginId(engine)).toBe("lossless-claw");
});
});
// ═══════════════════════════════════════════════════════════════════════════
// 3. Default engine selection
// ═══════════════════════════════════════════════════════════════════════════
describe("Default engine selection", () => {
// Ensure both legacy and a custom test engine are registered before these tests.
beforeEach(() => {
// Registration is idempotent (Map.set), so calling again is safe.
registerLegacyContextEngine();
// Register a lightweight custom stub so we don't need external resources.
registerTestContextEngine("test-engine", () => {
const engine: ContextEngine = {
info: { id: "test-engine", name: "Custom Test Engine", version: "0.0.0" },
...createPassthroughEngineMethods(),
};
return engine;
});
});
it("resolveContextEngine() with no config returns the default ('legacy') engine", async () => {
const engine = await resolveContextEngine();
expect(engine.info.id).toBe("legacy");
});
it("preserves native compaction watchdog ownership through default resolution", async () => {
const engine = await resolveContextEngine();
expect(isRuntimeCompactionDelegate(Reflect.get(engine, "compact", engine))).toBe(true);
});
it("resolveContextEngine() with config contextEngine='legacy' returns legacy engine", async () => {
const engine = await resolveContextEngine(configWithSlot("legacy"));
expect(engine.info.id).toBe("legacy");
});
it("resolveContextEngine() with config contextEngine='test-engine' returns the custom engine", async () => {
const engine = await resolveContextEngine(configWithSlot("test-engine"));
expect(engine.info.id).toBe("test-engine");
});
it.each([
{
label: "implicit legacy without an admission receipt",
config: undefined,
admission: undefined,
},
{
label: "explicit legacy without an admission receipt",
config: configWithSlot("legacy"),
admission: undefined,
},
{
label: "implicit legacy without declared transcript fencing",
config: undefined,
admission: testAdmissionReceipt(),
},
])("keeps $label configured without a warning", async ({ config, admission }) => {
const warn = vi.fn();
const lease = await createContextEngineLogicalTurnLease({
identity: { runId: "test-run", sessionId: "test-session" },
config,
warn,
});
const selected = selectContextEngineForTranscriptHost({
lease,
host: { id: "agent-harness:test", label: "test harness", capabilities: [] },
operation: "agent-run",
recorder: { getAdmissionReceipt: () => admission, hasPersisted: () => true },
});
expect(selected).toMatchObject({ registeredId: "legacy", mode: "configured" });
expect(lease.effectiveEngineId).toBe("legacy");
expect(lease.degraded).toBe(false);
expect(lease.degradedReason).toBeUndefined();
expect(warn).not.toHaveBeenCalled();
await lease.dispose();
});
it("keeps repeated baseline host selection stable after the turn starts", async () => {
const warn = vi.fn();
const lease = await createContextEngineLogicalTurnLease({
identity: { runId: "test-run", sessionId: "test-session" },
warn,
});
const selection = {
host: { id: "agent-harness:test", label: "test harness", capabilities: [] },
operation: "agent-run" as const,
requiresDurableCommit: true,
};
const first = lease.selectForHost(selection);
lease.begin();
const second = lease.selectForHost(selection);
expect(second).toMatchObject({ registeredId: "legacy", mode: "configured" });
expect(second.engine).toBe(first.engine);
expect(lease.degraded).toBe(false);
expect(warn).not.toHaveBeenCalled();
await lease.dispose();
});
it("keeps repeated baseline transcript-host selection stable after the turn starts", async () => {
const warn = vi.fn();
const lease = await createContextEngineLogicalTurnLease({
identity: { runId: "test-run", sessionId: "test-session" },
warn,
});
const selection = {
lease,
host: { id: "agent-harness:test", label: "test harness", capabilities: [] },
operation: "agent-run" as const,
recorder: { getAdmissionReceipt: () => undefined, hasPersisted: () => true },
};
const first = selectContextEngineForTranscriptHost(selection);
lease.begin();
const second = selectContextEngineForTranscriptHost(selection);
expect(second).toMatchObject({ registeredId: "legacy", mode: "configured" });
expect(second.engine).toBe(first.engine);
expect(lease.degraded).toBe(false);
expect(warn).not.toHaveBeenCalled();
await lease.dispose();
});
it("rejects baseline transcript-host selection after disposal", async () => {
const lease = await createContextEngineLogicalTurnLease({
identity: { runId: "test-run", sessionId: "test-session" },
});
await lease.dispose();
expect(() =>
selectContextEngineForTranscriptHost({
lease,
host: { id: "agent-harness:test", label: "test harness", capabilities: [] },
operation: "agent-run",
recorder: { getAdmissionReceipt: () => undefined, hasPersisted: () => true },
}),
).toThrow("context-engine logical turn selection is already pinned");
});
it.each(["resolve", "reject"] as const)(
"disposes once after retained turn work settles with %s",
async (settlement) => {
const engineId = uniqueEngineId("logical-turn-retained-work");
const hold = createDeferred();
const disposed = createDeferred();
const engine = new MockContextEngine();
const dispose = vi.spyOn(engine, "dispose").mockImplementation(async () => {
disposed.resolve();
});
registerTestContextEngine(engineId, () => engine);
const lease = await createContextEngineLogicalTurnLease({
identity: { runId: "retained-run", sessionId: "retained-session" },
config: configWithSlot(engineId),
});
lease.deferDisposalUntil(hold.promise);
await lease.dispose();
await lease.dispose();
expect(dispose).not.toHaveBeenCalled();
expect(() => lease.begin()).toThrow("already disposed");
if (settlement === "reject") {
hold.reject(new Error("pending turn work failed"));
} else {
hold.resolve();
}
await disposed.promise;
await lease.dispose();
expect(dispose).toHaveBeenCalledOnce();
},
);
it.each([false, true])(
"bounds configured and fallback disposal in parallel (fast failure=%s)",
async (fastFailure) => {
const registry = await import("./registry.js");
const configured = new MockContextEngine();
const fallback = new MockContextEngine();
const configuredGate = createDeferred();
const fallbackGate = createDeferred();
const configuredDispose = vi.spyOn(configured, "dispose").mockImplementation(async () => {
if (fastFailure) {
throw new Error("configured engine disposal failed");
}
await configuredGate.promise;
});
const fallbackDispose = vi
.spyOn(fallback, "dispose")
.mockImplementation(() => fallbackGate.promise);
const resolve = vi.spyOn(registry, "resolveLogicalTurnContextEngines").mockResolvedValue({
configured: { engine: configured, registeredId: "configured" },
configuredId: "configured",
fallback: { engine: fallback, registeredId: "legacy" },
});
vi.useFakeTimers();
vi.stubEnv("OPENCLAW_AGENT_CLEANUP_TIMEOUT_MS", "25");
const scope = createAgentCleanupScope();
const lease = await createContextEngineLogicalTurnLease({
identity: { runId: "parallel-run", sessionId: "parallel-session" },
warn: vi.fn(),
});
let settled = false;
const cleanup = scope
.run(() => lease.dispose())
.then(() => {
settled = true;
});
try {
await vi.advanceTimersByTimeAsync(0);
expect(configuredDispose).toHaveBeenCalledOnce();
expect(fallbackDispose).toHaveBeenCalledOnce();
if (fastFailure) {
expect(scope.outcome).toBe("uncertain");
}
await vi.advanceTimersByTimeAsync(24);
expect(settled).toBe(false);
await vi.advanceTimersByTimeAsync(1);
expect(settled).toBe(true);
expect(scope.outcome).toBe("uncertain");
await lease.dispose();
expect(configuredDispose).toHaveBeenCalledOnce();
expect(fallbackDispose).toHaveBeenCalledOnce();
} finally {
configuredGate.resolve();
fallbackGate.resolve();
await cleanup;
vi.useRealTimers();
vi.unstubAllEnvs();
resolve.mockRestore();
}
},
);
it("still rejects an attempted custom-engine transition after the turn starts", async () => {
const engineId = uniqueEngineId("logical-turn-late-transition");
registerTestContextEngine(engineId, () => ({
info: {
id: engineId,
name: "Late Transition",
transcriptSemantics: {
currentTurnFence: "before-current-turn-entry-v1",
turnAdvancementIdempotency: "atomic-idempotent-v1",
},
},
...createPassthroughEngineMethods(),
async commitTurn() {
return { status: "committed" };
},
}));
const lease = await createContextEngineLogicalTurnLease({
identity: { runId: "test-run", sessionId: "test-session" },
config: configWithSlot(engineId),
});
lease.begin();
expect(() => lease.degradeBeforeStart("late transition")).toThrow(
"context-engine logical turn selection is already pinned",
);
await lease.dispose();
});
it("degrades and warns when an invalid configured id resolves to legacy", async () => {
const engineId = uniqueEngineId("logical-turn-invalid");
const warn = vi.fn();
const lease = await createContextEngineLogicalTurnLease({
identity: { runId: "test-run", sessionId: "test-session" },
config: configWithSlot(engineId),
warn,
});
expect(lease.effectiveEngineId).toBe("legacy");
expect(lease.degraded).toBe(true);
expect(lease.degradedReason).toBe(`context engine "${engineId}" is not registered`);
expect(warn).toHaveBeenCalledWith(
expect.stringContaining(`Context engine "${engineId}" degraded to "legacy"`),
);
await lease.dispose();
});
it("degrades and warns when configured engine discovery is read-only", async () => {
const engineId = uniqueEngineId("logical-turn-discovery");
registerContextEngineForOwner(engineId, () => new MockContextEngine(), `test:${engineId}`, {
lifecycle: "readOnlyDiscovery",
});
const warn = vi.fn();
const lease = await createContextEngineLogicalTurnLease({
identity: { runId: "test-run", sessionId: "test-session" },
config: configWithSlot(engineId),
warn,
});
expect(lease.effectiveEngineId).toBe("legacy");
expect(lease.degradedReason).toBe(
`context engine "${engineId}" is available for discovery only`,
);
expect(warn).toHaveBeenCalledOnce();
await lease.dispose();
});
it("degrades and warns when the configured engine factory fails", async () => {
const engineId = uniqueEngineId("logical-turn-factory");
registerTestContextEngine(engineId, () => {
throw new Error("factory unavailable");
});
const warn = vi.fn();
const lease = await createContextEngineLogicalTurnLease({
identity: { runId: "test-run", sessionId: "test-session" },
config: configWithSlot(engineId),
warn,
});
expect(lease.effectiveEngineId).toBe("legacy");
expect(lease.degradedReason).toBe("factory unavailable");
expect(warn).toHaveBeenCalledOnce();
await lease.dispose();
});
it("uses registered identity when custom engine metadata is also legacy", async () => {
const engineId = uniqueEngineId("logical-turn-legacy-alias");
registerTestContextEngine(engineId, () => ({
info: { id: "legacy", name: "Legacy Alias" },
...createPassthroughEngineMethods(),
}));
const warn = vi.fn();
const lease = await createContextEngineLogicalTurnLease({
identity: { runId: "test-run", sessionId: "test-session" },
config: configWithSlot(engineId),
warn,
});
const selected = selectContextEngineForTranscriptHost({
lease,
host: { id: "agent-harness:test", label: "test harness", capabilities: [] },
operation: "agent-run",
recorder: { getAdmissionReceipt: testAdmissionReceipt, hasPersisted: () => true },
});
expect(selected).toMatchObject({ registeredId: "legacy", mode: "legacy-degraded" });
expect(lease.degradedReason).toBe("current-turn transcript fencing is not declared");
expect(warn).toHaveBeenCalledWith(
expect.stringContaining(`Context engine "${engineId}" degraded to "legacy"`),
);
await lease.dispose();
});
it("does not replay a started engine operation and retries the configured engine next turn", async () => {
const engineId = uniqueEngineId("logical-turn-retry");
const assemble = vi
.fn<ContextEngine["assemble"]>()
.mockRejectedValueOnce(new Error("configured engine unavailable"))
.mockImplementation(async ({ messages }) => ({ messages, estimatedTokens: 0 }));
registerTestContextEngine(engineId, () => ({
info: { id: engineId, name: "Logical Turn Retry" },
async ingest() {
return { ingested: true };
},
assemble,
async compact() {
return { ok: true, compacted: false };
},
}));
const warn = vi.fn();
const first = await createContextEngineLogicalTurnLease({
identity: { runId: "test-run", sessionId: "test-session" },
config: configWithSlot(engineId),
warn,
});
const messages = [makeMockMessage()];
first.begin();
await expect(first.engine.assemble({ sessionId: "first", messages })).rejects.toThrow(
"configured engine unavailable",
);
expect(first.degraded).toBe(false);
expect(first.engine.info.id).toBe(engineId);
expect(assemble).toHaveBeenCalledTimes(1);
expect(warn).not.toHaveBeenCalled();
await first.dispose();
const second = await createContextEngineLogicalTurnLease({
identity: { runId: "test-run", sessionId: "test-session" },
config: configWithSlot(engineId),
warn,
});
await expect(second.engine.assemble({ sessionId: "second", messages })).resolves.toMatchObject({
messages,
});
expect(second.degraded).toBe(false);
expect(second.engine.info.id).toBe(engineId);
expect(assemble).toHaveBeenCalledTimes(2);
await second.dispose();
});
it("rejects an incompatible fallback host after the logical turn starts", async () => {
const engineId = uniqueEngineId("logical-turn-host-transition");
registerTestContextEngine(engineId, () => ({
info: {
id: engineId,
name: "Host Transition",
hostRequirements: {
"agent-run": { requiredCapabilities: ["thread-bootstrap-projection"] },
},
transcriptSemantics: {
currentTurnFence: "before-current-turn-entry-v1",
turnAdvancementIdempotency: "atomic-idempotent-v1",
},
},
...createPassthroughEngineMethods(),
async commitTurn() {
return { status: "committed" };
},
}));
const lease = await createContextEngineLogicalTurnLease({
identity: { runId: "test-run", sessionId: "test-session" },
config: configWithSlot(engineId),
});
lease.selectForHost({
host: {
id: "agent-harness:first",
label: 'agent harness "first"',
capabilities: ["thread-bootstrap-projection"],
},
operation: "agent-run",
requiresDurableCommit: true,
});
lease.begin();
expect(() =>
lease.selectForHost({
host: {
id: "agent-harness:fallback",
label: 'agent harness "fallback"',
capabilities: [],
},
operation: "agent-run",
requiresDurableCommit: true,
}),
).toThrow(
'context-engine logical turn cannot change to incompatible agent harness "fallback": host "agent-harness:fallback" is missing thread-bootstrap-projection',
);
expect(lease.engine.info.id).toBe(engineId);
await lease.dispose();
});
it.each([
{
label: "persisted without a receipt",
persisted: true,
declaresFence: true,
expectedEngine: "legacy",
expectedReason: "current-turn transcript admission receipt is unavailable",
},
{
label: "not yet persisted",
persisted: false,
declaresFence: true,
expectedEngine: "configured",
expectedReason: undefined,
},
{
label: "not yet persisted without declared fencing",
persisted: false,
declaresFence: false,
expectedEngine: "legacy",
expectedReason: "current-turn transcript fencing is not declared",
},
])("selects $expectedEngine for a turn $label", async (testCase) => {
const engineId = uniqueEngineId("logical-turn-recorder-state");
registerTestContextEngine(engineId, () => ({
info: {
id: engineId,
name: "Recorder State",
transcriptSemantics: {
...(testCase.declaresFence
? { currentTurnFence: "before-current-turn-entry-v1" as const }
: {}),
turnAdvancementIdempotency: "atomic-idempotent-v1",
},
},
...createPassthroughEngineMethods(),
async commitTurn() {
return { status: "committed" };
},
}));
const warn = vi.fn();
const lease = await createContextEngineLogicalTurnLease({
identity: { runId: "test-run", sessionId: "test-session" },
config: configWithSlot(engineId),
warn,
});
const selected = selectContextEngineForTranscriptHost({
lease,
host: { id: "agent-harness:test", label: "test harness", capabilities: [] },
operation: "agent-run",
recorder: {
getAdmissionReceipt: () => undefined,
hasPersisted: () => testCase.persisted,
},
});
lease.begin();
expect(selected.engine.info.id).toBe(
testCase.expectedEngine === "configured" ? engineId : "legacy",
);
expect(lease.degradedReason).toBe(testCase.expectedReason);
if (testCase.expectedReason) {
expect(warn).toHaveBeenCalledWith(expect.stringContaining(testCase.expectedReason));
} else {
expect(warn).not.toHaveBeenCalled();
}
await lease.dispose();
});
});
// ═══════════════════════════════════════════════════════════════════════════
// 3b. Factory context passing
// ═══════════════════════════════════════════════════════════════════════════
describe("Factory context passing", () => {
it("passes ContextEngineFactoryContext to factories that accept a parameter", async () => {
const engineId = `factory-ctx-${Date.now().toString(36)}`;
let receivedCtx: ContextEngineFactoryContext | undefined;
const factory: ContextEngineFactory = (ctx: ContextEngineFactoryContext) => {
receivedCtx = ctx;
return {
info: { id: engineId, name: "Ctx Engine" },
...createPassthroughEngineMethods(),
};
};
registerTestContextEngine(engineId, factory);
const cfg = configWithSlot(engineId);
await resolveContextEngine(cfg, {
agentDir: "/tmp/agent",
workspaceDir: "/tmp/workspace",
});
const context = requireFactoryContext(receivedCtx);
expect(context.config).toBe(cfg);
expect(context.agentDir).toBe("/tmp/agent");
expect(context.workspaceDir).toBe("/tmp/workspace");
});
it("no-arg factories still work when context is passed", async () => {
const engineId = `factory-noarg-${Date.now().toString(36)}`;
let called = false;
const factory: ContextEngineFactory = () => {
called = true;
return {
info: { id: engineId, name: "No-Arg Engine" },
...createPassthroughEngineMethods(),
};
};
registerTestContextEngine(engineId, factory);
const engine = await resolveContextEngine(configWithSlot(engineId), {
agentDir: "/tmp/agent",
workspaceDir: "/tmp/workspace",
});
expect(called).toBe(true);
expect(engine.info.id).toBe(engineId);
});
it("passes undefined config when resolveContextEngine is called without config", async () => {
let receivedCtx: ContextEngineFactoryContext | undefined;
// Override the default "legacy" engine to intercept the no-config path
registerContextEngineForOwner(
"legacy",
(ctx: ContextEngineFactoryContext) => {
receivedCtx = ctx;
return {
info: { id: "legacy", name: "NoConfig Engine", version: "1" },
...createPassthroughEngineMethods(),
};
},
"core",
{ allowSameOwnerRefresh: true },
);
await resolveContextEngine(undefined);
const context = requireFactoryContext(receivedCtx);
expect(context.config).toBeUndefined();
expect(context.agentDir).toBeUndefined();
expect(context.workspaceDir).toBeUndefined();
});
});
describe("Read-only plugin discovery registrations", () => {
beforeEach(() => {
registerLegacyContextEngine();
resetContextEngineRuntimeQuarantineForTests();
vi.spyOn(console, "warn").mockImplementation(() => {});
});
afterEach(() => {
vi.restoreAllMocks();
});
it("does not construct or quarantine read-only discovery context-engine factories", async () => {
const engineId = uniqueEngineId("lossless-readonly");
const owner = "plugin:lossless-claw";
let readOnlyFactoryCalls = 0;
let runtimeFactoryCalls = 0;
registerContextEngineForOwner(
engineId,
() => {
readOnlyFactoryCalls += 1;
throw new Error("Engine initialization is disabled during read-only plugin registration");
},
owner,
{ allowSameOwnerRefresh: true, lifecycle: "readOnlyDiscovery" },
);
const discoveryFallback = await resolveContextEngine(configWithSlot(engineId));
expect(discoveryFallback.info.id).toBe("legacy");
expect(readOnlyFactoryCalls).toBe(0);
expect(listContextEngineQuarantines().some((entry) => entry.engineId === engineId)).toBe(false);
expect(console.warn).toHaveBeenCalledWith(
`[context-engine] Context engine "${engineId}" owner=${owner} is registered for read-only discovery only; falling back to default engine "legacy" without quarantine until runtime activation registers it.`,
);
registerContextEngineForOwner(
engineId,
() => {
runtimeFactoryCalls += 1;
return {
info: { id: "lossless-claw", name: "Lossless Claw" },
...createPassthroughEngineMethods(),
} satisfies ContextEngine;
},
owner,
{ allowSameOwnerRefresh: true, lifecycle: "runtime" },
);
const runtimeEngine = await resolveContextEngine(configWithSlot(engineId));
expect(runtimeEngine.info.id).toBe("lossless-claw");
expect(readOnlyFactoryCalls).toBe(0);
expect(runtimeFactoryCalls).toBe(1);
expect(listContextEngineQuarantines().some((entry) => entry.engineId === engineId)).toBe(false);
registerContextEngineForOwner(
engineId,
() => {
readOnlyFactoryCalls += 1;
throw new Error("read-only discovery should not replace runtime registration");
},
owner,
{ allowSameOwnerRefresh: true, lifecycle: "readOnlyDiscovery" },
);
const stillRuntimeEngine = await resolveContextEngine(configWithSlot(engineId));
expect(stillRuntimeEngine.info.id).toBe("lossless-claw");
expect(readOnlyFactoryCalls).toBe(0);
expect(runtimeFactoryCalls).toBe(2);
});
});
// ═══════════════════════════════════════════════════════════════════════════
// 4. Invalid engine fallback
// ═══════════════════════════════════════════════════════════════════════════
describe("Invalid engine fallback", () => {
beforeEach(() => {
registerLegacyContextEngine();
resetContextEngineRuntimeQuarantineForTests();
vi.spyOn(console, "error").mockImplementation(() => {});
});
afterEach(() => {
vi.restoreAllMocks();
});
it("falls back to default engine for missing or invalid requested engines", async () => {
const cases = [
{
name: "missing registration",
engineId: uniqueEngineId("does-not-exist"),
register: () => undefined,
expectedError: (engineId: string) =>
`[context-engine] Context engine "${engineId}" failed during resolve: not registered; quarantining it for this process and falling back to default engine "legacy".`,
},
{
name: "factory throws",
engineId: uniqueEngineId("factory-throw"),
register: (engineId: string) => {
registerTestContextEngine(engineId, () => {
throw new Error("plugin version mismatch");
});
},
expectedError: (engineId: string) =>
`[context-engine] Context engine "${engineId}" owner=test:${engineId} failed during factory: plugin version mismatch; quarantining it for this process and falling back to default engine "legacy".`,
},
{
name: "missing info metadata",
engineId: uniqueEngineId("invalid-info"),
register: (engineId: string) => {
registerTestContextEngine(
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,
);
},
expectedError: (engineId: string) =>
`[context-engine] Context engine "${engineId}" owner=test:${engineId} failed during contract-validation: Context engine "${engineId}" factory returned an invalid ContextEngine: missing info.; quarantining it for this process and falling back to default engine "legacy".`,
},
{
name: "missing lifecycle methods",
engineId: uniqueEngineId("invalid-methods"),
register: (engineId: string) => {
registerTestContextEngine(
engineId,
() =>
({
info: { id: engineId, name: "Broken Engine" },
async ingest() {
return { ingested: false };
},
}) as unknown as ContextEngine,
);
},
expectedError: (engineId: string) =>
`[context-engine] Context engine "${engineId}" owner=test:${engineId} failed during contract-validation: Context engine "${engineId}" factory returned an invalid ContextEngine: missing assemble(), missing compact().; quarantining it for this process and falling back to default engine "legacy".`,
},
{
name: "contract validation throws",
engineId: uniqueEngineId("validation-throw"),
register: (engineId: string) => {
registerTestContextEngine(engineId, () => 42n as unknown as ContextEngine);
},
expectedError: (engineId: string) =>
new RegExp(
`^\\[context-engine\\] Context engine "${escapeRegExp(engineId)}" owner=test:${escapeRegExp(engineId)} failed during contract-validation: .*BigInt.*; quarantining it for this process and falling back to default engine "legacy"\\.$`,
),
},
] as const;
for (const testCase of cases) {
vi.mocked(console.error).mockClear();
testCase.register(testCase.engineId);
const engine = await resolveContextEngine(configWithSlot(testCase.engineId));
expect(engine.info.id, testCase.name).toBe("legacy");
const expectedError = testCase.expectedError(testCase.engineId);
expect(console.error, testCase.name).toHaveBeenCalledWith(
typeof expectedError === "string" ? expectedError : expect.stringMatching(expectedError),
);
expect(
listContextEngineQuarantines().some((entry) => entry.engineId === testCase.engineId),
).toBe(true);
}
});
it("quarantines a selected engine after lifecycle failure and resolves legacy next time", async () => {
const engineId = uniqueEngineId("runtime-fail");
const assemble = vi.fn(async () => {
throw new Error("lcm db is corrupt");
});
let factoryCalls = 0;
registerTestContextEngine(engineId, () => {
factoryCalls += 1;
return {
info: { id: "lcm", name: "Lossless Context Manager" },
async ingest() {
return { ingested: true };
},
assemble,
async compact() {
return { ok: true, compacted: false };
},
};
});
const engine = await resolveContextEngine(configWithSlot(engineId));
const message = makeMockMessage("user", "hello");
const result = await engine.assemble({
sessionId: "s1",
messages: [message],
});
const nextEngine = await resolveContextEngine(configWithSlot(engineId));
expect(result.messages).toEqual([message]);
expect(nextEngine.info.id).toBe("legacy");
expect(factoryCalls).toBe(1);
expect(assemble).toHaveBeenCalledTimes(1);
expect(listContextEngineQuarantines()).toEqual([
expect.objectContaining({
engineId,
owner: `test:${engineId}`,
operation: "assemble",
reason: "lcm db is corrupt",
}),
]);
expect(console.error).toHaveBeenCalledWith(
`[context-engine] Context engine "${engineId}" owner=test:${engineId} failed during assemble: lcm db is corrupt; quarantining it for this process and falling back to default engine "legacy".`,
);
});
it("coalesces fallback initialization across concurrent lifecycle failures", async () => {
const defaultFactory = vi.fn(async () => new LegacyContextEngine());
registerContextEngineForOwner("legacy", defaultFactory, "core", {
allowSameOwnerRefresh: true,
});
const engineId = uniqueEngineId("concurrent-runtime-fail");
const assemble = vi.fn(async () => {
await Promise.resolve();
throw new Error("plugin context unavailable");
});
registerTestContextEngine(engineId, () => ({
info: { id: engineId, name: "Concurrent Context Engine" },
async ingest() {
return { ingested: true };
},
assemble,
async compact() {
return { ok: true, compacted: false };
},
}));
const engine = await resolveContextEngine(configWithSlot(engineId));
const messages = [makeMockMessage("user", "first"), makeMockMessage("user", "second")];
const results = await Promise.all(
messages.map((message, index) =>
engine.assemble({ sessionId: `session-${index}`, messages: [message] }),
),
);
expect(results.map(({ messages: assembled }) => assembled)).toEqual(
messages.map((message) => [message]),
);
expect(assemble).toHaveBeenCalledTimes(2);
expect(defaultFactory).toHaveBeenCalledTimes(1);
expect(listContextEngineQuarantines()).toEqual([
expect.objectContaining({ engineId, operation: "assemble" }),
]);
});
it("exposes fallback metadata on the same engine after lifecycle quarantine", async () => {
const engineId = uniqueEngineId("runtime-fail-metadata");
const assemble = vi.fn(async () => {
throw new Error("plugin store unavailable");
});
registerContextEngineForOwner(
engineId,
() => ({
info: {
id: "lcm",
name: "Lossless Context Manager",
ownsCompaction: true,
},
async ingest() {
return { ingested: true };
},
assemble,
async compact() {
return { ok: true, compacted: false };
},
}),
"plugin:lossless-claw",
{ allowSameOwnerRefresh: true },
);
const engine = await resolveContextEngine(configWithSlot(engineId));
expect(engine.info.ownsCompaction).toBe(true);
expect(resolveContextEngineOwnerPluginId(engine)).toBe("lossless-claw");
const message = makeMockMessage("user", "hello");
const result = await engine.assemble({
sessionId: "s1",
messages: [message],
});
expect(result.messages).toEqual([message]);
expect(engine.info.id).toBe("legacy");
expect(engine.info.ownsCompaction).toBeUndefined();
expect(resolveContextEngineOwnerPluginId(engine)).toBeUndefined();
expect(isRuntimeCompactionDelegate(Reflect.get(engine, "compact", engine))).toBe(true);
expect(assemble).toHaveBeenCalledTimes(1);
});
it("routes legacy resolver fence failures through normal quarantine", async () => {
const engineId = uniqueEngineId("transcript-fence-fallback");
const ingest = vi.fn(async () => ({ ingested: true }));
const assemble = vi.fn(async () => {
throw new SessionTranscriptReadFenceError("admitted user row is unavailable");
});
registerContextEngineForOwner(
engineId,
() => ({
info: {
id: "lcm",
name: "Lossless Context Manager",
ownsCompaction: true,
},
ingest,
assemble,
async compact() {
return { ok: true, compacted: false };
},
}),
"plugin:lossless-claw",
{ allowSameOwnerRefresh: true },
);
const engine = await resolveContextEngine(configWithSlot(engineId));
expect(resolveContextEngineOwnerPluginId(engine)).toBe("lossless-claw");
const first = makeMockMessage("user", "first");
const second = makeMockMessage("user", "second");
await expect(engine.assemble({ sessionId: "s1", messages: [first] })).resolves.toMatchObject({
messages: [first],
});
await expect(engine.assemble({ sessionId: "s1", messages: [second] })).resolves.toMatchObject({
messages: [second],
});
await engine.ingest({ sessionId: "s1", message: second });
expect(engine.info.id).toBe("legacy");
expect(resolveContextEngineOwnerPluginId(engine)).toBeUndefined();
expect(listContextEngineQuarantines()).toEqual([
expect.objectContaining({
engineId,
operation: "assemble",
reason: "admitted user row is unavailable",
}),
]);
expect(assemble).toHaveBeenCalledTimes(1);
expect(ingest).not.toHaveBeenCalled();
});
it("quarantines compact failures without same-call legacy fallback", async () => {
const engineId = uniqueEngineId("runtime-fail-compact");
const compact = vi.fn(async () => {
throw new Error("plugin compaction failed");
});
registerContextEngineForOwner(
engineId,
() => ({
info: {
id: "lcm",
name: "Lossless Context Manager",
ownsCompaction: true,
},
async ingest() {
return { ingested: true };
},
async assemble({ messages }: { messages: AgentMessage[] }) {
return { messages, estimatedTokens: 0 };
},
compact,
}),
"plugin:lossless-claw",
{ allowSameOwnerRefresh: true },
);
const engine = await resolveContextEngine(configWithSlot(engineId));
await expect(
engine.compact({
sessionId: "s1",
sessionKey: "agent:main:s1",
}),
).rejects.toThrow("plugin compaction failed");
expect(engine.info.id).toBe("legacy");
expect(engine.info.ownsCompaction).toBeUndefined();
expect(resolveContextEngineOwnerPluginId(engine)).toBeUndefined();
expect(isRuntimeCompactionDelegate(Reflect.get(engine, "compact", engine))).toBe(true);
expect(compact).toHaveBeenCalledTimes(1);
});
it("clears a missing-engine quarantine when the plugin registers later", async () => {
const engineId = uniqueEngineId("late-register");
const missingEngine = await resolveContextEngine(configWithSlot(engineId));
expect(missingEngine.info.id).toBe("legacy");
expect(listContextEngineQuarantines()).toEqual([
expect.objectContaining({
engineId,
operation: "resolve",
reason: "not registered",
}),
]);
registerTestContextEngine(engineId, () => ({
info: { id: engineId, name: "Late Registered Engine" },
...createPassthroughEngineMethods(),
}));
const registeredEngine = await resolveContextEngine(configWithSlot(engineId));
expect(listContextEngineQuarantines()).toEqual([]);
expect(registeredEngine.info.id).toBe(engineId);
});
it("defers quarantine clearing for builder-context direct registrations", async () => {
const engineId = uniqueEngineId("builder-register");
await resolveContextEngine(configWithSlot(engineId));
const builder = createEmptyPluginRegistry();
withPluginRegistrationContext(builder, "context-builder", () => {
registerContextEngineForOwner(
engineId,
() => new MockContextEngine(),
"plugin:context-builder",
{ allowSameOwnerRefresh: true },
);
});
expect(builder.contextEngines.has(engineId)).toBe(true);
expect(getContextEngineRegistration(engineId)).toBeUndefined();
expect(listContextEngineQuarantines()).toEqual([
expect.objectContaining({ engineId, reason: "not registered" }),
]);
setActivePluginRegistry(builder);
activateContextEngineRegistrations(builder);
expect(listContextEngineQuarantines()).toEqual([]);
});
it("does not quarantine causal abort rejections from lifecycle methods", async () => {
const engineId = uniqueEngineId("abort-rejection");
const compactAbortReason = new Error("user stopped compaction");
const compactAbortError = new Error("compaction aborted", { cause: compactAbortReason });
compactAbortError.name = "AbortError";
const compactController = new AbortController();
const maintainAbortReason = new Error("gateway shutdown");
const maintainAbortError = new Error("maintenance cancelled", {
cause: maintainAbortReason,
});
maintainAbortError.name = "AbortError";
const maintainController = new AbortController();
registerTestContextEngine(engineId, () => ({
info: { id: engineId, name: "Abort Aware Engine" },
async ingest() {
return { ingested: true };
},
async assemble({ messages }: { messages: AgentMessage[] }) {
return { messages, estimatedTokens: 0 };
},
async compact() {
compactController.abort(compactAbortReason);
throw compactAbortError;
},
async maintain() {
maintainController.abort(maintainAbortReason);
throw maintainAbortError;
},
}));
const engine = await resolveContextEngine(configWithSlot(engineId));
await expect(
engine.compact({
sessionId: "s1",
sessionKey: "agent:main:s1",
abortSignal: compactController.signal,
}),
).rejects.toThrow("compaction aborted");
await expect(
engine.maintain?.({
sessionId: "s1",
sessionFile: "/tmp/s1.jsonl",
abortSignal: maintainController.signal,
}),
).rejects.toThrow("maintenance cancelled");
const nextEngine = await resolveContextEngine(configWithSlot(engineId));
expect(nextEngine.info.id).toBe(engineId);
expect(listContextEngineQuarantines()).toEqual([]);
expect(console.error).not.toHaveBeenCalled();
});
it("does not invoke guarded maintenance for an already-aborted signal", async () => {
const engineId = uniqueEngineId("maintain-pre-abort");
const maintain = vi.fn(async () => ({
changed: false,
bytesFreed: 0,
rewrittenEntries: 0,
}));
registerTestContextEngine(engineId, () => ({
info: { id: engineId, name: "Pre-Abort Engine" },
...createPassthroughEngineMethods(),
maintain,
}));
const controller = new AbortController();
const reason = new Error("shutdown already requested");
controller.abort(reason);
const engine = await resolveContextEngine(configWithSlot(engineId));
await expect(
engine.maintain?.({
sessionId: "s1",
sessionFile: "/tmp/s1.jsonl",
abortSignal: controller.signal,
}),
).rejects.toBe(reason);
expect(maintain).not.toHaveBeenCalled();
expect((await resolveContextEngine(configWithSlot(engineId))).info.id).toBe(engineId);
expect(listContextEngineQuarantines()).toEqual([]);
});
it("does not quarantine standard AbortError from aborted maintenance", async () => {
const engineId = uniqueEngineId("maintain-standard-abort");
const controller = new AbortController();
const abortError = new Error("This operation was aborted");
abortError.name = "AbortError";
let observedSignal: AbortSignal | undefined;
registerTestContextEngine(engineId, () => ({
info: { id: engineId, name: "Standard Abort Engine" },
...createPassthroughEngineMethods(),
async maintain({ abortSignal }) {
observedSignal = abortSignal;
await new Promise<void>((_resolve, reject) => {
abortSignal?.addEventListener("abort", () => reject(abortError), { once: true });
});
return { changed: false, bytesFreed: 0, rewrittenEntries: 0 };
},
}));
const engine = await resolveContextEngine(configWithSlot(engineId));
const maintenance = engine.maintain?.({
sessionId: "s1",
sessionFile: "/tmp/s1.jsonl",
abortSignal: controller.signal,
});
await vi.waitFor(() => expect(observedSignal).toBe(controller.signal));
controller.abort(new Error("gateway shutdown"));
await expect(maintenance).rejects.toBe(abortError);
expect((await resolveContextEngine(configWithSlot(engineId))).info.id).toBe(engineId);
expect(listContextEngineQuarantines()).toEqual([]);
});
it("quarantines standard AbortError when maintenance was not aborted", async () => {
const engineId = uniqueEngineId("maintain-unrelated-standard-abort");
const controller = new AbortController();
const abortError = new Error("This operation was aborted");
abortError.name = "AbortError";
registerTestContextEngine(engineId, () => ({
info: { id: engineId, name: "Unrelated Standard Abort Engine" },
...createPassthroughEngineMethods(),
async maintain() {
throw abortError;
},
}));
const engine = await resolveContextEngine(configWithSlot(engineId));
await expect(
engine.maintain?.({
sessionId: "s1",
sessionFile: "/tmp/s1.jsonl",
abortSignal: controller.signal,
}),
).resolves.toMatchObject({ changed: false });
expect(controller.signal.aborted).toBe(false);
expect((await resolveContextEngine(configWithSlot(engineId))).info.id).toBe("legacy");
expect(listContextEngineQuarantines()).toEqual([
expect.objectContaining({
engineId,
operation: "maintain",
reason: "This operation was aborted",
}),
]);
});
it("quarantines subagent preparation failures while failing the active spawn closed", async () => {
const engineId = uniqueEngineId("prepare-subagent-fail");
registerTestContextEngine(engineId, () => ({
info: { id: engineId, name: "Spawn Aware Engine" },
...createPassthroughEngineMethods(),
async prepareSubagentSpawn() {
throw new Error("child context projection failed");
},
}));
const engine = await resolveContextEngine(configWithSlot(engineId));
await expect(
engine.prepareSubagentSpawn?.({
parentSessionKey: "agent:main",
childSessionKey: "agent:child",
contextMode: "isolated",
}),
).rejects.toThrow("child context projection failed");
const nextEngine = await resolveContextEngine(configWithSlot(engineId));
expect(nextEngine.info.id).toBe("legacy");
expect(listContextEngineQuarantines()).toEqual([
expect.objectContaining({
engineId,
operation: "prepareSubagentSpawn",
reason: "child context projection failed",
}),
]);
});
it("throws when the default engine itself is not registered", async () => {
const registryState = requireRegistryState();
const snapshot = new Map(registryState.engines);
registryState.engines.clear();
try {
await expect(resolveContextEngine()).rejects.toThrow("not registered");
} finally {
for (const [key, value] of snapshot) {
registryState.engines.set(key, value);
}
}
});
it("propagates error when default engine factory throws", async () => {
// Override the default "legacy" engine with a throwing factory via the
// core-owner path so the registration is accepted.
registerContextEngineForOwner(
"legacy",
() => {
throw new Error("default engine init failed");
},
"core",
{ allowSameOwnerRefresh: true },
);
await expect(resolveContextEngine()).rejects.toThrow("default engine init failed");
});
it("propagates error when default engine fails contract validation", async () => {
registerContextEngineForOwner(
"legacy",
() => ({ broken: true }) as unknown as ContextEngine,
"core",
{ allowSameOwnerRefresh: true },
);
await expect(resolveContextEngine()).rejects.toThrow(
'Context engine "legacy" factory returned an invalid ContextEngine',
);
});
it("accepts resolved engines whose info.id differs from the registered slot id (#66601)", async () => {
// Regression for openclaw/openclaw#66601: third-party plugins like
// lossless-claw register under an external slot id ("lossless-claw") but
// the ContextEngine they return uses the plugin's own internal id
// (e.g. "lcm"). That id is metadata, not the lookup key.
const engineId = `plugin-slot-${Date.now().toString(36)}`;
const internalInfoId = "lcm";
registerTestContextEngine(
engineId,
() =>
({
info: { id: internalInfoId, name: "Lossless Context Manager", version: "0.5.2" },
...createPassthroughEngineMethods(),
}) as unknown as ContextEngine,
);
const engine = await resolveContextEngine(configWithSlot(engineId));
// The engine's own info.id is preserved; resolution does not overwrite it.
expect(engine.info.id).toBe(internalInfoId);
expect(engine.info.name).toBe("Lossless Context Manager");
// And the engine is usable through the wrapper.
const result = await engine.assemble({
sessionId: "s1",
messages: [makeMockMessage("user", "hello")],
});
expect(result.estimatedTokens).toBe(0);
});
});
// ═══════════════════════════════════════════════════════════════════════════
// 5. LegacyContextEngine parity
// ═══════════════════════════════════════════════════════════════════════════
describe("LegacyContextEngine parity", () => {
it("ingest() returns { ingested: false } (no-op)", async () => {
const engine = new LegacyContextEngine();
const result = await engine.ingest({
sessionId: "s1",
message: makeMockMessage(),
});
expect(result).toEqual({ ingested: false });
});
it("assemble() returns messages as-is (pass-through)", async () => {
const engine = new LegacyContextEngine();
const messages = [
makeMockMessage("user", "first"),
makeMockMessage("assistant", "second"),
makeMockMessage("user", "third"),
];
const result = await engine.assemble({
sessionId: "s1",
messages,
});
// Messages should be the exact same array reference (pass-through)
expect(result.messages).toBe(messages);
expect(result.messages).toHaveLength(3);
expect(result.estimatedTokens).toBe(0);
expect(result.systemPromptAddition).toBeUndefined();
});
});
// ═══════════════════════════════════════════════════════════════════════════
// 5b. assemble() prompt forwarding
// ═══════════════════════════════════════════════════════════════════════════
describe("assemble() prompt forwarding", () => {
it("forwards prompt only when callers provide one", async () => {
const cases = [
{
name: "provided",
params: { prompt: "hello" },
expectedPrompt: "hello",
},
{
name: "omitted",
params: {},
expectedPrompt: null,
},
] as const;
for (const testCase of cases) {
const engineId = uniqueEngineId(`prompt-${testCase.name.replace(/\s+/g, "-")}`);
const calls = registerPromptTrackingEngine(engineId);
const engine = await resolveContextEngine(configWithSlot(engineId));
await engine.assemble({
sessionId: "s1",
messages: [makeMockMessage("user", "hello")],
...testCase.params,
});
expect(calls, testCase.name).toHaveLength(1);
if (testCase.expectedPrompt === null) {
expect(calls[0], testCase.name).not.toHaveProperty("prompt");
} else {
expect(calls[0], testCase.name).toHaveProperty("prompt", testCase.expectedPrompt);
}
}
});
});
// ═══════════════════════════════════════════════════════════════════════════
// 6. Initialization guard
// ═══════════════════════════════════════════════════════════════════════════
describe("Initialization guard", () => {
it("ensureContextEnginesInitialized() is idempotent and registers legacy", async () => {
const { ensureContextEnginesInitialized } = await import("./init.js");
expect(ensureContextEnginesInitialized()).toBeUndefined();
expect(ensureContextEnginesInitialized()).toBeUndefined();
expect(getContextEngineRegistration("legacy")).toBeDefined();
});
});
// ═══════════════════════════════════════════════════════════════════════════
// 7. Bundle chunk isolation (#40096)
//
// Published builds may split the context-engine registry across multiple
// output chunks. The Symbol.for() keyed global ensures that a plugin
// registering an owned engine from chunk A is visible to
// resolveContextEngine() imported from chunk B.
//
// These tests exercise the invariant that failed in 2026.3.7 when
// lossless-claw registered successfully but resolution could not find it.
// ═══════════════════════════════════════════════════════════════════════════
describe("Bundle chunk isolation (#40096)", () => {
it("shares registrations and keeps concurrent chunk registration visible", async () => {
const ts = Date.now().toString(36);
const registryUrl = new URL("./registry.ts", import.meta.url).href;
const dynamicChunk = await import(/* @vite-ignore */ `${registryUrl}?chunk=${ts}-dynamic`);
const chunks = [
{
registerContextEngineForOwner,
getContextEngineRegistration,
resolveContextEngine,
},
dynamicChunk,
];
const engineId = `cross-chunk-${ts}`;
const factory = () => ({
info: { id: engineId, name: "Cross-chunk Engine", version: "0.0.1" },
...createPassthroughEngineMethods(),
});
chunks[0].registerContextEngineForOwner(engineId, factory, `test:${engineId}`);
expect(chunks[1].getContextEngineRegistration(engineId)?.factory).toBe(factory);
const engine = await chunks[1].resolveContextEngine(configWithSlot(engineId));
expect(engine.info.id).toBe(engineId);
const ids = chunks.map((_, i) => `concurrent-${ts}-${i}`);
const registrationTasks = chunks.map((chunk, i) =>
Promise.resolve().then(() => {
const id = `concurrent-${ts}-${i}`;
chunk.registerContextEngineForOwner(id, () => new MockContextEngine(), `test:${id}`);
}),
);
await Promise.all(registrationTasks);
for (const id of ids) {
expect(chunks[0].getContextEngineRegistration(id)).toBeDefined();
}
});
});
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */