mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix empty cli response handling
This commit is contained in:
committed by
Peter Steinberger
parent
3a5627d911
commit
76ce72cbe5
@@ -160,6 +160,7 @@ describe("runCliAgent cron before_agent_reply seam", () => {
|
||||
const { runCliAgent } = await import("./cli-runner.js");
|
||||
hasHooksMock.mockImplementation((hookName) => hookName === "before_agent_reply");
|
||||
runBeforeAgentReplyMock.mockResolvedValue(undefined);
|
||||
executePreparedCliRunMock.mockResolvedValue({ text: "real reply" });
|
||||
const onExecutionPhase = vi.fn();
|
||||
|
||||
await runCliAgent({
|
||||
@@ -183,6 +184,19 @@ describe("runCliAgent cron before_agent_reply seam", () => {
|
||||
expect(executePreparedCliRunMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("treats empty CLI subprocess output as a failover failure, not a green cron run", async () => {
|
||||
const { runCliAgent } = await import("./cli-runner.js");
|
||||
executePreparedCliRunMock.mockResolvedValue({ text: " " });
|
||||
|
||||
await expect(runCliAgent({ ...baseRunParams, trigger: "cron" })).rejects.toMatchObject({
|
||||
name: "FailoverError",
|
||||
reason: "empty_response",
|
||||
provider: baseRunParams.provider,
|
||||
model: baseRunParams.model,
|
||||
sessionId: baseRunParams.sessionId,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a silent payload when a cron hook claims without a reply body", async () => {
|
||||
const { runCliAgent } = await import("./cli-runner.js");
|
||||
hasHooksMock.mockImplementation((hookName) => hookName === "before_agent_reply");
|
||||
|
||||
@@ -426,6 +426,48 @@ describe("runPreparedCliAgent context engine lifecycle", () => {
|
||||
expect(dispose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not finalize context-engine turns for empty successful CLI output", async () => {
|
||||
executePreparedCliRunMock.mockResolvedValue({
|
||||
text: " ",
|
||||
rawText: " ",
|
||||
sessionId: "external-cli-session-empty",
|
||||
usage: { input: 11, output: 0, total: 11 },
|
||||
});
|
||||
const bootstrap = vi.fn<NonNullable<ContextEngine["bootstrap"]>>(async () => ({
|
||||
bootstrapped: true,
|
||||
}));
|
||||
const afterTurn = vi.fn<NonNullable<ContextEngine["afterTurn"]>>(async () => {});
|
||||
const ingestBatch = vi.fn<NonNullable<ContextEngine["ingestBatch"]>>(async () => ({
|
||||
ingestedCount: 0,
|
||||
}));
|
||||
const maintain = vi.fn<NonNullable<ContextEngine["maintain"]>>(async () =>
|
||||
createMaintenanceResult(),
|
||||
);
|
||||
const dispose = vi.fn(async () => {});
|
||||
const contextEngine = createContextEngine({
|
||||
bootstrap,
|
||||
afterTurn,
|
||||
ingestBatch,
|
||||
maintain,
|
||||
dispose,
|
||||
});
|
||||
const { runPreparedCliAgent } = await import("./cli-runner.js");
|
||||
|
||||
await expect(runPreparedCliAgent(buildPreparedContext(contextEngine))).rejects.toMatchObject({
|
||||
name: "FailoverError",
|
||||
reason: "empty_response",
|
||||
provider: "claude-cli",
|
||||
model: "sonnet-4.6",
|
||||
sessionId: "openclaw-session-1",
|
||||
});
|
||||
|
||||
expect(bootstrap).toHaveBeenCalledTimes(1);
|
||||
expect(afterTurn).not.toHaveBeenCalled();
|
||||
expect(ingestBatch).not.toHaveBeenCalled();
|
||||
expect(maintain).toHaveBeenCalledTimes(1);
|
||||
expect(dispose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not dispose context engines when CLI attempts fail", async () => {
|
||||
executePreparedCliRunMock.mockRejectedValue(new Error("cli boom"));
|
||||
const dispose = vi.fn(async () => {
|
||||
|
||||
@@ -402,6 +402,15 @@ export async function runPreparedCliAgent(
|
||||
const executeCliAttempt = async (cliSessionIdToUse?: string) => {
|
||||
const output = await executePreparedCliRun(context, cliSessionIdToUse);
|
||||
const assistantText = output.text.trim();
|
||||
if (!assistantText) {
|
||||
throw new FailoverError("CLI backend returned an empty response.", {
|
||||
reason: "empty_response",
|
||||
provider: params.provider,
|
||||
model: context.modelId,
|
||||
sessionId: params.sessionId,
|
||||
lane: params.lane,
|
||||
});
|
||||
}
|
||||
const assistantTexts = assistantText ? [assistantText] : [];
|
||||
const lastAssistant =
|
||||
assistantText.length > 0
|
||||
|
||||
@@ -45,6 +45,20 @@ describe("resolveAuthProfileFailureReason", () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("does not persist empty responses as auth-profile health", () => {
|
||||
expect(
|
||||
resolveAuthProfileFailureReason({
|
||||
failoverReason: "empty_response",
|
||||
}),
|
||||
).toBeNull();
|
||||
expect(
|
||||
resolveAuthProfileFailureReason({
|
||||
failoverReason: "empty_response",
|
||||
policy: "shared",
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("does not persist request-shape (format) rejections as auth-profile health (#77228)", () => {
|
||||
// A format rejection (e.g. the github-copilot prefill-strict 400
|
||||
// "conversation must end with a user message" reported in #77228) is
|
||||
|
||||
@@ -6,7 +6,7 @@ export function resolveAuthProfileFailureReason(params: {
|
||||
failoverReason: FailoverReason | null;
|
||||
policy?: AuthProfileFailurePolicy;
|
||||
}): AuthProfileFailureReason | null {
|
||||
// Helper-local runs, transport/server failures, and request-shape ("format") rejections
|
||||
// Helper-local runs, transport/server failures, empty responses, and request-shape ("format") rejections
|
||||
// should not poison shared provider auth health. A `format` failure means the
|
||||
// provider rejected the request payload (e.g. an assistant-prefill 400 from a
|
||||
// strict provider when a session transcript ends with a stream-error placeholder
|
||||
@@ -20,6 +20,7 @@ export function resolveAuthProfileFailureReason(params: {
|
||||
!params.failoverReason ||
|
||||
params.failoverReason === "timeout" ||
|
||||
params.failoverReason === "server_error" ||
|
||||
params.failoverReason === "empty_response" ||
|
||||
params.failoverReason === "format"
|
||||
) {
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user