openxiangda-cli 2.0.0-alpha.60 → 2.0.0-alpha.62

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 (40) hide show
  1. package/dist/create-workspace.d.ts +1 -0
  2. package/dist/create-workspace.d.ts.map +1 -1
  3. package/dist/create-workspace.js +1 -0
  4. package/dist/create-workspace.js.map +1 -1
  5. package/package.json +4 -4
  6. package/template/AGENTS.md +1 -1
  7. package/template/README.md +7 -7
  8. package/template/apps/server/package.json +1 -1
  9. package/template/apps/server/src/app.module.ts +1 -3
  10. package/template/apps/server/test/smoke.test.ts +6 -23
  11. package/template/apps/web/e2e/resources.spec.ts +20 -0
  12. package/template/apps/web/index.html +1 -1
  13. package/template/apps/web/package.json +1 -1
  14. package/template/apps/web/scripts/check.mjs +5 -2
  15. package/template/apps/web/src/AuthoritativeSelector.tsx +38 -29
  16. package/template/apps/web/src/Shell.tsx +39 -211
  17. package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +1 -1
  18. package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +306 -0
  19. package/template/apps/web/src/components/resource/StandardResourcePages.tsx +431 -0
  20. package/template/apps/web/src/components/resource/SurfaceFields.tsx +500 -0
  21. package/template/apps/web/src/data-provider.ts +36 -31
  22. package/template/apps/web/src/main.tsx +24 -54
  23. package/template/apps/web/src/platform-client.ts +131 -382
  24. package/template/apps/web/src/runtime-meta.ts +1 -1
  25. package/template/apps/web/src/styles.css +100 -3
  26. package/template/apps/web/test/contracts.test.ts +31 -693
  27. package/template/openxiangda.config.ts +14 -81
  28. package/template/package.json +3 -3
  29. package/template/packages/contracts/src/generated.ts +7 -130
  30. package/template/apps/server/src/instrument-context.controller.ts +0 -25
  31. package/template/apps/web/e2e/instruments.spec.ts +0 -1338
  32. package/template/apps/web/src/CollegePage.tsx +0 -181
  33. package/template/apps/web/src/InstrumentDetailPage.tsx +0 -134
  34. package/template/apps/web/src/InstrumentForm.tsx +0 -575
  35. package/template/apps/web/src/InstrumentFormPage.tsx +0 -117
  36. package/template/apps/web/src/InstrumentListPage.tsx +0 -385
  37. package/template/apps/web/src/fields.ts +0 -83
  38. package/template/apps/web/src/instrument.ts +0 -55
  39. package/template/platform/data/colleges.ts +0 -21
  40. package/template/platform/data/instruments.ts +0 -81
@@ -1,697 +1,35 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import test from 'node:test';
4
- import {
5
- instrumentFieldAllowed,
6
- sanitizeInstrumentValues,
7
- } from '../src/InstrumentForm';
8
- import { instrumentFields } from '../src/fields';
9
- import {
10
- browseDepartmentTree,
11
- browseDepartmentUsers,
12
- instrumentDataApi,
13
- fetchDataFileBlob,
14
- loadDataFilePreview,
15
- loadCurrentIdentity,
16
- resolveCollegeScope,
17
- resolveDirectory,
18
- searchCollegeScope,
19
- searchDirectory,
20
- } from '../src/platform-client';
21
- import {
22
- applicationApiPath,
23
- readRuntimeMount,
24
- resolveAttachmentPreviewPath,
25
- resolveApplicationApiPath,
26
- resolveApplicationBasename,
27
- } from '../src/runtime-meta';
28
-
29
- const collegeId = '11111111-1111-4111-8111-111111111111';
30
- const userId = '22222222-2222-4222-8222-222222222222';
31
-
32
- test('the golden module exposes exactly 30 fields and five restricted fields', () => {
33
- assert.equal(instrumentFields.length, 30);
34
- assert.deepEqual(
35
- instrumentFields
36
- .filter(field => field.createCapability && field.updateCapability)
37
- .map(field => field.key),
38
- [
39
- 'instrumentCode',
40
- 'assetCode',
41
- 'assetValue',
42
- 'collegeId',
43
- 'instrumentAdminIds',
44
- ]
45
- );
46
- assert.deepEqual(
47
- instrumentFields.filter(field => field.system).map(field => field.key),
48
- ['id', 'revision']
49
- );
50
- });
51
-
52
- test('application shell and instrument surfaces match the approved navigation model', () => {
53
- const shell = readFileSync(new URL('../src/Shell.tsx', import.meta.url), 'utf8');
54
- const list = readFileSync(
55
- new URL('../src/InstrumentListPage.tsx', import.meta.url),
56
- 'utf8'
57
- );
58
- const routes = readFileSync(new URL('../src/main.tsx', import.meta.url), 'utf8');
59
- const form = readFileSync(
60
- new URL('../src/InstrumentForm.tsx', import.meta.url),
61
- 'utf8'
62
- );
63
- const colleges = readFileSync(
64
- new URL('../src/CollegePage.tsx', import.meta.url),
65
- 'utf8'
66
- );
67
- const directory = readFileSync(
68
- new URL('../src/components/platform-fields/PlatformDirectoryPicker.tsx', import.meta.url),
69
- 'utf8'
70
- );
71
- const attachment = readFileSync(
72
- new URL('../src/components/platform-fields/AttachmentFileList.tsx', import.meta.url),
73
- 'utf8'
74
- );
75
- const preview = readFileSync(
76
- new URL('../src/FilePreviewPage.tsx', import.meta.url),
77
- 'utf8'
78
- );
79
- for (const marker of [
80
- '仪器资源管理',
81
- '工作台',
82
- '仪器管理',
83
- '仪器资源',
84
- '开放记录',
85
- '基础数据',
86
- '学院字典',
87
- '系统设置',
88
- '应用管理员',
89
- '收起侧边栏',
90
- ]) {
91
- assert.match(shell, new RegExp(marker));
92
- }
93
- assert.doesNotMatch(shell, /<span>\{identity\.userId\}<\/span>/);
94
- assert.doesNotMatch(shell, /验证本地 Nest|roleCodes\.join|environment\.key/);
95
- assert.doesNotMatch(list, /<Drawer|surface="drawer"/);
96
- assert.match(routes, /path="\/instruments\/new"[\s\S]*<InstrumentFormPage mode="create"/);
97
- assert.match(colleges, /<Drawer/);
98
- assert.match(colleges, /size=\{520\}/);
99
- assert.match(directory, /width=\{kind === 'user' \? 980 : 760\}/);
100
- assert.match(directory, /browseDepartmentTree|browseDepartmentUsers/);
101
- assert.match(directory, /选择成员|选择部门|组织架构|已选/);
102
- assert.match(form, /oxa-form-full/);
103
- assert.match(form, /PlatformDirectoryPicker/);
104
- assert.match(form, /oxa-file-dropzone/);
105
- assert.match(form, /oxa-detail-layout/);
106
- assert.match(routes, /path="\/files\/:resourceCode\/:fileId\/preview"/);
107
- assert.match(attachment, /Image\.PreviewGroup/);
108
- assert.match(attachment, /attachmentPreviewPath/);
109
- assert.match(preview, /import\('docx-preview'\)/);
110
- assert.match(preview, /import\('xlsx'\)/);
111
- assert.match(preview, /只读预览/);
112
- assert.doesNotMatch(form, /instrumentFileContentUrl|window\.open/);
113
- assert.doesNotMatch(
114
- `${form}\n${directory}`,
115
- /FormContext|tools\/openxiangda|RoleSession/
116
- );
117
- });
118
-
119
- test('college, user, and department selectors have authoritative owners only', () => {
120
- const appConfig = readFileSync(
121
- new URL('../../../openxiangda.config.ts', import.meta.url),
122
- 'utf8'
123
- );
124
- const colleges = readFileSync(
125
- new URL('../../../platform/data/colleges.ts', import.meta.url),
126
- 'utf8'
127
- );
128
- const fields = readFileSync(new URL('../src/fields.ts', import.meta.url), 'utf8');
129
- assert.match(appConfig, /valueType:\s*'uuid'/);
130
- assert.match(appConfig, /kind:\s*'native_resource'/);
131
- assert.match(appConfig, /resourceCode:\s*'colleges'/);
132
- assert.match(appConfig, /data:\s*\{\s*resources:\s*\[colleges, instruments\]/);
133
- assert.match(colleges, /code:\s*'name'.*type:\s*'string'/s);
134
- assert.match(colleges, /code:\s*'enabled'.*type:\s*'boolean'/s);
135
- assert.match(fields, /kind:\s*'scope'/);
136
- assert.match(fields, /kind:\s*'directory-user'/);
137
- assert.match(fields, /kind:\s*'directory-department'/);
138
- for (const marker of [
139
- ['college', 'am'].join('-'),
140
- ['user', 'wang'].join('-'),
141
- ['dept', 'am-test'].join('-'),
142
- ]) {
143
- assert.equal(appConfig.includes(marker), false);
144
- assert.equal(fields.includes(marker), false);
145
- }
146
- });
147
-
148
- test('college create restores five fields while update omits college reassignment', () => {
149
- const protectedFields = instrumentFields.filter(
150
- field => field.createCapability && field.updateCapability
151
- );
152
- const collegeCapabilities = new Set<string>([
153
- ...protectedFields.map(field => field.createCapability!),
154
- ...protectedFields
155
- .filter(field => field.key !== 'collegeId')
156
- .map(field => field.updateCapability!),
157
- ]);
158
- const hasCapability = (capability: string) =>
159
- collegeCapabilities.has(capability);
160
- const values = {
161
- instrumentCode: 'INS-001',
162
- chineseName: '电子显微镜',
163
- assetCode: 'ASSET-001',
164
- assetValue: 100,
165
- collegeId,
166
- instrumentAdminIds: [userId],
167
- revision: 7,
168
- };
169
- assert.equal(
170
- instrumentFieldAllowed(
171
- instrumentFields.find(field => field.key === 'collegeId')!,
172
- 'create',
173
- hasCapability
174
- ),
175
- true
176
- );
177
- assert.equal(
178
- instrumentFieldAllowed(
179
- instrumentFields.find(field => field.key === 'collegeId')!,
180
- 'edit',
181
- hasCapability
182
- ),
183
- false
184
- );
185
- assert.deepEqual(sanitizeInstrumentValues(values, 'create', hasCapability), {
186
- instrumentCode: 'INS-001',
187
- chineseName: '电子显微镜',
188
- assetCode: 'ASSET-001',
189
- assetValue: 100,
190
- collegeId,
191
- instrumentAdminIds: [userId],
192
- });
193
- assert.deepEqual(sanitizeInstrumentValues(values, 'edit', hasCapability), {
194
- instrumentCode: 'INS-001',
195
- chineseName: '电子显微镜',
196
- assetCode: 'ASSET-001',
197
- assetValue: 100,
198
- instrumentAdminIds: [userId],
199
- });
200
- });
201
-
202
- test('instrument admin update payload omits every protected field', () => {
203
- const values = Object.fromEntries(
204
- instrumentFields
205
- .filter(field => !field.system)
206
- .map(field => [field.key, `value:${field.key}`])
207
- );
208
- const sanitized = sanitizeInstrumentValues(values, 'edit', () => false);
209
- assert.equal(sanitized.chineseName, 'value:chineseName');
210
- for (const field of instrumentFields.filter(
211
- item => item.createCapability && item.updateCapability
212
- )) {
213
- assert.equal(
214
- Object.prototype.hasOwnProperty.call(sanitized, field.key),
215
- false
216
- );
217
- }
218
- });
219
-
220
- test('school admin keeps every protected field in create and update payloads', () => {
221
- const values = Object.fromEntries(
222
- instrumentFields
223
- .filter(field => !field.system)
224
- .map(field => [field.key, `value:${field.key}`])
225
- );
226
- for (const mode of ['create', 'edit'] as const) {
227
- const sanitized = sanitizeInstrumentValues(values, mode, () => true);
228
- for (const field of instrumentFields.filter(
229
- item => item.createCapability && item.updateCapability
230
- )) {
231
- assert.equal(sanitized[field.key], `value:${field.key}`);
232
- }
233
- }
4
+ import { resourceDefinitions, resourceCodes } from '../../../packages/contracts/src/generated.js';
5
+ import { readRuntimeMount, resolveApplicationBasename } from '../src/runtime-meta';
6
+
7
+ test('clean template is resource-driven and contains no HGY fixture', () => {
8
+ const config = readFileSync(new URL('../../../openxiangda.config.ts', import.meta.url), 'utf8');
9
+ const source = readFileSync(new URL('../src/main.tsx', import.meta.url), 'utf8');
10
+ assert.deepEqual(Object.keys(resourceDefinitions).sort(), [...resourceCodes].sort());
11
+ assert.match(config, /data:\s*\{\s*resources:/);
12
+ assert.match(source, /Object\.keys\(resourceDefinitions\)/);
13
+ const legacyMarkers = ['instruments', 'colleges', '仪器' + '资源管理', '学院' + '字典', 'HGY', 'Instrument'];
14
+ assert.doesNotMatch(`${config}\n${source}`, new RegExp(legacyMarkers.join('|')));
15
+ });
16
+
17
+ test('standard surfaces keep authoritative directory/resource selectors and mobile routes', () => {
18
+ const surface = readFileSync(new URL('../src/components/resource/SurfaceFields.tsx', import.meta.url), 'utf8');
19
+ const selector = readFileSync(new URL('../src/AuthoritativeSelector.tsx', import.meta.url), 'utf8');
20
+ const crud = readFileSync(new URL('../src/components/resource/GeneratedResourceCrud.tsx', import.meta.url), 'utf8');
21
+ assert.match(surface, /PlatformDirectoryPicker/);
22
+ assert.match(surface, /source="resource"/);
23
+ assert.match(selector, /searchResource|resolveResource/);
24
+ assert.match(crud, /MobileResourceListPage/);
25
+ assert.match(crud, /fieldWritable/);
26
+ });
27
+
28
+ test('runtime metadata stays mount-scoped', () => {
29
+ const mount = readRuntimeMount(name => ({
30
+ 'openxiangda-runtime-base': '/view/example-app/',
31
+ 'openxiangda-app-code': 'example-app',
32
+ 'openxiangda-environment': 'preproduction',
33
+ }[name]));
34
+ assert.equal(resolveApplicationBasename(mount), '/view/example-app');
234
35
  });
235
-
236
- test('deployed app/environment come only from platform-injected runtime meta', () => {
237
- const values = new Map([
238
- ['openxiangda-runtime-base', '/view/instrument-center/'],
239
- ['openxiangda-app-code', 'instrument-center'],
240
- ['openxiangda-environment', 'production'],
241
- ]);
242
- const mount = readRuntimeMount(name => values.get(name));
243
- assert.deepEqual(mount, {
244
- runtimeBase:
245
- '/view/instrument-center/',
246
- appCode: 'instrument-center',
247
- environmentKey: 'production',
248
- });
249
- assert.equal(readRuntimeMount(() => undefined), null);
250
- assert.throws(
251
- () =>
252
- readRuntimeMount(name =>
253
- name === 'openxiangda-environment' ? 'production' : undefined
254
- ),
255
- /OPENXIANGDA_RUNTIME_META_INVALID/
256
- );
257
- assert.equal(
258
- applicationApiPath('api/instruments/context'),
259
- '/api/instruments/context'
260
- );
261
- assert.equal(resolveApplicationBasename(mount), '/view/instrument-center');
262
- assert.equal(
263
- resolveApplicationBasename({
264
- ...mount!,
265
- runtimeBase: '/dev/instrument-center/',
266
- environmentKey: 'preproduction',
267
- }),
268
- '/dev/instrument-center'
269
- );
270
- assert.equal(
271
- resolveApplicationApiPath(mount, 'api/instruments/context'),
272
- '/service/openxiangda-app-api/v2/instrument-center/production/api/instruments/context'
273
- );
274
- assert.equal(
275
- resolveAttachmentPreviewPath(
276
- mount,
277
- 'instruments',
278
- '77777777-7777-4777-8777-777777777777'
279
- ),
280
- '/view/instrument-center/files/instruments/77777777-7777-4777-8777-777777777777/preview'
281
- );
282
- });
283
-
284
- test('managed file preview reads metadata and bytes without a RoleSession', async () => {
285
- const calls: Array<{ path: string; init?: RequestInit }> = [];
286
- const originalFetch = globalThis.fetch;
287
- globalThis.fetch = async (input, init) => {
288
- const path = String(input);
289
- calls.push({ path, init });
290
- if (path.endsWith('/dev-sessions/current')) {
291
- return Response.json({
292
- code: 200,
293
- data: {
294
- environment: { id: 'test-id', key: 'preproduction' },
295
- principal: {
296
- userId,
297
- roleCodes: ['school_admin'],
298
- capabilityCodes: [],
299
- isAppSuperAdmin: false,
300
- },
301
- },
302
- });
303
- }
304
- if (path.includes('/files/file-1/preview?')) {
305
- return Response.json({
306
- code: 200,
307
- data: {
308
- schemaVersion: 'openxiangda.data-file-preview/v2',
309
- resourceCode: 'instruments',
310
- file: {
311
- schemaVersion: 'openxiangda.data-file-ref/v2',
312
- id: 'file-1',
313
- name: 'manual.docx',
314
- size: 12,
315
- contentType:
316
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
317
- },
318
- extension: 'docx',
319
- previewType: 'office',
320
- renderMode: 'docx-html',
321
- previewSurface: 'document',
322
- previewProvider: 'browser',
323
- canPreview: true,
324
- canDownload: true,
325
- },
326
- });
327
- }
328
- if (path.includes('/files/file-1/content?')) {
329
- return new Response(new Uint8Array([80, 75, 3, 4]), {
330
- status: 200,
331
- headers: {
332
- 'content-type':
333
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
334
- },
335
- });
336
- }
337
- return Response.json({ code: 404, message: 'not mocked' }, { status: 404 });
338
- };
339
- try {
340
- await loadCurrentIdentity();
341
- const preview = await loadDataFilePreview('instruments', 'file-1');
342
- const blob = await fetchDataFileBlob('instruments', 'file-1');
343
- assert.equal(preview.renderMode, 'docx-html');
344
- assert.equal(blob.size, 4);
345
- } finally {
346
- globalThis.fetch = originalFetch;
347
- }
348
- const previewCall = calls.find(call => call.path.includes('/files/file-1/preview?'))!;
349
- const contentCall = calls.find(call => call.path.includes('/files/file-1/content?'))!;
350
- assert.match(previewCall.path, /environmentKey=preproduction/);
351
- assert.match(contentCall.path, /disposition=inline/);
352
- assert.equal(
353
- calls.some(call =>
354
- Object.keys(call.init?.headers || {}).some(header =>
355
- header.toLowerCase().includes('role-session')
356
- )
357
- ),
358
- false
359
- );
360
- });
361
-
362
- test('list uses the native Data API with server paging, filters, and sorting', async () => {
363
- const calls: Array<{ path: string; init?: RequestInit }> = [];
364
- const originalFetch = globalThis.fetch;
365
- globalThis.fetch = async (input, init) => {
366
- calls.push({ path: String(input), init });
367
- if (String(input).endsWith('/dev-sessions/current')) {
368
- return new Response(
369
- JSON.stringify({
370
- code: 200,
371
- data: {
372
- environment: { id: 'test-id', key: 'preproduction' },
373
- principal: {
374
- userId: 'user-test',
375
- roleCodes: ['college_admin'],
376
- capabilityCodes: [],
377
- isAppSuperAdmin: false,
378
- },
379
- },
380
- }),
381
- { status: 200, headers: { 'content-type': 'application/json' } }
382
- );
383
- }
384
- return new Response(
385
- JSON.stringify({
386
- code: 200,
387
- data: {
388
- schemaVersion: 'openxiangda.data-page/v2',
389
- resourceCode: 'instruments',
390
- items: [],
391
- total: 0,
392
- limit: 20,
393
- offset: 20,
394
- },
395
- }),
396
- { status: 200, headers: { 'content-type': 'application/json' } }
397
- );
398
- };
399
- try {
400
- await loadCurrentIdentity();
401
- await instrumentDataApi.list({
402
- page: 2,
403
- pageSize: 20,
404
- collegeId,
405
- instrumentAdminId: userId,
406
- sort: { field: 'assetValue', order: 'asc' },
407
- });
408
- await instrumentDataApi.list({ page: 1, pageSize: 20 });
409
- } finally {
410
- globalThis.fetch = originalFetch;
411
- }
412
- assert.equal(calls.length, 3);
413
- assert.match(
414
- calls[1]!.path,
415
- /\/native\/data\/instruments\/query$/
416
- );
417
- const body = JSON.parse(String(calls[1]!.init?.body));
418
- const resourceSource = readFileSync(
419
- new URL('../../../platform/data/instruments.ts', import.meta.url),
420
- 'utf8'
421
- );
422
- const declaredFields = new Set(
423
- [...resourceSource.matchAll(/\{\s*code:\s*'([^']+)'/g)].map(
424
- match => match[1]!
425
- )
426
- );
427
- assert.deepEqual(
428
- [...declaredFields],
429
- instrumentFields.filter(field => !field.system).map(field => field.key)
430
- );
431
- assert.equal(body.environmentKey, 'preproduction');
432
- assert.equal(body.limit, 20);
433
- assert.equal(body.offset, 20);
434
- assert.deepEqual(body.order, [{ field: 'assetValue', direction: 'asc' }]);
435
- assert.equal(
436
- [...body.filters, ...body.order].every(item =>
437
- declaredFields.has(item.field)
438
- ),
439
- true
440
- );
441
- assert.deepEqual(body.filters, [
442
- { field: 'collegeId', operator: 'eq', value: collegeId },
443
- {
444
- field: 'instrumentAdminIds',
445
- operator: 'cs',
446
- value: [userId],
447
- },
448
- ]);
449
- const defaultBody = JSON.parse(String(calls[2]!.init?.body));
450
- assert.deepEqual(defaultBody.order, [
451
- { field: 'instrumentCode', direction: 'asc' },
452
- ]);
453
- assert.equal(
454
- [...defaultBody.filters, ...defaultBody.order].every(item =>
455
- declaredFields.has(item.field)
456
- ),
457
- true
458
- );
459
- });
460
-
461
- test('list rejects an undeclared sorter before issuing a Data API request', async () => {
462
- let requests = 0;
463
- const originalFetch = globalThis.fetch;
464
- globalThis.fetch = async () => {
465
- requests += 1;
466
- throw new Error('unexpected fetch');
467
- };
468
- try {
469
- await assert.rejects(
470
- instrumentDataApi.list({
471
- page: 1,
472
- pageSize: 20,
473
- sort: { field: 'updatedAt', order: 'desc' },
474
- }),
475
- /OPENXIANGDA_INSTRUMENT_QUERY_FIELD_NOT_DECLARED:updatedAt/
476
- );
477
- } finally {
478
- globalThis.fetch = originalFetch;
479
- }
480
- assert.equal(requests, 0);
481
- });
482
-
483
- test('authoritative selectors page, resolve exact IDs, and never send RoleSession', async () => {
484
- const calls: Array<{ path: string; init?: RequestInit }> = [];
485
- const originalFetch = globalThis.fetch;
486
- globalThis.fetch = async (input, init) => {
487
- const path = String(input);
488
- calls.push({ path, init });
489
- if (path.endsWith('/dev-sessions/current')) {
490
- return Response.json({
491
- code: 200,
492
- data: {
493
- environment: {
494
- id: 'test-id',
495
- key: 'preproduction',
496
- activeAppVersionId: 'version-1',
497
- headRevision: 2,
498
- authzRevisionId: 'authz-1',
499
- authzVersion: 3,
500
- scopeDataVersion: 'scope-1',
501
- },
502
- principal: {
503
- userId,
504
- roleCodes: ['school_admin'],
505
- capabilityCodes: [],
506
- isAppSuperAdmin: false,
507
- },
508
- },
509
- });
510
- }
511
- if (path.includes('/directory/users?')) {
512
- return Response.json({
513
- code: 200,
514
- data: {
515
- schemaVersion: 'openxiangda.directory-entry-page/v2',
516
- kind: 'user',
517
- items: [
518
- { kind: 'user', id: userId, label: '王老师', selectable: true },
519
- ],
520
- nextCursor: 'next-user-page',
521
- },
522
- });
523
- }
524
- if (path.includes('/directory/departments/tree?')) {
525
- return Response.json({
526
- code: 200,
527
- data: {
528
- schemaVersion: 'openxiangda.directory-entry-page/v2',
529
- kind: 'department',
530
- items: [
531
- {
532
- kind: 'department',
533
- id: 'department-root',
534
- label: '材料学院',
535
- hasChildren: true,
536
- selectable: true,
537
- },
538
- ],
539
- nextCursor: null,
540
- },
541
- });
542
- }
543
- if (path.includes('/directory/departments/department-root/users?')) {
544
- return Response.json({
545
- code: 200,
546
- data: {
547
- schemaVersion: 'openxiangda.directory-entry-page/v2',
548
- kind: 'user',
549
- items: [
550
- { kind: 'user', id: userId, label: '王老师', selectable: true },
551
- ],
552
- nextCursor: null,
553
- },
554
- });
555
- }
556
- if (path.includes('/directory/resolve?')) {
557
- const body = JSON.parse(String(init?.body));
558
- return Response.json({
559
- code: 200,
560
- data: {
561
- schemaVersion: 'openxiangda.directory-entry-page/v2',
562
- kind: body.kind,
563
- items: body.ids.map((id: string) => ({
564
- kind: body.kind,
565
- id,
566
- label: '精确解析',
567
- selectable: false,
568
- })),
569
- nextCursor: null,
570
- },
571
- });
572
- }
573
- if (path.includes('/scope-values/resolve?')) {
574
- const body = JSON.parse(String(init?.body));
575
- return Response.json({
576
- code: 200,
577
- data: scopePage(
578
- body.operation,
579
- body.values.map((value: string) => ({
580
- value,
581
- label: '先进材料学院',
582
- selectable: false,
583
- }))
584
- ),
585
- });
586
- }
587
- if (path.includes('/scope-values?')) {
588
- return Response.json({
589
- code: 200,
590
- data: scopePage('create', [
591
- { value: collegeId, label: '先进材料学院', selectable: true },
592
- ]),
593
- });
594
- }
595
- return Response.json({ code: 404, message: 'not mocked' }, { status: 404 });
596
- };
597
- try {
598
- await loadCurrentIdentity();
599
- const users = await searchDirectory('user', {
600
- keyword: '王老',
601
- cursor: 'cursor-1',
602
- });
603
- assert.equal(users.nextCursor, 'next-user-page');
604
- assert.equal((await resolveDirectory('department', [userId])).items[0]?.selectable, false);
605
- assert.equal(
606
- (await browseDepartmentTree({ parentId: 'department-parent' })).items[0]
607
- ?.id,
608
- 'department-root'
609
- );
610
- assert.equal(
611
- (await browseDepartmentUsers('department-root', 2)).items[0]?.id,
612
- userId
613
- );
614
- assert.equal(
615
- (await searchCollegeScope('create', { keyword: '', cursor: 'scope-1' }))
616
- .items[0]?.value,
617
- collegeId
618
- );
619
- assert.equal(
620
- (await resolveCollegeScope('update', [collegeId])).items[0]?.selectable,
621
- false
622
- );
623
- await assert.rejects(
624
- resolveDirectory('user', []),
625
- /OPENXIANGDA_DIRECTORY_RESOLVE_IDS_REQUIRED/
626
- );
627
- await assert.rejects(
628
- resolveCollegeScope('update', []),
629
- /OPENXIANGDA_SCOPE_RESOLVE_VALUES_REQUIRED/
630
- );
631
- await assert.rejects(
632
- resolveDirectory(
633
- 'user',
634
- Array.from({ length: 51 }, (_, index) => `user-${index}`)
635
- ),
636
- /OPENXIANGDA_DIRECTORY_RESOLVE_IDS_EXCEEDED/
637
- );
638
- await assert.rejects(
639
- resolveCollegeScope(
640
- 'update',
641
- Array.from({ length: 51 }, (_, index) =>
642
- `${String(index).padStart(8, '0')}-0000-4000-8000-000000000000`
643
- )
644
- ),
645
- /OPENXIANGDA_SCOPE_RESOLVE_VALUES_EXCEEDED/
646
- );
647
- } finally {
648
- globalThis.fetch = originalFetch;
649
- }
650
- const userSearch = calls.find(call => call.path.includes('/directory/users?'))!;
651
- const treeBrowse = calls.find(call => call.path.includes('/directory/departments/tree?'))!;
652
- const memberBrowse = calls.find(call => call.path.includes('/directory/departments/department-root/users?'))!;
653
- const scopeSearch = calls.find(call => call.path.includes('/scope-values?'))!;
654
- assert.match(userSearch.path, /keyword=%E7%8E%8B%E8%80%81/);
655
- assert.match(userSearch.path, /cursor=cursor-1/);
656
- assert.match(treeBrowse.path, /parentId=department-parent/);
657
- assert.match(memberBrowse.path, /page=2/);
658
- assert.match(scopeSearch.path, /environmentKey=preproduction/);
659
- assert.match(
660
- scopeSearch.path,
661
- /\/native\/data-resources\/instruments\/fields\/collegeId\/scope-values\?/
662
- );
663
- assert.match(scopeSearch.path, /operation=create/);
664
- assert.match(scopeSearch.path, /cursor=scope-1/);
665
- assert.equal(
666
- calls.some(call =>
667
- Object.keys(call.init?.headers || {}).some(header =>
668
- header.toLowerCase().includes('role-session')
669
- )
670
- ),
671
- false
672
- );
673
- });
674
-
675
- function scopePage(
676
- operation: 'create' | 'update',
677
- items: Array<{ value: string; label: string; selectable: boolean }>
678
- ) {
679
- return {
680
- schemaVersion: 'openxiangda.native-scope-value-page/v2',
681
- environment: {
682
- id: 'test-id',
683
- key: 'preproduction',
684
- activeAppVersionId: 'version-1',
685
- headRevision: 2,
686
- authzRevisionId: 'authz-1',
687
- authzVersion: 3,
688
- scopeDataVersion: 'scope-1',
689
- },
690
- resourceCode: 'instruments',
691
- fieldCode: 'collegeId',
692
- dimensionCode: 'college',
693
- operation,
694
- items,
695
- nextCursor: null,
696
- };
697
- }