mirror of
https://github.com/layer-systems/website.git
synced 2026-09-15 15:59:41 +02:00
The original feedback mentioned "grimoire spells" — traced to
github.com/purrgrammer/grimoire, a third-party Nostr client with its
own draft NIP for kind 777 "Spell" events: a REQ filter (kinds,
authors, one tag filter, limit, time window) encoded as portable,
shareable tags, with $me/$contacts runtime variables and relative
timestamps ("7d", "now").
- src/hooks/useSpells.ts implements that draft NIP as-is (same tags,
same variables, same relative-time grammar) rather than a
reinterpretation, so a spell saved here round-trips with Grimoire.
- src/apps/spells: browse "My Spells" / "Discover", build one with
NewSpellForm, and Run it on demand against the resolved filter,
rendering kind-1 results with NoteCard.
- Only the "Spell" half is implemented; "Spellbook" (kind 30777,
saved window layouts) is left as a documented follow-up.
Closes #24
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto
82 lines
3.1 KiB
TypeScript
82 lines
3.1 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import type { NostrEvent } from '@nostrify/nostrify';
|
|
import { encodeSpellTags, parseSpell, resolveSpellFilter, resolveTimestamp } from './useSpells';
|
|
|
|
function spellEvent(tags: string[][], content = ''): NostrEvent {
|
|
return { id: 'x', pubkey: 'author', created_at: 0, kind: 777, tags, content, sig: '' };
|
|
}
|
|
|
|
describe('resolveTimestamp', () => {
|
|
it('resolves a relative duration against now', () => {
|
|
const now = Math.floor(Date.now() / 1000);
|
|
expect(resolveTimestamp('7d')).toBeCloseTo(now - 7 * 86400, -1);
|
|
});
|
|
|
|
it('resolves "now"', () => {
|
|
const now = Math.floor(Date.now() / 1000);
|
|
expect(resolveTimestamp('now')).toBeCloseTo(now, -1);
|
|
});
|
|
|
|
it('resolves an absolute unix timestamp', () => {
|
|
expect(resolveTimestamp('1700000000')).toBe(1700000000);
|
|
});
|
|
|
|
it('is undefined for blank input', () => {
|
|
expect(resolveTimestamp('')).toBeUndefined();
|
|
expect(resolveTimestamp(' ')).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('encodeSpellTags / parseSpell round-trip', () => {
|
|
it('round-trips a spell with authors, a tag filter, since and topics', () => {
|
|
const tags = encodeSpellTags({
|
|
name: 'Bitcoin from contacts',
|
|
description: 'Notes about Bitcoin from my contacts',
|
|
kinds: [1],
|
|
authors: ['$contacts'],
|
|
tagFilter: { letter: 't', values: ['bitcoin'] },
|
|
since: '7d',
|
|
limit: 50,
|
|
topics: ['bitcoin', 'social'],
|
|
});
|
|
const event = spellEvent(tags, 'Notes about Bitcoin from my contacts');
|
|
const parsed = parseSpell(event);
|
|
|
|
expect(parsed.name).toBe('Bitcoin from contacts');
|
|
expect(parsed.kinds).toEqual([1]);
|
|
expect(parsed.authors).toEqual(['$contacts']);
|
|
expect(parsed.tagFilter).toEqual({ letter: 't', values: ['bitcoin'] });
|
|
expect(parsed.since).toBe('7d');
|
|
expect(parsed.limit).toBe(50);
|
|
expect(parsed.topics).toEqual(['bitcoin', 'social']);
|
|
});
|
|
});
|
|
|
|
describe('resolveSpellFilter', () => {
|
|
it('resolves $me and $contacts into real pubkeys', () => {
|
|
const event = spellEvent(encodeSpellTags({ kinds: [1], authors: ['$me', '$contacts'] }));
|
|
const filter = resolveSpellFilter(parseSpell(event), { me: 'abc', contacts: ['def', 'ghi'] });
|
|
expect(filter?.authors).toEqual(['abc', 'def', 'ghi']);
|
|
});
|
|
|
|
it('returns null when $me is required but nobody is signed in', () => {
|
|
const event = spellEvent(encodeSpellTags({ kinds: [1], authors: ['$me'] }));
|
|
const filter = resolveSpellFilter(parseSpell(event), { me: undefined, contacts: [] });
|
|
expect(filter).toBeNull();
|
|
});
|
|
|
|
it('returns null when there are no kinds', () => {
|
|
const event = spellEvent(encodeSpellTags({ kinds: [] }));
|
|
const filter = resolveSpellFilter(parseSpell(event), { me: undefined, contacts: [] });
|
|
expect(filter).toBeNull();
|
|
});
|
|
|
|
it('turns a tag filter into a #<letter> filter field', () => {
|
|
const event = spellEvent(
|
|
encodeSpellTags({ kinds: [1], tagFilter: { letter: 't', values: ['bitcoin', 'nostr'] } }),
|
|
);
|
|
const filter = resolveSpellFilter(parseSpell(event), { me: undefined, contacts: [] });
|
|
expect(filter?.['#t']).toEqual(['bitcoin', 'nostr']);
|
|
});
|
|
});
|