Skip to main content
Glama
StructureValidator.test.tsโ€ข10.4 kB
/** * Tests for StructureValidator */ import { describe, it, expect, beforeEach, afterEach } from "@jest/globals"; import { StructureValidator } from "../../../analysis/StructureValidator.js"; import { WritingStorage } from "../../../storage/WritingStorage.js"; import { SQLiteManager } from "../../../storage/SQLiteManager.js"; import fs from "fs"; import os from "os"; import path from "path"; describe("StructureValidator", () => { let storage: WritingStorage; let validator: StructureValidator; const testDbPath = path.join(os.tmpdir(), "test-structure.db"); beforeEach(() => { if (fs.existsSync(testDbPath)) { fs.unlinkSync(testDbPath); } const sqliteManager = new SQLiteManager({ dbPath: testDbPath }); storage = new WritingStorage(sqliteManager); validator = new StructureValidator(storage); }); afterEach(() => { storage.close(); if (fs.existsSync(testDbPath)) { fs.unlinkSync(testDbPath); } }); describe("checkHeadingLevels", () => { it("should detect skipped heading levels", async () => { const content = `# Chapter 1\n### Subsection\nContent here`; await storage.addFile({ filePath: "chapter1.md", content, title: "Chapter 1", }); const report = await validator.validateStructure({ filePath: "chapter1.md", checks: ["heading-levels"], }); expect(report.issues.length).toBeGreaterThan(0); const skipIssue = report.issues.find((i) => i.type === "skipped-level"); expect(skipIssue).toBeDefined(); expect(skipIssue?.message).toContain("H1 to H3"); }); it("should not flag correct heading hierarchy", async () => { const content = `# Chapter 1\n## Section 1\n### Subsection\n## Section 2`; await storage.addFile({ filePath: "chapter1.md", content, title: "Chapter 1", }); const report = await validator.validateStructure({ filePath: "chapter1.md", checks: ["heading-levels"], }); const skipIssues = report.issues.filter((i) => i.type === "skipped-level"); expect(skipIssues.length).toBe(0); }); it("should warn about files with no headings", async () => { const content = `Just plain content without headings`; await storage.addFile({ filePath: "plain.md", content, title: "Plain", }); const report = await validator.validateStructure({ filePath: "plain.md", checks: ["heading-levels"], }); const missingHeading = report.issues.find( (i) => i.type === "missing-heading" ); expect(missingHeading).toBeDefined(); }); }); describe("checkDuplicateHeadings", () => { it("should detect duplicate headings at same level", async () => { const content = `# Introduction\n## Setup\nContent\n## Setup\nMore content`; await storage.addFile({ filePath: "chapter1.md", content, title: "Chapter 1", }); const report = await validator.validateStructure({ filePath: "chapter1.md", checks: ["duplicate-headings"], }); const dupIssue = report.issues.find((i) => i.type === "duplicate-heading"); expect(dupIssue).toBeDefined(); expect(dupIssue?.heading).toBe("Setup"); }); it("should allow same text at different levels", async () => { const content = `# Setup\n## Overview\n### Setup\nDifferent levels OK`; await storage.addFile({ filePath: "chapter1.md", content, title: "Chapter 1", }); const report = await validator.validateStructure({ filePath: "chapter1.md", checks: ["duplicate-headings"], }); const dupIssues = report.issues.filter((i) => i.type === "duplicate-heading"); expect(dupIssues.length).toBe(0); }); it("should be case-insensitive", async () => { const content = `## Introduction\n## INTRODUCTION`; await storage.addFile({ filePath: "chapter1.md", content, title: "Chapter 1", }); const report = await validator.validateStructure({ filePath: "chapter1.md", checks: ["duplicate-headings"], }); const dupIssue = report.issues.find((i) => i.type === "duplicate-heading"); expect(dupIssue).toBeDefined(); }); }); describe("checkSectionBalance", () => { it("should detect unbalanced sections", async () => { const shortSection = "Short."; const longSection = "Long ".repeat(100); // 100 words const content = `# Chapter\n## Section 1\n${longSection}\n## Section 2\n${shortSection}`; await storage.addFile({ filePath: "chapter1.md", content, title: "Chapter 1", }); const report = await validator.validateStructure({ filePath: "chapter1.md", checks: ["section-balance"], }); const balanceIssues = report.issues.filter( (i) => i.type === "unbalanced-section" ); expect(balanceIssues.length).toBeGreaterThan(0); }); it("should not flag balanced sections", async () => { const section1 = "Content ".repeat(50); const section2 = "Content ".repeat(55); const content = `# Chapter\n## Section 1\n${section1}\n## Section 2\n${section2}`; await storage.addFile({ filePath: "chapter1.md", content, title: "Chapter 1", }); const report = await validator.validateStructure({ filePath: "chapter1.md", checks: ["section-balance"], }); const balanceIssues = report.issues.filter( (i) => i.type === "unbalanced-section" ); expect(balanceIssues.length).toBe(0); }); it("should ignore short documents", async () => { const content = `# Title\n## Section 1\nShort\n## Section 2\nAlso short`; await storage.addFile({ filePath: "short.md", content, title: "Short", }); const report = await validator.validateStructure({ filePath: "short.md", checks: ["section-balance"], }); const balanceIssues = report.issues.filter( (i) => i.type === "unbalanced-section" ); expect(balanceIssues.length).toBe(0); }); }); describe("checkDeepNesting", () => { it("should warn about deeply nested headings", async () => { const content = `# H1\n## H2\n### H3\n#### H4\n##### H5\n###### H6`; await storage.addFile({ filePath: "deep.md", content, title: "Deep", }); const report = await validator.validateStructure({ filePath: "deep.md", checks: ["deep-nesting"], }); const deepIssues = report.issues.filter((i) => i.type === "deep-nesting"); expect(deepIssues.length).toBeGreaterThan(0); const h5Issue = deepIssues.find((i) => i.level === 5); expect(h5Issue).toBeDefined(); }); it("should allow reasonable nesting depth", async () => { const content = `# H1\n## H2\n### H3\n#### H4`; await storage.addFile({ filePath: "reasonable.md", content, title: "Reasonable", }); const report = await validator.validateStructure({ filePath: "reasonable.md", checks: ["deep-nesting"], }); const deepIssues = report.issues.filter((i) => i.type === "deep-nesting"); expect(deepIssues.length).toBe(0); }); }); describe("validateStructure", () => { it("should run all checks by default", async () => { const content = `# Title\n### Skipped Level\n### Skipped Level\n##### Too Deep`; await storage.addFile({ filePath: "issues.md", content, title: "Issues", }); const report = await validator.validateStructure({ filePath: "issues.md", }); expect(report.issues.length).toBeGreaterThan(0); expect(report.filesChecked).toBe(1); const types = new Set(report.issues.map((i) => i.type)); expect(types.size).toBeGreaterThan(1); // Multiple issue types }); it("should run only specified checks", async () => { const content = `# Title\n### Skipped\n### Skipped`; await storage.addFile({ filePath: "test.md", content, title: "Test", }); const report = await validator.validateStructure({ filePath: "test.md", checks: ["duplicate-headings"], }); const types = new Set(report.issues.map((i) => i.type)); expect(types.has("duplicate-heading")).toBe(true); expect(types.has("skipped-level")).toBe(false); }); it("should count severity levels correctly", async () => { const content = `# Title\n### Skipped\n##### Too Deep`; await storage.addFile({ filePath: "severity.md", content, title: "Severity", }); const report = await validator.validateStructure({ filePath: "severity.md", }); expect(report.errors + report.warnings + report.info).toBe( report.issues.length ); }); it("should validate multiple files", async () => { await storage.addFile({ filePath: "file1.md", content: "# Title\n### Skipped", title: "File 1", }); await storage.addFile({ filePath: "file2.md", content: "# Title\n##### Deep", title: "File 2", }); const report = await validator.validateStructure({}); expect(report.filesChecked).toBe(2); expect(report.issues.length).toBeGreaterThan(0); }); }); describe("edge cases", () => { it("should handle empty files", async () => { await storage.addFile({ filePath: "empty.md", content: "", title: "Empty", }); const report = await validator.validateStructure({ filePath: "empty.md", }); expect(report.filesChecked).toBe(1); }); it("should handle files with only headings", async () => { await storage.addFile({ filePath: "headings-only.md", content: "# H1\n## H2\n### H3", title: "Headings Only", }); const report = await validator.validateStructure({ filePath: "headings-only.md", }); expect(report.filesChecked).toBe(1); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/xiaolai/claude-writers-aid-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server