fix: preserve interaction replies across chat recovery

This commit is contained in:
2026-10-29 17:53:33 +08:00
parent fbe273e90a
commit 52681a8451
38 changed files with 1623 additions and 314 deletions
+28 -1
View File
@@ -1,8 +1,35 @@
import { expect, it, mock } from "bun:test";
import { ResponseInputTracker, type ResponseInput } from "../../src/chat/response-inputs.js";
import { ResponseInputTracker, WaitingResponseInputQueue, type ResponseInput } from "../../src/chat/response-inputs.js";
const input = (id: number): ResponseInput => ({ id: `msg_${String(id).padStart(32, "0")}`, text: `Input ${id}`, delivery: "steer", status: "pending" });
it("serializes waiting input admission, deduplicates retries and enforces capacity without resuming", async () => {
const pending: ResponseInput[] = [];
const enqueue = mock(async (_session: string, item: ResponseInput, options?: { resume?: boolean }) => {
expect(options?.resume).toBe(false);
pending.push(item);
});
const queue = new WaitingResponseInputQueue({ listPendingUserInputs: async () => pending,
agentMessageState: async () => "missing", enqueueUserInput: enqueue });
await Promise.all([queue.submit("s", input(1)), queue.submit("s", input(1))]);
expect(enqueue).toHaveBeenCalledTimes(1);
await expect(queue.submit("s", { ...input(1), text: "different" })).rejects.toMatchObject({ status: 409 });
const results = await Promise.allSettled(Array.from({ length: 20 }, (_, i) => queue.submit("s", input(i + 2))));
expect(results.filter((result) => result.status === "fulfilled")).toHaveLength(19);
expect(pending).toHaveLength(20);
expect(enqueue).toHaveBeenCalledTimes(20);
});
it("does not reinject a consumed waiting input or infer absence after a native read failure", async () => {
const enqueue = mock(async () => {});
const runtime = { listPendingUserInputs: async () => [], agentMessageState: async () => "delivered" as const, enqueueUserInput: enqueue };
await expect(new WaitingResponseInputQueue(runtime).submit("s", input(1))).resolves.toMatchObject({ status: "delivered" });
expect(enqueue).not.toHaveBeenCalled();
const unavailable = { ...runtime, agentMessageState: async () => { throw new Error("native unavailable"); } };
await expect(new WaitingResponseInputQueue(unavailable).submit("s", input(2))).rejects.toThrow("native unavailable");
expect(enqueue).not.toHaveBeenCalled();
});
it("waits for admission at completion, deduplicates concurrent retries and closes only after native consumption", async () => {
let release = () => {};
const gate = new Promise<void>((resolve) => { release = resolve; });