fix: mark a rejected standalone response entry as an error too

extractStatus returned none for any non-request entry, so a clientError
annotation that landed on a standalone response frame — the fallback
messageLogState uses when no request entry was there to fold into, e.g. a
trimmed log or a reconnect boundary — rendered with no status badge at
all. The clientError check now runs before the request-only lifecycle.

Caught in Copilot's review of #1953.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SW1p8E2uiyyLx4RKwrwSrt
This commit is contained in:
cliffhall 2026-08-08 17:11:34 -04:00
commit 1e427c05f3
2 changed files with 42 additions and 3 deletions

View file

@ -756,6 +756,41 @@ describe("ProtocolEntry — client-rejected response", () => {
expect(screen.getByText(REASON)).toBeInTheDocument();
});
// messageLogState falls back to the standalone response frame when no request
// entry was there to fold into (a trimmed log, or a reconnect boundary). That
// entry is not `direction: "request"`, so the request-only status lifecycle
// would have rendered it without any badge at all.
const rejectedStandaloneResponse: MessageEntry = {
id: "rej-standalone",
timestamp: new Date("2026-07-28T10:00:00Z"),
direction: "response",
origin: "server",
message: {
jsonrpc: "2.0",
id: 11,
result: { resultType: "complete", tools: [] },
},
clientError: REASON,
};
it("renders Error on a rejected standalone response entry", () => {
renderWithMantine(
<ProtocolEntry {...baseProps} entry={rejectedStandaloneResponse} />,
);
expect(screen.getByText("Error")).toBeInTheDocument();
});
it("still renders no status badge on an unrejected standalone response", () => {
const clean: MessageEntry = {
...rejectedStandaloneResponse,
clientError: undefined,
};
renderWithMantine(<ProtocolEntry {...baseProps} entry={clean} />);
expect(screen.queryByText("Error")).not.toBeInTheDocument();
expect(screen.queryByText("OK")).not.toBeInTheDocument();
expect(screen.queryByText("Pending")).not.toBeInTheDocument();
});
it("shows no rejection alert on an ordinary success", () => {
renderWithMantine(
<ProtocolEntry {...baseProps} entry={successEntry} isListExpanded />,

View file

@ -220,12 +220,16 @@ function extractResourceUri(entry: MessageEntry): string | undefined {
function extractStatus(
entry: MessageEntry,
): "success" | "error" | "pending" | "none" {
// A response the CLIENT refused is an error whichever entry carries it, so
// this is checked BEFORE the request-only lifecycle below (#1953).
// messageLogState annotates the request entry when the response was folded
// into one, but falls back to the standalone response frame when there was
// no matching request (a trimmed log, or a reconnect boundary) — and that
// entry would otherwise fall straight through to "none" and render no badge.
if (entry.clientError) return "error";
if (entry.direction !== "request") return "none";
if (!entry.response) return "pending";
if ("error" in entry.response) return "error";
// The server answered with a valid result the CLIENT then refused (#1953).
// The call failed, so the entry must not read as a success.
if (entry.clientError) return "error";
return "success";
}