mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
The rule table mapped HOG_ -> FN_ ahead of any POSTHOG rule, so POSTHOG_FOO split into POST + HOG_ and came out POSTFN_FOO. Nothing sets that. The live pods set INSIGHTS_SKIP_MIGRATION_CHECKS, which bin/migrate-check reads on the container entrypoint, so the misspelling does not degrade a feature -- it runs migrate --check against a database whose history this tree does not share and the container never starts. Both replacements are measured against the shipped tree rather than chosen: every HOG_ name with a counterpart there is spelled INSIGHTS_ (HOG_QL -> INSIGHTS_QL, HOG_FUNCTION_TEMPLATES -> INSIGHTS_FUNCTION_TEMPLATES, 12 of 12 determinate cases), and FN_ appears in it nowhere. Upstream carries 95 HOG_ tokens and zero FN_ ones, so repairing FN_ back out is exactly the inverse of the bad rule and cannot collide with an upstream name. The rule is fixed for the next convergence, and the tree it already produced is repaired in the same commit -- the codemod is idempotent against its own output, so it cannot undo this itself.
283 lines
13 KiB
TypeScript
283 lines
13 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
|
|
import { buildAppStubHtml } from '@/resources/ui-apps'
|
|
import { DISPATCHABLE_APP_KEYS, UI_APPS, URI_MAP } from '@/resources/ui-apps.generated'
|
|
|
|
import {
|
|
generateDispatchModule,
|
|
resolveCustomApp,
|
|
resolveDetailApp,
|
|
resolveListApp,
|
|
} from '../../scripts/generate-ui-apps'
|
|
|
|
describe('ui-apps', () => {
|
|
describe('buildAppStubHtml', () => {
|
|
it('generates correct stub HTML for production base URL', () => {
|
|
const html = buildAppStubHtml('debug', 'https://mcp.hanzo.ai')
|
|
expect(html).toMatchInlineSnapshot(`
|
|
"<!DOCTYPE html>
|
|
<html lang="en"><head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<link rel="stylesheet" href="https://mcp.hanzo.ai/ui-apps/debug/styles.css">
|
|
</head><body>
|
|
<div id="root"></div>
|
|
<script src="https://mcp.hanzo.ai/ui-apps/debug/main.js"></script>
|
|
</body></html>"
|
|
`)
|
|
})
|
|
|
|
it('generates correct stub HTML for local development', () => {
|
|
const html = buildAppStubHtml('query-results', 'http://localhost:8787')
|
|
expect(html).toMatchInlineSnapshot(`
|
|
"<!DOCTYPE html>
|
|
<html lang="en"><head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<link rel="stylesheet" href="http://localhost:8787/ui-apps/query-results/styles.css">
|
|
</head><body>
|
|
<div id="root"></div>
|
|
<script src="http://localhost:8787/ui-apps/query-results/main.js"></script>
|
|
</body></html>"
|
|
`)
|
|
})
|
|
|
|
it('handles hyphenated app names', () => {
|
|
const html = buildAppStubHtml('error-issue-list', 'https://mcp.hanzo.ai')
|
|
expect(html).toContain('href="https://mcp.hanzo.ai/ui-apps/error-issue-list/styles.css"')
|
|
expect(html).toContain('src="https://mcp.hanzo.ai/ui-apps/error-issue-list/main.js"')
|
|
})
|
|
|
|
it('produces valid HTML structure', () => {
|
|
const html = buildAppStubHtml('debug', 'https://example.com')
|
|
expect(html).toMatch(/^<!DOCTYPE html>/)
|
|
expect(html).toContain('<html lang="en">')
|
|
expect(html).toContain('<meta charset="UTF-8">')
|
|
expect(html).toContain('<div id="root"></div>')
|
|
expect(html).toContain('</body></html>')
|
|
})
|
|
|
|
it('uses script tag without type=module (IIFE format)', () => {
|
|
const html = buildAppStubHtml('debug', 'https://example.com')
|
|
expect(html).toContain('<script src=')
|
|
expect(html).not.toContain('type="module"')
|
|
})
|
|
})
|
|
|
|
describe('registerUiAppResources', () => {
|
|
function createMockServer(): {
|
|
registerResource: ReturnType<typeof vi.fn>
|
|
} {
|
|
return { registerResource: vi.fn() }
|
|
}
|
|
|
|
function createMockContext(env: Record<string, string | undefined> = {}): {
|
|
env: Record<string, string | undefined>
|
|
} {
|
|
return {
|
|
env: {
|
|
MCP_APPS_BASE_URL: undefined,
|
|
INSIGHTS_MCP_APPS_ANALYTICS_BASE_URL: undefined,
|
|
INSIGHTS_UI_APPS_TOKEN: undefined,
|
|
INSIGHTS_API_BASE_URL: undefined,
|
|
INSIGHTS_ANALYTICS_API_KEY: undefined,
|
|
INSIGHTS_ANALYTICS_HOST: undefined,
|
|
...env,
|
|
},
|
|
}
|
|
}
|
|
|
|
it('skips registration when MCP_APPS_BASE_URL is not set', async () => {
|
|
const { registerUiAppResources } = await import('@/resources/ui-apps')
|
|
const server = createMockServer()
|
|
const context = createMockContext()
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
|
|
await registerUiAppResources(server as any, context as any)
|
|
|
|
expect(server.registerResource).not.toHaveBeenCalled()
|
|
expect(warnSpy).toHaveBeenCalledWith(
|
|
'MCP_APPS_BASE_URL is not set — UI app resources will not be registered'
|
|
)
|
|
warnSpy.mockRestore()
|
|
})
|
|
|
|
it('registers all apps when MCP_APPS_BASE_URL is set', async () => {
|
|
const { registerUiAppResources } = await import('@/resources/ui-apps')
|
|
const server = createMockServer()
|
|
const context = createMockContext({ MCP_APPS_BASE_URL: 'https://mcp.hanzo.ai' })
|
|
|
|
await registerUiAppResources(server as any, context as any)
|
|
|
|
expect(server.registerResource).toHaveBeenCalledTimes(UI_APPS.length)
|
|
})
|
|
|
|
it('registers apps with correct names and URIs', async () => {
|
|
const { registerUiAppResources } = await import('@/resources/ui-apps')
|
|
const server = createMockServer()
|
|
const context = createMockContext({ MCP_APPS_BASE_URL: 'https://mcp.hanzo.ai' })
|
|
|
|
await registerUiAppResources(server as any, context as any)
|
|
|
|
const registeredNames = server.registerResource.mock.calls.map((call: unknown[]) => call[0])
|
|
expect(registeredNames).toContain('MCP Apps Debug')
|
|
expect(registeredNames).toContain('Query Results')
|
|
expect(registeredNames).toContain('Insights Feature Flag')
|
|
expect(registeredNames).toContain('Insights Feature Flag Testing')
|
|
expect(registeredNames).toContain('Insights Experiment Results')
|
|
|
|
expect(URI_MAP['feature-flag-testing']).toBe('ui://insights/feature-flag-testing.html')
|
|
})
|
|
|
|
it('includes base URL in CSP resourceDomains', async () => {
|
|
const { registerUiAppResources } = await import('@/resources/ui-apps')
|
|
const server = createMockServer()
|
|
const context = createMockContext({ MCP_APPS_BASE_URL: 'https://mcp.hanzo.ai' })
|
|
|
|
await registerUiAppResources(server as any, context as any)
|
|
|
|
// Call the resource handler for the first registered app
|
|
const handler = server.registerResource.mock.calls[0]![3]
|
|
const result = await handler(new URL('ui://insights/debug.html'))
|
|
|
|
expect(result.contents[0]._meta.ui.csp.resourceDomains).toContain('https://mcp.hanzo.ai')
|
|
expect(result.contents[0]._meta['openai/widgetCSP'].resource_domains).toContain('https://mcp.hanzo.ai')
|
|
})
|
|
|
|
it('includes analytics URL in CSP when set', async () => {
|
|
const { registerUiAppResources } = await import('@/resources/ui-apps')
|
|
const server = createMockServer()
|
|
const context = createMockContext({
|
|
MCP_APPS_BASE_URL: 'https://mcp.hanzo.ai',
|
|
INSIGHTS_MCP_APPS_ANALYTICS_BASE_URL: 'https://us.i.hanzo.ai',
|
|
})
|
|
|
|
await registerUiAppResources(server as any, context as any)
|
|
|
|
const handler = server.registerResource.mock.calls[0]![3]
|
|
const result = await handler(new URL('ui://insights/debug.html'))
|
|
|
|
expect(result.contents[0]._meta.ui.csp.resourceDomains).toContain('https://us.i.hanzo.ai')
|
|
expect(result.contents[0]._meta.ui.csp.connectDomains).toContain('https://us.i.hanzo.ai')
|
|
expect(result.contents[0]._meta['openai/widgetCSP'].resource_domains).toContain('https://us.i.hanzo.ai')
|
|
expect(result.contents[0]._meta['openai/widgetCSP'].connect_domains).toContain('https://us.i.hanzo.ai')
|
|
})
|
|
|
|
it('omits analytics URL from CSP when not set', async () => {
|
|
const { registerUiAppResources } = await import('@/resources/ui-apps')
|
|
const server = createMockServer()
|
|
const context = createMockContext({ MCP_APPS_BASE_URL: 'https://mcp.hanzo.ai' })
|
|
|
|
await registerUiAppResources(server as any, context as any)
|
|
|
|
const handler = server.registerResource.mock.calls[0]![3]
|
|
const result = await handler(new URL('ui://insights/debug.html'))
|
|
|
|
expect(result.contents[0]._meta.ui.csp.resourceDomains).toEqual(['https://mcp.hanzo.ai'])
|
|
expect(result.contents[0]._meta.ui.csp.connectDomains).toEqual([])
|
|
expect(result.contents[0]._meta['openai/widgetCSP'].resource_domains).toEqual(['https://mcp.hanzo.ai'])
|
|
expect(result.contents[0]._meta['openai/widgetCSP'].connect_domains).toEqual([])
|
|
})
|
|
|
|
it('resource handler returns stub HTML with correct base URL', async () => {
|
|
const { registerUiAppResources } = await import('@/resources/ui-apps')
|
|
const server = createMockServer()
|
|
const context = createMockContext({ MCP_APPS_BASE_URL: 'https://mcp.hanzo.ai' })
|
|
|
|
await registerUiAppResources(server as any, context as any)
|
|
|
|
// Find the debug app registration
|
|
const debugCall = server.registerResource.mock.calls.find((call: unknown[]) => call[0] === 'MCP Apps Debug')
|
|
const handler = debugCall![3]
|
|
const result = await handler(new URL('ui://insights/debug.html'))
|
|
|
|
expect(result.contents[0].text).toMatchInlineSnapshot(`
|
|
"<!DOCTYPE html>
|
|
<html lang="en"><head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<link rel="stylesheet" href="https://mcp.hanzo.ai/ui-apps/debug/styles.css">
|
|
</head><body>
|
|
<div id="root"></div>
|
|
<script src="https://mcp.hanzo.ai/ui-apps/debug/main.js"></script>
|
|
</body></html>"
|
|
`)
|
|
})
|
|
|
|
it('resource handler returns correct MIME type', async () => {
|
|
const { registerUiAppResources } = await import('@/resources/ui-apps')
|
|
const server = createMockServer()
|
|
const context = createMockContext({ MCP_APPS_BASE_URL: 'https://mcp.hanzo.ai' })
|
|
|
|
await registerUiAppResources(server as any, context as any)
|
|
|
|
const handler = server.registerResource.mock.calls[0]![3]
|
|
const result = await handler(new URL('ui://insights/debug.html'))
|
|
|
|
expect(result.contents[0].mimeType).toBe('text/html;profile=mcp-app')
|
|
})
|
|
})
|
|
|
|
describe('render-ui dispatch', () => {
|
|
it('DISPATCHABLE_APP_KEYS includes opted-in custom apps', () => {
|
|
expect(DISPATCHABLE_APP_KEYS).toContain('survey')
|
|
expect(DISPATCHABLE_APP_KEYS).toContain('survey-list')
|
|
expect(DISPATCHABLE_APP_KEYS).toContain('query-results')
|
|
for (const customKey of ['debug', 'render-ui', 'visual-review-snapshots']) {
|
|
expect(DISPATCHABLE_APP_KEYS).not.toContain(customKey)
|
|
}
|
|
})
|
|
|
|
it('generates dispatch entries for reusable views and a Content component for list apps', () => {
|
|
const queryResultsConfig = resolveCustomApp({
|
|
type: 'custom',
|
|
app_name: 'Query Results',
|
|
description: 'Interactive query results',
|
|
render_ui: {
|
|
component_import: '../components/Component',
|
|
view_component: 'Component',
|
|
view_prop: 'data',
|
|
},
|
|
})
|
|
if (!queryResultsConfig.render_ui) {
|
|
throw new Error('Expected query results to configure render-ui dispatch')
|
|
}
|
|
|
|
const code = generateDispatchModule([
|
|
{
|
|
appKey: 'survey',
|
|
type: 'detail',
|
|
config: resolveDetailApp(
|
|
'survey',
|
|
{ type: 'detail', view_prop: 'survey' },
|
|
'products/surveys/mcp/apps'
|
|
),
|
|
},
|
|
{
|
|
appKey: 'query-results',
|
|
type: 'custom',
|
|
config: { ...queryResultsConfig, render_ui: queryResultsConfig.render_ui },
|
|
},
|
|
{
|
|
appKey: 'survey-list',
|
|
type: 'list',
|
|
config: resolveListApp(
|
|
'survey-list',
|
|
{ type: 'list', detail_tool: 'survey-get' },
|
|
'products/surveys/mcp/apps'
|
|
),
|
|
},
|
|
])
|
|
|
|
expect(code).toContain("'survey': ({ data }) => <SurveyView survey={data as SurveyData} />")
|
|
expect(code).toContain("import { Component } from '../components/Component'")
|
|
expect(code).toContain("'query-results': ({ data }) => <Component data={data} />")
|
|
expect(code).toContain(
|
|
"'survey-list': ({ data, app }) => <SurveyListContent data={data as SurveyListData} app={app} />"
|
|
)
|
|
expect(code).toContain('function SurveyListContent(')
|
|
expect(code).toContain("name: 'survey-get'")
|
|
})
|
|
})
|
|
})
|