Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/afk/__tests__/continuation.test.ts

Raw
import { createEventBus } from "@earendil-works/pi-coding-agent";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import afkExtension from "../index.ts";

type Handler = (event: never, ctx: never) => unknown;

function createHarness() {
	const handlers = new Map<string, Handler>();
	const commands = new Map<
		string,
		{ handler(args: string, ctx: never): Promise<void> }
	>();
	const sent: Array<{ customType: string; display: boolean }> = [];
	const events = createEventBus();
	let tool: { execute(): Promise<unknown> };
	let tools: string[] = [];
	let sessionId = "session-1";
	let idle = true;
	let pending = false;
	let sendError = false;
	const notify = vi.fn();
	const ctx = {
		mode: "print",
		sessionManager: { getSessionId: () => sessionId },
		isIdle: () => idle,
		hasPendingMessages: () => pending,
		ui: { notify },
	};
	afkExtension({
		events,
		on(name: string, handler: Handler) {
			handlers.set(name, handler);
		},
		registerCommand(
			name: string,
			command: { handler(args: string, ctx: never): Promise<void> },
		) {
			commands.set(name, command);
		},
		registerTool(definition: typeof tool) {
			tool = definition;
		},
		getActiveTools: () => tools,
		setActiveTools(names: string[]) {
			tools = names;
		},
		sendMessage(message: (typeof sent)[number]) {
			if (sendError) throw new Error("dispatch failed");
			sent.push(message);
		},
	} as never);
	const emit = (name: string, event: unknown = {}) =>
		handlers.get(name)?.(event as never, ctx as never);
	return {
		events,
		notify,
		emit,
		async command(name: string) {
			await commands.get(name)?.handler("", ctx as never);
		},
		async complete() {
			await tool.execute();
			emit("agent_before_settle", { outcome: "completed" });
		},
		compact(overrides: object = {}) {
			emit("session_compact", { willRetry: false, ...overrides });
		},
		continuations: () =>
			sent.filter((message) => message.customType === "afk-continue"),
		ownership(id = sessionId) {
			const states: unknown[] = [];
			events.emit("continuation:query", {
				sessionId: id,
				respond: (state: unknown) => states.push(state),
			});
			return states;
		},
		setIdle(value: boolean) {
			idle = value;
		},
		setPending(value: boolean) {
			pending = value;
		},
		setSessionId(value: string) {
			sessionId = value;
		},
		failDispatch() {
			sendError = true;
		},
	};
}

beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());

describe("AFK continuation ownership", () => {
	it("answers synchronously only for the active session, including napping state", async () => {
		const h = createHarness();
		expect(h.ownership()).toEqual([]);
		await h.command("afk");
		expect(h.ownership()).toEqual(["working"]);
		expect(h.ownership("foreign-session")).toEqual([]);
		for (const data of [
			null,
			{},
			{ sessionId: "session-1", respond: "invalid" },
		])
			h.events.emit("continuation:query", data);
		await h.complete();
		expect(h.ownership()).toEqual(["done"]);
		await h.command("back");
		expect(h.ownership()).toEqual([]);
	});

	it("owns new user work after napping and releases ownership on shutdown", async () => {
		const h = createHarness();
		await h.command("afk");
		await h.complete();
		h.emit("message_start", { message: { role: "user" } });
		expect(h.ownership()).toEqual(["working"]);
		h.emit("session_shutdown");
		expect(h.ownership()).toEqual([]);
		h.emit("session_start");
		await h.command("afk");
		expect(h.ownership()).toEqual(["working"]);
	});

	it("resumes idle compaction without Continuity, replacing duplicate schedules", async () => {
		const h = createHarness();
		await h.command("afk");
		h.compact();
		h.compact();
		await vi.runAllTimersAsync();
		expect(h.continuations()).toHaveLength(1);
		expect(h.continuations()[0]?.display).toBe(false);
	});

	it.each(["inactive", "done", "retry", "pending", "busy"])(
		"does not resume %s compaction",
		async (state) => {
			const h = createHarness();
			if (state !== "inactive") await h.command("afk");
			if (state === "done") await h.complete();
			if (state === "pending") h.setPending(true);
			if (state === "busy") h.setIdle(false);
			h.compact({ willRetry: state === "retry" });
			await vi.runAllTimersAsync();
			expect(h.continuations()).toEqual([]);
		},
	);

	it.each([
		"back",
		"start",
		"tree",
		"shutdown",
		"session",
		"pending",
		"completed",
		"abort",
	])("cancels stale compaction after %s", async (change) => {
		const h = createHarness();
		await h.command("afk");
		h.compact();
		if (change === "back") await h.command("back");
		if (change === "start") h.emit("agent_start");
		if (change === "tree") h.emit("session_tree");
		if (change === "shutdown") h.emit("session_shutdown");
		if (change === "session") h.setSessionId("replacement");
		if (change === "pending") h.setPending(true);
		if (change === "completed") await h.complete();
		if (change === "abort")
			h.emit("agent_before_settle", { outcome: "aborted" });
		await vi.runAllTimersAsync();
		expect(h.continuations()).toEqual([]);
	});

	it("disables AFK and reports a failed compaction dispatch", async () => {
		const h = createHarness();
		await h.command("afk");
		h.failDispatch();
		h.compact();
		await vi.runAllTimersAsync();
		expect(h.ownership()).toEqual([]);
		expect(h.notify).toHaveBeenLastCalledWith(
			"AFK could not resume: dispatch failed",
			"error",
		);
	});
});