feat: more work on mcp server integration

This commit is contained in:
2026-02-28 12:36:13 +01:00
parent e71e478776
commit 6c22e69805
36 changed files with 1420 additions and 635 deletions

View File

@@ -86,7 +86,7 @@ describe('GenerationRouteRendererFactory', () => {
}
if (typeof filter.month === 'number') {
filtered = filtered.filter((post) => post.createdAt.getMonth() === filter.month);
filtered = filtered.filter((post) => post.createdAt.getMonth() === filter.month - 1);
}
if (filter.startDate) {

View File

@@ -0,0 +1,339 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { MCPAgentConfigEngine, type MCPAgentId, type AgentConfigResult } from '../../src/main/engine/MCPAgentConfigEngine';
// Mock fs and os
const mockReadFileSync = vi.fn();
const mockWriteFileSync = vi.fn();
const mockExistsSync = vi.fn();
const mockMkdirSync = vi.fn();
vi.mock('fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('fs')>();
return {
...actual,
default: {
...actual,
readFileSync: (...args: unknown[]) => mockReadFileSync(...args),
writeFileSync: (...args: unknown[]) => mockWriteFileSync(...args),
existsSync: (...args: unknown[]) => mockExistsSync(...args),
mkdirSync: (...args: unknown[]) => mockMkdirSync(...args),
},
readFileSync: (...args: unknown[]) => mockReadFileSync(...args),
writeFileSync: (...args: unknown[]) => mockWriteFileSync(...args),
existsSync: (...args: unknown[]) => mockExistsSync(...args),
mkdirSync: (...args: unknown[]) => mockMkdirSync(...args),
};
});
describe('MCPAgentConfigEngine', () => {
let engine: MCPAgentConfigEngine;
beforeEach(() => {
vi.clearAllMocks();
engine = new MCPAgentConfigEngine({
homeDir: '/home/testuser',
platform: 'darwin',
mcpUrl: 'http://127.0.0.1:4124/mcp',
});
});
describe('getAgents', () => {
it('returns all supported agent definitions', () => {
const agents = engine.getAgents();
expect(agents).toHaveLength(4);
const ids = agents.map((a) => a.id);
expect(ids).toContain('claude-code');
expect(ids).toContain('github-copilot');
expect(ids).toContain('gemini-cli');
expect(ids).toContain('opencode');
});
it('includes display labels for each agent', () => {
const agents = engine.getAgents();
for (const agent of agents) {
expect(agent.label).toBeTruthy();
expect(typeof agent.label).toBe('string');
}
});
});
describe('getConfigPath', () => {
it('returns ~/.claude.json for claude-code', () => {
expect(engine.getConfigPath('claude-code')).toBe('/home/testuser/.claude.json');
});
it('returns macOS VS Code user mcp.json for github-copilot on darwin', () => {
expect(engine.getConfigPath('github-copilot')).toBe(
'/home/testuser/Library/Application Support/Code/User/mcp.json',
);
});
it('returns Linux VS Code user mcp.json for github-copilot on linux', () => {
const linuxEngine = new MCPAgentConfigEngine({
homeDir: '/home/user',
platform: 'linux',
mcpUrl: 'http://127.0.0.1:4124/mcp',
});
expect(linuxEngine.getConfigPath('github-copilot')).toBe(
'/home/user/.config/Code/User/mcp.json',
);
});
it('returns ~/.gemini/settings.json for gemini-cli', () => {
expect(engine.getConfigPath('gemini-cli')).toBe('/home/testuser/.gemini/settings.json');
});
it('returns ~/.opencode.json for opencode', () => {
expect(engine.getConfigPath('opencode')).toBe('/home/testuser/.opencode.json');
});
});
describe('addToConfig (claude-code)', () => {
it('creates new config when file does not exist', () => {
mockExistsSync.mockReturnValue(false);
const result = engine.addToConfig('claude-code');
expect(result.success).toBe(true);
expect(result.configPath).toBe('/home/testuser/.claude.json');
expect(mockWriteFileSync).toHaveBeenCalledOnce();
const written = JSON.parse(mockWriteFileSync.mock.calls[0]![1] as string);
expect(written.mcpServers.bDS).toEqual({ type: 'http', url: 'http://127.0.0.1:4124/mcp' });
});
it('merges into existing config without overwriting other servers', () => {
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(
JSON.stringify({
mcpServers: { other: { type: 'stdio', command: 'npx' } },
someOtherKey: 'keep',
}),
);
const result = engine.addToConfig('claude-code');
expect(result.success).toBe(true);
const written = JSON.parse(mockWriteFileSync.mock.calls[0]![1] as string);
expect(written.mcpServers.other).toEqual({ type: 'stdio', command: 'npx' });
expect(written.mcpServers.bDS).toEqual({ type: 'http', url: 'http://127.0.0.1:4124/mcp' });
expect(written.someOtherKey).toBe('keep');
});
it('overwrites existing bDS entry to update URL', () => {
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(
JSON.stringify({
mcpServers: { bDS: { type: 'http', url: 'http://old:1234/mcp' } },
}),
);
const result = engine.addToConfig('claude-code');
expect(result.success).toBe(true);
const written = JSON.parse(mockWriteFileSync.mock.calls[0]![1] as string);
expect(written.mcpServers.bDS.url).toBe('http://127.0.0.1:4124/mcp');
});
});
describe('addToConfig (github-copilot)', () => {
it('creates new .vscode/mcp.json with correct servers key', () => {
mockExistsSync.mockReturnValue(false);
const result = engine.addToConfig('github-copilot');
expect(result.success).toBe(true);
const written = JSON.parse(mockWriteFileSync.mock.calls[0]![1] as string);
expect(written.servers.bDS).toEqual({ type: 'http', url: 'http://127.0.0.1:4124/mcp' });
// Should NOT have mcpServers key
expect(written.mcpServers).toBeUndefined();
});
it('creates parent directory if needed', () => {
mockExistsSync.mockReturnValue(false);
engine.addToConfig('github-copilot');
expect(mockMkdirSync).toHaveBeenCalledWith(
expect.stringContaining('Code/User'),
{ recursive: true },
);
});
it('merges into existing VS Code config preserving other servers', () => {
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(
JSON.stringify({
servers: { github: { type: 'http', url: 'https://api.githubcopilot.com/mcp' } },
}),
);
const result = engine.addToConfig('github-copilot');
expect(result.success).toBe(true);
const written = JSON.parse(mockWriteFileSync.mock.calls[0]![1] as string);
expect(written.servers.github.url).toBe('https://api.githubcopilot.com/mcp');
expect(written.servers.bDS.url).toBe('http://127.0.0.1:4124/mcp');
});
});
describe('addToConfig (gemini-cli)', () => {
it('creates new settings.json with httpUrl entry', () => {
mockExistsSync.mockReturnValue(false);
const result = engine.addToConfig('gemini-cli');
expect(result.success).toBe(true);
const written = JSON.parse(mockWriteFileSync.mock.calls[0]![1] as string);
expect(written.mcpServers.bDS).toEqual({ httpUrl: 'http://127.0.0.1:4124/mcp' });
});
it('creates ~/.gemini directory if needed', () => {
mockExistsSync.mockReturnValue(false);
engine.addToConfig('gemini-cli');
expect(mockMkdirSync).toHaveBeenCalledWith(
expect.stringContaining('.gemini'),
{ recursive: true },
);
});
it('preserves existing settings when adding MCP server', () => {
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(
JSON.stringify({
theme: 'dark',
mcpServers: { existing: { command: 'python' } },
}),
);
const result = engine.addToConfig('gemini-cli');
expect(result.success).toBe(true);
const written = JSON.parse(mockWriteFileSync.mock.calls[0]![1] as string);
expect(written.theme).toBe('dark');
expect(written.mcpServers.existing).toEqual({ command: 'python' });
expect(written.mcpServers.bDS).toEqual({ httpUrl: 'http://127.0.0.1:4124/mcp' });
});
});
describe('addToConfig (opencode)', () => {
it('creates new .opencode.json with sse type', () => {
mockExistsSync.mockReturnValue(false);
const result = engine.addToConfig('opencode');
expect(result.success).toBe(true);
const written = JSON.parse(mockWriteFileSync.mock.calls[0]![1] as string);
expect(written.mcpServers.bDS).toEqual({ type: 'sse', url: 'http://127.0.0.1:4124/mcp' });
});
it('merges into existing opencode config', () => {
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(
JSON.stringify({
providers: { openai: { apiKey: 'key' } },
mcpServers: { debugger: { type: 'stdio', command: 'debug' } },
}),
);
const result = engine.addToConfig('opencode');
expect(result.success).toBe(true);
const written = JSON.parse(mockWriteFileSync.mock.calls[0]![1] as string);
expect(written.providers.openai.apiKey).toBe('key');
expect(written.mcpServers.debugger.command).toBe('debug');
expect(written.mcpServers.bDS.type).toBe('sse');
});
});
describe('error handling', () => {
it('returns error result when read fails', () => {
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockImplementation(() => {
throw new Error('Permission denied');
});
const result = engine.addToConfig('claude-code');
expect(result.success).toBe(false);
expect(result.error).toContain('Permission denied');
});
it('returns error result when write fails', () => {
mockExistsSync.mockReturnValue(false);
mockWriteFileSync.mockImplementation(() => {
throw new Error('Disk full');
});
const result = engine.addToConfig('claude-code');
expect(result.success).toBe(false);
expect(result.error).toContain('Disk full');
});
it('returns error for invalid existing JSON', () => {
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue('not valid json{{{');
const result = engine.addToConfig('claude-code');
expect(result.success).toBe(false);
expect(result.error).toBeTruthy();
});
});
describe('isConfigured', () => {
it('returns true when bDS entry exists in config', () => {
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(
JSON.stringify({
mcpServers: { bDS: { type: 'http', url: 'http://127.0.0.1:4124/mcp' } },
}),
);
expect(engine.isConfigured('claude-code')).toBe(true);
});
it('returns false when config file does not exist', () => {
mockExistsSync.mockReturnValue(false);
expect(engine.isConfigured('claude-code')).toBe(false);
});
it('returns false when bDS entry is missing', () => {
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(
JSON.stringify({ mcpServers: { other: {} } }),
);
expect(engine.isConfigured('claude-code')).toBe(false);
});
it('checks VS Code servers key for github-copilot', () => {
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(
JSON.stringify({ servers: { bDS: { type: 'http', url: 'x' } } }),
);
expect(engine.isConfigured('github-copilot')).toBe(true);
});
});
describe('dynamic port', () => {
it('uses the provided mcpUrl in server entries', () => {
const customEngine = new MCPAgentConfigEngine({
homeDir: '/tmp',
platform: 'darwin',
mcpUrl: 'http://127.0.0.1:9999/mcp',
});
mockExistsSync.mockReturnValue(false);
customEngine.addToConfig('claude-code');
const written = JSON.parse(mockWriteFileSync.mock.calls[0]![1] as string);
expect(written.mcpServers.bDS.url).toBe('http://127.0.0.1:9999/mcp');
});
});
});

View File

@@ -32,9 +32,11 @@ function createMockDeps(): MCPServerDependencies {
}),
getScriptEngine: () => ({
createScript: vi.fn().mockResolvedValue({ id: 's1' }),
validateScript: vi.fn().mockResolvedValue({ valid: true, errors: [] }),
}),
getTemplateEngine: () => ({
createTemplate: vi.fn().mockResolvedValue({ id: 't1' }),
validateTemplate: vi.fn().mockResolvedValue({ valid: true, errors: [] }),
}),
getMetaEngine: () => ({
getProjectMetadata: vi.fn().mockResolvedValue(null),

View File

@@ -1380,7 +1380,7 @@ tags: ["nature", "sunset"]`;
return chain;
});
const result = await mediaEngine.getMediaFiltered({ year: 2024, month: 5 }); // June (0-indexed)
const result = await mediaEngine.getMediaFiltered({ year: 2024, month: 6 }); // June (1-indexed)
expect(result).toHaveLength(1);
});
@@ -1452,9 +1452,9 @@ tags: ["nature", "sunset"]`;
const result = await mediaEngine.getMediaByYearMonth();
// Note: month is 0-indexed from Date.getMonth()
expect(result).toContainEqual({ year: 2024, month: 0, count: 2 }); // January
expect(result).toContainEqual({ year: 2024, month: 1, count: 1 }); // February
// month is 1-indexed
expect(result).toContainEqual({ year: 2024, month: 1, count: 2 }); // January
expect(result).toContainEqual({ year: 2024, month: 2, count: 1 }); // February
});
it('should sort by year and month descending', async () => {
@@ -1479,7 +1479,7 @@ tags: ["nature", "sunset"]`;
const result = await mediaEngine.getMediaByYearMonth();
expect(result[0].year).toBe(2024);
expect(result[0].month).toBe(2); // March is month 2 (0-indexed)
expect(result[0].month).toBe(3); // March is month 3 (1-indexed)
});
});

View File

@@ -2653,10 +2653,10 @@ Published snapshot content`);
const result = await postEngine.getPostsByYearMonth();
// Note: getMonth() returns 0-11, so January is 0, February is 1, etc.
expect(result).toContainEqual({ year: 2024, month: 0, count: 2 }); // January
expect(result).toContainEqual({ year: 2024, month: 1, count: 1 }); // February
expect(result).toContainEqual({ year: 2023, month: 11, count: 1 }); // December
// months are 1-indexed (January=1, February=2, etc.)
expect(result).toContainEqual({ year: 2024, month: 1, count: 2 }); // January
expect(result).toContainEqual({ year: 2024, month: 2, count: 1 }); // February
expect(result).toContainEqual({ year: 2023, month: 12, count: 1 }); // December
});
it('should sort by year and month descending', async () => {
@@ -2677,7 +2677,7 @@ Published snapshot content`);
const result = await postEngine.getPostsByYearMonth();
expect(result[0].year).toBe(2024);
expect(result[0].month).toBe(2); // March (0-indexed)
expect(result[0].month).toBe(3); // March (1-indexed)
expect(result[result.length - 1].year).toBe(2023);
});
});

View File

@@ -81,7 +81,7 @@ function makeEngine(posts: PostData[]): PostEngineLike {
}
if (filter.month !== undefined && filter.year !== undefined) {
result = result.filter((post) => post.createdAt.getUTCMonth() === filter.month);
result = result.filter((post) => post.createdAt.getUTCMonth() === filter.month - 1);
}
if (filter.startDate) {

View File

@@ -59,7 +59,7 @@ function makeEngine(posts: PostData[], snapshotsById: Record<string, PostData |
}
if (filter.month !== undefined && filter.year !== undefined) {
result = result.filter((post) => post.createdAt.getMonth() === filter.month);
result = result.filter((post) => post.createdAt.getMonth() === filter.month - 1);
}
if (filter.startDate) {
@@ -110,7 +110,7 @@ describe('SharedSnapshotService', () => {
engine,
'my-post',
{ useDraftContent: true, draftPostId: 'draft-1' },
{ year: 2025, month: 2, day: 21 },
{ year: 2025, month: 3, day: 21 },
);
expect(result?.id).toBe('draft-1');
@@ -125,7 +125,7 @@ describe('SharedSnapshotService', () => {
findPublishedBySlug,
};
const result = await findSinglePostBySlug(engineWithShortcut, 'shortcut', undefined, { year: 2025, month: 0, day: 2 });
const result = await findSinglePostBySlug(engineWithShortcut, 'shortcut', undefined, { year: 2025, month: 1, day: 2 });
expect(result?.id).toBe('x1');
expect(findPublishedBySlug).toHaveBeenCalled();

View File

@@ -468,11 +468,16 @@ describe('main bootstrap preview behavior', () => {
PreviewServer: MockPreviewServer,
}));
vi.doMock('fs', () => ({
existsSync: vi.fn((targetPath: string) => targetPath.includes('window-state.json')),
readFileSync: vi.fn(() => JSON.stringify({ x: 120, y: 80, width: 1280, height: 820 })),
writeFileSync: vi.fn(),
}));
vi.doMock('fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('fs')>();
const mocked = {
...actual,
existsSync: vi.fn((targetPath: string) => targetPath.includes('window-state.json')),
readFileSync: vi.fn(() => JSON.stringify({ x: 120, y: 80, width: 1280, height: 820 })),
writeFileSync: vi.fn(),
};
return { ...mocked, default: mocked };
});
vi.doMock('../../src/main/database', () => ({
getDatabase: vi.fn(() => ({
@@ -593,11 +598,16 @@ describe('main bootstrap preview behavior', () => {
PreviewServer: MockPreviewServer,
}));
vi.doMock('fs', () => ({
existsSync: vi.fn((targetPath: string) => targetPath.includes('window-state.json')),
readFileSync: vi.fn(() => JSON.stringify({ x: -40, y: -10, width: 1800, height: 1000 })),
writeFileSync: vi.fn(),
}));
vi.doMock('fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('fs')>();
const mocked = {
...actual,
existsSync: vi.fn((targetPath: string) => targetPath.includes('window-state.json')),
readFileSync: vi.fn(() => JSON.stringify({ x: -40, y: -10, width: 1800, height: 1000 })),
writeFileSync: vi.fn(),
};
return { ...mocked, default: mocked };
});
vi.doMock('../../src/main/database', () => ({
getDatabase: vi.fn(() => ({

View File

@@ -1,40 +1,68 @@
import { describe, it, expect } from 'vitest';
import path from 'path';
import {
reviewPostHtml,
reviewScriptHtml,
reviewTemplateHtml,
reviewMetadataHtml,
resolveMcpViewsDirs,
loadViewHtml,
} from '../../src/main/engine/mcp-views';
const viewOpts = {
moduleDir: path.resolve(__dirname, '../../src/main/engine'),
};
describe('mcp-views', () => {
describe('resolveMcpViewsDirs', () => {
it('returns candidate directories', () => {
const dirs = resolveMcpViewsDirs(viewOpts);
expect(dirs.length).toBeGreaterThanOrEqual(2);
expect(dirs.some(d => d.includes('mcp-views'))).toBe(true);
});
});
describe('loadViewHtml', () => {
it('loads an existing view file', () => {
const html = loadViewHtml('review-post.html', viewOpts);
expect(html).toContain('<!DOCTYPE html>');
});
it('throws for a non-existent view', () => {
expect(() => loadViewHtml('does-not-exist.html', viewOpts)).toThrow(
/not found/,
);
});
});
describe('reviewPostHtml', () => {
it('returns valid HTML document', () => {
const html = reviewPostHtml();
const html = reviewPostHtml(viewOpts);
expect(html).toContain('<!DOCTYPE html>');
expect(html).toContain('</html>');
});
it('contains App import from ext-apps', () => {
const html = reviewPostHtml();
const html = reviewPostHtml(viewOpts);
expect(html).toContain('@modelcontextprotocol/ext-apps/app-with-deps');
expect(html).toContain('new App(');
});
it('contains accept and discard buttons', () => {
const html = reviewPostHtml();
const html = reviewPostHtml(viewOpts);
expect(html).toContain('acceptProposal()');
expect(html).toContain('discardProposal()');
});
it('calls accept_proposal and discard_proposal tools via app bridge', () => {
const html = reviewPostHtml();
const html = reviewPostHtml(viewOpts);
expect(html).toContain('app.callServerTool');
expect(html).toContain('"accept_proposal"');
expect(html).toContain('"discard_proposal"');
});
it('contains post-specific UI elements', () => {
const html = reviewPostHtml();
const html = reviewPostHtml(viewOpts);
expect(html).toContain('Review Post');
expect(html).toContain('Publish');
expect(html).toContain('badge-draft');
@@ -42,13 +70,13 @@ describe('mcp-views', () => {
});
it('renders tool result data via ontoolresult handler', () => {
const html = reviewPostHtml();
const html = reviewPostHtml(viewOpts);
expect(html).toContain('app.ontoolresult');
expect(html).toContain('renderReview');
});
it('uses XSS-safe escaping function', () => {
const html = reviewPostHtml();
const html = reviewPostHtml(viewOpts);
expect(html).toContain('function esc(');
expect(html).toContain('document.createElement("div")');
});
@@ -56,24 +84,24 @@ describe('mcp-views', () => {
describe('reviewScriptHtml', () => {
it('returns valid HTML document', () => {
const html = reviewScriptHtml();
const html = reviewScriptHtml(viewOpts);
expect(html).toContain('<!DOCTYPE html>');
expect(html).toContain('</html>');
});
it('contains App import from ext-apps', () => {
const html = reviewScriptHtml();
const html = reviewScriptHtml(viewOpts);
expect(html).toContain('@modelcontextprotocol/ext-apps/app-with-deps');
});
it('contains accept and discard buttons', () => {
const html = reviewScriptHtml();
const html = reviewScriptHtml(viewOpts);
expect(html).toContain('acceptProposal()');
expect(html).toContain('discardProposal()');
});
it('contains script-specific UI elements', () => {
const html = reviewScriptHtml();
const html = reviewScriptHtml(viewOpts);
expect(html).toContain('Review Script');
expect(html).toContain('Create Script');
expect(html).toContain('Python Code');
@@ -82,24 +110,24 @@ describe('mcp-views', () => {
describe('reviewTemplateHtml', () => {
it('returns valid HTML document', () => {
const html = reviewTemplateHtml();
const html = reviewTemplateHtml(viewOpts);
expect(html).toContain('<!DOCTYPE html>');
expect(html).toContain('</html>');
});
it('contains App import from ext-apps', () => {
const html = reviewTemplateHtml();
const html = reviewTemplateHtml(viewOpts);
expect(html).toContain('@modelcontextprotocol/ext-apps/app-with-deps');
});
it('contains accept and discard buttons', () => {
const html = reviewTemplateHtml();
const html = reviewTemplateHtml(viewOpts);
expect(html).toContain('acceptProposal()');
expect(html).toContain('discardProposal()');
});
it('contains template-specific UI elements', () => {
const html = reviewTemplateHtml();
const html = reviewTemplateHtml(viewOpts);
expect(html).toContain('Review Template');
expect(html).toContain('Create Template');
expect(html).toContain('Liquid Template');
@@ -108,24 +136,24 @@ describe('mcp-views', () => {
describe('reviewMetadataHtml', () => {
it('returns valid HTML document', () => {
const html = reviewMetadataHtml();
const html = reviewMetadataHtml(viewOpts);
expect(html).toContain('<!DOCTYPE html>');
expect(html).toContain('</html>');
});
it('contains App import from ext-apps', () => {
const html = reviewMetadataHtml();
const html = reviewMetadataHtml(viewOpts);
expect(html).toContain('@modelcontextprotocol/ext-apps/app-with-deps');
});
it('contains accept and discard buttons', () => {
const html = reviewMetadataHtml();
const html = reviewMetadataHtml(viewOpts);
expect(html).toContain('acceptProposal()');
expect(html).toContain('discardProposal()');
});
it('contains metadata-diff UI elements', () => {
const html = reviewMetadataHtml();
const html = reviewMetadataHtml(viewOpts);
expect(html).toContain('Metadata Changes');
expect(html).toContain('Apply Changes');
expect(html).toContain('diff-table');
@@ -134,7 +162,7 @@ describe('mcp-views', () => {
});
it('contains diff formatting function', () => {
const html = reviewMetadataHtml();
const html = reviewMetadataHtml(viewOpts);
expect(html).toContain('function fmt(');
expect(html).toContain('diff-old');
expect(html).toContain('diff-new');
@@ -143,10 +171,10 @@ describe('mcp-views', () => {
describe('shared behavior', () => {
const allViews = [
{ name: 'reviewPostHtml', fn: reviewPostHtml },
{ name: 'reviewScriptHtml', fn: reviewScriptHtml },
{ name: 'reviewTemplateHtml', fn: reviewTemplateHtml },
{ name: 'reviewMetadataHtml', fn: reviewMetadataHtml },
{ name: 'reviewPostHtml', fn: () => reviewPostHtml(viewOpts) },
{ name: 'reviewScriptHtml', fn: () => reviewScriptHtml(viewOpts) },
{ name: 'reviewTemplateHtml', fn: () => reviewTemplateHtml(viewOpts) },
{ name: 'reviewMetadataHtml', fn: () => reviewMetadataHtml(viewOpts) },
];
it.each(allViews)('$name connects the App on load', ({ fn }) => {