feat: gated project experience & prompt intelligence
Implements the full gated session flow and prompt intelligence system: - Prisma schema: add gated, priority, summary, chapters, linkTarget fields - Session gate: state machine (gated → begin_session → ungated) with LLM-powered tool selection based on prompt index - Tag matcher: intelligent prompt-to-tool matching with project/server/action tags - LLM selector: tiered provider selection (fast for gating, heavy for complex tasks) - Link resolver: cross-project MCP resource references (project/server:uri format) - Prompt summary service: LLM-generated summaries and chapter extraction - System project bootstrap: ensures default project exists on startup - Structural link health checks: enrichWithLinkStatus on prompt GET endpoints - CLI: create prompt --priority/--link, create project --gated/--no-gated, describe project shows prompts section, get prompts shows PRI/LINK/STATUS - Apply/edit: priority, linkTarget, gated fields supported - Shell completions: fish updated with new flags - 1,253 tests passing across all packages Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
110
src/mcpd/tests/services/prompt-summary.test.ts
Normal file
110
src/mcpd/tests/services/prompt-summary.test.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import {
|
||||
PromptSummaryService,
|
||||
extractFirstSentence,
|
||||
extractHeadings,
|
||||
type LlmSummaryGenerator,
|
||||
} from '../../src/services/prompt-summary.service.js';
|
||||
|
||||
describe('extractFirstSentence', () => {
|
||||
it('extracts first sentence from plain text', () => {
|
||||
const result = extractFirstSentence('This is the first sentence. And this is the second.', 20);
|
||||
expect(result).toBe('This is the first sentence.');
|
||||
});
|
||||
|
||||
it('truncates to maxWords', () => {
|
||||
const long = 'word '.repeat(30).trim();
|
||||
const result = extractFirstSentence(long, 5);
|
||||
expect(result).toBe('word word word word word...');
|
||||
});
|
||||
|
||||
it('skips markdown headings to find content', () => {
|
||||
const content = '# Title\n\n## Subtitle\n\nActual content here. More text.';
|
||||
expect(extractFirstSentence(content, 20)).toBe('Actual content here.');
|
||||
});
|
||||
|
||||
it('falls back to first heading if no content lines', () => {
|
||||
const content = '# Only Headings\n## Nothing Else';
|
||||
expect(extractFirstSentence(content, 20)).toBe('Only Headings');
|
||||
});
|
||||
|
||||
it('strips markdown formatting', () => {
|
||||
const content = 'This has **bold** and *italic* and `code` and [link](http://example.com).';
|
||||
expect(extractFirstSentence(content, 20)).toBe('This has bold and italic and code and link.');
|
||||
});
|
||||
|
||||
it('handles empty content', () => {
|
||||
expect(extractFirstSentence('', 20)).toBe('');
|
||||
expect(extractFirstSentence(' ', 20)).toBe('');
|
||||
});
|
||||
|
||||
it('handles content with no sentence boundary', () => {
|
||||
const content = 'No period at the end';
|
||||
expect(extractFirstSentence(content, 20)).toBe('No period at the end');
|
||||
});
|
||||
|
||||
it('handles exclamation and question marks', () => {
|
||||
expect(extractFirstSentence('Is this a question? Yes it is.', 20)).toBe('Is this a question?');
|
||||
expect(extractFirstSentence('Watch out! Be careful.', 20)).toBe('Watch out!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractHeadings', () => {
|
||||
it('extracts all levels of markdown headings', () => {
|
||||
const content = '# H1\n## H2\n### H3\nSome text\n#### H4';
|
||||
expect(extractHeadings(content)).toEqual(['H1', 'H2', 'H3', 'H4']);
|
||||
});
|
||||
|
||||
it('returns empty array for content without headings', () => {
|
||||
expect(extractHeadings('Just plain text\nMore text')).toEqual([]);
|
||||
});
|
||||
|
||||
it('handles empty content', () => {
|
||||
expect(extractHeadings('')).toEqual([]);
|
||||
});
|
||||
|
||||
it('trims heading text', () => {
|
||||
const content = '# Spaced Heading \n## Another ';
|
||||
expect(extractHeadings(content)).toEqual(['Spaced Heading', 'Another']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PromptSummaryService', () => {
|
||||
it('uses regex fallback when no LLM', async () => {
|
||||
const service = new PromptSummaryService(null);
|
||||
const result = await service.generateSummary('# Overview\n\nThis is a test document. It has content.\n\n## Section One\n\n## Section Two');
|
||||
expect(result.summary).toBe('This is a test document.');
|
||||
expect(result.chapters).toEqual(['Overview', 'Section One', 'Section Two']);
|
||||
});
|
||||
|
||||
it('uses LLM when available', async () => {
|
||||
const mockLlm: LlmSummaryGenerator = {
|
||||
generate: vi.fn(async () => ({
|
||||
summary: 'LLM-generated summary',
|
||||
chapters: ['LLM Chapter 1'],
|
||||
})),
|
||||
};
|
||||
const service = new PromptSummaryService(mockLlm);
|
||||
const result = await service.generateSummary('Some content');
|
||||
expect(result.summary).toBe('LLM-generated summary');
|
||||
expect(result.chapters).toEqual(['LLM Chapter 1']);
|
||||
expect(mockLlm.generate).toHaveBeenCalledWith('Some content');
|
||||
});
|
||||
|
||||
it('falls back to regex on LLM failure', async () => {
|
||||
const mockLlm: LlmSummaryGenerator = {
|
||||
generate: vi.fn(async () => { throw new Error('LLM unavailable'); }),
|
||||
};
|
||||
const service = new PromptSummaryService(mockLlm);
|
||||
const result = await service.generateSummary('Fallback content here. Second sentence.');
|
||||
expect(result.summary).toBe('Fallback content here.');
|
||||
expect(mockLlm.generate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('generateWithRegex works directly', () => {
|
||||
const service = new PromptSummaryService(null);
|
||||
const result = service.generateWithRegex('# Title\n\nContent line. More.\n\n## Chapter A\n\n## Chapter B');
|
||||
expect(result.summary).toBe('Content line.');
|
||||
expect(result.chapters).toEqual(['Title', 'Chapter A', 'Chapter B']);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user