import test from "node:test"; import assert from "node:assert/strict"; import { EditorTabStore, type EditorTab } from "./editorTabStore.ts"; const makeTab = (overrides: Partial = {}): EditorTab => ({ id: "edt_1", kind: "editor", sessionId: "conn_1", hostId: "host_1", remotePath: "/etc/nginx/nginx.conf", fileName: "nginx.conf", languageId: "ini", content: "worker_processes auto;", baselineContent: "worker_processes auto;", wordWrap: false, viewState: null, savingState: "idle", saveError: null, ...overrides, }); test("updateContent stores content and viewState; dirty flag derives from baseline", () => { const store = new EditorTabStore(); store._debugInsert(makeTab()); store.updateContent("edt_1", "worker_processes 4;", null); const tab = store.getTab("edt_1")!; assert.equal(tab.content, "worker_processes 4;"); assert.equal(store.isDirty("edt_1"), true); }); test("markSaved moves baseline to current content and clears dirty", () => { const store = new EditorTabStore(); store._debugInsert(makeTab({ content: "changed", baselineContent: "orig" })); assert.equal(store.isDirty("edt_1"), true); store.markSaved("edt_1", "changed"); assert.equal(store.isDirty("edt_1"), false); assert.equal(store.getTab("edt_1")!.baselineContent, "changed"); }); test("setWordWrap updates only that tab", () => { const store = new EditorTabStore(); store._debugInsert(makeTab({ id: "edt_1" })); store._debugInsert(makeTab({ id: "edt_2", remotePath: "/b.txt", fileName: "b.txt" })); store.setWordWrap("edt_1", true); assert.equal(store.getTab("edt_1")!.wordWrap, true); assert.equal(store.getTab("edt_2")!.wordWrap, false); }); test("setSavingState transitions and clears error on idle", () => { const store = new EditorTabStore(); store._debugInsert(makeTab()); store.setSavingState("edt_1", "saving"); assert.equal(store.getTab("edt_1")!.savingState, "saving"); store.setSavingState("edt_1", "error", "EACCES"); assert.equal(store.getTab("edt_1")!.saveError, "EACCES"); store.setSavingState("edt_1", "idle"); assert.equal(store.getTab("edt_1")!.saveError, null); }); test("close removes the tab and returns remaining ids in order", () => { const store = new EditorTabStore(); store._debugInsert(makeTab({ id: "edt_1" })); store._debugInsert(makeTab({ id: "edt_2", remotePath: "/b.txt", fileName: "b.txt" })); store.close("edt_1"); assert.equal(store.getTab("edt_1"), undefined); assert.deepEqual(store.getTabs().map((t) => t.id), ["edt_2"]); }); test("subscribers fire on change and not on read", () => { const store = new EditorTabStore(); store._debugInsert(makeTab()); let count = 0; const unsub = store.subscribe(() => { count++; }); store.getTab("edt_1"); store.getTabs(); assert.equal(count, 0); store.updateContent("edt_1", "x", null); // notifications are microtask-deferred, flush via awaiting a resolved promise return Promise.resolve().then(() => { assert.equal(count, 1); unsub(); }); }); test("promoteFromModal creates a new tab and returns its id", () => { const store = new EditorTabStore(); const id = store.promoteFromModal({ sessionId: "conn_1", hostId: "host_1", remotePath: "/etc/nginx/nginx.conf", fileName: "nginx.conf", languageId: "ini", content: "x", baselineContent: "x", wordWrap: false, viewState: null, }); const tab = store.getTab(id)!; assert.equal(tab.remotePath, "/etc/nginx/nginx.conf"); assert.equal(tab.fileName, "nginx.conf"); assert.equal(tab.kind, "editor"); }); test("promoteFromModal focuses existing tab for same sessionId+normalized path and overrides content", () => { const store = new EditorTabStore(); const first = store.promoteFromModal({ sessionId: "conn_1", hostId: "host_1", remotePath: "/etc/nginx/./nginx.conf", fileName: "nginx.conf", languageId: "ini", content: "v1", baselineContent: "v1", wordWrap: false, viewState: null, }); const second = store.promoteFromModal({ sessionId: "conn_1", hostId: "host_1", remotePath: "/etc/nginx/nginx.conf", fileName: "nginx.conf", languageId: "ini", content: "v2", baselineContent: "v1", wordWrap: false, viewState: null, }); assert.equal(second, first); assert.equal(store.getTab(first)!.content, "v2"); assert.equal(store.getTabs().length, 1); }); test("dedup scope is per-sessionId — same path on different sessions are distinct tabs", () => { const store = new EditorTabStore(); const a = store.promoteFromModal({ sessionId: "conn_A", hostId: "host_1", remotePath: "/etc/hosts", fileName: "hosts", languageId: "plaintext", content: "", baselineContent: "", wordWrap: false, viewState: null, }); const b = store.promoteFromModal({ sessionId: "conn_B", hostId: "host_2", remotePath: "/etc/hosts", fileName: "hosts", languageId: "plaintext", content: "", baselineContent: "", wordWrap: false, viewState: null, }); assert.notEqual(a, b); assert.equal(store.getTabs().length, 2); }); test("confirmCloseBySession returns true when no tabs match", async () => { const store = new EditorTabStore(); store._debugInsert(makeTab()); const ok = await store.confirmCloseBySession("other_conn", async () => "discard"); assert.equal(ok, true); assert.equal(store.getTabs().length, 1); }); test("confirmCloseBySession discards all dirty matching tabs when prompt returns 'discard'", async () => { const store = new EditorTabStore(); store._debugInsert(makeTab({ id: "edt_1", content: "x", baselineContent: "y" })); store._debugInsert(makeTab({ id: "edt_2", remotePath: "/b.txt", fileName: "b.txt", content: "x", baselineContent: "y" })); const ok = await store.confirmCloseBySession("conn_1", async () => "discard"); assert.equal(ok, true); assert.equal(store.getTabs().length, 0); }); test("confirmCloseBySession closes clean tabs without prompting; aborts on cancel", async () => { const store = new EditorTabStore(); store._debugInsert(makeTab({ id: "edt_clean" })); // content == baseline store._debugInsert(makeTab({ id: "edt_dirty", remotePath: "/b.txt", fileName: "b.txt", content: "x", baselineContent: "y" })); let prompts = 0; const ok = await store.confirmCloseBySession("conn_1", async () => { prompts++; return "cancel"; }); assert.equal(ok, false); assert.equal(prompts, 1, "prompt fires only for dirty tab"); // clean tab was closed before the dirty cancel aborted the batch assert.equal(store.getTab("edt_clean"), undefined); assert.ok(store.getTab("edt_dirty")); }); test("confirmCloseBySession invokes save callback for 'save' choice and only closes on save success", async () => { const store = new EditorTabStore(); store._debugInsert(makeTab({ id: "edt_1", content: "new", baselineContent: "old" })); let saved = false; const ok = await store.confirmCloseBySession("conn_1", async () => "save", async (id) => { assert.equal(id, "edt_1"); saved = true; store.markSaved(id, "new"); }); assert.equal(saved, true); assert.equal(ok, true); assert.equal(store.getTab("edt_1"), undefined); }); test("confirmCloseBySession reports every closed editor tab to cleanup callback", async () => { const store = new EditorTabStore(); store._debugInsert(makeTab({ id: "edt_clean" })); store._debugInsert(makeTab({ id: "edt_dirty", remotePath: "/b.txt", fileName: "b.txt", content: "new", baselineContent: "old" })); const closed: string[] = []; const ok = await store.confirmCloseBySession( "conn_1", async () => "save", async (id) => { const tab = store.getTab(id)!; store.markSaved(id, tab.content); }, (id) => closed.push(id), ); assert.equal(ok, true); assert.deepEqual(closed, ["edt_clean", "edt_dirty"]); assert.equal(store.getTabs().length, 0); });