





















@@ -197,6 +197,52 @@ function createContextEngine(overrides: Partial<ContextEngine> = {}): ContextEng
197197return engine;
198198}
199199200+type MockCallReader = { mock: { calls: unknown[][] } };
201+202+function requireRecord(value: unknown, label: string): Record<string, unknown> {
203+if (!value || typeof value !== "object" || Array.isArray(value)) {
204+throw new Error(`expected ${label} to be an object`);
205+}
206+return value as Record<string, unknown>;
207+}
208+209+function requireFirstCallArg<T>(mock: unknown, label: string): T {
210+const call = (mock as MockCallReader).mock.calls.at(0);
211+if (!call) {
212+throw new Error(`expected ${label} to be called`);
213+}
214+return call[0] as T;
215+}
216+217+function requireRequestParams(
218+harness: ReturnType<typeof createStartedThreadHarness>,
219+method: string,
220+): Record<string, unknown> {
221+const request = harness.requests.find((entry) => entry.method === method);
222+return requireRecord(request?.params, `${method} params`);
223+}
224+225+function requireArray(value: unknown, label: string): unknown[] {
226+if (!Array.isArray(value)) {
227+throw new Error(`expected ${label} to be an array`);
228+}
229+return value;
230+}
231+232+function expectRequestInputTextContains(
233+harness: ReturnType<typeof createStartedThreadHarness>,
234+expected: string,
235+): void {
236+const params = requireRequestParams(harness, "turn/start");
237+const input = requireArray(params.input, "turn/start input");
238+expect(
239+input.some((entry) => {
240+const item = requireRecord(entry, "turn/start input entry");
241+return item.type === "text" && String(item.text ?? "").includes(expected);
242+}),
243+).toBe(true);
244+}
245+200246describe("runCodexAppServerAttempt context-engine lifecycle", () => {
201247beforeEach(async () => {
202248tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-context-engine-"));
@@ -225,46 +271,36 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
225271const run = runCodexAppServerAttempt(params);
226272await harness.waitForMethod("turn/start");
227273228-expect(contextEngine.bootstrap).toHaveBeenCalledWith(
229-expect.objectContaining({
230-sessionId: "session-1",
231-sessionKey: "agent:main:session-1",
232- sessionFile,
233-}),
234-);
235-expect(contextEngine.assemble).toHaveBeenCalledWith(
236-expect.objectContaining({
237-sessionId: "session-1",
238-sessionKey: "agent:main:session-1",
239-tokenBudget: 321,
240-citationsMode: "on",
241-model: "gpt-5.4-codex",
242-prompt: "hello",
243-messages: [expect.objectContaining({ role: "assistant" })],
244-availableTools: new Set(),
245-}),
274+if (!contextEngine.bootstrap) {
275+throw new Error("expected bootstrap hook");
276+}
277+expect(contextEngine.bootstrap).toHaveBeenCalledTimes(1);
278+const bootstrapParams = requireFirstCallArg<
279+Parameters<NonNullable<ContextEngine["bootstrap"]>>[0]
280+>(contextEngine.bootstrap, "bootstrap");
281+expect(bootstrapParams.sessionId).toBe("session-1");
282+expect(bootstrapParams.sessionKey).toBe("agent:main:session-1");
283+expect(bootstrapParams.sessionFile).toBe(sessionFile);
284+285+expect(contextEngine.assemble).toHaveBeenCalledTimes(1);
286+const assembleParams = requireFirstCallArg<Parameters<ContextEngine["assemble"]>[0]>(
287+contextEngine.assemble,
288+"assemble",
246289);
247-expect(harness.requests).toEqual(
248-expect.arrayContaining([
249-{
250-method: "thread/start",
251-params: expect.objectContaining({
252-developerInstructions: expect.stringContaining("context-engine system"),
253-}),
254-},
255-{
256-method: "turn/start",
257-params: expect.objectContaining({
258-input: expect.arrayContaining([
259-expect.objectContaining({
260-type: "text",
261-text: expect.stringContaining("OpenClaw assembled context for this turn:"),
262-}),
263-]),
264-}),
265-},
266-]),
290+expect(assembleParams.sessionId).toBe("session-1");
291+expect(assembleParams.sessionKey).toBe("agent:main:session-1");
292+expect(assembleParams.tokenBudget).toBe(321);
293+expect(assembleParams.citationsMode).toBe("on");
294+expect(assembleParams.model).toBe("gpt-5.4-codex");
295+expect(assembleParams.prompt).toBe("hello");
296+expect(assembleParams.messages.map((message) => message.role)).toEqual(["assistant"]);
297+expect(assembleParams.availableTools).toEqual(new Set());
298+299+const threadStartParams = requireRequestParams(harness, "thread/start");
300+expect(String(threadStartParams.developerInstructions ?? "")).toContain(
301+"context-engine system",
267302);
303+expectRequestInputTextContains(harness, "OpenClaw assembled context for this turn:");
268304269305await harness.completeTurn();
270306await run;
@@ -290,19 +326,15 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
290326await run;
291327292328expect(afterTurn).toHaveBeenCalledTimes(1);
293-const afterTurnCall = afterTurn.mock.calls.at(0)?.[0];
294-expect(afterTurnCall).toMatchObject({
295-sessionId: "session-1",
296-sessionKey: "agent:main:session-1",
297-prePromptMessageCount: 0,
298-tokenBudget: 111,
299-});
300-expect(afterTurnCall?.messages).toEqual(
301-expect.arrayContaining([
302-expect.objectContaining({ role: "user" }),
303-expect.objectContaining({ role: "assistant" }),
304-]),
305-);
329+const afterTurnCall = requireFirstCallArg<
330+Parameters<NonNullable<ContextEngine["afterTurn"]>>[0]
331+>(afterTurn, "afterTurn");
332+expect(afterTurnCall.sessionId).toBe("session-1");
333+expect(afterTurnCall.sessionKey).toBe("agent:main:session-1");
334+expect(afterTurnCall.prePromptMessageCount).toBe(0);
335+expect(afterTurnCall.tokenBudget).toBe(111);
336+expect(afterTurnCall.messages.some((message) => message.role === "user")).toBe(true);
337+expect(afterTurnCall.messages.some((message) => message.role === "assistant")).toBe(true);
306338expect(maintain).toHaveBeenCalledTimes(1);
307339});
308340@@ -337,30 +369,19 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
337369await harness.completeTurn();
338370await run;
339371340-expect(contextEngine.assemble).toHaveBeenCalledWith(
341-expect.objectContaining({
342-messages: [
343-expect.objectContaining({ role: "assistant" }),
344-expect.objectContaining({ role: "assistant" }),
345-],
346-}),
347-);
348-expect(afterTurn).toHaveBeenCalledWith(
349-expect.objectContaining({
350-prePromptMessageCount: 2,
351-}),
352-);
353-const turnStart = harness.requests.find((request) => request.method === "turn/start");
354-expect(turnStart?.params).toEqual(
355-expect.objectContaining({
356-input: expect.arrayContaining([
357-expect.objectContaining({
358-type: "text",
359-text: expect.stringContaining("bootstrap context"),
360-}),
361-]),
362-}),
372+const assembleParams = requireFirstCallArg<Parameters<ContextEngine["assemble"]>[0]>(
373+contextEngine.assemble,
374+"assemble",
363375);
376+expect(assembleParams.messages.map((message) => message.role)).toEqual([
377+"assistant",
378+"assistant",
379+]);
380+const afterTurnParams = requireFirstCallArg<
381+Parameters<NonNullable<ContextEngine["afterTurn"]>>[0]
382+>(afterTurn, "afterTurn");
383+expect(afterTurnParams.prePromptMessageCount).toBe(2);
384+expectRequestInputTextContains(harness, "bootstrap context");
364385});
365386366387it("logs assemble failures as a formatted message instead of the raw error object", async () => {
@@ -383,19 +404,13 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
383404await harness.completeTurn();
384405await run;
385406386-expect(warn).toHaveBeenCalledWith(
387-"context engine assemble failed; using Codex baseline prompt",
388-{
389-error: expect.any(String),
390-},
391-);
392407const warning = warn.mock.calls.find(
393408([message]) => message === "context engine assemble failed; using Codex baseline prompt",
394409);
410+const details = requireRecord(warning?.[1], "assemble warning details");
411+expect(typeof details.error).toBe("string");
395412expect(warning?.[1]).not.toEqual({ error: rawError });
396-expect(String((warning?.[1] as { error?: unknown } | undefined)?.error)).not.toContain(
397-"sk-abcdefghijklmnopqrstuv",
398-);
413+expect(String(details.error)).not.toContain("sk-abcdefghijklmnopqrstuv");
399414});
400415401416it("falls back to ingestBatch and skips turn maintenance on prompt failure", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。