aaronsb_obsidian-mcp-plugin/tests/graph-search.test.ts
fa1k3 42ac8b361e fix(graph): honor MCP ignore exclusions in graph traversal
Graph operations (neighbors/backlinks/forwardlinks/traverse/path/
statistics) read metadataCache.resolvedLinks and vault.getFiles()
directly and never consulted the MCPIgnoreManager. With path
exclusions enabled, .mcpignore-ignored paths surfaced as neighbors/
edges of non-ignored queries, and querying an ignored note directly
disclosed its relationships.

- GraphSearchTool rejects an excluded query root (treated as
  "File not found", matching ObsidianAPI's read-path behavior).
- GraphTraversal filters excluded paths out of the link primitives and
  every file enumeration (neighbors, root traversal, tag connections,
  vault statistics, all-nodes).
- Add ObsidianAPI.getIgnoreManager() accessor + regression tests.

No behavior change when path exclusions are disabled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 14:50:14 +02:00

70 lines
2.1 KiB
TypeScript

import { App, TFile } from 'obsidian';
import { GraphSearchTool } from '../src/tools/graph-search';
import { ObsidianAPI } from '../src/utils/obsidian-api';
function makeFile(path: string): TFile {
const file = new TFile();
file.path = path;
file.name = path.split('/').pop() ?? path;
// getNodeTitle (added in #207, merged before this PR landed) reads
// basename — make the mock match the real TFile API.
file.basename = file.name.replace(/\.md$/, '');
file.extension = 'md';
return file;
}
describe('GraphSearchTool', () => {
let app: App;
let tool: GraphSearchTool;
beforeEach(() => {
const resolvedTarget = makeFile('resolved.md');
app = new App();
(app as any).metadataCache = {
resolvedLinks: {
'source.md': { 'resolved.md': 1 }
},
unresolvedLinks: {
'source.md': { 'Missing Note': 1 }
},
getFileCache: jest.fn().mockReturnValue({ tags: [] })
};
app.vault.getAbstractFileByPath = jest.fn((path: string) =>
path === 'resolved.md' ? resolvedTarget : null
);
tool = new GraphSearchTool({ getIgnoreManager: () => undefined } as unknown as ObsidianAPI, app);
});
it('omits unresolved forward links by default', () => {
const result = tool.search({
operation: 'forwardlinks',
sourcePath: 'source.md'
});
expect(result.edges).toEqual([
{ source: 'source.md', target: 'resolved.md', type: 'link', count: 1 }
]);
expect(result.nodes).toEqual([
expect.objectContaining({ path: 'resolved.md', title: 'resolved' })
]);
});
it('includes unresolved forward links when requested', () => {
const result = tool.search({
operation: 'forwardlinks',
sourcePath: 'source.md',
includeUnresolved: true
});
expect(result.edges).toEqual([
{ source: 'source.md', target: 'resolved.md', type: 'link', count: 1 },
{ source: 'source.md', target: 'Missing Note', type: 'link', count: 1 }
]);
expect(result.nodes).toEqual([
expect.objectContaining({ path: 'resolved.md', title: 'resolved' })
]);
expect(result.message).toBe('Found 2 files linked from this file');
});
});