feat: first cut at mcp and mcp apps
This commit is contained in:
390
tests/engine/MCPServer.test.ts
Normal file
390
tests/engine/MCPServer.test.ts
Normal file
@@ -0,0 +1,390 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { MCPServer, type MCPServerDependencies } 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 {
|
||||
getAllPosts: vi.fn().mockResolvedValue({ items: [], hasMore: false, total: 0 }),
|
||||
getPost: vi.fn().mockResolvedValue(null),
|
||||
searchPosts: vi.fn().mockResolvedValue([]),
|
||||
createPost: vi.fn().mockResolvedValue({
|
||||
id: 'post-1', title: 'Test', slug: 'test', content: '', status: 'draft',
|
||||
tags: [], categories: [], createdAt: new Date(), updatedAt: new Date(),
|
||||
}),
|
||||
updatePost: vi.fn().mockResolvedValue(null),
|
||||
publishPost: vi.fn().mockResolvedValue(null),
|
||||
deletePost: vi.fn().mockResolvedValue(true),
|
||||
getTagsWithCounts: vi.fn().mockResolvedValue([]),
|
||||
getCategoriesWithCounts: vi.fn().mockResolvedValue([]),
|
||||
getBlogStats: vi.fn().mockResolvedValue({
|
||||
totalPosts: 0, draftCount: 0, publishedCount: 0, archivedCount: 0,
|
||||
oldestPostDate: null, newestPostDate: null, postsPerYear: [], tagCount: 0, categoryCount: 0,
|
||||
}),
|
||||
getLinkedBy: vi.fn().mockResolvedValue([]),
|
||||
getLinksTo: vi.fn().mockResolvedValue([]),
|
||||
getPostsFiltered: vi.fn().mockResolvedValue([]),
|
||||
};
|
||||
}
|
||||
|
||||
function createMockMediaEngine() {
|
||||
return {
|
||||
getAllMedia: vi.fn().mockResolvedValue([]),
|
||||
getMedia: vi.fn().mockResolvedValue(null),
|
||||
updateMedia: vi.fn().mockResolvedValue(null),
|
||||
};
|
||||
}
|
||||
|
||||
function createMockScriptEngine() {
|
||||
return {
|
||||
createScript: 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(),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function createMockTemplateEngine() {
|
||||
return {
|
||||
createTemplate: 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(),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function createMockMetaEngine() {
|
||||
return {
|
||||
getProjectMetadata: vi.fn().mockResolvedValue(null),
|
||||
};
|
||||
}
|
||||
|
||||
function createMockPostMediaEngine() {
|
||||
return {
|
||||
getLinkedMediaDataForPost: vi.fn().mockResolvedValue([]),
|
||||
getLinkedPostsForMedia: vi.fn().mockResolvedValue([]),
|
||||
};
|
||||
}
|
||||
|
||||
function createMockTagEngine() {
|
||||
return {
|
||||
getTagsWithCounts: vi.fn().mockResolvedValue([]),
|
||||
};
|
||||
}
|
||||
|
||||
/** Create stable mock instances that are returned by getter functions */
|
||||
function createDependencies() {
|
||||
const mockPostEngine = createMockPostEngine();
|
||||
const mockMediaEngine = createMockMediaEngine();
|
||||
const mockScriptEngine = createMockScriptEngine();
|
||||
const mockTemplateEngine = createMockTemplateEngine();
|
||||
const mockMetaEngine = createMockMetaEngine();
|
||||
const mockPostMediaEngine = createMockPostMediaEngine();
|
||||
const mockTagEngine = createMockTagEngine();
|
||||
|
||||
const deps: MCPServerDependencies = {
|
||||
getPostEngine: () => mockPostEngine,
|
||||
getMediaEngine: () => mockMediaEngine,
|
||||
getScriptEngine: () => mockScriptEngine,
|
||||
getTemplateEngine: () => mockTemplateEngine,
|
||||
getMetaEngine: () => mockMetaEngine,
|
||||
getPostMediaEngine: () => mockPostMediaEngine,
|
||||
getTagEngine: () => mockTagEngine,
|
||||
};
|
||||
|
||||
return { deps, mockPostEngine, mockMediaEngine, mockScriptEngine, mockTemplateEngine, mockMetaEngine, mockPostMediaEngine, mockTagEngine };
|
||||
}
|
||||
|
||||
/** Helper: check if a key exists in an McpServer internal registry (plain object) */
|
||||
function hasRegistered(mcpServer: unknown, registry: string, name: string): boolean {
|
||||
const obj = (mcpServer as Record<string, Record<string, unknown>>)[registry];
|
||||
return obj != null && name in obj;
|
||||
}
|
||||
|
||||
describe('MCPServer', () => {
|
||||
let server: MCPServer;
|
||||
let deps: MCPServerDependencies;
|
||||
let mockPostEngine: ReturnType<typeof createMockPostEngine>;
|
||||
let mockMediaEngine: ReturnType<typeof createMockMediaEngine>;
|
||||
let mockScriptEngine: ReturnType<typeof createMockScriptEngine>;
|
||||
let mockTemplateEngine: ReturnType<typeof createMockTemplateEngine>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
const mocks = createDependencies();
|
||||
deps = mocks.deps;
|
||||
mockPostEngine = mocks.mockPostEngine;
|
||||
mockMediaEngine = mocks.mockMediaEngine;
|
||||
mockScriptEngine = mocks.mockScriptEngine;
|
||||
mockTemplateEngine = mocks.mockTemplateEngine;
|
||||
server = new MCPServer(deps);
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('creates an MCPServer instance', () => {
|
||||
expect(server).toBeInstanceOf(MCPServer);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createMcpServer', () => {
|
||||
it('creates an McpServer with registered tools', () => {
|
||||
const mcpServer = server.createMcpServer();
|
||||
expect(mcpServer).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('proposal store', () => {
|
||||
it('exposes the proposal store', () => {
|
||||
expect(server.proposalStore).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('registered tools', () => {
|
||||
it('registers search_posts tool', () => {
|
||||
const mcpServer = server.createMcpServer();
|
||||
expect(hasRegistered(mcpServer, '_registeredTools', 'search_posts')).toBe(true);
|
||||
});
|
||||
|
||||
it('registers draft_post tool', () => {
|
||||
const mcpServer = server.createMcpServer();
|
||||
expect(hasRegistered(mcpServer, '_registeredTools', 'draft_post')).toBe(true);
|
||||
});
|
||||
|
||||
it('registers propose_script tool', () => {
|
||||
const mcpServer = server.createMcpServer();
|
||||
expect(hasRegistered(mcpServer, '_registeredTools', 'propose_script')).toBe(true);
|
||||
});
|
||||
|
||||
it('registers propose_template tool', () => {
|
||||
const mcpServer = server.createMcpServer();
|
||||
expect(hasRegistered(mcpServer, '_registeredTools', 'propose_template')).toBe(true);
|
||||
});
|
||||
|
||||
it('registers propose_media_metadata tool', () => {
|
||||
const mcpServer = server.createMcpServer();
|
||||
expect(hasRegistered(mcpServer, '_registeredTools', 'propose_media_metadata')).toBe(true);
|
||||
});
|
||||
|
||||
it('registers propose_post_metadata tool', () => {
|
||||
const mcpServer = server.createMcpServer();
|
||||
expect(hasRegistered(mcpServer, '_registeredTools', 'propose_post_metadata')).toBe(true);
|
||||
});
|
||||
|
||||
it('registers accept_proposal tool', () => {
|
||||
const mcpServer = server.createMcpServer();
|
||||
expect(hasRegistered(mcpServer, '_registeredTools', 'accept_proposal')).toBe(true);
|
||||
});
|
||||
|
||||
it('registers discard_proposal tool', () => {
|
||||
const mcpServer = server.createMcpServer();
|
||||
expect(hasRegistered(mcpServer, '_registeredTools', 'discard_proposal')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('registered resources', () => {
|
||||
it('registers bds://posts resource', () => {
|
||||
const mcpServer = server.createMcpServer();
|
||||
expect(hasRegistered(mcpServer, '_registeredResources', 'bds://posts')).toBe(true);
|
||||
});
|
||||
|
||||
it('registers bds://media resource', () => {
|
||||
const mcpServer = server.createMcpServer();
|
||||
expect(hasRegistered(mcpServer, '_registeredResources', 'bds://media')).toBe(true);
|
||||
});
|
||||
|
||||
it('registers bds://tags resource', () => {
|
||||
const mcpServer = server.createMcpServer();
|
||||
expect(hasRegistered(mcpServer, '_registeredResources', 'bds://tags')).toBe(true);
|
||||
});
|
||||
|
||||
it('registers bds://categories resource', () => {
|
||||
const mcpServer = server.createMcpServer();
|
||||
expect(hasRegistered(mcpServer, '_registeredResources', 'bds://categories')).toBe(true);
|
||||
});
|
||||
|
||||
it('registers bds://stats resource', () => {
|
||||
const mcpServer = server.createMcpServer();
|
||||
expect(hasRegistered(mcpServer, '_registeredResources', 'bds://stats')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('registered resource templates', () => {
|
||||
it('registers post resource template', () => {
|
||||
const mcpServer = server.createMcpServer();
|
||||
expect(hasRegistered(mcpServer, '_registeredResourceTemplates', 'post')).toBe(true);
|
||||
});
|
||||
|
||||
it('registers media-item resource template', () => {
|
||||
const mcpServer = server.createMcpServer();
|
||||
expect(hasRegistered(mcpServer, '_registeredResourceTemplates', 'media-item')).toBe(true);
|
||||
});
|
||||
|
||||
it('registers post-backlinks resource template', () => {
|
||||
const mcpServer = server.createMcpServer();
|
||||
expect(hasRegistered(mcpServer, '_registeredResourceTemplates', 'post-backlinks')).toBe(true);
|
||||
});
|
||||
|
||||
it('registers post-outlinks resource template', () => {
|
||||
const mcpServer = server.createMcpServer();
|
||||
expect(hasRegistered(mcpServer, '_registeredResourceTemplates', 'post-outlinks')).toBe(true);
|
||||
});
|
||||
|
||||
it('registers post-media resource template', () => {
|
||||
const mcpServer = server.createMcpServer();
|
||||
expect(hasRegistered(mcpServer, '_registeredResourceTemplates', 'post-media')).toBe(true);
|
||||
});
|
||||
|
||||
it('registers media-posts resource template', () => {
|
||||
const mcpServer = server.createMcpServer();
|
||||
expect(hasRegistered(mcpServer, '_registeredResourceTemplates', 'media-posts')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('registered prompts', () => {
|
||||
it('registers draft-blog-post prompt', () => {
|
||||
const mcpServer = server.createMcpServer();
|
||||
expect(hasRegistered(mcpServer, '_registeredPrompts', 'draft-blog-post')).toBe(true);
|
||||
});
|
||||
|
||||
it('registers improve-media-metadata prompt', () => {
|
||||
const mcpServer = server.createMcpServer();
|
||||
expect(hasRegistered(mcpServer, '_registeredPrompts', 'improve-media-metadata')).toBe(true);
|
||||
});
|
||||
|
||||
it('registers content-audit prompt', () => {
|
||||
const mcpServer = server.createMcpServer();
|
||||
expect(hasRegistered(mcpServer, '_registeredPrompts', 'content-audit')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('start and stop', () => {
|
||||
it('starts the server and returns a port', async () => {
|
||||
const port = await server.start(0);
|
||||
expect(port).toBeGreaterThan(0);
|
||||
await server.stop();
|
||||
});
|
||||
|
||||
it('stop is idempotent', async () => {
|
||||
await server.start(0);
|
||||
await server.stop();
|
||||
await expect(server.stop()).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanup', () => {
|
||||
it('cleans up proposals and stops the server', async () => {
|
||||
server.proposalStore.create('draftPost', { postId: 'test' });
|
||||
await server.start(0);
|
||||
await server.cleanup();
|
||||
expect(server.proposalStore.getAll()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('accept_proposal', () => {
|
||||
it('accepts a draftPost proposal by publishing', async () => {
|
||||
mockPostEngine.publishPost.mockResolvedValue({
|
||||
id: 'post-1', title: 'Test', slug: 'test', content: '', status: 'published',
|
||||
tags: [], categories: [], createdAt: new Date(), updatedAt: new Date(), publishedAt: new Date(),
|
||||
});
|
||||
|
||||
const proposalId = server.proposalStore.create('draftPost', { postId: 'post-1' });
|
||||
const result = await server.acceptProposal(proposalId);
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockPostEngine.publishPost).toHaveBeenCalledWith('post-1');
|
||||
expect(server.proposalStore.get(proposalId)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('accepts a proposeScript proposal by creating script', async () => {
|
||||
const proposalId = server.proposalStore.create('proposeScript', {
|
||||
title: 'My Script', kind: 'macro', content: 'print("hello")',
|
||||
});
|
||||
const result = await server.acceptProposal(proposalId);
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockScriptEngine.createScript).toHaveBeenCalledWith({
|
||||
title: 'My Script', kind: 'macro', content: 'print("hello")',
|
||||
});
|
||||
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>',
|
||||
});
|
||||
const result = await server.acceptProposal(proposalId);
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockTemplateEngine.createTemplate).toHaveBeenCalledWith({
|
||||
title: 'My Template', kind: 'post', content: '<h1>{{ title }}</h1>',
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts a proposeMediaMetadata proposal by updating media', async () => {
|
||||
mockMediaEngine.updateMedia.mockResolvedValue({ id: 'media-1' });
|
||||
const proposalId = server.proposalStore.create('proposeMediaMetadata', {
|
||||
mediaId: 'media-1', changes: { alt: 'New alt text' },
|
||||
});
|
||||
const result = await server.acceptProposal(proposalId);
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockMediaEngine.updateMedia).toHaveBeenCalledWith('media-1', { alt: 'New alt text' });
|
||||
});
|
||||
|
||||
it('accepts a proposePostMetadata proposal by updating post', async () => {
|
||||
mockPostEngine.updatePost.mockResolvedValue({ id: 'post-1' });
|
||||
const proposalId = server.proposalStore.create('proposePostMetadata', {
|
||||
postId: 'post-1', changes: { title: 'Updated Title' },
|
||||
});
|
||||
const result = await server.acceptProposal(proposalId);
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockPostEngine.updatePost).toHaveBeenCalledWith('post-1', { title: 'Updated Title' });
|
||||
});
|
||||
|
||||
it('returns failure for non-existent proposal', async () => {
|
||||
const result = await server.acceptProposal('non-existent');
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('discard_proposal', () => {
|
||||
it('discards a draftPost proposal by deleting the post', async () => {
|
||||
const proposalId = server.proposalStore.create('draftPost', { postId: 'post-1' });
|
||||
const result = await server.discardProposal(proposalId);
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockPostEngine.deletePost).toHaveBeenCalledWith('post-1');
|
||||
expect(server.proposalStore.get(proposalId)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('discards a proposeScript proposal by removing from store', async () => {
|
||||
const proposalId = server.proposalStore.create('proposeScript', { title: 'Script' });
|
||||
const result = await server.discardProposal(proposalId);
|
||||
expect(result.success).toBe(true);
|
||||
expect(server.proposalStore.get(proposalId)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns failure for non-existent proposal', async () => {
|
||||
const result = await server.discardProposal('non-existent');
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
148
tests/engine/ProposalStore.test.ts
Normal file
148
tests/engine/ProposalStore.test.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import { ProposalStore, type Proposal, type ProposalType } from '../../src/main/engine/ProposalStore';
|
||||
|
||||
describe('ProposalStore', () => {
|
||||
let store: ProposalStore;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
store = new ProposalStore();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
store.destroy();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('creates a proposal and returns an id', () => {
|
||||
const id = store.create('draftPost', { postId: 'post-1' });
|
||||
expect(id).toBeTruthy();
|
||||
expect(typeof id).toBe('string');
|
||||
});
|
||||
|
||||
it('generates unique ids for each proposal', () => {
|
||||
const id1 = store.create('draftPost', { postId: 'post-1' });
|
||||
const id2 = store.create('draftPost', { postId: 'post-2' });
|
||||
expect(id1).not.toBe(id2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('returns a created proposal', () => {
|
||||
const id = store.create('proposeScript', { title: 'My Script', content: 'print("hello")' });
|
||||
const proposal = store.get(id);
|
||||
expect(proposal).toBeDefined();
|
||||
expect(proposal!.type).toBe('proposeScript');
|
||||
expect(proposal!.data).toEqual({ title: 'My Script', content: 'print("hello")' });
|
||||
});
|
||||
|
||||
it('returns undefined for non-existent id', () => {
|
||||
expect(store.get('non-existent')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('removes a proposal', () => {
|
||||
const id = store.create('draftPost', { postId: 'post-1' });
|
||||
expect(store.get(id)).toBeDefined();
|
||||
store.remove(id);
|
||||
expect(store.get(id)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does nothing when removing non-existent id', () => {
|
||||
expect(() => store.remove('non-existent')).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAll', () => {
|
||||
it('returns all proposals', () => {
|
||||
store.create('draftPost', { postId: 'post-1' });
|
||||
store.create('proposeScript', { title: 'Script' });
|
||||
const all = store.getAll();
|
||||
expect(all).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('returns empty array when no proposals exist', () => {
|
||||
expect(store.getAll()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TTL expiry', () => {
|
||||
it('expires proposals after TTL', () => {
|
||||
const id = store.create('draftPost', { postId: 'post-1' });
|
||||
expect(store.get(id)).toBeDefined();
|
||||
|
||||
// Advance past 30-minute TTL
|
||||
vi.advanceTimersByTime(31 * 60 * 1000);
|
||||
store.cleanup();
|
||||
|
||||
expect(store.get(id)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('keeps proposals within TTL', () => {
|
||||
const id = store.create('draftPost', { postId: 'post-1' });
|
||||
|
||||
// Advance less than TTL
|
||||
vi.advanceTimersByTime(15 * 60 * 1000);
|
||||
store.cleanup();
|
||||
|
||||
expect(store.get(id)).toBeDefined();
|
||||
});
|
||||
|
||||
it('supports custom TTL', () => {
|
||||
const shortStore = new ProposalStore(5 * 60 * 1000); // 5 minutes
|
||||
const id = shortStore.create('draftPost', { postId: 'post-1' });
|
||||
|
||||
vi.advanceTimersByTime(6 * 60 * 1000);
|
||||
shortStore.cleanup();
|
||||
|
||||
expect(shortStore.get(id)).toBeUndefined();
|
||||
shortStore.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanup', () => {
|
||||
it('removes only expired proposals', () => {
|
||||
const id1 = store.create('draftPost', { postId: 'post-1' });
|
||||
|
||||
// Advance 20 minutes
|
||||
vi.advanceTimersByTime(20 * 60 * 1000);
|
||||
|
||||
const id2 = store.create('proposeScript', { title: 'Script' });
|
||||
|
||||
// Advance 15 more minutes (id1 = 35 min old = expired, id2 = 15 min old = ok)
|
||||
vi.advanceTimersByTime(15 * 60 * 1000);
|
||||
store.cleanup();
|
||||
|
||||
expect(store.get(id1)).toBeUndefined();
|
||||
expect(store.get(id2)).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroy', () => {
|
||||
it('clears all proposals', () => {
|
||||
store.create('draftPost', { postId: 'post-1' });
|
||||
store.create('proposeScript', { title: 'Script' });
|
||||
store.destroy();
|
||||
expect(store.getAll()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('proposal types', () => {
|
||||
const types: ProposalType[] = [
|
||||
'draftPost',
|
||||
'proposeScript',
|
||||
'proposeTemplate',
|
||||
'proposeMediaMetadata',
|
||||
'proposePostMetadata',
|
||||
];
|
||||
|
||||
it.each(types)('stores and retrieves %s type', (type) => {
|
||||
const id = store.create(type, { test: true });
|
||||
const proposal = store.get(id);
|
||||
expect(proposal).toBeDefined();
|
||||
expect(proposal!.type).toBe(type);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user