xuanwu-cli 2.2.0 → 2.3.3

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 (41) hide show
  1. package/.env.test.example +14 -0
  2. package/__tests__/E2E_TEST_REPORT.md +206 -0
  3. package/__tests__/README.md +322 -0
  4. package/__tests__/TEST_SUMMARY.md +215 -0
  5. package/__tests__/global-setup.ts +13 -0
  6. package/__tests__/global-teardown.ts +3 -0
  7. package/__tests__/helpers/test-utils.ts +70 -0
  8. package/__tests__/integration/app.integration.test.ts +363 -0
  9. package/__tests__/integration/auth.integration.test.ts +243 -0
  10. package/__tests__/integration/build.integration.test.ts +215 -0
  11. package/__tests__/integration/e2e.test.ts +267 -0
  12. package/__tests__/integration/service.integration.test.ts +267 -0
  13. package/__tests__/integration/webhook.integration.test.ts +246 -0
  14. package/__tests__/run-e2e.js +360 -0
  15. package/__tests__/setup.ts +9 -0
  16. package/bin/xuanwu +0 -0
  17. package/dist/api/client.d.ts +29 -4
  18. package/dist/api/client.js +113 -29
  19. package/dist/commands/app.js +44 -0
  20. package/dist/commands/auth/login.js +5 -4
  21. package/dist/commands/deploy.js +77 -49
  22. package/dist/commands/env.js +31 -48
  23. package/dist/commands/project.d.ts +5 -0
  24. package/dist/commands/project.js +134 -0
  25. package/dist/commands/svc.js +36 -0
  26. package/dist/config/types.d.ts +1 -0
  27. package/dist/index.js +2 -0
  28. package/jest.config.js +18 -0
  29. package/package.json +10 -2
  30. package/src/api/client.ts +142 -33
  31. package/src/commands/app.ts +53 -0
  32. package/src/commands/auth/login.ts +6 -4
  33. package/src/commands/deploy.ts +93 -48
  34. package/src/commands/env.ts +35 -52
  35. package/src/commands/project.ts +153 -0
  36. package/src/commands/svc.ts +40 -0
  37. package/src/config/types.ts +1 -0
  38. package/src/index.ts +2 -0
  39. package/test/cli-integration.sh +245 -0
  40. package/test/integration.js +3 -3
  41. package/test/integration.sh +252 -0
@@ -0,0 +1,267 @@
1
+ /**
2
+ * 端到端测试
3
+ * 测试完整的 CLI 命令流程
4
+ */
5
+
6
+ import { CLIExecutor, generateRandomCode, sleep, parseTableOutput, extractValue } from '../helpers/test-utils'
7
+
8
+ const API_URL = process.env.TEST_API_URL || 'https://i.xuanwu.dev.aimstek.cn'
9
+ const TEST_EMAIL = process.env.TEST_USER_EMAIL || ''
10
+ const TEST_PASSWORD = process.env.TEST_USER_PASSWORD || ''
11
+
12
+ describe('End-to-End CLI Tests', () => {
13
+ let cli: CLIExecutor
14
+ let testAppCode: string
15
+
16
+ beforeAll(() => {
17
+ cli = new CLIExecutor()
18
+ })
19
+
20
+ describe('Login Command', () => {
21
+ test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should login with email and password', async () => {
22
+ const result = await cli.execute([
23
+ 'login',
24
+ '--email', TEST_EMAIL,
25
+ '--password', TEST_PASSWORD,
26
+ '--api-url', API_URL,
27
+ '--expires-in', '30d'
28
+ ])
29
+
30
+ expect(result.exitCode).toBe(0)
31
+ expect(result.stdout).toContain('登录成功')
32
+ })
33
+
34
+ test('should show error for invalid credentials', async () => {
35
+ const result = await cli.execute([
36
+ 'login',
37
+ '--email', 'invalid@example.com',
38
+ '--password', 'wrongpassword',
39
+ '--api-url', API_URL
40
+ ])
41
+
42
+ expect(result.exitCode).not.toBe(0)
43
+ })
44
+ })
45
+
46
+ describe('Application Commands', () => {
47
+ test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should create and list application', async () => {
48
+ testAppCode = generateRandomCode('e2e-app')
49
+
50
+ const createResult = await cli.execute([
51
+ 'app', 'create',
52
+ '--name', 'E2E测试应用',
53
+ '--code', testAppCode,
54
+ '--git', 'https://github.com/example/test.git',
55
+ '--api-url', API_URL
56
+ ], {
57
+ env: {
58
+ TEST_EMAIL,
59
+ TEST_PASSWORD
60
+ }
61
+ })
62
+
63
+ expect(createResult.exitCode).toBe(0)
64
+ expect(createResult.stdout).toContain('created')
65
+
66
+ await sleep(1000)
67
+
68
+ const listResult = await cli.execute(['app', 'ls'], {
69
+ env: {
70
+ TEST_EMAIL,
71
+ TEST_PASSWORD
72
+ }
73
+ })
74
+
75
+ expect(listResult.exitCode).toBe(0)
76
+ expect(listResult.stdout).toContain(testAppCode)
77
+ })
78
+
79
+ test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should get application details', async () => {
80
+ if (!testAppCode) {
81
+ console.warn('No test app created, skipping')
82
+ return
83
+ }
84
+
85
+ const result = await cli.execute(['app', 'get', testAppCode], {
86
+ env: {
87
+ TEST_EMAIL,
88
+ TEST_PASSWORD
89
+ }
90
+ })
91
+
92
+ expect(result.exitCode).toBe(0)
93
+ expect(result.stdout).toContain(testAppCode)
94
+ })
95
+
96
+ test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should update application', async () => {
97
+ if (!testAppCode) {
98
+ console.warn('No test app created, skipping')
99
+ return
100
+ }
101
+
102
+ const result = await cli.execute([
103
+ 'app', 'update', testAppCode,
104
+ '--name', '更新后的应用名'
105
+ ], {
106
+ env: {
107
+ TEST_EMAIL,
108
+ TEST_PASSWORD
109
+ }
110
+ })
111
+
112
+ expect(result.exitCode).toBe(0)
113
+ expect(result.stdout).toContain('updated')
114
+ })
115
+
116
+ test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should trigger build', async () => {
117
+ if (!testAppCode) {
118
+ console.warn('No test app created, skipping')
119
+ return
120
+ }
121
+
122
+ const result = await cli.execute([
123
+ 'app', 'build', testAppCode
124
+ ], {
125
+ env: {
126
+ TEST_EMAIL,
127
+ TEST_PASSWORD
128
+ }
129
+ })
130
+
131
+ expect(result.exitCode).toBe(0)
132
+ expect(result.stdout).toContain('started')
133
+ })
134
+
135
+ test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should list builds for application', async () => {
136
+ if (!testAppCode) {
137
+ console.warn('No test app created, skipping')
138
+ return
139
+ }
140
+
141
+ await sleep(2000)
142
+
143
+ const result = await cli.execute([
144
+ 'app', 'builds', testAppCode
145
+ ], {
146
+ env: {
147
+ TEST_EMAIL,
148
+ TEST_PASSWORD
149
+ }
150
+ })
151
+
152
+ expect(result.exitCode).toBe(0)
153
+ })
154
+
155
+ test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should delete application', async () => {
156
+ if (!testAppCode) {
157
+ console.warn('No test app created, skipping')
158
+ return
159
+ }
160
+
161
+ const result = await cli.execute([
162
+ 'app', 'delete', testAppCode
163
+ ], {
164
+ env: {
165
+ TEST_EMAIL,
166
+ TEST_PASSWORD
167
+ }
168
+ })
169
+
170
+ expect(result.exitCode).toBe(0)
171
+ expect(result.stdout).toContain('deleted')
172
+ })
173
+ })
174
+
175
+ describe('Service Commands', () => {
176
+ test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should list services', async () => {
177
+ const result = await cli.execute(['svc', 'ls'], {
178
+ env: {
179
+ TEST_EMAIL,
180
+ TEST_PASSWORD
181
+ }
182
+ })
183
+
184
+ expect(result.exitCode).toBe(0)
185
+ })
186
+ })
187
+
188
+ describe('Build Commands', () => {
189
+ test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should list all builds', async () => {
190
+ const result = await cli.execute(['build', 'ls'], {
191
+ env: {
192
+ TEST_EMAIL,
193
+ TEST_PASSWORD
194
+ }
195
+ })
196
+
197
+ expect(result.exitCode).toBe(0)
198
+ })
199
+ })
200
+
201
+ describe('Whoami Command', () => {
202
+ test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should show current user info', async () => {
203
+ const result = await cli.execute(['whoami'], {
204
+ env: {
205
+ TEST_EMAIL,
206
+ TEST_PASSWORD
207
+ }
208
+ })
209
+
210
+ expect(result.exitCode).toBe(0)
211
+ expect(result.stdout).toContain(TEST_EMAIL)
212
+ })
213
+ })
214
+
215
+ describe('Tokens Command', () => {
216
+ test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should list tokens', async () => {
217
+ const result = await cli.execute(['tokens'], {
218
+ env: {
219
+ TEST_EMAIL,
220
+ TEST_PASSWORD
221
+ }
222
+ })
223
+
224
+ expect(result.exitCode).toBe(0)
225
+ })
226
+ })
227
+
228
+ describe('Logout Command', () => {
229
+ test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should logout successfully', async () => {
230
+ const result = await cli.execute(['logout'], {
231
+ env: {
232
+ TEST_EMAIL,
233
+ TEST_PASSWORD
234
+ }
235
+ })
236
+
237
+ expect(result.exitCode).toBe(0)
238
+ expect(result.stdout).toContain('Logged out')
239
+ })
240
+ })
241
+
242
+ describe('Error Handling', () => {
243
+ test('should show error for non-existent application', async () => {
244
+ const result = await cli.execute(['app', 'get', 'non-existent-app'], {
245
+ env: {
246
+ TEST_EMAIL,
247
+ TEST_PASSWORD
248
+ }
249
+ })
250
+
251
+ expect(result.exitCode).not.toBe(0)
252
+ })
253
+
254
+ test('should show error for invalid command', async () => {
255
+ const result = await cli.execute(['invalid-command'])
256
+
257
+ expect(result.exitCode).not.toBe(0)
258
+ })
259
+
260
+ test('should show help message', async () => {
261
+ const result = await cli.execute(['--help'])
262
+
263
+ expect(result.exitCode).toBe(0)
264
+ expect(result.stdout).toContain('玄武工厂')
265
+ })
266
+ })
267
+ })
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Service 命令集成测试
3
+ * 测试 Service (K8s) 相关的 CLI API
4
+ */
5
+
6
+ import { APIClient } from '../../src/api/client'
7
+ import { Connection } from '../../src/config/types'
8
+
9
+ const API_URL = process.env.TEST_API_URL || 'https://i.xuanwu.dev.aimstek.cn'
10
+ const TEST_EMAIL = process.env.TEST_USER_EMAIL || ''
11
+ const TEST_PASSWORD = process.env.TEST_USER_PASSWORD || ''
12
+ const TEST_NAMESPACE = process.env.TEST_NAMESPACE || 'default'
13
+
14
+ describe('Service Integration Tests', () => {
15
+ let apiClient: APIClient
16
+ let testToken: string
17
+
18
+ beforeAll(async () => {
19
+ if (!TEST_EMAIL || !TEST_PASSWORD) {
20
+ console.warn('Skipping tests: TEST_USER_EMAIL and TEST_USER_PASSWORD not set')
21
+ return
22
+ }
23
+
24
+ const response = await fetch(`${API_URL}/api/cli/auth/login`, {
25
+ method: 'POST',
26
+ headers: { 'Content-Type': 'application/json' },
27
+ body: JSON.stringify({
28
+ email: TEST_EMAIL,
29
+ password: TEST_PASSWORD,
30
+ deviceName: 'Test CLI',
31
+ expiresIn: '30d'
32
+ })
33
+ })
34
+
35
+ if (!response.ok) {
36
+ throw new Error('Failed to login')
37
+ }
38
+
39
+ const data = await response.json() as { token: string }
40
+ testToken = data.token
41
+
42
+ const conn: Connection = {
43
+ name: 'test',
44
+ endpoint: API_URL,
45
+ token: testToken,
46
+ isDefault: true
47
+ }
48
+ apiClient = new APIClient(conn)
49
+ })
50
+
51
+ describe('List Services', () => {
52
+ test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should list services successfully', async () => {
53
+ const result = await apiClient.listK8sServices()
54
+
55
+ expect(result.success).toBeTruthy()
56
+ expect(result.data).toBeDefined()
57
+ })
58
+
59
+ test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should list services in specific namespace', async () => {
60
+ const result = await apiClient.listK8sServices(TEST_NAMESPACE)
61
+
62
+ expect(result.success).toBeTruthy()
63
+ expect(result.data).toBeDefined()
64
+ })
65
+
66
+ test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should return services with correct fields', async () => {
67
+ const result = await apiClient.listK8sServices(TEST_NAMESPACE)
68
+
69
+ if (result.data && Array.isArray(result.data) && result.data.length > 0) {
70
+ const service = result.data[0]
71
+ expect(service).toHaveProperty('name')
72
+ expect(service).toHaveProperty('namespace')
73
+ }
74
+ })
75
+ })
76
+
77
+ describe('Get Service', () => {
78
+ test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should get service by namespace and name', async () => {
79
+ const listResult = await apiClient.listK8sServices(TEST_NAMESPACE)
80
+
81
+ if (!listResult.success || !listResult.data || (Array.isArray(listResult.data) && listResult.data.length === 0)) {
82
+ console.warn('No services found in namespace, skipping test')
83
+ return
84
+ }
85
+
86
+ const services = Array.isArray(listResult.data) ? listResult.data : []
87
+ const serviceName = services[0]?.name
88
+
89
+ if (!serviceName) {
90
+ console.warn('No service name found, skipping test')
91
+ return
92
+ }
93
+
94
+ const result = await apiClient.getK8sService(TEST_NAMESPACE, serviceName)
95
+
96
+ expect(result.success).toBeTruthy()
97
+ expect(result.data).toBeDefined()
98
+ expect(result.data?.name).toBe(serviceName)
99
+ expect(result.data?.namespace).toBe(TEST_NAMESPACE)
100
+ })
101
+
102
+ test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should fail to get non-existent service', async () => {
103
+ const result = await apiClient.getK8sService(TEST_NAMESPACE, 'non-existent-service')
104
+
105
+ expect(result.success).toBeFalsy()
106
+ expect(result.error).toBeDefined()
107
+ })
108
+ })
109
+
110
+ describe('Get Service Status', () => {
111
+ test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should get service status', async () => {
112
+ const listResult = await apiClient.listK8sServices(TEST_NAMESPACE)
113
+
114
+ if (!listResult.success || !listResult.data || (Array.isArray(listResult.data) && listResult.data.length === 0)) {
115
+ console.warn('No services found, skipping test')
116
+ return
117
+ }
118
+
119
+ const services = Array.isArray(listResult.data) ? listResult.data : []
120
+ const serviceName = services[0]?.name
121
+
122
+ if (!serviceName) {
123
+ console.warn('No service name found, skipping test')
124
+ return
125
+ }
126
+
127
+ const result = await apiClient.getK8sServiceStatus(TEST_NAMESPACE, serviceName)
128
+
129
+ expect(result.success).toBeTruthy()
130
+ expect(result.data).toBeDefined()
131
+ })
132
+ })
133
+
134
+ describe('Get Service Logs', () => {
135
+ test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should get service logs', async () => {
136
+ const listResult = await apiClient.listK8sServices(TEST_NAMESPACE)
137
+
138
+ if (!listResult.success || !listResult.data || (Array.isArray(listResult.data) && listResult.data.length === 0)) {
139
+ console.warn('No services found, skipping test')
140
+ return
141
+ }
142
+
143
+ const services = Array.isArray(listResult.data) ? listResult.data : []
144
+ const serviceName = services[0]?.name
145
+
146
+ if (!serviceName) {
147
+ console.warn('No service name found, skipping test')
148
+ return
149
+ }
150
+
151
+ const result = await apiClient.getK8sServiceLogs(TEST_NAMESPACE, serviceName, 10)
152
+
153
+ expect(result.success).toBeTruthy()
154
+ })
155
+ })
156
+
157
+ describe('Restart Service', () => {
158
+ test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should restart service', async () => {
159
+ const listResult = await apiClient.listK8sServices(TEST_NAMESPACE)
160
+
161
+ if (!listResult.success || !listResult.data || (Array.isArray(listResult.data) && listResult.data.length === 0)) {
162
+ console.warn('No services found, skipping test')
163
+ return
164
+ }
165
+
166
+ const services = Array.isArray(listResult.data) ? listResult.data : []
167
+ const serviceName = services[0]?.name
168
+
169
+ if (!serviceName) {
170
+ console.warn('No service name found, skipping test')
171
+ return
172
+ }
173
+
174
+ const result = await apiClient.restartK8sService(TEST_NAMESPACE, serviceName)
175
+
176
+ expect(result.success).toBeTruthy()
177
+ })
178
+ })
179
+
180
+ describe('Scale Service', () => {
181
+ test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should scale service replicas', async () => {
182
+ const listResult = await apiClient.listK8sServices(TEST_NAMESPACE)
183
+
184
+ if (!listResult.success || !listResult.data || (Array.isArray(listResult.data) && listResult.data.length === 0)) {
185
+ console.warn('No services found, skipping test')
186
+ return
187
+ }
188
+
189
+ const services = Array.isArray(listResult.data) ? listResult.data : []
190
+ const serviceName = services[0]?.name
191
+
192
+ if (!serviceName) {
193
+ console.warn('No service name found, skipping test')
194
+ return
195
+ }
196
+
197
+ const statusResult = await apiClient.getK8sServiceStatus(TEST_NAMESPACE, serviceName)
198
+ const currentReplicas = statusResult.data?.replicas || 1
199
+
200
+ const result = await apiClient.scaleK8sService(TEST_NAMESPACE, serviceName, currentReplicas)
201
+
202
+ expect(result.success).toBeTruthy()
203
+ })
204
+
205
+ test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should fail to scale non-existent service', async () => {
206
+ const result = await apiClient.scaleK8sService(TEST_NAMESPACE, 'non-existent-service', 3)
207
+
208
+ expect(result.success).toBeFalsy()
209
+ expect(result.error).toBeDefined()
210
+ })
211
+ })
212
+
213
+ describe('List Service Pods', () => {
214
+ test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should list pods for service', async () => {
215
+ const listResult = await apiClient.listK8sServices(TEST_NAMESPACE)
216
+
217
+ if (!listResult.success || !listResult.data || (Array.isArray(listResult.data) && listResult.data.length === 0)) {
218
+ console.warn('No services found, skipping test')
219
+ return
220
+ }
221
+
222
+ const services = Array.isArray(listResult.data) ? listResult.data : []
223
+ const serviceName = services[0]?.name
224
+
225
+ if (!serviceName) {
226
+ console.warn('No service name found, skipping test')
227
+ return
228
+ }
229
+
230
+ const result = await apiClient.listK8sServicePods(TEST_NAMESPACE, serviceName)
231
+
232
+ expect(result.success).toBeTruthy()
233
+ })
234
+ })
235
+
236
+ describe('Exec in Service', () => {
237
+ test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should execute command in service pod', async () => {
238
+ const listResult = await apiClient.listK8sServices(TEST_NAMESPACE)
239
+
240
+ if (!listResult.success || !listResult.data || (Array.isArray(listResult.data) && listResult.data.length === 0)) {
241
+ console.warn('No services found, skipping test')
242
+ return
243
+ }
244
+
245
+ const services = Array.isArray(listResult.data) ? listResult.data : []
246
+ const serviceName = services[0]?.name
247
+
248
+ if (!serviceName) {
249
+ console.warn('No service name found, skipping test')
250
+ return
251
+ }
252
+
253
+ const result = await apiClient.execK8sService(TEST_NAMESPACE, serviceName, 'echo test')
254
+
255
+ expect(result.success).toBeTruthy()
256
+ })
257
+ })
258
+
259
+ describe('Delete Service', () => {
260
+ test.skipIf(!TEST_EMAIL || !TEST_PASSWORD)('should fail to delete non-existent service', async () => {
261
+ const result = await apiClient.deleteK8sService(TEST_NAMESPACE, 'non-existent-service')
262
+
263
+ expect(result.success).toBeFalsy()
264
+ expect(result.error).toBeDefined()
265
+ })
266
+ })
267
+ })