mirror of
https://github.com/langgenius/dify.git
synced 2026-06-10 18:24:09 +08:00
156 lines
5.9 KiB
TypeScript
156 lines
5.9 KiB
TypeScript
/**
|
|
* E2E assertion helpers.
|
|
*
|
|
* These wrap vitest's `expect` with richer failure messages that include the
|
|
* full stdout / stderr of the failing process — essential for debugging CI.
|
|
*/
|
|
|
|
import type { RunResult } from './cli.js'
|
|
import { expect } from 'vitest'
|
|
import './vitest-context.js'
|
|
|
|
// ── ANSI ──────────────────────────────────────────────────────────────────
|
|
// eslint-disable-next-line no-control-regex
|
|
const ANSI_RE = /\x1B\[[0-9;]*[mGKHFA-DJsuhl]/g
|
|
|
|
function redact(text: string): string {
|
|
return text
|
|
.replace(/\bBearer\s+[\w.-]+\b/g, 'Bearer [REDACTED]')
|
|
.replace(/\bdfo[ae]_[\w-]+\b/g, 'dfo*_REDACTED')
|
|
}
|
|
|
|
// ── Exit code ─────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Assert the exit code matches `expected`.
|
|
* On failure, prints the full stdout and stderr so the cause is visible in CI.
|
|
*/
|
|
export function assertExitCode(result: RunResult, expected: number): void {
|
|
if (result.exitCode !== expected) {
|
|
process.stderr.write(
|
|
`\n[E2E assertExitCode] expected ${expected}, got ${result.exitCode}\n`
|
|
+ `stdout:\n${redact(result.stdout) || '(empty)'}\n`
|
|
+ `stderr:\n${redact(result.stderr) || '(empty)'}\n`,
|
|
)
|
|
}
|
|
expect(result.exitCode, `exit code should be ${expected}`).toBe(expected)
|
|
}
|
|
|
|
/**
|
|
* Assert the exit code is NOT 0 (i.e. some error occurred).
|
|
*/
|
|
export function assertNonZeroExit(result: RunResult): void {
|
|
expect(result.exitCode, 'exit code should be non-zero').not.toBe(0)
|
|
}
|
|
|
|
// ── Stdout / stderr content ───────────────────────────────────────────────
|
|
|
|
/**
|
|
* Assert stdout is valid JSON and return the parsed value.
|
|
*/
|
|
export function assertJson<T = unknown>(result: RunResult): T {
|
|
let parsed: T
|
|
try {
|
|
parsed = JSON.parse(result.stdout) as T
|
|
}
|
|
catch {
|
|
throw new Error(
|
|
`stdout is not valid JSON.\nstdout:\n${redact(result.stdout)}\nstderr:\n${redact(result.stderr)}`,
|
|
)
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
/**
|
|
* Assert stderr contains a valid JSON error envelope of the shape:
|
|
* { error: { code: string, message: string, hint?: string } }
|
|
*
|
|
* @param result - The run result to inspect.
|
|
* @param expectedCode - When provided, also asserts that error.code equals this value.
|
|
* Use the stable error codes from the CLI contract, e.g.:
|
|
* 'not_logged_in', 'app_not_found', 'insufficient_scope', 'auth_expired'
|
|
*
|
|
* @example
|
|
* assertErrorEnvelope(result, 'not_logged_in')
|
|
* assertErrorEnvelope(result, 'app_not_found')
|
|
*/
|
|
export function assertErrorEnvelope(
|
|
result: RunResult,
|
|
expectedCode?: string,
|
|
): { error: { code: string, message: string, hint?: string } } {
|
|
const raw = result.stderr.trim()
|
|
let parsed: { error: { code: string, message: string, hint?: string } }
|
|
try {
|
|
parsed = JSON.parse(raw) as typeof parsed
|
|
}
|
|
catch {
|
|
throw new Error(
|
|
`stderr is not valid JSON.\nstdout:\n${redact(result.stdout)}\nstderr:\n${redact(result.stderr)}`,
|
|
)
|
|
}
|
|
expect(parsed, 'stderr envelope missing "error" key').toHaveProperty('error')
|
|
expect(parsed.error, 'error.code must be a non-empty string').toHaveProperty('code')
|
|
expect(parsed.error, 'error.message must be a non-empty string').toHaveProperty('message')
|
|
expect(typeof parsed.error.code, 'error.code must be a string').toBe('string')
|
|
expect(parsed.error.code.length, 'error.code must be non-empty').toBeGreaterThan(0)
|
|
if (expectedCode !== undefined) {
|
|
expect(
|
|
parsed.error.code,
|
|
`error.code should be "${expectedCode}", got "${parsed.error.code}"\nstderr:\n${redact(result.stderr)}`,
|
|
).toBe(expectedCode)
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
// ── ANSI / formatting ────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Assert the given text contains no ANSI escape sequences.
|
|
* Pass `label` to identify which stream failed (e.g. 'stdout', 'stderr').
|
|
*/
|
|
export function assertNoAnsi(text: string, label = 'output'): void {
|
|
const clean = text.replace(ANSI_RE, '')
|
|
expect(text, `${label} must not contain ANSI control codes`).toBe(clean)
|
|
}
|
|
|
|
/**
|
|
* Assert stdout starts with `{` and ends with `\n` — the canonical format
|
|
* for pipe-friendly JSON output.
|
|
*/
|
|
export function assertPipeFriendlyJson(result: RunResult): void {
|
|
assertNoAnsi(result.stdout, 'stdout')
|
|
expect(
|
|
result.stdout.trimStart().startsWith('{') || result.stdout.trimStart().startsWith('['),
|
|
'stdout should start with { or [ for pipe-friendly JSON',
|
|
).toBe(true)
|
|
expect(result.stdout.endsWith('\n'), 'stdout should end with newline').toBe(true)
|
|
}
|
|
|
|
// ── stdout / stderr contains ──────────────────────────────────────────────
|
|
|
|
/**
|
|
* Assert stdout contains the given substring, printing full output on failure.
|
|
*/
|
|
export function assertStdoutContains(result: RunResult, expected: string): void {
|
|
if (!result.stdout.includes(expected)) {
|
|
process.stderr.write(
|
|
`\n[E2E assertStdoutContains] "${expected}" not found in stdout.\n`
|
|
+ `stdout:\n${redact(result.stdout)}\nstderr:\n${redact(result.stderr)}\n`,
|
|
)
|
|
}
|
|
expect(result.stdout).toContain(expected)
|
|
}
|
|
|
|
/**
|
|
* Assert stderr contains the given substring, printing full output on failure.
|
|
*/
|
|
export function assertStderrContains(result: RunResult, expected: string): void {
|
|
if (!result.stderr.includes(expected)) {
|
|
process.stderr.write(
|
|
`\n[E2E assertStderrContains] "${expected}" not found in stderr.\n`
|
|
+ `stdout:\n${redact(result.stdout)}\nstderr:\n${redact(result.stderr)}\n`,
|
|
)
|
|
}
|
|
expect(result.stderr).toContain(expected)
|
|
}
|