// scenarios/agent-lifecycle.mjs — full plugin lifecycle against the real // daemon: hello→welcome, persisted events, online flag, events after disconnect. import { agentClient, agentEvent, agentHello, rest, sessionSnapshot, sid, waitUntil } from "../lib.mjs"; export async function run(ctx) { const { r, base, token, agentUrl } = ctx; const id = sid("e2e-life"); const snapshot = sessionSnapshot(id); const agent = await agentClient(agentUrl, token); const welcome = await agentHello(agent, snapshot); r.check("hello → welcome received", welcome !== null && welcome.type === "welcome"); r.check("welcome echoes sessionId", welcome?.sessionId === id, JSON.stringify(welcome ?? null)); r.check("welcome lastSeq 0 on fresh session", welcome?.lastSeq === 0, `got ${welcome?.lastSeq}`); r.check("welcome envelope v=1", welcome?.v === 1); agentEvent(agent, id, 1, "message_end", { message: { role: "user", id: "m1", text: "hello from e2e", thinking: null, toolCalls: [], toolCallId: null }, }); agentEvent(agent, id, 2, "tool_execution_start", { toolCallId: "tc1", toolName: "bash", args: { command: "ls" } }); agentEvent(agent, id, 3, "tool_execution_end", { toolCallId: "tc1", toolName: "bash", isError: false, resultPreview: "file1\nfile2" }); agentEvent(agent, id, 4, "agent_end", { usage: { inputTokens: 10, outputTokens: 5, totalCost: 0.25 } }); const listUrl = "/api/sessions"; const online = await waitUntil(async () => { const res = await rest(base, token, listUrl); return res.json?.find((s) => s.id === id)?.online === true; }); r.check("GET /api/sessions shows online=true while agent connected", online); const listed = (await rest(base, token, listUrl)).json.find((s) => s.id === id); r.check( "session snapshot fields exposed", listed?.cwd === "/work/e2e" && listed?.model === "glm-5.3" && listed?.provider === "zai-renaud" && listed?.agent === false && listed?.repo === null && typeof listed?.startedAt === "number" && typeof listed?.lastEventAt === "number", JSON.stringify(listed ?? null), ); agent.close(); const offline = await waitUntil(async () => { const res = await rest(base, token, listUrl); return res.json?.find((s) => s.id === id)?.online === false; }); r.check("online=false after agent disconnect", offline); const eventsUrl = `/api/sessions/${id}/events?after=0`; const persisted = await waitUntil( async () => (await rest(base, token, eventsUrl)).json?.length === 4, ); r.check("all 4 persisted events served after=0", persisted); const ev = (await rest(base, token, eventsUrl)).json ?? []; r.check( "event seqs ascending [1,2,3,4]", JSON.stringify(ev.map((e) => e.seq)) === "[1,2,3,4]", JSON.stringify(ev.map((e) => e.seq)), ); r.check( "event types in order", JSON.stringify(ev.map((e) => e.type)) === JSON.stringify(["message_end", "tool_execution_start", "tool_execution_end", "agent_end"]), JSON.stringify(ev.map((e) => e.type)), ); r.check( "persisted frames re-serialized with envelope", ev[0]?.v === 1 && ev[0]?.sessionId === id && typeof ev[0]?.ts === "number" && ev[0]?.message?.text === "hello from e2e", JSON.stringify(ev[0] ?? null), ); const tail = (await rest(base, token, `/api/sessions/${id}/events?after=2`)).json ?? []; r.check("events?after=2 returns only seq≥3", JSON.stringify(tail.map((e) => e.seq)) === "[3,4]", JSON.stringify(tail.map((e) => e.seq))); ctx.state.lifecycle = { id, snapshot, lastSeq: 4 }; }