Files
grimoire/src/lib/spellbook-manager.test.ts
Alejandro Gómez f4d0e86f09 feat: complete spellbook UX overhaul with enhanced state tracking
- Redesign SpellbookDropdown with clear status indicators (ownership, storage)
- Add SpellbookStatus component showing you/other and local/published/network
- Enhance activeSpellbook type with source, localId, isPublished fields
- Fix PublishSpellbook action to properly yield events (caller handles side-effects)
- Add k tags extraction from REQ windows for kind-based filtering/discovery
- Update terminology from "Layout" to "Spellbook" consistently
- Add comprehensive tests for k tags and source tracking

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-21 19:19:24 +01:00

425 lines
12 KiB
TypeScript

import { describe, it, expect } from "vitest";
import {
createSpellbook,
parseSpellbook,
loadSpellbook,
slugify,
} from "./spellbook-manager";
import { GrimoireState, WindowInstance, Workspace } from "@/types/app";
import { SPELLBOOK_KIND, SpellbookEvent } from "@/types/spell";
// Mock Data
const mockWindow1: WindowInstance = {
id: "win-1",
appId: "profile",
props: { pubkey: "abc" },
spellId: "spell-1",
};
const mockWindow2: WindowInstance = {
id: "win-2",
appId: "kind",
props: { kind: 1 },
};
// REQ window with filter.kinds for k tag extraction
const mockReqWindow: WindowInstance = {
id: "win-req",
appId: "req",
props: {
filter: {
kinds: [1, 6, 7, 30023],
authors: ["abc"],
},
},
};
const mockReqWindow2: WindowInstance = {
id: "win-req-2",
appId: "req",
props: {
filter: {
kinds: [1, 4], // kind 1 duplicated to test dedup
},
},
};
const mockWorkspace1: Workspace = {
id: "ws-1",
number: 1,
layout: "win-1",
windowIds: ["win-1"],
};
const mockWorkspace2: Workspace = {
id: "ws-2",
number: 2,
layout: {
direction: "row",
first: "win-2",
second: "win-1",
},
windowIds: ["win-1", "win-2"],
};
const mockWorkspaceWithReq: Workspace = {
id: "ws-req",
number: 3,
layout: {
direction: "row",
first: "win-req",
second: "win-req-2",
},
windowIds: ["win-req", "win-req-2"],
};
const mockState: GrimoireState = {
__version: 6,
windows: {
"win-1": mockWindow1,
"win-2": mockWindow2,
},
workspaces: {
"ws-1": mockWorkspace1,
"ws-2": mockWorkspace2,
},
activeWorkspaceId: "ws-1",
layoutConfig: {
insertionMode: "smart",
splitPercentage: 50,
insertionPosition: "second",
},
};
const mockStateWithReq: GrimoireState = {
__version: 6,
windows: {
"win-req": mockReqWindow,
"win-req-2": mockReqWindow2,
},
workspaces: {
"ws-req": mockWorkspaceWithReq,
},
activeWorkspaceId: "ws-req",
layoutConfig: {
insertionMode: "smart",
splitPercentage: 50,
insertionPosition: "second",
},
};
describe("Spellbook Manager", () => {
describe("slugify", () => {
it("converts titles to slugs", () => {
expect(slugify("Hello World")).toBe("hello-world");
expect(slugify("My Cool Dashboard!")).toBe("my-cool-dashboard");
expect(slugify(" Trim Me ")).toBe("trim-me");
expect(slugify("Mixed Case Title")).toBe("mixed-case-title");
});
});
describe("createSpellbook", () => {
it("creates a valid spellbook from state", () => {
const result = createSpellbook({
state: mockState,
title: "My Backup",
description: "Test description",
workspaceIds: ["ws-1"],
});
const { eventProps, referencedSpells } = result;
const content = JSON.parse(eventProps.content);
// Check event props
expect(eventProps.kind).toBe(SPELLBOOK_KIND);
expect(eventProps.tags).toContainEqual(["d", "my-backup"]);
expect(eventProps.tags).toContainEqual(["title", "My Backup"]);
expect(eventProps.tags).toContainEqual([
"description",
"Test description",
]);
expect(eventProps.tags).toContainEqual(["client", "grimoire"]);
// Check referenced spells (e tags)
expect(referencedSpells).toContain("spell-1");
expect(eventProps.tags).toContainEqual(["e", "spell-1", "", "mention"]);
// Check content structure
expect(content.version).toBe(1);
expect(Object.keys(content.workspaces)).toHaveLength(1);
expect(content.workspaces["ws-1"]).toBeDefined();
// Should only include windows referenced in the workspace
expect(Object.keys(content.windows)).toHaveLength(1);
expect(content.windows["win-1"]).toBeDefined();
expect(content.windows["win-2"]).toBeUndefined();
});
it("includes all workspaces if no IDs provided", () => {
const result = createSpellbook({
state: mockState,
title: "Full Backup",
});
const content = JSON.parse(result.eventProps.content);
expect(Object.keys(content.workspaces)).toHaveLength(2);
expect(Object.keys(content.windows)).toHaveLength(2);
});
it("extracts k tags from REQ windows and deduplicates", () => {
const result = createSpellbook({
state: mockStateWithReq,
title: "REQ Dashboard",
});
const kTags = result.eventProps.tags.filter((t) => t[0] === "k");
// Should have k tags for kinds: 1, 4, 6, 7, 30023 (deduped and sorted)
expect(kTags).toContainEqual(["k", "1"]);
expect(kTags).toContainEqual(["k", "4"]);
expect(kTags).toContainEqual(["k", "6"]);
expect(kTags).toContainEqual(["k", "7"]);
expect(kTags).toContainEqual(["k", "30023"]);
// Should be sorted by kind number
expect(kTags).toHaveLength(5);
expect(kTags[0]).toEqual(["k", "1"]);
expect(kTags[kTags.length - 1]).toEqual(["k", "30023"]);
});
it("does not include k tags for non-REQ windows", () => {
const result = createSpellbook({
state: mockState,
title: "No REQ",
workspaceIds: ["ws-1"],
});
const kTags = result.eventProps.tags.filter((t) => t[0] === "k");
expect(kTags).toHaveLength(0);
});
});
describe("parseSpellbook", () => {
it("parses a valid spellbook event", () => {
const content = {
version: 1,
workspaces: { "ws-1": mockWorkspace1 },
windows: { "win-1": mockWindow1 },
};
const event: SpellbookEvent = {
id: "evt-1",
pubkey: "pub-1",
created_at: 123456,
kind: SPELLBOOK_KIND,
tags: [
["d", "my-slug"],
["title", "My Title"],
["description", "Desc"],
["e", "spell-1"],
],
content: JSON.stringify(content),
sig: "sig",
};
const parsed = parseSpellbook(event);
expect(parsed.slug).toBe("my-slug");
expect(parsed.title).toBe("My Title");
expect(parsed.description).toBe("Desc");
expect(parsed.content).toEqual(content);
expect(parsed.referencedSpells).toContain("spell-1");
});
it("handles parsing errors gracefully", () => {
const event = {
kind: SPELLBOOK_KIND,
content: "invalid json",
tags: [],
} as any;
expect(() => parseSpellbook(event)).toThrow(
"Failed to parse spellbook content",
);
});
});
describe("loadSpellbook", () => {
it("replaces all workspaces with imported ones and regenerates IDs", () => {
const spellbookContent = {
version: 1,
workspaces: { "ws-1": mockWorkspace1 },
windows: { "win-1": mockWindow1 },
};
const parsed = {
slug: "test",
title: "Test",
content: spellbookContent,
referencedSpells: [],
event: {} as any,
};
const newState = loadSpellbook(mockState, parsed);
// Should have only 1 workspace (replaces all existing)
expect(Object.keys(newState.workspaces)).toHaveLength(1);
// Get the new workspace
const newWs = Object.values(newState.workspaces)[0];
// IDs should be regenerated
expect(newWs.id).not.toBe("ws-1");
// Number should be 1 (sequential assignment)
expect(newWs.number).toBe(1);
// Window IDs should be regenerated
const newWinId = newWs.windowIds[0];
expect(newWinId).not.toBe("win-1");
expect(newState.windows[newWinId]).toBeDefined();
expect(newState.windows[newWinId].appId).toBe("profile");
// Layout should reference new window ID
expect(newWs.layout).toBe(newWinId);
});
it("updates layout tree with new window IDs", () => {
const spellbookContent = {
version: 1,
workspaces: { "ws-2": mockWorkspace2 },
windows: { "win-1": mockWindow1, "win-2": mockWindow2 },
};
const parsed = {
slug: "test",
title: "Test",
content: spellbookContent,
referencedSpells: [],
event: {} as any,
};
const newState = loadSpellbook(mockState, parsed);
// Since loadSpellbook replaces all workspaces, the imported workspace gets number 1
const newWs = Object.values(newState.workspaces).find(
(w) => w.number === 1,
)!;
expect(typeof newWs.layout).toBe("object");
if (typeof newWs.layout === "object" && newWs.layout !== null) {
// Check that leaf nodes are new UUIDs, not old IDs
expect(newWs.layout.first).not.toBe("win-2");
expect(newWs.layout.second).not.toBe("win-1");
// Check that they match the windowIds list
expect(newWs.windowIds).toContain(newWs.layout.first);
expect(newWs.windowIds).toContain(newWs.layout.second);
}
});
it("sets activeSpellbook with source tracking from network event", () => {
const spellbookContent = {
version: 1,
workspaces: { "ws-1": mockWorkspace1 },
windows: { "win-1": mockWindow1 },
};
const event: SpellbookEvent = {
id: "event-123",
pubkey: "author-pubkey",
created_at: 123456,
kind: SPELLBOOK_KIND,
tags: [["d", "test"]],
content: JSON.stringify(spellbookContent),
sig: "sig",
};
const parsed = {
slug: "test",
title: "Test Title",
description: "Test Description",
content: spellbookContent,
referencedSpells: [],
event,
// Simulating network-loaded spellbook (no localId)
source: "network" as const,
isPublished: true,
};
const newState = loadSpellbook(mockState, parsed);
// Check activeSpellbook has enhanced fields
expect(newState.activeSpellbook).toBeDefined();
expect(newState.activeSpellbook?.slug).toBe("test");
expect(newState.activeSpellbook?.title).toBe("Test Title");
expect(newState.activeSpellbook?.description).toBe("Test Description");
expect(newState.activeSpellbook?.pubkey).toBe("author-pubkey");
expect(newState.activeSpellbook?.source).toBe("network");
expect(newState.activeSpellbook?.isPublished).toBe(true);
expect(newState.activeSpellbook?.localId).toBeUndefined();
});
it("sets activeSpellbook with source tracking from local spellbook", () => {
const spellbookContent = {
version: 1,
workspaces: { "ws-1": mockWorkspace1 },
windows: { "win-1": mockWindow1 },
};
const parsed = {
slug: "local-test",
title: "Local Test",
content: spellbookContent,
referencedSpells: [],
// Simulating local-loaded spellbook
localId: "local-uuid-123",
source: "local" as const,
isPublished: false,
};
const newState = loadSpellbook(mockState, parsed);
// Check activeSpellbook has enhanced fields
expect(newState.activeSpellbook).toBeDefined();
expect(newState.activeSpellbook?.slug).toBe("local-test");
expect(newState.activeSpellbook?.source).toBe("local");
expect(newState.activeSpellbook?.isPublished).toBe(false);
expect(newState.activeSpellbook?.localId).toBe("local-uuid-123");
expect(newState.activeSpellbook?.pubkey).toBeUndefined();
});
it("infers source from event presence when not provided", () => {
const spellbookContent = {
version: 1,
workspaces: { "ws-1": mockWorkspace1 },
windows: { "win-1": mockWindow1 },
};
const event: SpellbookEvent = {
id: "event-abc",
pubkey: "some-pubkey",
created_at: 123456,
kind: SPELLBOOK_KIND,
tags: [],
content: JSON.stringify(spellbookContent),
sig: "sig",
};
const parsed = {
slug: "inferred",
title: "Inferred",
content: spellbookContent,
referencedSpells: [],
event, // Has event, so should infer network
// No source, localId, or isPublished provided
};
const newState = loadSpellbook(mockState, parsed);
// Should infer source as "network" and isPublished as true from event presence
expect(newState.activeSpellbook?.source).toBe("network");
expect(newState.activeSpellbook?.isPublished).toBe(true);
});
});
});