mirror of
https://github.com/langgenius/dify.git
synced 2026-06-07 16:32:01 +08:00
204 lines
6.8 KiB
TypeScript
204 lines
6.8 KiB
TypeScript
import type { FormattedPrintable, NamePrintable, TablePrintable } from './output.js'
|
|
import { describe, expect, it } from 'vitest'
|
|
import { OutputFormatNotSupportedError } from './errors.js'
|
|
import {
|
|
formatted,
|
|
raw,
|
|
stringifyOutput,
|
|
table,
|
|
} from './output.js'
|
|
|
|
function makeFormatted(opts: { text?: string, json?: unknown, name?: string }): FormattedPrintable & NamePrintable {
|
|
return {
|
|
text: () => opts.text ?? 'hello',
|
|
json: () => opts.json ?? { msg: 'hello' },
|
|
name: () => opts.name ?? 'my-name',
|
|
}
|
|
}
|
|
|
|
function makeTable(opts: {
|
|
columns?: Array<{ name: string, priority: number }>
|
|
rows?: Array<Array<string | number | boolean | null | undefined>>
|
|
json?: unknown
|
|
name?: string
|
|
}): TablePrintable & NamePrintable {
|
|
return {
|
|
tableColumns: () => opts.columns ?? [
|
|
{ name: 'NAME', priority: 0 },
|
|
{ name: 'STATUS', priority: 0 },
|
|
],
|
|
tableRows: () => opts.rows ?? [['alice', 'active']],
|
|
json: () => opts.json ?? [{ name: 'alice', status: 'active' }],
|
|
name: () => opts.name ?? 'table-name',
|
|
}
|
|
}
|
|
|
|
describe('raw', () => {
|
|
it('creates RawOutput with kind=raw', () => {
|
|
const out = raw('hello\n')
|
|
expect(out.kind).toBe('raw')
|
|
expect(out.data).toBe('hello\n')
|
|
})
|
|
})
|
|
|
|
describe('table', () => {
|
|
it('creates TableOutput with kind=table', () => {
|
|
const data = makeTable({})
|
|
const out = table({ format: 'json', data })
|
|
expect(out.kind).toBe('table')
|
|
expect(out.format).toBe('json')
|
|
expect(out.data).toBe(data)
|
|
})
|
|
})
|
|
|
|
describe('formatted', () => {
|
|
it('creates FormattedOutput with kind=formatted', () => {
|
|
const data = makeFormatted({})
|
|
const out = formatted({ format: 'text', data })
|
|
expect(out.kind).toBe('formatted')
|
|
expect(out.format).toBe('text')
|
|
expect(out.data).toBe(data)
|
|
})
|
|
})
|
|
|
|
describe('stringifyOutput — raw', () => {
|
|
it('returns raw data as-is', () => {
|
|
expect(stringifyOutput(raw('abc\n'))).toBe('abc\n')
|
|
})
|
|
})
|
|
|
|
describe('stringifyOutput — formatted', () => {
|
|
it('text: calls data.text()', () => {
|
|
const out = formatted({ format: 'text', data: makeFormatted({ text: 'plain text\n' }) })
|
|
expect(stringifyOutput(out)).toBe('plain text\n')
|
|
})
|
|
|
|
it('empty format: calls data.text()', () => {
|
|
const out = formatted({ format: '', data: makeFormatted({ text: 'default\n' }) })
|
|
expect(stringifyOutput(out)).toBe('default\n')
|
|
})
|
|
|
|
it('json: serializes data.json() with 2-space indent + newline', () => {
|
|
const out = formatted({ format: 'json', data: makeFormatted({ json: { x: 1 } }) })
|
|
expect(stringifyOutput(out)).toBe(`${JSON.stringify({ x: 1 }, null, 2)}\n`)
|
|
})
|
|
|
|
it('yaml: renders YAML of data.json()', () => {
|
|
const out = formatted({ format: 'yaml', data: makeFormatted({ json: { x: 1 } }) })
|
|
const result = stringifyOutput(out)
|
|
expect(result).toContain('x: 1')
|
|
})
|
|
|
|
it('name: returns data.name() + newline', () => {
|
|
const out = formatted({ format: 'name', data: makeFormatted({ name: 'my-app' }) })
|
|
expect(stringifyOutput(out)).toBe('my-app\n')
|
|
})
|
|
|
|
it('name: throws when data has no name()', () => {
|
|
const noName: FormattedPrintable = {
|
|
text: () => 'txt',
|
|
json: () => ({}),
|
|
}
|
|
const out = formatted({ format: 'name', data: noName })
|
|
expect(() => stringifyOutput(out)).toThrow(OutputFormatNotSupportedError)
|
|
})
|
|
|
|
it('unknown format: throws with allowed list', () => {
|
|
const out = formatted({ format: 'csv', data: makeFormatted({}) })
|
|
expect(() => stringifyOutput(out)).toThrow(OutputFormatNotSupportedError)
|
|
})
|
|
})
|
|
|
|
describe('stringifyOutput — table', () => {
|
|
it('default format: renders tabular text with header row', () => {
|
|
const out = table({ format: '', data: makeTable({}) })
|
|
const result = stringifyOutput(out)
|
|
expect(result).toContain('NAME')
|
|
expect(result).toContain('STATUS')
|
|
expect(result).toContain('alice')
|
|
expect(result).toContain('active')
|
|
})
|
|
|
|
it('wide: includes all columns', () => {
|
|
const data = makeTable({
|
|
columns: [
|
|
{ name: 'NAME', priority: 0 },
|
|
{ name: 'EXTRA', priority: 1 },
|
|
],
|
|
rows: [['bob', 'hidden']],
|
|
})
|
|
const wide = table({ format: 'wide', data })
|
|
const narrow = table({ format: '', data })
|
|
expect(stringifyOutput(wide)).toContain('EXTRA')
|
|
expect(stringifyOutput(narrow)).not.toContain('EXTRA')
|
|
})
|
|
|
|
it('aligns columns correctly when cells contain CJK double-width characters', () => {
|
|
const data = makeTable({
|
|
columns: [
|
|
{ name: 'NAME', priority: 0 },
|
|
{ name: 'ID', priority: 0 },
|
|
],
|
|
rows: [
|
|
['hello', 'aaa'],
|
|
['猜谜', 'bbb'], // 猜谜 = 2 CJK chars, display width 4
|
|
],
|
|
})
|
|
const result = stringifyOutput(table({ format: '', data }))
|
|
const lines = result.split('\n').filter(l => l.length > 0)
|
|
// 'hello' display width 5, '猜谜' display width 4 — column width=5
|
|
// padding after 'hello': 5-5+2=2 spaces → 'hello aaa'
|
|
// padding after '猜谜': 5-4+2=3 spaces → '猜谜 bbb'
|
|
expect(lines[1]).toBe('hello aaa')
|
|
expect(lines[2]).toBe('猜谜 bbb')
|
|
})
|
|
|
|
it('json: serializes data.json() + newline', () => {
|
|
const out = table({ format: 'json', data: makeTable({ json: [{ id: 1 }] }) })
|
|
expect(stringifyOutput(out)).toBe(`${JSON.stringify([{ id: 1 }], null, 2)}\n`)
|
|
})
|
|
|
|
it('yaml: renders YAML of data.json()', () => {
|
|
const out = table({ format: 'yaml', data: makeTable({ json: [{ id: 1 }] }) })
|
|
expect(stringifyOutput(out)).toContain('id: 1')
|
|
})
|
|
|
|
it('name: returns data.name() + newline', () => {
|
|
const out = table({ format: 'name', data: makeTable({ name: 'row-name' }) })
|
|
expect(stringifyOutput(out)).toBe('row-name\n')
|
|
})
|
|
|
|
it('name: throws when data has no name()', () => {
|
|
const noName: TablePrintable = {
|
|
tableColumns: () => [],
|
|
tableRows: () => [],
|
|
json: () => [],
|
|
}
|
|
const out = table({ format: 'name', data: noName })
|
|
expect(() => stringifyOutput(out)).toThrow(OutputFormatNotSupportedError)
|
|
})
|
|
|
|
it('unknown format: throws with allowed list', () => {
|
|
const out = table({ format: 'csv', data: makeTable({}) })
|
|
expect(() => stringifyOutput(out)).toThrow(OutputFormatNotSupportedError)
|
|
})
|
|
|
|
it('table renders column padding correctly', () => {
|
|
const data = makeTable({
|
|
columns: [{ name: 'NAME', priority: 0 }, { name: 'ID', priority: 0 }],
|
|
rows: [['alice-longname', '1'], ['bob', '2']],
|
|
})
|
|
const result = stringifyOutput(table({ format: '', data }))
|
|
const lines = result.split('\n').filter(Boolean)
|
|
expect(lines).toHaveLength(3)
|
|
const headerParts = lines[0]!.split(/\s{2,}/)
|
|
expect(headerParts[0]).toBe('NAME')
|
|
expect(headerParts[1]).toBe('ID')
|
|
})
|
|
|
|
it('empty columns produces only a newline', () => {
|
|
const data = makeTable({ columns: [], rows: [] })
|
|
expect(stringifyOutput(table({ format: '', data }))).toBe('\n')
|
|
})
|
|
})
|