// @vitest-environment node import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import fs from 'fs-extra'; import { createChatToolRuntime } from '../electron/gateway/chat-tooling'; import type { SkillCapability } from '../electron/gateway/skill-capability-parser'; const pdfJsMocks = vi.hoisted(() => ({ getDocument: vi.fn(), })); const skillConfigMocks = vi.hoisted(() => ({ getSkillConfig: vi.fn(), })); const fetchMocks = vi.hoisted(() => ({ fetch: vi.fn(), })); const browserOpenMocks = vi.hoisted(() => ({ openUrlInBrowser: vi.fn(), })); const clawHubMocks = vi.hoisted(() => ({ search: vi.fn(), })); const workbookRows = [ { hotel: 'Hongqiao', sales_amount: 128000, room_nights: 420, occupancy: 0.86 }, { hotel: 'Pudong', sales_amount: 98000, room_nights: 365, occupancy: 0.73 }, { hotel: 'Hongqiao', sales_amount: 128000, room_nights: 420, occupancy: 0.86 }, ]; vi.mock('xlsx', () => ({ readFile: vi.fn(() => ({ SheetNames: ['sales'], Sheets: { sales: { '!ref': 'A1:D4' }, }, })), utils: { sheet_to_json: vi.fn(() => workbookRows), }, })); vi.mock('pdfjs-dist/legacy/build/pdf.mjs', () => ({ getDocument: pdfJsMocks.getDocument, })); vi.mock('@electron/utils/skill-config', () => ({ getSkillConfig: skillConfigMocks.getSkillConfig, })); vi.mock('@electron/service/browser-open-service', () => ({ openUrlInBrowser: browserOpenMocks.openUrlInBrowser, })); vi.mock('../electron/gateway/clawhub', () => ({ ClawHubService: class { search = clawHubMocks.search; }, })); const spreadsheetCapability: SkillCapability = { skillKey: 'minimax-xlsx', slug: 'minimax-xlsx', name: 'MiniMax XLSX', description: 'Analyze spreadsheet files such as .xls, .xlsx, .csv, and .tsv.', enabled: true, category: 'document', allowedTools: [], operationHints: ['read', 'analyze'], triggerHints: ['spreadsheet', 'excel'], inputExtensions: ['.xls', '.xlsx', '.csv', '.tsv'], requiredEnvVars: [], requiresAuth: false, plannerSummary: 'document skill; operations: read, analyze; inputs: .xls, .xlsx, .csv, .tsv', renderHints: { card: 'document-analysis', preferredView: 'table', skillType: 'spreadsheet', }, }; const genericSearchCapability: SkillCapability = { skillKey: 'minimax-search', slug: 'minimax-search', name: 'MiniMax Search', description: 'Search indexed knowledge sources and summarize the results.', enabled: true, category: 'search', allowedTools: ['web.search'], operationHints: ['search', 'retrieve'], triggerHints: ['search', 'lookup'], inputExtensions: [], requiredEnvVars: ['ZN_AI_TEST_GENERIC_SKILL_ENV'], requiresAuth: true, plannerSummary: 'search skill; operations: search, retrieve; inputs: text', renderHints: { card: 'search-results', preferredView: 'summary', skillType: 'search', }, }; const braveSearchCapability: SkillCapability = { skillKey: 'brave-web-search', slug: 'brave-web-search', name: 'Brave Web Search', description: 'Search the web with Brave Search.', enabled: true, category: 'search', allowedTools: ['web.search'], operationHints: ['search'], triggerHints: ['search', 'web'], inputExtensions: [], requiredEnvVars: ['BRAVE_SEARCH_API_KEY'], requiresAuth: true, plannerSummary: 'search skill; operations: search; inputs: text', renderHints: { card: 'search-results', preferredView: 'summary', skillType: 'search', }, }; const tavilySearchCapability: SkillCapability = { skillKey: 'tavily-search', slug: 'tavily-search', name: 'Tavily Search', description: 'Search the web with Tavily.', enabled: true, category: 'search', allowedTools: ['Bash(tvly *)'], operationHints: ['search', 'research'], triggerHints: ['search', 'latest'], inputExtensions: [], requiredEnvVars: ['TAVILY_API_KEY'], requiresAuth: true, plannerSummary: 'search skill; operations: search, research; inputs: text', renderHints: { card: 'search-results', preferredView: 'summary', skillType: 'search', }, }; const browserSkillCapability: SkillCapability = { skillKey: 'browser-scout', slug: 'browser-scout', name: 'Browser Scout', description: 'Open an explicit URL in the local browser.', enabled: true, category: 'general', allowedTools: ['browser.open_url'], operationHints: ['open'], triggerHints: ['open url', 'visit page'], inputExtensions: [], requiredEnvVars: [], requiresAuth: false, plannerSummary: 'browser skill; operations: open; inputs: url', renderHints: { card: 'browser-step', preferredView: 'summary', skillType: 'browser', }, }; const commandSkillCapability: SkillCapability = { skillKey: 'find-skills', slug: 'find-skills', name: 'Find Skills', description: 'Discover installable skills for a task.', enabled: true, category: 'general', allowedTools: [], operationHints: ['find'], triggerHints: ['find a skill', 'discover skill'], inputExtensions: [], requiredEnvVars: [], requiresAuth: false, plannerSummary: 'command skill; operations: find; inputs: text', renderHints: { card: 'search-results', preferredView: 'summary', skillType: 'command', }, }; const docxCapability: SkillCapability = { skillKey: 'docx', slug: 'docx', name: 'DOCX', description: 'Create, read, edit, or analyze .docx files.', enabled: true, category: 'document', allowedTools: [], operationHints: ['read', 'analyze'], triggerHints: ['docx', 'word document'], inputExtensions: ['.docx'], requiredEnvVars: [], requiresAuth: false, plannerSummary: 'document skill; operations: read, analyze; inputs: .docx', renderHints: { card: 'document-analysis', preferredView: 'summary', skillType: 'document', }, }; const pptxCapability: SkillCapability = { skillKey: 'pptx', slug: 'pptx', name: 'PPTX', description: 'Create, read, edit, or analyze .pptx files.', enabled: true, category: 'document', allowedTools: [], operationHints: ['read', 'analyze'], triggerHints: ['pptx', 'presentation'], inputExtensions: ['.pptx'], requiredEnvVars: [], requiresAuth: false, plannerSummary: 'document skill; operations: read, analyze; inputs: .pptx', renderHints: { card: 'document-analysis', preferredView: 'summary', skillType: 'document', }, }; const pdfCapability: SkillCapability = { skillKey: 'pdf', slug: 'pdf', name: 'PDF', description: 'Create, read, edit, or analyze .pdf files.', enabled: true, category: 'document', allowedTools: [], operationHints: ['read', 'analyze'], triggerHints: ['pdf', 'document'], inputExtensions: ['.pdf'], requiredEnvVars: [], requiresAuth: false, plannerSummary: 'document skill; operations: read, analyze; inputs: .pdf', renderHints: { card: 'document-analysis', preferredView: 'summary', skillType: 'document', }, }; const tempDirs: string[] = []; async function createTempDir(prefix: string): Promise { const root = `${process.cwd().replace(/\\/g, '/')}/.tmp-vitest`; const dir = `${root}/${prefix}${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; await fs.ensureDir(dir); tempDirs.push(dir); return dir; } async function createDocxFixture(): Promise { const JSZip = (await import('jszip')).default; const dir = await createTempDir('zn-ai-docx-'); const filePath = `${dir}/brief.docx`; const zip = new JSZip(); zip.file('word/document.xml', ` Quarterly Hotel Review Revenue grew 12 percent compared with last quarter. Table Cell `); zip.file('word/comments.xml', ` Looks good `); zip.file('docProps/core.xml', ` Hotel Review ZN AI `); zip.file('docProps/app.xml', ` 3 `); await fs.writeFile(filePath, await zip.generateAsync({ type: 'nodebuffer' })); return filePath; } async function createPptxFixture(): Promise { const JSZip = (await import('jszip')).default; const dir = await createTempDir('zn-ai-pptx-'); const filePath = `${dir}/deck.pptx`; const zip = new JSZip(); zip.file('ppt/presentation.xml', ` `); zip.file('ppt/slides/slide1.xml', ` Executive Summary `); zip.file('ppt/slides/slide2.xml', ` Growth Plan `); await fs.writeFile(filePath, await zip.generateAsync({ type: 'nodebuffer' })); return filePath; } async function createPdfFixture(): Promise { const dir = await createTempDir('zn-ai-pdf-'); const filePath = `${dir}/brief.pdf`; await fs.writeFile(filePath, Buffer.from('%PDF-1.4\n%mock\n', 'utf-8')); return filePath; } async function createGenericCommandSkillFixture(scriptName = 'search.js'): Promise<{ baseDir: string; scriptPath: string }> { const dir = await createTempDir('zn-ai-command-skill-'); const scriptsDir = `${dir}/scripts`; const scriptPath = `${scriptsDir}/${scriptName}`; await fs.ensureDir(scriptsDir); await fs.writeFile( scriptPath, [ 'const query = process.argv.slice(2).join(" ").trim();', 'process.stdout.write(JSON.stringify({', ' query,', ' summary: `Ran generic command for ${query}`,', ' results: [{', ' title: `Result for ${query}`,', ' url: "https://example.com/result",', ' snippet: "Generated from local command script",', ' }],', '}));', ].join('\n'), 'utf8', ); return { baseDir: dir, scriptPath }; } describe('chat tooling adapters', () => { beforeEach(() => { delete process.env.ZN_AI_TEST_GENERIC_SKILL_ENV; pdfJsMocks.getDocument.mockReset(); skillConfigMocks.getSkillConfig.mockReset(); skillConfigMocks.getSkillConfig.mockResolvedValue(undefined); fetchMocks.fetch.mockReset(); browserOpenMocks.openUrlInBrowser.mockReset(); clawHubMocks.search.mockReset(); vi.stubGlobal('fetch', fetchMocks.fetch); }); afterEach(async () => { await Promise.all(tempDirs.splice(0).map((dir) => fs.remove(dir))); vi.unstubAllGlobals(); }); it('analyzes legacy .xls attachments without relying on a system python runtime', async () => { const runtime = createChatToolRuntime([spreadsheetCapability]); const result = await runtime.run({ toolCallId: 'tool-call-1', toolName: 'minimax-xlsx', input: { prompt: 'Analyze this hotel sales workbook.', skillKey: 'minimax-xlsx', attachments: [ { fileName: 'hotel-sales.xls', filePath: 'F:\\Downloads\\hotel-sales.xls', mimeType: 'application/vnd.ms-excel', source: 'user-upload', }, ], }, }); expect(result.execution.ok).toBe(true); expect(result.execution.summary).toContain('3 total row(s)'); const executionRaw = result.execution.raw as { reports: Array<{ report: { engine: string; workbook: { sheetNames: string[] }; structure: Record; quality: Record>; }; }>; }; expect(executionRaw.reports[0]?.report.engine).toBe('node-xlsx'); expect(executionRaw.reports[0]?.report.workbook.sheetNames).toContain('sales'); expect(executionRaw.reports[0]?.report.structure.sales?.shape).toEqual({ rows: 3, cols: 4, }); expect(executionRaw.reports[0]?.report.quality.sales).toEqual( expect.arrayContaining([ expect.objectContaining({ type: 'duplicate_rows', }), ]), ); }); it('analyzes docx attachments with the generic document adapter', async () => { const runtime = createChatToolRuntime([docxCapability]); const filePath = await createDocxFixture(); const result = await runtime.run({ toolCallId: 'tool-call-docx', toolName: 'docx', input: { prompt: 'Review this Word document.', attachments: [ { fileName: 'brief.docx', filePath, mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', source: 'user-upload', }, ], }, }); expect(result.execution.ok).toBe(true); expect(result.execution.summary).toContain('Document analysis completed.'); expect(result.execution.renderHints).toEqual(expect.objectContaining({ skillType: 'document', })); const executionRaw = result.execution.raw as { fileCount: number; kinds: string[]; reports: Array<{ kind: string; summary: string; metadata: { pageCount?: number; paragraphCount?: number; tableCount?: number; commentCount?: number; }; preview: Array<{ text?: string }>; }>; }; expect(executionRaw.fileCount).toBe(1); expect(executionRaw.kinds).toContain('docx'); expect(executionRaw.reports[0]?.kind).toBe('docx'); expect(executionRaw.reports[0]?.summary).toContain('2 paragraph(s)'); expect(executionRaw.reports[0]?.metadata).toEqual(expect.objectContaining({ pageCount: 3, paragraphCount: 2, tableCount: 1, commentCount: 1, })); expect(executionRaw.reports[0]?.preview[0]?.text).toContain('Quarterly Hotel Review'); }); it('analyzes pptx attachments with the generic document adapter', async () => { const runtime = createChatToolRuntime([pptxCapability]); const filePath = await createPptxFixture(); const result = await runtime.run({ toolCallId: 'tool-call-pptx', toolName: 'pptx', input: { prompt: 'Summarize this deck.', attachments: [ { fileName: 'deck.pptx', filePath, mimeType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', source: 'user-upload', }, ], }, }); expect(result.execution.ok).toBe(true); const executionRaw = result.execution.raw as { reports: Array<{ kind: string; metadata: { slideCount?: number; hiddenSlideCount?: number; }; preview: Array<{ slide?: number; text?: string }>; }>; }; expect(executionRaw.reports[0]?.kind).toBe('pptx'); expect(executionRaw.reports[0]?.metadata).toEqual(expect.objectContaining({ slideCount: 2, hiddenSlideCount: 1, })); expect(executionRaw.reports[0]?.preview[0]).toEqual(expect.objectContaining({ slide: 1, })); expect(executionRaw.reports[0]?.preview[0]?.text).toContain('Executive Summary'); }); it('analyzes pdf attachments with the generic document adapter', async () => { pdfJsMocks.getDocument.mockImplementation(() => ({ promise: Promise.resolve({ numPages: 2, getMetadata: async () => ({ info: { Title: 'Hotel Brief', Author: 'ZN AI', }, }), getPage: async (pageNumber: number) => ({ getTextContent: async () => ({ items: [ { str: pageNumber === 1 ? 'Hotel brief overview' : 'Revenue improved year over year' }, ], }), }), destroy: async () => undefined, }), destroy: async () => undefined, })); const runtime = createChatToolRuntime([pdfCapability]); const filePath = await createPdfFixture(); const result = await runtime.run({ toolCallId: 'tool-call-pdf', toolName: 'pdf', input: { prompt: 'Analyze this PDF.', attachments: [ { fileName: 'brief.pdf', filePath, mimeType: 'application/pdf', source: 'user-upload', }, ], }, }); expect(result.execution.ok).toBe(true); const executionRaw = result.execution.raw as { reports: Array<{ kind: string; engine: string; metadata: { title?: string; pageCount?: number; }; preview: Array<{ page?: number; text?: string }>; }>; }; expect(executionRaw.reports[0]?.kind).toBe('pdf'); expect(executionRaw.reports[0]?.engine).toBe('node-pdfjs'); expect(executionRaw.reports[0]?.metadata).toEqual(expect.objectContaining({ title: 'Hotel Brief', pageCount: 2, })); expect(executionRaw.reports[0]?.preview[1]).toEqual(expect.objectContaining({ page: 2, })); }); it('executes brave-web-search with configured credentials and normalizes results', async () => { skillConfigMocks.getSkillConfig.mockResolvedValue({ apiKey: 'brave-key', env: {}, }); fetchMocks.fetch.mockResolvedValue({ ok: true, status: 200, text: async () => JSON.stringify({ query: { original: 'hotel revenue trends', more_results_available: true, }, web: { family_friendly: true, results: [ { title: 'Hotel Revenue Trends Q1', url: 'https://example.com/revenue-q1', description: 'Revenue increased across the segment.', age: '2 days ago', page_age: '2026-04-20T10:00:00Z', profile: { name: 'Example News', }, }, { title: 'Hotel Demand Outlook', url: 'https://example.com/demand', description: 'Demand remains resilient.', }, ], }, }), }); const runtime = createChatToolRuntime([braveSearchCapability]); const result = await runtime.run({ toolCallId: 'tool-call-brave', toolName: 'brave-web-search', input: { prompt: 'hotel revenue trends', count: 2, timeRange: 'week', country: 'US', }, }); expect(result.execution.ok).toBe(true); expect(result.execution.summary).toContain('Found 2 result(s)'); expect(fetchMocks.fetch).toHaveBeenCalledTimes(1); const [requestUrl, requestInit] = fetchMocks.fetch.mock.calls[0] || []; expect(String(requestUrl)).toContain('https://api.search.brave.com/res/v1/web/search?'); expect(String(requestUrl)).toContain('q=hotel+revenue+trends'); expect(String(requestUrl)).toContain('count=2'); expect(String(requestUrl)).toContain('freshness=pw'); expect(String(requestUrl)).toContain('country=US'); expect(requestInit).toEqual(expect.objectContaining({ method: 'GET', headers: expect.objectContaining({ 'X-Subscription-Token': 'brave-key', }), })); const executionRaw = result.execution.raw as { provider: string; resultCount: number; query: string; results: Array<{ title: string; url: string; snippet?: string; source?: string }>; moreResultsAvailable?: boolean; }; expect(executionRaw.provider).toBe('brave'); expect(executionRaw.resultCount).toBe(2); expect(executionRaw.query).toBe('hotel revenue trends'); expect(executionRaw.results[0]).toEqual(expect.objectContaining({ title: 'Hotel Revenue Trends Q1', url: 'https://example.com/revenue-q1', snippet: 'Revenue increased across the segment.', source: 'Example News', })); expect(result.execution.artifacts?.[0]).toEqual(expect.objectContaining({ kind: 'url', uri: 'https://example.com/revenue-q1', })); }); it('executes tavily-search with configured env credentials and normalizes results', async () => { skillConfigMocks.getSkillConfig.mockResolvedValue({ env: { TAVILY_API_KEY: 'tvly-key', }, }); fetchMocks.fetch.mockResolvedValue({ ok: true, status: 200, text: async () => JSON.stringify({ query: 'hotel demand forecast', answer: 'Demand is increasing heading into the summer season.', response_time: 1.42, results: [ { title: 'Hospitality Forecast 2026', url: 'https://example.com/forecast', content: 'Forecast shows double-digit demand growth.', score: 0.9123, published_date: '2026-04-21', }, ], }), }); const runtime = createChatToolRuntime([tavilySearchCapability]); const result = await runtime.run({ toolCallId: 'tool-call-tavily', toolName: 'tavily-search', input: { prompt: 'hotel demand forecast', maxResults: 3, depth: 'advanced', timeRange: 'month', includeDomains: ['example.com', 'reuters.com'], includeAnswer: true, }, }); expect(result.execution.ok).toBe(true); expect(result.execution.summary).toContain('Found 1 result(s)'); expect(fetchMocks.fetch).toHaveBeenCalledTimes(1); const [requestUrl, requestInit] = fetchMocks.fetch.mock.calls[0] || []; expect(requestUrl).toBe('https://api.tavily.com/search'); expect(requestInit).toEqual(expect.objectContaining({ method: 'POST', headers: expect.objectContaining({ Authorization: 'Bearer tvly-key', }), })); const body = JSON.parse(String(requestInit?.body || '{}')) as Record; expect(body).toEqual(expect.objectContaining({ query: 'hotel demand forecast', max_results: 3, search_depth: 'advanced', time_range: 'month', include_answer: true, include_domains: ['example.com', 'reuters.com'], })); const executionRaw = result.execution.raw as { provider: string; query: string; answer?: string; responseTimeMs?: number; results: Array<{ title: string; score?: number; publishedAt?: string }>; }; expect(executionRaw.provider).toBe('tavily'); expect(executionRaw.query).toBe('hotel demand forecast'); expect(executionRaw.answer).toContain('Demand is increasing'); expect(executionRaw.responseTimeMs).toBe(1420); expect(executionRaw.results[0]).toEqual(expect.objectContaining({ title: 'Hospitality Forecast 2026', score: 0.9123, publishedAt: '2026-04-21', })); }); it('executes browser-capable skills by delegating to browser.open_url behavior', async () => { browserOpenMocks.openUrlInBrowser.mockResolvedValue({ pageUrl: 'https://example.com/hotels', title: 'Hotel Trends', }); const runtime = createChatToolRuntime([browserSkillCapability]); const result = await runtime.run({ toolCallId: 'tool-call-browser-skill', toolName: 'browser-scout', input: { prompt: 'Open https://example.com/hotels', }, }); expect(result.execution.ok).toBe(true); expect(browserOpenMocks.openUrlInBrowser).toHaveBeenCalledWith( 'https://example.com/hotels', expect.any(Object), ); expect(result.execution.renderHints).toEqual(expect.objectContaining({ skillType: 'browser', })); expect(result.execution.raw).toEqual(expect.objectContaining({ pageUrl: 'https://example.com/hotels', title: 'Hotel Trends', skillKey: 'browser-scout', })); }); it('executes find-skills through the command adapter and normalizes clawhub results', async () => { clawHubMocks.search.mockResolvedValue([ { slug: 'vercel-labs/agent-skills@react-best-practices', name: 'react-best-practices', description: 'React and Next.js guidance from Vercel.', version: '1.2.0', downloads: 185000, stars: 4200, }, ]); const runtime = createChatToolRuntime([commandSkillCapability]); const result = await runtime.run({ toolCallId: 'tool-call-find-skills', toolName: 'find-skills', input: { prompt: 'react performance', maxResults: 5, }, }); expect(result.execution.ok).toBe(true); expect(clawHubMocks.search).toHaveBeenCalledWith({ query: 'react performance', limit: 5, }); expect(result.execution.summary).toContain('1 matching skill result(s)'); expect(result.execution.logs).toContain('npx skills find react performance'); expect(result.execution.renderHints).toEqual(expect.objectContaining({ card: 'search-results', skillType: 'command', })); const executionRaw = result.execution.raw as { provider: string; command?: string; results: Array<{ title: string; url: string; installCommand?: string; downloads?: number; stars?: number; }>; }; expect(executionRaw.provider).toBe('clawhub'); expect(executionRaw.command).toBe('npx skills find'); expect(executionRaw.results[0]).toEqual(expect.objectContaining({ title: 'react-best-practices', url: 'https://skills.sh/vercel-labs/agent-skills/react-best-practices', installCommand: 'npx skills add vercel-labs/agent-skills@react-best-practices -g -y', downloads: 185000, stars: 4200, })); }); it('executes generic command-style skills from manifest command templates', async () => { const fixture = await createGenericCommandSkillFixture('search.js'); const capability: SkillCapability = { ...commandSkillCapability, skillKey: 'custom-command-skill', slug: 'custom-command-skill', name: 'Custom Command Skill', baseDir: fixture.baseDir, manifestPath: `${fixture.baseDir}/SKILL.md`, commandExamples: ['node scripts/search.js [query]'], }; const runtime = createChatToolRuntime([capability]); const result = await runtime.run({ toolCallId: 'tool-call-generic-command-template', toolName: 'custom-command-skill', input: { prompt: 'hotel strategy', }, }); expect(result.execution.ok).toBe(true); expect(result.execution.summary).toContain('Command completed via node scripts/search.js hotel strategy'); const executionRaw = result.execution.raw as { skillKey: string; command: string; query: string; summary: string; results: Array<{ title: string; url: string }>; }; expect(executionRaw.skillKey).toBe('custom-command-skill'); expect(executionRaw.command).toContain('node scripts/search.js hotel strategy'); expect(executionRaw.query).toBe('hotel strategy'); expect(executionRaw.summary).toContain('hotel strategy'); expect(executionRaw.results[0]).toEqual(expect.objectContaining({ title: 'Result for hotel strategy', url: 'https://example.com/result', })); }); it('executes generic command-style skills from a single scripts entrypoint when no manifest template exists', async () => { const fixture = await createGenericCommandSkillFixture('run.js'); const capability: SkillCapability = { ...commandSkillCapability, skillKey: 'script-entry-skill', slug: 'script-entry-skill', name: 'Script Entry Skill', baseDir: fixture.baseDir, manifestPath: `${fixture.baseDir}/SKILL.md`, commandExamples: [], }; const runtime = createChatToolRuntime([capability]); const result = await runtime.run({ toolCallId: 'tool-call-generic-command-script', toolName: 'script-entry-skill', input: { prompt: 'competitive review', }, }); expect(result.execution.ok).toBe(true); const executionRaw = result.execution.raw as { command: string; query: string; }; expect(executionRaw.command).toContain('run.js competitive review'); expect(executionRaw.query).toBe('competitive review'); }); it('keeps spreadsheet execution isolated from non-spreadsheet skills', async () => { const runtime = createChatToolRuntime([genericSearchCapability]); const result = await runtime.run({ toolCallId: 'tool-call-2', toolName: 'minimax-xlsx', input: { prompt: 'Analyze this workbook.', skillKey: 'minimax-xlsx', attachments: [ { fileName: 'hotel-sales.xls', filePath: 'F:\\Downloads\\hotel-sales.xls', mimeType: 'application/vnd.ms-excel', source: 'user-upload', }, ], }, }); expect(result.preflight.ok).toBe(false); expect(result.preflight.error).toEqual(expect.objectContaining({ code: 'missing_skill_runtime', })); }); it('surfaces missing env prerequisites for non-spreadsheet skills', async () => { const runtime = createChatToolRuntime([genericSearchCapability]); const result = await runtime.run({ toolCallId: 'tool-call-3', toolName: 'minimax-search', input: { prompt: 'Search for hotel sales anomalies.', }, }); expect(result.preflight.ok).toBe(false); expect(result.preflight.error).toEqual(expect.objectContaining({ code: 'missing_required_env', })); expect(result.execution.summary).toContain('ZN_AI_TEST_GENERIC_SKILL_ENV'); }); it('returns a capability-aware blocked result for unsupported generic skill runtimes', async () => { process.env.ZN_AI_TEST_GENERIC_SKILL_ENV = 'configured'; const runtime = createChatToolRuntime([ { ...genericSearchCapability, requiresAuth: false, requiredEnvVars: [], }, ]); const result = await runtime.run({ toolCallId: 'tool-call-4', toolName: 'minimax-search', input: { prompt: 'Search for hotel sales anomalies.', }, }); expect(result.preflight.ok).toBe(false); expect(result.preflight.error).toEqual(expect.objectContaining({ code: 'skill_runtime_not_implemented', })); expect(result.execution.summary).toContain('category=search'); expect(result.execution.summary).toContain('allowedTools=web.search'); }); });