web: open Agent at recent history and load older pages on demand
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
AgentRoomClient, AgentReadSourceChangedError, advanceAgentCursor, agentAuthFrame, agentDisplayMessages, agentUnreadCount,
|
||||
applyAgentEvent, applyAgentPage, applyAgentReadState, createAgentReadSession, createAgentReadWriter,
|
||||
createAgentRoomState, mergeAgentMessages, normalizeAgentCursor, parseAgentRoomEvent, syncAgentHistory,
|
||||
applyAgentHistoryPage, agentUnreadCountIsPartial,
|
||||
} from "../src/lib/agent-room.ts";
|
||||
import { createAgentOptimisticOutbox } from "../src/lib/agent-optimistic-outbox.ts";
|
||||
|
||||
@@ -32,7 +33,7 @@ test("tool starts and results merge across replay without downgrading completed
|
||||
assert.equal(messages[0].createdAt, start.createdAt);
|
||||
});
|
||||
|
||||
test("initial synchronization reads every page including invisible entries and counts only unread assistant messages", async () => {
|
||||
test("forward recovery reads every missing page including invisible entries and counts only unread assistant messages", async () => {
|
||||
const requests = [];
|
||||
const responses = [page("2", [message(1), message(2, "user")], true), page("4", [], true), page("7", [message(5), message(6, "tool"), message(7)], false)];
|
||||
let state = createAgentRoomState();
|
||||
@@ -47,6 +48,78 @@ test("initial synchronization reads every page including invisible entries and c
|
||||
assert.equal(agentUnreadCount(state), 1);
|
||||
});
|
||||
|
||||
test("recent history is one cursorless window and older history uses the returned source boundary", async () => {
|
||||
const requests = [];
|
||||
const client = new AgentRoomClient({ fetch: async (url, options) => {
|
||||
requests.push({ url, signal: options.signal });
|
||||
return new Response(JSON.stringify({ ...page("1200", [message(1200)], true), beforeCursor: "1101" }));
|
||||
} });
|
||||
const controller = new AbortController();
|
||||
let state = applyAgentHistoryPage(createAgentRoomState(), await client.history(undefined, controller.signal));
|
||||
assert.equal(requests.length, 1, "hasMore must not drain old history");
|
||||
assert.equal(requests[0].url, "/api/v1/agent/messages?limit=100");
|
||||
assert.equal(requests[0].signal, controller.signal);
|
||||
assert.equal(state.historyLoaded, true);
|
||||
assert.equal(state.cursor, "1200");
|
||||
assert.equal(state.beforeCursor, "1101", "boundary is not derived from visible message 1200");
|
||||
assert.equal(state.hasOlder, true);
|
||||
await client.history(state.beforeCursor, controller.signal);
|
||||
assert.equal(requests[1].url, "/api/v1/agent/messages?before=1101&limit=100");
|
||||
});
|
||||
|
||||
test("delayed older pages preserve newer edits, live cursor, source and read state", () => {
|
||||
let state = applyAgentHistoryPage(sourceRoom(), { ...page("100", [message(100)], true), beforeCursor: "90" });
|
||||
state = applyAgentPage(state, page("110", [message(100, "assistant", { content: "New streamed text" }), message(110)]));
|
||||
const next = applyAgentHistoryPage(state, { ...page("89", [message(80), message(100, "assistant", { content: "Old snapshot" })], true), beforeCursor: "70" }, "90");
|
||||
assert.equal(next.messages.find(m => m.id === "entry:100").content, "New streamed text");
|
||||
assert.equal(next.cursor, "110");
|
||||
assert.equal(next.readCursor, "90");
|
||||
assert.equal(next.sourceId, sourceA);
|
||||
assert.equal(next.beforeCursor, "70");
|
||||
assert.equal(next.hasOlder, true);
|
||||
assert.equal(applyAgentHistoryPage(next, page("89", [message(80)], false), "90"), next, "retired boundary cannot replace the next window");
|
||||
});
|
||||
|
||||
test("empty internal pages advance older boundaries without chasing another page or moving read state", () => {
|
||||
const initial = applyAgentHistoryPage(createAgentRoomState(), { ...page("1200", [], true), beforeCursor: "1101" });
|
||||
assert.equal(initial.historyLoaded, true);
|
||||
assert.equal(initial.messages.length, 0);
|
||||
const next = applyAgentHistoryPage(initial, { ...page("1100", [], true), beforeCursor: "1001" }, "1101");
|
||||
assert.equal(next.beforeCursor, "1001");
|
||||
assert.equal(next.cursor, "1200");
|
||||
assert.equal(next.readCursor, "0");
|
||||
assert.equal(next.hasOlder, true);
|
||||
const end = applyAgentHistoryPage(next, { ...page("0"), beforeCursor: "1001" }, "1001");
|
||||
assert.equal(end.hasOlder, false);
|
||||
assert.equal(end.cursor, "1200");
|
||||
});
|
||||
|
||||
test("malformed or stalled older pages cannot change history or delivery boundaries", () => {
|
||||
const state = applyAgentHistoryPage(createAgentRoomState(), { ...page("100", [message(100)], true), beforeCursor: "90" });
|
||||
for (const bad of [
|
||||
{ ...page("89", [], true), beforeCursor: "90" },
|
||||
{ ...page("89", [], true), beforeCursor: null },
|
||||
{ ...page("91", [], false), beforeCursor: "80" },
|
||||
{ ...page("89", [], false), beforeCursor: "91" },
|
||||
{ ...page("bad", [], false), beforeCursor: "80" },
|
||||
]) assert.throws(() => applyAgentHistoryPage(state, bad, "90"), /cursor|boundary/);
|
||||
assert.throws(() => applyAgentHistoryPage(state, { ...page("89"), conversationId: "different" }, "90"), /conversation changed/);
|
||||
assert.equal(state.beforeCursor, "90");
|
||||
assert.equal(state.cursor, "100");
|
||||
});
|
||||
|
||||
test("unread counts disclose unknown older replies until reading the latest rendered window", () => {
|
||||
let state = applyAgentHistoryPage(createAgentRoomState(), { ...page("1200", [message(1199), message(1200, "user")], true), beforeCursor: "1101" });
|
||||
assert.equal(agentUnreadCount(state), 1);
|
||||
assert.equal(agentUnreadCountIsPartial(state), true);
|
||||
state = applyAgentReadState(state, { cursor: "1100", updatedAt: null });
|
||||
assert.equal(agentUnreadCountIsPartial(state), false);
|
||||
assert.equal(agentUnreadCount(state), 1);
|
||||
state = applyAgentReadState(state, { cursor: "1200", updatedAt: null });
|
||||
assert.equal(agentUnreadCount(state), 0);
|
||||
assert.equal(state.hasOlder, true, "reading latest does not fetch or discard older pages");
|
||||
});
|
||||
|
||||
test("queued user messages keep stable IDs and disappear if withdrawn or promoted", () => {
|
||||
const pending = message(1, "user", { id: "inbound:sybil:request1", metadata: { kind: "message", status: "queued" } });
|
||||
let state = applyAgentEvent(createAgentRoomState(), { type: "activity", conversationId: "1", activity: activity([pending]) });
|
||||
|
||||
Reference in New Issue
Block a user