mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(matrix): retain visible drafts on handler abort
This commit is contained in:
@@ -3914,18 +3914,6 @@ describe("matrix monitor handler draft streaming", () => {
|
|||||||
|
|
||||||
const result = await deliver(payload, { kind: "final" });
|
const result = await deliver(payload, { kind: "final" });
|
||||||
await finish();
|
await finish();
|
||||||
if (redactEventMock.mock.calls.length > 0) {
|
|
||||||
throw new Error(
|
|
||||||
[
|
|
||||||
"non-visible replacement redacted the visible draft",
|
|
||||||
`draftSendOrder=${sendSingleTextMessageMatrixMock.mock.invocationCallOrder[0] ?? "none"}`,
|
|
||||||
`redactionOrders=${redactEventMock.mock.invocationCallOrder.join(",")}`,
|
|
||||||
`replacementOrder=${deliverMatrixRepliesMock.mock.invocationCallOrder[0] ?? "none"}`,
|
|
||||||
`replacementResult=${JSON.stringify(result)}`,
|
|
||||||
`postHandlerRedactionCount=${redactEventMock.mock.calls.length}`,
|
|
||||||
].join(" "),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(result).toMatchObject({
|
expect(result).toMatchObject({
|
||||||
messageIds: ["$draft1"],
|
messageIds: ["$draft1"],
|
||||||
@@ -4030,7 +4018,7 @@ describe("matrix monitor handler draft streaming", () => {
|
|||||||
it.each(
|
it.each(
|
||||||
(["retained", "consumed"] as const).flatMap((priorDisposition) =>
|
(["retained", "consumed"] as const).flatMap((priorDisposition) =>
|
||||||
(["block", "followup"] as const).flatMap((boundary) =>
|
(["block", "followup"] as const).flatMap((boundary) =>
|
||||||
(["complete", "abort"] as const).map((outcome) => ({
|
(["complete", "unfinished"] as const).map((outcome) => ({
|
||||||
priorDisposition,
|
priorDisposition,
|
||||||
boundary,
|
boundary,
|
||||||
outcome,
|
outcome,
|
||||||
@@ -4575,12 +4563,16 @@ describe("matrix monitor handler draft streaming", () => {
|
|||||||
await finish();
|
await finish();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("stops draft stream on handler error (no leaked timer)", async () => {
|
it("stops quiet draft stream on handler error and cleans a draft accepted during shutdown", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
try {
|
try {
|
||||||
sendSingleTextMessageMatrixMock
|
let resolveDraftSend: ((value: { messageId: string; roomId: string }) => void) | undefined;
|
||||||
.mockReset()
|
sendSingleTextMessageMatrixMock.mockReset().mockImplementation(
|
||||||
.mockResolvedValue({ messageId: "$draft1", roomId: "!room" });
|
() =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
resolveDraftSend = resolve;
|
||||||
|
}),
|
||||||
|
);
|
||||||
editMessageMatrixMock.mockReset().mockResolvedValue("$edited");
|
editMessageMatrixMock.mockReset().mockResolvedValue("$edited");
|
||||||
deliverMatrixRepliesMock.mockReset().mockResolvedValue(createMockMatrixDeliveryResult());
|
deliverMatrixRepliesMock.mockReset().mockResolvedValue(createMockMatrixDeliveryResult());
|
||||||
const redactEventMock = vi.fn(async () => "$redacted");
|
const redactEventMock = vi.fn(async () => "$redacted");
|
||||||
@@ -4600,18 +4592,20 @@ describe("matrix monitor handler draft streaming", () => {
|
|||||||
capturedReplyOpts = args?.replyOptions;
|
capturedReplyOpts = args?.replyOptions;
|
||||||
// Simulate streaming then model error.
|
// Simulate streaming then model error.
|
||||||
capturedReplyOpts?.onPartialReply?.({ text: "partial" });
|
capturedReplyOpts?.onPartialReply?.({ text: "partial" });
|
||||||
await waitForMatrixState(() => {
|
|
||||||
expect(sendSingleTextMessageMatrixMock).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
throw new Error("model timeout");
|
throw new Error("model timeout");
|
||||||
}) as never,
|
}) as never,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handler should not throw (outer catch absorbs it).
|
// Handler should not throw (outer catch absorbs it).
|
||||||
await handler(
|
const handlerPromise = handler(
|
||||||
"!room:example.org",
|
"!room:example.org",
|
||||||
createMatrixTextMessageEvent({ eventId: "$msg1", body: "hello" }),
|
createMatrixTextMessageEvent({ eventId: "$msg1", body: "hello" }),
|
||||||
);
|
);
|
||||||
|
await waitForMatrixState(() => {
|
||||||
|
expect(sendSingleTextMessageMatrixMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
resolveDraftSend?.({ messageId: "$draft1", roomId: "!room" });
|
||||||
|
await handlerPromise;
|
||||||
|
|
||||||
expect(redactEventMock).toHaveBeenCalledWith("!room:example.org", "$draft1");
|
expect(redactEventMock).toHaveBeenCalledWith("!room:example.org", "$draft1");
|
||||||
|
|
||||||
@@ -4626,7 +4620,7 @@ describe("matrix monitor handler draft streaming", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("redacts partial live drafts when generation aborts mid-stream", async () => {
|
it("retains visible live drafts when generation aborts mid-stream", async () => {
|
||||||
sendSingleTextMessageMatrixMock
|
sendSingleTextMessageMatrixMock
|
||||||
.mockReset()
|
.mockReset()
|
||||||
.mockResolvedValue({ messageId: "$draft1", roomId: "!room" });
|
.mockResolvedValue({ messageId: "$draft1", roomId: "!room" });
|
||||||
@@ -4660,7 +4654,7 @@ describe("matrix monitor handler draft streaming", () => {
|
|||||||
createMatrixTextMessageEvent({ eventId: "$msg1", body: "hello" }),
|
createMatrixTextMessageEvent({ eventId: "$msg1", body: "hello" }),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(redactEventMock).toHaveBeenCalledWith("!room:example.org", "$draft1");
|
expect(redactEventMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps shutdown cleanup for empty final payloads that send nothing", async () => {
|
it("keeps shutdown cleanup for empty final payloads that send nothing", async () => {
|
||||||
|
|||||||
@@ -611,6 +611,14 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
|||||||
);
|
);
|
||||||
await commitInboundEventIfClaimed();
|
await commitInboundEventIfClaimed();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
const draftController = draftControllerRef;
|
||||||
|
if (
|
||||||
|
draftController?.draftStream?.eventId() &&
|
||||||
|
draftController.draftDisposition() === "active"
|
||||||
|
) {
|
||||||
|
// A Matrix-accepted preview is the only visible reply after an abort.
|
||||||
|
draftController.markDraftRetained();
|
||||||
|
}
|
||||||
runtime.error?.(`matrix handler failed: ${String(err)}`);
|
runtime.error?.(`matrix handler failed: ${String(err)}`);
|
||||||
} finally {
|
} finally {
|
||||||
// Stop the draft stream timer so partial drafts don't leak if the
|
// Stop the draft stream timer so partial drafts don't leak if the
|
||||||
|
|||||||
Reference in New Issue
Block a user