























@@ -1150,6 +1150,222 @@ describe("CodexAppServerEventProjector", () => {
11501150expect(result.assistantTexts).toEqual(["final answer"]);
11511151});
115211521153+it("does not double-deliver a commentary note echoed on the raw response lane", async () => {
1154+const onAgentEvent = vi.fn();
1155+const projector = await createProjector({
1156+ ...(await createParams()),
1157+ onAgentEvent,
1158+});
1159+1160+// Typed agentMessage lane streams the note, keyed by the thread item id.
1161+await projector.handleNotification(
1162+forCurrentTurn("item/started", {
1163+item: { type: "agentMessage", id: "msg-commentary", phase: "commentary", text: "" },
1164+}),
1165+);
1166+await projector.handleNotification(
1167+agentMessageDelta("Checking the workspace", "msg-commentary"),
1168+);
1169+await projector.handleNotification(
1170+forCurrentTurn("item/completed", {
1171+item: {
1172+type: "agentMessage",
1173+id: "msg-commentary",
1174+phase: "commentary",
1175+text: "Checking the workspace",
1176+},
1177+}),
1178+);
1179+// Raw response lane echoes the same note. Codex omits the message id on the
1180+// wire (ResponseItem::Message.id is skip_serializing), so the projector
1181+// synthesizes a `raw-assistant-*` id that never matches the thread item id.
1182+await projector.handleNotification(
1183+forCurrentTurn("rawResponseItem/completed", {
1184+item: {
1185+type: "message",
1186+role: "assistant",
1187+phase: "commentary",
1188+content: [{ type: "output_text", text: "Checking the workspace" }],
1189+},
1190+}),
1191+);
1192+1193+const preambles = onAgentEvent.mock.calls
1194+.map((call) => call[0])
1195+.filter((event) => event.stream === "item" && event.data.kind === "preamble");
1196+1197+expect(preambles.map((event) => event.data.progressText)).toEqual(["Checking the workspace"]);
1198+expect(preambles.every((event) => event.data.itemId === "msg-commentary")).toBe(true);
1199+});
1200+1201+it("delivers distinct same-text commentary notes from the same lane within a turn", async () => {
1202+const onAgentEvent = vi.fn();
1203+const projector = await createProjector({
1204+ ...(await createParams()),
1205+ onAgentEvent,
1206+});
1207+1208+// Two separate notes that happen to share text must each be delivered.
1209+for (const id of ["msg-1", "msg-2"]) {
1210+await projector.handleNotification(
1211+forCurrentTurn("item/started", {
1212+item: { type: "agentMessage", id, phase: "commentary", text: "" },
1213+}),
1214+);
1215+await projector.handleNotification(agentMessageDelta("Checking the workspace", id));
1216+}
1217+1218+const preambles = onAgentEvent.mock.calls
1219+.map((call) => call[0])
1220+.filter((event) => event.stream === "item" && event.data.kind === "preamble");
1221+1222+expect(preambles.map((event) => event.data.itemId)).toEqual(["msg-1", "msg-2"]);
1223+expect(preambles.map((event) => event.data.progressText)).toEqual([
1224+"Checking the workspace",
1225+"Checking the workspace",
1226+]);
1227+});
1228+1229+it("delivers a later raw-only commentary note after consuming a same-text typed echo", async () => {
1230+const onAgentEvent = vi.fn();
1231+const projector = await createProjector({
1232+ ...(await createParams()),
1233+ onAgentEvent,
1234+});
1235+const rawCommentary = () =>
1236+forCurrentTurn("rawResponseItem/completed", {
1237+item: {
1238+type: "message",
1239+role: "assistant",
1240+phase: "commentary",
1241+content: [{ type: "output_text", text: "Checking the workspace" }],
1242+},
1243+});
1244+1245+await projector.handleNotification(
1246+forCurrentTurn("item/started", {
1247+item: { type: "agentMessage", id: "msg-commentary", phase: "commentary", text: "" },
1248+}),
1249+);
1250+await projector.handleNotification(
1251+agentMessageDelta("Checking the workspace", "msg-commentary"),
1252+);
1253+await projector.handleNotification(
1254+forCurrentTurn("item/completed", {
1255+item: {
1256+type: "agentMessage",
1257+id: "msg-commentary",
1258+phase: "commentary",
1259+text: "Checking the workspace",
1260+},
1261+}),
1262+);
1263+await projector.handleNotification(rawCommentary());
1264+await projector.handleNotification(rawCommentary());
1265+1266+const preambles = onAgentEvent.mock.calls
1267+.map((call) => call[0])
1268+.filter((event) => event.stream === "item" && event.data.kind === "preamble");
1269+1270+expect(preambles.map((event) => event.data.itemId)).toEqual([
1271+"msg-commentary",
1272+"raw-assistant-2",
1273+]);
1274+});
1275+1276+it("pairs a raw commentary echo after a rewritten typed completion", async () => {
1277+const onAgentEvent = vi.fn();
1278+const projector = await createProjector({
1279+ ...(await createParams()),
1280+ onAgentEvent,
1281+});
1282+1283+await projector.handleNotification(
1284+forCurrentTurn("item/started", {
1285+item: { type: "agentMessage", id: "msg-commentary", phase: "commentary", text: "" },
1286+}),
1287+);
1288+await projector.handleNotification(
1289+forCurrentTurn("item/completed", {
1290+item: {
1291+type: "agentMessage",
1292+id: "msg-commentary",
1293+phase: "commentary",
1294+text: "Contributor-rewritten note",
1295+},
1296+}),
1297+);
1298+await projector.handleNotification(
1299+forCurrentTurn("rawResponseItem/completed", {
1300+item: {
1301+type: "message",
1302+role: "assistant",
1303+phase: "commentary",
1304+content: [{ type: "output_text", text: "Original model note" }],
1305+},
1306+}),
1307+);
1308+1309+const preambles = onAgentEvent.mock.calls
1310+.map((call) => call[0])
1311+.filter((event) => event.stream === "item" && event.data.kind === "preamble");
1312+1313+expect(preambles.map((event) => event.data.progressText)).toEqual([
1314+"Contributor-rewritten note",
1315+]);
1316+expect(preambles.every((event) => event.data.itemId === "msg-commentary")).toBe(true);
1317+});
1318+1319+it("clears a pending commentary echo when the raw envelope has no text", async () => {
1320+const onAgentEvent = vi.fn();
1321+const projector = await createProjector({
1322+ ...(await createParams()),
1323+ onAgentEvent,
1324+});
1325+1326+await projector.handleNotification(
1327+forCurrentTurn("item/started", {
1328+item: { type: "agentMessage", id: "msg-commentary", phase: "commentary", text: "" },
1329+}),
1330+);
1331+await projector.handleNotification(
1332+forCurrentTurn("item/completed", {
1333+item: {
1334+type: "agentMessage",
1335+id: "msg-commentary",
1336+phase: "commentary",
1337+text: " ",
1338+},
1339+}),
1340+);
1341+await projector.handleNotification(
1342+forCurrentTurn("rawResponseItem/completed", {
1343+item: {
1344+type: "message",
1345+role: "assistant",
1346+phase: "commentary",
1347+content: [],
1348+},
1349+}),
1350+);
1351+await projector.handleNotification(
1352+forCurrentTurn("rawResponseItem/completed", {
1353+item: {
1354+type: "message",
1355+role: "assistant",
1356+phase: "commentary",
1357+content: [{ type: "output_text", text: "Later raw-only note" }],
1358+},
1359+}),
1360+);
1361+1362+const preambles = onAgentEvent.mock.calls
1363+.map((call) => call[0])
1364+.filter((event) => event.stream === "item" && event.data.kind === "preamble");
1365+1366+expect(preambles.map((event) => event.data.progressText)).toEqual(["Later raw-only note"]);
1367+});
1368+11531369it("does not resolve commentary-phase assistant text as the final reply", async () => {
11541370const projector = await createProjector();
11551371此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。