* chore: add v1.31.0 npm known-issue notice to issue template config Adds a top-priority contact link to the issue template chooser so users are redirected to the Discussions announcement before opening a duplicate issue about v1.31.0 not being on npm. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(sdk): reduce context prompt sizes with truncation and cache-friendly ordering (#1614) - Reorder prompt assembly in PromptFactory to place stable content (role, workflow, phase instructions) before variable content (.planning/ files), enabling Anthropic prompt caching at 0.1x input cost on cache hits - Add markdown-aware truncation for oversized context files (headings + first paragraphs preserved, rest omitted with line counts) - Add ROADMAP.md milestone extraction to inject only the current milestone instead of the full roadmap - Export truncation utilities from SDK public API - 60 new + updated tests covering truncation, milestone extraction, cache-friendly ordering, and ContextEngine integration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
164 lines
5.9 KiB
TypeScript
164 lines
5.9 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
truncateMarkdown,
|
|
extractCurrentMilestone,
|
|
DEFAULT_TRUNCATION_OPTIONS,
|
|
} from './context-truncation.js';
|
|
|
|
// ─── truncateMarkdown ───────────────────────────────────────────────────────
|
|
|
|
describe('truncateMarkdown', () => {
|
|
it('returns content unchanged when below threshold', () => {
|
|
const content = '# Title\n\nShort content.';
|
|
const result = truncateMarkdown(content, 'TEST.md');
|
|
expect(result).toBe(content);
|
|
});
|
|
|
|
it('truncates content above threshold, keeping headings and first paragraphs', () => {
|
|
const sections = [];
|
|
for (let i = 0; i < 20; i++) {
|
|
sections.push(`## Section ${i}\n\nFirst paragraph of section ${i}.\n\nSecond paragraph with lots of detail.\nMore detail here.\nEven more detail.`);
|
|
}
|
|
const content = `# Title\n\n${sections.join('\n\n')}`;
|
|
const result = truncateMarkdown(content, 'BIG.md', { maxContentLength: 100 });
|
|
|
|
// Headings preserved
|
|
expect(result).toContain('# Title');
|
|
expect(result).toContain('## Section 0');
|
|
expect(result).toContain('## Section 19');
|
|
|
|
// First paragraphs preserved
|
|
expect(result).toContain('First paragraph of section 0.');
|
|
expect(result).toContain('First paragraph of section 19.');
|
|
|
|
// Second paragraphs omitted
|
|
expect(result).not.toContain('Second paragraph');
|
|
expect(result).not.toContain('More detail here.');
|
|
|
|
// Truncation markers present
|
|
expect(result).toContain('[...');
|
|
expect(result).toContain('lines omitted]');
|
|
expect(result).toContain('[Truncated: read .planning/BIG.md for full content]');
|
|
});
|
|
|
|
it('preserves YAML frontmatter entirely', () => {
|
|
const content = `---\nphase: "01"\nstatus: active\n---\n\n# Title\n\nParagraph 1.\n\nParagraph 2.\n${'x'.repeat(10000)}`;
|
|
const result = truncateMarkdown(content, 'STATE.md', { maxContentLength: 100 });
|
|
|
|
expect(result).toContain('---\nphase: "01"\nstatus: active\n---');
|
|
expect(result).toContain('# Title');
|
|
expect(result).toContain('Paragraph 1.');
|
|
});
|
|
|
|
it('is smaller than original when truncated', () => {
|
|
const longContent = Array.from({ length: 200 }, (_, i) =>
|
|
`## Section ${i}\n\nFirst paragraph.\n\nLong detail paragraph ${'x'.repeat(100)}.`
|
|
).join('\n\n');
|
|
|
|
const result = truncateMarkdown(longContent, 'HUGE.md', { maxContentLength: 100 });
|
|
expect(result.length).toBeLessThan(longContent.length);
|
|
});
|
|
|
|
it('handles content with no headings', () => {
|
|
const content = `First line.\n\nSecond paragraph.\n\nThird paragraph.\n${'x'.repeat(10000)}`;
|
|
const result = truncateMarkdown(content, 'FLAT.md', { maxContentLength: 100 });
|
|
|
|
// Should still truncate — first paragraph kept
|
|
expect(result).toContain('First line.');
|
|
expect(result.length).toBeLessThan(content.length);
|
|
});
|
|
|
|
it('default threshold is 8192 characters', () => {
|
|
expect(DEFAULT_TRUNCATION_OPTIONS.maxContentLength).toBe(8192);
|
|
});
|
|
});
|
|
|
|
// ─── extractCurrentMilestone ────────────────────────────────────────────────
|
|
|
|
describe('extractCurrentMilestone', () => {
|
|
const makeRoadmap = () => `# Project Roadmap
|
|
|
|
## Milestone 1: Foundation
|
|
### Phase 01: Setup
|
|
Requirements for setup.
|
|
### Phase 02: Core
|
|
Requirements for core.
|
|
|
|
## Milestone 2: Features
|
|
### Phase 03: Auth
|
|
Requirements for auth.
|
|
### Phase 04: API
|
|
Requirements for API.
|
|
|
|
## Milestone 3: Polish
|
|
### Phase 05: UI
|
|
Requirements for UI.`;
|
|
|
|
it('returns full roadmap when no state provided', () => {
|
|
const roadmap = makeRoadmap();
|
|
expect(extractCurrentMilestone(roadmap)).toBe(roadmap);
|
|
});
|
|
|
|
it('returns full roadmap when milestone not found in state', () => {
|
|
const roadmap = makeRoadmap();
|
|
const state = '# State\nstatus: active';
|
|
expect(extractCurrentMilestone(roadmap, state)).toBe(roadmap);
|
|
});
|
|
|
|
it('extracts current milestone section by name', () => {
|
|
const roadmap = makeRoadmap();
|
|
const state = 'Current Milestone: Features';
|
|
const result = extractCurrentMilestone(roadmap, state);
|
|
|
|
expect(result).toContain('## Milestone 2: Features');
|
|
expect(result).toContain('### Phase 03: Auth');
|
|
expect(result).toContain('### Phase 04: API');
|
|
|
|
// Other milestones omitted
|
|
expect(result).not.toContain('### Phase 01: Setup');
|
|
expect(result).not.toContain('### Phase 05: UI');
|
|
expect(result).toContain('other milestone(s) omitted');
|
|
});
|
|
|
|
it('matches milestone name case-insensitively', () => {
|
|
const roadmap = makeRoadmap();
|
|
const state = 'current milestone: features';
|
|
const result = extractCurrentMilestone(roadmap, state);
|
|
|
|
expect(result).toContain('## Milestone 2: Features');
|
|
expect(result).not.toContain('### Phase 01: Setup');
|
|
});
|
|
|
|
it('matches milestone from "milestone:" field in state', () => {
|
|
const roadmap = makeRoadmap();
|
|
const state = '# State\nmilestone: Foundation\nphase: 01';
|
|
const result = extractCurrentMilestone(roadmap, state);
|
|
|
|
expect(result).toContain('## Milestone 1: Foundation');
|
|
expect(result).toContain('### Phase 01: Setup');
|
|
expect(result).not.toContain('### Phase 03: Auth');
|
|
});
|
|
|
|
it('matches milestone from Current Position block', () => {
|
|
const roadmap = makeRoadmap();
|
|
const state = `# State
|
|
|
|
## Current Position
|
|
milestone: Polish
|
|
phase: 05`;
|
|
const result = extractCurrentMilestone(roadmap, state);
|
|
|
|
expect(result).toContain('## Milestone 3: Polish');
|
|
expect(result).toContain('### Phase 05: UI');
|
|
expect(result).not.toContain('### Phase 01: Setup');
|
|
});
|
|
|
|
it('preserves roadmap title in output', () => {
|
|
const roadmap = makeRoadmap();
|
|
const state = 'Current Milestone: Features';
|
|
const result = extractCurrentMilestone(roadmap, state);
|
|
|
|
expect(result).toContain('# Project Roadmap');
|
|
});
|
|
});
|