mirror of
https://github.com/langgenius/dify.git
synced 2026-05-26 20:07:46 +08:00
Add comprehensive integration tests under cli/test/testcases/ covering: Discovery: - App list (list, single, all-workspaces) - Describe App - Cross-workspace query Run: - Basic App execution - Streaming output - HITL (Human-in-the-Loop) — all 19 cases incl. multi-action / expired-token / already-consumed - File input - Conversation mode - Environment variable injection - Cache and version consistency Output: - JSON/YAML output - Table output Error Handling: - Exit code end-to-end validation - Error message spec CLI Framework: - Global Flags - Non-Interactive mode Also extend test fixtures: - scenarios.ts: add hitl-pause-multi-action / hitl-resume-expired-token / hitl-resume-already-consumed - server.ts: add GET /form/human_input route, multi-action HITL response, expired/consumed token error handling Known bugs tracked as it.todo: - WTA-249: server 4xx in -o json mode exit code should be 1 (currently 0 in some cases) - WTA-252: --help missing GLOBAL FLAGS section and Quick start examples - WTA-255: hosts.yml YAML parse failure should output JSON envelope - WTA-257: uncaught TypeError should output JSON envelope in -o json mode
204 lines
5.4 KiB
TypeScript
204 lines
5.4 KiB
TypeScript
import yaml from 'js-yaml'
|
|
|
|
export type RawOutput = {
|
|
readonly kind: 'raw'
|
|
readonly data: string
|
|
}
|
|
|
|
export type TableCell = string | number | boolean | null | undefined
|
|
|
|
export type TableColumn = {
|
|
readonly name: string
|
|
readonly priority: number
|
|
}
|
|
|
|
export type TablePrintable = {
|
|
readonly tableColumns: () => readonly TableColumn[]
|
|
readonly tableRows: () => readonly (readonly TableCell[])[]
|
|
readonly json: () => unknown
|
|
}
|
|
|
|
export type FormattedPrintable = {
|
|
readonly text: () => string
|
|
readonly json: () => unknown
|
|
}
|
|
|
|
export type NamePrintable = {
|
|
readonly name: () => string
|
|
}
|
|
|
|
export type JsonPrintable = {
|
|
readonly json: () => unknown
|
|
}
|
|
|
|
export type TableOutput<TRow extends TablePrintable> = {
|
|
readonly kind: 'table'
|
|
readonly format: string
|
|
readonly data: TRow
|
|
}
|
|
|
|
export type FormattedOutput<TData extends FormattedPrintable> = {
|
|
readonly kind: 'formatted'
|
|
readonly format: string
|
|
readonly data: TData
|
|
}
|
|
|
|
export type CommandOutput = RawOutput | TableOutput<TablePrintable> | FormattedOutput<FormattedPrintable>
|
|
|
|
export function raw(data: string): RawOutput {
|
|
return { kind: 'raw', data }
|
|
}
|
|
|
|
export function table<TRow extends TablePrintable>(opts: {
|
|
readonly format: string
|
|
readonly data: TRow
|
|
}): TableOutput<TRow> {
|
|
return { kind: 'table', ...opts }
|
|
}
|
|
|
|
export function formatted<TData extends FormattedPrintable>(opts: {
|
|
readonly format: string
|
|
readonly data: TData
|
|
}): FormattedOutput<TData> {
|
|
return { kind: 'formatted', ...opts }
|
|
}
|
|
|
|
export function stringifyOutput(output: CommandOutput): string {
|
|
switch (output.kind) {
|
|
case 'raw':
|
|
return output.data
|
|
case 'table':
|
|
return stringifyTableOutput(output)
|
|
case 'formatted':
|
|
return stringifyFormattedOutput(output)
|
|
}
|
|
}
|
|
|
|
function stringifyFormattedOutput(output: FormattedOutput<FormattedPrintable>): string {
|
|
switch (output.format) {
|
|
case '':
|
|
case 'text':
|
|
return output.data.text()
|
|
default:
|
|
return stringifyJsonLike(output.format, output.data, 'json, name, text, yaml')
|
|
}
|
|
}
|
|
|
|
function stringifyTableOutput(output: TableOutput<TablePrintable>): string {
|
|
switch (output.format) {
|
|
case '':
|
|
case 'wide':
|
|
return renderTable(output)
|
|
default:
|
|
return stringifyJsonLike(output.format, output.data, 'json, name, wide, yaml')
|
|
}
|
|
}
|
|
|
|
function stringifyJsonLike(
|
|
format: string,
|
|
data: TablePrintable | FormattedPrintable,
|
|
allowed: string,
|
|
): string {
|
|
switch (format) {
|
|
case 'json':
|
|
return `${JSON.stringify(data.json(), null, 2)}\n`
|
|
case 'yaml':
|
|
return yaml.dump(data.json(), { indent: 2, lineWidth: -1 })
|
|
case 'name':
|
|
return `${toName(data)}\n`
|
|
default:
|
|
throw new Error(`output format ${JSON.stringify(format)} not supported, allowed: ${allowed}`)
|
|
}
|
|
}
|
|
|
|
function renderTable(output: TableOutput<TablePrintable>): string {
|
|
const wide = output.format === 'wide'
|
|
const columns = output.data.tableColumns()
|
|
const keep: number[] = []
|
|
for (let i = 0; i < columns.length; i++) {
|
|
const column = columns[i]
|
|
if (column !== undefined && (column.priority === 0 || wide))
|
|
keep.push(i)
|
|
}
|
|
|
|
const rows = [
|
|
keep.map(i => columns[i]?.name ?? ''),
|
|
...output.data.tableRows().map(row => keep.map((idx) => {
|
|
const cell = row[idx]
|
|
return cell === null || cell === undefined ? '' : String(cell)
|
|
})),
|
|
]
|
|
return formatTable(rows)
|
|
}
|
|
|
|
function isWideCodePoint(cp: number): boolean {
|
|
return (
|
|
(cp >= 0x1100 && cp <= 0x115F)
|
|
|| cp === 0x2329 || cp === 0x232A
|
|
|| (cp >= 0x2E80 && cp <= 0x3247)
|
|
|| (cp >= 0x3250 && cp <= 0x4DBF)
|
|
|| (cp >= 0x4E00 && cp <= 0xA4C6)
|
|
|| (cp >= 0xA960 && cp <= 0xA97C)
|
|
|| (cp >= 0xAC00 && cp <= 0xD7A3)
|
|
|| (cp >= 0xF900 && cp <= 0xFAFF)
|
|
|| (cp >= 0xFE10 && cp <= 0xFE19)
|
|
|| (cp >= 0xFE30 && cp <= 0xFE6B)
|
|
|| (cp >= 0xFF01 && cp <= 0xFF60)
|
|
|| (cp >= 0xFFE0 && cp <= 0xFFE6)
|
|
|| (cp >= 0x1B000 && cp <= 0x1B001)
|
|
|| (cp >= 0x1F200 && cp <= 0x1F251)
|
|
|| (cp >= 0x20000 && cp <= 0x3FFFD)
|
|
)
|
|
}
|
|
|
|
function displayWidth(s: string): number {
|
|
let w = 0
|
|
for (const ch of s)
|
|
w += isWideCodePoint(ch.codePointAt(0) ?? 0) ? 2 : 1
|
|
return w
|
|
}
|
|
|
|
function formatTable(rows: readonly (readonly string[])[]): string {
|
|
if (rows.length === 0)
|
|
return ''
|
|
const colCount = rows[0]?.length ?? 0
|
|
const widths: number[] = Array.from({ length: colCount }, () => 0)
|
|
const rowWidths: number[][] = []
|
|
for (const row of rows) {
|
|
const rw: number[] = Array.from({ length: colCount }, (_v, i) => displayWidth(row[i] ?? ''))
|
|
rowWidths.push(rw)
|
|
for (let i = 0; i < colCount; i++) {
|
|
const w = rw[i] ?? 0
|
|
if (w > (widths[i] ?? 0))
|
|
widths[i] = w
|
|
}
|
|
}
|
|
const lines = rows.map((row, rowIdx) => {
|
|
const rw = rowWidths[rowIdx] ?? []
|
|
const cells: string[] = []
|
|
for (let i = 0; i < colCount; i++) {
|
|
const cell = row[i] ?? ''
|
|
const isLast = i === colCount - 1
|
|
if (isLast) {
|
|
cells.push(cell)
|
|
}
|
|
else {
|
|
const pad = (widths[i] ?? 0) - (rw[i] ?? 0) + 2
|
|
cells.push(cell + ' '.repeat(pad))
|
|
}
|
|
}
|
|
return cells.join('')
|
|
})
|
|
return `${lines.join('\n')}\n`
|
|
}
|
|
|
|
function toName(data: TablePrintable | FormattedPrintable): string {
|
|
if (!isNamePrintable(data))
|
|
throw new Error('name output requires data.name()')
|
|
return data.name()
|
|
}
|
|
|
|
function isNamePrintable(data: TablePrintable | FormattedPrintable): data is (TablePrintable | FormattedPrintable) & NamePrintable {
|
|
return typeof (data as { name?: unknown }).name === 'function'
|
|
}
|