@tbox.cn/app-contracts-mall 0.1.1 → 0.9.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 (78) hide show
  1. package/README.md +16 -2
  2. package/dist/alipay-activity/cards.d.ts +305 -0
  3. package/dist/assembly.d.ts +54 -0
  4. package/dist/chunk-KIZD6FZ2.js +43 -0
  5. package/dist/deployment.d.ts +42 -0
  6. package/dist/domain/activity.d.ts +64 -0
  7. package/dist/domain/coupon.d.ts +2 -0
  8. package/dist/domain/entitlement.d.ts +53 -0
  9. package/dist/domain/offer.d.ts +111 -0
  10. package/dist/domain/operation.d.ts +22 -0
  11. package/dist/domain/parking.d.ts +195 -12
  12. package/dist/domain/points.d.ts +3 -2
  13. package/dist/domain/shopping-guide.d.ts +201 -0
  14. package/dist/guard.d.ts +50 -0
  15. package/dist/index.d.ts +22 -2
  16. package/dist/member/cards.d.ts +940 -0
  17. package/dist/parking/cards.d.ts +1271 -0
  18. package/dist/promotion/cards.d.ts +2529 -0
  19. package/dist/query-context.d.ts +47 -0
  20. package/dist/runtime.d.ts +20 -0
  21. package/dist/runtime.js +1281 -0
  22. package/dist/scope.d.ts +76 -0
  23. package/dist/server/index.d.ts +11 -0
  24. package/dist/server/index.js +80 -0
  25. package/dist/server/mall-context.d.ts +24 -0
  26. package/dist/server/mall-control-plane.d.ts +8 -0
  27. package/dist/server/request-credentials.d.ts +22 -0
  28. package/dist/services/customer-service.d.ts +11 -0
  29. package/dist/services/mall-info.d.ts +26 -0
  30. package/dist/services/member.d.ts +243 -9
  31. package/dist/services/parking.d.ts +105 -12
  32. package/dist/services/promotion.d.ts +100 -4
  33. package/dist/services/shopping-guide.d.ts +97 -0
  34. package/dist/shared/cards.d.ts +42 -0
  35. package/dist/shopping-guide/cards.d.ts +7039 -0
  36. package/package.json +19 -8
  37. package/src/alipay-activity/cards.ts +66 -0
  38. package/src/assembly.ts +103 -0
  39. package/src/deployment.ts +44 -0
  40. package/src/domain/activity.ts +66 -0
  41. package/src/domain/coupon.ts +3 -0
  42. package/src/domain/entitlement.ts +56 -0
  43. package/src/domain/offer.ts +120 -0
  44. package/src/domain/operation.ts +40 -0
  45. package/src/domain/parking.ts +206 -12
  46. package/src/domain/points.ts +3 -2
  47. package/src/domain/shopping-guide.ts +242 -0
  48. package/src/guard.ts +48 -0
  49. package/src/index.ts +22 -2
  50. package/src/member/cards.ts +209 -0
  51. package/src/parking/cards.ts +233 -0
  52. package/src/promotion/cards.ts +290 -0
  53. package/src/query-context.ts +81 -0
  54. package/src/runtime.ts +41 -0
  55. package/src/scope.ts +92 -0
  56. package/src/server/index.ts +11 -0
  57. package/src/server/mall-context.ts +39 -0
  58. package/src/server/mall-control-plane.ts +56 -0
  59. package/src/server/request-credentials.ts +43 -0
  60. package/src/service-slots.json +23 -0
  61. package/src/services/customer-service.ts +12 -0
  62. package/src/services/mall-info.ts +31 -0
  63. package/src/services/member.ts +354 -9
  64. package/src/services/parking.ts +161 -11
  65. package/src/services/promotion.ts +124 -4
  66. package/src/services/shopping-guide.ts +138 -0
  67. package/src/shared/cards.ts +25 -0
  68. package/src/shopping-guide/cards.ts +438 -0
  69. package/tests/assembly-validate-scope.test.ts +132 -0
  70. package/tests/cards.test.ts +586 -0
  71. package/tests/context-unification.test.ts +73 -0
  72. package/tests/mall-control-plane.test.ts +86 -0
  73. package/tests/member-status.test.ts +197 -0
  74. package/tests/query-context.test.ts +111 -0
  75. package/tests/request-credentials.test.ts +38 -0
  76. package/tests/resolve-primitives.test.ts +55 -0
  77. package/tests/service-slots.test.ts +78 -0
  78. package/tsconfig.json +1 -1
@@ -0,0 +1,586 @@
1
+ /**
2
+ * 015 S1:契约 zod 域单测。
3
+ * - itemActions 判别联合拒未知 kind(F1)
4
+ * - 新卡 schema sample round-trip(promotion 6 + parking 5 + member 入会 2)
5
+ * - host-effect 协议单源经 contracts 包导出可达
6
+ */
7
+ import { describe, expect, it } from 'vitest';
8
+ import {
9
+ actionDraftSchema,
10
+ pageInfoSchema,
11
+ shoppingGuideCardDataSchemas,
12
+ promotionCardDataSchemas,
13
+ parkingCardDataSchemas,
14
+ memberEnrollmentFormCardDataSchema,
15
+ memberEnrollmentStatusCardDataSchema,
16
+ memberEnrollmentCtaCardDataSchema,
17
+ memberCardSummaryCardDataSchema,
18
+ } from '../src/index';
19
+ import { TBOX_EFFECT_EVENT_NAME } from '@tbox.cn/app-contracts';
20
+
21
+ describe('actionDraftSchema 判别联合(F1)', () => {
22
+ it('接受 send/link 两变体', () => {
23
+ expect(
24
+ actionDraftSchema.safeParse({
25
+ kind: 'send',
26
+ label: '查看详情',
27
+ value: '老凤祥',
28
+ }).success,
29
+ ).toBe(true);
30
+ expect(
31
+ actionDraftSchema.safeParse({ kind: 'send', label: '联系导购' }).success,
32
+ ).toBe(true);
33
+ expect(
34
+ actionDraftSchema.safeParse({
35
+ kind: 'link',
36
+ label: '拨打客服',
37
+ url: 'tel:0571-88888888',
38
+ }).success,
39
+ ).toBe(true);
40
+ });
41
+
42
+ it('拒绝 write 变体与未知 kind(无 token 机制支撑,不进联合)', () => {
43
+ expect(
44
+ actionDraftSchema.safeParse({
45
+ kind: 'write',
46
+ actionId: 'x',
47
+ label: '领券',
48
+ }).success,
49
+ ).toBe(false);
50
+ expect(
51
+ actionDraftSchema.safeParse({ kind: 'navigate', label: '切换商场' })
52
+ .success,
53
+ ).toBe(false);
54
+ // 旧形状(actionId 顶层)不再合法
55
+ expect(
56
+ actionDraftSchema.safeParse({ actionId: 'a', label: 'x', input: {} })
57
+ .success,
58
+ ).toBe(false);
59
+ });
60
+
61
+ it('link 缺 url 拒绝;send 缺 label 拒绝', () => {
62
+ expect(
63
+ actionDraftSchema.safeParse({ kind: 'link', label: '导航' }).success,
64
+ ).toBe(false);
65
+ expect(actionDraftSchema.safeParse({ kind: 'send' }).success).toBe(false);
66
+ });
67
+ });
68
+
69
+ describe('member-card-summary 动态展示契约', () => {
70
+ const base = {
71
+ userId: 'u1',
72
+ cardRef: 'card-1',
73
+ cardDisplay: '金卡会员',
74
+ status: 'active' as const,
75
+ availablePoints: 987,
76
+ };
77
+
78
+ it('接受通用卡面、消费进度和权益摘要,旧数据仍兼容', () => {
79
+ expect(memberCardSummaryCardDataSchema.safeParse(base).success).toBe(true);
80
+ expect(memberCardSummaryCardDataSchema.safeParse({
81
+ ...base,
82
+ cardImageUrl: 'https://cdn.example.test/card.png',
83
+ pointsRuleImageUrl: 'https://cdn.example.test/points-rule.jpg',
84
+ presentation: { variant: 'gold' },
85
+ progress: {
86
+ daySpentCents: 12_345,
87
+ yearSpentCents: 500_000,
88
+ percent: 25,
89
+ message: '今日已消费123.45元,年累计已消费5000.00元',
90
+ },
91
+ benefits: [{
92
+ benefitRef: 'b1',
93
+ name: '免费停车',
94
+ iconUrl: 'https://cdn.example.test/icon.png',
95
+ description: '可免2小时',
96
+ }],
97
+ }).success).toBe(true);
98
+ });
99
+
100
+ it('拒绝越界进度、负消费额与非 URL 图标', () => {
101
+ expect(memberCardSummaryCardDataSchema.safeParse({
102
+ ...base,
103
+ progress: { daySpentCents: -1, yearSpentCents: 0, percent: 101, message: '' },
104
+ benefits: [{ benefitRef: 'b1', name: '权益', iconUrl: 'not-a-url' }],
105
+ }).success).toBe(false);
106
+ });
107
+
108
+ it('拒绝非 URL 的积分规则图片', () => {
109
+ expect(memberCardSummaryCardDataSchema.safeParse({
110
+ ...base,
111
+ pointsRuleImageUrl: 'not-a-url',
112
+ }).success).toBe(false);
113
+ });
114
+ });
115
+
116
+ describe('0.3.2 D4 契约纯增(optional 双向兼容)', () => {
117
+ it('actionDraftSchema 经 shared/cards 单源可达(index + runtime 导出面)', async () => {
118
+ const runtime = await import('../src/runtime');
119
+ expect(runtime.actionDraftSchema).toBe(actionDraftSchema);
120
+ });
121
+
122
+ it('pageInfo.pageSize 上限 200(0.7.5 放宽:品牌导览常驻入口 port 直调回显过卡 schema)', () => {
123
+ const base = { page: 1, total: 12, hasMore: false };
124
+ expect(
125
+ pageInfoSchema.safeParse({ ...base, pageSize: 200 }).success,
126
+ ).toBe(true);
127
+ expect(
128
+ pageInfoSchema.safeParse({ ...base, pageSize: 201 }).success,
129
+ ).toBe(false);
130
+ });
131
+
132
+ it('五处条目 itemActions 均可缺省(旧数据零迁移通过)', () => {
133
+ // sg category-list categories[] / mall-directory floors[]
134
+ expect(
135
+ shoppingGuideCardDataSchemas['category-list'].safeParse({
136
+ categories: [{ code: 'c1', name: '餐饮', merchantCount: 12 }],
137
+ observedAt: '2026-08-01T00:00:00.000Z',
138
+ }).success,
139
+ ).toBe(true);
140
+ expect(
141
+ shoppingGuideCardDataSchemas['mall-directory'].safeParse({
142
+ mallId: 'm1',
143
+ mallName: '银泰',
144
+ observedAt: '2026-08-01T00:00:00.000Z',
145
+ buildings: [
146
+ {
147
+ buildingCode: 'b1',
148
+ buildingName: '主楼',
149
+ floors: [
150
+ {
151
+ floorCode: 'f1',
152
+ floorName: 'L1',
153
+ businessCategories: ['餐饮'],
154
+ merchantCount: 8,
155
+ },
156
+ ],
157
+ },
158
+ ],
159
+ }).success,
160
+ ).toBe(true);
161
+ // promotion offer-list / entitlement-list
162
+ expect(
163
+ promotionCardDataSchemas['promotion-offer-list'].safeParse({
164
+ mode: 'search',
165
+ items: [
166
+ {
167
+ offerRef: 'o1',
168
+ title: '满减券',
169
+ offerType: 'COUPON',
170
+ claimable: true,
171
+ tags: [],
172
+ },
173
+ ],
174
+ }).success,
175
+ ).toBe(true);
176
+ expect(
177
+ promotionCardDataSchemas['promotion-entitlement-list'].safeParse({
178
+ items: [
179
+ {
180
+ entitlementRef: 'e1',
181
+ title: '停车券',
182
+ entitlementType: 'COUPON',
183
+ status: 'EFFECTIVE',
184
+ },
185
+ ],
186
+ }).success,
187
+ ).toBe(true);
188
+ });
189
+
190
+ it('itemActions 含 send/link 合法;未知 kind 拒', () => {
191
+ const ok = shoppingGuideCardDataSchemas['category-list'].safeParse({
192
+ categories: [
193
+ {
194
+ code: 'c1',
195
+ name: '餐饮',
196
+ merchantCount: 2,
197
+ itemActions: [
198
+ { kind: 'send', label: '看看有哪些餐厅' },
199
+ { kind: 'link', label: '电话', url: 'tel:10086' },
200
+ ],
201
+ },
202
+ ],
203
+ observedAt: '2026-08-01T00:00:00.000Z',
204
+ });
205
+ expect(ok.success).toBe(true);
206
+ const bad = shoppingGuideCardDataSchemas['category-list'].safeParse({
207
+ categories: [
208
+ {
209
+ code: 'c1',
210
+ name: '餐饮',
211
+ merchantCount: 2,
212
+ itemActions: [{ kind: 'write', label: 'x' }],
213
+ },
214
+ ],
215
+ observedAt: '2026-08-01T00:00:00.000Z',
216
+ });
217
+ expect(bad.success).toBe(false);
218
+ });
219
+
220
+ it('refreshRequiredAt 格式校验;referenceUrl 仅 https', () => {
221
+ const base = {
222
+ products: [
223
+ {
224
+ productId: 'p1',
225
+ name: '商品',
226
+ price: {},
227
+ availability: {
228
+ status: 'in_stock',
229
+ label: '有货',
230
+ marketable: true,
231
+ observedAt: '2026-08-01T00:00:00.000Z',
232
+ },
233
+ merchant: { merchantId: 'm1', name: '商户' },
234
+ attributes: [],
235
+ caveats: [],
236
+ },
237
+ ],
238
+ observedAt: '2026-08-01T00:00:00.000Z',
239
+ };
240
+ expect(
241
+ shoppingGuideCardDataSchemas['product-comparison'].safeParse({
242
+ ...base,
243
+ refreshRequiredAt: '2026-08-18T12:00:00Z',
244
+ }).success,
245
+ ).toBe(true);
246
+ expect(
247
+ shoppingGuideCardDataSchemas['product-comparison'].safeParse({
248
+ ...base,
249
+ refreshRequiredAt: '2026/08/18',
250
+ }).success,
251
+ ).toBe(false);
252
+ const faqOk = shoppingGuideCardDataSchemas[
253
+ 'customer-service-faq'
254
+ ].safeParse({
255
+ query: '发票',
256
+ faqs: [
257
+ {
258
+ question: 'q',
259
+ answer: 'a',
260
+ relevanceScore: 0.9,
261
+ referenceUrl: 'https://example.com/faq',
262
+ },
263
+ ],
264
+ });
265
+ expect(faqOk.success).toBe(true);
266
+ const faqBad = shoppingGuideCardDataSchemas[
267
+ 'customer-service-faq'
268
+ ].safeParse({
269
+ query: '发票',
270
+ faqs: [
271
+ {
272
+ question: 'q',
273
+ answer: 'a',
274
+ relevanceScore: 0.9,
275
+ referenceUrl: 'http://example.com/faq',
276
+ },
277
+ ],
278
+ });
279
+ expect(faqBad.success).toBe(false);
280
+ });
281
+ });
282
+
283
+ describe('promotion 9 卡 schema round-trip', () => {
284
+ const offerListSample = {
285
+ mode: 'search',
286
+ items: [
287
+ {
288
+ offerRef: 'off_1',
289
+ title: '满 100 减 20 券',
290
+ offerType: 'COUPON',
291
+ benefitText: '满 100 减 20',
292
+ claimable: true,
293
+ tags: ['餐饮'],
294
+ },
295
+ ],
296
+ };
297
+ const offerDetailSample = {
298
+ offerRef: 'off_1',
299
+ title: '满 100 减 20 券',
300
+ offerType: 'COUPON',
301
+ rules: ['单笔满 100 可用', '限领 1 张'],
302
+ claimable: false,
303
+ unavailableReason: '需登录会员',
304
+ caveats: ['资格、库存和最终优惠以厂商实时校验为准'],
305
+ };
306
+ const entitlementListSample = {
307
+ items: [
308
+ {
309
+ entitlementRef: 'ent_1',
310
+ title: '停车 2 小时券',
311
+ entitlementType: 'COUPON',
312
+ status: 'EFFECTIVE',
313
+ codeMasked: 'AB**CD',
314
+ },
315
+ ],
316
+ };
317
+ const activityListSample = {
318
+ items: [
319
+ {
320
+ activityRef: 'act_1',
321
+ title: '周末亲子市集',
322
+ registrationRequired: true,
323
+ remainingQuota: 12,
324
+ },
325
+ ],
326
+ };
327
+ const activityDetailSample = {
328
+ activityRef: 'act_1',
329
+ title: '周末亲子市集',
330
+ registrationRequired: true,
331
+ registrationUrl:
332
+ 'alipays://platformapi/startapp?appId=demo&page=v%2Fmarketing%2Factivity%2Fdetail%3Fid%3Ddemo-activity',
333
+ sessions: [
334
+ {
335
+ sessionRef: 's_1',
336
+ title: '第一场',
337
+ startText: '周六 10:00',
338
+ status: 'OPEN',
339
+ },
340
+ ],
341
+ guideSteps: ['打开支付宝扫现场码', '选择场次', '凭报名码入场'],
342
+ conditions: ['限会员本人参与'],
343
+ };
344
+
345
+ it('全部 sample 解析通过', () => {
346
+ const samples: Record<string, unknown> = {
347
+ 'promotion-offer-list': offerListSample,
348
+ 'promotion-offer-detail': offerDetailSample,
349
+ 'promotion-entitlement-list': entitlementListSample,
350
+ 'promotion-activity-list': activityListSample,
351
+ 'promotion-activity-detail': activityDetailSample,
352
+ 'promotion-activity-category-list': {
353
+ items: [{ categoryId: 'family', name: '亲子' }],
354
+ },
355
+ 'promotion-best-deal': {
356
+ authoritative: true,
357
+ originalAmountCents: 10000,
358
+ savingsCents: 2000,
359
+ expectedPayCents: 8000,
360
+ appliedOfferRefs: ['off_1'],
361
+ },
362
+ };
363
+ for (const [cardType, data] of Object.entries(samples)) {
364
+ const schema =
365
+ promotionCardDataSchemas[
366
+ cardType as keyof typeof promotionCardDataSchemas
367
+ ];
368
+ const result = schema.safeParse(data);
369
+ expect(result.success, `${cardType} should parse`).toBe(true);
370
+ }
371
+ });
372
+
373
+ it('9 个传输卡型显式覆盖 19 个 legacy presentation 语义', () => {
374
+ for (const presentation of [
375
+ 'offer',
376
+ 'promotion',
377
+ 'coupon',
378
+ 'recommendation',
379
+ ] as const) {
380
+ expect(
381
+ promotionCardDataSchemas['promotion-offer-list'].safeParse({
382
+ ...offerListSample,
383
+ presentation,
384
+ }).success,
385
+ ).toBe(true);
386
+ }
387
+ for (const presentation of [
388
+ 'offer',
389
+ 'promotion',
390
+ 'promotion-guide',
391
+ 'coupon',
392
+ 'package',
393
+ ] as const) {
394
+ expect(
395
+ promotionCardDataSchemas['promotion-offer-detail'].safeParse({
396
+ ...offerDetailSample,
397
+ presentation,
398
+ }).success,
399
+ ).toBe(true);
400
+ }
401
+ for (const presentation of ['detail', 'schedule', 'guide'] as const) {
402
+ expect(
403
+ promotionCardDataSchemas['promotion-activity-detail'].safeParse({
404
+ ...activityDetailSample,
405
+ presentation,
406
+ }).success,
407
+ ).toBe(true);
408
+ }
409
+ expect(
410
+ promotionCardDataSchemas['promotion-offer-list'].safeParse({
411
+ ...offerListSample,
412
+ presentation: 'generic',
413
+ }).success,
414
+ ).toBe(false);
415
+ });
416
+ });
417
+
418
+ describe('parking 5 卡 schema round-trip', () => {
419
+ const paymentSample = {
420
+ quoteRef: 'q_1',
421
+ plateNo: '浙A12345',
422
+ feeItems: [{ name: '停车费(3 小时)', amountText: '15.00 元' }],
423
+ totalText: '15.00 元',
424
+ payableText: '10.00 元',
425
+ discountText: '5.00 元',
426
+ appliedBenefits: [
427
+ { benefitRef: 'b_1', title: '会员停车 95 折', discountText: '0.75 元' },
428
+ ],
429
+ selectableBenefits: [
430
+ { benefitRef: 'b_2', title: '积分抵 5 元', autoSelected: false },
431
+ ],
432
+ revision: 2,
433
+ caveats: ['费用以出场实际计费为准'],
434
+ };
435
+ const resultSample = {
436
+ paymentRef: 'p_1',
437
+ plateNo: '浙A12345',
438
+ status: 'PAID',
439
+ amountText: '10.00 元',
440
+ message: '支付成功',
441
+ };
442
+ const recordsSample = {
443
+ items: [
444
+ {
445
+ recordRef: 'r_1',
446
+ plateNo: '浙A12345',
447
+ amountText: '10.00 元',
448
+ paidAtText: '08-14 10:00',
449
+ status: 'PAID',
450
+ },
451
+ ],
452
+ total: 1,
453
+ hasMore: false,
454
+ caveats: ['仅展示近 6 个月记录'],
455
+ };
456
+ const vehiclesSample = {
457
+ bound: [
458
+ { vehicleRef: 'v_1', plateNo: '浙A12345', parked: true },
459
+ { vehicleRef: 'v_2', plateNo: '浙B67890' },
460
+ ],
461
+ };
462
+ const infoSample = {
463
+ kind: 'chargeRules',
464
+ title: '收费规则',
465
+ entries: [{ label: '首小时', value: '5.00 元' }],
466
+ };
467
+
468
+ it('全部 sample 解析通过', () => {
469
+ const samples: Record<string, unknown> = {
470
+ 'parking-payment': paymentSample,
471
+ 'parking-payment-result': resultSample,
472
+ 'parking-payment-records': recordsSample,
473
+ 'parking-vehicle-management': vehiclesSample,
474
+ 'parking-info': infoSample,
475
+ };
476
+ for (const [cardType, data] of Object.entries(samples)) {
477
+ const schema =
478
+ parkingCardDataSchemas[cardType as keyof typeof parkingCardDataSchemas];
479
+ expect(schema.safeParse(data).success, `${cardType} should parse`).toBe(
480
+ true,
481
+ );
482
+ }
483
+ });
484
+
485
+ it('payment 缺 revision 拒绝(乐观锁必填)', () => {
486
+ const { revision: _rev, ...withoutRevision } = paymentSample;
487
+ expect(
488
+ parkingCardDataSchemas['parking-payment'].safeParse(withoutRevision)
489
+ .success,
490
+ ).toBe(false);
491
+ });
492
+
493
+ it('缴费记录接受详情、开票状态与动作反馈字段', () => {
494
+ const result = parkingCardDataSchemas['parking-payment-records'].safeParse({
495
+ ...recordsSample,
496
+ message: '开票申请已提交',
497
+ items: [
498
+ {
499
+ ...recordsSample.items[0],
500
+ orderCompleted: true,
501
+ invoiceEligible: false,
502
+ invoiceIssued: true,
503
+ },
504
+ ],
505
+ });
506
+ expect(result.success).toBe(true);
507
+ expect(result.success && result.data.message).toBe('开票申请已提交');
508
+ expect(result.success && result.data.items[0]?.invoiceIssued).toBe(true);
509
+ });
510
+ });
511
+
512
+ describe('member 入会 2 卡 schema(015 扩展)', () => {
513
+ const formSample = {
514
+ userId: 'u_1',
515
+ state: 'not_enrolled',
516
+ formVersion: 'v2026.08',
517
+ fields: [
518
+ {
519
+ fieldId: 'mobile',
520
+ label: '手机号',
521
+ type: 'tel',
522
+ required: true,
523
+ readOnly: true,
524
+ prefill: '138****0000',
525
+ },
526
+ { fieldId: 'displayName', label: '姓名', type: 'text', required: false },
527
+ ],
528
+ consents: [
529
+ {
530
+ consentId: 'privacy',
531
+ version: '1',
532
+ title: '隐私政策',
533
+ required: true,
534
+ url: 'https://example.com/privacy',
535
+ },
536
+ ],
537
+ authorization: { status: 'authorized', mobileMasked: '138****0000' },
538
+ };
539
+ const statusSample = {
540
+ userId: 'u_1',
541
+ attemptRef: 'att_1',
542
+ state: 'pending_confirmation',
543
+ message: '厂商确认中,请稍后刷新',
544
+ };
545
+
546
+ it('form/status sample 解析通过', () => {
547
+ expect(
548
+ memberEnrollmentFormCardDataSchema.safeParse(formSample).success,
549
+ ).toBe(true);
550
+ expect(
551
+ memberEnrollmentStatusCardDataSchema.safeParse(statusSample).success,
552
+ ).toBe(true);
553
+ });
554
+
555
+ it('form 缺 formVersion/authorization 拒绝', () => {
556
+ const { formVersion: _f, ...a } = formSample;
557
+ expect(memberEnrollmentFormCardDataSchema.safeParse(a).success).toBe(false);
558
+ const { authorization: _au, ...b } = formSample;
559
+ expect(memberEnrollmentFormCardDataSchema.safeParse(b).success).toBe(false);
560
+ });
561
+ });
562
+
563
+ describe('member 入会 CTA 状态边界', () => {
564
+ const sample = {
565
+ userId: 'u_1',
566
+ authState: 'authenticated',
567
+ state: 'not_enrolled',
568
+ enrollUrl: 'alipays://platformapi/startapp?appId=demo&page=enroll',
569
+ } as const;
570
+
571
+ it('只接受已登录的未入会/已关闭状态与受支持地址', () => {
572
+ expect(memberEnrollmentCtaCardDataSchema.safeParse(sample).success).toBe(true);
573
+ expect(memberEnrollmentCtaCardDataSchema.safeParse({ ...sample, state: 'unknown' }).success).toBe(false);
574
+ expect(memberEnrollmentCtaCardDataSchema.safeParse({ ...sample, authState: 'unauthenticated' }).success).toBe(false);
575
+ expect(memberEnrollmentCtaCardDataSchema.safeParse({ ...sample, enrollUrl: 'javascript:alert(1)' }).success).toBe(false);
576
+ });
577
+ });
578
+
579
+ describe('host-effect 协议单源(P1,经 contracts-mall 传递可达)', () => {
580
+ it('TBOX_EFFECT_EVENT_NAME 从 contracts re-export 可达', async () => {
581
+ const contracts = await import('@tbox.cn/app-contracts');
582
+ expect(contracts.TBOX_EFFECT_EVENT_NAME).toBe('tbox:effect');
583
+ expect(TBOX_EFFECT_EVENT_NAME).toBe('tbox:effect');
584
+ expect(contracts.TBOX_EFFECT_EVENT_NAME).toBe(TBOX_EFFECT_EVENT_NAME);
585
+ });
586
+ });
@@ -0,0 +1,73 @@
1
+ /**
2
+ * 上下文统一化 Stage 2 锁:mallSlot(TIER0 ContextSlot 协议接线)、mallIdentityView(身份视图组装)。
3
+ */
4
+ import { describe, expect, it } from 'vitest';
5
+ import type { RequestContext } from '@tbox.cn/app-contracts';
6
+ import { mallSlot, parseMallScope, extractMallScope, MALL_SCOPE_KEY } from '../src/scope';
7
+ import { mallIdentityView } from '../src/server/mall-context';
8
+
9
+ const IDENTITY = { userId: 'u_1', source: 'external' as const };
10
+
11
+ const SCOPE = { miniAppId: 'mp_1', mallId: 'm_1', subjectId: 's_1' };
12
+
13
+ const FULL_CTX: RequestContext = {
14
+ identity: IDENTITY,
15
+ conversationId: 'c_1',
16
+ credentials: {
17
+ token: 'tok_1',
18
+ provider: 'mall-gateway',
19
+ attributes: { openid: 'o_abc', memberId: 'mb_9' },
20
+ },
21
+ attributes: { [MALL_SCOPE_KEY]: SCOPE },
22
+ };
23
+
24
+ describe('mallSlot(TIER0 槽位协议接线)', () => {
25
+ it('read:合法 attributes.mall → 解析结果;引用键 = MALL_SCOPE_KEY', () => {
26
+ expect(mallSlot.key).toBe('mall');
27
+ expect(mallSlot.read({ [MALL_SCOPE_KEY]: SCOPE })).toEqual(SCOPE);
28
+ });
29
+
30
+ it('read fail-soft:缺省/非法形状/解析失败 → undefined(不 throw)', () => {
31
+ expect(mallSlot.read(undefined)).toBeUndefined();
32
+ expect(mallSlot.read({})).toBeUndefined();
33
+ expect(mallSlot.read({ [MALL_SCOPE_KEY]: 'plain-string' })).toBeUndefined();
34
+ // v4:mallId-only 合法(miniAppId 可选——N1 缺陷修复);解析失败面收敛到 mallId 缺失/非法
35
+ expect(mallSlot.read({ [MALL_SCOPE_KEY]: { mallId: 'm_1' } })).toEqual({ mallId: 'm_1' });
36
+ expect(mallSlot.read({ [MALL_SCOPE_KEY]: { miniAppId: 'a' } })).toBeUndefined(); // 缺 mallId(会话边界键)
37
+ expect(mallSlot.read({ [MALL_SCOPE_KEY]: { mallId: '' } })).toBeUndefined(); // 空 mallId 非法
38
+ });
39
+
40
+ it('parseMallScope 薄包装与 slot 等价;extractMallScope 从 RequestContext 提取', () => {
41
+ expect(parseMallScope(FULL_CTX.attributes)).toEqual(mallSlot.read(FULL_CTX.attributes));
42
+ expect(extractMallScope(FULL_CTX)).toEqual(SCOPE);
43
+ });
44
+ });
45
+
46
+ describe('mallIdentityView(身份视图组装)', () => {
47
+ it('全字段:identity 原样 + scope + token + memberId + openId', () => {
48
+ const view = mallIdentityView(FULL_CTX);
49
+ expect(view.identity).toBe(IDENTITY);
50
+ expect(view.scope).toEqual(SCOPE);
51
+ expect(view.sessionToken).toBe('tok_1');
52
+ expect(view.memberId).toBe('mb_9');
53
+ expect(view.openId).toBe('o_abc');
54
+ });
55
+
56
+ it('最小形态:无 credentials/attributes → scope undefined、token 空串、可选字段缺省', () => {
57
+ const view = mallIdentityView({ identity: { userId: 'anon_1', source: 'anonymous' } });
58
+ expect(view.scope).toBeUndefined();
59
+ expect(view.sessionToken).toBe('');
60
+ expect(view.memberId).toBeUndefined();
61
+ expect(view.openId).toBeUndefined();
62
+ });
63
+
64
+ it('非法 mall scope(fail-soft)不阻断视图组装', () => {
65
+ const view = mallIdentityView({
66
+ identity: IDENTITY,
67
+ // v4:mallId-only 已合法(miniAppId 可选)——非法形状用「缺会话边界键 mallId」代表
68
+ attributes: { [MALL_SCOPE_KEY]: { miniAppId: 'app-only' } },
69
+ });
70
+ expect(view.scope).toBeUndefined();
71
+ expect(view.identity).toBe(IDENTITY);
72
+ });
73
+ });