import { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, it, vi } from "vitest"; import { createTestSession, type TestSession } from "../../../test/harness"; it("records a terminal transition after session shutdown", async () => { const root = mkdtempSync(join(tmpdir(), "pi-ext-notes-debug-")); const original = { debug: process.env.PI_NOTES_DEBUG, state: process.env.XDG_STATE_HOME, }; let session: TestSession | undefined; try { process.env.PI_NOTES_DEBUG = "1"; process.env.XDG_STATE_HOME = join(root, "state"); const { default: extension } = await import("../index.ts"); session = await createTestSession({ cwd: root, env: { PI_NOTES_DEBUG: "1", XDG_STATE_HOME: join(root, "state"), }, extensionFactories: [extension], }); const runner = session.session.extensionRunner; await runner.emit({ type: "session_start", reason: "startup" }); const command = runner.extensions[0].commands.get("note"); if (!command) throw new Error("missing note command"); await command.handler("add failed delivery", runner.createCommandContext()); const ctx = runner.createCommandContext() as any; const manager = session.session.sessionManager 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("delivery failed"); }, }); return { cancelled: false }; }); await command.handler("new 1", ctx); await command.handler("add later", runner.createCommandContext()); await command.handler("add", runner.createCommandContext()); const directory = join(root, "state", "pi-ext", "debug", "notes"); const files = readdirSync(directory); expect(files).toHaveLength(2); const events = files.flatMap((file) => readFileSync(join(directory, file), "utf8") .trim() .split("\n") .map((line) => JSON.parse(line)), ); const starts = events.filter( (event) => event.event === "note.session_transition.start", ); const terminals = events.filter( (event) => event.event === "note.session_transition.finish" || event.event === "note.session_transition.error", ); expect(starts).toHaveLength(1); expect(events).toEqual( expect.arrayContaining([ expect.objectContaining({ event: "note.change", kind: "add", outcome: "applied", }), expect.objectContaining({ event: "note.change", kind: "add", outcome: "invalid", }), ]), ); expect(terminals).toEqual([ expect.objectContaining({ extension: "notes", event: "note.session_transition.finish", kind: "new", outcome: "delivery_failed", spanId: starts[0]?.spanId, durationMs: expect.any(Number), }), ]); } finally { session?.dispose(); vi.restoreAllMocks(); if (original.debug === undefined) delete process.env.PI_NOTES_DEBUG; else process.env.PI_NOTES_DEBUG = original.debug; if (original.state === undefined) delete process.env.XDG_STATE_HOME; else process.env.XDG_STATE_HOME = original.state; rmSync(root, { recursive: true, force: true }); } });