import { describe, expect, it, vi, afterEach } from 'vitest' import { Context } from '@deepseek-ai/cordis' import { CallId } from '@deepseek-ai/dsh-llm' import ToolRuntime from '@deepseek-ai/dsh-tools' import { FileSystem } from '@deepseek-ai/dsh-fs' import type { FsTarget, FsDirEntry, FsEditOutcome, FsEditRequest, FsInfo, FsPathInfo, FsWriteIntent, FsWriteOutcome, FsVersion, FsTargetKey } from '@deepseek-ai/dsh-fs' import * as ToolLab from '@deepseek-ai/dsh-tool-lab' import { runComfyImage, resolveImageArgs, buildWorkflow } from '@deepseek-ai/dsh-tool-lab/src/comfy.ts' import { runDoclingOcr } from '@deepseek-ai/dsh-tool-lab/src/docling.ts' import { runWhishTranscribe } from '@deepseek-ai/dsh-tool-lab/src/whish.ts' import type { SandboxExecutionPolicy, SandboxMode } from '@deepseek-ai/dsh-sandbox' const testToolSignal = new AbortController().signal /** In-memory fake ctx.fs backend for upload tools. */ class FakeFs extends FileSystem { files = new Map() override async resolve(path: string): Promise { return { targetKey: FsTargetKey(`key:${path}`), displayPath: `/abs/${path}` } } override processPath(target: FsTarget): string { return String(target.targetKey) } override fileUrl(target: FsTarget): string { return `file://${target.targetKey}` } override contains(parent: FsTarget, child: FsTarget): boolean { return child.targetKey === parent.targetKey || String(child.targetKey).startsWith(`${parent.targetKey}/`) } override async stat(): Promise { return undefined } override async lstat(): Promise { return undefined } override async readText(target: FsTarget): Promise { return new TextDecoder().decode(this.files.get(target.targetKey) ?? new Uint8Array()) } override async streamText(target: FsTarget): Promise> { const text = await this.readText(target) return (async function* () { yield text })() } override async readBytes(target: FsTarget, _signal: AbortSignal | undefined, maxBytes: number): Promise { const bytes = this.files.get(target.targetKey) ?? new Uint8Array() if (bytes.length > maxBytes) throw new Error('FS_TOO_LARGE') return bytes } override async listDir(): Promise { return [] } override async writeText(target: FsTarget, content: string): Promise { const before = this.files.get(target.targetKey) ?? null this.files.set(target.targetKey, new TextEncoder().encode(content)) return { operation: before !== null ? 'update' : 'create', version: FsVersion('v2'), before, after: content } } override async editText(target: FsTarget, edit: FsEditRequest): Promise { const content = new TextDecoder().decode(this.files.get(target.targetKey) ?? new Uint8Array()) const after = content.split(edit.oldString).join(edit.newString) this.files.set(target.targetKey, new TextEncoder().encode(after)) return { version: FsVersion('v3'), before: content, after } } } async function mount(opts: { files?: Record } = {}) { const ctx = new Context() await ctx.plugin(ToolRuntime) const fs = new FakeFs() if (opts.files) for (const [k, v] of Object.entries(opts.files)) fs.files.set(`key:${k}`, v) await ctx.plugin(fs, {}) await ctx.plugin(ToolLab, {}) let counter = 0 const call = (name: string, args: unknown) => ctx.tools.execute({ signal: testToolSignal, callId: CallId(`call-${++counter}`), name, arguments: args, }) return { ctx, fs, call } } function jsonResponse(body: unknown, { status = 200, headers }: { status?: number; headers?: Record } = {}): Response { return new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json', ...headers } }) } const pngBytes = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13]) afterEach(() => { vi.unstubAllGlobals() }) describe('lab_generate_image (ComfyUI)', () => { it('queues the workflow and returns the served PNG URL after polling history', async () => { const calls: string[] = [] vi.stubGlobal('fetch', vi.fn(async (input: RequestInfo | URL) => { const url = String(input) calls.push(url) if (url.endsWith('/prompt')) return jsonResponse({ prompt_id: 'p-1', node_errors: {} }) if (url.includes('/history/p-1')) return jsonResponse({ 'p-1': { outputs: { '9': { images: [{ filename: 'dsh_00001_.png' }] } } } }) throw new Error(`unexpected fetch ${url}`) })) const { fs } = await mountTarget({ files: { 'x.png': pngBytes } }) void fs const out = await runComfyImage('http://192.168.31.240:8188', resolveImageArgs({ prompt: 'a cat' }), undefined, 30_000) expect(out).toBe('http://192.168.31.240:8188/view?filename=dsh_00001_.png&subfolder=&type=output') expect(calls.some(u => u.endsWith('/prompt'))).toBe(true) }) it('rejects a workflow with non-empty node_errors', async () => { vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({ prompt_id: 'p-2', node_errors: { '3': ['bad'] } }))) await expect(runComfyImage('http://x', resolveImageArgs({ prompt: 'x' }), undefined, 30_000)) .rejects.toThrow(/workflow rejected/) }) it('rejects non-positive integer dimensions and steps', () => { expect(() => resolveImageArgs({ prompt: 'x', width: 0 })).toThrow(/width must be a positive integer/) expect(() => resolveImageArgs({ prompt: 'x', steps: -1 })).toThrow(/steps must be a positive integer/) }) it('uses the documented SDXL-turbo defaults in the workflow', () => { const wf = buildWorkflow({ prompt: 'p', negative: 'negative prompt', width: 512, height: 512, steps: 4, seed: 123, model: 'sdxl_turbo.safetensors', filenamePrefix: 'dsh' }) const sampler = wf['3'] as { inputs: Record } expect(sampler.inputs.cfg).toBe(1.0) expect(sampler.inputs.sampler_name).toBe('euler') expect(sampler.inputs.scheduler).toBe('normal') expect(sampler.inputs.steps).toBe(4) }) }) describe('lab_ocr_pdf (Docling)', () => { it('uploads a PDF and returns document.md_content after polling', async () => { vi.stubGlobal('fetch', vi.fn(async (input: RequestInfo | URL) => { const url = String(input) if (url.includes('/v1/convert/file/async')) return jsonResponse({ task_id: 't-1', task_status: 'pending' }) if (url.includes('/v1/status/poll/t-1')) return jsonResponse({ task_status: 'success' }) if (url.includes('/v1/result/t-1')) return jsonResponse({ status: 'success', errors: [], document: { filename: 'a.pdf', md_content: '# OCRed' } }) throw new Error(`unexpected ${url}`) })) const out = await runDoclingOcr('http://192.168.31.159:5001', new TextEncoder().encode('%PDF-1.4'), 'a.pdf', undefined, 30_000) expect(out).toBe('# OCRed') expect(globalThis.fetch).toHaveBeenCalled() }) it('rejects an empty task id', async () => { vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({ task_id: undefined }))) await expect(runDoclingOcr('http://x', new Uint8Array(), 'a.pdf', undefined, 30_000)).rejects.toThrow(/no task_id/) }) }) describe('lab_transcribe_audio (Whishper)', () => { it('polls until result.text is non-empty', async () => { let polls = 0 vi.stubGlobal('fetch', vi.fn(async (input: RequestInfo | URL) => { const url = String(input) if (url.endsWith('/api/transcriptions')) return jsonResponse({ id: 'w-1', status: -1 }) polls += 1 if (polls === 1) return jsonResponse({ id: 'w-1', status: -1, result: { text: '' } }) return jsonResponse({ id: 'w-1', status: 0, result: { text: 'hello world', language: 'en', duration: 1.2 } }) })) const out = await runWhishTranscribe('http://192.168.31.159:8082', new TextEncoder().encode('wav'), 'a.wav', undefined, undefined, undefined, 30_000) expect(out).toBe('hello world') }) it('rejects an upload with no id', async () => { vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({ id: undefined }))) await expect(runWhishTranscribe('http://x', new Uint8Array(), 'a.wav', undefined, undefined, undefined, 30_000)) .rejects.toThrow(/no id/) }) }) // keep imports referenced for the fake implementation; these types come from // @deepseek-ai/dsh-fs and are used by FakeFs above. type _ = SandboxExecutionPolicy | SandboxMode export { FakeFs }