test: unit cover adaptor subwiki routing

This commit is contained in:
linonetwo 2025-12-06 02:27:56 +08:00
parent a90ccdf44e
commit 8dfdfeb412
3 changed files with 425 additions and 5 deletions

View file

@ -857,7 +857,6 @@ ${tiddler.content}
fileSystemPathFilterEnable: Boolean(options.fileSystemPathFilter), fileSystemPathFilterEnable: Boolean(options.fileSystemPathFilter),
fileSystemPathFilter: options.fileSystemPathFilter ?? null, fileSystemPathFilter: options.fileSystemPathFilter ?? null,
tagNames: [tagName], tagNames: [tagName],
tagName: tagName,
userName: '', userName: '',
order: 1, order: 1,
port: 5213, port: 5213,

View file

@ -45,7 +45,7 @@ const workspacesSubject = new BehaviorSubject([
mainWikiID: 'workspace-1', mainWikiID: 'workspace-1',
mainWikiToLink: '/path/to/wiki1', mainWikiToLink: '/path/to/wiki1',
port: 5213, port: 5213,
tagName: 'WorkNotes', tagNames: ['WorkNotes'],
metadata: { badgeCount: 5 }, metadata: { badgeCount: 5 },
}, },
// Built-in page workspaces generated from pageTypes // Built-in page workspaces generated from pageTypes

View file

@ -37,6 +37,13 @@ global.$tw = {
files: {} as Record<string, IFileInfo>, files: {} as Record<string, IFileInfo>,
}, },
utils: mockUtils, utils: mockUtils,
wiki: {
filterTiddlers: vi.fn(() => []),
makeTiddlerIterator: vi.fn((titles) => titles),
},
rootWidget: {
makeFakeWidgetWithVariables: vi.fn(() => ({})),
},
}; };
describe('FileSystemAdaptor - Routing Logic', () => { describe('FileSystemAdaptor - Routing Logic', () => {
@ -275,7 +282,7 @@ describe('FileSystemAdaptor - Routing Logic', () => {
const tiddler: Tiddler = { const tiddler: Tiddler = {
fields: { title: 'TestTiddler', tags: ['SubWikiTag', 'OtherTag'] }, fields: { title: 'TestTiddler', tags: ['SubWikiTag', 'OtherTag'] },
} as Tiddler; } as unknown as Tiddler;
await adaptor.getTiddlerFileInfo(tiddler); await adaptor.getTiddlerFileInfo(tiddler);
@ -319,7 +326,7 @@ describe('FileSystemAdaptor - Routing Logic', () => {
const tiddler: Tiddler = { const tiddler: Tiddler = {
fields: { title: 'TestTiddler', tags: ['Tag1', 'Tag2'] }, fields: { title: 'TestTiddler', tags: ['Tag1', 'Tag2'] },
} as Tiddler; } as unknown as Tiddler;
await adaptor.getTiddlerFileInfo(tiddler); await adaptor.getTiddlerFileInfo(tiddler);
@ -378,7 +385,7 @@ describe('FileSystemAdaptor - Routing Logic', () => {
// Tiddler with unmatched tags // Tiddler with unmatched tags
const tiddler: Tiddler = { const tiddler: Tiddler = {
fields: { title: 'TestTiddler', tags: ['UnmatchedTag'] }, fields: { title: 'TestTiddler', tags: ['UnmatchedTag'] },
} as Tiddler; } as unknown as Tiddler;
await adaptor.getTiddlerFileInfo(tiddler); await adaptor.getTiddlerFileInfo(tiddler);
@ -509,4 +516,418 @@ describe('FileSystemAdaptor - Routing Logic', () => {
); );
}); });
}); });
describe('getTiddlerFileInfo - Tag Tree Routing (includeTagTree)', () => {
beforeEach(async () => {
vi.mocked(workspace.get).mockResolvedValue(
{
id: 'test-workspace',
name: 'Test Workspace',
wikiFolderLocation: '/test/wiki',
} as Parameters<typeof workspace.get>[0] extends Promise<infer T> ? T : never,
);
// Setup mock wiki with workspace ID
mockWiki = {
getTiddlerText: vi.fn((title) => {
if (title === '$:/info/tidgi/workspaceID') return 'test-workspace';
return '';
}),
tiddlerExists: vi.fn(() => false),
addTiddler: vi.fn(),
} as unknown as Wiki;
});
it('should route to sub-wiki when tiddler matches tag tree', async () => {
const subWiki = {
id: 'sub-wiki-tagtree',
name: 'Sub Wiki TagTree',
isSubWiki: true,
mainWikiID: 'test-workspace',
tagNames: ['RootTag'],
includeTagTree: true,
wikiFolderLocation: '/test/wiki/subwiki/tagtree',
};
vi.mocked(workspace.getWorkspacesAsList).mockResolvedValue([subWiki] as IWikiWorkspace[]);
// Mock filterTiddlers to return the tiddler when using in-tagtree-of filter
// @ts-expect-error - TiddlyWiki global
global.$tw.wiki.filterTiddlers = vi.fn(() => ['ChildTiddler']);
adaptor = new FileSystemAdaptor({
wiki: mockWiki,
// @ts-expect-error - TiddlyWiki global
boot: global.$tw.boot,
});
await adaptor['updateSubWikisCache']();
const tiddler: Tiddler = {
fields: { title: 'ChildTiddler', tags: ['ParentTag'] }, // Not directly tagged with RootTag
} as unknown as Tiddler;
await adaptor.getTiddlerFileInfo(tiddler);
// Should use sub-wiki directory because tag tree matching found a match
expect(mockUtils.generateTiddlerFileInfo).toHaveBeenCalledWith(
tiddler,
expect.objectContaining({
directory: '/test/wiki/subwiki/tagtree',
}),
);
});
it('should not match tag tree when includeTagTree is disabled', async () => {
const subWiki = {
id: 'sub-wiki-notree',
name: 'Sub Wiki NoTree',
isSubWiki: true,
mainWikiID: 'test-workspace',
tagNames: ['RootTag'],
includeTagTree: false, // Disabled
wikiFolderLocation: '/test/wiki/subwiki/notree',
};
vi.mocked(workspace.getWorkspacesAsList).mockResolvedValue([subWiki] as IWikiWorkspace[]);
// Even if filterTiddlers would return a match, it shouldn't be called
// @ts-expect-error - TiddlyWiki global
global.$tw.wiki.filterTiddlers = vi.fn(() => ['ChildTiddler']);
adaptor = new FileSystemAdaptor({
wiki: mockWiki,
// @ts-expect-error - TiddlyWiki global
boot: global.$tw.boot,
});
await adaptor['updateSubWikisCache']();
const tiddler: Tiddler = {
fields: { title: 'ChildTiddler', tags: ['ParentTag'] }, // Not directly tagged with RootTag
} as unknown as Tiddler;
await adaptor.getTiddlerFileInfo(tiddler);
// Should use default directory because includeTagTree is disabled
expect(mockUtils.generateTiddlerFileInfo).toHaveBeenCalledWith(
tiddler,
expect.objectContaining({
directory: '/test/wiki/tiddlers',
}),
);
});
});
describe('getTiddlerFileInfo - Custom Filter Routing (fileSystemPathFilter)', () => {
beforeEach(async () => {
vi.mocked(workspace.get).mockResolvedValue(
{
id: 'test-workspace',
name: 'Test Workspace',
wikiFolderLocation: '/test/wiki',
} as Parameters<typeof workspace.get>[0] extends Promise<infer T> ? T : never,
);
mockWiki = {
getTiddlerText: vi.fn((title) => {
if (title === '$:/info/tidgi/workspaceID') return 'test-workspace';
return '';
}),
tiddlerExists: vi.fn(() => false),
addTiddler: vi.fn(),
} as unknown as Wiki;
});
it('should route to sub-wiki when tiddler matches custom filter', async () => {
const subWiki = {
id: 'sub-wiki-filter',
name: 'Sub Wiki Filter',
isSubWiki: true,
mainWikiID: 'test-workspace',
tagNames: ['SomeTag'],
fileSystemPathFilterEnable: true,
fileSystemPathFilter: '[has[customfield]]',
wikiFolderLocation: '/test/wiki/subwiki/filter',
};
vi.mocked(workspace.getWorkspacesAsList).mockResolvedValue([subWiki] as IWikiWorkspace[]);
// Mock filterTiddlers to return the tiddler for custom filter
// @ts-expect-error - TiddlyWiki global
global.$tw.wiki.filterTiddlers = vi.fn((filter) => {
if (filter === '[has[customfield]]') {
return ['FilterMatchTiddler'];
}
return [];
});
adaptor = new FileSystemAdaptor({
wiki: mockWiki,
// @ts-expect-error - TiddlyWiki global
boot: global.$tw.boot,
});
await adaptor['updateSubWikisCache']();
const tiddler: Tiddler = {
fields: { title: 'FilterMatchTiddler', tags: [] },
} as unknown as Tiddler;
await adaptor.getTiddlerFileInfo(tiddler);
// Should use sub-wiki directory because custom filter matched
expect(mockUtils.generateTiddlerFileInfo).toHaveBeenCalledWith(
tiddler,
expect.objectContaining({
directory: '/test/wiki/subwiki/filter',
}),
);
});
it('should not match custom filter when fileSystemPathFilterEnable is disabled', async () => {
const subWiki = {
id: 'sub-wiki-filter-disabled',
name: 'Sub Wiki Filter Disabled',
isSubWiki: true,
mainWikiID: 'test-workspace',
tagNames: ['SomeTag'],
fileSystemPathFilterEnable: false, // Disabled
fileSystemPathFilter: '[has[customfield]]',
wikiFolderLocation: '/test/wiki/subwiki/filter-disabled',
};
vi.mocked(workspace.getWorkspacesAsList).mockResolvedValue([subWiki] as IWikiWorkspace[]);
adaptor = new FileSystemAdaptor({
wiki: mockWiki,
// @ts-expect-error - TiddlyWiki global
boot: global.$tw.boot,
});
await adaptor['updateSubWikisCache']();
const tiddler: Tiddler = {
fields: { title: 'FilterMatchTiddler', tags: [] },
} as unknown as Tiddler;
await adaptor.getTiddlerFileInfo(tiddler);
// Should use default directory because filter is disabled
expect(mockUtils.generateTiddlerFileInfo).toHaveBeenCalledWith(
tiddler,
expect.objectContaining({
directory: '/test/wiki/tiddlers',
}),
);
});
it('should support multiple filter lines (any match wins)', async () => {
const subWiki = {
id: 'sub-wiki-multifilter',
name: 'Sub Wiki MultiFilter',
isSubWiki: true,
mainWikiID: 'test-workspace',
tagNames: [],
fileSystemPathFilterEnable: true,
fileSystemPathFilter: '[has[field1]]\n[has[field2]]',
wikiFolderLocation: '/test/wiki/subwiki/multifilter',
};
vi.mocked(workspace.getWorkspacesAsList).mockResolvedValue([subWiki] as unknown as IWikiWorkspace[]);
// Mock filterTiddlers to return match on second filter
// @ts-expect-error - TiddlyWiki global
global.$tw.wiki.filterTiddlers = vi.fn((filter) => {
if (filter === '[has[field2]]') {
return ['TiddlerWithField2'];
}
return [];
});
adaptor = new FileSystemAdaptor({
wiki: mockWiki,
// @ts-expect-error - TiddlyWiki global
boot: global.$tw.boot,
});
await adaptor['updateSubWikisCache']();
const tiddler: Tiddler = {
fields: { title: 'TiddlerWithField2', tags: [] },
} as unknown as Tiddler;
await adaptor.getTiddlerFileInfo(tiddler);
// Should use sub-wiki directory because second filter line matched
expect(mockUtils.generateTiddlerFileInfo).toHaveBeenCalledWith(
tiddler,
expect.objectContaining({
directory: '/test/wiki/subwiki/multifilter',
}),
);
});
});
describe('getTiddlerFileInfo - Routing Priority', () => {
beforeEach(async () => {
vi.mocked(workspace.get).mockResolvedValue(
{
id: 'test-workspace',
name: 'Test Workspace',
wikiFolderLocation: '/test/wiki',
} as Parameters<typeof workspace.get>[0] extends Promise<infer T> ? T : never,
);
mockWiki = {
getTiddlerText: vi.fn((title) => {
if (title === '$:/info/tidgi/workspaceID') return 'test-workspace';
return '';
}),
tiddlerExists: vi.fn(() => false),
addTiddler: vi.fn(),
} as unknown as Wiki;
});
it('should prioritize direct tag match over tag tree match', async () => {
const subWiki1 = {
id: 'sub-wiki-direct',
name: 'Sub Wiki Direct Tag',
isSubWiki: true,
mainWikiID: 'test-workspace',
order: 0,
tagNames: ['DirectTag'],
includeTagTree: false,
wikiFolderLocation: '/test/wiki/subwiki/direct',
};
const subWiki2 = {
id: 'sub-wiki-tagtree',
name: 'Sub Wiki TagTree',
isSubWiki: true,
mainWikiID: 'test-workspace',
order: 1,
tagNames: ['RootTag'],
includeTagTree: true,
wikiFolderLocation: '/test/wiki/subwiki/tagtree',
};
vi.mocked(workspace.getWorkspacesAsList).mockResolvedValue([subWiki1, subWiki2] as IWikiWorkspace[]);
// Mock tag tree matching to return the tiddler
// @ts-expect-error - TiddlyWiki global
global.$tw.wiki.filterTiddlers = vi.fn(() => ['TestTiddler']);
adaptor = new FileSystemAdaptor({
wiki: mockWiki,
// @ts-expect-error - TiddlyWiki global
boot: global.$tw.boot,
});
await adaptor['updateSubWikisCache']();
// Tiddler has both DirectTag (direct match) and would match RootTag via tag tree
const tiddler: Tiddler = {
fields: { title: 'TestTiddler', tags: ['DirectTag'] },
} as unknown as Tiddler;
await adaptor.getTiddlerFileInfo(tiddler);
// Should use direct tag sub-wiki (first match wins, and direct tag check happens before tag tree)
expect(mockUtils.generateTiddlerFileInfo).toHaveBeenCalledWith(
tiddler,
expect.objectContaining({
directory: '/test/wiki/subwiki/direct',
}),
);
});
it('should prioritize tag match over custom filter match within same workspace', async () => {
const subWiki = {
id: 'sub-wiki-both',
name: 'Sub Wiki Both',
isSubWiki: true,
mainWikiID: 'test-workspace',
tagNames: ['MatchTag'],
fileSystemPathFilterEnable: true,
fileSystemPathFilter: '[has[customfield]]',
wikiFolderLocation: '/test/wiki/subwiki/both',
};
vi.mocked(workspace.getWorkspacesAsList).mockResolvedValue([subWiki] as IWikiWorkspace[]);
// Reset filterTiddlers mock
// @ts-expect-error - TiddlyWiki global
global.$tw.wiki.filterTiddlers = vi.fn(() => []);
adaptor = new FileSystemAdaptor({
wiki: mockWiki,
// @ts-expect-error - TiddlyWiki global
boot: global.$tw.boot,
});
await adaptor['updateSubWikisCache']();
// Tiddler has the matching tag
const tiddler: Tiddler = {
fields: { title: 'TestTiddler', tags: ['MatchTag'] },
} as unknown as Tiddler;
await adaptor.getTiddlerFileInfo(tiddler);
// Should match via tag (filter shouldn't even be checked for this tiddler)
expect(mockUtils.generateTiddlerFileInfo).toHaveBeenCalledWith(
tiddler,
expect.objectContaining({
directory: '/test/wiki/subwiki/both',
}),
);
});
it('should check workspaces in order and use first match', async () => {
const subWiki1 = {
id: 'sub-wiki-first',
name: 'Sub Wiki First',
isSubWiki: true,
mainWikiID: 'test-workspace',
order: 0,
tagNames: ['SharedTag'],
wikiFolderLocation: '/test/wiki/subwiki/first',
};
const subWiki2 = {
id: 'sub-wiki-second',
name: 'Sub Wiki Second',
isSubWiki: true,
mainWikiID: 'test-workspace',
order: 1,
tagNames: ['SharedTag'], // Same tag
wikiFolderLocation: '/test/wiki/subwiki/second',
};
vi.mocked(workspace.getWorkspacesAsList).mockResolvedValue([subWiki1, subWiki2] as IWikiWorkspace[]);
adaptor = new FileSystemAdaptor({
wiki: mockWiki,
// @ts-expect-error - TiddlyWiki global
boot: global.$tw.boot,
});
await adaptor['updateSubWikisCache']();
const tiddler: Tiddler = {
fields: { title: 'TestTiddler', tags: ['SharedTag'] },
} as unknown as Tiddler;
await adaptor.getTiddlerFileInfo(tiddler);
// Should use first sub-wiki (order 0)
expect(mockUtils.generateTiddlerFileInfo).toHaveBeenCalledWith(
tiddler,
expect.objectContaining({
directory: '/test/wiki/subwiki/first',
}),
);
});
});
}); });