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>; } 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 { const session = await createPiSession({ extensionPaths: [extensionPath()], ...(options.cwd ? { cwd: options.cwd } : {}), }); const calls: Array<{ method: string; args: unknown[] }> = []; const messages: Array> = []; session.session.extensionRunner.setUIContext( buildRecordingUI(options.mockUI ?? {}, calls), "print", ); session.session.subscribe((event) => { if (event.type === "message_end") { messages.push((event as { message: Record }).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> { return ( s().sessionManager.getEntries() as Array<{ type: string; customType?: string; data?: Record; }> ) .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 { 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((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), }, ]); }); });