shawnxixi-cli 0.3.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/.env.example +8 -2
  2. package/.github/workflows/ci.yml +9 -22
  3. package/.github/workflows/release.yml +30 -0
  4. package/CLAUDE.md +167 -63
  5. package/README.md +345 -65
  6. package/commitlint.config.js +10 -0
  7. package/dist/commands/biz/calendar.d.ts +17 -2
  8. package/dist/commands/biz/calendar.d.ts.map +1 -1
  9. package/dist/commands/biz/calendar.js +47 -9
  10. package/dist/commands/biz/calendar.js.map +1 -1
  11. package/dist/commands/biz/finance.d.ts +32 -1
  12. package/dist/commands/biz/finance.d.ts.map +1 -1
  13. package/dist/commands/biz/finance.js +99 -12
  14. package/dist/commands/biz/finance.js.map +1 -1
  15. package/dist/commands/biz/health.d.ts +32 -0
  16. package/dist/commands/biz/health.d.ts.map +1 -0
  17. package/dist/commands/biz/health.js +92 -0
  18. package/dist/commands/biz/health.js.map +1 -0
  19. package/dist/commands/biz/item.d.ts +41 -0
  20. package/dist/commands/biz/item.d.ts.map +1 -1
  21. package/dist/commands/biz/item.js +89 -5
  22. package/dist/commands/biz/item.js.map +1 -1
  23. package/dist/commands/biz/record.d.ts +0 -8
  24. package/dist/commands/biz/record.d.ts.map +1 -1
  25. package/dist/commands/biz/record.js +29 -8
  26. package/dist/commands/biz/record.js.map +1 -1
  27. package/dist/commands/biz/task.d.ts +38 -0
  28. package/dist/commands/biz/task.d.ts.map +1 -0
  29. package/dist/commands/biz/task.js +110 -0
  30. package/dist/commands/biz/task.js.map +1 -0
  31. package/dist/commands/biz/todo.d.ts +52 -8
  32. package/dist/commands/biz/todo.d.ts.map +1 -1
  33. package/dist/commands/biz/todo.js +167 -17
  34. package/dist/commands/biz/todo.js.map +1 -1
  35. package/dist/commands/sys/health.d.ts.map +1 -1
  36. package/dist/commands/sys/health.js +20 -3
  37. package/dist/commands/sys/health.js.map +1 -1
  38. package/dist/index.d.ts +5 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +281 -42
  41. package/dist/index.js.map +1 -1
  42. package/dist/services/api.d.ts +22 -8
  43. package/dist/services/api.d.ts.map +1 -1
  44. package/dist/services/api.js +108 -28
  45. package/dist/services/api.js.map +1 -1
  46. package/dist/utils/env.d.ts +16 -0
  47. package/dist/utils/env.d.ts.map +1 -0
  48. package/dist/utils/env.js +78 -0
  49. package/dist/utils/env.js.map +1 -0
  50. package/dist/utils/finance.model.d.ts +80 -0
  51. package/dist/utils/finance.model.d.ts.map +1 -0
  52. package/dist/utils/finance.model.js +150 -0
  53. package/dist/utils/finance.model.js.map +1 -0
  54. package/dist/utils/output.d.ts +42 -0
  55. package/dist/utils/output.d.ts.map +1 -0
  56. package/dist/utils/output.js +124 -0
  57. package/dist/utils/output.js.map +1 -0
  58. package/dist/utils/todo.model.d.ts +55 -0
  59. package/dist/utils/todo.model.d.ts.map +1 -0
  60. package/dist/utils/todo.model.js +135 -0
  61. package/dist/utils/todo.model.js.map +1 -0
  62. package/package.json +18 -2
  63. package/src/commands/biz/calendar.ts +61 -10
  64. package/src/commands/biz/finance.ts +112 -13
  65. package/src/commands/biz/health.ts +96 -0
  66. package/src/commands/biz/item.ts +113 -6
  67. package/src/commands/biz/record.ts +32 -8
  68. package/src/commands/biz/task.ts +115 -0
  69. package/src/commands/biz/todo.ts +193 -21
  70. package/src/commands/sys/health.ts +23 -3
  71. package/src/index.ts +311 -54
  72. package/src/services/api.ts +111 -30
  73. package/src/utils/env.ts +85 -0
  74. package/src/utils/finance.model.ts +182 -0
  75. package/src/utils/output.ts +126 -0
  76. package/src/utils/todo.model.ts +167 -0
  77. package/tests/commands/finance.test.ts +281 -0
  78. package/tests/commands/item.test.ts +215 -0
  79. package/tests/commands/todo.test.ts +292 -9
  80. package/tests/services/api.test.ts +292 -20
  81. package/tests/utils/finance.model.test.ts +319 -0
  82. package/tests/utils/todo.model.test.ts +315 -0
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Todo Model Utilities
3
+ * 包含重复规则计算、标签解析、时长格式化等功能
4
+ */
5
+
6
+ export type RepeatType = 'none' | 'daily' | 'weekly' | 'monthly';
7
+
8
+ export interface RepeatRule {
9
+ type: RepeatType;
10
+ interval: number; // 重复间隔,如 2 表示每2天/周/月
11
+ endDate?: string; // 重复结束日期
12
+ }
13
+
14
+ /**
15
+ * 解析重复规则字符串
16
+ * 格式: "daily", "weekly", "monthly", "2d", "3w", "1m" 等
17
+ */
18
+ export function parseRepeatRule(rule: string): RepeatRule {
19
+ if (!rule || rule === 'none') {
20
+ return { type: 'none', interval: 1 };
21
+ }
22
+
23
+ const lower = rule.toLowerCase();
24
+
25
+ if (lower === 'daily') {
26
+ return { type: 'daily', interval: 1 };
27
+ }
28
+ if (lower === 'weekly') {
29
+ return { type: 'weekly', interval: 1 };
30
+ }
31
+ if (lower === 'monthly') {
32
+ return { type: 'monthly', interval: 1 };
33
+ }
34
+
35
+ // 解析 "2d", "3w", "1m" 格式
36
+ const match = rule.match(/^(\d+)([dwm])$/i);
37
+ if (match) {
38
+ const num = parseInt(match[1], 10);
39
+ const unit = match[2].toLowerCase();
40
+ switch (unit) {
41
+ case 'd':
42
+ return { type: 'daily', interval: num };
43
+ case 'w':
44
+ return { type: 'weekly', interval: num };
45
+ case 'm':
46
+ return { type: 'monthly', interval: num };
47
+ }
48
+ }
49
+
50
+ return { type: 'none', interval: 1 };
51
+ }
52
+
53
+ /**
54
+ * 计算下一次重复日期
55
+ */
56
+ export function calculateNextOccurrence(
57
+ currentDate: Date,
58
+ rule: RepeatRule
59
+ ): Date | null {
60
+ if (rule.type === 'none') {
61
+ return null;
62
+ }
63
+
64
+ const next = new Date(currentDate);
65
+
66
+ switch (rule.type) {
67
+ case 'daily':
68
+ next.setDate(next.getDate() + rule.interval);
69
+ break;
70
+ case 'weekly':
71
+ next.setDate(next.getDate() + 7 * rule.interval);
72
+ break;
73
+ case 'monthly':
74
+ next.setMonth(next.getMonth() + rule.interval);
75
+ break;
76
+ }
77
+
78
+ // 检查是否超过结束日期
79
+ if (rule.endDate) {
80
+ const endDate = new Date(rule.endDate);
81
+ if (next > endDate) {
82
+ return null;
83
+ }
84
+ }
85
+
86
+ return next;
87
+ }
88
+
89
+ /**
90
+ * 解析标签字符串
91
+ * 格式: "work,important,urgent" -> ["work", "important", "urgent"]
92
+ */
93
+ export function parseTags(tagsString: string | undefined): string[] {
94
+ if (!tagsString || tagsString.trim() === '') {
95
+ return [];
96
+ }
97
+ return tagsString
98
+ .split(',')
99
+ .map(tag => tag.trim())
100
+ .filter(tag => tag.length > 0);
101
+ }
102
+
103
+ /**
104
+ * 格式化时长
105
+ * 秒数转换为可读格式: "1h30m", "45m", "2h"
106
+ */
107
+ export function formatDuration(seconds: number): string {
108
+ if (seconds < 0) {
109
+ return '0m';
110
+ }
111
+
112
+ const hours = Math.floor(seconds / 3600);
113
+ const minutes = Math.floor((seconds % 3600) / 60);
114
+
115
+ if (hours === 0) {
116
+ return `${minutes}m`;
117
+ }
118
+ if (minutes === 0) {
119
+ return `${hours}h`;
120
+ }
121
+ return `${hours}h${minutes}m`;
122
+ }
123
+
124
+ /**
125
+ * 解析时长字符串为秒数
126
+ * 格式: "1h30m", "45m", "2h" -> 秒数
127
+ */
128
+ export function parseDuration(duration: string): number {
129
+ const hourMatch = duration.match(/(\d+)h/);
130
+ const minMatch = duration.match(/(\d+)m/);
131
+
132
+ const hours = hourMatch ? parseInt(hourMatch[1], 10) : 0;
133
+ const minutes = minMatch ? parseInt(minMatch[1], 10) : 0;
134
+
135
+ return hours * 3600 + minutes * 60;
136
+ }
137
+
138
+ export interface TodoItem {
139
+ id: string;
140
+ title: string;
141
+ completed: boolean;
142
+ priority: 'low' | 'medium' | 'high';
143
+ dueDate?: string;
144
+ tags?: string[];
145
+ repeatRule?: RepeatRule;
146
+ estimatedDuration?: number; // 秒
147
+ createdAt: string;
148
+ completedAt?: string;
149
+ }
150
+
151
+ /**
152
+ * 序列化 Todo 为 JSON
153
+ */
154
+ export function serializeTodo(todo: TodoItem): string {
155
+ return JSON.stringify(todo);
156
+ }
157
+
158
+ /**
159
+ * 反序列化 Todo
160
+ */
161
+ export function deserializeTodo(json: string): TodoItem | null {
162
+ try {
163
+ return JSON.parse(json) as TodoItem;
164
+ } catch {
165
+ return null;
166
+ }
167
+ }
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Finance Command Tests
3
+ * Comprehensive CLI integration tests for biz finance commands
4
+ */
5
+
6
+ import { financeCommands } from '../../src/commands/biz/finance';
7
+ import { setGlobalFlags } from '../../src/utils/output';
8
+
9
+ // Mock apiService
10
+ jest.mock('../../src/services/api', () => ({
11
+ apiService: {
12
+ createFinance: jest.fn().mockResolvedValue({ id: 1, amount: 100 }),
13
+ listFinances: jest.fn().mockResolvedValue({ data: [{ id: 1, amount: 100 }] }),
14
+ getFinanceStats: jest.fn().mockResolvedValue({ totalIncome: 10000, totalExpense: 5000 }),
15
+ getFinanceReport: jest.fn().mockResolvedValue({ report: 'test' }),
16
+ },
17
+ }));
18
+
19
+ import { apiService } from '../../src/services/api';
20
+
21
+ // Mock process.exit to prevent test from exiting
22
+ const mockExit = jest.spyOn(process, 'exit').mockImplementation((() => {}) as any);
23
+
24
+ // Suppress console output during tests
25
+ beforeAll(() => {
26
+ setGlobalFlags({ quiet: true });
27
+ });
28
+
29
+ afterAll(() => {
30
+ setGlobalFlags({ quiet: false });
31
+ mockExit.mockRestore();
32
+ });
33
+
34
+ describe('biz finance commands', () => {
35
+ beforeEach(() => {
36
+ jest.clearAllMocks();
37
+ });
38
+
39
+ describe('create', () => {
40
+ it('should call apiService.createFinance with amount only', async () => {
41
+ await financeCommands.create('100');
42
+ expect(apiService.createFinance).toHaveBeenCalledWith(
43
+ 100,
44
+ 'income', // positive amounts default to income
45
+ 'other',
46
+ undefined, // note
47
+ 'cash', // account default
48
+ undefined, // date
49
+ undefined // tags
50
+ );
51
+ });
52
+
53
+ it('should call apiService.createFinance with expense type', async () => {
54
+ await financeCommands.create('100', { type: 'expense' });
55
+ expect(apiService.createFinance).toHaveBeenCalledWith(
56
+ 100,
57
+ 'expense',
58
+ 'other',
59
+ undefined,
60
+ 'cash',
61
+ undefined,
62
+ undefined
63
+ );
64
+ });
65
+
66
+ it('should call apiService.createFinance with income type', async () => {
67
+ await financeCommands.create('1000', { type: 'income' });
68
+ expect(apiService.createFinance).toHaveBeenCalledWith(
69
+ 1000,
70
+ 'income',
71
+ 'other',
72
+ undefined,
73
+ 'cash',
74
+ undefined,
75
+ undefined
76
+ );
77
+ });
78
+
79
+ it('should call apiService.createFinance with category', async () => {
80
+ await financeCommands.create('100', { category: 'food' });
81
+ expect(apiService.createFinance).toHaveBeenCalledWith(
82
+ 100,
83
+ 'income',
84
+ 'food',
85
+ undefined,
86
+ 'cash',
87
+ undefined,
88
+ undefined
89
+ );
90
+ });
91
+
92
+ it('should call apiService.createFinance with note', async () => {
93
+ await financeCommands.create('100', { note: '午饭' });
94
+ expect(apiService.createFinance).toHaveBeenCalledWith(
95
+ 100,
96
+ 'income',
97
+ 'other',
98
+ '午饭',
99
+ 'cash',
100
+ undefined,
101
+ undefined
102
+ );
103
+ });
104
+
105
+ it('should call apiService.createFinance with account', async () => {
106
+ await financeCommands.create('100', { account: 'bank' });
107
+ expect(apiService.createFinance).toHaveBeenCalledWith(
108
+ 100,
109
+ 'income',
110
+ 'other',
111
+ undefined,
112
+ 'bank',
113
+ undefined,
114
+ undefined
115
+ );
116
+ });
117
+
118
+ it('should call apiService.createFinance with date', async () => {
119
+ await financeCommands.create('100', { date: '2026-04-01' });
120
+ expect(apiService.createFinance).toHaveBeenCalledWith(
121
+ 100,
122
+ 'income',
123
+ 'other',
124
+ undefined,
125
+ 'cash',
126
+ '2026-04-01',
127
+ undefined
128
+ );
129
+ });
130
+
131
+ it('should call apiService.createFinance with tags', async () => {
132
+ await financeCommands.create('100', { tags: 'food,lunch' });
133
+ expect(apiService.createFinance).toHaveBeenCalledWith(
134
+ 100,
135
+ 'income',
136
+ 'other',
137
+ undefined,
138
+ 'cash',
139
+ undefined,
140
+ 'food,lunch'
141
+ );
142
+ });
143
+
144
+ it('should call apiService.createFinance with all options', async () => {
145
+ await financeCommands.create('500', { type: 'income', category: 'salary', note: '月薪', account: 'bank', date: '2026-04-01', tags: 'monthly' });
146
+ expect(apiService.createFinance).toHaveBeenCalledWith(
147
+ 500,
148
+ 'income',
149
+ 'salary',
150
+ '月薪',
151
+ 'bank',
152
+ '2026-04-01',
153
+ 'monthly'
154
+ );
155
+ });
156
+
157
+ it('should treat negative amounts as expense', async () => {
158
+ await financeCommands.create('-50');
159
+ expect(apiService.createFinance).toHaveBeenCalledWith(
160
+ 50,
161
+ 'expense',
162
+ 'other',
163
+ undefined,
164
+ 'cash',
165
+ undefined,
166
+ undefined
167
+ );
168
+ });
169
+
170
+ it('should reject invalid amount', async () => {
171
+ await financeCommands.create('invalid');
172
+ expect(mockExit).toHaveBeenCalledWith(1);
173
+ });
174
+
175
+ it('should handle api errors gracefully', async () => {
176
+ (apiService.createFinance as jest.Mock).mockRejectedValueOnce(new Error('API Error'));
177
+
178
+ await financeCommands.create('100');
179
+
180
+ expect(mockExit).toHaveBeenCalledWith(1);
181
+ });
182
+ });
183
+
184
+ describe('list', () => {
185
+ it('should call apiService.listFinances without filters', async () => {
186
+ await financeCommands.list();
187
+ expect(apiService.listFinances).toHaveBeenCalledWith(undefined, undefined, undefined);
188
+ });
189
+
190
+ it('should call apiService.listFinances with type filter', async () => {
191
+ await financeCommands.list({ type: 'expense' });
192
+ expect(apiService.listFinances).toHaveBeenCalledWith('expense', undefined, undefined);
193
+ });
194
+
195
+ it('should call apiService.listFinances with category filter', async () => {
196
+ await financeCommands.list({ category: 'food' });
197
+ expect(apiService.listFinances).toHaveBeenCalledWith(undefined, 'food', undefined);
198
+ });
199
+
200
+ it('should call apiService.listFinances with month filter', async () => {
201
+ await financeCommands.list({ month: '2026-04' });
202
+ expect(apiService.listFinances).toHaveBeenCalledWith(undefined, undefined, '2026-04');
203
+ });
204
+
205
+ it('should call apiService.listFinances with all filters', async () => {
206
+ await financeCommands.list({ type: 'expense', category: 'food', month: '2026-04' });
207
+ expect(apiService.listFinances).toHaveBeenCalledWith('expense', 'food', '2026-04');
208
+ });
209
+
210
+ it('should handle empty results', async () => {
211
+ (apiService.listFinances as jest.Mock).mockResolvedValueOnce({ data: [] });
212
+
213
+ await financeCommands.list();
214
+
215
+ expect(apiService.listFinances).toHaveBeenCalled();
216
+ });
217
+
218
+ it('should handle api errors gracefully', async () => {
219
+ (apiService.listFinances as jest.Mock).mockRejectedValueOnce(new Error('Network Error'));
220
+
221
+ await financeCommands.list();
222
+
223
+ expect(mockExit).toHaveBeenCalledWith(1);
224
+ });
225
+ });
226
+
227
+ describe('stats', () => {
228
+ it('should call apiService.getFinanceStats with default month', async () => {
229
+ const currentMonth = new Date().toISOString().slice(0, 7);
230
+
231
+ await financeCommands.stats();
232
+
233
+ expect(apiService.getFinanceStats).toHaveBeenCalledWith(currentMonth);
234
+ });
235
+
236
+ it('should call apiService.getFinanceStats with specified month', async () => {
237
+ await financeCommands.stats({ month: '2026-03' });
238
+ expect(apiService.getFinanceStats).toHaveBeenCalledWith('2026-03');
239
+ });
240
+
241
+ it('should return stats data', async () => {
242
+ const stats = { totalIncome: 10000, totalExpense: 5000, netBalance: 5000 };
243
+ (apiService.getFinanceStats as jest.Mock).mockResolvedValueOnce(stats);
244
+
245
+ await financeCommands.stats();
246
+
247
+ expect(apiService.getFinanceStats).toHaveBeenCalled();
248
+ });
249
+
250
+ it('should handle api errors gracefully', async () => {
251
+ (apiService.getFinanceStats as jest.Mock).mockRejectedValueOnce(new Error('API Error'));
252
+
253
+ await financeCommands.stats();
254
+
255
+ expect(mockExit).toHaveBeenCalledWith(1);
256
+ });
257
+ });
258
+
259
+ describe('report', () => {
260
+ it('should call apiService.getFinanceReport with default month', async () => {
261
+ const currentMonth = new Date().toISOString().slice(0, 7);
262
+
263
+ await financeCommands.report();
264
+
265
+ expect(apiService.getFinanceReport).toHaveBeenCalledWith(currentMonth);
266
+ });
267
+
268
+ it('should call apiService.getFinanceReport with specified month', async () => {
269
+ await financeCommands.report({ month: '2026-03' });
270
+ expect(apiService.getFinanceReport).toHaveBeenCalledWith('2026-03');
271
+ });
272
+
273
+ it('should handle api errors gracefully', async () => {
274
+ (apiService.getFinanceReport as jest.Mock).mockRejectedValueOnce(new Error('API Error'));
275
+
276
+ await financeCommands.report();
277
+
278
+ expect(mockExit).toHaveBeenCalledWith(1);
279
+ });
280
+ });
281
+ });
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Item Command Tests
3
+ * Comprehensive CLI integration tests for biz item commands
4
+ */
5
+
6
+ import { itemCommands } from '../../src/commands/biz/item';
7
+ import { setGlobalFlags } from '../../src/utils/output';
8
+
9
+ // Mock apiService
10
+ jest.mock('../../src/services/api', () => ({
11
+ apiService: {
12
+ createItem: jest.fn().mockResolvedValue({ id: 1, name: '测试物品' }),
13
+ listItems: jest.fn().mockResolvedValue({ data: [{ id: 1, name: '测试物品' }] }),
14
+ getExpiringItems: jest.fn().mockResolvedValue({ data: [] }),
15
+ getItemStats: jest.fn().mockResolvedValue({ total: 0 }),
16
+ },
17
+ }));
18
+
19
+ import { apiService } from '../../src/services/api';
20
+
21
+ // Mock process.exit to prevent test from exiting
22
+ const mockExit = jest.spyOn(process, 'exit').mockImplementation((() => {}) as any);
23
+
24
+ // Suppress console output during tests
25
+ beforeAll(() => {
26
+ setGlobalFlags({ quiet: true });
27
+ });
28
+
29
+ afterAll(() => {
30
+ setGlobalFlags({ quiet: false });
31
+ mockExit.mockRestore();
32
+ });
33
+
34
+ describe('biz item commands', () => {
35
+ beforeEach(() => {
36
+ jest.clearAllMocks();
37
+ });
38
+
39
+ describe('create', () => {
40
+ it('should call apiService.createItem with name only', async () => {
41
+ await itemCommands.create('测试物品');
42
+ expect(apiService.createItem).toHaveBeenCalledWith(
43
+ '测试物品',
44
+ undefined, // category
45
+ undefined, // location
46
+ undefined, // expire
47
+ undefined, // quantity
48
+ undefined, // unit
49
+ undefined, // brand
50
+ undefined, // barcode
51
+ undefined, // price
52
+ undefined, // supplier
53
+ undefined // purchaseDate
54
+ );
55
+ });
56
+
57
+ it('should call apiService.createItem with category', async () => {
58
+ await itemCommands.create('测试物品', { category: 'electronics' });
59
+ expect(apiService.createItem).toHaveBeenCalledWith(
60
+ '测试物品',
61
+ 'electronics',
62
+ undefined,
63
+ undefined,
64
+ undefined,
65
+ undefined,
66
+ undefined,
67
+ undefined,
68
+ undefined,
69
+ undefined,
70
+ undefined
71
+ );
72
+ });
73
+
74
+ it('should call apiService.createItem with location', async () => {
75
+ await itemCommands.create('测试物品', { location: '客厅' });
76
+ expect(apiService.createItem).toHaveBeenCalledWith(
77
+ '测试物品',
78
+ undefined,
79
+ '客厅',
80
+ undefined,
81
+ undefined,
82
+ undefined,
83
+ undefined,
84
+ undefined,
85
+ undefined,
86
+ undefined,
87
+ undefined
88
+ );
89
+ });
90
+
91
+ it('should call apiService.createItem with expire date', async () => {
92
+ await itemCommands.create('测试物品', { expire: '2026-12-31' });
93
+ expect(apiService.createItem).toHaveBeenCalledWith(
94
+ '测试物品',
95
+ undefined,
96
+ undefined,
97
+ '2026-12-31',
98
+ undefined,
99
+ undefined,
100
+ undefined,
101
+ undefined,
102
+ undefined,
103
+ undefined,
104
+ undefined
105
+ );
106
+ });
107
+
108
+ it('should call apiService.createItem with all options', async () => {
109
+ await itemCommands.create('测试物品', {
110
+ category: 'electronics',
111
+ location: '客厅',
112
+ expire: '2026-12-31',
113
+ quantity: 2,
114
+ unit: '个',
115
+ brand: 'Apple',
116
+ barcode: '123456789',
117
+ price: 99.99,
118
+ supplier: '供应商A',
119
+ purchaseDate: '2026-01-01',
120
+ });
121
+ expect(apiService.createItem).toHaveBeenCalledWith(
122
+ '测试物品',
123
+ 'electronics',
124
+ '客厅',
125
+ '2026-12-31',
126
+ 2,
127
+ '个',
128
+ 'Apple',
129
+ '123456789',
130
+ 99.99,
131
+ '供应商A',
132
+ '2026-01-01'
133
+ );
134
+ });
135
+
136
+ it('should handle api errors gracefully', async () => {
137
+ (apiService.createItem as jest.Mock).mockRejectedValueOnce(new Error('API Error'));
138
+
139
+ await itemCommands.create('测试物品');
140
+
141
+ expect(mockExit).toHaveBeenCalledWith(1);
142
+ });
143
+ });
144
+
145
+ describe('list', () => {
146
+ it('should call apiService.listItems without filters', async () => {
147
+ await itemCommands.list();
148
+ expect(apiService.listItems).toHaveBeenCalledWith(undefined);
149
+ });
150
+
151
+ it('should call apiService.listItems with category filter', async () => {
152
+ await itemCommands.list({ category: 'electronics' });
153
+ expect(apiService.listItems).toHaveBeenCalledWith('electronics');
154
+ });
155
+
156
+ it('should handle empty results', async () => {
157
+ (apiService.listItems as jest.Mock).mockResolvedValueOnce({ data: [] });
158
+
159
+ await itemCommands.list();
160
+
161
+ expect(apiService.listItems).toHaveBeenCalled();
162
+ });
163
+
164
+ it('should handle api errors gracefully', async () => {
165
+ (apiService.listItems as jest.Mock).mockRejectedValueOnce(new Error('Network Error'));
166
+
167
+ await itemCommands.list();
168
+
169
+ expect(mockExit).toHaveBeenCalledWith(1);
170
+ });
171
+ });
172
+
173
+ describe('expiring', () => {
174
+ it('should call apiService.getExpiringItems with default days', async () => {
175
+ await itemCommands.expiring();
176
+ expect(apiService.getExpiringItems).toHaveBeenCalledWith(7);
177
+ });
178
+
179
+ it('should call apiService.getExpiringItems with custom days', async () => {
180
+ await itemCommands.expiring({ days: 14 });
181
+ expect(apiService.getExpiringItems).toHaveBeenCalledWith(14);
182
+ });
183
+
184
+ it('should handle empty results', async () => {
185
+ (apiService.getExpiringItems as jest.Mock).mockResolvedValueOnce({ data: [] });
186
+
187
+ await itemCommands.expiring();
188
+
189
+ expect(apiService.getExpiringItems).toHaveBeenCalled();
190
+ });
191
+
192
+ it('should handle api errors gracefully', async () => {
193
+ (apiService.getExpiringItems as jest.Mock).mockRejectedValueOnce(new Error('API Error'));
194
+
195
+ await itemCommands.expiring();
196
+
197
+ expect(mockExit).toHaveBeenCalledWith(1);
198
+ });
199
+ });
200
+
201
+ describe('stats', () => {
202
+ it('should call apiService.getItemStats', async () => {
203
+ await itemCommands.stats();
204
+ expect(apiService.getItemStats).toHaveBeenCalled();
205
+ });
206
+
207
+ it('should handle api errors gracefully', async () => {
208
+ (apiService.getItemStats as jest.Mock).mockRejectedValueOnce(new Error('API Error'));
209
+
210
+ await itemCommands.stats();
211
+
212
+ expect(mockExit).toHaveBeenCalledWith(1);
213
+ });
214
+ });
215
+ });