chore: added missing tests

This commit is contained in:
2026-02-28 22:37:07 +01:00
parent 1dc2994b08
commit ca258ea14d
5 changed files with 403 additions and 22 deletions

View File

@@ -0,0 +1,71 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { NoopNotifier, DbNotifier, type NotifyEntity, type NotifyAction } from '../../src/main/engine/CliNotifier';
import { dbNotifications } from '../../src/main/database/schema';
describe('NoopNotifier', () => {
it('resolves without side effects', async () => {
const notifier = new NoopNotifier();
await expect(notifier.notify('post', 'p-1', 'created')).resolves.toBeUndefined();
});
it('accepts every valid entity and action combination', async () => {
const notifier = new NoopNotifier();
const entities: NotifyEntity[] = ['post', 'media', 'script', 'template'];
const actions: NotifyAction[] = ['created', 'updated', 'deleted'];
for (const entity of entities) {
for (const action of actions) {
await expect(notifier.notify(entity, 'id-1', action)).resolves.toBeUndefined();
}
}
});
});
describe('DbNotifier', () => {
let valuesFn: ReturnType<typeof vi.fn>;
let insertFn: ReturnType<typeof vi.fn>;
let mockDb: { insert: ReturnType<typeof vi.fn> };
beforeEach(() => {
valuesFn = vi.fn().mockResolvedValue(undefined);
insertFn = vi.fn().mockReturnValue({ values: valuesFn });
mockDb = { insert: insertFn };
});
it('inserts a notification row into db_notifications', async () => {
const notifier = new DbNotifier(mockDb);
const beforeMs = Date.now();
await notifier.notify('post', 'post-42', 'created');
expect(insertFn).toHaveBeenCalledWith(dbNotifications);
expect(valuesFn).toHaveBeenCalledTimes(1);
const row = valuesFn.mock.calls[0][0];
expect(row.entity).toBe('post');
expect(row.entityId).toBe('post-42');
expect(row.action).toBe('created');
expect(row.fromCli).toBe(1);
expect(row.seenAt).toBeNull();
expect(row.createdAt).toBeGreaterThanOrEqual(beforeMs);
expect(row.createdAt).toBeLessThanOrEqual(Date.now());
});
it('inserts distinct rows for consecutive calls', async () => {
const notifier = new DbNotifier(mockDb);
await notifier.notify('media', 'm-1', 'updated');
await notifier.notify('script', 's-1', 'deleted');
expect(valuesFn).toHaveBeenCalledTimes(2);
expect(valuesFn.mock.calls[0][0].entity).toBe('media');
expect(valuesFn.mock.calls[1][0].entity).toBe('script');
});
it('propagates database insertion errors', async () => {
valuesFn.mockRejectedValueOnce(new Error('SQLITE_BUSY'));
const notifier = new DbNotifier(mockDb);
await expect(notifier.notify('template', 't-1', 'created')).rejects.toThrow('SQLITE_BUSY');
});
});

View File

@@ -1,28 +1,7 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { MCPServer, type MCPServerDependencies, DEFAULT_PAGE_SIZE, encodeCursor, decodeCursor } from '../../src/main/engine/MCPServer';
// Mock all engine singletons
vi.mock('../../src/main/engine/PostEngine', () => ({
getPostEngine: vi.fn(),
}));
vi.mock('../../src/main/engine/MediaEngine', () => ({
getMediaEngine: vi.fn(),
}));
vi.mock('../../src/main/engine/ScriptEngine', () => ({
getScriptEngine: vi.fn(),
}));
vi.mock('../../src/main/engine/TemplateEngine', () => ({
getTemplateEngine: vi.fn(),
}));
vi.mock('../../src/main/engine/MetaEngine', () => ({
getMetaEngine: vi.fn(),
}));
vi.mock('../../src/main/engine/PostMediaEngine', () => ({
getPostMediaEngine: vi.fn(),
}));
vi.mock('../../src/main/engine/TagEngine', () => ({
getTagEngine: vi.fn(),
}));
function createMockPostEngine() {
return {

View File

@@ -445,4 +445,142 @@ describe('ScriptEngine', () => {
expect(endFn).toHaveBeenCalled();
});
});
describe('draft lifecycle', () => {
it('createDraftScript inserts a row with status draft and content in DB', async () => {
const created = await scriptEngine.createDraftScript({
title: 'Draft Macro',
kind: 'macro',
content: 'def render(ctx): return {"html": "<p>draft</p>"}',
});
expect(created.status).toBe('draft');
expect(created.content).toBe('def render(ctx): return {"html": "<p>draft</p>"}');
expect(created.slug).toBe('draft_macro');
expect(mockScripts.has(created.id)).toBe(true);
const row = mockScripts.get(created.id);
expect(row.status).toBe('draft');
expect(row.content).toBe('def render(ctx): return {"html": "<p>draft</p>"}');
});
it('createDraftScript does not write a file to disk', async () => {
const fsModule = await import('fs/promises');
vi.mocked(fsModule.writeFile).mockClear();
await scriptEngine.createDraftScript({
title: 'No File Draft',
kind: 'utility',
content: 'print("no file")',
});
expect(fsModule.writeFile).not.toHaveBeenCalled();
});
it('createDraftScript generates a unique slug', async () => {
await scriptEngine.createDraftScript({
title: 'Unique Slug',
kind: 'macro',
content: 'pass',
});
vi.mocked((await import('uuid')).v4).mockReturnValueOnce('mock-script-id-2');
const second = await scriptEngine.createDraftScript({
title: 'Unique Slug',
kind: 'macro',
content: 'pass',
});
expect(second.slug).toBe('unique_slug_2');
});
it('publishScript writes file and sets status to published', async () => {
const draft = await scriptEngine.createDraftScript({
title: 'Publish Me',
kind: 'macro',
content: 'def render(ctx): return {"html": "<p>publish</p>"}',
});
const published = await scriptEngine.publishScript(draft.id);
expect(published).not.toBeNull();
expect(published!.status).toBe('published');
const row = mockScripts.get(draft.id);
expect(row.status).toBe('published');
expect(row.content).toBeNull();
const fileContent = mockFiles.get(draft.filePath);
expect(fileContent).toBeDefined();
expect(fileContent).toContain('title: "Publish Me"');
expect(fileContent).toContain('def render');
});
it('publishScript returns null for non-existent script', async () => {
const result = await scriptEngine.publishScript('nonexistent-id');
expect(result).toBeNull();
});
it('publishScript calls notifier with updated action', async () => {
const mockNotifier = { notify: vi.fn().mockResolvedValue(undefined) };
const notifiedEngine = new ScriptEngine(mockNotifier);
notifiedEngine.setProjectContext('default', '/mock/userData/projects/default');
const draft = await notifiedEngine.createDraftScript({
title: 'Notified Publish',
kind: 'macro',
content: 'def render(ctx): pass',
});
await notifiedEngine.publishScript(draft.id);
expect(mockNotifier.notify).toHaveBeenCalledWith('script', draft.id, 'updated');
});
it('deleteDraftScript removes a draft row from the database', async () => {
const draft = await scriptEngine.createDraftScript({
title: 'Delete Draft',
kind: 'utility',
content: 'pass',
});
const deleted = await scriptEngine.deleteDraftScript(draft.id);
expect(deleted).toBe(true);
expect(mockScripts.size).toBe(0);
});
it('deleteDraftScript returns false for non-existent script', async () => {
const result = await scriptEngine.deleteDraftScript('no-such-id');
expect(result).toBe(false);
});
it('deleteDraftScript returns false for published scripts', async () => {
const created = await scriptEngine.createScript({
title: 'Published Script',
kind: 'macro',
content: 'def render(ctx): pass',
});
const result = await scriptEngine.deleteDraftScript(created.id);
expect(result).toBe(false);
});
it('deleteDraftScript calls notifier with deleted action', async () => {
const mockNotifier = { notify: vi.fn().mockResolvedValue(undefined) };
const notifiedEngine = new ScriptEngine(mockNotifier);
notifiedEngine.setProjectContext('default', '/mock/userData/projects/default');
const draft = await notifiedEngine.createDraftScript({
title: 'Notified Delete',
kind: 'utility',
content: 'pass',
});
await notifiedEngine.deleteDraftScript(draft.id);
expect(mockNotifier.notify).toHaveBeenCalledWith('script', draft.id, 'deleted');
});
});
});

View File

@@ -517,4 +517,142 @@ describe('TemplateEngine', () => {
expect(row.title).toBe(originalTitle);
});
});
describe('draft lifecycle', () => {
it('createDraftTemplate inserts a row with status draft and content in DB', async () => {
const created = await templateEngine.createDraftTemplate({
title: 'Draft Post Layout',
kind: 'post',
content: '<main>{{ post.title }}</main>',
});
expect(created.status).toBe('draft');
expect(created.content).toBe('<main>{{ post.title }}</main>');
expect(created.slug).toBe('draft_post_layout');
expect(mockTemplates.has(created.id)).toBe(true);
const row = mockTemplates.get(created.id);
expect(row.status).toBe('draft');
expect(row.content).toBe('<main>{{ post.title }}</main>');
});
it('createDraftTemplate does not write a file to disk', async () => {
const fsModule = await import('fs/promises');
vi.mocked(fsModule.writeFile).mockClear();
await templateEngine.createDraftTemplate({
title: 'No File Draft',
kind: 'list',
content: '<main>{{ day_blocks }}</main>',
});
expect(fsModule.writeFile).not.toHaveBeenCalled();
});
it('createDraftTemplate generates a unique slug', async () => {
await templateEngine.createDraftTemplate({
title: 'Unique Slug',
kind: 'post',
content: '<main>first</main>',
});
vi.mocked((await import('uuid')).v4).mockReturnValueOnce('mock-template-id-2');
const second = await templateEngine.createDraftTemplate({
title: 'Unique Slug',
kind: 'post',
content: '<main>second</main>',
});
expect(second.slug).toBe('unique_slug_2');
});
it('publishTemplate writes file and sets status to published', async () => {
const draft = await templateEngine.createDraftTemplate({
title: 'Publish Me',
kind: 'post',
content: '<main>{{ post.content }}</main>',
});
const published = await templateEngine.publishTemplate(draft.id);
expect(published).not.toBeNull();
expect(published!.status).toBe('published');
const row = mockTemplates.get(draft.id);
expect(row.status).toBe('published');
expect(row.content).toBeNull();
const fileContent = mockFiles.get(draft.filePath);
expect(fileContent).toBeDefined();
expect(fileContent).toContain('title: "Publish Me"');
expect(fileContent).toContain('{{ post.content }}');
});
it('publishTemplate returns null for non-existent template', async () => {
const result = await templateEngine.publishTemplate('nonexistent-id');
expect(result).toBeNull();
});
it('publishTemplate calls notifier with updated action', async () => {
const mockNotifier = { notify: vi.fn().mockResolvedValue(undefined) };
const notifiedEngine = new TemplateEngine(mockNotifier);
notifiedEngine.setProjectContext('default', '/mock/userData/projects/default');
const draft = await notifiedEngine.createDraftTemplate({
title: 'Notified Publish',
kind: 'post',
content: '<main>notify</main>',
});
await notifiedEngine.publishTemplate(draft.id);
expect(mockNotifier.notify).toHaveBeenCalledWith('template', draft.id, 'updated');
});
it('deleteDraftTemplate removes a draft row from the database', async () => {
const draft = await templateEngine.createDraftTemplate({
title: 'Delete Draft',
kind: 'partial',
content: '<footer>draft</footer>',
});
const deleted = await templateEngine.deleteDraftTemplate(draft.id);
expect(deleted).toBe(true);
expect(mockTemplates.size).toBe(0);
});
it('deleteDraftTemplate returns false for non-existent template', async () => {
const result = await templateEngine.deleteDraftTemplate('no-such-id');
expect(result).toBe(false);
});
it('deleteDraftTemplate returns false for published templates', async () => {
const created = await templateEngine.createTemplate({
title: 'Published Template',
kind: 'post',
content: '<main>published</main>',
});
const result = await templateEngine.deleteDraftTemplate(created.id);
expect(result).toBe(false);
});
it('deleteDraftTemplate calls notifier with deleted action', async () => {
const mockNotifier = { notify: vi.fn().mockResolvedValue(undefined) };
const notifiedEngine = new TemplateEngine(mockNotifier);
notifiedEngine.setProjectContext('default', '/mock/userData/projects/default');
const draft = await notifiedEngine.createDraftTemplate({
title: 'Notified Delete',
kind: 'partial',
content: '<footer>notify</footer>',
});
await notifiedEngine.deleteDraftTemplate(draft.id);
expect(mockNotifier.notify).toHaveBeenCalledWith('template', draft.id, 'deleted');
});
});
});