Skip to content

Commit 37189f7

Browse files
authored
fix(devnet): only show init hint for InitializationError (#408)
* feat(devnet): add InitializationError class for config errors * refactor(devnet): throw InitializationError for missing config files * fix(devnet): only show init hint for InitializationError * test(devnet): add tests for error classification * chore: add changeset for devnet config hint fix * test(devnet): reset mock implementation in beforeEach for test isolation
1 parent e90cfe5 commit 37189f7

4 files changed

Lines changed: 132 additions & 6 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@offckb/cli': patch
3+
---
4+
5+
fix(devnet): only show init hint for InitializationError
6+
7+
The `offckb devnet config` command was showing the "run `offckb node` once to initialize devnet config files first" hint for ALL errors, including user input errors like invalid `--set` syntax or validation failures.
8+
9+
Now the hint is only shown for actual initialization errors (missing config path, ckb.toml, or miner.toml), making error messages clearer and less misleading.
10+
11+
- Added `InitializationError` class to distinguish initialization errors from user input errors
12+
- Updated `createDevnetConfigEditor()` to throw `InitializationError` for missing files/paths
13+
- Modified `devnetConfig()` catch block to only show hint for `InitializationError`
14+
- Added type safety guard for error handling
15+
16+
Fixes #406

src/cmd/devnet-config.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { readSettings } from '../cfg/setting';
22
import { logger } from '../util/logger';
3-
import { createDevnetConfigEditor } from '../devnet/config-editor';
3+
import { createDevnetConfigEditor, InitializationError } from '../devnet/config-editor';
44
import { runDevnetConfigTui } from '../tui/devnet-config-tui';
55

66
export interface DevnetConfigOptions {
@@ -72,8 +72,13 @@ export async function devnetConfig(options: DevnetConfigOptions = {}) {
7272

7373
logger.info('No changes saved.');
7474
} catch (error) {
75-
logger.error((error as Error).message);
76-
logger.info('Tip: run `offckb node` once to initialize devnet config files first.');
75+
const message = error instanceof Error ? error.message : String(error);
76+
logger.error(message);
77+
78+
if (error instanceof InitializationError) {
79+
logger.info('Tip: run `offckb node` once to initialize devnet config files first.');
80+
}
81+
7782
process.exitCode = 1;
7883
}
7984
}

src/devnet/config-editor.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -679,17 +679,24 @@ export function createDevnetConfigEditor(configPath: string): DevnetConfigEditor
679679
const minerTomlPath = path.join(configPath, 'ckb-miner.toml');
680680

681681
if (!fs.existsSync(configPath)) {
682-
throw new Error(`Devnet config path does not exist: ${configPath}`);
682+
throw new InitializationError(`Devnet config path does not exist: ${configPath}`);
683683
}
684684
if (!fs.existsSync(ckbTomlPath)) {
685-
throw new Error(`Missing file: ${ckbTomlPath}`);
685+
throw new InitializationError(`Missing file: ${ckbTomlPath}`);
686686
}
687687
if (!fs.existsSync(minerTomlPath)) {
688-
throw new Error(`Missing file: ${minerTomlPath}`);
688+
throw new InitializationError(`Missing file: ${minerTomlPath}`);
689689
}
690690

691691
const ckbConfig = readTomlFile(ckbTomlPath);
692692
const minerConfig = readTomlFile(minerTomlPath);
693693

694694
return new DevnetConfigEditor(configPath, ckbConfig, minerConfig);
695695
}
696+
697+
export class InitializationError extends Error {
698+
constructor(message: string) {
699+
super(message);
700+
this.name = 'InitializationError';
701+
}
702+
}

tests/devnet-config-command.test.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ jest.mock('../src/cfg/setting', () => ({
1313

1414
jest.mock('../src/devnet/config-editor', () => ({
1515
createDevnetConfigEditor: jest.fn(),
16+
InitializationError: class InitializationError extends Error {
17+
constructor(message: string) {
18+
super(message);
19+
this.name = 'InitializationError';
20+
}
21+
},
1622
}));
1723

1824
jest.mock('../src/tui/devnet-config-tui', () => ({
@@ -78,3 +84,95 @@ describe('devnet config command fallback behavior', () => {
7884
expect(process.exitCode).toBe(1);
7985
});
8086
});
87+
88+
describe('error handling with init hint', () => {
89+
beforeEach(() => {
90+
jest.clearAllMocks();
91+
process.exitCode = undefined;
92+
(createDevnetConfigEditor as jest.Mock).mockReturnValue({
93+
setFieldValue: jest.fn(),
94+
save: jest.fn(),
95+
});
96+
});
97+
98+
afterEach(() => {
99+
process.exitCode = undefined;
100+
});
101+
102+
it('should NOT show init hint for parse errors (--set invalid)', async () => {
103+
await devnetConfig({ set: ['invalid'] });
104+
105+
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('Invalid --set item'));
106+
expect(logger.info).not.toHaveBeenCalledWith(
107+
'Tip: run `offckb node` once to initialize devnet config files first.',
108+
);
109+
expect(process.exitCode).toBe(1);
110+
});
111+
112+
it('should NOT show init hint for unknown field errors', async () => {
113+
(createDevnetConfigEditor as jest.Mock).mockImplementation(() => {
114+
throw new Error("Unknown field 'unknown.field'.");
115+
});
116+
117+
await devnetConfig({ set: ['unknown.field=value'] });
118+
119+
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('Unknown field'));
120+
expect(logger.info).not.toHaveBeenCalledWith(
121+
'Tip: run `offckb node` once to initialize devnet config files first.',
122+
);
123+
expect(process.exitCode).toBe(1);
124+
});
125+
126+
it('should NOT show init hint for validation errors', async () => {
127+
(createDevnetConfigEditor as jest.Mock).mockImplementation(() => {
128+
throw new Error('Value must be a positive integer.');
129+
});
130+
131+
await devnetConfig({ set: ['miner.client.poll_interval=0'] });
132+
133+
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('Value must be a positive integer'));
134+
expect(logger.info).not.toHaveBeenCalledWith(
135+
'Tip: run `offckb node` once to initialize devnet config files first.',
136+
);
137+
expect(process.exitCode).toBe(1);
138+
});
139+
140+
it('should show init hint for missing config path (InitializationError)', async () => {
141+
const { InitializationError } = require('../src/devnet/config-editor');
142+
(createDevnetConfigEditor as jest.Mock).mockImplementation(() => {
143+
throw new InitializationError('Devnet config path does not exist: /missing/path');
144+
});
145+
146+
await devnetConfig({ set: ['ckb.logger.filter=info'] });
147+
148+
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('Devnet config path does not exist'));
149+
expect(logger.info).toHaveBeenCalledWith('Tip: run `offckb node` once to initialize devnet config files first.');
150+
expect(process.exitCode).toBe(1);
151+
});
152+
153+
it('should show init hint for missing ckb.toml (InitializationError)', async () => {
154+
const { InitializationError } = require('../src/devnet/config-editor');
155+
(createDevnetConfigEditor as jest.Mock).mockImplementation(() => {
156+
throw new InitializationError('Missing file: /path/ckb.toml');
157+
});
158+
159+
await devnetConfig({ set: ['ckb.logger.filter=info'] });
160+
161+
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('Missing file'));
162+
expect(logger.info).toHaveBeenCalledWith('Tip: run `offckb node` once to initialize devnet config files first.');
163+
expect(process.exitCode).toBe(1);
164+
});
165+
166+
it('should show init hint for missing miner.toml (InitializationError)', async () => {
167+
const { InitializationError } = require('../src/devnet/config-editor');
168+
(createDevnetConfigEditor as jest.Mock).mockImplementation(() => {
169+
throw new InitializationError('Missing file: /path/ckb-miner.toml');
170+
});
171+
172+
await devnetConfig({ set: ['ckb.logger.filter=info'] });
173+
174+
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('Missing file'));
175+
expect(logger.info).toHaveBeenCalledWith('Tip: run `offckb node` once to initialize devnet config files first.');
176+
expect(process.exitCode).toBe(1);
177+
});
178+
});

0 commit comments

Comments
 (0)