mirror of
https://github.com/jackwener/OpenCLI.git
synced 2026-09-14 18:25:42 +08:00
dbac7fc921
* 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
122 lines
3.7 KiB
TypeScript
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');
|
|
});
|
|
});
|