Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/notes/__tests__/contract.test.ts

Raw
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { fauxAssistantMessage } from "@earendil-works/pi-ai";
import {
	type ExtensionUIContext,
	initTheme,
	SessionManager,
} from "@earendil-works/pi-coding-agent";
import { Text } from "@earendil-works/pi-tui";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
	createPiSession,
	type PiSession,
} from "../../../test/fixture/create-pi-session.ts";
import { __test } from "../index.ts";

// --- Owned-fixture harness shim -------------------------------------------
//
// Reproduces the behavior the legacy `@marcfargas/pi-test-harness` provided to
// the notes tests: a recording UI context (so `hasUI` is true and `ctx.ui.*`
// calls are captured the way `t.events.ui` was), configurable `confirm`/`editor`
// responses, and a `message_end` collector equivalent to `t.events.messages`.

interface UIConfig {
	confirm?: boolean | ((title: string, message: string) => boolean);
	editor?: string | ((title: string, prefill?: string) => string | undefined);
}

interface Box {
	session: PiSession;
	cwd: string;
	calls: Array<{ method: string; args: unknown[] }>;
	messages: Array<Record<string, unknown>>;
}

function extensionPath(): string {
	return fileURLToPath(new URL("../index.ts", import.meta.url));
}

function buildRecordingUI(
	config: UIConfig,
	calls: Array<{ method: string; args: unknown[] }>,
): ExtensionUIContext {
	const record = (method: string, args: unknown[]): void => {
		calls.push({ method, args });
	};
	const ui = {
		async select(title: string, options: string[]) {
			record("select", [title, options]);
			return options[0];
		},
		async confirm(title: string, message: string) {
			const handler = config.confirm;
			const result =
				typeof handler === "boolean"
					? handler
					: typeof handler === "function"
						? handler(title, message)
						: true;
			record("confirm", [title, message]);
			return result;
		},
		async input(title: string, placeholder?: string) {
			record("input", [title, placeholder]);
			return "";
		},
		async editor(title: string, prefill?: string) {
			const handler = config.editor;
			const result =
				typeof handler === "string"
					? handler
					: typeof handler === "function"
						? handler(title, prefill)
						: "";
			record("editor", [title, prefill]);
			return result;
		},
		notify(message: string, type?: string) {
			record("notify", [message, type]);
		},
		onTerminalInput() {
			return () => {};
		},
		setStatus(key: string, text: string) {
			record("setStatus", [key, text]);
		},
		setWorkingMessage(message: string) {
			record("setWorkingMessage", [message]);
		},
		setWidget(key: string, content: unknown) {
			record("setWidget", [key, content]);
		},
		setFooter(...args: unknown[]) {
			record("setFooter", args);
		},
		setHeader(...args: unknown[]) {
			record("setHeader", args);
		},
		setTitle(title: string) {
			record("setTitle", [title]);
		},
		async custom() {
			return undefined;
		},
		pasteToEditor(...args: unknown[]) {
			record("pasteToEditor", args);
		},
		setEditorText(...args: unknown[]) {
			record("setEditorText", args);
		},
		getEditorText() {
			return "";
		},
		setEditorComponent(...args: unknown[]) {
			record("setEditorComponent", args);
		},
		get theme() {
			return {
				fg: (_color: unknown, text: string) => text,
				bold: (text: string) => text,
				italic: (text: string) => text,
				strikethrough: (text: string) => text,
			};
		},
		getAllThemes() {
			return [];
		},
		getTheme() {
			return undefined;
		},
		setTheme() {
			return { success: false, error: "Test mode" };
		},
		getToolsExpanded() {
			return false;
		},
		setToolsExpanded() {},
	};
	return ui as unknown as ExtensionUIContext;
}

async function start(
	options: { mockUI?: UIConfig; cwd?: string } = {},
): Promise<Box> {
	const session = await createPiSession({
		extensionPaths: [extensionPath()],
		...(options.cwd ? { cwd: options.cwd } : {}),
	});
	const calls: Array<{ method: string; args: unknown[] }> = [];
	const messages: Array<Record<string, unknown>> = [];
	session.session.extensionRunner.setUIContext(
		buildRecordingUI(options.mockUI ?? {}, calls),
		"print",
	);
	session.session.subscribe((event) => {
		if (event.type === "message_end") {
			messages.push((event as { message: Record<string, unknown> }).message);
		}
	});
	return { session, cwd: session.cwd, calls, messages };
}

let box: Box | undefined;

function s(): Box["session"]["session"] {
	if (!box) throw new Error("missing test session");
	return box.session.session;
}

/** Loaded extensions via the fixture's public API (never runner internals). */
function exts(): Box["session"]["extensions"] {
	if (!box) throw new Error("missing test session");
	return box.session.extensions;
}

/** Typed view over the runner's message-sending runtime for test stubbing. */
function runtimeOf(session: Box["session"]["session"]): {
	sendUserMessage: (content: unknown, options: unknown) => unknown;
} {
	return (
		session.extensionRunner as unknown as {
			runtime: {
				sendUserMessage: (content: unknown, options: unknown) => unknown;
			};
		}
	).runtime;
}

/** The notes extension's single `note` command; throws if absent. */
function noteCommandOf() {
	const command = exts()[0]!.commands.get("note");
	if (!command) throw new Error("missing note command");
	return command;
}

/** The notes extension's `alt+n` shortcut; throws if absent. */
function noteShortcutOf() {
	const shortcut = exts()[0]!.shortcuts.get("alt+n");
	if (!shortcut) throw new Error("missing note shortcut");
	return shortcut;
}

function uiArgs(method: string): unknown[][] {
	return (box?.calls ?? [])
		.filter((call) => call.method === method)
		.map((call) => call.args);
}

function uiCallsFor(
	target: Box,
	method: string,
): Array<{ method: string; args: unknown[] }> {
	return target.calls.filter((call) => call.method === method);
}

function customEntries(kind: string): Array<Record<string, unknown>> {
	return (
		s().sessionManager.getEntries() as Array<{
			type: string;
			customType?: string;
			data?: Record<string, unknown>;
		}>
	)
		.filter((entry) => entry.type === "custom" && entry.customType === kind)
		.map((entry) => entry.data ?? {});
}

function latestWidget(): unknown {
	return uiArgs("setWidget")
		.filter((args) => args[0] === "notes")
		.map((args) => args[1])
		.at(-1);
}

function renderLatestWidget(width = 80): string[] | undefined {
	const widget = latestWidget();
	if (!widget) return undefined;
	if (Array.isArray(widget)) {
		return widget.flatMap((line) => new Text(line, 1, 0).render(width));
	}
	if (typeof widget === "function") {
		return (widget({}, {}) as { render(width: number): string[] }).render(
			width,
		);
	}
	throw new Error("unknown widget shape");
}

async function noteCommand(args: string): Promise<void> {
	const command = noteCommandOf();
	await command.handler(args, s().extensionRunner.createCommandContext());
}

afterEach(async () => {
	vi.restoreAllMocks();
	__test.clearLaunch();
	__test.takeNewSessionNotes();
	await box?.session.dispose();
	box = undefined;
});

describe("notes contract", () => {
	it("loads in real Pi extension runtime and registers command plus shortcut", async () => {
		box = await start();
		const [extension] = exts();
		if (!extension) throw new Error("missing extension");

		expect(extension.path).toBe(extensionPath());
		expect([...extension.commands.keys()]).toEqual(["note"]);
		expect([...extension.tools.keys()]).toEqual([]);
		expect(extension.shortcuts.has("alt+n")).toBe(true);
	});

	it("renders compact count and last note after add", async () => {
		box = await start();
		await noteCommand("add first note");
		await noteCommand("add second note");

		expect(renderLatestWidget()).toEqual([
			"󰎚  2 notes · alt+n expand · last: second note",
		]);
	});

	it("renders widget at column zero in Pi", async () => {
		box = await start();
		await noteCommand("add first note");

		expect(renderLatestWidget()?.[0]).toMatch(/^󰎚/u);
	});

	it("toggles expanded widget with slash command and shortcut", async () => {
		box = await start();
		await noteCommand("add first note");
		await noteCommand("add second note");
		await noteCommand("");

		expect(renderLatestWidget()).toEqual([
			"󰎚  notes · 2",
			"   1  first note",
			"   2  second note",
		]);

		const shortcut = noteShortcutOf();
		await shortcut.handler(s().extensionRunner.createCommandContext());

		expect(renderLatestWidget()).toEqual([
			"󰎚  2 notes · alt+n expand · last: second note",
		]);
	});

	it("shows compact widget on session_start when current branch has pending notes", async () => {
		box = await start();
		(s().sessionManager as any).appendCustomEntry("notes-state", {
			op: "add",
			id: 7,
			text: "restored note",
			createdAt: 1,
		});

		await s().extensionRunner.emit({
			type: "session_start",
			reason: "resume",
		});

		expect(renderLatestWidget()).toEqual([
			"󰎚  1 notes · alt+n expand · last: restored note",
		]);
	});

	it("restores notes after tree navigation", async () => {
		box = await start();
		await noteCommand("add abandoned note");

		const manager = s().sessionManager as any;
		manager.resetLeaf();
		manager.appendCustomEntry("notes-state", {
			op: "add",
			id: 7,
			text: "target note",
			createdAt: 1,
		});
		await s().extensionRunner.emit({
			type: "session_tree",
			oldLeafId: "old",
			newLeafId: manager.getLeafId(),
		});

		expect(renderLatestWidget()).toEqual([
			"󰎚  1 notes · alt+n expand · last: target note",
		]);
		await noteCommand("add next note");
		expect(customEntries("notes-state").at(-1)).toEqual({
			op: "add",
			id: 8,
			key: expect.any(String),
			text: "next note",
			createdAt: expect.any(Number),
		});
	});

	it("offers to carry pending notes into a new session", async () => {
		box = await start({ mockUI: { confirm: true } });
		await noteCommand("add carried note");
		await s().extensionRunner.emit({
			type: "session_shutdown",
			reason: "new",
		});
		(s().sessionManager as any).newSession();
		await s().extensionRunner.emit({
			type: "session_start",
			reason: "new",
		});

		expect(uiArgs("confirm").at(-1)).toEqual([
			"Carry notes?",
			"Copy 1 pending notes into the new session?",
		]);
		expect(customEntries("notes-state")).toEqual([
			{
				op: "add",
				id: 1,
				key: expect.any(String),
				text: "carried note",
				createdAt: expect.any(Number),
			},
		]);
	});

	it("discards pending notes when new-session carry is rejected", async () => {
		box = await start({ mockUI: { confirm: false } });
		await noteCommand("add old note");
		await s().extensionRunner.emit({
			type: "session_shutdown",
			reason: "new",
		});
		(s().sessionManager as any).newSession();
		await s().extensionRunner.emit({
			type: "session_start",
			reason: "new",
		});

		expect(customEntries("notes-state")).toEqual([]);
		expect(latestWidget()).toBeUndefined();
	});

	it("leaves widget hidden and notifies when toggling with no notes", async () => {
		box = await start();
		await noteCommand("");

		expect(uiArgs("notify").at(-1)).toEqual(["No pending notes", "info"]);
		expect(latestWidget()).toBeUndefined();
	});

	it("removes one note and clears widget when none remain", async () => {
		box = await start();
		await noteCommand("add first note");
		await noteCommand("rm 1");

		expect(customEntries("notes-state").at(-1)).toEqual({
			op: "remove",
			id: 1,
			removedAt: expect.any(Number),
		});
		expect(latestWidget()).toBeUndefined();
	});

	it("confirms before clearing all pending notes", async () => {
		box = await start({ mockUI: { confirm: true } });
		await noteCommand("add first note");
		await noteCommand("add second note");
		await noteCommand("clear");

		expect(uiArgs("confirm").at(-1)).toEqual([
			"Clear notes?",
			"Remove 2 pending notes?",
		]);
		expect(customEntries("notes-state").at(-1)).toEqual({
			op: "clear",
			clearedAt: expect.any(Number),
		});
		expect(latestWidget()).toBeUndefined();
	});

	it("keeps notes when clear confirmation is rejected", async () => {
		box = await start({ mockUI: { confirm: false } });
		await noteCommand("add first note");
		await noteCommand("clear");

		expect(customEntries("notes-state")).toHaveLength(1);
		expect(renderLatestWidget()).toEqual([
			"󰎚  1 notes · alt+n expand · last: first note",
		]);
	});

	it("edits note text and keeps old text on cancel or whitespace save", async () => {
		box = await start({ mockUI: { editor: "edited note" } });
		await noteCommand("add old note");
		await noteCommand("edit 1");

		expect(uiArgs("editor").at(-1)).toEqual(["Edit note 1", "old note"]);
		expect(customEntries("notes-state").at(-1)).toEqual({
			op: "edit",
			id: 1,
			text: "edited note",
			updatedAt: expect.any(Number),
		});
		expect(renderLatestWidget()).toEqual([
			"󰎚  1 notes · alt+n expand · last: edited note",
		]);

		await box.session.dispose();
		box = await start({ mockUI: { editor: () => undefined } });
		await noteCommand("add stable note");
		await noteCommand("edit 1");
		expect(customEntries("notes-state")).toHaveLength(1);

		await box.session.dispose();
		box = await start({ mockUI: { editor: "   " } });
		await noteCommand("add stable note");
		await noteCommand("edit 1");
		expect(customEntries("notes-state")).toHaveLength(1);
		expect(uiArgs("notify").at(-1)).toEqual(["Note text required", "warning"]);
	});

	it("unknown ids do not mutate state", async () => {
		box = await start();
		await noteCommand("add first note");
		await noteCommand("rm 99");
		await noteCommand("edit 99");

		expect(customEntries("notes-state")).toHaveLength(1);
		expect(uiArgs("notify").at(-1)).toEqual(["Unknown note id: 99", "error"]);
	});

	it("sends exact note text when idle and removes it", async () => {
		box = await start();
		const sent: Array<{ content: unknown; options: unknown }> = [];
		runtimeOf(s()).sendUserMessage = (content: unknown, options: unknown) =>
			sent.push({ content, options });

		await noteCommand("add exact text");
		await noteCommand("send 1");

		expect(sent).toEqual([
			{
				content: "exact text",
				options: { expandPromptTemplates: true, deliverAs: undefined },
			},
		]);
		expect(customEntries("notes-state").at(-1)).toEqual({
			op: "remove",
			id: 1,
			removedAt: expect.any(Number),
		});
		expect(latestWidget()).toBeUndefined();
	});

	it("queues send as follow-up when busy", async () => {
		box = await start();
		const sent: Array<{ content: unknown; options: unknown }> = [];
		runtimeOf(s()).sendUserMessage = (content: unknown, options: unknown) =>
			sent.push({ content, options });

		await noteCommand("add busy text");
		const command = noteCommandOf();
		const ctx = s().extensionRunner.createCommandContext();
		ctx.isIdle = () => false;
		await command.handler("send 1", ctx);

		expect(sent).toEqual([
			{
				content: "busy text",
				options: { expandPromptTemplates: true, deliverAs: "followUp" },
			},
		]);
		expect(customEntries("notes-state").at(-1)).toEqual({
			op: "remove",
			id: 1,
			removedAt: expect.any(Number),
		});
	});

	it.each([false, true])(
		"expands Pi templates with arguments at send time (busy=%s)",
		async (busy) => {
			box = await start();
			await noteCommand('add /review "src/main.ts"');
			vi.spyOn(s(), "promptTemplates", "get").mockReturnValue([
				{
					name: "review",
					description: "Review a file",
					content: "Review $1 carefully.",
				},
			] as never);

			if (busy) {
				vi.spyOn(s(), "isStreaming", "get").mockReturnValue(true);
				const command = noteCommandOf();
				const ctx = s().extensionRunner.createCommandContext();
				ctx.isIdle = () => false;
				await command.handler("send 1", ctx);
				await expect
					.poll(() => s().getFollowUpMessages())
					.toEqual(["Review src/main.ts carefully."]);
			} else {
				box.session.setResponses([fauxAssistantMessage("Reviewed.")]);
				await noteCommand("send 1");
				await expect
					.poll(() => box?.session.providerState.callCount)
					.toBeGreaterThan(0);
				await expect.poll(() => s().isIdle).toBe(true);
				expect(box.messages).toContainEqual(
					expect.objectContaining({
						role: "user",
						content: [{ type: "text", text: "Review src/main.ts carefully." }],
					}),
				);
			}
			expect(customEntries("notes-state").at(-1)?.["op"]).toBe("remove");
			expect(latestWidget()).toBeUndefined();
		},
	);

	it("unknown send id does not send or mutate", async () => {
		box = await start();
		const sent: Array<{ content: unknown; options: unknown }> = [];
		runtimeOf(s()).sendUserMessage = (content: unknown, options: unknown) =>
			sent.push({ content, options });
		await noteCommand("add stable text");
		await noteCommand("send 99");

		expect(sent).toEqual([]);
		expect(customEntries("notes-state")).toHaveLength(1);
		expect(uiArgs("notify").at(-1)).toEqual(["Unknown note id: 99", "error"]);
	});

	it("clones through the current leaf and consumes the persisted source note", async () => {
		box = await start();
		await noteCommand("add launch this");
		await noteCommand("add leave this pending");
		const runner = s().extensionRunner;
		const source = SessionManager.create(box.cwd, join(box.cwd, "sessions"));
		source.appendMessage({
			role: "user",
			content: "source prompt",
			timestamp: Date.now(),
		});
		source.appendMessage({
			role: "assistant",
			content: [{ type: "text", text: "source response" }],
			timestamp: Date.now(),
		} as never);
		for (const entry of customEntries("notes-state")) {
			source.appendCustomEntry("notes-state", entry);
		}
		const sourceFile = source.getSessionFile();
		const sourceLeafId = source.getLeafId();
		expect(sourceFile).toBeDefined();
		if (sourceFile === undefined) throw new Error("missing source file");
		const command = noteCommandOf();
		const ctx = runner.createCommandContext() as any;
		const sourceView = Object.create(ctx.sessionManager);
		sourceView.getSessionFile = () => sourceFile;
		sourceView.getSessionId = () => source.getSessionId();
		sourceView.getLeafId = () => sourceLeafId;
		Object.defineProperty(ctx, "sessionManager", { value: sourceView });
		const sent: Array<{ content: unknown; options: unknown }> = [];
		ctx.fork = vi.fn(async (entryId, options) => {
			expect(entryId).toBe(sourceLeafId);
			expect(options.position).toBe("at");
			await runner.emit({
				type: "session_shutdown",
				reason: "fork",
			});
			await runner.emit({
				type: "session_start",
				reason: "fork",
				previousSessionFile: sourceFile,
			});
			await options.withSession({
				...ctx,
				sendUserMessage: async (content: unknown, sendOptions: unknown) => {
					sent.push({ content, options: sendOptions });
				},
			});
			return { cancelled: false };
		});

		await command.handler("clone 1", ctx);

		expect(sent).toEqual([
			{
				content: "launch this",
				options: { expandPromptTemplates: true },
			},
		]);
		expect(renderLatestWidget()).toEqual([
			"󰎚  1 notes · alt+n expand · last: leave this pending",
		]);
		expect(uiArgs("setEditorText").at(-1)).toEqual([""]);
		const sourceState = __test.replayNotes(
			SessionManager.open(sourceFile ?? "").getBranch() as any,
		);
		expect(__test.pendingNotes(sourceState).map((note) => note.text)).toEqual([
			"leave this pending",
		]);
	});

	it("forks before the message chosen with Pi's selector", async () => {
		box = await start();
		initTheme("dark", false);
		const runner = s().extensionRunner;
		const manager = s().sessionManager as any;
		const messageId = manager.appendMessage({
			role: "user",
			content: [{ type: "text", text: "replace this prompt" }],
			timestamp: Date.now(),
		});
		await runner.emit({
			type: "session_tree",
			oldLeafId: "old",
			newLeafId: messageId,
		});
		await noteCommand("add replacement note");
		const command = noteCommandOf();
		const ctx = runner.createCommandContext() as any;
		Object.defineProperty(ctx, "mode", { configurable: true, value: "tui" });
		ctx.ui.custom = vi.fn(async (factory) => {
			const component = factory({ requestRender() {} }, {}, {}, () => {});
			expect(component.render(80).join("\n")).toContain("replace this prompt");
			return messageId;
		});
		const sent: string[] = [];
		ctx.fork = vi.fn(async (entryId, options) => {
			expect(entryId).toBe(messageId);
			expect(options.position).toBe("before");
			await runner.emit({
				type: "session_shutdown",
				reason: "fork",
			});
			manager.resetLeaf();
			await runner.emit({
				type: "session_start",
				reason: "fork",
			});
			await options.withSession({
				...ctx,
				sendUserMessage: async (content: string) => sent.push(content),
			});
			return { cancelled: false };
		});

		await command.handler("fork 1", ctx);

		expect(ctx.ui.custom).toHaveBeenCalledOnce();
		expect(sent).toEqual(["replacement note"]);
		expect(latestWidget()).toBeUndefined();
	});

	it("starts an independent fresh extension session without carrying other notes", async () => {
		box = await start();
		const destination = await start();
		try {
			await noteCommand("add launch this");
			await noteCommand("add do not carry this");
			const sourceRunner = s().extensionRunner;
			const destinationRunner = destination.session.session.extensionRunner;
			const destinationManager = destination.session.session
				.sessionManager as any;
			const command = noteCommandOf();
			const sourceCtx = sourceRunner.createCommandContext() as any;
			const sent: string[] = [];
			sourceCtx.newSession = vi.fn(async (options) => {
				expect(options.parentSession).toBeUndefined();
				await sourceRunner.emit({
					type: "session_shutdown",
					reason: "new",
				});
				await destinationRunner.emit({
					type: "session_start",
					reason: "new",
				});
				await options.withSession({
					...(destinationRunner.createCommandContext() as any),
					sendUserMessage: async (content: string) => sent.push(content),
				});
				return { cancelled: false };
			});

			await command.handler("new 1", sourceCtx);

			expect(sent).toEqual(["launch this"]);
			expect(uiCallsFor(destination, "confirm")).toEqual([]);
			expect(
				uiCallsFor(destination, "setWidget")
					.filter((call) => call.args[0] === "notes")
					.at(-1)?.args[1],
			).toBeUndefined();
			expect(destinationManager.getHeader().parentSession).toBeUndefined();
		} finally {
			await destination.session.dispose();
		}
	});

	it("keeps the note in the destination when delivery fails", async () => {
		box = await start();
		await noteCommand("add recover me");
		const runner = s().extensionRunner;
		const manager = s().sessionManager as any;
		const command = noteCommandOf();
		const ctx = runner.createCommandContext() as any;
		ctx.newSession = vi.fn(async (options) => {
			await runner.emit({
				type: "session_shutdown",
				reason: "new",
			});
			manager.newSession();
			await runner.emit({
				type: "session_start",
				reason: "new",
			});
			await options.withSession({
				...ctx,
				sendUserMessage: async () => {
					throw new Error("dispatch failed");
				},
			});
			return { cancelled: false };
		});

		await command.handler("new 1", ctx);

		expect(renderLatestWidget()).toEqual([
			"󰎚  1 notes · alt+n expand · last: recover me",
		]);
		expect(uiArgs("notify").at(-1)).toEqual([
			"Note remains pending: dispatch failed",
			"error",
		]);
	});

	it("reserves the destination note while delivery is pending", async () => {
		box = await start();
		await noteCommand("add in flight");
		const runner = s().extensionRunner;
		const manager = s().sessionManager as any;
		const command = noteCommandOf();
		const ctx = runner.createCommandContext() as any;
		let finishDelivery: (() => void) | undefined;
		ctx.newSession = vi.fn(async (options) => {
			await runner.emit({
				type: "session_shutdown",
				reason: "new",
			});
			manager.newSession();
			await runner.emit({
				type: "session_start",
				reason: "new",
			});
			await options.withSession({
				...ctx,
				sendUserMessage: () =>
					new Promise<void>((resolve) => {
						finishDelivery = resolve;
					}),
			});
			return { cancelled: false };
		});

		const delivery = command.handler("new 1", ctx);
		await expect.poll(() => finishDelivery).toBeTypeOf("function");
		await command.handler("edit 1", runner.createCommandContext());

		expect(uiArgs("editor")).toEqual([]);
		expect(uiArgs("notify").at(-1)).toEqual([
			"Note 1 is being delivered",
			"warning",
		]);
		finishDelivery?.();
		await delivery;
		expect(latestWidget()).toBeUndefined();
	});

	it("claims the destination once across a nested in-memory switch", async () => {
		box = await start({ mockUI: { confirm: true } });
		await noteCommand("add switch again");
		const runner = s().extensionRunner;
		const manager = s().sessionManager as any;
		const command = noteCommandOf();
		const ctx = runner.createCommandContext() as any;
		ctx.newSession = vi.fn(async (options) => {
			await runner.emit({
				type: "session_shutdown",
				reason: "new",
			});
			manager.newSession();
			await runner.emit({
				type: "session_start",
				reason: "new",
			});
			await options.withSession({
				...ctx,
				sendUserMessage: async () => {
					await command.handler(
						"add nested note",
						runner.createCommandContext(),
					);
					await runner.emit({
						type: "session_shutdown",
						reason: "new",
					});
					manager.newSession();
					await runner.emit({
						type: "session_start",
						reason: "new",
					});
				},
			});
			return { cancelled: false };
		});

		await expect(command.handler("new 1", ctx)).resolves.toBeUndefined();
		expect(uiArgs("confirm").at(-1)).toEqual([
			"Carry notes?",
			"Copy 2 pending notes into the new session?",
		]);
		expect(renderLatestWidget()).toEqual([
			"󰎚  2 notes · alt+n expand · last: nested note",
		]);

		await command.handler("rm 1", runner.createCommandContext());
		expect(renderLatestWidget()).toEqual([
			"󰎚  1 notes · alt+n expand · last: nested note",
		]);
	});

	it("keeps the source note when a transition is cancelled or busy", async () => {
		box = await start();
		await noteCommand("add stable note");
		const runner = s().extensionRunner;
		const command = noteCommandOf();
		const cancelled = runner.createCommandContext() as any;
		cancelled.newSession = vi.fn(async () => ({ cancelled: true }));
		await command.handler("new 1", cancelled);

		const busy = runner.createCommandContext() as any;
		busy.isIdle = () => false;
		busy.newSession = vi.fn();
		await command.handler("new 1", busy);

		expect(renderLatestWidget()).toEqual([
			"󰎚  1 notes · alt+n expand · last: stable note",
		]);
		expect(busy.newSession).not.toHaveBeenCalled();
		expect(uiArgs("notify").at(-1)).toEqual([
			"Cannot new while the agent is busy",
			"warning",
		]);
	});

	it("rejects a persisted source that has not been saved yet", async () => {
		box = await start();
		await noteCommand("add stable note");
		const runner = s().extensionRunner;
		const command = noteCommandOf();
		const base = runner.createCommandContext() as any;
		const sessionManager = Object.create(base.sessionManager);
		sessionManager.getSessionFile = () => join(box!.cwd, "not-created.jsonl");
		const ctx = Object.create(base);
		Object.defineProperty(ctx, "sessionManager", { value: sessionManager });
		ctx.newSession = vi.fn();

		await command.handler("new 1", ctx);

		expect(ctx.newSession).not.toHaveBeenCalled();
		expect(uiArgs("notify").at(-1)).toEqual([
			"Current session is not saved yet; wait for the first assistant response",
			"warning",
		]);
		expect(renderLatestWidget()).toEqual([
			"󰎚  1 notes · alt+n expand · last: stable note",
		]);
	});

	it("autocompletes subcommands and pending note ids with previews", async () => {
		box = await start();
		const command = noteCommandOf();
		await noteCommand("add consider split parser tests");
		await noteCommand("add ask whether command should support autocomplete");
		if (!command.getArgumentCompletions)
			throw new Error("missing getArgumentCompletions");

		expect(command.getArgumentCompletions("")).toEqual([
			{ value: "add", label: "add" },
			{ value: "send", label: "send" },
			{ value: "clone", label: "clone" },
			{ value: "fork", label: "fork" },
			{ value: "new", label: "new" },
			{ value: "edit", label: "edit" },
			{ value: "rm", label: "rm" },
			{ value: "clear", label: "clear" },
		]);
		expect(command.getArgumentCompletions("send ")).toEqual([
			{ value: "send 1", label: "1  consider split parser tests" },
			{
				value: "send 2",
				label: "2  ask whether command should support autocomplete",
			},
		]);
		expect(command.getArgumentCompletions("clone 1")).toEqual([
			{ value: "clone 1", label: "1  consider split parser tests" },
		]);
		expect(command.getArgumentCompletions("fork 2")).toEqual([
			{
				value: "fork 2",
				label: "2  ask whether command should support autocomplete",
			},
		]);
		expect(command.getArgumentCompletions("new 1")).toEqual([
			{ value: "new 1", label: "1  consider split parser tests" },
		]);
		expect(command.getArgumentCompletions("edit 2")).toEqual([
			{
				value: "edit 2",
				label: "2  ask whether command should support autocomplete",
			},
		]);
		expect(command.getArgumentCompletions("rm ")).toEqual([
			{ value: "rm 1", label: "1  consider split parser tests" },
			{
				value: "rm 2",
				label: "2  ask whether command should support autocomplete",
			},
		]);
	});

	it("replay uses only entries returned by the current branch", () => {
		const state = __test.replayNotes([
			{
				type: "custom",
				customType: "notes-state",
				data: { op: "add", id: 2, text: "current branch", createdAt: 1 },
			},
		]);

		expect(__test.pendingNotes(state)).toEqual([
			{
				id: 2,
				key: "legacy:2:1",
				text: "current branch",
			},
		]);
		expect(state.nextId).toBe(3);
	});

	it("replays add, edit, remove, and clear events in order", () => {
		const state = __test.replayNotes([
			{
				type: "custom",
				customType: "notes-state",
				data: { op: "add", id: 1, text: "one", createdAt: 1 },
			},
			{
				type: "custom",
				customType: "notes-state",
				data: { op: "add", id: 2, text: "two", createdAt: 2 },
			},
			{
				type: "custom",
				customType: "notes-state",
				data: { op: "edit", id: 2, text: "two edited", updatedAt: 3 },
			},
			{
				type: "custom",
				customType: "notes-state",
				data: { op: "remove", id: 1, removedAt: 4 },
			},
			{
				type: "custom",
				customType: "notes-state",
				data: { op: "clear", clearedAt: 5 },
			},
			{
				type: "custom",
				customType: "notes-state",
				data: { op: "add", id: 3, text: "three", createdAt: 6 },
			},
		]);

		expect(__test.pendingNotes(state)).toEqual([
			{ id: 3, key: "legacy:3:6", text: "three" },
		]);
		expect(state.nextId).toBe(4);
	});

	it("ignores malformed entries and invalid edits or removes", () => {
		const state = __test.replayNotes([
			{
				type: "custom",
				customType: "notes-state",
				data: { op: "edit", id: 9, text: "missing", updatedAt: 1 },
			},
			{
				type: "custom",
				customType: "notes-state",
				data: { op: "remove", id: 9, removedAt: 2 },
			},
			{
				type: "custom",
				customType: "other",
				data: { op: "add", id: 1, text: "wrong", createdAt: 3 },
			},
			{
				type: "custom",
				customType: "notes-state",
				data: { op: "add", id: 2, text: "ok", createdAt: 4 },
			},
			{
				type: "custom",
				customType: "notes-state",
				data: { op: "add", id: "bad", text: "bad", createdAt: 5 },
			},
		]);

		expect(__test.pendingNotes(state)).toEqual([
			{ id: 2, key: "legacy:2:4", text: "ok" },
		]);
		expect(state.nextId).toBe(3);
	});

	it("appends notes-state entries through commands", async () => {
		box = await start();
		const command = noteCommandOf();
		await command.handler(
			"add remember parser tests",
			s().extensionRunner.createCommandContext(),
		);

		expect(customEntries("notes-state")).toEqual([
			{
				op: "add",
				id: 1,
				key: expect.any(String),
				text: "remember parser tests",
				createdAt: expect.any(Number),
			},
		]);
	});
});