From 56927e79e090a28ddbdd49b2e1f3d8923550be2b Mon Sep 17 00:00:00 2001 From: highperfocused Date: Sun, 6 Sep 2026 18:32:25 +0200 Subject: [PATCH] fix: reject non-integer/negative spell kinds, de-flake a timing test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per remaining "previously missed" findings: - parseSpell() now requires k tags to be non-negative integers (Number.isInteger && >= 0), not just finite — a relay-sourced spell claiming kind "1.5" or "-1" no longer passes through into a malformed filter. - The resolveTimestamp wall-clock tests asserted toBeCloseTo a single captured `now`, which a slow runner or timing skew between the two Date.now() calls could flake. Replaced with a before/after range assertion. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto --- src/hooks/useSpells.test.ts | 30 ++++++++++++++++++++++++++---- src/hooks/useSpells.ts | 6 +++++- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/hooks/useSpells.test.ts b/src/hooks/useSpells.test.ts index 9658772..638f2d8 100644 --- a/src/hooks/useSpells.test.ts +++ b/src/hooks/useSpells.test.ts @@ -27,13 +27,22 @@ function parsedSpell(overrides: Partial): ParsedSpell { describe('resolveTimestamp', () => { it('resolves a relative duration against now', () => { - const now = Math.floor(Date.now() / 1000); - expect(resolveTimestamp('7d')).toBeCloseTo(now - 7 * 86400, -1); + const before = Math.floor(Date.now() / 1000) - 7 * 86400; + const result = resolveTimestamp('7d'); + const after = Math.floor(Date.now() / 1000) - 7 * 86400; + // A range, not toBeCloseTo against a single captured `now` — a slow test + // runner or wall-clock skew between the two Date.now() calls could + // otherwise make this flaky. + expect(result).toBeGreaterThanOrEqual(before); + expect(result).toBeLessThanOrEqual(after); }); it('resolves "now"', () => { - const now = Math.floor(Date.now() / 1000); - expect(resolveTimestamp('now')).toBeCloseTo(now, -1); + const before = Math.floor(Date.now() / 1000); + const result = resolveTimestamp('now'); + const after = Math.floor(Date.now() / 1000); + expect(result).toBeGreaterThanOrEqual(before); + expect(result).toBeLessThanOrEqual(after); }); it('resolves an absolute unix timestamp', () => { @@ -88,6 +97,19 @@ describe('encodeSpellTags / parseSpell round-trip', () => { }); }); +describe('parseSpell', () => { + it('drops non-integer and negative k tags — kinds are non-negative integers', () => { + const event = spellEvent([ + ['cmd', 'REQ'], + ['k', '1'], + ['k', '1.5'], + ['k', '-1'], + ['k', 'not-a-number'], + ]); + expect(parseSpell(event).kinds).toEqual([1]); + }); +}); + describe('isValidTagLetter', () => { it('accepts a single letter', () => { expect(isValidTagLetter('t')).toBe(true); diff --git a/src/hooks/useSpells.ts b/src/hooks/useSpells.ts index e09a458..90d54e8 100644 --- a/src/hooks/useSpells.ts +++ b/src/hooks/useSpells.ts @@ -107,7 +107,11 @@ export function encodeSpellTags(input: SpellInput): string[][] { } export function parseSpell(event: NostrEvent): ParsedSpell { - const kinds = tagValues(event, 'k').map(Number).filter((n) => Number.isFinite(n)); + // Nostr kinds are non-negative integers — a relay-sourced spell claiming + // e.g. "1.5" or "-1" would otherwise pass through into a malformed filter. + const kinds = tagValues(event, 'k') + .map(Number) + .filter((n) => Number.isInteger(n) && n >= 0); const authorsTag = event.tags.find(([name]) => name === 'authors'); const tagFilterTag = event.tags.find(([name]) => name === 'tag'); const limitValue = tagValue(event, 'limit');