Files
openclaw/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts
2026-04-03 16:57:20 +08:00

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");
});
});