mdcontext 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.changeset/config.json +9 -9
- package/.claude/settings.local.json +25 -0
- package/.github/workflows/claude-code-review.yml +44 -0
- package/.github/workflows/claude.yml +85 -0
- package/CONTRIBUTING.md +186 -0
- package/NOTES/NOTES +44 -0
- package/README.md +206 -3
- package/biome.json +1 -1
- package/dist/chunk-23UPXDNL.js +3044 -0
- package/dist/chunk-2W7MO2DL.js +1366 -0
- package/dist/chunk-3NUAZGMA.js +1689 -0
- package/dist/chunk-7TOWB2XB.js +366 -0
- package/dist/chunk-7XOTOADQ.js +3065 -0
- package/dist/chunk-AH2PDM2K.js +3042 -0
- package/dist/chunk-BNXWSZ63.js +3742 -0
- package/dist/chunk-BTL5DJVU.js +3222 -0
- package/dist/chunk-HDHYG7E4.js +104 -0
- package/dist/chunk-HLR4KZBP.js +3234 -0
- package/dist/chunk-IP3FRFEB.js +1045 -0
- package/dist/chunk-KHU56VDO.js +3042 -0
- package/dist/chunk-KRYIFLQR.js +85 -89
- package/dist/chunk-LBSDNLEM.js +287 -0
- package/dist/chunk-MNTQ7HCP.js +2643 -0
- package/dist/chunk-MUJELQQ6.js +1387 -0
- package/dist/chunk-MXJGMSLV.js +2199 -0
- package/dist/chunk-N6QJGC3Z.js +2636 -0
- package/dist/chunk-OBELGBPM.js +1713 -0
- package/dist/chunk-OT7R5XTA.js +3192 -0
- package/dist/chunk-P7X4RA2T.js +106 -0
- package/dist/chunk-PIDUQNC2.js +3185 -0
- package/dist/chunk-POGCDIH4.js +3187 -0
- package/dist/chunk-PSIEOQGZ.js +3043 -0
- package/dist/chunk-PVRT3IHA.js +3238 -0
- package/dist/chunk-QNN4TT23.js +1430 -0
- package/dist/chunk-RE3R45RJ.js +3042 -0
- package/dist/chunk-S7E6TFX6.js +718 -657
- package/dist/chunk-SG6GLU4U.js +1378 -0
- package/dist/chunk-SJCDV2ST.js +274 -0
- package/dist/chunk-SYE5XLF3.js +104 -0
- package/dist/chunk-T5VLYBZD.js +103 -0
- package/dist/chunk-TOQB7VWU.js +3238 -0
- package/dist/chunk-VFNMZ4ZQ.js +3228 -0
- package/dist/chunk-VVTGZNBT.js +1533 -1423
- package/dist/chunk-W7Q4RFEV.js +104 -0
- package/dist/chunk-XTYYVRLO.js +3190 -0
- package/dist/chunk-Y6MDYVJD.js +3063 -0
- package/dist/cli/main.js +4072 -629
- package/dist/index.d.ts +420 -33
- package/dist/index.js +8 -15
- package/dist/mcp/server.js +103 -7
- package/dist/schema-BAWSG7KY.js +22 -0
- package/dist/schema-E3QUPL26.js +20 -0
- package/dist/schema-EHL7WUT6.js +20 -0
- package/docs/019-USAGE.md +44 -5
- package/docs/020-current-implementation.md +8 -8
- package/docs/021-DOGFOODING-FINDINGS.md +1 -1
- package/docs/CONFIG.md +1123 -0
- package/docs/ERRORS.md +383 -0
- package/docs/summarization.md +320 -0
- package/justfile +40 -0
- package/package.json +39 -33
- package/research/INDEX.md +315 -0
- package/research/code-review/README.md +90 -0
- package/research/code-review/cli-error-handling-review.md +979 -0
- package/research/code-review/code-review-validation-report.md +464 -0
- package/research/code-review/main-ts-review.md +1128 -0
- package/research/config-docs/SUMMARY.md +357 -0
- package/research/config-docs/TEST-RESULTS.md +776 -0
- package/research/config-docs/TODO.md +542 -0
- package/research/config-docs/analysis.md +744 -0
- package/research/config-docs/fix-validation.md +502 -0
- package/research/config-docs/help-audit.md +264 -0
- package/research/config-docs/help-system-analysis.md +890 -0
- package/research/frontmatter/COMMENTS-ARE-SKIPPED.md +149 -0
- package/research/frontmatter/LLM-CODE-NAVIGATION.md +276 -0
- package/research/issue-review.md +603 -0
- package/research/llm-summarization/agent-cli-tools-2026.md +1082 -0
- package/research/llm-summarization/alternative-providers-2026.md +1428 -0
- package/research/llm-summarization/anthropic-2026.md +367 -0
- package/research/llm-summarization/claude-cli-integration.md +1706 -0
- package/research/llm-summarization/cli-integration-patterns.md +3155 -0
- package/research/llm-summarization/openai-2026.md +473 -0
- package/research/llm-summarization/openai-compatible-providers-2026.md +1022 -0
- package/research/llm-summarization/opencode-cli-integration.md +1552 -0
- package/research/llm-summarization/prompt-engineering-2026.md +1426 -0
- package/research/llm-summarization/prototype-results.md +56 -0
- package/research/llm-summarization/provider-switching-patterns-2026.md +2153 -0
- package/research/llm-summarization/typescript-llm-libraries-2026.md +2436 -0
- package/research/mdcontext-pudding/00-EXECUTIVE-SUMMARY.md +282 -0
- package/research/mdcontext-pudding/01-index-embed.md +956 -0
- package/research/mdcontext-pudding/02-search-COMMANDS.md +142 -0
- package/research/mdcontext-pudding/02-search-SUMMARY.md +146 -0
- package/research/mdcontext-pudding/02-search.md +970 -0
- package/research/mdcontext-pudding/03-context.md +779 -0
- package/research/mdcontext-pudding/04-navigation-and-analytics.md +803 -0
- package/research/mdcontext-pudding/04-tree.md +704 -0
- package/research/mdcontext-pudding/05-config.md +1038 -0
- package/research/mdcontext-pudding/06-links-summary.txt +87 -0
- package/research/mdcontext-pudding/06-links.md +679 -0
- package/research/mdcontext-pudding/07-stats.md +693 -0
- package/research/mdcontext-pudding/BUG-FIX-PLAN.md +388 -0
- package/research/mdcontext-pudding/P0-BUG-VALIDATION.md +167 -0
- package/research/mdcontext-pudding/README.md +168 -0
- package/research/mdcontext-pudding/TESTING-SUMMARY.md +128 -0
- package/research/research-quality-review.md +834 -0
- package/research/semantic-search/embedding-text-analysis.md +156 -0
- package/research/semantic-search/multi-word-failure-reproduction.md +171 -0
- package/research/semantic-search/query-processing-analysis.md +207 -0
- package/research/semantic-search/root-cause-and-solution.md +114 -0
- package/research/semantic-search/threshold-validation-report.md +69 -0
- package/research/semantic-search/vector-search-analysis.md +63 -0
- package/research/test-path-issues.md +276 -0
- package/review/ALP-76/1-error-type-design.md +962 -0
- package/review/ALP-76/2-error-handling-patterns.md +906 -0
- package/review/ALP-76/3-error-presentation.md +624 -0
- package/review/ALP-76/4-test-coverage.md +625 -0
- package/review/ALP-76/5-migration-completeness.md +440 -0
- package/review/ALP-76/6-effect-best-practices.md +755 -0
- package/scripts/apply-branch-protection.sh +47 -0
- package/scripts/branch-protection-templates.json +79 -0
- package/scripts/prototype-summarization.ts +346 -0
- package/scripts/rebuild-hnswlib.js +32 -37
- package/scripts/setup-branch-protection.sh +64 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/active-provider.json +7 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.json +541 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.meta.json +5 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/config.json +8 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/documents.json +60 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/links.json +13 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/sections.json +1197 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/configuration-management.md +99 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/distributed-systems.md +92 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/error-handling.md +78 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/failure-automation.md +55 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/job-context.md +69 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/process-orchestration.md +99 -0
- package/src/cli/argv-preprocessor.test.ts +2 -2
- package/src/cli/cli.test.ts +230 -33
- package/src/cli/commands/config-cmd.ts +642 -0
- package/src/cli/commands/context.ts +97 -9
- package/src/cli/commands/duplicates.ts +122 -0
- package/src/cli/commands/embeddings.ts +529 -0
- package/src/cli/commands/index-cmd.ts +210 -30
- package/src/cli/commands/index.ts +3 -0
- package/src/cli/commands/search.ts +894 -64
- package/src/cli/commands/stats.ts +3 -0
- package/src/cli/commands/tree.ts +26 -5
- package/src/cli/config-layer.ts +176 -0
- package/src/cli/error-handler.test.ts +235 -0
- package/src/cli/error-handler.ts +655 -0
- package/src/cli/flag-schemas.ts +66 -0
- package/src/cli/help.ts +209 -7
- package/src/cli/main.ts +348 -58
- package/src/cli/options.ts +10 -0
- package/src/cli/shared-error-handling.ts +199 -0
- package/src/cli/utils.ts +150 -17
- package/src/config/file-provider.test.ts +320 -0
- package/src/config/file-provider.ts +273 -0
- package/src/config/index.ts +72 -0
- package/src/config/integration.test.ts +667 -0
- package/src/config/precedence.test.ts +277 -0
- package/src/config/precedence.ts +451 -0
- package/src/config/schema.test.ts +414 -0
- package/src/config/schema.ts +603 -0
- package/src/config/service.test.ts +320 -0
- package/src/config/service.ts +243 -0
- package/src/config/testing.test.ts +264 -0
- package/src/config/testing.ts +110 -0
- package/src/core/types.ts +6 -33
- package/src/duplicates/detector.test.ts +183 -0
- package/src/duplicates/detector.ts +414 -0
- package/src/duplicates/index.ts +18 -0
- package/src/embeddings/embedding-namespace.test.ts +300 -0
- package/src/embeddings/embedding-namespace.ts +947 -0
- package/src/embeddings/heading-boost.test.ts +222 -0
- package/src/embeddings/hnsw-build-options.test.ts +198 -0
- package/src/embeddings/hyde.test.ts +272 -0
- package/src/embeddings/hyde.ts +264 -0
- package/src/embeddings/index.ts +2 -0
- package/src/embeddings/openai-provider.ts +332 -83
- package/src/embeddings/pricing.json +22 -0
- package/src/embeddings/provider-constants.ts +204 -0
- package/src/embeddings/provider-errors.test.ts +967 -0
- package/src/embeddings/provider-errors.ts +565 -0
- package/src/embeddings/provider-factory.test.ts +240 -0
- package/src/embeddings/provider-factory.ts +225 -0
- package/src/embeddings/provider-integration.test.ts +788 -0
- package/src/embeddings/query-preprocessing.test.ts +187 -0
- package/src/embeddings/semantic-search-threshold.test.ts +508 -0
- package/src/embeddings/semantic-search.ts +780 -93
- package/src/embeddings/types.ts +293 -16
- package/src/embeddings/vector-store.ts +486 -77
- package/src/embeddings/voyage-provider.ts +313 -0
- package/src/errors/errors.test.ts +845 -0
- package/src/errors/index.ts +533 -0
- package/src/index/ignore-patterns.test.ts +354 -0
- package/src/index/ignore-patterns.ts +305 -0
- package/src/index/indexer.ts +286 -48
- package/src/index/storage.ts +94 -30
- package/src/index/types.ts +40 -2
- package/src/index/watcher.ts +67 -9
- package/src/index.ts +22 -0
- package/src/integration/search-keyword.test.ts +678 -0
- package/src/mcp/server.ts +135 -6
- package/src/parser/parser.ts +18 -19
- package/src/parser/section-filter.test.ts +277 -0
- package/src/parser/section-filter.ts +125 -3
- package/src/search/__tests__/hybrid-search.test.ts +650 -0
- package/src/search/bm25-store.ts +366 -0
- package/src/search/cross-encoder.test.ts +253 -0
- package/src/search/cross-encoder.ts +406 -0
- package/src/search/fuzzy-search.test.ts +419 -0
- package/src/search/fuzzy-search.ts +273 -0
- package/src/search/hybrid-search.ts +448 -0
- package/src/search/path-matcher.test.ts +276 -0
- package/src/search/path-matcher.ts +33 -0
- package/src/search/searcher.test.ts +99 -1
- package/src/search/searcher.ts +189 -67
- package/src/search/wink-bm25.d.ts +30 -0
- package/src/summarization/cli-providers/claude.ts +202 -0
- package/src/summarization/cli-providers/detection.test.ts +273 -0
- package/src/summarization/cli-providers/detection.ts +118 -0
- package/src/summarization/cli-providers/index.ts +8 -0
- package/src/summarization/cost.test.ts +139 -0
- package/src/summarization/cost.ts +102 -0
- package/src/summarization/error-handler.test.ts +127 -0
- package/src/summarization/error-handler.ts +111 -0
- package/src/summarization/index.ts +102 -0
- package/src/summarization/pipeline.test.ts +498 -0
- package/src/summarization/pipeline.ts +231 -0
- package/src/summarization/prompts.test.ts +269 -0
- package/src/summarization/prompts.ts +133 -0
- package/src/summarization/provider-factory.test.ts +396 -0
- package/src/summarization/provider-factory.ts +178 -0
- package/src/summarization/types.ts +184 -0
- package/src/summarize/summarizer.ts +104 -35
- package/src/types/huggingface-transformers.d.ts +66 -0
- package/tests/fixtures/cli/.mdcontext/active-provider.json +7 -0
- package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
- package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
- package/tests/fixtures/cli/.mdcontext/indexes/documents.json +4 -4
- package/tests/fixtures/cli/.mdcontext/indexes/sections.json +14 -0
- package/tests/integration/embed-index.test.ts +712 -0
- package/tests/integration/search-context.test.ts +469 -0
- package/tests/integration/search-semantic.test.ts +522 -0
- package/vitest.config.ts +1 -6
- package/AGENTS.md +0 -46
- package/tests/fixtures/cli/.mdcontext/vectors.bin +0 -0
- package/tests/fixtures/cli/.mdcontext/vectors.meta.json +0 -1264
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Summarization Pipeline Module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Effect } from 'effect'
|
|
6
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
7
|
+
import {
|
|
8
|
+
formatResultsForSummary,
|
|
9
|
+
type PipelineOptions,
|
|
10
|
+
runSummarizationPipeline,
|
|
11
|
+
type SummarizableResult,
|
|
12
|
+
summarizeResults,
|
|
13
|
+
} from './pipeline.js'
|
|
14
|
+
import type { SearchContext } from './prompts.js'
|
|
15
|
+
import { SummarizationError } from './types.js'
|
|
16
|
+
|
|
17
|
+
vi.mock('./provider-factory.js', () => ({
|
|
18
|
+
createSummarizer: vi.fn(),
|
|
19
|
+
getBestAvailableSummarizer: vi.fn(),
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
vi.mock('./cost.js', () => ({
|
|
23
|
+
estimateSummaryCost: vi.fn(),
|
|
24
|
+
}))
|
|
25
|
+
|
|
26
|
+
import { estimateSummaryCost } from './cost.js'
|
|
27
|
+
import {
|
|
28
|
+
createSummarizer,
|
|
29
|
+
getBestAvailableSummarizer,
|
|
30
|
+
} from './provider-factory.js'
|
|
31
|
+
|
|
32
|
+
const mockedCreateSummarizer = vi.mocked(createSummarizer)
|
|
33
|
+
const mockedGetBestAvailableSummarizer = vi.mocked(getBestAvailableSummarizer)
|
|
34
|
+
const mockedEstimateSummaryCost = vi.mocked(estimateSummaryCost)
|
|
35
|
+
|
|
36
|
+
describe('formatResultsForSummary', () => {
|
|
37
|
+
it('should format basic results with documentPath and heading', () => {
|
|
38
|
+
const results: SummarizableResult[] = [
|
|
39
|
+
{ documentPath: '/docs/readme.md', heading: 'Getting Started' },
|
|
40
|
+
{ documentPath: '/docs/api.md', heading: 'API Reference' },
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
const formatted = formatResultsForSummary(results)
|
|
44
|
+
|
|
45
|
+
expect(formatted).toContain('[1] /docs/readme.md')
|
|
46
|
+
expect(formatted).toContain('Heading: Getting Started')
|
|
47
|
+
expect(formatted).toContain('[2] /docs/api.md')
|
|
48
|
+
expect(formatted).toContain('Heading: API Reference')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should include score when present', () => {
|
|
52
|
+
const results: SummarizableResult[] = [
|
|
53
|
+
{ documentPath: '/docs/guide.md', heading: 'User Guide', score: 0.95 },
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
const formatted = formatResultsForSummary(results)
|
|
57
|
+
|
|
58
|
+
expect(formatted).toContain('Score: 95.0%')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('should include similarity when present', () => {
|
|
62
|
+
const results: SummarizableResult[] = [
|
|
63
|
+
{
|
|
64
|
+
documentPath: '/docs/guide.md',
|
|
65
|
+
heading: 'User Guide',
|
|
66
|
+
similarity: 0.87,
|
|
67
|
+
},
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
const formatted = formatResultsForSummary(results)
|
|
71
|
+
|
|
72
|
+
expect(formatted).toContain('Similarity: 87.0%')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should include both score and similarity when present', () => {
|
|
76
|
+
const results: SummarizableResult[] = [
|
|
77
|
+
{
|
|
78
|
+
documentPath: '/docs/guide.md',
|
|
79
|
+
heading: 'User Guide',
|
|
80
|
+
score: 0.95,
|
|
81
|
+
similarity: 0.87,
|
|
82
|
+
},
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
const formatted = formatResultsForSummary(results)
|
|
86
|
+
|
|
87
|
+
expect(formatted).toContain('Score: 95.0%')
|
|
88
|
+
expect(formatted).toContain('Similarity: 87.0%')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('should truncate long content to 500 chars', () => {
|
|
92
|
+
const longContent = 'x'.repeat(600)
|
|
93
|
+
const results: SummarizableResult[] = [
|
|
94
|
+
{
|
|
95
|
+
documentPath: '/docs/long.md',
|
|
96
|
+
heading: 'Long Content',
|
|
97
|
+
content: longContent,
|
|
98
|
+
},
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
const formatted = formatResultsForSummary(results)
|
|
102
|
+
|
|
103
|
+
expect(formatted).toContain(`Content: ${'x'.repeat(500)}...`)
|
|
104
|
+
expect(formatted).not.toContain('x'.repeat(501))
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('should include full content when under 500 chars', () => {
|
|
108
|
+
const shortContent = 'This is short content'
|
|
109
|
+
const results: SummarizableResult[] = [
|
|
110
|
+
{
|
|
111
|
+
documentPath: '/docs/short.md',
|
|
112
|
+
heading: 'Short Content',
|
|
113
|
+
content: shortContent,
|
|
114
|
+
},
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
const formatted = formatResultsForSummary(results)
|
|
118
|
+
|
|
119
|
+
expect(formatted).toContain('Content: This is short content')
|
|
120
|
+
expect(formatted).not.toContain('...')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('should handle empty array', () => {
|
|
124
|
+
const results: SummarizableResult[] = []
|
|
125
|
+
|
|
126
|
+
const formatted = formatResultsForSummary(results)
|
|
127
|
+
|
|
128
|
+
expect(formatted).toBe('')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('should number results correctly', () => {
|
|
132
|
+
const results: SummarizableResult[] = [
|
|
133
|
+
{ documentPath: '/a.md', heading: 'First' },
|
|
134
|
+
{ documentPath: '/b.md', heading: 'Second' },
|
|
135
|
+
{ documentPath: '/c.md', heading: 'Third' },
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
const formatted = formatResultsForSummary(results)
|
|
139
|
+
|
|
140
|
+
expect(formatted).toContain('[1] /a.md')
|
|
141
|
+
expect(formatted).toContain('[2] /b.md')
|
|
142
|
+
expect(formatted).toContain('[3] /c.md')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('should separate results with blank lines', () => {
|
|
146
|
+
const results: SummarizableResult[] = [
|
|
147
|
+
{ documentPath: '/a.md', heading: 'First' },
|
|
148
|
+
{ documentPath: '/b.md', heading: 'Second' },
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
const formatted = formatResultsForSummary(results)
|
|
152
|
+
|
|
153
|
+
expect(formatted).toContain('\n\n')
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
describe('runSummarizationPipeline', () => {
|
|
158
|
+
const mockSearchContext: SearchContext = {
|
|
159
|
+
query: 'test query',
|
|
160
|
+
resultCount: 3,
|
|
161
|
+
searchMode: 'hybrid',
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const mockResults: SummarizableResult[] = [
|
|
165
|
+
{ documentPath: '/docs/test.md', heading: 'Test', content: 'Test content' },
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
const mockSummarizer = {
|
|
169
|
+
summarize: vi.fn(),
|
|
170
|
+
isAvailable: vi.fn().mockResolvedValue(true),
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const mockCostEstimate = {
|
|
174
|
+
inputTokens: 100,
|
|
175
|
+
outputTokens: 500,
|
|
176
|
+
estimatedCost: 0,
|
|
177
|
+
provider: 'claude',
|
|
178
|
+
isPaid: false,
|
|
179
|
+
formattedCost: 'FREE (subscription)',
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
beforeEach(() => {
|
|
183
|
+
vi.clearAllMocks()
|
|
184
|
+
mockedEstimateSummaryCost.mockReturnValue(mockCostEstimate)
|
|
185
|
+
mockSummarizer.summarize.mockResolvedValue({
|
|
186
|
+
summary: 'Test summary',
|
|
187
|
+
provider: 'claude',
|
|
188
|
+
mode: 'cli',
|
|
189
|
+
estimatedCost: 0,
|
|
190
|
+
durationMs: 1000,
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('should get summarizer from config when provided', async () => {
|
|
195
|
+
mockedCreateSummarizer.mockResolvedValue(mockSummarizer)
|
|
196
|
+
|
|
197
|
+
const options: PipelineOptions = {
|
|
198
|
+
config: { mode: 'cli', provider: 'claude' },
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const result = await Effect.runPromise(
|
|
202
|
+
runSummarizationPipeline(mockResults, mockSearchContext, options),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
expect(mockedCreateSummarizer).toHaveBeenCalled()
|
|
206
|
+
expect(mockedGetBestAvailableSummarizer).not.toHaveBeenCalled()
|
|
207
|
+
expect(result.summary).toBe('Test summary')
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('should fall back to auto-detection when no config', async () => {
|
|
211
|
+
mockedGetBestAvailableSummarizer.mockResolvedValue({
|
|
212
|
+
summarizer: mockSummarizer,
|
|
213
|
+
config: { mode: 'cli', provider: 'claude' },
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
const result = await Effect.runPromise(
|
|
217
|
+
runSummarizationPipeline(mockResults, mockSearchContext),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
expect(mockedGetBestAvailableSummarizer).toHaveBeenCalled()
|
|
221
|
+
expect(mockedCreateSummarizer).not.toHaveBeenCalled()
|
|
222
|
+
expect(result.summary).toBe('Test summary')
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('should estimate cost and include in result', async () => {
|
|
226
|
+
mockedCreateSummarizer.mockResolvedValue(mockSummarizer)
|
|
227
|
+
|
|
228
|
+
const options: PipelineOptions = {
|
|
229
|
+
config: { mode: 'cli', provider: 'claude' },
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const result = await Effect.runPromise(
|
|
233
|
+
runSummarizationPipeline(mockResults, mockSearchContext, options),
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
expect(mockedEstimateSummaryCost).toHaveBeenCalled()
|
|
237
|
+
expect(result.estimatedCost).toEqual(mockCostEstimate)
|
|
238
|
+
expect(result.isFree).toBe(true)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('should check consent for paid providers when not skipped', async () => {
|
|
242
|
+
mockedCreateSummarizer.mockResolvedValue(mockSummarizer)
|
|
243
|
+
|
|
244
|
+
const paidCostEstimate = {
|
|
245
|
+
...mockCostEstimate,
|
|
246
|
+
isPaid: true,
|
|
247
|
+
estimatedCost: 0.01,
|
|
248
|
+
formattedCost: '$0.0100',
|
|
249
|
+
}
|
|
250
|
+
mockedEstimateSummaryCost.mockReturnValue(paidCostEstimate)
|
|
251
|
+
|
|
252
|
+
const onConsentPrompt = vi.fn().mockResolvedValue(true)
|
|
253
|
+
|
|
254
|
+
const options: PipelineOptions = {
|
|
255
|
+
config: { mode: 'api', provider: 'deepseek' },
|
|
256
|
+
onConsentPrompt,
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const result = await Effect.runPromise(
|
|
260
|
+
runSummarizationPipeline(mockResults, mockSearchContext, options),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
expect(onConsentPrompt).toHaveBeenCalledWith(paidCostEstimate)
|
|
264
|
+
expect(result.isFree).toBe(false)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('should fail when consent is declined for paid providers', async () => {
|
|
268
|
+
mockedCreateSummarizer.mockResolvedValue(mockSummarizer)
|
|
269
|
+
|
|
270
|
+
const paidCostEstimate = {
|
|
271
|
+
...mockCostEstimate,
|
|
272
|
+
isPaid: true,
|
|
273
|
+
estimatedCost: 0.01,
|
|
274
|
+
}
|
|
275
|
+
mockedEstimateSummaryCost.mockReturnValue(paidCostEstimate)
|
|
276
|
+
|
|
277
|
+
const onConsentPrompt = vi.fn().mockResolvedValue(false)
|
|
278
|
+
|
|
279
|
+
const options: PipelineOptions = {
|
|
280
|
+
config: { mode: 'api', provider: 'deepseek' },
|
|
281
|
+
onConsentPrompt,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
await expect(
|
|
285
|
+
Effect.runPromise(
|
|
286
|
+
runSummarizationPipeline(mockResults, mockSearchContext, options),
|
|
287
|
+
),
|
|
288
|
+
).rejects.toThrow('User declined summarization')
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('should skip consent when skipConsent is true', async () => {
|
|
292
|
+
mockedCreateSummarizer.mockResolvedValue(mockSummarizer)
|
|
293
|
+
|
|
294
|
+
const paidCostEstimate = {
|
|
295
|
+
...mockCostEstimate,
|
|
296
|
+
isPaid: true,
|
|
297
|
+
estimatedCost: 0.01,
|
|
298
|
+
}
|
|
299
|
+
mockedEstimateSummaryCost.mockReturnValue(paidCostEstimate)
|
|
300
|
+
|
|
301
|
+
const onConsentPrompt = vi.fn()
|
|
302
|
+
|
|
303
|
+
const options: PipelineOptions = {
|
|
304
|
+
config: { mode: 'api', provider: 'deepseek' },
|
|
305
|
+
skipConsent: true,
|
|
306
|
+
onConsentPrompt,
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
await Effect.runPromise(
|
|
310
|
+
runSummarizationPipeline(mockResults, mockSearchContext, options),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
expect(onConsentPrompt).not.toHaveBeenCalled()
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it('should return complete PipelineResult with all fields', async () => {
|
|
317
|
+
mockedCreateSummarizer.mockResolvedValue(mockSummarizer)
|
|
318
|
+
mockSummarizer.summarize.mockResolvedValue({
|
|
319
|
+
summary: 'Complete summary',
|
|
320
|
+
provider: 'claude',
|
|
321
|
+
mode: 'cli',
|
|
322
|
+
estimatedCost: 0,
|
|
323
|
+
durationMs: 1234,
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
const options: PipelineOptions = {
|
|
327
|
+
config: { mode: 'cli', provider: 'claude' },
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const result = await Effect.runPromise(
|
|
331
|
+
runSummarizationPipeline(mockResults, mockSearchContext, options),
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
expect(result).toEqual({
|
|
335
|
+
summary: 'Complete summary',
|
|
336
|
+
provider: 'claude',
|
|
337
|
+
mode: 'cli',
|
|
338
|
+
estimatedCost: mockCostEstimate,
|
|
339
|
+
actualCost: 0,
|
|
340
|
+
durationMs: 1234,
|
|
341
|
+
isFree: true,
|
|
342
|
+
})
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('should handle errors from createSummarizer gracefully', async () => {
|
|
346
|
+
mockedCreateSummarizer.mockRejectedValue(
|
|
347
|
+
new SummarizationError('Provider not found', 'PROVIDER_NOT_FOUND'),
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
const options: PipelineOptions = {
|
|
351
|
+
config: { mode: 'cli', provider: 'claude' },
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
await expect(
|
|
355
|
+
Effect.runPromise(
|
|
356
|
+
runSummarizationPipeline(mockResults, mockSearchContext, options),
|
|
357
|
+
),
|
|
358
|
+
).rejects.toThrow('Provider not found')
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
it('should handle errors from getBestAvailableSummarizer gracefully', async () => {
|
|
362
|
+
mockedGetBestAvailableSummarizer.mockResolvedValue(null)
|
|
363
|
+
|
|
364
|
+
await expect(
|
|
365
|
+
Effect.runPromise(
|
|
366
|
+
runSummarizationPipeline(mockResults, mockSearchContext),
|
|
367
|
+
),
|
|
368
|
+
).rejects.toThrow('No summarization providers available')
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
it('should handle errors from summarize gracefully', async () => {
|
|
372
|
+
mockedCreateSummarizer.mockResolvedValue(mockSummarizer)
|
|
373
|
+
mockSummarizer.summarize.mockRejectedValue(
|
|
374
|
+
new Error('Summarization failed'),
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
const options: PipelineOptions = {
|
|
378
|
+
config: { mode: 'cli', provider: 'claude' },
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
await expect(
|
|
382
|
+
Effect.runPromise(
|
|
383
|
+
runSummarizationPipeline(mockResults, mockSearchContext, options),
|
|
384
|
+
),
|
|
385
|
+
).rejects.toThrow('Summarization failed')
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
it('should pass stream option to config', async () => {
|
|
389
|
+
mockedCreateSummarizer.mockResolvedValue(mockSummarizer)
|
|
390
|
+
|
|
391
|
+
const options: PipelineOptions = {
|
|
392
|
+
config: { mode: 'cli', provider: 'claude' },
|
|
393
|
+
stream: true,
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
await Effect.runPromise(
|
|
397
|
+
runSummarizationPipeline(mockResults, mockSearchContext, options),
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
expect(mockedCreateSummarizer).toHaveBeenCalledWith(
|
|
401
|
+
expect.objectContaining({ stream: true }),
|
|
402
|
+
)
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
it('should build prompt with provided template', async () => {
|
|
406
|
+
mockedCreateSummarizer.mockResolvedValue(mockSummarizer)
|
|
407
|
+
|
|
408
|
+
const options: PipelineOptions = {
|
|
409
|
+
config: { mode: 'cli', provider: 'claude' },
|
|
410
|
+
template: 'concise',
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
await Effect.runPromise(
|
|
414
|
+
runSummarizationPipeline(mockResults, mockSearchContext, options),
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
expect(mockSummarizer.summarize).toHaveBeenCalledWith(
|
|
418
|
+
expect.any(String),
|
|
419
|
+
expect.stringContaining('Query: "test query"'),
|
|
420
|
+
)
|
|
421
|
+
})
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
describe('summarizeResults', () => {
|
|
425
|
+
const mockSearchContext: SearchContext = {
|
|
426
|
+
query: 'test query',
|
|
427
|
+
resultCount: 1,
|
|
428
|
+
searchMode: 'semantic',
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const mockResults: SummarizableResult[] = [
|
|
432
|
+
{ documentPath: '/docs/test.md', heading: 'Test' },
|
|
433
|
+
]
|
|
434
|
+
|
|
435
|
+
const mockSummarizer = {
|
|
436
|
+
summarize: vi.fn().mockResolvedValue({
|
|
437
|
+
summary: 'Promise summary',
|
|
438
|
+
provider: 'claude',
|
|
439
|
+
mode: 'cli',
|
|
440
|
+
estimatedCost: 0,
|
|
441
|
+
durationMs: 500,
|
|
442
|
+
}),
|
|
443
|
+
isAvailable: vi.fn().mockResolvedValue(true),
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const mockCostEstimate = {
|
|
447
|
+
inputTokens: 50,
|
|
448
|
+
outputTokens: 500,
|
|
449
|
+
estimatedCost: 0,
|
|
450
|
+
provider: 'claude',
|
|
451
|
+
isPaid: false,
|
|
452
|
+
formattedCost: 'FREE (subscription)',
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
beforeEach(() => {
|
|
456
|
+
vi.clearAllMocks()
|
|
457
|
+
mockedEstimateSummaryCost.mockReturnValue(mockCostEstimate)
|
|
458
|
+
mockedGetBestAvailableSummarizer.mockResolvedValue({
|
|
459
|
+
summarizer: mockSummarizer,
|
|
460
|
+
config: { mode: 'cli', provider: 'claude' },
|
|
461
|
+
})
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
it('should return a Promise that resolves to PipelineResult', async () => {
|
|
465
|
+
const result = await summarizeResults(mockResults, mockSearchContext)
|
|
466
|
+
|
|
467
|
+
expect(result).toEqual({
|
|
468
|
+
summary: 'Promise summary',
|
|
469
|
+
provider: 'claude',
|
|
470
|
+
mode: 'cli',
|
|
471
|
+
estimatedCost: mockCostEstimate,
|
|
472
|
+
actualCost: 0,
|
|
473
|
+
durationMs: 500,
|
|
474
|
+
isFree: true,
|
|
475
|
+
})
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
it('should pass options to underlying pipeline', async () => {
|
|
479
|
+
mockedCreateSummarizer.mockResolvedValue(mockSummarizer)
|
|
480
|
+
|
|
481
|
+
const options: PipelineOptions = {
|
|
482
|
+
config: { mode: 'cli', provider: 'claude' },
|
|
483
|
+
template: 'detailed',
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
await summarizeResults(mockResults, mockSearchContext, options)
|
|
487
|
+
|
|
488
|
+
expect(mockedCreateSummarizer).toHaveBeenCalled()
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
it('should reject with error when pipeline fails', async () => {
|
|
492
|
+
mockedGetBestAvailableSummarizer.mockResolvedValue(null)
|
|
493
|
+
|
|
494
|
+
await expect(
|
|
495
|
+
summarizeResults(mockResults, mockSearchContext),
|
|
496
|
+
).rejects.toThrow('No summarization providers available')
|
|
497
|
+
})
|
|
498
|
+
})
|