Additive test coverage across server, editor-ext, client and mcp. #192 — AiChatService.stream integration (Section 3, against real Postgres): - new apps/server/test/integration/ai-chat-stream.int-spec.ts drives the real streamText through a seeded ai/test MockLanguageModelV3 and a real Node ServerResponse, covering: onError persists an assistant error record (status 'error' + partial answer + provider cause in metadata); external MCP client closed exactly once on BOTH onFinish and onError; anti-tamper — history is rebuilt from the DB transcript, not from body.messages. #206 — red-team findings (most already fixed+tested in #212): - mdrt-2 (UNFIXED, data loss): turndown.dataloss.test.ts documents that pageBreak / transclusionReference / mention are silently dropped on Markdown export (characterization + it.fails for the desired survive-export contract). - persist-6 (UNFIXED, data loss): persistence-store.spec.ts adds an it.failing documenting that a momentarily-empty live doc overwrites non-empty content (left unfixed — a store-side empty-guard is a behaviour change). #204 — test-strategy plan, highest-priority subset: - Phase 1: mcp-clients.lease.spec.ts covers the external MCP client lease/refcount/eviction lifecycle (leak / premature-close / double-close). - Phase 2 data-integrity pure functions: editor-ext table-utils (transpose/moveRow/convert round-trip) and math tokenizer false-positive guard; client emoji-menu (+ it.fails for the unguarded localStorage JSON.parse bug), sort-cells, normalizeTableColumnWidths; mcp htmlEmbed/ pageBreak markdown data-loss + footnote-diff; server export getInternalLinkPageName extensionless-path bug — FIXED (small/clear) + tested. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -146,6 +146,19 @@ describe('getInternalLinkPageName', () => {
|
||||
expect(getInternalLinkPageName('Parent/My%20Page.md')).toBe('My Page');
|
||||
});
|
||||
|
||||
it('keeps the full basename when the path has no extension (#204)', () => {
|
||||
// An extensionless link target must NOT be stripped to an empty string —
|
||||
// there is no extension to drop. Previously `.split('.').slice(0,-1)`
|
||||
// collapsed "My Page" to "" and the internal link rendered with no text.
|
||||
expect(getInternalLinkPageName('Parent/My%20Page')).toBe('My Page');
|
||||
expect(getInternalLinkPageName('Just A Name')).toBe('Just A Name');
|
||||
});
|
||||
|
||||
it('preserves dots in a dotted name that has a real extension (#204)', () => {
|
||||
// "v1.2.md" -> "v1.2": only the final ".md" segment is the extension.
|
||||
expect(getInternalLinkPageName('docs/v1.2.md')).toBe('v1.2');
|
||||
});
|
||||
|
||||
it('falls back to the raw name without throwing on malformed encoding', () => {
|
||||
// "%E0%A4" is an incomplete escape; decodeURIComponent throws and the
|
||||
// helper returns the raw (still-encoded) name.
|
||||
|
||||
@@ -106,7 +106,14 @@ export function replaceInternalLinks(
|
||||
}
|
||||
|
||||
export function getInternalLinkPageName(path: string, currentFilePath?: string): string {
|
||||
const name = path?.split('/').pop().split('.').slice(0, -1).join('.');
|
||||
// Strip a trailing file extension from the basename, but only when there IS
|
||||
// one: an extensionless link target (e.g. "My Page") has no extension to drop,
|
||||
// so `split('.').slice(0,-1)` would otherwise collapse it to an empty string,
|
||||
// producing an internal link with no visible text (#204 export bug). Dotted
|
||||
// page names without an extension (e.g. "v1.2") keep their dots.
|
||||
const base = path?.split('/').pop();
|
||||
const parts = base?.split('.');
|
||||
const name = parts && parts.length > 1 ? parts.slice(0, -1).join('.') : base;
|
||||
try {
|
||||
return decodeURIComponent(name);
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user