mirror of
https://github.com/aaronsb/obsidian-mcp-plugin.git
synced 2026-07-22 06:45:14 +00:00
Reimplemented from PR #126 (djsplice, who also reported #125), reduced to the parts that are verified-clean. The worker-offload half of #126 is split out to a tracking issue (inert as wired + correctness divergences — see PR discussion / the follow-up issue). SSE route deconfliction In @modelcontextprotocol/sdk@1.29.0 (the pinned version), `GET /mcp` is the standalone server->client SSE stream for server-initiated messages — verified in webStandardStreamableHttp.js:handleGetRequest (opens `_GET_stream`, Content-Type text/event-stream, gated on Accept: text/event-stream + session + protocol version). A debug route returning application/json on `GET /mcp` shadowed it, so any client that opens the standalone stream (mcp-remote-class bridges — #128's logs show one in use here) received JSON instead of an SSE stream and treated it as failed. Moved debug to `GET /mcp-info`; `GET /mcp` and `POST /mcp` both reach handleMCPRequest. Unlike #126's `app.all('/mcp')`, GET/POST are registered individually so the existing explicit `app.delete('/mcp')` session-close handler is preserved. Scope note: this restores the server-initiated-notification channel (a real latent defect on its own). It is NOT claimed to fix #128 — per the #190 investigation that is a separate SDK-1.29 compat-init problem — and #125's "SSE reconnection loop" is the reporter's un-reproduced diagnosis. Two-row Levenshtein fuzzy-match.ts rewritten from a full (m+1)x(n+1) matrix to a two-row formulation (O(n) memory, no per-line array-of-arrays allocation) plus length heuristics and a 0.95 early-exit. Pure CPU/memory win on the main-thread edit.window path; behaviour-preserving. Adds the first fuzzy-match tests (classic Levenshtein distances, symmetry, heuristic-preservation). build.yml: PR-status-comment step set continue-on-error so it cannot red an otherwise-green build on fork/limited-token PRs. Co-authored-by: djsplice <barrows.jeff@gmail.com>
67 lines
2.4 KiB
TypeScript
67 lines
2.4 KiB
TypeScript
import { calculateSimilarity, findFuzzyMatches } from '../src/utils/fuzzy-match';
|
|
|
|
// Guards the two-row Levenshtein rewrite (#126 / ADR-104). The optimization
|
|
// must not change matching *outcomes* — only memory/CPU. These pin known
|
|
// Levenshtein distances so a regression in the row-swap logic is caught.
|
|
describe('calculateSimilarity (two-row Levenshtein)', () => {
|
|
test('identical strings score 1', () => {
|
|
expect(calculateSimilarity('hello world', 'hello world')).toBe(1);
|
|
});
|
|
|
|
test('both empty score 1', () => {
|
|
expect(calculateSimilarity('', '')).toBe(1);
|
|
});
|
|
|
|
test('one empty scores 0', () => {
|
|
expect(calculateSimilarity('', 'abc')).toBe(0);
|
|
expect(calculateSimilarity('abc', '')).toBe(0);
|
|
});
|
|
|
|
test('kitten/sitting → distance 3 of max-len 7', () => {
|
|
// classic Levenshtein example: distance 3
|
|
expect(calculateSimilarity('kitten', 'sitting')).toBeCloseTo(1 - 3 / 7, 6);
|
|
});
|
|
|
|
test('flaw/lawn → distance 2 of max-len 4', () => {
|
|
expect(calculateSimilarity('flaw', 'lawn')).toBeCloseTo(1 - 2 / 4, 6);
|
|
});
|
|
|
|
test('is case-insensitive', () => {
|
|
expect(calculateSimilarity('Hello', 'hello')).toBe(1);
|
|
});
|
|
|
|
test('is symmetric (row order must not matter after the rewrite)', () => {
|
|
const a = 'the quick brown fox';
|
|
const b = 'the quikc brwn fx';
|
|
expect(calculateSimilarity(a, b)).toBeCloseTo(calculateSimilarity(b, a), 9);
|
|
});
|
|
});
|
|
|
|
describe('findFuzzyMatches', () => {
|
|
const content = [
|
|
'The quick brown fox jumps',
|
|
'over the lazy dog',
|
|
'completely unrelated text here',
|
|
].join('\n');
|
|
|
|
test('exact substring is a similarity-1 match with correct line number', () => {
|
|
const m = findFuzzyMatches(content, 'lazy dog', 0.7);
|
|
expect(m.length).toBeGreaterThan(0);
|
|
expect(m[0].similarity).toBe(1.0);
|
|
expect(m[0].lineNumber).toBe(2);
|
|
});
|
|
|
|
test('near match above threshold is found despite the heuristic skips', () => {
|
|
const m = findFuzzyMatches(content, 'quick brown fox', 0.6);
|
|
expect(m.some(x => x.lineNumber === 1)).toBe(true);
|
|
});
|
|
|
|
test('unrelated query below threshold yields nothing', () => {
|
|
expect(findFuzzyMatches(content, 'xylophone zeppelin quasar', 0.7)).toEqual([]);
|
|
});
|
|
|
|
test('empty / whitespace search returns no matches', () => {
|
|
expect(findFuzzyMatches(content, '', 0.7)).toEqual([]);
|
|
expect(findFuzzyMatches(content, ' ', 0.7)).toEqual([]);
|
|
});
|
|
});
|