repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/notes/__tests__/harness.test.ts
Rawimport { join } from "node:path";
import { fileURLToPath } from "node:url";
import { initTheme, SessionManager } from "@earendil-works/pi-coding-agent";
import { Text } from "@earendil-works/pi-tui";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
createTestSession,
says,
type TestSession,
when,
} from "../../../test/harness";
import { __test } from "../index.ts";
let t: TestSession | undefined;
function extensionPath(): string {
return fileURLToPath(new URL("../index.ts", import.meta.url));
}
function customEntries(kind: string): Array<Record<string, unknown>> {
if (!t) throw new Error("missing test session");
return (
t.session.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 uiArgs(method: string): unknown[][] {
return (
t?.events.ui
.filter((call) => call.method === method)
.map((call) => call.args) ?? []
);
}
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 = uiArgs("setWidget")
.filter((args) => args[0] === "notes")
.map((args) => args[1])
.at(-1);
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 = t?.session.extensionRunner.extensions[0].commands.get("note");
if (!command || !t) throw new Error("missing note command");
await command.handler(args, t.session.extensionRunner.createCommandContext());
}
afterEach(() => {
vi.restoreAllMocks();
__test.clearLaunch();
__test.takeNewSessionNotes();
t?.dispose();
t = undefined;
});
describe("notes pi-test-harness", () => {
it("loads in real Pi extension runtime and registers command plus shortcut", async () => {
t = await createTestSession({ extensions: [extensionPath()] });
const [extension] = t.session.extensionRunner.extensions;
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 () => {
t = await createTestSession({ extensions: [extensionPath()] });
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 () => {
t = await createTestSession({ extensions: [extensionPath()] });
await noteCommand("add first note");
expect(renderLatestWidget()?.[0]).toMatch(/^/u);
});
it("toggles expanded widget with slash command and shortcut", async () => {
t = await createTestSession({ extensions: [extensionPath()] });
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 =
t.session.extensionRunner.extensions[0].shortcuts.get("alt+n");
await shortcut.handler(t.session.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 () => {
t = await createTestSession({ extensions: [extensionPath()] });
(t.session.sessionManager as any).appendCustomEntry("notes-state", {
op: "add",
id: 7,
text: "restored note",
createdAt: 1,
});
await t.session.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 () => {
t = await createTestSession({ extensions: [extensionPath()] });
await noteCommand("add abandoned note");
const manager = t.session.sessionManager as any;
manager.resetLeaf();
manager.appendCustomEntry("notes-state", {
op: "add",
id: 7,
text: "target note",
createdAt: 1,
});
await t.session.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 () => {
t = await createTestSession({
extensions: [extensionPath()],
mockUI: { confirm: true },
});
await noteCommand("add carried note");
await t.session.extensionRunner.emit({
type: "session_shutdown",
reason: "new",
});
(t.session.sessionManager as any).newSession();
await t.session.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 () => {
t = await createTestSession({
extensions: [extensionPath()],
mockUI: { confirm: false },
});
await noteCommand("add old note");
await t.session.extensionRunner.emit({
type: "session_shutdown",
reason: "new",
});
(t.session.sessionManager as any).newSession();
await t.session.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 () => {
t = await createTestSession({ extensions: [extensionPath()] });
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 () => {
t = await createTestSession({ extensions: [extensionPath()] });
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 () => {
t = await createTestSession({
extensions: [extensionPath()],
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 () => {
t = await createTestSession({
extensions: [extensionPath()],
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 () => {
t = await createTestSession({
extensions: [extensionPath()],
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",
]);
t.dispose();
t = await createTestSession({
extensions: [extensionPath()],
mockUI: { editor: () => undefined },
});
await noteCommand("add stable note");
await noteCommand("edit 1");
expect(customEntries("notes-state")).toHaveLength(1);
t.dispose();
t = await createTestSession({
extensions: [extensionPath()],
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 () => {
t = await createTestSession({ extensions: [extensionPath()] });
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 () => {
t = await createTestSession({ extensions: [extensionPath()] });
const sent: Array<{ content: unknown; options: unknown }> = [];
t.session.extensionRunner.runtime.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 () => {
t = await createTestSession({ extensions: [extensionPath()] });
const sent: Array<{ content: unknown; options: unknown }> = [];
t.session.extensionRunner.runtime.sendUserMessage = (
content: unknown,
options: unknown,
) => sent.push({ content, options });
await noteCommand("add busy text");
const command =
t.session.extensionRunner.extensions[0].commands.get("note");
const ctx = t.session.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) => {
t = await createTestSession({ extensions: [extensionPath()] });
await noteCommand('add /review "src/main.ts"');
vi.spyOn(t.session, "promptTemplates", "get").mockReturnValue([
{
name: "review",
description: "Review a file",
content: "Review $1 carefully.",
},
]);
if (busy) {
vi.spyOn(t.session, "isStreaming", "get").mockReturnValue(true);
const command =
t.session.extensionRunner.extensions[0].commands.get("note");
const ctx = t.session.extensionRunner.createCommandContext();
ctx.isIdle = () => false;
await command.handler("send 1", ctx);
await expect
.poll(() => t?.session.getFollowUpMessages())
.toEqual(["Review src/main.ts carefully."]);
} else {
await t.run(when("/note send 1", [says("Reviewed.")]));
expect(t.events.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 () => {
t = await createTestSession({ extensions: [extensionPath()] });
const sent: Array<{ content: unknown; options: unknown }> = [];
t.session.extensionRunner.runtime.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 () => {
t = await createTestSession({ extensions: [extensionPath()] });
await noteCommand("add launch this");
await noteCommand("add leave this pending");
const runner = t.session.extensionRunner;
const source = SessionManager.create(t.cwd, join(t.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();
const command = runner.extensions[0].commands.get("note");
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",
targetSessionFile: undefined,
});
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 () => {
t = await createTestSession({ extensions: [extensionPath()] });
initTheme("dark", false);
const runner = t.session.extensionRunner;
const manager = t.session.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 = runner.extensions[0].commands.get("note");
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",
targetSessionFile: undefined,
});
manager.resetLeaf();
await runner.emit({
type: "session_start",
reason: "fork",
previousSessionFile: undefined,
});
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 () => {
t = await createTestSession({ extensions: [extensionPath()] });
const destination = await createTestSession({
extensions: [extensionPath()],
});
try {
await noteCommand("add launch this");
await noteCommand("add do not carry this");
const sourceRunner = t.session.extensionRunner;
const destinationRunner = destination.session.extensionRunner;
const destinationManager = destination.session.sessionManager as any;
const command = sourceRunner.extensions[0].commands.get("note");
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",
targetSessionFile: undefined,
});
await destinationRunner.emit({
type: "session_start",
reason: "new",
previousSessionFile: undefined,
});
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(destination.events.uiCallsFor("confirm")).toEqual([]);
expect(
destination.events
.uiCallsFor("setWidget")
.filter((call) => call.args[0] === "notes")
.at(-1)?.args[1],
).toBeUndefined();
expect(destinationManager.getHeader().parentSession).toBeUndefined();
} finally {
destination.dispose();
}
});
it("keeps the note in the destination when delivery fails", async () => {
t = await createTestSession({ extensions: [extensionPath()] });
await noteCommand("add recover me");
const runner = t.session.extensionRunner;
const manager = t.session.sessionManager as any;
const command = runner.extensions[0].commands.get("note");
const ctx = runner.createCommandContext() as any;
ctx.newSession = vi.fn(async (options) => {
await runner.emit({
type: "session_shutdown",
reason: "new",
targetSessionFile: undefined,
});
manager.newSession();
await runner.emit({
type: "session_start",
reason: "new",
previousSessionFile: undefined,
});
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 () => {
t = await createTestSession({ extensions: [extensionPath()] });
await noteCommand("add in flight");
const runner = t.session.extensionRunner;
const manager = t.session.sessionManager as any;
const command = runner.extensions[0].commands.get("note");
const ctx = runner.createCommandContext() as any;
let finishDelivery: (() => void) | undefined;
ctx.newSession = vi.fn(async (options) => {
await runner.emit({
type: "session_shutdown",
reason: "new",
targetSessionFile: undefined,
});
manager.newSession();
await runner.emit({
type: "session_start",
reason: "new",
previousSessionFile: undefined,
});
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 () => {
t = await createTestSession({
extensions: [extensionPath()],
mockUI: { confirm: true },
});
await noteCommand("add switch again");
const runner = t.session.extensionRunner;
const manager = t.session.sessionManager as any;
const command = runner.extensions[0].commands.get("note");
const ctx = runner.createCommandContext() as any;
ctx.newSession = vi.fn(async (options) => {
await runner.emit({
type: "session_shutdown",
reason: "new",
targetSessionFile: undefined,
});
manager.newSession();
await runner.emit({
type: "session_start",
reason: "new",
previousSessionFile: undefined,
});
await options.withSession({
...ctx,
sendUserMessage: async () => {
await command.handler(
"add nested note",
runner.createCommandContext(),
);
await runner.emit({
type: "session_shutdown",
reason: "new",
targetSessionFile: undefined,
});
manager.newSession();
await runner.emit({
type: "session_start",
reason: "new",
previousSessionFile: undefined,
});
},
});
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 () => {
t = await createTestSession({ extensions: [extensionPath()] });
await noteCommand("add stable note");
const runner = t.session.extensionRunner;
const command = runner.extensions[0].commands.get("note");
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 () => {
t = await createTestSession({ extensions: [extensionPath()] });
await noteCommand("add stable note");
const runner = t.session.extensionRunner;
const command = runner.extensions[0].commands.get("note");
const base = runner.createCommandContext() as any;
const sessionManager = Object.create(base.sessionManager);
sessionManager.getSessionFile = () => join(t.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 () => {
t = await createTestSession({ extensions: [extensionPath()] });
const command =
t.session.extensionRunner.extensions[0].commands.get("note");
await noteCommand("add consider split parser tests");
await noteCommand("add ask whether command should support autocomplete");
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 () => {
t = await createTestSession({ extensions: [extensionPath()] });
const command =
t.session.extensionRunner.extensions[0].commands.get("note");
await command.handler(
"add remember parser tests",
t.session.extensionRunner.createCommandContext(),
);
expect(customEntries("notes-state")).toEqual([
{
op: "add",
id: 1,
key: expect.any(String),
text: "remember parser tests",
createdAt: expect.any(Number),
},
]);
});
});