mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
fix(cli): make app-info cache resilient to corrupt entries + route errors through structured envelope (WTA-257) (#37852)
This commit is contained in:
1 parent
2112115962
commit
32dc9ff2d9
5 files changed
+144
-28
No files matched your search
@@ -212,18 +212,30 @@ describe('run() catch routing', () => {
|
||||
expect(result.exit).toBe(ExitCode.Generic)
|
||||
})
|
||||
|
||||
it('falls through to generic Error branch and exits 1', async () => {
|
||||
it('routes non-BaseError to JSON envelope with -o json (exit 1)', async () => {
|
||||
class Throwing extends Command {
|
||||
async run(_argv: string[]) {
|
||||
throw new Error('boom')
|
||||
}
|
||||
}
|
||||
const result = await captureRun(makeTree(Throwing), ['cmd', '-o', 'json'])
|
||||
expect(result.stderr).toBe(`${JSON.stringify({ error: { code: 'unknown', message: 'boom' } })}\n`)
|
||||
expect(result.exit).toBe(ExitCode.Generic)
|
||||
expect(result.stdout).toBe('')
|
||||
})
|
||||
|
||||
it('wraps a generic Error into the human unknown form and exits 1', async () => {
|
||||
class Throwing extends Command {
|
||||
async run(_argv: string[]) {
|
||||
throw new Error('oops')
|
||||
}
|
||||
}
|
||||
const result = await captureRun(makeTree(Throwing), ['cmd'])
|
||||
expect(result.stderr).toBe('oops\n')
|
||||
expect(result.exit).toBe(1)
|
||||
expect(result.stderr).toBe('unknown: oops\n')
|
||||
expect(result.exit).toBe(ExitCode.Generic)
|
||||
})
|
||||
|
||||
it('handles non-Error throw via String() coercion', async () => {
|
||||
it('wraps a non-Error throw via String() coercion into unknown form', async () => {
|
||||
class Throwing extends Command {
|
||||
async run(_argv: string[]) {
|
||||
// eslint-disable-next-line no-throw-literal
|
||||
@@ -231,8 +243,52 @@ describe('run() catch routing', () => {
|
||||
}
|
||||
}
|
||||
const result = await captureRun(makeTree(Throwing), ['cmd'])
|
||||
expect(result.stderr).toBe('plain string\n')
|
||||
expect(result.exit).toBe(1)
|
||||
expect(result.stderr).toBe('unknown: plain string\n')
|
||||
expect(result.exit).toBe(ExitCode.Generic)
|
||||
})
|
||||
|
||||
it('exits 0 on EPIPE without writing an error envelope', async () => {
|
||||
class Throwing extends Command {
|
||||
async run(_argv: string[]) {
|
||||
throw Object.assign(new Error('broken pipe'), { code: 'EPIPE' })
|
||||
}
|
||||
}
|
||||
// process.exit is typed `never`; stub it to halt (throw) like the real call,
|
||||
// so the EPIPE early-exit doesn't fall through to the envelope path.
|
||||
let exitCode: number | undefined
|
||||
let stderr = ''
|
||||
const origExit = process.exit.bind(process)
|
||||
const origStderr = process.stderr.write.bind(process.stderr)
|
||||
process.exit = ((code?: number) => {
|
||||
exitCode = code
|
||||
throw new Error('__exit__')
|
||||
}) as typeof process.exit
|
||||
process.stderr.write = ((chunk: string | Uint8Array) => {
|
||||
stderr += typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk)
|
||||
return true
|
||||
}) as typeof process.stderr.write
|
||||
try {
|
||||
await run(makeTree(Throwing), ['cmd', '-o', 'json'])
|
||||
}
|
||||
catch (e) {
|
||||
expect((e as Error).message).toBe('__exit__')
|
||||
}
|
||||
finally {
|
||||
process.exit = origExit
|
||||
process.stderr.write = origStderr
|
||||
}
|
||||
expect(exitCode).toBe(0)
|
||||
expect(stderr).toBe('')
|
||||
})
|
||||
|
||||
it('preserves RateLimited semantic exit code through the collapsed catch', async () => {
|
||||
class Throwing extends Command {
|
||||
async run(_argv: string[]) {
|
||||
throw newError(ErrorCode.RateLimited, 'slow down')
|
||||
}
|
||||
}
|
||||
const result = await captureRun(makeTree(Throwing), ['cmd'])
|
||||
expect(result.exit).toBe(ExitCode.RateLimited)
|
||||
})
|
||||
|
||||
it('does not call process.exit when command runs successfully', async () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CommandTree } from './registry'
|
||||
import { BaseError } from '@/errors/base'
|
||||
import { BaseError, unknownError } from '@/errors/base'
|
||||
import { formatErrorForCli } from '@/errors/format'
|
||||
import { findTopic } from '@/help/topics'
|
||||
import { formatCommandList, formatHelp, formatTopic, formatTopLevelHelp } from './help'
|
||||
@@ -106,19 +106,12 @@ export async function run(tree: CommandTree, argv: string[]): Promise<void> {
|
||||
catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code === 'EPIPE')
|
||||
process.exit(0)
|
||||
if (err instanceof BaseError) {
|
||||
const format = sniffOutputFormat(argv)
|
||||
process.stderr.write(`${formatErrorForCli(err, { format, isErrTTY: process.stderr.isTTY })}\n`)
|
||||
process.exit(err.exit())
|
||||
return
|
||||
}
|
||||
if (err instanceof Error) {
|
||||
process.stderr.write(`${err.message}\n`)
|
||||
process.exit(1)
|
||||
return
|
||||
}
|
||||
process.stderr.write(`${String(err)}\n`)
|
||||
process.exit(1)
|
||||
const e = err instanceof BaseError
|
||||
? err
|
||||
: unknownError(err instanceof Error ? err.message : String(err), err)
|
||||
const format = sniffOutputFormat(argv)
|
||||
process.stderr.write(`${formatErrorForCli(e, { format, isErrTTY: process.stderr.isTTY })}\n`)
|
||||
process.exit(e.exit())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user