aaronsb_obsidian-mcp-plugin/tests/graph-node-title.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

98 lines
3.3 KiB
TypeScript

import { App, TFile } from 'obsidian';
import { GraphSearchTool } from '../src/tools/graph-search';
import { GraphTraversal } from '../src/utils/graph-traversal';
import { ObsidianAPI } from '../src/utils/obsidian-api';
function makeFile(path: string, basename: string, parentName?: string): TFile {
const file = new TFile();
file.path = path;
file.name = `${basename}.md`;
file.basename = basename;
file.extension = 'md';
(file as any).parent = parentName
? { name: parentName }
: null;
return file;
}
describe('graph node titles', () => {
let app: App;
let traversal: GraphTraversal;
let search: GraphSearchTool;
let indexFile: TFile;
let regularFile: TFile;
let sourceFile: TFile;
beforeEach(() => {
indexFile = makeFile('topics/rendering/index.md', 'index', 'rendering');
regularFile = makeFile('topics/rendering/overview.md', 'overview', 'rendering');
sourceFile = makeFile('source.md', 'source');
const filesByPath = new Map([
[indexFile.path, indexFile],
[regularFile.path, regularFile],
[sourceFile.path, sourceFile]
]);
app = new App();
(app as any).metadataCache = {
resolvedLinks: {
'source.md': { 'topics/rendering/index.md': 1 },
'topics/rendering/index.md': { 'topics/rendering/overview.md': 1 }
},
unresolvedLinks: {},
getFileCache: jest.fn().mockReturnValue({ tags: [] })
};
app.vault.getFiles = jest.fn(() => [indexFile, regularFile, sourceFile]);
app.vault.getAbstractFileByPath = jest.fn((path: string) => filesByPath.get(path) ?? null);
traversal = new GraphTraversal(app);
search = new GraphSearchTool({ getIgnoreManager: () => undefined } as unknown as ObsidianAPI, app);
});
it('uses the parent folder as the graph title for index files', () => {
expect(traversal.getNodeTitle(indexFile)).toBe('rendering');
expect(traversal.getNodeTitle(regularFile)).toBe('overview');
});
it('uses resolved graph titles in vault node listings', () => {
const nodes = traversal.getAllNodes();
expect(nodes).toEqual(
expect.arrayContaining([
expect.objectContaining({ path: 'topics/rendering/index.md', title: 'rendering' }),
expect.objectContaining({ path: 'topics/rendering/overview.md', title: 'overview' })
])
);
});
it('uses resolved graph titles in forwardlink results', () => {
const result = search.search({ operation: 'forwardlinks', sourcePath: 'source.md' });
expect(result.nodes).toEqual([
expect.objectContaining({ path: 'topics/rendering/index.md', title: 'rendering' })
]);
});
it('uses resolved graph titles in backlink results', () => {
const result = search.search({ operation: 'backlinks', sourcePath: 'topics/rendering/overview.md' });
expect(result.nodes).toEqual([
expect.objectContaining({ path: 'topics/rendering/index.md', title: 'rendering' })
]);
});
it('uses resolved graph titles in path results', () => {
const result = search.search({
operation: 'path',
sourcePath: 'source.md',
targetPath: 'topics/rendering/overview.md'
});
expect(result.paths?.[0]).toEqual([
{ path: 'source.md', title: 'source' },
{ path: 'topics/rendering/index.md', title: 'rendering' },
{ path: 'topics/rendering/overview.md', title: 'overview' }
]);
});
});