mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-09 00:01:17 +00:00
282 lines
8.6 KiB
TypeScript
282 lines
8.6 KiB
TypeScript
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
type AttemptContextEngine,
|
|
assembleAttemptContextEngine,
|
|
finalizeAttemptContextEngineTurn,
|
|
runAttemptContextEngineBootstrap,
|
|
} from "./attempt.context-engine-helpers.js";
|
|
import {
|
|
cleanupTempPaths,
|
|
createContextEngineBootstrapAndAssemble,
|
|
createContextEngineAttemptRunner,
|
|
expectCalledWithSessionKey,
|
|
getHoisted,
|
|
resetEmbeddedAttemptHarness,
|
|
} from "./attempt.spawn-workspace.test-support.js";
|
|
|
|
const hoisted = getHoisted();
|
|
const embeddedSessionId = "embedded-session";
|
|
const sessionFile = "/tmp/session.jsonl";
|
|
const seedMessage = { role: "user", content: "seed", timestamp: 1 } as AgentMessage;
|
|
const doneMessage = { role: "assistant", content: "done", timestamp: 2 } as unknown as AgentMessage;
|
|
|
|
function createTestContextEngine(params: Partial<AttemptContextEngine>): AttemptContextEngine {
|
|
return {
|
|
info: {
|
|
id: "test-context-engine",
|
|
name: "Test Context Engine",
|
|
version: "0.0.1",
|
|
},
|
|
ingest: async () => ({ ingested: true }),
|
|
compact: async () => ({
|
|
ok: false,
|
|
compacted: false,
|
|
reason: "not used in this test",
|
|
}),
|
|
...params,
|
|
} as AttemptContextEngine;
|
|
}
|
|
|
|
async function runBootstrap(
|
|
sessionKey: string,
|
|
contextEngine: AttemptContextEngine,
|
|
overrides: Partial<Parameters<typeof runAttemptContextEngineBootstrap>[0]> = {},
|
|
) {
|
|
await runAttemptContextEngineBootstrap({
|
|
hadSessionFile: true,
|
|
contextEngine,
|
|
sessionId: embeddedSessionId,
|
|
sessionKey,
|
|
sessionFile,
|
|
sessionManager: hoisted.sessionManager,
|
|
runtimeContext: {},
|
|
runMaintenance: hoisted.runContextEngineMaintenanceMock,
|
|
warn: () => {},
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
async function runAssemble(
|
|
sessionKey: string,
|
|
contextEngine: AttemptContextEngine,
|
|
overrides: Partial<Parameters<typeof assembleAttemptContextEngine>[0]> = {},
|
|
) {
|
|
await assembleAttemptContextEngine({
|
|
contextEngine,
|
|
sessionId: embeddedSessionId,
|
|
sessionKey,
|
|
messages: [seedMessage],
|
|
tokenBudget: 2048,
|
|
modelId: "gpt-test",
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
async function finalizeTurn(
|
|
sessionKey: string,
|
|
contextEngine: AttemptContextEngine,
|
|
overrides: Partial<Parameters<typeof finalizeAttemptContextEngineTurn>[0]> = {},
|
|
) {
|
|
await finalizeAttemptContextEngineTurn({
|
|
contextEngine,
|
|
promptError: false,
|
|
aborted: false,
|
|
yieldAborted: false,
|
|
sessionIdUsed: embeddedSessionId,
|
|
sessionKey,
|
|
sessionFile,
|
|
messagesSnapshot: [doneMessage],
|
|
prePromptMessageCount: 0,
|
|
tokenBudget: 2048,
|
|
runtimeContext: {},
|
|
runMaintenance: hoisted.runContextEngineMaintenanceMock,
|
|
sessionManager: hoisted.sessionManager,
|
|
warn: () => {},
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
describe("runEmbeddedAttempt context engine sessionKey forwarding", () => {
|
|
const sessionKey = "agent:main:discord:channel:test-ctx-engine";
|
|
const tempPaths: string[] = [];
|
|
|
|
beforeEach(() => {
|
|
resetEmbeddedAttemptHarness();
|
|
hoisted.runContextEngineMaintenanceMock.mockReset().mockResolvedValue(undefined);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await cleanupTempPaths(tempPaths);
|
|
});
|
|
|
|
it("forwards sessionKey to bootstrap, assemble, and afterTurn", async () => {
|
|
const { bootstrap, assemble } = createContextEngineBootstrapAndAssemble();
|
|
const afterTurn = vi.fn(async (_params: { sessionKey?: string }) => {});
|
|
const contextEngine = createTestContextEngine({
|
|
bootstrap,
|
|
assemble,
|
|
afterTurn,
|
|
});
|
|
|
|
await runBootstrap(sessionKey, contextEngine);
|
|
await runAssemble(sessionKey, contextEngine);
|
|
await finalizeTurn(sessionKey, contextEngine);
|
|
|
|
expectCalledWithSessionKey(bootstrap, sessionKey);
|
|
expectCalledWithSessionKey(assemble, sessionKey);
|
|
expectCalledWithSessionKey(afterTurn, sessionKey);
|
|
});
|
|
|
|
it("forwards modelId to assemble", async () => {
|
|
const { bootstrap, assemble } = createContextEngineBootstrapAndAssemble();
|
|
const contextEngine = createTestContextEngine({ bootstrap, assemble });
|
|
|
|
await runBootstrap(sessionKey, contextEngine);
|
|
await runAssemble(sessionKey, contextEngine);
|
|
|
|
expect(assemble).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
model: "gpt-test",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("forwards sessionKey to ingestBatch when afterTurn is absent", async () => {
|
|
const { bootstrap, assemble } = createContextEngineBootstrapAndAssemble();
|
|
const ingestBatch = vi.fn(
|
|
async (_params: { sessionKey?: string; messages: AgentMessage[] }) => ({ ingestedCount: 1 }),
|
|
);
|
|
|
|
await finalizeTurn(sessionKey, createTestContextEngine({ bootstrap, assemble, ingestBatch }), {
|
|
messagesSnapshot: [seedMessage, doneMessage],
|
|
prePromptMessageCount: 1,
|
|
});
|
|
|
|
expectCalledWithSessionKey(ingestBatch, sessionKey);
|
|
});
|
|
|
|
it("forwards sessionKey to per-message ingest when ingestBatch is absent", async () => {
|
|
const { bootstrap, assemble } = createContextEngineBootstrapAndAssemble();
|
|
const ingest = vi.fn(async (_params: { sessionKey?: string; message: AgentMessage }) => ({
|
|
ingested: true,
|
|
}));
|
|
|
|
await finalizeTurn(sessionKey, createTestContextEngine({ bootstrap, assemble, ingest }), {
|
|
messagesSnapshot: [seedMessage, doneMessage],
|
|
prePromptMessageCount: 1,
|
|
});
|
|
|
|
expect(ingest).toHaveBeenCalled();
|
|
expect(
|
|
ingest.mock.calls.every((call) => {
|
|
const params = call[0];
|
|
return params.sessionKey === sessionKey;
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("forwards silentExpected to the embedded subscription", async () => {
|
|
const { bootstrap, assemble } = createContextEngineBootstrapAndAssemble();
|
|
|
|
const result = await createContextEngineAttemptRunner({
|
|
contextEngine: {
|
|
bootstrap,
|
|
assemble,
|
|
},
|
|
attemptOverrides: {
|
|
silentExpected: true,
|
|
},
|
|
sessionKey,
|
|
tempPaths,
|
|
});
|
|
|
|
expect(result.promptError).toBeNull();
|
|
expect(hoisted.subscribeEmbeddedPiSessionMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
silentExpected: true,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("skips maintenance when afterTurn fails", async () => {
|
|
const { bootstrap, assemble } = createContextEngineBootstrapAndAssemble();
|
|
const afterTurn = vi.fn(async () => {
|
|
throw new Error("afterTurn failed");
|
|
});
|
|
|
|
await finalizeTurn(sessionKey, createTestContextEngine({ bootstrap, assemble, afterTurn }));
|
|
|
|
expect(afterTurn).toHaveBeenCalled();
|
|
expect(hoisted.runContextEngineMaintenanceMock).not.toHaveBeenCalledWith(
|
|
expect.objectContaining({ reason: "turn" }),
|
|
);
|
|
});
|
|
|
|
it("runs startup maintenance for existing sessions even without bootstrap()", async () => {
|
|
const { assemble } = createContextEngineBootstrapAndAssemble();
|
|
|
|
await runBootstrap(
|
|
sessionKey,
|
|
createTestContextEngine({
|
|
assemble,
|
|
maintain: async () => ({
|
|
changed: false,
|
|
bytesFreed: 0,
|
|
rewrittenEntries: 0,
|
|
reason: "test maintenance",
|
|
}),
|
|
}),
|
|
);
|
|
|
|
expect(hoisted.runContextEngineMaintenanceMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({ reason: "bootstrap" }),
|
|
);
|
|
});
|
|
|
|
it("skips maintenance when ingestBatch fails", async () => {
|
|
const { bootstrap, assemble } = createContextEngineBootstrapAndAssemble();
|
|
const ingestBatch = vi.fn(async () => {
|
|
throw new Error("ingestBatch failed");
|
|
});
|
|
|
|
await finalizeTurn(sessionKey, createTestContextEngine({ bootstrap, assemble, ingestBatch }), {
|
|
messagesSnapshot: [seedMessage, doneMessage],
|
|
prePromptMessageCount: 1,
|
|
});
|
|
|
|
expect(ingestBatch).toHaveBeenCalled();
|
|
expect(hoisted.runContextEngineMaintenanceMock).not.toHaveBeenCalledWith(
|
|
expect.objectContaining({ reason: "turn" }),
|
|
);
|
|
});
|
|
|
|
it("releases the session lock even when teardown cleanup throws", async () => {
|
|
const { bootstrap, assemble } = createContextEngineBootstrapAndAssemble();
|
|
const releaseMock = vi.fn(async () => {});
|
|
hoisted.acquireSessionWriteLockMock.mockResolvedValue({
|
|
release: releaseMock,
|
|
});
|
|
let flushCallCount = 0;
|
|
hoisted.flushPendingToolResultsAfterIdleMock.mockImplementation(async () => {
|
|
flushCallCount += 1;
|
|
if (flushCallCount >= 2) {
|
|
throw new Error("flush failed");
|
|
}
|
|
});
|
|
|
|
const result = await createContextEngineAttemptRunner({
|
|
contextEngine: {
|
|
bootstrap,
|
|
assemble,
|
|
},
|
|
sessionKey,
|
|
tempPaths,
|
|
});
|
|
|
|
expect(result.promptError).toBeNull();
|
|
expect(releaseMock).toHaveBeenCalledTimes(1);
|
|
expect(hoisted.releaseWsSessionMock).toHaveBeenCalledWith("embedded-session");
|
|
});
|
|
});
|