Implements FED-010: Agent Spawn via Federation feature that enables spawning and managing Claude agents on remote federated Mosaic Stack instances via COMMAND message type. Features: - Federation agent command types (spawn, status, kill) - FederationAgentService for handling agent operations - Integration with orchestrator's agent spawner/lifecycle services - API endpoints for spawning, querying status, and killing agents - Full command routing through federation COMMAND infrastructure - Comprehensive test coverage (12/12 tests passing) Architecture: - Hub → Spoke: Spawn agents on remote instances - Command flow: FederationController → FederationAgentService → CommandService → Remote Orchestrator - Response handling: Remote orchestrator returns agent status/results - Security: Connection validation, signature verification Files created: - apps/api/src/federation/types/federation-agent.types.ts - apps/api/src/federation/federation-agent.service.ts - apps/api/src/federation/federation-agent.service.spec.ts Files modified: - apps/api/src/federation/command.service.ts (agent command routing) - apps/api/src/federation/federation.controller.ts (agent endpoints) - apps/api/src/federation/federation.module.ts (service registration) - apps/orchestrator/src/api/agents/agents.controller.ts (status endpoint) - apps/orchestrator/src/api/agents/agents.module.ts (lifecycle integration) Testing: - 12/12 tests passing for FederationAgentService - All command service tests passing - TypeScript compilation successful - Linting passed Refs #93 Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
349 lines
10 KiB
TypeScript
349 lines
10 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { renderMarkdown, renderMarkdownSync, markdownToPlainText } from "./markdown";
|
|
|
|
describe("Markdown Rendering", () => {
|
|
describe("renderMarkdown", () => {
|
|
it("should render basic markdown to HTML", async () => {
|
|
const markdown = "# Hello World\n\nThis is **bold** text.";
|
|
const html = await renderMarkdown(markdown);
|
|
|
|
expect(html).toContain("<h1");
|
|
expect(html).toContain("Hello World");
|
|
expect(html).toContain("<strong>bold</strong>");
|
|
});
|
|
|
|
it("should handle empty input", async () => {
|
|
const html = await renderMarkdown("");
|
|
expect(html).toBe("");
|
|
});
|
|
|
|
it("should handle null/undefined input", async () => {
|
|
expect(await renderMarkdown(null as any)).toBe("");
|
|
expect(await renderMarkdown(undefined as any)).toBe("");
|
|
});
|
|
|
|
it("should sanitize potentially dangerous HTML", async () => {
|
|
const markdown = '<script>alert("XSS")</script>\n\n**Safe** content';
|
|
const html = await renderMarkdown(markdown);
|
|
|
|
expect(html).not.toContain("<script>");
|
|
expect(html).not.toContain("alert");
|
|
expect(html).toContain("<strong>Safe</strong>");
|
|
});
|
|
|
|
it("should sanitize onclick handlers", async () => {
|
|
const markdown = '[Click me](javascript:alert("XSS"))';
|
|
const html = await renderMarkdown(markdown);
|
|
|
|
expect(html).not.toContain("javascript:");
|
|
expect(html).not.toContain("onclick");
|
|
});
|
|
});
|
|
|
|
describe("GFM Features", () => {
|
|
it("should render tables", async () => {
|
|
const markdown = `
|
|
| Header 1 | Header 2 |
|
|
|----------|----------|
|
|
| Cell 1 | Cell 2 |
|
|
| Cell 3 | Cell 4 |
|
|
`.trim();
|
|
|
|
const html = await renderMarkdown(markdown);
|
|
|
|
expect(html).toContain("<table>");
|
|
expect(html).toContain("<thead>");
|
|
expect(html).toContain("<tbody>");
|
|
expect(html).toContain("<th>Header 1</th>");
|
|
expect(html).toContain("<td>Cell 1</td>");
|
|
});
|
|
|
|
it("should render strikethrough text", async () => {
|
|
const markdown = "This is ~~deleted~~ text";
|
|
const html = await renderMarkdown(markdown);
|
|
|
|
expect(html).toContain("<del>deleted</del>");
|
|
});
|
|
|
|
it("should render task lists", async () => {
|
|
const markdown = `
|
|
- [ ] Unchecked task
|
|
- [x] Checked task
|
|
`.trim();
|
|
|
|
const html = await renderMarkdown(markdown);
|
|
|
|
expect(html).toContain("<input");
|
|
expect(html).toContain('type="checkbox"');
|
|
expect(html).toContain('disabled="disabled"'); // Should be disabled for safety
|
|
});
|
|
|
|
it("should render autolinks", async () => {
|
|
const markdown = "Visit https://example.com for more info";
|
|
const html = await renderMarkdown(markdown);
|
|
|
|
expect(html).toContain('<a href="https://example.com"');
|
|
});
|
|
});
|
|
|
|
describe("Code Highlighting", () => {
|
|
it("should render inline code", async () => {
|
|
const markdown = "Use the `console.log()` function";
|
|
const html = await renderMarkdown(markdown);
|
|
|
|
expect(html).toContain("<code>");
|
|
expect(html).toContain("console.log()");
|
|
});
|
|
|
|
it("should render code blocks with language", async () => {
|
|
const markdown = `
|
|
\`\`\`typescript
|
|
const greeting: string = "Hello";
|
|
console.log(greeting);
|
|
\`\`\`
|
|
`.trim();
|
|
|
|
const html = await renderMarkdown(markdown);
|
|
|
|
expect(html).toContain("<pre>");
|
|
expect(html).toContain("<code");
|
|
expect(html).toContain("language-typescript");
|
|
// Should have syntax highlighting
|
|
expect(html.length).toBeGreaterThan(markdown.length);
|
|
});
|
|
|
|
it("should render code blocks without language", async () => {
|
|
const markdown = `
|
|
\`\`\`
|
|
plain text code
|
|
\`\`\`
|
|
`.trim();
|
|
|
|
const html = await renderMarkdown(markdown);
|
|
|
|
expect(html).toContain("<pre>");
|
|
expect(html).toContain("<code");
|
|
expect(html).toContain("plain text code");
|
|
});
|
|
});
|
|
|
|
describe("Links and Images", () => {
|
|
it("should render links with target=_blank for external URLs", async () => {
|
|
const markdown = "[External Link](https://example.com)";
|
|
const html = await renderMarkdown(markdown);
|
|
|
|
expect(html).toContain('href="https://example.com"');
|
|
expect(html).toContain('target="_blank"');
|
|
expect(html).toContain('rel="noopener noreferrer"');
|
|
});
|
|
|
|
it("should render images", async () => {
|
|
const markdown = "";
|
|
const html = await renderMarkdown(markdown);
|
|
|
|
expect(html).toContain("<img");
|
|
expect(html).toContain('src="https://example.com/image.png"');
|
|
expect(html).toContain('alt="Alt text"');
|
|
});
|
|
|
|
it("should allow data URIs for images", async () => {
|
|
const markdown =
|
|
"";
|
|
const html = await renderMarkdown(markdown);
|
|
|
|
expect(html).toContain("<img");
|
|
expect(html).toContain('src="data:image/png;base64');
|
|
});
|
|
});
|
|
|
|
describe("Headers and IDs", () => {
|
|
it("should generate IDs for headers", async () => {
|
|
const markdown = "# My Header Title";
|
|
const html = await renderMarkdown(markdown);
|
|
|
|
expect(html).toContain("<h1");
|
|
expect(html).toContain('id="');
|
|
});
|
|
|
|
it("should render all header levels", async () => {
|
|
const markdown = `
|
|
# H1
|
|
## H2
|
|
### H3
|
|
#### H4
|
|
##### H5
|
|
###### H6
|
|
`.trim();
|
|
|
|
const html = await renderMarkdown(markdown);
|
|
|
|
expect(html).toContain("<h1");
|
|
expect(html).toContain("<h2");
|
|
expect(html).toContain("<h3");
|
|
expect(html).toContain("<h4");
|
|
expect(html).toContain("<h5");
|
|
expect(html).toContain("<h6");
|
|
});
|
|
});
|
|
|
|
describe("Lists", () => {
|
|
it("should render unordered lists", async () => {
|
|
const markdown = `
|
|
- Item 1
|
|
- Item 2
|
|
- Nested item
|
|
`.trim();
|
|
|
|
const html = await renderMarkdown(markdown);
|
|
|
|
expect(html).toContain("<ul>");
|
|
expect(html).toContain("<li>Item 1</li>");
|
|
expect(html).toContain("Nested item");
|
|
});
|
|
|
|
it("should render ordered lists", async () => {
|
|
const markdown = `
|
|
1. First
|
|
2. Second
|
|
3. Third
|
|
`.trim();
|
|
|
|
const html = await renderMarkdown(markdown);
|
|
|
|
expect(html).toContain("<ol>");
|
|
expect(html).toContain("<li>First</li>");
|
|
expect(html).toContain("<li>Second</li>");
|
|
});
|
|
});
|
|
|
|
describe("Quotes and Formatting", () => {
|
|
it("should render blockquotes", async () => {
|
|
const markdown = "> This is a quote\n> Multi-line quote";
|
|
const html = await renderMarkdown(markdown);
|
|
|
|
expect(html).toContain("<blockquote>");
|
|
expect(html).toContain("This is a quote");
|
|
});
|
|
|
|
it("should render emphasis and strong", async () => {
|
|
const markdown = "*italic* **bold** ***bold italic***";
|
|
const html = await renderMarkdown(markdown);
|
|
|
|
expect(html).toContain("<em>italic</em>");
|
|
expect(html).toContain("<strong>bold</strong>");
|
|
});
|
|
|
|
it("should render horizontal rules", async () => {
|
|
const markdown = "Content\n\n---\n\nMore content";
|
|
const html = await renderMarkdown(markdown);
|
|
|
|
expect(html).toContain("<hr");
|
|
});
|
|
});
|
|
|
|
describe("renderMarkdownSync", () => {
|
|
it("should render markdown synchronously", () => {
|
|
const markdown = "# Sync Test\n\n**Bold** text";
|
|
const html = renderMarkdownSync(markdown);
|
|
|
|
expect(html).toContain("<h1");
|
|
expect(html).toContain("Sync Test");
|
|
expect(html).toContain("<strong>Bold</strong>");
|
|
});
|
|
|
|
it("should handle empty input synchronously", () => {
|
|
const html = renderMarkdownSync("");
|
|
expect(html).toBe("");
|
|
});
|
|
|
|
it("should sanitize XSS synchronously", () => {
|
|
const markdown = '<script>alert("XSS")</script>';
|
|
const html = renderMarkdownSync(markdown);
|
|
|
|
expect(html).not.toContain("<script>");
|
|
expect(html).not.toContain("alert");
|
|
});
|
|
});
|
|
|
|
describe("markdownToPlainText", () => {
|
|
it("should extract plain text from markdown", async () => {
|
|
const markdown = "# Header\n\n**Bold** and *italic* text";
|
|
const plainText = await markdownToPlainText(markdown);
|
|
|
|
expect(plainText).toContain("Header");
|
|
expect(plainText).toContain("Bold");
|
|
expect(plainText).toContain("italic");
|
|
expect(plainText).not.toContain("<h1");
|
|
expect(plainText).not.toContain("<strong>");
|
|
expect(plainText).not.toContain("<em>");
|
|
});
|
|
|
|
it("should strip all HTML tags", async () => {
|
|
const markdown = "[Link](https://example.com)\n\n";
|
|
const plainText = await markdownToPlainText(markdown);
|
|
|
|
expect(plainText).not.toContain("<a");
|
|
expect(plainText).not.toContain("<img");
|
|
expect(plainText).toContain("Link");
|
|
});
|
|
});
|
|
|
|
describe("Security Tests", () => {
|
|
it("should block iframe injection", async () => {
|
|
const markdown = '<iframe src="https://evil.com"></iframe>';
|
|
const html = await renderMarkdown(markdown);
|
|
|
|
expect(html).not.toContain("<iframe");
|
|
});
|
|
|
|
it("should block object/embed tags", async () => {
|
|
const markdown = '<object data="malware.swf"></object>';
|
|
const html = await renderMarkdown(markdown);
|
|
|
|
expect(html).not.toContain("<object");
|
|
});
|
|
|
|
it("should block event handlers in markdown links", async () => {
|
|
const markdown = '[Click](# "onclick=alert(1)")';
|
|
const html = await renderMarkdown(markdown);
|
|
|
|
expect(html).not.toContain("onclick");
|
|
});
|
|
|
|
it("should sanitize SVG with script", async () => {
|
|
const markdown = '<svg><script>alert("XSS")</script></svg>';
|
|
const html = await renderMarkdown(markdown);
|
|
|
|
expect(html).not.toContain("<svg");
|
|
expect(html).not.toContain("<script>");
|
|
});
|
|
});
|
|
|
|
describe("Edge Cases", () => {
|
|
it("should handle very long content", async () => {
|
|
const markdown = "# Test\n\n" + "A ".repeat(10000);
|
|
const html = await renderMarkdown(markdown);
|
|
|
|
expect(html.length).toBeGreaterThan(0);
|
|
expect(html).toContain("<h1");
|
|
});
|
|
|
|
it("should handle unicode characters", async () => {
|
|
const markdown = "# 你好 World 🌍\n\n**Émojis**: 😀 ✨ 🚀";
|
|
const html = await renderMarkdown(markdown);
|
|
|
|
expect(html).toContain("你好");
|
|
expect(html).toContain("🌍");
|
|
expect(html).toContain("😀");
|
|
});
|
|
|
|
it("should handle nested markdown correctly", async () => {
|
|
const markdown = "**Bold with _italic_ inside**";
|
|
const html = await renderMarkdown(markdown);
|
|
|
|
expect(html).toContain("<strong>");
|
|
expect(html).toContain("<em>");
|
|
});
|
|
});
|
|
});
|