Add #rss command family with LLM tags and summaries (v0.20.0)
CI / build + unit (pull_request) Successful in 1m15s
CI / build + unit (pull_request) Successful in 1m15s
#rss subcommands on the command channel: help / add <url> <name> / remove <name> / tags / sources / get <name|#tag>. Sources are stored in a new rss_sources table in the existing MESH_DB_PATH file (second node:sqlite connection, case-insensitive names); feeds parsed with fast-xml-parser (RSS 2.0 + Atom); AI tags and headline summaries via injected LlmService.chat() with bounded maxTokens (50/200). CommandService gains prefix registration so one handler owns the whole #rss family while #ping stays an exact match; wired in CommandModule's factory — still no constructor-level cycle, no forwardRef. One dead feed does not sink a tag summary.
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { parseFeed } from './rss.feed.js';
|
||||
|
||||
const RSS2 = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
<channel>
|
||||
<title>Some Feed</title>
|
||||
<item>
|
||||
<title>First headline</title>
|
||||
<link>https://example.com/first</link>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[Second: with CDATA]]></title>
|
||||
<description>ignored</description>
|
||||
</item>
|
||||
<item>
|
||||
<title></title>
|
||||
<link>https://example.com/empty-title</link>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>`;
|
||||
|
||||
const ATOM = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<title>An Atom feed</title>
|
||||
<entry>
|
||||
<title>Only entry</title>
|
||||
<link rel="alternate" href="https://example.com/atom"/>
|
||||
</entry>
|
||||
</feed>`;
|
||||
|
||||
describe('parseFeed', () => {
|
||||
it('parses RSS 2.0 items in document order, keeping links where present and skipping empty titles', () => {
|
||||
expect(parseFeed(RSS2)).toEqual([
|
||||
{ title: 'First headline', link: 'https://example.com/first' },
|
||||
{ title: 'Second: with CDATA' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('parses Atom entries (single entry object, link as href attribute)', () => {
|
||||
expect(parseFeed(ATOM)).toEqual([
|
||||
{ title: 'Only entry', link: 'https://example.com/atom' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns [] for malformed XML', () => {
|
||||
expect(parseFeed('this is not xml at all')).toEqual([]);
|
||||
expect(parseFeed('<rss><channel><item><title>broken')).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns [] when there are no items or none with a usable title', () => {
|
||||
expect(
|
||||
parseFeed('<rss version="2.0"><channel><title>x</title></channel></rss>'),
|
||||
).toEqual([]);
|
||||
expect(
|
||||
parseFeed('<feed xmlns="http://www.w3.org/2005/Atom"></feed>'),
|
||||
).toEqual([]);
|
||||
expect(
|
||||
parseFeed(
|
||||
'<rss version="2.0"><channel><item><title> </title></item></channel></rss>',
|
||||
),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user