feat: first round of mcp standalone server
This commit is contained in:
@@ -8,7 +8,7 @@ const { mockProjectEngine, mockDatabase, mockReadFile } = vi.hoisted(() => ({
|
||||
getDefaultProjectBaseDir: vi.fn().mockResolvedValue('/home/user/bDS/p1'),
|
||||
},
|
||||
mockDatabase: {
|
||||
getDataPaths: vi.fn().mockReturnValue({ database: '/data/bds.db' }),
|
||||
getDbPath: vi.fn(() => '/data/bds.db'),
|
||||
},
|
||||
mockReadFile: vi.fn(),
|
||||
}));
|
||||
@@ -38,8 +38,7 @@ describe('AppApiAdapter', () => {
|
||||
mockProjectEngine.getActiveProject.mockResolvedValue({ id: 'p1', dataPath: '/projects/blog' });
|
||||
mockProjectEngine.getProjectPaths.mockReturnValue({ posts: '/projects/blog/posts', media: '/projects/blog/media' });
|
||||
mockProjectEngine.getDefaultProjectBaseDir.mockResolvedValue('/home/user/bDS/p1');
|
||||
mockDatabase.getDataPaths.mockReturnValue({ database: '/data/bds.db' });
|
||||
adapter = new AppApiAdapter();
|
||||
adapter = new AppApiAdapter(mockProjectEngine as any);
|
||||
});
|
||||
|
||||
it('getDataPaths returns database, posts, and media paths', async () => {
|
||||
|
||||
@@ -154,6 +154,7 @@ describe('BlogGenerationEngine', () => {
|
||||
let tempDir: string;
|
||||
let mockPostEngine: any;
|
||||
let mockMediaEngine: any;
|
||||
let mockPostMediaEngine: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
@@ -165,6 +166,8 @@ describe('BlogGenerationEngine', () => {
|
||||
mockPostEngine = __mockPostEngine;
|
||||
const { __mockMediaEngine } = await import('../../src/main/engine/MediaEngine') as any;
|
||||
mockMediaEngine = __mockMediaEngine;
|
||||
const { __mockPostMediaEngine } = await import('../../src/main/engine/PostMediaEngine') as any;
|
||||
mockPostMediaEngine = __mockPostMediaEngine;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -210,7 +213,7 @@ describe('BlogGenerationEngine', () => {
|
||||
) {
|
||||
setupPosts(posts);
|
||||
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||
const engine = new BlogGenerationEngine();
|
||||
const engine = new BlogGenerationEngine(mockPostEngine, mockMediaEngine, mockPostMediaEngine);
|
||||
const onProgress = vi.fn();
|
||||
return engine.generate({
|
||||
projectId: 'test',
|
||||
@@ -726,7 +729,7 @@ describe('BlogGenerationEngine', () => {
|
||||
});
|
||||
|
||||
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||
const engine = new BlogGenerationEngine();
|
||||
const engine = new BlogGenerationEngine(mockPostEngine, mockMediaEngine, mockPostMediaEngine);
|
||||
const result = await engine.generate({
|
||||
projectId: 'test',
|
||||
projectName: 'Test Blog',
|
||||
@@ -759,7 +762,7 @@ describe('BlogGenerationEngine', () => {
|
||||
setupPosts(posts);
|
||||
|
||||
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||
const engine = new BlogGenerationEngine();
|
||||
const engine = new BlogGenerationEngine(mockPostEngine, mockMediaEngine, mockPostMediaEngine);
|
||||
await engine.generate({
|
||||
projectId: 'test',
|
||||
projectName: 'Test Blog',
|
||||
@@ -789,7 +792,7 @@ describe('BlogGenerationEngine', () => {
|
||||
setupPosts(posts);
|
||||
|
||||
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||
const engine = new BlogGenerationEngine();
|
||||
const engine = new BlogGenerationEngine(mockPostEngine, mockMediaEngine, mockPostMediaEngine);
|
||||
|
||||
await engine.generate({
|
||||
projectId: 'test',
|
||||
@@ -816,7 +819,7 @@ describe('BlogGenerationEngine', () => {
|
||||
setupPosts(posts);
|
||||
|
||||
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||
const engine = new BlogGenerationEngine();
|
||||
const engine = new BlogGenerationEngine(mockPostEngine, mockMediaEngine, mockPostMediaEngine);
|
||||
const onProgress = vi.fn();
|
||||
|
||||
await engine.generate({
|
||||
@@ -848,7 +851,7 @@ describe('BlogGenerationEngine', () => {
|
||||
setupPosts(posts);
|
||||
|
||||
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||
const engine = new BlogGenerationEngine();
|
||||
const engine = new BlogGenerationEngine(mockPostEngine, mockMediaEngine, mockPostMediaEngine);
|
||||
const onProgress = vi.fn();
|
||||
|
||||
await engine.generate({
|
||||
@@ -878,7 +881,7 @@ describe('BlogGenerationEngine', () => {
|
||||
const canonicalPathSpy = vi.spyOn(pageRendererModule, 'buildCanonicalPostPath');
|
||||
|
||||
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||
const engine = new BlogGenerationEngine();
|
||||
const engine = new BlogGenerationEngine(mockPostEngine, mockMediaEngine, mockPostMediaEngine);
|
||||
|
||||
await engine.generate({
|
||||
projectId: 'test',
|
||||
@@ -902,7 +905,7 @@ describe('BlogGenerationEngine', () => {
|
||||
setupPosts(posts);
|
||||
|
||||
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||
const engine = new BlogGenerationEngine();
|
||||
const engine = new BlogGenerationEngine(mockPostEngine, mockMediaEngine, mockPostMediaEngine);
|
||||
|
||||
await engine.generate({
|
||||
projectId: 'test',
|
||||
@@ -933,7 +936,7 @@ describe('BlogGenerationEngine', () => {
|
||||
|
||||
const filterSpy = vi.spyOn(Array.prototype, 'filter');
|
||||
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||
const engine = new BlogGenerationEngine();
|
||||
const engine = new BlogGenerationEngine(mockPostEngine, mockMediaEngine, mockPostMediaEngine);
|
||||
|
||||
await engine.generate({
|
||||
projectId: 'test',
|
||||
@@ -975,7 +978,7 @@ describe('BlogGenerationEngine', () => {
|
||||
await writeFile(path.join(tempDir, 'html', 'stale', 'index.html'), '<html>stale</html>', 'utf-8');
|
||||
|
||||
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||
const engine = new BlogGenerationEngine();
|
||||
const engine = new BlogGenerationEngine(mockPostEngine, mockMediaEngine, mockPostMediaEngine);
|
||||
|
||||
const report = await engine.validateSite({
|
||||
projectId: 'test',
|
||||
@@ -1006,7 +1009,7 @@ describe('BlogGenerationEngine', () => {
|
||||
setupPosts([post]);
|
||||
|
||||
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||
const engine = new BlogGenerationEngine();
|
||||
const engine = new BlogGenerationEngine(mockPostEngine, mockMediaEngine, mockPostMediaEngine);
|
||||
|
||||
await engine.generate({
|
||||
projectId: 'test',
|
||||
@@ -1049,7 +1052,7 @@ describe('BlogGenerationEngine', () => {
|
||||
setupPosts([post]);
|
||||
|
||||
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||
const engine = new BlogGenerationEngine();
|
||||
const engine = new BlogGenerationEngine(mockPostEngine, mockMediaEngine, mockPostMediaEngine);
|
||||
|
||||
await engine.generate({
|
||||
projectId: 'test',
|
||||
@@ -1112,7 +1115,7 @@ describe('BlogGenerationEngine', () => {
|
||||
await writeFile(path.join(tempDir, 'html', 'obsolete', 'deep', 'index.html'), '<html>obsolete</html>', 'utf-8');
|
||||
|
||||
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||
const engine = new BlogGenerationEngine();
|
||||
const engine = new BlogGenerationEngine(mockPostEngine, mockMediaEngine, mockPostMediaEngine);
|
||||
|
||||
const report = await engine.validateSite({
|
||||
projectId: 'test',
|
||||
@@ -1158,7 +1161,7 @@ describe('BlogGenerationEngine', () => {
|
||||
setupPosts(posts);
|
||||
|
||||
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||
const engine = new BlogGenerationEngine();
|
||||
const engine = new BlogGenerationEngine(mockPostEngine, mockMediaEngine, mockPostMediaEngine);
|
||||
|
||||
await engine.generate({
|
||||
projectId: 'test',
|
||||
@@ -1237,7 +1240,7 @@ describe('BlogGenerationEngine', () => {
|
||||
await writeFile(path.join(tempDir, 'html', 'stale', 'index.html'), '<html>stale</html>', 'utf-8');
|
||||
|
||||
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||
const engine = new BlogGenerationEngine();
|
||||
const engine = new BlogGenerationEngine(mockPostEngine, mockMediaEngine, mockPostMediaEngine);
|
||||
|
||||
const generateSpy = vi.spyOn(engine, 'generate');
|
||||
|
||||
@@ -1273,7 +1276,7 @@ describe('BlogGenerationEngine', () => {
|
||||
setupPosts(posts);
|
||||
|
||||
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||
const engine = new BlogGenerationEngine();
|
||||
const engine = new BlogGenerationEngine(mockPostEngine, mockMediaEngine, mockPostMediaEngine);
|
||||
|
||||
await engine.applyValidation({
|
||||
projectId: 'test',
|
||||
@@ -1303,7 +1306,7 @@ describe('BlogGenerationEngine', () => {
|
||||
setupPosts(posts);
|
||||
|
||||
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||
const engine = new BlogGenerationEngine();
|
||||
const engine = new BlogGenerationEngine(mockPostEngine, mockMediaEngine, mockPostMediaEngine);
|
||||
|
||||
await engine.applyValidation({
|
||||
projectId: 'test',
|
||||
@@ -1335,7 +1338,7 @@ describe('BlogGenerationEngine', () => {
|
||||
setupPosts(posts);
|
||||
|
||||
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||
const engine = new BlogGenerationEngine();
|
||||
const engine = new BlogGenerationEngine(mockPostEngine, mockMediaEngine, mockPostMediaEngine);
|
||||
|
||||
await engine.applyValidation({
|
||||
projectId: 'test',
|
||||
@@ -1371,7 +1374,7 @@ describe('BlogGenerationEngine', () => {
|
||||
await writeFile(path.join(tempDir, 'html', 'index.html'), '<html><body>stale-root</body></html>', 'utf-8');
|
||||
|
||||
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||
const engine = new BlogGenerationEngine();
|
||||
const engine = new BlogGenerationEngine(mockPostEngine, mockMediaEngine, mockPostMediaEngine);
|
||||
|
||||
await engine.applyValidation({
|
||||
projectId: 'test',
|
||||
@@ -1401,7 +1404,7 @@ describe('BlogGenerationEngine', () => {
|
||||
|
||||
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||
const { PageRenderer } = await import('../../src/main/engine/PageRenderer');
|
||||
const engine = new BlogGenerationEngine();
|
||||
const engine = new BlogGenerationEngine(mockPostEngine, mockMediaEngine, mockPostMediaEngine);
|
||||
|
||||
const renderPostListSpy = vi.spyOn(PageRenderer.prototype, 'renderPostList');
|
||||
|
||||
@@ -1454,7 +1457,7 @@ describe('BlogGenerationEngine', () => {
|
||||
setupPosts(posts);
|
||||
|
||||
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||
const engine = new BlogGenerationEngine();
|
||||
const engine = new BlogGenerationEngine(mockPostEngine, mockMediaEngine, mockPostMediaEngine);
|
||||
|
||||
await engine.applyValidation({
|
||||
projectId: 'test',
|
||||
@@ -1497,7 +1500,7 @@ describe('BlogGenerationEngine', () => {
|
||||
|
||||
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||
const { PageRenderer } = await import('../../src/main/engine/PageRenderer');
|
||||
const engine = new BlogGenerationEngine();
|
||||
const engine = new BlogGenerationEngine(mockPostEngine, mockMediaEngine, mockPostMediaEngine);
|
||||
|
||||
const renderPostListSpy = vi.spyOn(PageRenderer.prototype, 'renderPostList');
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ describe('GitApiAdapter', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
adapter = new GitApiAdapter();
|
||||
adapter = new GitApiAdapter(mockGitEngine as any, mockProjectEngine as any);
|
||||
});
|
||||
|
||||
it('checkAvailability delegates directly (no projectPath)', async () => {
|
||||
|
||||
@@ -233,7 +233,12 @@ describe('ImportExecutionEngine E2E Tests', () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Create engine instance
|
||||
engine = new ImportExecutionEngine();
|
||||
engine = new ImportExecutionEngine({
|
||||
tagEngine: mockTagEngine as any,
|
||||
postEngine: mockPostEngine as any,
|
||||
mediaEngine: mockMediaEngine as any,
|
||||
postMediaEngine: mockPostMediaEngine as any,
|
||||
});
|
||||
engine.setProjectContext('test-project', '/mock/test/data');
|
||||
|
||||
// Parse the WXR content (mocked readFile will return our pre-loaded content)
|
||||
|
||||
@@ -108,6 +108,17 @@ const mockMediaEngine = {
|
||||
updateMedia: vi.fn().mockResolvedValue({}),
|
||||
};
|
||||
|
||||
// Mock the PostMediaEngine
|
||||
const mockPostMediaEngine = {
|
||||
setProjectContext: vi.fn(),
|
||||
linkMediaToPost: vi.fn().mockResolvedValue(undefined),
|
||||
getLinkedMediaDataForPost: vi.fn().mockResolvedValue([]),
|
||||
};
|
||||
|
||||
vi.mock('../../src/main/engine/PostMediaEngine', () => ({
|
||||
PostMediaEngine: vi.fn(() => mockPostMediaEngine),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/main/engine/MediaEngine', () => ({
|
||||
getMediaEngine: vi.fn(() => mockMediaEngine),
|
||||
}));
|
||||
@@ -275,7 +286,12 @@ describe('ImportExecutionEngine', () => {
|
||||
insertedPosts.length = 0;
|
||||
insertedMedia.length = 0;
|
||||
updatedPosts.length = 0;
|
||||
engine = new ImportExecutionEngine();
|
||||
engine = new ImportExecutionEngine({
|
||||
tagEngine: mockTagEngine as any,
|
||||
postEngine: mockPostEngine as any,
|
||||
mediaEngine: mockMediaEngine as any,
|
||||
postMediaEngine: mockPostMediaEngine as any,
|
||||
});
|
||||
engine.setProjectContext('test-project', '/mock/project/data');
|
||||
});
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('MCPAgentConfigEngine', () => {
|
||||
let engine: MCPAgentConfigEngine;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.resetAllMocks();
|
||||
engine = new MCPAgentConfigEngine({
|
||||
homeDir: '/home/testuser',
|
||||
platform: 'darwin',
|
||||
@@ -40,9 +40,10 @@ describe('MCPAgentConfigEngine', () => {
|
||||
describe('getAgents', () => {
|
||||
it('returns all supported agent definitions', () => {
|
||||
const agents = engine.getAgents();
|
||||
expect(agents).toHaveLength(4);
|
||||
expect(agents).toHaveLength(5);
|
||||
const ids = agents.map((a) => a.id);
|
||||
expect(ids).toContain('claude-code');
|
||||
expect(ids).toContain('claude-desktop');
|
||||
expect(ids).toContain('github-copilot');
|
||||
expect(ids).toContain('gemini-cli');
|
||||
expect(ids).toContain('opencode');
|
||||
@@ -336,4 +337,160 @@ describe('MCPAgentConfigEngine', () => {
|
||||
expect(written.mcpServers.bDS.url).toBe('http://127.0.0.1:9999/mcp');
|
||||
});
|
||||
});
|
||||
|
||||
describe('claude-desktop', () => {
|
||||
let desktopEngine: MCPAgentConfigEngine;
|
||||
|
||||
beforeEach(() => {
|
||||
desktopEngine = new MCPAgentConfigEngine({
|
||||
homeDir: '/home/testuser',
|
||||
platform: 'darwin',
|
||||
mcpUrl: 'http://127.0.0.1:4124/mcp',
|
||||
execPath: '/Applications/Blogging Desktop Server.app/Contents/MacOS/Blogging Desktop Server',
|
||||
scriptPath: '/Applications/Blogging Desktop Server.app/Contents/Resources/bds-mcp.cjs',
|
||||
});
|
||||
});
|
||||
|
||||
it('includes claude-desktop in getAgents()', () => {
|
||||
const agents = desktopEngine.getAgents();
|
||||
expect(agents.map((a) => a.id)).toContain('claude-desktop');
|
||||
});
|
||||
|
||||
it('returns correct config path for claude-desktop on macOS', () => {
|
||||
expect(desktopEngine.getConfigPath('claude-desktop')).toBe(
|
||||
'/home/testuser/Library/Application Support/Claude/claude_desktop_config.json',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns correct config path for claude-desktop on Windows', () => {
|
||||
const winEngine = new MCPAgentConfigEngine({
|
||||
homeDir: 'C:\\Users\\testuser',
|
||||
platform: 'win32',
|
||||
mcpUrl: 'http://127.0.0.1:4124/mcp',
|
||||
execPath: 'C:\\path\\to\\app.exe',
|
||||
scriptPath: 'C:\\path\\to\\bds-mcp.cjs',
|
||||
});
|
||||
const configPath = winEngine.getConfigPath('claude-desktop');
|
||||
// On Windows path.join uses backslashes; on macOS it uses forward slashes
|
||||
// so normalise for cross-platform CI
|
||||
const normalised = configPath.replace(/[\\/]/g, '/');
|
||||
expect(normalised).toBe(
|
||||
'C:/Users/testuser/AppData/Roaming/Claude/claude_desktop_config.json',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns correct config path for claude-desktop on Linux', () => {
|
||||
const linuxEngine = new MCPAgentConfigEngine({
|
||||
homeDir: '/home/user',
|
||||
platform: 'linux',
|
||||
mcpUrl: 'http://127.0.0.1:4124/mcp',
|
||||
});
|
||||
expect(linuxEngine.getConfigPath('claude-desktop')).toBe(
|
||||
'/home/user/.config/Claude/claude_desktop_config.json',
|
||||
);
|
||||
});
|
||||
|
||||
it('adds stdio entry with command/args/env to claude_desktop_config.json', () => {
|
||||
mockExistsSync.mockReturnValue(false);
|
||||
|
||||
const result = desktopEngine.addToConfig('claude-desktop');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const written = JSON.parse(mockWriteFileSync.mock.calls[0]![1] as string);
|
||||
expect(written.mcpServers.bDS).toEqual({
|
||||
command: '/Applications/Blogging Desktop Server.app/Contents/MacOS/Blogging Desktop Server',
|
||||
args: ['/Applications/Blogging Desktop Server.app/Contents/Resources/bds-mcp.cjs'],
|
||||
env: { ELECTRON_RUN_AS_NODE: '1' },
|
||||
});
|
||||
});
|
||||
|
||||
it('throws descriptive error if execPath/scriptPath missing for claude-desktop', () => {
|
||||
const noPathEngine = new MCPAgentConfigEngine({
|
||||
homeDir: '/home/testuser',
|
||||
platform: 'darwin',
|
||||
mcpUrl: 'http://127.0.0.1:4124/mcp',
|
||||
});
|
||||
mockExistsSync.mockReturnValue(false);
|
||||
|
||||
const result = noPathEngine.addToConfig('claude-desktop');
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('execPath');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeFromConfig', () => {
|
||||
it('removes bDS entry from config and returns success', () => {
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReadFileSync.mockReturnValue(
|
||||
JSON.stringify({
|
||||
mcpServers: {
|
||||
bDS: { type: 'http', url: 'http://127.0.0.1:4124/mcp' },
|
||||
other: { type: 'http', url: 'http://other' },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const result = engine.removeFromConfig('claude-code');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const written = JSON.parse(mockWriteFileSync.mock.calls[0]![1] as string);
|
||||
expect(written.mcpServers.bDS).toBeUndefined();
|
||||
expect(written.mcpServers.other).toBeDefined();
|
||||
});
|
||||
|
||||
it('removes the mcpServers key entirely when bDS was the only entry', () => {
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReadFileSync.mockReturnValue(
|
||||
JSON.stringify({
|
||||
mcpServers: { bDS: { type: 'http', url: 'http://127.0.0.1:4124/mcp' } },
|
||||
}),
|
||||
);
|
||||
|
||||
engine.removeFromConfig('claude-code');
|
||||
|
||||
const written = JSON.parse(mockWriteFileSync.mock.calls[0]![1] as string);
|
||||
expect(written.mcpServers).toBeUndefined();
|
||||
});
|
||||
|
||||
it('no-ops gracefully when file does not exist', () => {
|
||||
mockExistsSync.mockReturnValue(false);
|
||||
|
||||
const result = engine.removeFromConfig('claude-code');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockWriteFileSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('no-ops gracefully when bDS entry is not in config', () => {
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReadFileSync.mockReturnValue(JSON.stringify({ mcpServers: { other: {} } }));
|
||||
|
||||
const result = engine.removeFromConfig('claude-code');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockWriteFileSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses the servers key for github-copilot', () => {
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReadFileSync.mockReturnValue(
|
||||
JSON.stringify({ servers: { bDS: { type: 'http', url: 'x' } } }),
|
||||
);
|
||||
|
||||
const result = engine.removeFromConfig('github-copilot');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const written = JSON.parse(mockWriteFileSync.mock.calls[0]![1] as string);
|
||||
expect(written.servers).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns success with configPath', () => {
|
||||
mockExistsSync.mockReturnValue(false);
|
||||
|
||||
const result = engine.removeFromConfig('claude-code');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.configPath).toBe('/home/testuser/.claude.json');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,22 +60,34 @@ function createMockMediaEngine() {
|
||||
|
||||
function createMockScriptEngine() {
|
||||
return {
|
||||
createScript: vi.fn().mockResolvedValue({
|
||||
createDraftScript: vi.fn().mockResolvedValue({
|
||||
id: 'script-1', title: 'Test', slug: 'test', kind: 'macro',
|
||||
entrypoint: 'main.py', content: '', enabled: true, version: 1,
|
||||
filePath: '/test', createdAt: new Date(), updatedAt: new Date(),
|
||||
}),
|
||||
publishScript: vi.fn().mockResolvedValue({
|
||||
id: 'script-1', title: 'Test', slug: 'test', kind: 'macro',
|
||||
entrypoint: 'main.py', content: '', enabled: true, version: 1,
|
||||
filePath: '/test', createdAt: new Date(), updatedAt: new Date(),
|
||||
}),
|
||||
deleteDraftScript: vi.fn().mockResolvedValue(true),
|
||||
validateScript: vi.fn().mockResolvedValue({ valid: true, errors: [] }),
|
||||
};
|
||||
}
|
||||
|
||||
function createMockTemplateEngine() {
|
||||
return {
|
||||
createTemplate: vi.fn().mockResolvedValue({
|
||||
createDraftTemplate: vi.fn().mockResolvedValue({
|
||||
id: 'tpl-1', title: 'Test', slug: 'test', kind: 'post',
|
||||
enabled: true, version: 1, filePath: '/test', content: '',
|
||||
createdAt: new Date(), updatedAt: new Date(),
|
||||
}),
|
||||
publishTemplate: vi.fn().mockResolvedValue({
|
||||
id: 'tpl-1', title: 'Test', slug: 'test', kind: 'post',
|
||||
enabled: true, version: 1, filePath: '/test', content: '',
|
||||
createdAt: new Date(), updatedAt: new Date(),
|
||||
}),
|
||||
deleteDraftTemplate: vi.fn().mockResolvedValue(true),
|
||||
validateTemplate: vi.fn().mockResolvedValue({ valid: true, errors: [] }),
|
||||
};
|
||||
}
|
||||
@@ -110,13 +122,13 @@ function createDependencies() {
|
||||
const mockTagEngine = createMockTagEngine();
|
||||
|
||||
const deps: MCPServerDependencies = {
|
||||
getPostEngine: () => mockPostEngine,
|
||||
getMediaEngine: () => mockMediaEngine,
|
||||
getScriptEngine: () => mockScriptEngine,
|
||||
getTemplateEngine: () => mockTemplateEngine,
|
||||
getMetaEngine: () => mockMetaEngine,
|
||||
getPostMediaEngine: () => mockPostMediaEngine,
|
||||
getTagEngine: () => mockTagEngine,
|
||||
postEngine: mockPostEngine,
|
||||
mediaEngine: mockMediaEngine,
|
||||
scriptEngine: mockScriptEngine,
|
||||
templateEngine: mockTemplateEngine,
|
||||
metaEngine: mockMetaEngine,
|
||||
postMediaEngine: mockPostMediaEngine,
|
||||
tagEngine: mockTagEngine,
|
||||
};
|
||||
|
||||
return { deps, mockPostEngine, mockMediaEngine, mockScriptEngine, mockTemplateEngine, mockMetaEngine, mockPostMediaEngine, mockTagEngine };
|
||||
@@ -358,25 +370,21 @@ describe('MCPServer', () => {
|
||||
|
||||
it('accepts a proposeScript proposal by creating script', async () => {
|
||||
const proposalId = server.proposalStore.create('proposeScript', {
|
||||
title: 'My Script', kind: 'macro', content: 'print("hello")',
|
||||
scriptId: 'script-1',
|
||||
});
|
||||
const result = await server.acceptProposal(proposalId);
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockScriptEngine.createScript).toHaveBeenCalledWith({
|
||||
title: 'My Script', kind: 'macro', content: 'print("hello")',
|
||||
});
|
||||
expect(mockScriptEngine.publishScript).toHaveBeenCalledWith('script-1');
|
||||
expect(server.proposalStore.get(proposalId)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('accepts a proposeTemplate proposal by creating template', async () => {
|
||||
const proposalId = server.proposalStore.create('proposeTemplate', {
|
||||
title: 'My Template', kind: 'post', content: '<h1>{{ title }}</h1>',
|
||||
templateId: 'tpl-1',
|
||||
});
|
||||
const result = await server.acceptProposal(proposalId);
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockTemplateEngine.createTemplate).toHaveBeenCalledWith({
|
||||
title: 'My Template', kind: 'post', content: '<h1>{{ title }}</h1>',
|
||||
});
|
||||
expect(mockTemplateEngine.publishTemplate).toHaveBeenCalledWith('tpl-1');
|
||||
});
|
||||
|
||||
it('accepts a proposeMediaMetadata proposal by updating media', async () => {
|
||||
@@ -415,9 +423,10 @@ describe('MCPServer', () => {
|
||||
});
|
||||
|
||||
it('discards a proposeScript proposal by removing from store', async () => {
|
||||
const proposalId = server.proposalStore.create('proposeScript', { title: 'Script' });
|
||||
const proposalId = server.proposalStore.create('proposeScript', { scriptId: 'script-1' });
|
||||
const result = await server.discardProposal(proposalId);
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockScriptEngine.deleteDraftScript).toHaveBeenCalledWith('script-1');
|
||||
expect(server.proposalStore.get(proposalId)).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -830,7 +839,10 @@ describe('MCPServer', () => {
|
||||
const proposal = server.proposalStore.get(parsed.proposalId);
|
||||
expect(proposal).toBeDefined();
|
||||
expect(proposal!.type).toBe('proposeScript');
|
||||
expect(proposal!.data.content).toBe('print("hi")');
|
||||
expect(mockScriptEngine.createDraftScript).toHaveBeenCalledWith({
|
||||
title: 'My Script', kind: 'macro', content: 'print("hi")', entrypoint: undefined,
|
||||
});
|
||||
expect(proposal!.data.scriptId).toBe('script-1');
|
||||
});
|
||||
|
||||
it('propose_script calls validateScript and includes validation result in preview', async () => {
|
||||
|
||||
@@ -173,7 +173,7 @@ describe('MetadataDiffEngine', () => {
|
||||
mockAllPostsRows = [];
|
||||
mockSyncPublishedPostFile.mockClear();
|
||||
resetMockCounters();
|
||||
engine = new MetadataDiffEngine();
|
||||
engine = new MetadataDiffEngine({ syncPublishedPostFile: mockSyncPublishedPostFile } as any);
|
||||
engine.setProjectContext('test-project');
|
||||
});
|
||||
|
||||
|
||||
248
tests/engine/NotificationWatcher.test.ts
Normal file
248
tests/engine/NotificationWatcher.test.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach, type MockedFunction } from 'vitest';
|
||||
|
||||
// ── Chokidar mock ────────────────────────────────────────────────────────────
|
||||
|
||||
interface MockFSWatcher {
|
||||
on: MockedFunction<(event: string, handler: () => void) => MockFSWatcher>;
|
||||
close: MockedFunction<() => Promise<void>>;
|
||||
}
|
||||
|
||||
let mockWatcher: MockFSWatcher;
|
||||
let capturedWatchPaths: string[] = [];
|
||||
let capturedWatchOptions: Record<string, unknown> = {};
|
||||
|
||||
vi.mock('chokidar', () => ({
|
||||
default: {
|
||||
watch: (paths: string[], options: Record<string, unknown>) => {
|
||||
capturedWatchPaths = paths;
|
||||
capturedWatchOptions = options;
|
||||
return mockWatcher;
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// ── Imports (after mocks) ────────────────────────────────────────────────────
|
||||
|
||||
import {
|
||||
NotificationWatcher,
|
||||
type WatchableEngines,
|
||||
} from '../../src/main/engine/NotificationWatcher';
|
||||
|
||||
// ── DB mock helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
type MockDb = {
|
||||
select: MockedFunction<() => {
|
||||
from: (table: unknown) => { where: MockedFunction<() => Promise<unknown[]>> };
|
||||
}>;
|
||||
update: MockedFunction<(table: unknown) => {
|
||||
set: (values: unknown) => { where: MockedFunction<() => Promise<void>> };
|
||||
}>;
|
||||
delete: MockedFunction<(table: unknown) => { where: MockedFunction<() => Promise<void>> }>;
|
||||
};
|
||||
|
||||
function makeSelectChain(rows: unknown[]): ReturnType<MockDb['select']> {
|
||||
const whereSelect = vi.fn().mockResolvedValue(rows);
|
||||
return {
|
||||
from: (_table: unknown) => ({ where: whereSelect }),
|
||||
};
|
||||
}
|
||||
|
||||
function makeUpdateChain(): ReturnType<MockDb['update']> {
|
||||
const whereUpdate = vi.fn().mockResolvedValue(undefined);
|
||||
return {
|
||||
set: (_values: unknown) => ({ where: whereUpdate }),
|
||||
};
|
||||
}
|
||||
|
||||
function makeDeleteChain(): ReturnType<MockDb['delete']> {
|
||||
return { where: vi.fn().mockResolvedValue(undefined) };
|
||||
}
|
||||
|
||||
// ── Test suite ───────────────────────────────────────────────────────────────
|
||||
|
||||
describe('NotificationWatcher', () => {
|
||||
const DB_PATH = '/home/user/.config/bDS/bds.db';
|
||||
|
||||
let db: MockDb;
|
||||
let engines: WatchableEngines;
|
||||
let mockSend: MockedFunction<(channel: string, payload: unknown) => void>;
|
||||
let mainWindow: { webContents: { send: typeof mockSend } };
|
||||
let watcher: NotificationWatcher;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
mockWatcher = {
|
||||
on: vi.fn().mockReturnThis(),
|
||||
close: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
capturedWatchPaths = [];
|
||||
capturedWatchOptions = {};
|
||||
|
||||
db = {
|
||||
select: vi.fn().mockReturnValue(makeSelectChain([])),
|
||||
update: vi.fn().mockReturnValue(makeUpdateChain()),
|
||||
delete: vi.fn().mockReturnValue(makeDeleteChain()),
|
||||
};
|
||||
|
||||
engines = {
|
||||
post: { invalidate: vi.fn() },
|
||||
media: { invalidate: vi.fn() },
|
||||
script: { invalidate: vi.fn() },
|
||||
template: { invalidate: vi.fn() },
|
||||
};
|
||||
|
||||
mockSend = vi.fn();
|
||||
mainWindow = { webContents: { send: mockSend } };
|
||||
|
||||
watcher = new NotificationWatcher(DB_PATH, db as any, engines, mainWindow as any, 100);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
// ── start() ───────────────────────────────────────────────────────────────
|
||||
|
||||
describe('start()', () => {
|
||||
it('watches both db and wal paths', () => {
|
||||
watcher.start();
|
||||
expect(capturedWatchPaths).toEqual([DB_PATH, `${DB_PATH}-wal`]);
|
||||
});
|
||||
|
||||
it('sets persistent:false and ignoreInitial:true', () => {
|
||||
watcher.start();
|
||||
expect(capturedWatchOptions.persistent).toBe(false);
|
||||
expect(capturedWatchOptions.ignoreInitial).toBe(true);
|
||||
});
|
||||
|
||||
it('registers change and add handlers', () => {
|
||||
watcher.start();
|
||||
const events = mockWatcher.on.mock.calls.map((c) => c[0]);
|
||||
expect(events).toContain('change');
|
||||
expect(events).toContain('add');
|
||||
});
|
||||
|
||||
it('debounces rapid file-change events', async () => {
|
||||
watcher.start();
|
||||
const changeHandler = mockWatcher.on.mock.calls.find((c) => c[0] === 'change')![1];
|
||||
|
||||
db.select.mockReturnValue(makeSelectChain([]));
|
||||
|
||||
changeHandler();
|
||||
changeHandler();
|
||||
changeHandler();
|
||||
|
||||
expect(db.select).not.toHaveBeenCalled();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
// Only one process() call despite three change events
|
||||
expect(db.select).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ── process() ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('process()', () => {
|
||||
async function triggerProcess(rows: unknown[] = []): Promise<void> {
|
||||
db.select.mockReturnValue(makeSelectChain(rows));
|
||||
watcher.start();
|
||||
const changeHandler = mockWatcher.on.mock.calls.find((c) => c[0] === 'change')![1];
|
||||
changeHandler();
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
}
|
||||
|
||||
it('queries db_notifications for unprocessed CLI rows', async () => {
|
||||
await triggerProcess([]);
|
||||
expect(db.select).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls invalidate on the matching engine for each row', async () => {
|
||||
const rows = [
|
||||
{ id: 1, entity: 'post', entityId: 'p1', action: 'created', fromCli: 1, seenAt: null, createdAt: Date.now() },
|
||||
{ id: 2, entity: 'media', entityId: 'm1', action: 'updated', fromCli: 1, seenAt: null, createdAt: Date.now() },
|
||||
];
|
||||
await triggerProcess(rows);
|
||||
|
||||
expect(engines.post.invalidate).toHaveBeenCalledWith('p1');
|
||||
expect(engines.media.invalidate).toHaveBeenCalledWith('m1');
|
||||
});
|
||||
|
||||
it('sends entity:changed IPC event for each row', async () => {
|
||||
const rows = [
|
||||
{ id: 1, entity: 'post', entityId: 'p1', action: 'created', fromCli: 1, seenAt: null, createdAt: Date.now() },
|
||||
{ id: 2, entity: 'script', entityId: 's1', action: 'deleted', fromCli: 1, seenAt: null, createdAt: Date.now() },
|
||||
];
|
||||
await triggerProcess(rows);
|
||||
|
||||
expect(mockSend).toHaveBeenCalledWith('entity:changed', {
|
||||
entity: 'post',
|
||||
entityId: 'p1',
|
||||
action: 'created',
|
||||
});
|
||||
expect(mockSend).toHaveBeenCalledWith('entity:changed', {
|
||||
entity: 'script',
|
||||
entityId: 's1',
|
||||
action: 'deleted',
|
||||
});
|
||||
});
|
||||
|
||||
it('stamps seenAt on each processed row', async () => {
|
||||
const rows = [
|
||||
{ id: 42, entity: 'post', entityId: 'p1', action: 'updated', fromCli: 1, seenAt: null, createdAt: Date.now() },
|
||||
];
|
||||
db.select.mockReturnValue(makeSelectChain(rows));
|
||||
const updateChain = makeUpdateChain();
|
||||
db.update.mockReturnValue(updateChain);
|
||||
|
||||
watcher.start();
|
||||
const changeHandler = mockWatcher.on.mock.calls.find((c) => c[0] === 'change')![1];
|
||||
changeHandler();
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
expect(db.update).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('prunes old seen rows (>1h) and old unprocessed rows (>24h)', async () => {
|
||||
await triggerProcess([]);
|
||||
// delete is called twice: once for seenAt > 1h, once for unprocessed > 24h
|
||||
expect(db.delete).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('skips unknown entity types gracefully', async () => {
|
||||
const rows = [
|
||||
{ id: 1, entity: 'unknown_entity', entityId: 'x1', action: 'created', fromCli: 1, seenAt: null, createdAt: Date.now() },
|
||||
];
|
||||
await expect(triggerProcess(rows)).resolves.not.toThrow();
|
||||
// No IPC send for unknown entities, but the watcher finishes without error
|
||||
});
|
||||
});
|
||||
|
||||
// ── stop() ────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('stop()', () => {
|
||||
it('closes the file watcher', () => {
|
||||
watcher.start();
|
||||
watcher.stop();
|
||||
expect(mockWatcher.close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('cancels a pending debounce timer', async () => {
|
||||
watcher.start();
|
||||
const changeHandler = mockWatcher.on.mock.calls.find((c) => c[0] === 'change')![1];
|
||||
changeHandler();
|
||||
|
||||
watcher.stop();
|
||||
await vi.advanceTimersByTimeAsync(200);
|
||||
|
||||
// process() must NOT have run after stop
|
||||
expect(db.select).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not throw if called before start()', () => {
|
||||
expect(() => watcher.stop()).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -31,6 +31,14 @@ const mockUpdateMedia = vi.fn();
|
||||
const mockGetAllMedia = vi.fn();
|
||||
const mockImportMedia = vi.fn();
|
||||
|
||||
// Aggregated mock MediaEngine object for constructor injection
|
||||
const mockMediaEngineForPostMedia = {
|
||||
getMedia: mockGetMedia,
|
||||
updateMedia: mockUpdateMedia,
|
||||
getAllMedia: mockGetAllMedia,
|
||||
importMedia: mockImportMedia,
|
||||
};
|
||||
|
||||
// Mock MediaEngine
|
||||
vi.mock('../../src/main/engine/MediaEngine', () => ({
|
||||
getMediaEngine: vi.fn(() => ({
|
||||
@@ -144,7 +152,7 @@ describe('PostMediaEngine', () => {
|
||||
mockGetAllMedia.mockResolvedValue([]);
|
||||
mockImportMedia.mockResolvedValue({ id: 'imported-media-id' });
|
||||
|
||||
engine = new PostMediaEngine();
|
||||
engine = new PostMediaEngine(mockMediaEngineForPostMedia as any);
|
||||
engine.setProjectContext('test-project');
|
||||
});
|
||||
|
||||
|
||||
@@ -159,6 +159,9 @@ describe('PreviewServer', () => {
|
||||
server = new PreviewServer({
|
||||
postEngine: makeEngine(posts),
|
||||
settingsEngine: makeSettings(50),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -200,6 +203,8 @@ describe('PreviewServer', () => {
|
||||
},
|
||||
],
|
||||
}),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -242,6 +247,8 @@ describe('PreviewServer', () => {
|
||||
{ id: 'dev', title: 'Dev', kind: 'category-archive', categoryName: 'news', children: [] },
|
||||
],
|
||||
}),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -278,6 +285,8 @@ describe('PreviewServer', () => {
|
||||
{ id: 'news', title: 'news', kind: 'category-archive', categoryName: 'news', children: [] },
|
||||
],
|
||||
}),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -293,6 +302,9 @@ describe('PreviewServer', () => {
|
||||
server = new PreviewServer({
|
||||
postEngine: makeEngine([makePost()]),
|
||||
settingsEngine: makeSettings(50),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -369,6 +381,9 @@ describe('PreviewServer', () => {
|
||||
server = new PreviewServer({
|
||||
postEngine: makeEngine([makePost()]),
|
||||
settingsEngine: makeSettings(50),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default', dataDir: tempDir ?? undefined }),
|
||||
});
|
||||
|
||||
@@ -397,6 +412,9 @@ describe('PreviewServer', () => {
|
||||
server = new PreviewServer({
|
||||
postEngine: makeEngine([postWithCode]),
|
||||
settingsEngine: makeSettings(50),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -432,6 +450,7 @@ describe('PreviewServer', () => {
|
||||
postMediaEngine,
|
||||
settingsEngine: settingsEngine as any,
|
||||
menuEngine,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default', dataDir: '/tmp/default' }),
|
||||
});
|
||||
|
||||
@@ -474,6 +493,9 @@ describe('PreviewServer', () => {
|
||||
server = new PreviewServer({
|
||||
postEngine: makeEngine(posts),
|
||||
settingsEngine: makeSettings(50),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -520,6 +542,9 @@ describe('PreviewServer', () => {
|
||||
server = new PreviewServer({
|
||||
postEngine: makeEngine(posts),
|
||||
settingsEngine: makeSettings(50),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -557,6 +582,9 @@ describe('PreviewServer', () => {
|
||||
server = new PreviewServer({
|
||||
postEngine: makeEngine([publishedPost, draftPost]),
|
||||
settingsEngine: makeSettings(50),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -589,6 +617,9 @@ describe('PreviewServer', () => {
|
||||
};
|
||||
},
|
||||
} as any,
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -618,6 +649,9 @@ describe('PreviewServer', () => {
|
||||
};
|
||||
},
|
||||
} as any,
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -645,6 +679,9 @@ describe('PreviewServer', () => {
|
||||
server = new PreviewServer({
|
||||
postEngine: makeEngine(posts),
|
||||
settingsEngine: makeSettings(50),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -665,6 +702,9 @@ describe('PreviewServer', () => {
|
||||
server = new PreviewServer({
|
||||
postEngine: makeEngine([matchingDay, sameMonth, sameYear, differentYear]),
|
||||
settingsEngine: makeSettings(50),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -696,6 +736,9 @@ describe('PreviewServer', () => {
|
||||
server = new PreviewServer({
|
||||
postEngine: makeEngine(posts),
|
||||
settingsEngine: makeSettings(50),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -728,6 +771,9 @@ describe('PreviewServer', () => {
|
||||
server = new PreviewServer({
|
||||
postEngine: makeEngine([post]),
|
||||
settingsEngine: makeSettings(50),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -753,6 +799,9 @@ describe('PreviewServer', () => {
|
||||
server = new PreviewServer({
|
||||
postEngine: makeEngine([post]),
|
||||
settingsEngine: makeSettings(50),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -766,6 +815,9 @@ describe('PreviewServer', () => {
|
||||
server = new PreviewServer({
|
||||
postEngine: makeEngine([makePost({ content: '```js\nconst line = "x".repeat(1000);\n```' })]),
|
||||
settingsEngine: makeSettings(50),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -789,6 +841,9 @@ describe('PreviewServer', () => {
|
||||
server = new PreviewServer({
|
||||
postEngine: makeEngine([post]),
|
||||
settingsEngine: makeSettings(50),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -829,6 +884,9 @@ describe('PreviewServer', () => {
|
||||
server = new PreviewServer({
|
||||
postEngine: makeEngine([post]),
|
||||
settingsEngine: makeSettings(50),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default', dataDir: tempDir || undefined }),
|
||||
});
|
||||
|
||||
@@ -904,6 +962,9 @@ describe('PreviewServer', () => {
|
||||
};
|
||||
},
|
||||
},
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -933,6 +994,9 @@ describe('PreviewServer', () => {
|
||||
};
|
||||
},
|
||||
},
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -959,6 +1023,9 @@ describe('PreviewServer', () => {
|
||||
};
|
||||
},
|
||||
},
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -988,6 +1055,9 @@ describe('PreviewServer', () => {
|
||||
};
|
||||
},
|
||||
},
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -1015,6 +1085,9 @@ describe('PreviewServer', () => {
|
||||
return { description: 'Beschreibung', maxPostsPerPage: 2 };
|
||||
},
|
||||
},
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -1043,6 +1116,9 @@ describe('PreviewServer', () => {
|
||||
return { description: 'Beschreibung', maxPostsPerPage: 2 };
|
||||
},
|
||||
},
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -1075,6 +1151,9 @@ describe('PreviewServer', () => {
|
||||
};
|
||||
},
|
||||
} as any,
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -1111,6 +1190,9 @@ describe('PreviewServer', () => {
|
||||
};
|
||||
},
|
||||
} as any,
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -1147,6 +1229,9 @@ describe('PreviewServer', () => {
|
||||
};
|
||||
},
|
||||
} as any,
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -1166,6 +1251,9 @@ describe('PreviewServer', () => {
|
||||
server = new PreviewServer({
|
||||
postEngine: makeEngine([tagged, categorized, page, regular]),
|
||||
settingsEngine: makeSettings(50),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -1215,6 +1303,9 @@ describe('PreviewServer', () => {
|
||||
server = new PreviewServer({
|
||||
postEngine: makeEngine([tagDayOneA, tagDayOneB, tagDayTwo]),
|
||||
settingsEngine: makeSettings(50),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -1247,6 +1338,9 @@ describe('PreviewServer', () => {
|
||||
server = new PreviewServer({
|
||||
postEngine: makeEngine(posts),
|
||||
settingsEngine: makeSettings(50),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -1302,6 +1396,9 @@ describe('PreviewServer', () => {
|
||||
server = new PreviewServer({
|
||||
postEngine: makeEngine(posts),
|
||||
settingsEngine: makeSettings(7),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -1326,6 +1423,9 @@ describe('PreviewServer', () => {
|
||||
};
|
||||
},
|
||||
},
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -1353,6 +1453,9 @@ describe('PreviewServer', () => {
|
||||
};
|
||||
},
|
||||
},
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -1381,6 +1484,9 @@ describe('PreviewServer', () => {
|
||||
: null;
|
||||
},
|
||||
} as any,
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -1401,6 +1507,9 @@ describe('PreviewServer', () => {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({
|
||||
projectId: 'default',
|
||||
dataDir: '/tmp/default',
|
||||
@@ -1465,7 +1574,9 @@ describe('PreviewServer', () => {
|
||||
createdAt: new Date('2025-02-03T10:00:00.000Z'),
|
||||
},
|
||||
]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
settingsEngine: makeSettings(50),
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -1495,6 +1606,9 @@ describe('PreviewServer', () => {
|
||||
server = new PreviewServer({
|
||||
postEngine: makeEngine([post]),
|
||||
settingsEngine: makeSettings(50),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -1546,7 +1660,11 @@ describe('PreviewServer', () => {
|
||||
linkedPostIds: [],
|
||||
} as any,
|
||||
]) as any,
|
||||
postMediaEngine: makePostMediaEngine({
|
||||
'macro-1': [{ media: { id: 'media-1' } }, { media: { id: 'media-2' } }],
|
||||
}) as any,
|
||||
settingsEngine: makeSettings(50),
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -1587,6 +1705,9 @@ describe('PreviewServer', () => {
|
||||
server = new PreviewServer({
|
||||
postEngine: makeEngine([post]),
|
||||
settingsEngine: makeSettings(50),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -1624,6 +1745,7 @@ describe('PreviewServer', () => {
|
||||
'macro-junction-1': [{ media: { id: 'junction-media-1' } }],
|
||||
}) as any,
|
||||
settingsEngine: makeSettings(50),
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
} as any);
|
||||
|
||||
@@ -1646,6 +1768,9 @@ describe('PreviewServer', () => {
|
||||
server = new PreviewServer({
|
||||
postEngine: makeEngine([makePost()]),
|
||||
settingsEngine: makeSettings(50),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({
|
||||
projectId: 'default',
|
||||
dataDir: tempDir!,
|
||||
@@ -1691,6 +1816,9 @@ describe('PreviewServer', () => {
|
||||
server = new PreviewServer({
|
||||
postEngine: engine,
|
||||
settingsEngine: makeSettings(50),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -1738,6 +1866,9 @@ describe('PreviewServer', () => {
|
||||
server = new PreviewServer({
|
||||
postEngine: engine,
|
||||
settingsEngine: makeSettings(50),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -1790,6 +1921,9 @@ describe('PreviewServer', () => {
|
||||
server = new PreviewServer({
|
||||
postEngine: engine,
|
||||
settingsEngine: makeSettings(50),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
@@ -1805,6 +1939,9 @@ describe('PreviewServer', () => {
|
||||
server = new PreviewServer({
|
||||
postEngine: makeEngine([makePost()]),
|
||||
settingsEngine: makeSettings(50),
|
||||
mediaEngine: makeMediaEngine([]) as any,
|
||||
postMediaEngine: makePostMediaEngine({}) as any,
|
||||
menuEngine: makeMenuEngine({ items: [] }) as any,
|
||||
getActiveProjectContext: async () => ({ projectId: 'default' }),
|
||||
});
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('PublishApiAdapter', () => {
|
||||
mockTaskManager.runTask.mockImplementation((opts: { execute: (onProgress: () => void) => Promise<unknown> }) => {
|
||||
return opts.execute(() => {});
|
||||
});
|
||||
adapter = new PublishApiAdapter();
|
||||
adapter = new PublishApiAdapter(mockProjectEngine as any, mockPublishEngine as any, mockTaskManager as any);
|
||||
});
|
||||
|
||||
it('sets project context before uploading', async () => {
|
||||
|
||||
@@ -99,13 +99,6 @@ describe('PublishEngine', () => {
|
||||
});
|
||||
|
||||
describe('constructor and project context', () => {
|
||||
it('should be instantiated via getPublishEngine singleton', async () => {
|
||||
const { getPublishEngine } = await import('../../src/main/engine/PublishEngine');
|
||||
const e1 = getPublishEngine();
|
||||
const e2 = getPublishEngine();
|
||||
expect(e1).toBe(e2);
|
||||
});
|
||||
|
||||
it('should throw if no project context is set', async () => {
|
||||
const noContextEngine = new PublishEngine();
|
||||
await expect(
|
||||
|
||||
@@ -157,7 +157,7 @@ describe('TagEngine', () => {
|
||||
mockSelectDataDefault = [];
|
||||
mockPostEngine.syncPublishedPostFile.mockClear();
|
||||
resetMockCounters();
|
||||
tagEngine = new TagEngine();
|
||||
tagEngine = new TagEngine(mockPostEngine as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -351,7 +351,12 @@ describe('WXR Reference Comparison E2E Tests', () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Create engine instances
|
||||
executionEngine = new ImportExecutionEngine();
|
||||
executionEngine = new ImportExecutionEngine({
|
||||
tagEngine: mockTagEngine as any,
|
||||
postEngine: mockPostEngine as any,
|
||||
mediaEngine: mockMediaEngine as any,
|
||||
postMediaEngine: mockPostMediaEngine as any,
|
||||
});
|
||||
executionEngine.setProjectContext('test-project', '/mock/test/data');
|
||||
|
||||
analysisEngine = new ImportAnalysisEngine();
|
||||
|
||||
Reference in New Issue
Block a user