@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,86 @@
1
+ /**
2
+ * mallControlPlaneOf 投影单测(integrations v4):
3
+ * root instances 注册表 → MallDeploymentControlPlane。
4
+ * 覆盖:无表 → 空平面 / 全字段投影 / attributes strict 拒拼错键(报允许键)/
5
+ * defaultInstanceId 解析(显式 / 单实例唯一解 / 多实例无默认 → undefined)/ 无表 + 独立默认。
6
+ */
7
+ import { describe, expect, it } from 'vitest';
8
+ import { mallControlPlaneOf } from '../src/server/mall-control-plane';
9
+ import type { IntegrationsConfig } from '@tbox.cn/app-contracts';
10
+
11
+ function config(overrides: Partial<IntegrationsConfig> = {}): IntegrationsConfig {
12
+ return { services: {}, ...overrides };
13
+ }
14
+
15
+ describe('mallControlPlaneOf:参考表投影', () => {
16
+ it('无参考表 → 空平面(模板装配零拓扑分支)', () => {
17
+ const cp = mallControlPlaneOf(config());
18
+ expect(cp.malls).toEqual([]);
19
+ expect(cp.defaultMallId).toBeUndefined();
20
+ });
21
+
22
+ it('双实例全字段投影(miniAppId/contactPhone/location)', () => {
23
+ const cp = mallControlPlaneOf(config({
24
+ instances: [
25
+ { id: 'mall-20001', name: '示例广场' },
26
+ {
27
+ id: 'mall-20002',
28
+ name: '另一广场',
29
+ attributes: {
30
+ miniAppId: '2021002188612345',
31
+ contactPhone: '0571-88888888',
32
+ location: { lat: 30.274, lon: 120.155, address: '杭州市西湖区' },
33
+ },
34
+ },
35
+ ],
36
+ defaultInstanceId: 'mall-20001',
37
+ }));
38
+ expect(cp.malls.length).toBe(2);
39
+ expect(cp.malls[0]).toEqual({ mallId: 'mall-20001', name: '示例广场' });
40
+ expect(cp.malls[1]).toEqual({
41
+ mallId: 'mall-20002',
42
+ name: '另一广场',
43
+ miniAppId: '2021002188612345',
44
+ contactPhone: '0571-88888888',
45
+ location: { lat: 30.274, lon: 120.155, address: '杭州市西湖区' },
46
+ });
47
+ expect(cp.defaultMallId).toBe('mall-20001');
48
+ });
49
+
50
+ it('attributes 拼错键 fail-fast(strict——错误消息含允许键清单)', () => {
51
+ expect(() =>
52
+ mallControlPlaneOf(config({
53
+ instances: [{ id: 'mall-20001', name: 'x', attributes: { miniAppid: 'typo' } }],
54
+ })),
55
+ ).toThrow(/miniAppId|contactPhone|location/);
56
+ });
57
+
58
+ it('defaultMallId:单实例免填唯一解', () => {
59
+ const cp = mallControlPlaneOf(config({ instances: [{ id: 'mall-20001', name: 'x' }] }));
60
+ expect(cp.defaultMallId).toBe('mall-20001');
61
+ });
62
+
63
+ it('defaultMallId:多实例无显式默认 → undefined(doctor warning 位)', () => {
64
+ const cp = mallControlPlaneOf(config({
65
+ instances: [
66
+ { id: 'mall-20001', name: 'a' },
67
+ { id: 'mall-20002', name: 'b' },
68
+ ],
69
+ }));
70
+ expect(cp.defaultMallId).toBeUndefined();
71
+ });
72
+
73
+ it('defaultMallId:无表 + defaultInstanceId 独立在场(env 注入动态默认形态)', () => {
74
+ const cp = mallControlPlaneOf(config({ defaultInstanceId: '42020300520' }));
75
+ expect(cp.malls).toEqual([]);
76
+ expect(cp.defaultMallId).toBe('42020300520');
77
+ });
78
+
79
+ it('defaultMallId:显式 null → undefined', () => {
80
+ const cp = mallControlPlaneOf(config({
81
+ instances: [{ id: 'mall-20001', name: 'x' }],
82
+ defaultInstanceId: null,
83
+ }));
84
+ expect(cp.defaultMallId).toBeUndefined();
85
+ });
86
+ });
@@ -0,0 +1,197 @@
1
+ /**
2
+ * 会员状态折叠单源锁(F6):classifyMemberOutcome 双轴优先级矩阵 +
3
+ * externalMemberRefOf 引用取值 + runtime 出口接线。
4
+ */
5
+ import { describe, expect, it } from 'vitest';
6
+ import { classifyMemberOutcome, externalMemberRefOf, memberGateCardOf } from '../src/services/member';
7
+ import type { MemberAccountSnapshot, MemberStatusResolution } from '../src/services/member';
8
+ import { resolveMemberGate } from '../src/services/member';
9
+ import * as runtime from '../src/runtime';
10
+
11
+ const anonymous = { source: 'anonymous' as const };
12
+ const dev = { source: 'dev' as const };
13
+
14
+ function account(authState: 'authenticated' | 'unauthenticated', state: MemberAccountSnapshot['state']): MemberAccountSnapshot {
15
+ return { authState, state, observedAt: '2026-08-30T00:00:00.000Z' };
16
+ }
17
+
18
+ describe('classifyMemberOutcome(双轴折叠单源)', () => {
19
+ it('平台匿名优先于账户态(轴优先级锁:不触 provider 语义)', () => {
20
+ expect(classifyMemberOutcome(anonymous, account('authenticated', 'active'))).toBe('unauthenticated');
21
+ });
22
+
23
+ it('匿名 × 无 account', () => {
24
+ expect(classifyMemberOutcome(anonymous, undefined)).toBe('unauthenticated');
25
+ });
26
+
27
+ it('非匿名 × 无 account → unknown(不可臆测为未入会)', () => {
28
+ expect(classifyMemberOutcome(dev, undefined)).toBe('unknown');
29
+ expect(classifyMemberOutcome(dev, null)).toBe('unknown');
30
+ });
31
+
32
+ it('非匿名 × 凭证轴未认证 → unauthenticated(独立于资格轴)', () => {
33
+ expect(classifyMemberOutcome(dev, account('unauthenticated', 'unknown'))).toBe('unauthenticated');
34
+ });
35
+
36
+ it('非匿名 × 四资格态直通', () => {
37
+ expect(classifyMemberOutcome(dev, account('authenticated', 'unknown'))).toBe('unknown');
38
+ expect(classifyMemberOutcome(dev, account('authenticated', 'active'))).toBe('active');
39
+ expect(classifyMemberOutcome(dev, account('authenticated', 'not_enrolled'))).toBe('not_enrolled');
40
+ expect(classifyMemberOutcome(dev, account('authenticated', 'closed'))).toBe('closed');
41
+ });
42
+
43
+ it('identity 缺席 × active → active(调用方已保证在场的形态)', () => {
44
+ expect(classifyMemberOutcome(undefined, account('authenticated', 'active'))).toBe('active');
45
+ });
46
+ });
47
+
48
+ describe('externalMemberRefOf(厂商侧会员标识取值)', () => {
49
+ it('真实卡号优先于 opaque 引用', () => {
50
+ expect(externalMemberRefOf({ ...account('authenticated', 'active'), primaryMallCardNo: 'C-1001', memberRef: 'member:C-1001' })).toBe('C-1001');
51
+ });
52
+
53
+ it('仅 opaque 引用时兜底透传(演示链形态)', () => {
54
+ expect(externalMemberRefOf({ ...account('authenticated', 'active'), memberRef: 'demo:member:demo-user' })).toBe('demo:member:demo-user');
55
+ });
56
+
57
+ it('双缺 → undefined(调用方按软失败处理)', () => {
58
+ expect(externalMemberRefOf(account('authenticated', 'active'))).toBeUndefined();
59
+ });
60
+ });
61
+
62
+ describe('runtime 出口接线(跨模块值导入必经此处)', () => {
63
+ it('classifyMemberOutcome / externalMemberRefOf 经 runtime 出口可达', () => {
64
+ expect(runtime.classifyMemberOutcome).toBeTypeOf('function');
65
+ expect(runtime.externalMemberRefOf).toBeTypeOf('function');
66
+ });
67
+
68
+ it('memberGateCardOf / resolveMemberGate 经 runtime 出口可达(不策展 = 运行时 undefined)', () => {
69
+ expect(runtime.memberGateCardOf).toBeTypeOf('function');
70
+ expect(runtime.resolveMemberGate).toBeTypeOf('function');
71
+ });
72
+ });
73
+
74
+ describe('memberGateCardOf(纯卡映射)', () => {
75
+ const enroll = { outcome: 'not_enrolled' as const, enrollUrl: 'alipays://enroll', loginUrl: 'alipays://login' };
76
+ const closed = { outcome: 'closed' as const, enrollUrl: 'alipays://enroll', loginUrl: 'alipays://login' };
77
+ const unauth = { outcome: 'unauthenticated' as const, enrollUrl: 'alipays://enroll', loginUrl: 'alipays://login' };
78
+
79
+ it('not_enrolled + enrollUrl → member-enrollment-cta,enroll 卡全字段', () => {
80
+ const card = memberGateCardOf('u-1', enroll);
81
+ expect(card).toBeDefined();
82
+ expect(card!.cardType).toBe('member-enrollment-cta');
83
+ expect(card!.data.userId).toBe('u-1');
84
+ expect(card!.data.authState).toBe('authenticated');
85
+ expect(card!.data.state).toBe('not_enrolled');
86
+ expect(card!.data.enrollUrl).toBe('alipays://enroll');
87
+ expect(card!.note).toBe('未入会 · 点击卡片完成入会');
88
+ });
89
+
90
+ it('closed + enrollUrl → member-enrollment-cta,state=closed 且 title 与 not_enrolled 一致(文案统一)', () => {
91
+ const card = memberGateCardOf('u-1', closed);
92
+ const enrolled = memberGateCardOf('u-1', enroll);
93
+ expect(card).toBeDefined();
94
+ expect(card!.data.state).toBe('closed');
95
+ expect(card!.data.title).toBe(enrolled!.data.title);
96
+ });
97
+
98
+ it('unauthenticated + loginUrl → member-login-cta,login 卡无 userId', () => {
99
+ const card = memberGateCardOf('u-1', unauth);
100
+ expect(card).toBeDefined();
101
+ expect(card!.cardType).toBe('member-login-cta');
102
+ expect(card!.data.loginUrl).toBe('alipays://login');
103
+ expect(card!.data.userId).toBeUndefined();
104
+ expect(card!.note).toBe('未登录 · 点击卡片登录');
105
+ });
106
+
107
+ it('未配深链 → 恒出卡(无 URL 字段;深链为可选透传素材)', () => {
108
+ const enrollCard = memberGateCardOf('u-1', { outcome: 'not_enrolled' });
109
+ expect(enrollCard).toBeDefined();
110
+ expect(enrollCard!.cardType).toBe('member-enrollment-cta');
111
+ expect(enrollCard!.data.enrollUrl).toBeUndefined();
112
+ expect(enrollCard!.data.userId).toBe('u-1');
113
+ const loginCard = memberGateCardOf('u-1', { outcome: 'unauthenticated' });
114
+ expect(loginCard).toBeDefined();
115
+ expect(loginCard!.cardType).toBe('member-login-cta');
116
+ expect(loginCard!.data.loginUrl).toBeUndefined();
117
+ });
118
+
119
+ it('空串/空白串深链归一为无 URL 卡(trim 归一,等价缺省)', () => {
120
+ const enrollCard = memberGateCardOf('u-1', { outcome: 'not_enrolled', enrollUrl: '' });
121
+ expect(enrollCard).toBeDefined();
122
+ expect(enrollCard!.data.enrollUrl).toBeUndefined();
123
+ const loginCard = memberGateCardOf('u-1', { outcome: 'unauthenticated', loginUrl: ' ' });
124
+ expect(loginCard).toBeDefined();
125
+ expect(loginCard!.data.loginUrl).toBeUndefined();
126
+ });
127
+
128
+ it('带 URL 时 enroll 卡全字段回归(userId/authState/state/title/enrollUrl 齐全)', () => {
129
+ const card = memberGateCardOf('u-1', enroll);
130
+ expect(card!.data).toMatchObject({
131
+ userId: 'u-1',
132
+ authState: 'authenticated',
133
+ state: 'not_enrolled',
134
+ title: '入会后继续',
135
+ enrollUrl: 'alipays://enroll',
136
+ });
137
+ });
138
+
139
+ it('active / unknown / unavailable → undefined(软放行态不产卡)', () => {
140
+ expect(memberGateCardOf('u-1', { outcome: 'active' })).toBeUndefined();
141
+ expect(memberGateCardOf('u-1', { outcome: 'unknown' })).toBeUndefined();
142
+ expect(memberGateCardOf('u-1', { outcome: 'unavailable' })).toBeUndefined();
143
+ });
144
+
145
+ it('空 userId + not_enrolled → undefined', () => {
146
+ expect(memberGateCardOf('', enroll)).toBeUndefined();
147
+ });
148
+ });
149
+
150
+ describe('resolveMemberGate(编排单源)', () => {
151
+ const reqCtx = { identity: { userId: 'u-1', source: 'dev' as const } } as never;
152
+ const statusOf = (s: Partial<MemberStatusResolution>): MemberStatusResolution =>
153
+ ({ outcome: 'active', ...s }) as MemberStatusResolution;
154
+
155
+ it('工厂缺席 → 放行(fail-open)', async () => {
156
+ await expect(resolveMemberGate(undefined, reqCtx)).resolves.toBeUndefined();
157
+ });
158
+
159
+ it('工厂抛错 → 放行(fail-open 兜底)', async () => {
160
+ const factory = async () => { throw new Error('boom'); };
161
+ await expect(resolveMemberGate(factory, reqCtx)).resolves.toBeUndefined();
162
+ });
163
+
164
+ it('软放行:active / unknown / unavailable', async () => {
165
+ for (const outcome of ['active', 'unknown', 'unavailable'] as const) {
166
+ await expect(resolveMemberGate(async () => statusOf({ outcome }), reqCtx)).resolves.toBeUndefined();
167
+ }
168
+ });
169
+
170
+ it('硬门禁:unauthenticated + loginUrl → login 卡', async () => {
171
+ const gate = await resolveMemberGate(
172
+ async () => statusOf({ outcome: 'unauthenticated', loginUrl: 'alipays://login' }),
173
+ reqCtx,
174
+ );
175
+ expect(gate).toBeDefined();
176
+ expect(gate!.card?.cardType).toBe('member-login-cta');
177
+ expect(gate!.notice).toBe('请先登录后再使用该服务');
178
+ });
179
+
180
+ it('硬门禁:not_enrolled + enrollUrl → enrollment 卡', async () => {
181
+ const gate = await resolveMemberGate(
182
+ async () => statusOf({ outcome: 'not_enrolled', enrollUrl: 'alipays://enroll' }),
183
+ reqCtx,
184
+ );
185
+ expect(gate).toBeDefined();
186
+ expect(gate!.card?.cardType).toBe('member-enrollment-cta');
187
+ expect(gate!.notice).toBe('完成入会后即可使用该服务');
188
+ });
189
+
190
+ it('硬门禁未配深链 → 恒出卡(无 URL 字段),notice 保留', async () => {
191
+ const gate = await resolveMemberGate(async () => statusOf({ outcome: 'closed' }), reqCtx);
192
+ expect(gate).toBeDefined();
193
+ expect(gate!.card?.cardType).toBe('member-enrollment-cta');
194
+ expect(gate!.card?.data.enrollUrl).toBeUndefined();
195
+ expect(gate!.notice).toBe('重新入会后可恢复使用该服务');
196
+ });
197
+ });
@@ -0,0 +1,111 @@
1
+ /**
2
+ * query-context 组装单源单测(0.8.0 消费层统一化):
3
+ * withMallScopeFromQuery 写入口契约(Q1-Q6)+ 消费原子/组合子(Q7-Q9)。
4
+ * Q6 = plazaId 契约锁:helper 只认 mallId,别名归一属客户端 mall-scope-url 单点职责。
5
+ */
6
+ import { describe, expect, it } from 'vitest';
7
+ import type { RequestContext } from '@tbox.cn/app-contracts';
8
+
9
+ import {
10
+ mallScopeOrEmpty,
11
+ mallSubjectIdOf,
12
+ toMallQueryContext,
13
+ withMallScopeFromQuery,
14
+ } from '../src/query-context';
15
+ import { parseMallScope } from '../src/scope';
16
+
17
+ function reqCtxOf(overrides: Partial<RequestContext> = {}): RequestContext {
18
+ return {
19
+ identity: { userId: 'u-1', source: 'external' },
20
+ credentials: { provider: 'mock', token: 't', attributes: {} },
21
+ attributes: {},
22
+ ...overrides,
23
+ };
24
+ }
25
+
26
+ describe('withMallScopeFromQuery(GET query 注入唯一写入口)', () => {
27
+ it('Q1: mallId+miniAppId 双全 → 注入且 parseMallScope 往返等价', () => {
28
+ const base = reqCtxOf();
29
+ const result = withMallScopeFromQuery(base, { mallId: ' m1 ', miniAppId: 'app1' });
30
+ expect(result).not.toBe(base);
31
+ expect(parseMallScope(result.attributes)).toEqual({ mallId: 'm1', miniAppId: 'app1' });
32
+ });
33
+
34
+ it('Q2: mallId 缺/空串/纯空白/非 string → 原样返回(不注入)', () => {
35
+ const base = reqCtxOf();
36
+ for (const query of [{}, { mallId: '' }, { mallId: ' ' }, { mallId: ['a', 'b'] }, { mallId: 42 }]) {
37
+ expect(withMallScopeFromQuery(base, query as Record<string, unknown>)).toBe(base);
38
+ }
39
+ });
40
+
41
+ it('Q3: query miniAppId 缺 + opts.miniAppId → 注入 opts 值', () => {
42
+ const result = withMallScopeFromQuery(reqCtxOf(), { mallId: 'm1' }, { miniAppId: 'from-opts' });
43
+ expect(parseMallScope(result.attributes)).toEqual({ mallId: 'm1', miniAppId: 'from-opts' });
44
+ });
45
+
46
+ it('Q4: mallId 在而 miniAppId 双缺(query 空 + 无 opts)→ 注入 mallId-only(v4:miniAppId 可选)', () => {
47
+ const base = reqCtxOf();
48
+ expect(parseMallScope(withMallScopeFromQuery(base, { mallId: 'm1' }).attributes)).toEqual({
49
+ mallId: 'm1',
50
+ });
51
+ expect(parseMallScope(withMallScopeFromQuery(base, { mallId: 'm1' }, {}).attributes)).toEqual({
52
+ mallId: 'm1',
53
+ });
54
+ // miniAppId 显式空串(trim 后空)双缺等价 → 仍 mallId-only
55
+ expect(
56
+ parseMallScope(withMallScopeFromQuery(base, { mallId: 'm1', miniAppId: '' }, { miniAppId: ' ' }).attributes),
57
+ ).toEqual({ mallId: 'm1' });
58
+ });
59
+
60
+ it('Q5: query miniAppId 优先于 opts(透传真源优先)', () => {
61
+ const result = withMallScopeFromQuery(
62
+ reqCtxOf(),
63
+ { mallId: 'm1', miniAppId: 'from-query' },
64
+ { miniAppId: 'from-opts' },
65
+ );
66
+ expect(parseMallScope(result.attributes)?.miniAppId).toBe('from-query');
67
+ });
68
+
69
+ it('Q6: 仅 plazaId(客户端别名)→ 不注入(契约锁:helper 零别名感知)', () => {
70
+ const base = reqCtxOf();
71
+ expect(withMallScopeFromQuery(base, { plazaId: 'p1' })).toBe(base);
72
+ });
73
+
74
+ it('Q5b: 覆盖既有 attributes.mall(与既有 spread 覆盖语义一致)', () => {
75
+ const base = reqCtxOf({ attributes: { mall: { mallId: 'old', miniAppId: 'old-app' } } });
76
+ const result = withMallScopeFromQuery(base, { mallId: 'new', miniAppId: 'new-app' });
77
+ expect(parseMallScope(result.attributes)).toEqual({ mallId: 'new', miniAppId: 'new-app' });
78
+ });
79
+ });
80
+
81
+ describe('消费原子与组合子', () => {
82
+ it('Q7: mallScopeOrEmpty——有 scope 原样透传;无 scope 空兜底新对象', () => {
83
+ const scoped = reqCtxOf({ attributes: { mall: { mallId: 'm1', miniAppId: 'a1' } } });
84
+ expect(mallScopeOrEmpty(scoped)).toEqual({ mallId: 'm1', miniAppId: 'a1' });
85
+ const bare = reqCtxOf();
86
+ const fallback = mallScopeOrEmpty(bare);
87
+ expect(fallback).toEqual({ miniAppId: '', mallId: '' });
88
+ expect(mallScopeOrEmpty(bare)).not.toBe(fallback); // 免疫共享引用
89
+ });
90
+
91
+ it('Q8: mallSubjectIdOf——anonymous → undefined;local/external/dev → userId', () => {
92
+ for (const source of ['local', 'external', 'dev'] as const) {
93
+ expect(mallSubjectIdOf(reqCtxOf({ identity: { userId: 'u-1', source } }))).toBe('u-1');
94
+ }
95
+ expect(mallSubjectIdOf(reqCtxOf({ identity: { userId: 'u-1', source: 'anonymous' } }))).toBeUndefined();
96
+ });
97
+
98
+ it('Q9: toMallQueryContext = mallScopeOrEmpty ∪ mallSubjectIdOf 的组合', () => {
99
+ const scoped = reqCtxOf({ attributes: { mall: { mallId: 'm1', miniAppId: 'a1' } } });
100
+ expect(toMallQueryContext(scoped)).toEqual({
101
+ scope: { mallId: 'm1', miniAppId: 'a1' },
102
+ subjectId: 'u-1',
103
+ requestContext: scoped,
104
+ });
105
+ const bare = reqCtxOf({ identity: { userId: 'u-1', source: 'anonymous' } });
106
+ expect(toMallQueryContext(bare)).toEqual({
107
+ scope: { miniAppId: '', mallId: '' },
108
+ requestContext: bare,
109
+ });
110
+ });
111
+ });
@@ -0,0 +1,38 @@
1
+ /**
2
+ * 请求凭证访问器通道锁(F5 单源):credentialsOpenId 必须读
3
+ * ExternalCredentials.attributes.openid(identity.ts 契约唯一通道)——
4
+ * 曾误读顶层 credentials.openid(契约无此键,恒 undefined),
5
+ * 致身份绑定 OpenId 恒空;本文件防止回退到顶层读取。
6
+ */
7
+ import { describe, expect, it } from 'vitest';
8
+ import type { RequestContext } from '@tbox.cn/app-contracts';
9
+ import { credentialsOpenId, credentialsMemberId, credentialsToken } from '../src/server/request-credentials';
10
+
11
+ const ctx = (credentials: unknown) => ({ credentials }) as never as RequestContext;
12
+
13
+ describe('request-credentials 凭证访问器', () => {
14
+ it('openid 从 attributes 通道提取(外部身份三元组)', () => {
15
+ const result = credentialsOpenId(ctx({
16
+ provider: 'mall-gateway',
17
+ token: 't_1',
18
+ attributes: { openid: 'o_abc', lightuid: 'l_1', signid: 's_1' },
19
+ }));
20
+ expect(result).toBe('o_abc');
21
+ });
22
+
23
+ it('顶层 openid 键不读取(契约无此键——缺陷形态回归锁)', () => {
24
+ const result = credentialsOpenId(ctx({ provider: 'x', token: 't', openid: 'top-level-o' }));
25
+ expect(result).toBe('');
26
+ });
27
+
28
+ it('attributes 缺失 / 非字符串值 → 空串不抛', () => {
29
+ expect(credentialsOpenId(ctx({ provider: 'x', token: 't' }))).toBe('');
30
+ expect(credentialsOpenId(ctx({ provider: 'x', token: 't', attributes: { openid: 123 } }))).toBe('');
31
+ expect(credentialsOpenId(ctx(undefined))).toBe('');
32
+ });
33
+
34
+ it('token 顶层键 / memberId attributes 通道(对照基准)', () => {
35
+ expect(credentialsToken(ctx({ provider: 'x', token: 'tok' }))).toBe('tok');
36
+ expect(credentialsMemberId(ctx({ provider: 'x', token: 't', attributes: { memberId: 'm_1' } }))).toBe('m_1');
37
+ });
38
+ });
@@ -0,0 +1,55 @@
1
+ /**
2
+ * 解析原语单测(017 槽位原子化):
3
+ * - mallScopeOf:模块 ports resolveService 入参 scope 形状钉死({mallId}|{},不含 miniAppId)
4
+ * - isExpectedProviderAbsence:兜底矩阵唯一谓词(三码 true;NOT_IMPLEMENTED 等其余 false)
5
+ */
6
+ import { describe, expect, it } from 'vitest';
7
+ import { mallScopeOf, parseMallScope } from '../src/scope';
8
+ import { isExpectedProviderAbsence } from '@tbox.cn/app-contracts';
9
+
10
+ describe('mallScopeOf(F10 行为钉死)', () => {
11
+ it('mall scope 在场 → { mallId }(flat,不含 miniAppId/subjectId)', () => {
12
+ const scope = mallScopeOf({
13
+ attributes: { mall: { mallId: 'm1', miniAppId: 'app1', subjectId: 's1' } },
14
+ });
15
+ expect(scope).toEqual({ mallId: 'm1' });
16
+ });
17
+
18
+ it('attributes 空对象 → {}(与四模块 ports 旧直连行为逐字等价)', () => {
19
+ expect(mallScopeOf({ attributes: {} })).toEqual({});
20
+ });
21
+
22
+ it('attributes undefined → {}', () => {
23
+ expect(mallScopeOf({})).toEqual({});
24
+ });
25
+
26
+ it('mall 键在场但形状非法 → {}(zod fail-soft)', () => {
27
+ expect(mallScopeOf({ attributes: { mall: { mallId: '' } } })).toEqual({});
28
+ expect(mallScopeOf({ attributes: { mall: 'oops' } })).toEqual({});
29
+ });
30
+
31
+ it('与 parseMallScope 派生关系一致(单源薄包装)', () => {
32
+ const attrs = { mall: { mallId: 'm2' } };
33
+ expect(mallScopeOf({ attributes: attrs })).toEqual(
34
+ parseMallScope(attrs)?.mallId ? { mallId: parseMallScope(attrs)!.mallId } : {},
35
+ );
36
+ });
37
+ });
38
+
39
+ describe('isExpectedProviderAbsence(兜底矩阵谓词)', () => {
40
+ const errOf = (code: string) => Object.assign(new Error('x'), { code });
41
+
42
+ it('三缺席码 → true(槽未配置/实例缺失/实例禁用)', () => {
43
+ expect(isExpectedProviderAbsence(errOf('PROVIDER_SERVICE_NOT_CONFIGURED'))).toBe(true);
44
+ expect(isExpectedProviderAbsence(errOf('PROVIDER_INSTANCE_NOT_FOUND'))).toBe(true);
45
+ expect(isExpectedProviderAbsence(errOf('PROVIDER_INSTANCE_DISABLED'))).toBe(true);
46
+ });
47
+
48
+ it('NOT_IMPLEMENTED / BINDING_UNRESOLVED / 普通错误 → false(上抛)', () => {
49
+ expect(isExpectedProviderAbsence(errOf('PROVIDER_NOT_IMPLEMENTED'))).toBe(false);
50
+ expect(isExpectedProviderAbsence(errOf('PROVIDER_BINDING_UNRESOLVED'))).toBe(false);
51
+ expect(isExpectedProviderAbsence(new Error('boom'))).toBe(false);
52
+ expect(isExpectedProviderAbsence(undefined)).toBe(false);
53
+ expect(isExpectedProviderAbsence(null)).toBe(false);
54
+ });
55
+ });
@@ -0,0 +1,78 @@
1
+ /**
2
+ * service slot 词汇交叉锁(017 槽位原子化):
3
+ * 共址 TS 常量并集 ≡ service-slots.json slots(逐元素相等,含计数)。
4
+ * JSON 是 guard(scripts/guard-service-slots.mjs)的真源;常量是 TS 消费方的带锁视图。
5
+ */
6
+ import { describe, expect, it } from 'vitest';
7
+ import slotsJson from '../src/service-slots.json';
8
+ import {
9
+ MEMBER_ACCOUNT_SERVICE,
10
+ MEMBER_PROFILE_SERVICE,
11
+ MEMBER_CARD_SERVICE,
12
+ MEMBER_BENEFITS_SERVICE,
13
+ MEMBER_ENROLLMENT_SERVICE,
14
+ } from '../src/services/member';
15
+ import {
16
+ PARKING_QUERY_SERVICE,
17
+ PARKING_PAYMENT_SERVICE,
18
+ PARKING_VEHICLE_SERVICE,
19
+ } from '../src/services/parking';
20
+ import {
21
+ PROMOTION_OFFERS_SERVICE,
22
+ PROMOTION_ENTITLEMENTS_SERVICE,
23
+ PROMOTION_ACTIVITIES_SERVICE,
24
+ PROMOTION_ACQUISITION_SERVICE,
25
+ } from '../src/services/promotion';
26
+ import {
27
+ SHOPPING_CATALOG_SERVICE,
28
+ SHOPPING_SHOPS_SERVICE,
29
+ SHOPPING_CATEGORIES_SERVICE,
30
+ SHOPPING_DIRECTORY_SERVICE,
31
+ } from '../src/services/shopping-guide';
32
+ import { MALL_INFO_SERVICE } from '../src/services/mall-info';
33
+ import { AUTH_LOGIN_SERVICE } from '../src/assembly';
34
+
35
+ const constants = [
36
+ MEMBER_ACCOUNT_SERVICE,
37
+ MEMBER_PROFILE_SERVICE,
38
+ MEMBER_CARD_SERVICE,
39
+ MEMBER_BENEFITS_SERVICE,
40
+ MEMBER_ENROLLMENT_SERVICE,
41
+ PARKING_QUERY_SERVICE,
42
+ PARKING_PAYMENT_SERVICE,
43
+ PARKING_VEHICLE_SERVICE,
44
+ PROMOTION_OFFERS_SERVICE,
45
+ PROMOTION_ENTITLEMENTS_SERVICE,
46
+ PROMOTION_ACTIVITIES_SERVICE,
47
+ PROMOTION_ACQUISITION_SERVICE,
48
+ SHOPPING_CATALOG_SERVICE,
49
+ SHOPPING_SHOPS_SERVICE,
50
+ SHOPPING_CATEGORIES_SERVICE,
51
+ SHOPPING_DIRECTORY_SERVICE,
52
+ AUTH_LOGIN_SERVICE,
53
+ MALL_INFO_SERVICE,
54
+ ];
55
+
56
+ describe('service slot 词汇交叉锁(JSON 真源 ↔ TS 共址常量)', () => {
57
+ it('计数 = 18(auth.alipay-login 与 mall.info 含在内)', () => {
58
+ expect(slotsJson.slots).toHaveLength(18);
59
+ expect(constants).toHaveLength(18);
60
+ });
61
+
62
+ it('常量并集 ≡ JSON slots(互为子集;无重复无缺漏)', () => {
63
+ const jsonSet = new Set(slotsJson.slots);
64
+ const constSet = new Set(constants);
65
+ expect(jsonSet.size).toBe(18);
66
+ expect(constSet.size).toBe(18);
67
+ for (const id of jsonSet) expect(constSet.has(id), `常量缺 ${id}`).toBe(true);
68
+ for (const id of constSet) expect(jsonSet.has(id), `JSON 缺 ${id}`).toBe(true);
69
+ });
70
+
71
+ it('词汇形状:domain.capability 且 domain ∈ 五域 + auth', () => {
72
+ const domains = new Set(['member', 'parking', 'promotion', 'shopping-guide', 'auth', 'mall']);
73
+ for (const id of slotsJson.slots) {
74
+ expect(id).toMatch(/^[a-z-]+\.[a-z-]+$/);
75
+ expect(domains.has(id.split('.')[0]), `${id} 域前缀未登记`).toBe(true);
76
+ }
77
+ });
78
+ });
package/tsconfig.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "extends": "../../tsconfig.base.json",
3
3
  "compilerOptions": {},
4
- "include": ["src"]
4
+ "include": ["src", "tests"]
5
5
  }