95 lines
4.4 KiB
JavaScript
95 lines
4.4 KiB
JavaScript
// scenarios/prompt-routing.mjs — web-side subscribe → live event fan-out
|
|
// (including unpersisted message_update), REST prompt routed to the agent WS,
|
|
// 409 when the agent is offline.
|
|
|
|
import { agentClient, agentEvent, agentHello, rest, sessionSnapshot, sid, waitUntil, webClient } from "../lib.mjs";
|
|
|
|
export async function run(ctx) {
|
|
const { r, base, token, agentUrl, webUrl, state } = ctx;
|
|
const id = sid("e2e-prompt");
|
|
const snapshot = sessionSnapshot(id, { name: "prompt-routing", repo: "e2e/harness" });
|
|
|
|
const browser = await webClient(webUrl, token);
|
|
const initList = await browser.waitForFrame((f) => f.type === "session_list");
|
|
r.check("web WS sends initial session_list on connect", initList !== null);
|
|
browser.send({ type: "subscribe", sessionId: id });
|
|
|
|
const agent = await agentClient(agentUrl, token);
|
|
const welcome = await agentHello(agent, snapshot);
|
|
r.check("agent handshake under active web subscription", welcome?.type === "welcome");
|
|
|
|
// session change (hello registers the session) must reach the browser.
|
|
const listed = await browser.waitForFrame(
|
|
(f) => f.type === "session_list" && f.sessions?.some((s) => s.id === id),
|
|
);
|
|
r.check("session_list frame carries the new session", listed !== null);
|
|
const listedRow = listed?.sessions?.find((s) => s.id === id);
|
|
r.check(
|
|
"session_list row matches hello snapshot",
|
|
listedRow?.name === "prompt-routing" && listedRow?.repo === "e2e/harness" && listedRow?.online === true,
|
|
JSON.stringify(listedRow ?? null),
|
|
);
|
|
|
|
// Live fan-out: persisted + unpersisted kinds both batched to the subscriber.
|
|
agentEvent(agent, id, 1, "message_start", { message: { role: "assistant", id: "m1" } });
|
|
agentEvent(agent, id, 2, "message_update", { delta: "live delta one " });
|
|
agentEvent(agent, id, 3, "message_update", { delta: "live delta two" });
|
|
agentEvent(agent, id, 4, "message_end", {
|
|
message: { role: "assistant", id: "m1", text: "live delta one live delta two", thinking: null, toolCalls: [], toolCallId: null },
|
|
});
|
|
|
|
const frame = await browser.waitForFrame((f) => f.type === "events" && f.sessionId === id);
|
|
r.check("events frame received for subscribed session", frame !== null);
|
|
// Deltas flush in ≤40ms batches; wait until the seq-4 message_end fan-out lands.
|
|
const fanoutDone = await browser.waitForFrame(
|
|
(f) => f.type === "events" && f.sessionId === id && (f.events ?? []).some((e) => e.seq === 4),
|
|
);
|
|
r.check("browser received events through message_end (seq 4)", fanoutDone !== null);
|
|
const all = browser.frames
|
|
.filter((f) => f.type === "events" && f.sessionId === id)
|
|
.flatMap((f) => f.events ?? []);
|
|
r.check(
|
|
"browser saw live message_update deltas (seq 2,3)",
|
|
all.some((e) => e.seq === 2 && e.type === "message_update" && e.delta === "live delta one ") &&
|
|
all.some((e) => e.seq === 3 && e.delta === "live delta two"),
|
|
JSON.stringify(all.map((e) => `${e.seq}:${e.type}`)),
|
|
);
|
|
const evFrames = browser.frames.filter((f) => f.type === "events" && f.sessionId === id);
|
|
r.check(
|
|
"events frame after = first seq - 1",
|
|
evFrames.every((f) => f.after === (f.events?.[0]?.seq ?? f.after + 1) - 1),
|
|
JSON.stringify(evFrames.map((f) => f.after)),
|
|
);
|
|
|
|
// Prompt routing: REST → agent WS.
|
|
const prompted = await rest(base, token, `/api/sessions/${id}/prompt`, {
|
|
method: "POST",
|
|
body: { message: "e2e routed prompt" },
|
|
});
|
|
r.check("POST prompt while online → 200 {ok:true}", prompted.status === 200 && prompted.json?.ok === true, JSON.stringify(prompted.json));
|
|
|
|
const promptFrame = await agent.waitForFrame((f) => f.type === "prompt");
|
|
r.check("agent received prompt frame", promptFrame !== null);
|
|
r.check(
|
|
"prompt frame envelope + payload",
|
|
promptFrame?.v === 1 && promptFrame?.sessionId === id && promptFrame?.message === "e2e routed prompt" && typeof promptFrame?.promptId === "string",
|
|
JSON.stringify(promptFrame ?? null),
|
|
);
|
|
|
|
agent.close();
|
|
const goneOffline = await waitUntil(async () => {
|
|
const res = await rest(base, token, "/api/sessions");
|
|
return res.json?.find((s) => s.id === id)?.online === false;
|
|
});
|
|
r.check("agent offline after close", goneOffline);
|
|
|
|
const rejected = await rest(base, token, `/api/sessions/${id}/prompt`, {
|
|
method: "POST",
|
|
body: { message: "nobody home" },
|
|
});
|
|
r.check("POST prompt while offline → 409", rejected.status === 409, `got ${rejected.status}`);
|
|
|
|
browser.close();
|
|
state.prompt = { id };
|
|
}
|