Skip to content

Commit c90fd1a

Browse files
test: improve stream-object test coverage across packages (#79)
* test(sdk/typescript): improve stream-object test coverage Add tests for uncovered code paths in stream-object.ts: - null/non-object parsed values (skip handling) - sessionId resolution from result event (fallback) - sessionId rejection on error before session event - SSE parse errors (critical and non-critical) - NO_SESSION and NO_USAGE flush handlers Add test for streamObject via client interface to reach 100% coverage on client.ts. Coverage improvements: - stream-object.ts: 80% → 99.56% - client.ts: 93.75% → 100% Closes part of #74 * test(sdk/python): improve stream-object test coverage Add tests for uncovered code paths in stream_object.py: - sessionId resolution from result event when no session event - sessionId rejection on error before session event - SSE_PARSE_ERROR for critical events with malformed JSON - Non-critical parse errors (partial-object) continue streaming - NO_SESSION when stream ends without session or result events Coverage improvement: - stream_object.py: 75% → 84% Closes part of #74 * test(gateway): improve stream-object test coverage Add tests for uncovered code paths in stream-object.ts: - Warning events for markdown-block extraction fallback - Warning events for object-extraction fallback - Warning events for array-extraction fallback - Buffer warning on clean exit with unparseable data - Interrupt exit code with buffer data (no warning) - Parse error on final object in close handler Coverage improvement: - stream-object.ts: 67.98% → 78.94% Closes part of #74
1 parent 6eff72a commit c90fd1a

File tree

4 files changed

+857
-0
lines changed

4 files changed

+857
-0
lines changed

packages/gateway/__tests__/routes/stream-object.test.ts

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -837,5 +837,258 @@ describe("Stream Object Route", () => {
837837
(objectEvent?.data as { object: Array<{ name: string }> }).object,
838838
).toEqual([{ name: "Alice" }, { name: "Bob" }]);
839839
});
840+
841+
it("emits warning event when using markdown-block extraction", async () => {
842+
const mockProc = createMockChildProcess();
843+
mockSpawn.mockReturnValue(mockProc as never);
844+
845+
const app = createTestApp();
846+
const responsePromise = request(app)
847+
.post("/stream-object")
848+
.send({ prompt: "Extract", schema: validSchema });
849+
850+
afterSpawnCalled(mockSpawn, () => {
851+
// Claude wraps JSON in markdown - this triggers fallback extraction
852+
mockProc.stdout.emit(
853+
"data",
854+
Buffer.from(
855+
`${createStreamEventDelta('```json\n{"name": "FallbackTest", "age": 30}\n```')}\n`,
856+
),
857+
);
858+
mockProc.stdout.emit(
859+
"data",
860+
Buffer.from(`${createStreamResultMessage()}\n`),
861+
);
862+
mockProc.emit("close", 0, null);
863+
});
864+
865+
const res = await responsePromise;
866+
const events = parseSSEResponse(res.text);
867+
868+
// Should emit warning about fallback extraction
869+
const warningEvent = events.find((e) => e.event === "warning");
870+
expect(warningEvent).toBeDefined();
871+
expect((warningEvent?.data as { code: string }).code).toBe(
872+
"EXTRACTION_FALLBACK",
873+
);
874+
expect((warningEvent?.data as { message: string }).message).toContain(
875+
"markdown-block",
876+
);
877+
878+
// Object should still be extracted correctly
879+
const objectEvent = events.find((e) => e.event === "object");
880+
expect(objectEvent).toBeDefined();
881+
expect(
882+
(objectEvent?.data as { object: { name: string } }).object.name,
883+
).toBe("FallbackTest");
884+
});
885+
886+
it("emits warning event when using object-extraction strategy", async () => {
887+
const mockProc = createMockChildProcess();
888+
mockSpawn.mockReturnValue(mockProc as never);
889+
890+
const app = createTestApp();
891+
const responsePromise = request(app)
892+
.post("/stream-object")
893+
.send({ prompt: "Extract", schema: validSchema });
894+
895+
afterSpawnCalled(mockSpawn, () => {
896+
// Text with surrounding content triggers object-extraction fallback
897+
mockProc.stdout.emit(
898+
"data",
899+
Buffer.from(
900+
`${createStreamEventDelta('Sure! Here is the result: {"name": "ObjectExtract", "age": 40} Hope this helps!')}\n`,
901+
),
902+
);
903+
mockProc.stdout.emit(
904+
"data",
905+
Buffer.from(`${createStreamResultMessage()}\n`),
906+
);
907+
mockProc.emit("close", 0, null);
908+
});
909+
910+
const res = await responsePromise;
911+
const events = parseSSEResponse(res.text);
912+
913+
// Should emit warning about fallback extraction
914+
const warningEvent = events.find((e) => e.event === "warning");
915+
expect(warningEvent).toBeDefined();
916+
expect((warningEvent?.data as { code: string }).code).toBe(
917+
"EXTRACTION_FALLBACK",
918+
);
919+
expect((warningEvent?.data as { message: string }).message).toContain(
920+
"object-extraction",
921+
);
922+
});
923+
924+
it("emits warning event when using array-extraction strategy", async () => {
925+
const arraySchema = {
926+
type: "array",
927+
items: {
928+
type: "object",
929+
properties: { name: { type: "string" } },
930+
},
931+
};
932+
933+
const mockProc = createMockChildProcess();
934+
mockSpawn.mockReturnValue(mockProc as never);
935+
936+
const app = createTestApp();
937+
const responsePromise = request(app)
938+
.post("/stream-object")
939+
.send({ prompt: "List people", schema: arraySchema });
940+
941+
afterSpawnCalled(mockSpawn, () => {
942+
// Text with surrounding content and array triggers array-extraction
943+
mockProc.stdout.emit(
944+
"data",
945+
Buffer.from(
946+
`${createStreamEventDelta('Here are the people: [{"name": "ArrayTest1"}, {"name": "ArrayTest2"}] Done!')}\n`,
947+
),
948+
);
949+
mockProc.stdout.emit(
950+
"data",
951+
Buffer.from(`${createStreamResultMessage()}\n`),
952+
);
953+
mockProc.emit("close", 0, null);
954+
});
955+
956+
const res = await responsePromise;
957+
const events = parseSSEResponse(res.text);
958+
959+
// Should emit warning about fallback extraction
960+
const warningEvent = events.find((e) => e.event === "warning");
961+
expect(warningEvent).toBeDefined();
962+
expect((warningEvent?.data as { code: string }).code).toBe(
963+
"EXTRACTION_FALLBACK",
964+
);
965+
expect((warningEvent?.data as { message: string }).message).toContain(
966+
"array-extraction",
967+
);
968+
969+
// Array should still be extracted correctly
970+
const objectEvent = events.find((e) => e.event === "object");
971+
expect(objectEvent).toBeDefined();
972+
expect(
973+
(objectEvent?.data as { object: Array<{ name: string }> }).object,
974+
).toEqual([{ name: "ArrayTest1" }, { name: "ArrayTest2" }]);
975+
});
976+
977+
it("emits warning when clean exit has unparseable buffer data", async () => {
978+
const mockProc = createMockChildProcess();
979+
mockSpawn.mockReturnValue(mockProc as never);
980+
981+
const app = createTestApp();
982+
const responsePromise = request(app)
983+
.post("/stream-object")
984+
.send({ prompt: "Hello", schema: validSchema });
985+
986+
afterSpawnCalled(mockSpawn, () => {
987+
// Stream valid JSON first
988+
mockProc.stdout.emit(
989+
"data",
990+
Buffer.from(
991+
`${createStreamEventDelta('{"name": "Test", "age": 1}')}\n`,
992+
),
993+
);
994+
mockProc.stdout.emit(
995+
"data",
996+
Buffer.from(`${createStreamResultMessage()}\n`),
997+
);
998+
// Leave garbage in buffer (no newline - stays in buffer)
999+
mockProc.stdout.emit(
1000+
"data",
1001+
Buffer.from("garbage data without newline"),
1002+
);
1003+
// Clean exit (code=0, signal=null) with garbage in buffer
1004+
mockProc.emit("close", 0, null);
1005+
});
1006+
1007+
const res = await responsePromise;
1008+
const events = parseSSEResponse(res.text);
1009+
1010+
// Should emit warning about unparseable buffer data
1011+
const warningEvent = events.find((e) => e.event === "warning");
1012+
expect(warningEvent).toBeDefined();
1013+
expect((warningEvent?.data as { code: string }).code).toBe(
1014+
"BUFFER_PARSE_WARNING",
1015+
);
1016+
});
1017+
1018+
it("handles interrupt exit code with buffer data gracefully", async () => {
1019+
const mockProc = createMockChildProcess();
1020+
mockSpawn.mockReturnValue(mockProc as never);
1021+
1022+
const app = createTestApp();
1023+
const responsePromise = request(app)
1024+
.post("/stream-object")
1025+
.send({ prompt: "Hello", schema: validSchema });
1026+
1027+
afterSpawnCalled(mockSpawn, () => {
1028+
// Stream valid JSON first
1029+
mockProc.stdout.emit(
1030+
"data",
1031+
Buffer.from(
1032+
`${createStreamEventDelta('{"name": "Test", "age": 1}')}\n`,
1033+
),
1034+
);
1035+
mockProc.stdout.emit(
1036+
"data",
1037+
Buffer.from(`${createStreamResultMessage()}\n`),
1038+
);
1039+
// Leave incomplete data in buffer (no newline)
1040+
mockProc.stdout.emit("data", Buffer.from("incomplete data"));
1041+
// Non-zero exit (interrupt) - buffer warning should NOT be emitted
1042+
mockProc.emit("close", 1, "SIGTERM");
1043+
});
1044+
1045+
const res = await responsePromise;
1046+
const events = parseSSEResponse(res.text);
1047+
1048+
// Should NOT emit warning for interrupt (non-zero exit code is expected)
1049+
const warningEvent = events.find(
1050+
(e) =>
1051+
e.event === "warning" &&
1052+
(e.data as { code: string }).code === "BUFFER_PARSE_WARNING",
1053+
);
1054+
expect(warningEvent).toBeUndefined();
1055+
1056+
// Should emit CLI_EXIT_ERROR for non-zero exit
1057+
const errorEvent = events.find((e) => e.event === "error");
1058+
expect(errorEvent).toBeDefined();
1059+
expect((errorEvent?.data as { code: string }).code).toBe(
1060+
"CLI_EXIT_ERROR",
1061+
);
1062+
});
1063+
1064+
it("handles parse error on final object in close handler", async () => {
1065+
const mockProc = createMockChildProcess();
1066+
mockSpawn.mockReturnValue(mockProc as never);
1067+
1068+
const app = createTestApp();
1069+
const responsePromise = request(app)
1070+
.post("/stream-object")
1071+
.send({ prompt: "Hello", schema: validSchema });
1072+
1073+
afterSpawnCalled(mockSpawn, () => {
1074+
// Stream completely malformed JSON that can't be extracted
1075+
mockProc.stdout.emit(
1076+
"data",
1077+
Buffer.from(`${createStreamEventDelta("not json at all {{{{")}\n`),
1078+
);
1079+
// Send result without newline - triggers close handler parsing
1080+
const resultMessage = createStreamResultMessage();
1081+
mockProc.stdout.emit("data", Buffer.from(resultMessage));
1082+
mockProc.emit("close", 0, null);
1083+
});
1084+
1085+
const res = await responsePromise;
1086+
const events = parseSSEResponse(res.text);
1087+
1088+
// Should emit parse error
1089+
const errorEvent = events.find((e) => e.event === "error");
1090+
expect(errorEvent).toBeDefined();
1091+
expect((errorEvent?.data as { code: string }).code).toBe("PARSE_ERROR");
1092+
});
8401093
});
8411094
});

0 commit comments

Comments
 (0)