3e237e177a
* feat: add code-walkthrough backend (MCP tool, service, persistence, WS events) * feat: render agent code walkthroughs as line-anchored cards + editor window * test: add code-walkthrough re-emit mock-agent scenario * fix: rework code-walkthrough UX (floating card, line reveal, minimize, clickable file) * fix: improve code walkthrough editor UX * fix: backfill walkthrough launcher after websocket connect * fix: improve walkthrough requests and editor anchors * fix: clear walkthrough highlights on close * feat: add walkthrough discard and prompt customization * fix: send walkthrough requests through saved prompt * fix: support postgres built-in prompt seeding * fix: address walkthrough review feedback * fix: address walkthrough review follow-ups * fix: harden walkthrough lifecycle races * fix: reuse walkthrough notification cleanup * fix: guard stale walkthrough file opens * fix: abort stale file opens before paint * fix: stabilize github settings e2e readiness * fix: use stable github settings e2e selector
194 lines
5.3 KiB
TypeScript
194 lines
5.3 KiB
TypeScript
import { beforeEach, describe, expect, it } from "vitest";
|
|
import {
|
|
cleanupTaskStorage,
|
|
clearGlobalSidebarWidth,
|
|
getGlobalSidebarWidth,
|
|
getOpenFileTabs,
|
|
markPRClosedBannerDismissed,
|
|
markPRMergedBannerDismissed,
|
|
restoreAttachmentPreview,
|
|
setGlobalSidebarWidth,
|
|
setOpenFileTabs,
|
|
wasPRClosedBannerDismissed,
|
|
wasPRMergedBannerDismissed,
|
|
} from "./local-storage";
|
|
|
|
describe("PR merged banner dismissal storage", () => {
|
|
beforeEach(() => {
|
|
window.sessionStorage.clear();
|
|
});
|
|
|
|
it("returns false when no dismissal has been recorded", () => {
|
|
expect(wasPRMergedBannerDismissed("task-a")).toBe(false);
|
|
});
|
|
|
|
it("persists dismissal per task and reads it back", () => {
|
|
markPRMergedBannerDismissed("task-a");
|
|
|
|
expect(wasPRMergedBannerDismissed("task-a")).toBe(true);
|
|
expect(wasPRMergedBannerDismissed("task-b")).toBe(false);
|
|
});
|
|
|
|
it("clears the dismissal flag via cleanupTaskStorage", () => {
|
|
markPRMergedBannerDismissed("task-a");
|
|
markPRMergedBannerDismissed("task-b");
|
|
|
|
cleanupTaskStorage("task-a", []);
|
|
|
|
expect(wasPRMergedBannerDismissed("task-a")).toBe(false);
|
|
expect(wasPRMergedBannerDismissed("task-b")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("PR closed banner dismissal storage", () => {
|
|
beforeEach(() => {
|
|
window.sessionStorage.clear();
|
|
});
|
|
|
|
it("returns false when no dismissal has been recorded", () => {
|
|
expect(wasPRClosedBannerDismissed("task-a")).toBe(false);
|
|
});
|
|
|
|
it("persists dismissal per task and reads it back", () => {
|
|
markPRClosedBannerDismissed("task-a");
|
|
|
|
expect(wasPRClosedBannerDismissed("task-a")).toBe(true);
|
|
expect(wasPRClosedBannerDismissed("task-b")).toBe(false);
|
|
});
|
|
|
|
it("is independent from the merged banner dismissal flag", () => {
|
|
markPRMergedBannerDismissed("task-a");
|
|
|
|
expect(wasPRClosedBannerDismissed("task-a")).toBe(false);
|
|
});
|
|
|
|
it("clears the dismissal flag via cleanupTaskStorage", () => {
|
|
markPRClosedBannerDismissed("task-a");
|
|
markPRClosedBannerDismissed("task-b");
|
|
|
|
cleanupTaskStorage("task-a", []);
|
|
|
|
expect(wasPRClosedBannerDismissed("task-a")).toBe(false);
|
|
expect(wasPRClosedBannerDismissed("task-b")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("global sidebar width storage", () => {
|
|
beforeEach(() => {
|
|
window.localStorage.clear();
|
|
});
|
|
|
|
it("returns null when no width has been stored", () => {
|
|
expect(getGlobalSidebarWidth()).toBeNull();
|
|
});
|
|
|
|
it("persists a width globally and reads it back (rounded)", () => {
|
|
setGlobalSidebarWidth(412.6);
|
|
expect(getGlobalSidebarWidth()).toBe(413);
|
|
});
|
|
|
|
it("ignores non-positive or non-finite widths", () => {
|
|
setGlobalSidebarWidth(0);
|
|
setGlobalSidebarWidth(-100);
|
|
setGlobalSidebarWidth(Number.NaN);
|
|
expect(getGlobalSidebarWidth()).toBeNull();
|
|
});
|
|
|
|
it("clears the stored width", () => {
|
|
setGlobalSidebarWidth(320);
|
|
clearGlobalSidebarWidth();
|
|
expect(getGlobalSidebarWidth()).toBeNull();
|
|
});
|
|
|
|
it("is NOT removed by cleanupTaskStorage (it is global, not task-scoped)", () => {
|
|
setGlobalSidebarWidth(320);
|
|
cleanupTaskStorage("task-a", []);
|
|
expect(getGlobalSidebarWidth()).toBe(320);
|
|
});
|
|
});
|
|
|
|
describe("task-scoped artifact notification storage", () => {
|
|
beforeEach(() => {
|
|
window.localStorage.clear();
|
|
});
|
|
|
|
it("clears walkthrough last-seen state via cleanupTaskStorage", () => {
|
|
window.localStorage.setItem(
|
|
"kandev.walkthrough.lastSeenByTask",
|
|
JSON.stringify({ "task-a": "2026-01-01T00:00:00Z", "task-b": "2026-01-02T00:00:00Z" }),
|
|
);
|
|
|
|
cleanupTaskStorage("task-a", []);
|
|
|
|
expect(
|
|
JSON.parse(window.localStorage.getItem("kandev.walkthrough.lastSeenByTask") ?? "{}"),
|
|
).toEqual({
|
|
"task-b": "2026-01-02T00:00:00Z",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("open file tabs storage", () => {
|
|
beforeEach(() => {
|
|
window.sessionStorage.clear();
|
|
});
|
|
|
|
it("round-trips the multi-repo repo subpath so a restored tab refetches under the right repo", () => {
|
|
setOpenFileTabs("sess-1", [
|
|
{
|
|
path: "src/foo.ts",
|
|
name: "foo.ts",
|
|
repo: "enrichment-commons",
|
|
markdownPreview: true,
|
|
pinned: true,
|
|
},
|
|
]);
|
|
|
|
const tabs = getOpenFileTabs("sess-1");
|
|
expect(tabs).toHaveLength(1);
|
|
expect(tabs[0]).toEqual({
|
|
path: "src/foo.ts",
|
|
name: "foo.ts",
|
|
repo: "enrichment-commons",
|
|
markdownPreview: true,
|
|
pinned: true,
|
|
});
|
|
});
|
|
|
|
it("leaves repo undefined for single-repo tabs", () => {
|
|
setOpenFileTabs("sess-1", [{ path: "src/foo.ts", name: "foo.ts", pinned: true }]);
|
|
expect(getOpenFileTabs("sess-1")[0].repo).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("chat draft attachment storage", () => {
|
|
it("normalizes invalid restored image delivery modes to prompt", () => {
|
|
const restored = restoreAttachmentPreview({
|
|
id: "att-1",
|
|
data: "abc",
|
|
mimeType: "image/png",
|
|
fileName: "shot.png",
|
|
size: 3,
|
|
isImage: true,
|
|
deliveryMode: "inline" as "prompt",
|
|
});
|
|
|
|
expect(restored.deliveryMode).toBe("prompt");
|
|
expect(restored.preview).toBe("data:image/png;base64,abc");
|
|
});
|
|
|
|
it("normalizes invalid restored file delivery modes to path", () => {
|
|
const restored = restoreAttachmentPreview({
|
|
id: "att-2",
|
|
data: "abc",
|
|
mimeType: "application/pdf",
|
|
fileName: "doc.pdf",
|
|
size: 3,
|
|
isImage: false,
|
|
deliveryMode: "inline" as "path",
|
|
});
|
|
|
|
expect(restored.deliveryMode).toBe("path");
|
|
});
|
|
});
|