- fetchTracking: count NDJSON streams line by line instead of leaving them at zero forever; justify the detached watcher promise; document that a partial SSE block at end-of-stream is dropped, as the SDK's own parser (no flush) and the SSE spec drop it. - uncapturedBodyNote: case-insensitive content-type lookup, and a stream label (SSE / NDJSON) for the Network badge. - InspectorClient: `openedAt` is when the response headers arrived, not when the GET went out; the snapshot copies its request records; a request the transport failed to send is rolled back out of the unanswered set via a new `trackSendFailure` callback; a raw-wire timeout is built as the SDK's own timeout shape and annotated the same way, so callers see one kind of timeout. - Remote session storage revives the nested `stream.closedAt` Date. - Hook test models a legacy double with `Object.defineProperty`, not a double cast. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APKSYHGxXBUpnR7ErFiLJn Signed-off-by: cliffhall <cliff@futurescale.com>
79 lines
2.3 KiB
TypeScript
79 lines
2.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { FetchRequestEntry } from "@inspector/core/mcp/types.js";
|
|
import {
|
|
isLongLivedStreamEntry,
|
|
longLivedStreamLabel,
|
|
uncapturedBodyNote,
|
|
} from "./uncapturedBodyNote";
|
|
|
|
const streamEntry: FetchRequestEntry = {
|
|
id: "n-1",
|
|
timestamp: new Date("2026-03-17T10:00:00Z"),
|
|
method: "GET",
|
|
url: "https://example.com/mcp",
|
|
requestHeaders: {},
|
|
responseStatus: 200,
|
|
responseHeaders: { "content-type": "text/event-stream" },
|
|
category: "transport",
|
|
};
|
|
|
|
describe("isLongLivedStreamEntry", () => {
|
|
it("is true for a GET answered with an event stream and false otherwise", () => {
|
|
expect(isLongLivedStreamEntry(streamEntry)).toBe(true);
|
|
expect(isLongLivedStreamEntry({ ...streamEntry, method: "POST" })).toBe(
|
|
false,
|
|
);
|
|
expect(
|
|
isLongLivedStreamEntry({ ...streamEntry, responseHeaders: undefined }),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("reads the content type whatever casing the entry recorded it with", () => {
|
|
expect(
|
|
isLongLivedStreamEntry({
|
|
...streamEntry,
|
|
responseHeaders: { "Content-Type": "text/event-stream" },
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("longLivedStreamLabel", () => {
|
|
it("names the framing the stream uses", () => {
|
|
expect(longLivedStreamLabel(streamEntry)).toBe("SSE");
|
|
expect(
|
|
longLivedStreamLabel({
|
|
...streamEntry,
|
|
responseHeaders: { "Content-Type": "application/x-ndjson" },
|
|
}),
|
|
).toBe("NDJSON");
|
|
});
|
|
});
|
|
|
|
describe("uncapturedBodyNote", () => {
|
|
it("says (empty) for a bounded response", () => {
|
|
expect(uncapturedBodyNote({ ...streamEntry, method: "POST" })).toBe(
|
|
"(empty)",
|
|
);
|
|
});
|
|
|
|
it("says only that the body was not captured before the stream is watched", () => {
|
|
expect(uncapturedBodyNote(streamEntry)).toBe(
|
|
"Long-lived stream — body not captured",
|
|
);
|
|
});
|
|
|
|
it("reports the event count and open state, with the singular for one event", () => {
|
|
expect(
|
|
uncapturedBodyNote({ ...streamEntry, stream: { eventCount: 3 } }),
|
|
).toBe(
|
|
"Long-lived stream — 3 events delivered, still open; body not captured",
|
|
);
|
|
expect(
|
|
uncapturedBodyNote({
|
|
...streamEntry,
|
|
stream: { eventCount: 1, closedAt: new Date() },
|
|
}),
|
|
).toBe("Long-lived stream — 1 event delivered, closed; body not captured");
|
|
});
|
|
});
|