Files
jackwener__opencli/src/errors.test.ts
jakevin dbac7fc921 refactor(errors): unify error output as YAML envelope to stderr (#923)
* refactor(errors): unify error output as YAML envelope to stderr

Replace the 100+ line chalk renderError() switch-case with a single
YAML envelope output path. All errors now output a structured
{ok, error: {code, message, help, exitCode}} envelope to stderr,
regardless of TTY status.

This simplifies the error system from 5 mechanisms to 3:
1. Error Envelope (YAML → stderr) — unified error output
2. Exit codes (sysexits.h) — process exit semantics
3. Diagnostic (OPENCLI_DIAGNOSTIC=1) — autofix repair context

Removed: chalk error rendering, ERROR_ICONS map, classifyGenericError
regex classifier, BrowserConnectError-specific bridge status display.
Added: toEnvelope() utility, ErrorEnvelope type.

* refactor(errors): migrate adapters to throw CliError, update docs

- Migrate xueqiu adapters from return [{error,help}] to throw CliError
- xueqiu/utils.ts: fetchXueqiuJson now throws AuthRequiredError/
  CommandExecutionError instead of returning {error, help} objects
- Remove resolveColumns error fallback from output.ts (no longer needed)
- Add verbose stack trace support to error envelope
- Add ADAPTER_LOAD to AutoFix hint trigger codes
- Update skill docs (adapter-templates, explorer, oneshot, advanced-patterns)
  to recommend throw CliError pattern instead of return [{error, help}]

* fix: remove remaining dead error-forwarding in 4 xueqiu adapters + review fixes

- Remove `if ('error' in d) return [d]` from feed, hot, search, kline
  (fetchXueqiuJson now throws, so these were dead code)
- Add `stack?: string` to ErrorEnvelope interface (removes type cast hack)
- Fix adapter-templates.md: use AuthRequiredError instead of plain Error

* fix: migrate barchart/quote and yahoo-finance/quote to throw CliError

Last two adapters that silently returned [] on error instead of
throwing CommandExecutionError.

* fix: self-review fixes — doc evaluate crash, error messages, kline consistency

- adapter-templates.md: getServerContext was throwing AuthRequiredError
  inside a function serialized into page.evaluate() (browser has no
  CliError). Reverted to return {error} sentinel + func() body throw.
- yahoo-finance/quote, barchart/quote: include symbol in fallback error msg
- xueqiu/kline: throw EmptyResultError instead of returning [] for
  consistency with other xueqiu adapters
2026-04-10 03:20:53 +08:00

122 lines
3.7 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import {
CliError,
BrowserConnectError,
AdapterLoadError,
CommandExecutionError,
ConfigError,
AuthRequiredError,
TimeoutError,
ArgumentError,
EmptyResultError,
SelectorError,
toEnvelope,
} from './errors.js';
describe('Error type hierarchy', () => {
it('all error types extend CliError', () => {
const errors = [
new BrowserConnectError('test'),
new AdapterLoadError('test'),
new CommandExecutionError('test'),
new ConfigError('test'),
new AuthRequiredError('example.com'),
new TimeoutError('test', 30),
new ArgumentError('test'),
new EmptyResultError('test/cmd'),
new SelectorError('.btn'),
];
for (const err of errors) {
expect(err).toBeInstanceOf(CliError);
expect(err).toBeInstanceOf(Error);
}
});
it('AuthRequiredError has correct code, domain, and auto-generated hint', () => {
const err = new AuthRequiredError('bilibili.com');
expect(err.code).toBe('AUTH_REQUIRED');
expect(err.domain).toBe('bilibili.com');
expect(err.message).toBe('Not logged in to bilibili.com');
expect(err.hint).toContain('https://bilibili.com');
});
it('AuthRequiredError accepts custom message', () => {
const err = new AuthRequiredError('x.com', 'No ct0 cookie found');
expect(err.message).toBe('No ct0 cookie found');
expect(err.hint).toContain('https://x.com');
});
it('TimeoutError has correct code and hint', () => {
const err = new TimeoutError('bilibili/hot', 60);
expect(err.code).toBe('TIMEOUT');
expect(err.message).toBe('bilibili/hot timed out after 60s');
expect(err.hint).toContain('timeout');
});
it('ArgumentError has correct code', () => {
const err = new ArgumentError('Argument "limit" must be a valid number');
expect(err.code).toBe('ARGUMENT');
});
it('EmptyResultError has default hint', () => {
const err = new EmptyResultError('hackernews/top');
expect(err.code).toBe('EMPTY_RESULT');
expect(err.message).toBe('hackernews/top returned no data');
expect(err.hint).toBeTruthy();
});
it('SelectorError has default hint about page changes', () => {
const err = new SelectorError('.submit-btn');
expect(err.code).toBe('SELECTOR');
expect(err.message).toContain('.submit-btn');
expect(err.hint).toContain('report');
});
it('BrowserConnectError has correct code', () => {
const err = new BrowserConnectError('Cannot connect');
expect(err.code).toBe('BROWSER_CONNECT');
});
});
describe('toEnvelope', () => {
it('converts CliError to structured envelope', () => {
const err = new AuthRequiredError('bilibili.com');
const envelope = toEnvelope(err);
expect(envelope).toEqual({
ok: false,
error: {
code: 'AUTH_REQUIRED',
message: 'Not logged in to bilibili.com',
help: expect.stringContaining('https://bilibili.com'),
exitCode: 77,
},
});
});
it('converts CliError without hint (omits help field)', () => {
const err = new CommandExecutionError('Something broke');
const envelope = toEnvelope(err);
expect(envelope.error.code).toBe('COMMAND_EXEC');
expect(envelope.error).not.toHaveProperty('help');
});
it('converts unknown Error to UNKNOWN envelope', () => {
const envelope = toEnvelope(new Error('random failure'));
expect(envelope).toEqual({
ok: false,
error: {
code: 'UNKNOWN',
message: 'random failure',
exitCode: 1,
},
});
});
it('converts non-Error values to UNKNOWN envelope', () => {
const envelope = toEnvelope('string error');
expect(envelope.error.code).toBe('UNKNOWN');
expect(envelope.error.message).toBe('string error');
});
});