fix(harness): satisfy lifecycle lint gates

This commit is contained in:
Vincent Koc
2026-06-21 03:30:17 +08:00
committed by Vincent Koc
parent 9cb3b4ea2b
commit d7f747af3b
7 changed files with 45 additions and 30 deletions

View File

@@ -1101,12 +1101,12 @@ export async function runCodexAppServerAttempt(
};
};
let promptBuild = await buildPromptFromCurrentInputs();
const decorateCodexTurnPromptText = (promptBuild: {
const decorateCodexTurnPromptText = (promptBuildResult: {
prompt: string;
promptInputRange?: { start: number; end: number };
}) => {
const turnPromptText = prependCodexOpenClawPromptContext(
promptBuild.prompt,
promptBuildResult.prompt,
openClawPromptContext,
{
preservePromptWithoutContext:
@@ -1115,19 +1115,19 @@ export async function runCodexAppServerAttempt(
},
);
const projectedRanges = resolveShiftedPromptContextRange(
promptBuild.prompt,
promptBuild.promptInputRange,
promptBuildResult.prompt,
promptBuildResult.promptInputRange,
turnPromptText,
);
const preservedRange =
resolveShiftedPromptInputRange(
promptBuild.prompt,
promptBuild.promptInputRange,
promptBuildResult.prompt,
promptBuildResult.promptInputRange,
turnPromptText,
) ??
resolveCodexDeliveryHintPreservedInputRange({
prompt: promptBuild.prompt,
promptInputRange: promptBuild.promptInputRange,
prompt: promptBuildResult.prompt,
promptInputRange: promptBuildResult.promptInputRange,
decoratedPrompt: turnPromptText,
});
return fitCodexProjectedContextForTurnStart({

View File

@@ -1540,7 +1540,7 @@ describe("createCopilotAgentHarness", () => {
await harness.runAttempt(makeCompactParams());
const result = await harness.compact?.(makeCompactParams());
expect(pool.acquire).not.toHaveBeenCalled();
expect(pool.acquire.mock.calls).toHaveLength(0);
expect(result).toEqual({
ok: false,
compacted: false,

View File

@@ -642,10 +642,10 @@ export function createCopilotAgentHarness(
cleanup: Promise<DeferredCompactionCleanupOutcome>;
sdkSessionId: string;
}) => {
const tracked = trackedSessions.get(openclawSessionId);
const stored = lookupStoredBinding(options?.sessionStore, openclawSessionId);
const ownsTrackedSession = tracked?.sdkSessionId === sdkSessionId;
const ownsStoredSession = stored?.sdkSessionId === sdkSessionId;
const trackedBinding = trackedSessions.get(openclawSessionId);
const storedBinding = lookupStoredBinding(options?.sessionStore, openclawSessionId);
const ownsTrackedSession = trackedBinding?.sdkSessionId === sdkSessionId;
const ownsStoredSession = storedBinding?.sdkSessionId === sdkSessionId;
if (!ownsTrackedSession && !ownsStoredSession) {
return;
}

View File

@@ -108,6 +108,12 @@ function flushAsync() {
return tick().then(tick).then(tick);
}
function waitForEventLoopTurn(): Promise<void> {
return new Promise<void>((resolve) => {
setImmediate(resolve);
});
}
function getPromptErrorCode(result: AgentHarnessAttemptResult): string | undefined {
return (result.promptError as { code?: string } | undefined)?.code;
}
@@ -343,7 +349,7 @@ describe("runCopilotAttempt", () => {
createToolBridge,
pool: makeFakePool(sdk),
});
await new Promise<void>((resolve) => setImmediate(resolve));
await waitForEventLoopTurn();
expect(beforePromptBuild).toHaveBeenCalledWith(
expect.objectContaining({ prompt: "hello" }),
@@ -499,7 +505,7 @@ describe("runCopilotAttempt", () => {
expect(sdk.sessions[0]?.rpc.history.cancelBackgroundCompaction).toHaveBeenCalledTimes(1);
expect(sdk.sessions[0]?.disconnect).toHaveBeenCalledTimes(1);
expect(sdk.client.deleteSession).toHaveBeenCalledWith("sess-1");
expect(pool.release).toHaveBeenCalledTimes(1);
expect(pool.release.mock.calls).toHaveLength(1);
});
it("cancels retained compaction when the caller aborts after a turn result", async () => {
@@ -602,7 +608,7 @@ describe("runCopilotAttempt", () => {
await runCopilotAttempt(makeParams({ hooksConfig: { onUserPromptSubmitted } } as never), {
pool: makeFakePool(sdk),
});
await new Promise<void>((resolve) => setImmediate(resolve));
await waitForEventLoopTurn();
expect(onUserPromptSubmitted).toHaveBeenCalledWith(
expect.objectContaining({ prompt: "hello" }),
@@ -668,7 +674,7 @@ describe("runCopilotAttempt", () => {
pool: makeFakePool(sdk),
},
);
await new Promise<void>((resolve) => setImmediate(resolve));
await waitForEventLoopTurn();
expect(result.aborted).toBe(true);
expect(sdk.sessions[0]?.sendAndWait).not.toHaveBeenCalled();
@@ -695,7 +701,7 @@ describe("runCopilotAttempt", () => {
settled = true;
return result;
});
await new Promise<void>((resolve) => setImmediate(resolve));
await waitForEventLoopTurn();
expect(agentEnd).toHaveBeenCalledTimes(1);
expect(settled).toBe(false);
@@ -1935,7 +1941,7 @@ describe("runCopilotAttempt", () => {
expect(sdk.sessions[0]?.rpc.history.cancelBackgroundCompaction).toHaveBeenCalledTimes(1);
expect(sdk.sessions[0]?.disconnect).toHaveBeenCalledTimes(1);
expect(sdk.client.deleteSession).toHaveBeenCalledWith("sess-1");
expect(pool.release).toHaveBeenCalledTimes(1);
expect(pool.release.mock.calls).toHaveLength(1);
});
it("cancels deferred cleanup when the timed-out caller aborts", async () => {

View File

@@ -723,22 +723,22 @@ export async function runCopilotAttempt(
}
bridge = attachEventBridge(session, {
onAssistantDelta: input.onAssistantDelta,
onCompactionStart: () => {
onCompactionStart: async () => {
const sessionFile = readString(input.sessionFile);
if (!sessionFile) {
return;
}
return runAgentHarnessBeforeCompactionHook({
await runAgentHarnessBeforeCompactionHook({
sessionFile,
ctx: hookContext,
});
},
onCompactionComplete: ({ messagesRemoved, success }) => {
onCompactionComplete: async ({ messagesRemoved, success }) => {
const sessionFile = readString(input.sessionFile);
if (!success || !sessionFile) {
return;
}
return runAgentHarnessAfterCompactionHook({
await runAgentHarnessAfterCompactionHook({
sessionFile,
compactedCount: messagesRemoved ?? -1,
ctx: hookContext,

View File

@@ -263,12 +263,7 @@ export function attachEventBridge(
return compactionChain;
},
async awaitCompactionCompletion() {
// Background compaction can outlive session.idle. Keep the observer
// attached until its completion callback has run before releasing the session.
while (activeCompactionCount > 0) {
await compactionIdle;
}
await compactionChain;
await awaitStableCompaction();
},
awaitSessionIdle() {
return observedSessionIdle ? Promise.resolve() : sessionIdle;
@@ -338,6 +333,20 @@ export function attachEventBridge(
const queued = compactionChain.then(callback, callback);
compactionChain = queued.catch(() => undefined);
}
async function awaitStableCompaction(): Promise<void> {
const idle = activeCompactionCount > 0 ? compactionIdle : undefined;
if (idle) {
await idle;
}
const callbacks = compactionChain;
await callbacks;
// Compaction events can arrive while an earlier hook callback settles.
// Recheck both queues before teardown so the root observer stays attached.
if (activeCompactionCount > 0 || compactionChain !== callbacks) {
await awaitStableCompaction();
}
}
}
function buildAssistantMessage(params: {

View File

@@ -424,7 +424,7 @@ export function convertOpenClawToolToSdkTool(
};
const notifyToolCompleted = (completion: CopilotToolCompletion) => {
try {
void Promise.resolve(ctx.onToolCompleted?.(completion)).catch((error) => {
void Promise.resolve(ctx.onToolCompleted?.(completion)).catch((error: unknown) => {
console.warn("[copilot-tool-bridge] onToolCompleted handler threw; continuing", error);
});
} catch (error) {