@tbox.cn/app-toolkit 0.4.0 → 0.6.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.
- package/dist/chunk-WUULQDOV.js +1 -0
- package/dist/contracts-resolver-PS2UH2G6.js +1 -0
- package/dist/index.d.ts +374 -150
- package/dist/index.js +9 -9
- package/package.json +2 -2
- package/src/assembly/provider-catalog.ts +1 -1
- package/src/assembly/resolve-key.ts +75 -0
- package/src/assembly/walk-manifests.ts +138 -85
- package/src/core/contracts-expected.ts +2 -2
- package/src/core/contracts-resolver.ts +12 -1
- package/src/core/module-schema.ts +90 -14
- package/src/dto.ts +80 -33
- package/src/factory.ts +78 -26
- package/src/index.ts +16 -7
- package/src/integrations/mock-bindings.ts +1 -1
- package/src/integrations/module-binding.ts +60 -0
- package/src/integrations/predicate-io.ts +26 -5
- package/src/integrations/predicate.ts +97 -57
- package/src/integrations/prune-bindings.ts +33 -14
- package/src/integrations/write.ts +47 -27
- package/src/views/app.ts +4 -4
- package/src/views/context.ts +15 -14
- package/src/views/integration-schemas.ts +76 -18
- package/src/views/modules.ts +93 -25
- package/src/views/providers.ts +5 -5
- package/src/views/service-detail.ts +1 -1
- package/src/views/service-resolutions.ts +14 -13
- package/tests/credentials-view.test.ts +2 -2
- package/tests/demo-app.ts +11 -6
- package/tests/dev-manifest-catalog.test.ts +14 -14
- package/tests/file-cache.test.ts +5 -5
- package/tests/module-resources.test.ts +148 -0
- package/tests/module-schema.test.ts +120 -20
- package/tests/resolve-key.test.ts +154 -0
- package/tests/vendor-schema-keywords.test.ts +74 -0
- package/tests/views.test.ts +585 -247
- package/tests/write-core.test.ts +117 -0
- package/dist/chunk-KPD3LUH4.js +0 -1
- package/dist/contracts-resolver-E4SECBOG.js +0 -1
- package/src/integrations/domain-binding.ts +0 -81
package/tests/views.test.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { createAppToolkit, type AppToolkit } from '../src/factory.js';
|
|
6
6
|
import { invalidateContractsResolver } from '../src/core/contracts-resolver.js';
|
|
7
7
|
import { walkManifests, type WalkManifestsResult } from '../src/assembly/walk-manifests.js';
|
|
8
|
-
import { buildIntegrationFieldRegistry, loadModuleFormViews, loadModuleIntegrationSchemas } from '../src/views/integration-schemas.js';
|
|
8
|
+
import { buildIntegrationFieldRegistry, loadModuleFormViews, loadModuleIntegrationSchemas, loadServiceFieldRegistry, loadServiceIntegrationSchemas } from '../src/views/integration-schemas.js';
|
|
9
9
|
import { resolveServiceDisplay } from '../src/views/context.js';
|
|
10
10
|
import type { ViewSnapshot } from '../src/views/context.js';
|
|
11
11
|
import { URL_METHOD_MAP } from './naming-alignment.test.js';
|
|
@@ -42,11 +42,12 @@ function harnessWithContracts(
|
|
|
42
42
|
'packages/module-parking/tbox.module.json',
|
|
43
43
|
JSON.stringify({
|
|
44
44
|
schemaVersion: 1,
|
|
45
|
-
|
|
45
|
+
id: 'module-parking',
|
|
46
46
|
version: '0.1.0',
|
|
47
47
|
kind: 'business',
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
declare: {
|
|
49
|
+
owns: [{ service: 'parking.query' }],
|
|
50
|
+
consumes: [{ service: 'parking.query', optional: false, name: '车位查询' }],
|
|
50
51
|
},
|
|
51
52
|
dependencies: { modules: [] },
|
|
52
53
|
env: [],
|
|
@@ -62,7 +63,7 @@ function harnessWithContracts(
|
|
|
62
63
|
{ id: 'mall-sh-01', name: '上海店' },
|
|
63
64
|
],
|
|
64
65
|
defaultInstanceId: 'mall-bj-01',
|
|
65
|
-
|
|
66
|
+
modules: { 'module-parking': { provider: 'wanda' } },
|
|
66
67
|
services: {
|
|
67
68
|
'parking.query': { provider: { provider: 'my-custom-api', credentialType: '-' }, implementation: 'my-custom-parking@1' },
|
|
68
69
|
'auth.alipay-login': {},
|
|
@@ -104,9 +105,12 @@ function layerHarness(): {
|
|
|
104
105
|
].join(' '),
|
|
105
106
|
});
|
|
106
107
|
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
108
|
+
// G 批:fixture 全字段标 x-tbox-global(表单收窄过滤后 E1/E2/E5 字段面逐字不变);
|
|
109
|
+
// wanda parking.json 另含未标记键 parkLotId(P16 负例——layer.properties 在场、registry.fields 不收)
|
|
110
|
+
const g = (sub: Record<string, unknown>): Record<string, unknown> => ({ ...sub, 'x-tbox-global': true });
|
|
107
111
|
const schemas = {
|
|
108
|
-
query: { type: 'object', properties: { baseUrl: { type: 'string', format: 'uri' } }, required: ['baseUrl'] },
|
|
109
|
-
parking: { type: 'object', properties: { mallId: { type: 'string' } } },
|
|
112
|
+
query: { type: 'object', properties: { baseUrl: g({ type: 'string', format: 'uri' }) }, required: ['baseUrl'] },
|
|
113
|
+
parking: { type: 'object', properties: { mallId: g({ type: 'string' }), parkLotId: { type: 'string', title: '车场组' } } },
|
|
110
114
|
};
|
|
111
115
|
// v4.6 joycity 代数 fixture:R1(baseUrl 双无 enum / env 同集异序 / envMixed 混布)/
|
|
112
116
|
// R2(options 深相等 / meta 异构)/ R3(lotId string≠integer / timeout number≠integer / untyped 无 type)/
|
|
@@ -115,14 +119,14 @@ function layerHarness(): {
|
|
|
115
119
|
query: {
|
|
116
120
|
type: 'object',
|
|
117
121
|
properties: {
|
|
118
|
-
baseUrl: { type: 'string', format: 'uri' },
|
|
119
|
-
lotId: { type: 'string' },
|
|
120
|
-
env: { type: 'string', enum: ['dev', 'staging', 'prod'] },
|
|
121
|
-
envMixed: { type: 'string', enum: ['a', 'b'] },
|
|
122
|
-
meta: { type: 'object', properties: { region: { type: 'string' } } },
|
|
123
|
-
options: { type: 'object', properties: { x: { type: 'string' } } },
|
|
124
|
-
untyped: { description: 'x' },
|
|
125
|
-
timeout: { type: 'number' },
|
|
122
|
+
baseUrl: { 'x-tbox-global': true, type: 'string', format: 'uri' },
|
|
123
|
+
lotId: { 'x-tbox-global': true, type: 'string' },
|
|
124
|
+
env: { 'x-tbox-global': true, type: 'string', enum: ['dev', 'staging', 'prod'] },
|
|
125
|
+
envMixed: { 'x-tbox-global': true, type: 'string', enum: ['a', 'b'] },
|
|
126
|
+
meta: { 'x-tbox-global': true, type: 'object', properties: { region: { 'x-tbox-global': true, type: 'string' } } },
|
|
127
|
+
options: { 'x-tbox-global': true, type: 'object', properties: { x: { 'x-tbox-global': true, type: 'string' } } },
|
|
128
|
+
untyped: { 'x-tbox-global': true, description: 'x' },
|
|
129
|
+
timeout: { 'x-tbox-global': true, type: 'number' },
|
|
126
130
|
},
|
|
127
131
|
required: ['baseUrl', 'lotId'],
|
|
128
132
|
},
|
|
@@ -130,15 +134,15 @@ function layerHarness(): {
|
|
|
130
134
|
type: 'object',
|
|
131
135
|
additionalProperties: false,
|
|
132
136
|
properties: {
|
|
133
|
-
apiKey: { type: 'string' },
|
|
134
|
-
baseUrl: { type: 'string' },
|
|
135
|
-
lotId: { type: 'integer' },
|
|
136
|
-
env: { type: 'string', enum: ['prod', 'dev', 'staging'] },
|
|
137
|
-
envMixed: { type: 'string' },
|
|
138
|
-
meta: { type: 'object', properties: { zone: { type: 'number' } } },
|
|
139
|
-
options: { type: 'object', properties: { x: { type: 'string' } } },
|
|
140
|
-
untyped: { type: 'string' },
|
|
141
|
-
timeout: { type: 'integer' },
|
|
137
|
+
apiKey: { 'x-tbox-global': true, type: 'string' },
|
|
138
|
+
baseUrl: { 'x-tbox-global': true, type: 'string' },
|
|
139
|
+
lotId: { 'x-tbox-global': true, type: 'integer' },
|
|
140
|
+
env: { 'x-tbox-global': true, type: 'string', enum: ['prod', 'dev', 'staging'] },
|
|
141
|
+
envMixed: { 'x-tbox-global': true, type: 'string' },
|
|
142
|
+
meta: { 'x-tbox-global': true, type: 'object', properties: { zone: { 'x-tbox-global': true, type: 'number' } } },
|
|
143
|
+
options: { 'x-tbox-global': true, type: 'object', properties: { x: { 'x-tbox-global': true, type: 'string' } } },
|
|
144
|
+
untyped: { 'x-tbox-global': true, type: 'string' },
|
|
145
|
+
timeout: { 'x-tbox-global': true, type: 'integer' },
|
|
142
146
|
},
|
|
143
147
|
required: ['apiKey', 'options'],
|
|
144
148
|
},
|
|
@@ -148,10 +152,10 @@ function layerHarness(): {
|
|
|
148
152
|
'packages/provider-w/tbox.module.json',
|
|
149
153
|
JSON.stringify({
|
|
150
154
|
schemaVersion: 1,
|
|
151
|
-
|
|
155
|
+
id: 'provider-w',
|
|
152
156
|
version: '0.1.0',
|
|
153
157
|
kind: 'business',
|
|
154
|
-
|
|
158
|
+
declare: {
|
|
155
159
|
providers: {
|
|
156
160
|
slots: [
|
|
157
161
|
{ service: 'parking.query', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/query.json' },
|
|
@@ -171,10 +175,10 @@ function layerHarness(): {
|
|
|
171
175
|
'packages/provider-j/tbox.module.json',
|
|
172
176
|
JSON.stringify({
|
|
173
177
|
schemaVersion: 1,
|
|
174
|
-
|
|
178
|
+
id: 'provider-j',
|
|
175
179
|
version: '0.1.0',
|
|
176
180
|
kind: 'business',
|
|
177
|
-
|
|
181
|
+
declare: {
|
|
178
182
|
providers: {
|
|
179
183
|
slots: [
|
|
180
184
|
{ service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c', configSchema: 'schemas/query.json' },
|
|
@@ -194,10 +198,10 @@ function layerHarness(): {
|
|
|
194
198
|
'packages/provider-p/tbox.module.json',
|
|
195
199
|
JSON.stringify({
|
|
196
200
|
schemaVersion: 1,
|
|
197
|
-
|
|
201
|
+
id: 'provider-p',
|
|
198
202
|
version: '0.1.0',
|
|
199
203
|
kind: 'business',
|
|
200
|
-
|
|
204
|
+
declare: {
|
|
201
205
|
providers: {
|
|
202
206
|
slots: [{ service: 'parking.payment', provider: 'partial', implementation: 'partial-payment@1', credentialType: 'partial-c' }],
|
|
203
207
|
},
|
|
@@ -211,10 +215,13 @@ function layerHarness(): {
|
|
|
211
215
|
'packages/module-parking/tbox.module.json',
|
|
212
216
|
JSON.stringify({
|
|
213
217
|
schemaVersion: 1,
|
|
214
|
-
|
|
218
|
+
id: 'module-parking',
|
|
215
219
|
version: '0.1.0',
|
|
216
220
|
kind: 'business',
|
|
217
|
-
|
|
221
|
+
declare: {
|
|
222
|
+
owns: [{ service: 'parking.query' }, { service: 'parking.payment' }],
|
|
223
|
+
consumes: [{ service: 'parking.query', optional: false }, { service: 'parking.payment', optional: true }],
|
|
224
|
+
},
|
|
218
225
|
dependencies: { modules: [] },
|
|
219
226
|
env: [],
|
|
220
227
|
}),
|
|
@@ -226,13 +233,103 @@ function layerHarness(): {
|
|
|
226
233
|
provider: 'wanda',
|
|
227
234
|
instances: [{ id: 'mall-bj-01', name: '北京店' }],
|
|
228
235
|
defaultInstanceId: 'mall-bj-01',
|
|
229
|
-
|
|
236
|
+
modules: { 'module-parking': { provider: 'wanda' } },
|
|
230
237
|
services: { 'parking.query': { provider: 'wanda', implementation: 'wanda-parking@1' } },
|
|
231
238
|
}),
|
|
232
239
|
);
|
|
233
240
|
return { app, tk: createAppToolkit(app.appDir), schemas, joycity };
|
|
234
241
|
}
|
|
235
242
|
|
|
243
|
+
/** E 系列基座(v4.7 D38 引入;v5.1 D39 提升至模块层共用——原 D38 describe 内定义):wanda 双槽
|
|
244
|
+
* (layerHarness 形态)+ joycity 双 impl(O6 形态)+ config/schemas/ok.json(O15 形态)+
|
|
245
|
+
* module-parking demand。schema 内容互异防断言串扰:
|
|
246
|
+
* wQuery={baseUrl} / wParking={mallId} / joycity@1={joycityUrl} / joycity@2={endpointV2} / ok={okEndpoint}。
|
|
247
|
+
* 每用例独立 eHarness()——fixture 全新 temp app,零 file-cache stat 指纹串扰(不取重写+bumpMtime 方案)。
|
|
248
|
+
* 供给矩阵:wanda 供给 {parking.query, parking.payment};joycity 仅供给 {parking.query}
|
|
249
|
+
* (provider-multi 双 impl 皆 query)——D39 用例 (厂商 × 服务) 选型须对照本矩阵。 */
|
|
250
|
+
function eHarness(): {
|
|
251
|
+
app: TempApp;
|
|
252
|
+
tk: AppToolkit;
|
|
253
|
+
schemas: { wQuery: object; wParking: object; joycity1: object; joycity2: object; ok: object };
|
|
254
|
+
} {
|
|
255
|
+
const app = createTempApp('tbox-d38-');
|
|
256
|
+
invalidateContractsResolver();
|
|
257
|
+
const store = join(app.appDir, 'contracts');
|
|
258
|
+
writeFakeContractsPackage(store, { version: '0.9.0' });
|
|
259
|
+
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
260
|
+
const schemas = {
|
|
261
|
+
wQuery: { type: 'object', properties: { baseUrl: { type: 'string', format: 'uri' } }, required: ['baseUrl'] },
|
|
262
|
+
wParking: { type: 'object', properties: { mallId: { type: 'string' } } },
|
|
263
|
+
joycity1: { type: 'object', properties: { joycityUrl: { type: 'string' } } },
|
|
264
|
+
joycity2: { type: 'object', properties: { endpointV2: { type: 'string' } } },
|
|
265
|
+
ok: { type: 'object', properties: { okEndpoint: { type: 'string' } } },
|
|
266
|
+
};
|
|
267
|
+
writeAppFile(
|
|
268
|
+
app.appDir,
|
|
269
|
+
'packages/provider-w/tbox.module.json',
|
|
270
|
+
JSON.stringify({
|
|
271
|
+
schemaVersion: 1,
|
|
272
|
+
id: 'provider-w',
|
|
273
|
+
version: '0.1.0',
|
|
274
|
+
kind: 'business',
|
|
275
|
+
declare: {
|
|
276
|
+
providers: {
|
|
277
|
+
slots: [
|
|
278
|
+
{ service: 'parking.query', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/query.json' },
|
|
279
|
+
{ service: 'parking.payment', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/parking.json' },
|
|
280
|
+
],
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
dependencies: { modules: [] },
|
|
284
|
+
env: [],
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
writeAppFile(app.appDir, 'packages/provider-w/schemas/query.json', JSON.stringify(schemas.wQuery));
|
|
288
|
+
writeAppFile(app.appDir, 'packages/provider-w/schemas/parking.json', JSON.stringify(schemas.wParking));
|
|
289
|
+
writeAppFile(
|
|
290
|
+
app.appDir,
|
|
291
|
+
'packages/provider-multi/tbox.module.json',
|
|
292
|
+
JSON.stringify({
|
|
293
|
+
schemaVersion: 1,
|
|
294
|
+
id: 'provider-multi',
|
|
295
|
+
version: '0.1.0',
|
|
296
|
+
kind: 'business',
|
|
297
|
+
declare: {
|
|
298
|
+
providers: {
|
|
299
|
+
slots: [
|
|
300
|
+
{ service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c', configSchema: 'schemas/query.json' },
|
|
301
|
+
{ service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@2', credentialType: 'joycity-c', configSchema: 'schemas/query2.json' },
|
|
302
|
+
],
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
dependencies: { modules: [] },
|
|
306
|
+
env: [],
|
|
307
|
+
}),
|
|
308
|
+
);
|
|
309
|
+
writeAppFile(app.appDir, 'packages/provider-multi/schemas/query.json', JSON.stringify(schemas.joycity1));
|
|
310
|
+
writeAppFile(app.appDir, 'packages/provider-multi/schemas/query2.json', JSON.stringify(schemas.joycity2));
|
|
311
|
+
writeAppFile(app.appDir, 'config/schemas/ok.json', JSON.stringify(schemas.ok));
|
|
312
|
+
writeAppFile(
|
|
313
|
+
app.appDir,
|
|
314
|
+
'packages/module-parking/tbox.module.json',
|
|
315
|
+
JSON.stringify({
|
|
316
|
+
schemaVersion: 1,
|
|
317
|
+
id: 'module-parking',
|
|
318
|
+
version: '0.1.0',
|
|
319
|
+
kind: 'business',
|
|
320
|
+
declare: { owns: [{ service: 'parking.query' }], consumes: [{ service: 'parking.query', optional: false }] },
|
|
321
|
+
dependencies: { modules: [] },
|
|
322
|
+
env: [],
|
|
323
|
+
}),
|
|
324
|
+
);
|
|
325
|
+
return { app, tk: createAppToolkit(app.appDir), schemas };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** integrations.json 单次写入(每用例独立 harness——零缓存串扰) */
|
|
329
|
+
function writeIntegrations(appDir: string, config: Record<string, unknown>): void {
|
|
330
|
+
writeAppFile(appDir, 'config/integrations.json', JSON.stringify(config));
|
|
331
|
+
}
|
|
332
|
+
|
|
236
333
|
describe('views/工厂(O 矩阵核心族)', () => {
|
|
237
334
|
let h: Harness;
|
|
238
335
|
|
|
@@ -259,8 +356,9 @@ describe('views/工厂(O 矩阵核心族)', () => {
|
|
|
259
356
|
expect(Object.keys(appView)).toEqual(['services', 'integration']);
|
|
260
357
|
const mod = await h.tk.loadModule('module-parking');
|
|
261
358
|
// v4.4 D34:domainKeys/providerBindings/configEditable 显式化(include 两键可选缺席——F4)
|
|
262
|
-
//
|
|
263
|
-
|
|
359
|
+
// v7 词汇统一:模块展示名 name 前置(恒发——兜底 id)
|
|
360
|
+
// resources 只读镜像(N2 读半边——基座 integrations modules 节无 resources → null 仍恒发)
|
|
361
|
+
expect(Object.keys(mod)).toEqual(['name', 'description', 'ownedServices', 'integration', 'resources', 'binding', 'configEditable', 'services']);
|
|
264
362
|
const svc = await h.tk.loadService('parking.query');
|
|
265
363
|
expect(Object.keys(svc).some((k) => ['resolution', 'issues', 'contracts'].includes(k))).toBe(false);
|
|
266
364
|
// 静态端点在 <0.9 下照常(永不 503——O13 反例联动)
|
|
@@ -513,8 +611,8 @@ describe('O6/O7/O10/O14(FX-3 补全)', () => {
|
|
|
513
611
|
'packages/provider-multi/tbox.module.json',
|
|
514
612
|
JSON.stringify({
|
|
515
613
|
schemaVersion: 1,
|
|
516
|
-
|
|
517
|
-
|
|
614
|
+
id: 'provider-multi',
|
|
615
|
+
declare: {
|
|
518
616
|
providers: {
|
|
519
617
|
slots: [
|
|
520
618
|
{ service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c', configSchema: 'schemas/query.json' },
|
|
@@ -585,8 +683,11 @@ describe('O6/O7/O10/O14(FX-3 补全)', () => {
|
|
|
585
683
|
// api.md §5 表
|
|
586
684
|
'CREDENTIAL_FILE_MISSING', 'CREDENTIAL_TYPE_MISMATCH', 'IMPL_WITHOUT_PROVIDER', 'INSTANCE_SUPPLY_UNMATCHED',
|
|
587
685
|
'SCHEMA_FILE_MISSING', 'DOMAIN_CONFIG_UNUSED', 'DOMAIN_CONFIG_INVALID', 'INSTANCE_KEY_NOT_REGISTERED',
|
|
588
|
-
'REALM_SWITCH_HINT', 'WILDCARD_ALIGNMENT', 'VENDOR_NOT_INSTALLED', 'BINDING_UNRESOLVED',
|
|
686
|
+
'REALM_SWITCH_HINT', 'WILDCARD_ALIGNMENT', 'VENDOR_NOT_INSTALLED', 'BINDING_UNRESOLVED',
|
|
589
687
|
'RESOURCE_TYPE_UNKNOWN', 'RESOURCE_NOT_DECLARED',
|
|
688
|
+
// v6 additive(api.md §5 表已收口):MODULE_CONFIG_DEAD / #17 GLOBAL_FIELD_UNDECLARED;
|
|
689
|
+
// v6 退役:DOMAIN_KEY_UNMATCHED(#14——domains 键退场)
|
|
690
|
+
'MODULE_CONFIG_DEAD', 'GLOBAL_FIELD_UNDECLARED',
|
|
590
691
|
// 谓词头映射表 additive(检查族编号映射:#3/#4/#5 内联/#13/v4)
|
|
591
692
|
'SLOT_NOT_DECLARED', 'SLOT_NOT_CONFIGURED', 'INLINE_PROVIDER_INVALID', 'MULTI_INSTANCE_NO_DEFAULT', 'INSTANCE_DISABLED',
|
|
592
693
|
]);
|
|
@@ -636,18 +737,18 @@ describe('A2 错误面(P-4 notes 优先 / P-5 畸形包 503)', () => {
|
|
|
636
737
|
});
|
|
637
738
|
});
|
|
638
739
|
|
|
639
|
-
describe('D34
|
|
640
|
-
it('D1
|
|
740
|
+
describe('v6 所有权族(D34 多域钉死退役 → ownedServices/binding 单数重写)', () => {
|
|
741
|
+
it('D1 单槽回归:ownedServices/binding/configEditable 装配 + integration 集成位原值', async () => {
|
|
641
742
|
const h2 = harnessWithContracts();
|
|
642
743
|
const mod = await h2.tk.loadModule('module-parking');
|
|
643
|
-
expect(mod.
|
|
744
|
+
expect(mod.ownedServices).toEqual(['parking.query']);
|
|
644
745
|
expect(mod.configEditable).toBe(true);
|
|
645
|
-
expect(mod.
|
|
746
|
+
expect(mod.binding).toEqual({ provider: 'wanda', layer: 'module' });
|
|
646
747
|
expect(mod.integration).toEqual({ provider: 'wanda' });
|
|
647
748
|
h2.app.dispose();
|
|
648
749
|
});
|
|
649
750
|
|
|
650
|
-
it('D2
|
|
751
|
+
it('D2 多域归属合法化(v6 正例化——跨域 owns 单键落位):binding 单值作用全 owned + PUT 全量替换', async () => {
|
|
651
752
|
const app = createTempApp('tbox-d2-');
|
|
652
753
|
invalidateContractsResolver();
|
|
653
754
|
const store = joinStore(app.appDir, 'contracts');
|
|
@@ -658,33 +759,30 @@ describe('D34 多域模块钉死(v4.4 F3)', () => {
|
|
|
658
759
|
'packages/module-multi/tbox.module.json',
|
|
659
760
|
JSON.stringify({
|
|
660
761
|
schemaVersion: 1,
|
|
661
|
-
|
|
762
|
+
id: 'module-multi',
|
|
662
763
|
version: '0.1.0',
|
|
663
764
|
kind: 'business',
|
|
664
|
-
|
|
765
|
+
declare: { owns: [{ service: 'parking.query' }, { service: 'member.account' }], consumes: [{ service: 'parking.query', optional: false }] },
|
|
665
766
|
dependencies: { modules: [] },
|
|
666
767
|
env: [],
|
|
667
768
|
}),
|
|
668
769
|
);
|
|
669
770
|
const tk = createAppToolkit(app.appDir);
|
|
670
771
|
const mod = await tk.loadModule('module-multi');
|
|
671
|
-
expect(mod.
|
|
772
|
+
expect(mod.ownedServices).toEqual(['member.account', 'parking.query']);
|
|
672
773
|
expect(mod.integration).toBeNull();
|
|
673
|
-
expect(mod.configEditable).toBe(
|
|
674
|
-
expect(mod.
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
await
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
message: expect.stringContaining('跨 2 个域(member、parking)'),
|
|
682
|
-
});
|
|
683
|
-
await expect(tk.writeModuleIntegration('module-multi', {})).rejects.toMatchObject({ code: 'VALIDATION_FAILED' });
|
|
774
|
+
expect(mod.configEditable).toBe(true);
|
|
775
|
+
expect(mod.binding).toEqual({ provider: null, layer: null });
|
|
776
|
+
// v6 写通道:多域归属合法——模块级 provider 单值落 modules[module-multi]
|
|
777
|
+
const r = await tk.writeModuleIntegration('module-multi', { provider: 'wanda' });
|
|
778
|
+
expect(r.written).toBe(true);
|
|
779
|
+
const mod2 = await tk.loadModule('module-multi');
|
|
780
|
+
expect(mod2.integration).toEqual({ provider: 'wanda' });
|
|
781
|
+
expect(mod2.binding).toEqual({ provider: 'wanda', layer: 'module' });
|
|
684
782
|
app.dispose();
|
|
685
783
|
});
|
|
686
784
|
|
|
687
|
-
it('D3
|
|
785
|
+
it('D3 零归属模块:ownedServices [] + configEditable false + PUT 400(zero-ownership)', async () => {
|
|
688
786
|
const app = createTempApp('tbox-d3-');
|
|
689
787
|
invalidateContractsResolver();
|
|
690
788
|
const store = joinStore(app.appDir, 'contracts');
|
|
@@ -695,63 +793,95 @@ describe('D34 多域模块钉死(v4.4 F3)', () => {
|
|
|
695
793
|
'packages/module-zero/tbox.module.json',
|
|
696
794
|
JSON.stringify({
|
|
697
795
|
schemaVersion: 1,
|
|
698
|
-
|
|
796
|
+
id: 'module-zero',
|
|
699
797
|
version: '0.1.0',
|
|
700
798
|
kind: 'business',
|
|
701
|
-
|
|
799
|
+
declare: {},
|
|
702
800
|
dependencies: { modules: [] },
|
|
703
801
|
env: [],
|
|
704
802
|
}),
|
|
705
803
|
);
|
|
706
804
|
const tk = createAppToolkit(app.appDir);
|
|
707
805
|
const mod = await tk.loadModule('module-zero');
|
|
708
|
-
expect(mod.
|
|
709
|
-
expect(mod.
|
|
806
|
+
expect(mod.ownedServices).toEqual([]);
|
|
807
|
+
expect(mod.binding).toEqual({ provider: null, layer: null });
|
|
710
808
|
expect(mod.integration).toBeNull();
|
|
711
809
|
await expect(tk.writeModuleIntegration('module-zero', { provider: 'x' })).rejects.toMatchObject({
|
|
712
810
|
code: 'VALIDATION_FAILED',
|
|
713
811
|
httpStatus: 400,
|
|
714
|
-
message: expect.stringContaining('
|
|
812
|
+
message: expect.stringContaining('未声明任何 declare.owns'),
|
|
715
813
|
});
|
|
716
814
|
app.dispose();
|
|
717
815
|
});
|
|
718
816
|
|
|
719
|
-
it('D4
|
|
817
|
+
it('D4 v6 互斥归属:双模块 owns 同服务 → OWNERSHIP_CONFLICT(先声明胜出)+ 双方可写(归属冲突≠写禁)', async () => {
|
|
720
818
|
const app = createTempApp('tbox-d4-');
|
|
721
819
|
invalidateContractsResolver();
|
|
722
820
|
const store = joinStore(app.appDir, 'contracts');
|
|
723
821
|
writeFakeContractsPackage(store, { version: '0.9.0' });
|
|
724
822
|
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
725
|
-
const manifest = (
|
|
823
|
+
const manifest = (id: string, service: string): string =>
|
|
726
824
|
JSON.stringify({
|
|
727
825
|
schemaVersion: 1,
|
|
728
|
-
|
|
826
|
+
id,
|
|
729
827
|
version: '0.1.0',
|
|
730
828
|
kind: 'business',
|
|
731
|
-
|
|
829
|
+
declare: { owns: [{ service }], consumes: [{ service, optional: false }] },
|
|
732
830
|
dependencies: { modules: [] },
|
|
733
831
|
env: [],
|
|
734
832
|
});
|
|
735
833
|
writeAppFile(app.appDir, 'packages/module-a/tbox.module.json', manifest('module-a', 'parking.alpha'));
|
|
736
|
-
writeAppFile(app.appDir, 'packages/module-b/tbox.module.json', manifest('module-b', 'parking.
|
|
834
|
+
writeAppFile(app.appDir, 'packages/module-b/tbox.module.json', manifest('module-b', 'parking.alpha'));
|
|
737
835
|
const tk = createAppToolkit(app.appDir);
|
|
836
|
+
const walk = tk.walkManifests();
|
|
837
|
+
// 先声明胜出(packages/ 目录字母序:module-a 先)
|
|
838
|
+
expect(walk.ownership['parking.alpha']).toBe('module-a');
|
|
839
|
+
// N4 执法分层:冲突 = declarationIssues error(doctor 面向第三方组合态执法;写通道不拒——归属冲突非写禁)
|
|
840
|
+
const conflict = walk.declarationIssues.find((i) => i.code === 'OWNERSHIP_CONFLICT');
|
|
841
|
+
expect(conflict).toBeDefined();
|
|
842
|
+
expect(conflict!.severity).toBe('error');
|
|
843
|
+
expect(conflict!.message).toContain('module-b');
|
|
844
|
+
// 双方 configEditable 均 true(owns>0 唯一判据——v6 无 shared-key 写禁)
|
|
738
845
|
const a = await tk.loadModule('module-a');
|
|
739
846
|
const b = await tk.loadModule('module-b');
|
|
740
|
-
expect(a.configEditable).toBe(
|
|
741
|
-
expect(b.configEditable).toBe(
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
847
|
+
expect(a.configEditable).toBe(true);
|
|
848
|
+
expect(b.configEditable).toBe(true);
|
|
849
|
+
app.dispose();
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
it('D6 偏斜态 503 第四分支(P1 标记门):v5 contracts 假件(无 OWNERSHIP_BINDING_V6)+ v6 toolkit → 503', async () => {
|
|
853
|
+
const app = createTempApp('tbox-d6-');
|
|
854
|
+
invalidateContractsResolver();
|
|
855
|
+
const store = joinStore(app.appDir, 'contracts');
|
|
856
|
+
writeFakeContractsPackage(store, { version: '0.9.0', withMarker: false });
|
|
857
|
+
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
858
|
+
writeAppFile(app.appDir, 'packages/module-parking/tbox.module.json', JSON.stringify({
|
|
859
|
+
schemaVersion: 1, id: 'module-parking', version: '0.1.0', kind: 'business',
|
|
860
|
+
declare: { owns: [{ service: 'parking.query' }] },
|
|
861
|
+
}));
|
|
862
|
+
const tk = createAppToolkit(app.appDir);
|
|
863
|
+
// 写路径门控(求值轴同此门——assertContractsUsable 单点)
|
|
864
|
+
await expect(tk.writeModuleIntegration('module-parking', { provider: 'wanda' })).rejects.toMatchObject({
|
|
865
|
+
code: 'CONTRACTS_NOT_RESOLVED',
|
|
866
|
+
httpStatus: 503,
|
|
867
|
+
message: expect.stringContaining('OWNERSHIP_BINDING_V6'),
|
|
746
868
|
});
|
|
869
|
+
await expect(tk.loadServiceResolutions()).rejects.toMatchObject({ code: 'CONTRACTS_NOT_RESOLVED', httpStatus: 503 });
|
|
870
|
+
// 静态端点照常(永不 503——门控 policy at edge 只拦求值/写/migrate)
|
|
871
|
+
const mod = await tk.loadModule('module-parking');
|
|
872
|
+
expect(mod.ownedServices).toEqual(['parking.query']);
|
|
747
873
|
app.dispose();
|
|
748
874
|
});
|
|
749
875
|
|
|
750
|
-
it('D5 死键扫尾:
|
|
876
|
+
it('D5 死键扫尾:providerBindings/moduleDomainWritable/domain-binding 全链删除(源文件内容断言)', () => {
|
|
751
877
|
const read = (rel: string): string => readFileSync(fileURLToPath(new URL(rel, import.meta.url)), 'utf8');
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
expect(read('../src/
|
|
878
|
+
const exists = (rel: string): boolean => readFileSync(fileURLToPath(new URL(rel, import.meta.url)), 'utf8').length >= 0;
|
|
879
|
+
void exists;
|
|
880
|
+
expect(read('../src/views/modules.ts')).not.toContain('providerBindings:');
|
|
881
|
+
expect(read('../src/integrations/write.ts')).not.toContain('moduleDomain');
|
|
882
|
+
expect(read('../src/index.ts')).not.toContain('domain-binding');
|
|
883
|
+
// v6 新面在场(正向锚)
|
|
884
|
+
expect(read('../src/integrations/module-binding.ts')).toContain('moduleOwnershipWritable');
|
|
755
885
|
});
|
|
756
886
|
});
|
|
757
887
|
|
|
@@ -845,7 +975,7 @@ describe('v4.4 include 协议 + Entry.layer + 层级 schema(F4/F5)', () => {
|
|
|
845
975
|
const h2 = layerHarness();
|
|
846
976
|
const mod = await h2.tk.loadModule('module-parking', { include: ['integrationSchemas'] });
|
|
847
977
|
expect(mod.integrationSchemas).toEqual({
|
|
848
|
-
provider: 'wanda', //
|
|
978
|
+
provider: 'wanda', // resolveModuleBinding 单源(modules['module-parking'])
|
|
849
979
|
services: {
|
|
850
980
|
'parking.query': { configSchema: h2.schemas.query },
|
|
851
981
|
'parking.payment': { configSchema: h2.schemas.parking },
|
|
@@ -874,7 +1004,7 @@ describe('v4.4 include 协议 + Entry.layer + 层级 schema(F4/F5)', () => {
|
|
|
874
1004
|
'config/integrations.json',
|
|
875
1005
|
JSON.stringify({
|
|
876
1006
|
instances: [{ id: 'mall-bj-01', name: '北京店' }],
|
|
877
|
-
|
|
1007
|
+
modules: { 'module-parking': { provider: 'wanda' } },
|
|
878
1008
|
services: { 'parking.query': { provider: 'wanda', implementation: 'wanda-parking@1' } },
|
|
879
1009
|
}),
|
|
880
1010
|
);
|
|
@@ -903,10 +1033,10 @@ describe('v4.4 include 协议 + Entry.layer + 层级 schema(F4/F5)', () => {
|
|
|
903
1033
|
'packages/module-multi/tbox.module.json',
|
|
904
1034
|
JSON.stringify({
|
|
905
1035
|
schemaVersion: 1,
|
|
906
|
-
|
|
1036
|
+
id: 'module-multi',
|
|
907
1037
|
version: '0.1.0',
|
|
908
1038
|
kind: 'business',
|
|
909
|
-
|
|
1039
|
+
declare: { owns: [{ service: 'parking.query' }], consumes: [{ service: 'parking.query', optional: false }, { service: 'member.account', optional: false }] },
|
|
910
1040
|
dependencies: { modules: [] },
|
|
911
1041
|
env: [],
|
|
912
1042
|
}),
|
|
@@ -930,10 +1060,10 @@ describe('模块清单 provider 家族排除(2026-09-16 双缺陷批——供
|
|
|
930
1060
|
'packages/provider-custom/tbox.module.json',
|
|
931
1061
|
JSON.stringify({
|
|
932
1062
|
schemaVersion: 1,
|
|
933
|
-
|
|
1063
|
+
id: 'provider-custom',
|
|
934
1064
|
version: '0.1.0',
|
|
935
1065
|
kind: 'business',
|
|
936
|
-
|
|
1066
|
+
declare: { providers: { slots: [] }, consumes: [], owns: [] },
|
|
937
1067
|
dependencies: { modules: [] },
|
|
938
1068
|
env: [],
|
|
939
1069
|
}),
|
|
@@ -951,8 +1081,8 @@ describe('模块清单 provider 家族排除(2026-09-16 双缺陷批——供
|
|
|
951
1081
|
join('node_modules', '@tbox.cn', 'app-provider-mock', 'tbox.module.json'),
|
|
952
1082
|
JSON.stringify({
|
|
953
1083
|
schemaVersion: 1,
|
|
954
|
-
|
|
955
|
-
|
|
1084
|
+
id: 'provider-mock',
|
|
1085
|
+
declare: {
|
|
956
1086
|
providers: { slots: [{ service: 'parking.query', provider: 'mock', implementation: 'mock-parking@1', credentialType: '-' }] },
|
|
957
1087
|
},
|
|
958
1088
|
}),
|
|
@@ -970,11 +1100,11 @@ describe('模块清单 provider 家族排除(2026-09-16 双缺陷批——供
|
|
|
970
1100
|
expect(walk.modules.map((m) => m.id).sort()).toEqual(['module-parking', 'provider-custom', 'vendor-mock']);
|
|
971
1101
|
// 详情 200 保持(装包仍合法——零域空态;写通道零域由 D34 守卫拦)
|
|
972
1102
|
const custom = await h2.tk.loadModule('provider-custom');
|
|
973
|
-
expect(custom.
|
|
1103
|
+
expect(custom.ownedServices).toEqual([]);
|
|
974
1104
|
expect(custom.configEditable).toBe(false);
|
|
975
1105
|
expect(custom.services).toEqual([]);
|
|
976
1106
|
const mock = await h2.tk.loadModule('vendor-mock');
|
|
977
|
-
expect(mock.
|
|
1107
|
+
expect(mock.ownedServices).toEqual([]);
|
|
978
1108
|
// 供给轴不受影响:provider-mock 槽位照常进 catalog(清单排除不伤聚合)
|
|
979
1109
|
const providers = await h2.tk.loadProviders();
|
|
980
1110
|
expect(providers.providers.map((p) => p.provider)).toContain('mock');
|
|
@@ -983,93 +1113,6 @@ describe('模块清单 provider 家族排除(2026-09-16 双缺陷批——供
|
|
|
983
1113
|
});
|
|
984
1114
|
|
|
985
1115
|
describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
|
|
986
|
-
/** E 系列基座:wanda 双槽(layerHarness 形态)+ joycity 双 impl(O6 形态)+ config/schemas/ok.json
|
|
987
|
-
* (O15 形态)+ module-parking demand。schema 内容互异防断言串扰:
|
|
988
|
-
* wQuery={baseUrl} / wParking={mallId} / joycity@1={joycityUrl} / joycity@2={endpointV2} / ok={okEndpoint}。
|
|
989
|
-
* 每用例独立 eHarness()——fixture 全新 temp app,零 file-cache stat 指纹串扰(不取重写+bumpMtime 方案)。 */
|
|
990
|
-
function eHarness(): {
|
|
991
|
-
app: TempApp;
|
|
992
|
-
tk: AppToolkit;
|
|
993
|
-
schemas: { wQuery: object; wParking: object; joycity1: object; joycity2: object; ok: object };
|
|
994
|
-
} {
|
|
995
|
-
const app = createTempApp('tbox-d38-');
|
|
996
|
-
invalidateContractsResolver();
|
|
997
|
-
const store = join(app.appDir, 'contracts');
|
|
998
|
-
writeFakeContractsPackage(store, { version: '0.9.0' });
|
|
999
|
-
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
1000
|
-
const schemas = {
|
|
1001
|
-
wQuery: { type: 'object', properties: { baseUrl: { type: 'string', format: 'uri' } }, required: ['baseUrl'] },
|
|
1002
|
-
wParking: { type: 'object', properties: { mallId: { type: 'string' } } },
|
|
1003
|
-
joycity1: { type: 'object', properties: { joycityUrl: { type: 'string' } } },
|
|
1004
|
-
joycity2: { type: 'object', properties: { endpointV2: { type: 'string' } } },
|
|
1005
|
-
ok: { type: 'object', properties: { okEndpoint: { type: 'string' } } },
|
|
1006
|
-
};
|
|
1007
|
-
writeAppFile(
|
|
1008
|
-
app.appDir,
|
|
1009
|
-
'packages/provider-w/tbox.module.json',
|
|
1010
|
-
JSON.stringify({
|
|
1011
|
-
schemaVersion: 1,
|
|
1012
|
-
name: 'provider-w',
|
|
1013
|
-
version: '0.1.0',
|
|
1014
|
-
kind: 'business',
|
|
1015
|
-
contributes: {
|
|
1016
|
-
providers: {
|
|
1017
|
-
slots: [
|
|
1018
|
-
{ service: 'parking.query', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/query.json' },
|
|
1019
|
-
{ service: 'parking.payment', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/parking.json' },
|
|
1020
|
-
],
|
|
1021
|
-
},
|
|
1022
|
-
},
|
|
1023
|
-
dependencies: { modules: [] },
|
|
1024
|
-
env: [],
|
|
1025
|
-
}),
|
|
1026
|
-
);
|
|
1027
|
-
writeAppFile(app.appDir, 'packages/provider-w/schemas/query.json', JSON.stringify(schemas.wQuery));
|
|
1028
|
-
writeAppFile(app.appDir, 'packages/provider-w/schemas/parking.json', JSON.stringify(schemas.wParking));
|
|
1029
|
-
writeAppFile(
|
|
1030
|
-
app.appDir,
|
|
1031
|
-
'packages/provider-multi/tbox.module.json',
|
|
1032
|
-
JSON.stringify({
|
|
1033
|
-
schemaVersion: 1,
|
|
1034
|
-
name: 'provider-multi',
|
|
1035
|
-
version: '0.1.0',
|
|
1036
|
-
kind: 'business',
|
|
1037
|
-
contributes: {
|
|
1038
|
-
providers: {
|
|
1039
|
-
slots: [
|
|
1040
|
-
{ service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c', configSchema: 'schemas/query.json' },
|
|
1041
|
-
{ service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@2', credentialType: 'joycity-c', configSchema: 'schemas/query2.json' },
|
|
1042
|
-
],
|
|
1043
|
-
},
|
|
1044
|
-
},
|
|
1045
|
-
dependencies: { modules: [] },
|
|
1046
|
-
env: [],
|
|
1047
|
-
}),
|
|
1048
|
-
);
|
|
1049
|
-
writeAppFile(app.appDir, 'packages/provider-multi/schemas/query.json', JSON.stringify(schemas.joycity1));
|
|
1050
|
-
writeAppFile(app.appDir, 'packages/provider-multi/schemas/query2.json', JSON.stringify(schemas.joycity2));
|
|
1051
|
-
writeAppFile(app.appDir, 'config/schemas/ok.json', JSON.stringify(schemas.ok));
|
|
1052
|
-
writeAppFile(
|
|
1053
|
-
app.appDir,
|
|
1054
|
-
'packages/module-parking/tbox.module.json',
|
|
1055
|
-
JSON.stringify({
|
|
1056
|
-
schemaVersion: 1,
|
|
1057
|
-
name: 'module-parking',
|
|
1058
|
-
version: '0.1.0',
|
|
1059
|
-
kind: 'business',
|
|
1060
|
-
contributes: { services: [{ service: 'parking.query', optional: false }] },
|
|
1061
|
-
dependencies: { modules: [] },
|
|
1062
|
-
env: [],
|
|
1063
|
-
}),
|
|
1064
|
-
);
|
|
1065
|
-
return { app, tk: createAppToolkit(app.appDir), schemas };
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
/** integrations.json 单次写入(每用例独立 harness——零缓存串扰) */
|
|
1069
|
-
function writeIntegrations(appDir: string, config: Record<string, unknown>): void {
|
|
1070
|
-
writeAppFile(appDir, 'config/integrations.json', JSON.stringify(config));
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
1116
|
it('E1 显式 string + catalog schema(分支②)', async () => {
|
|
1074
1117
|
const h = eHarness();
|
|
1075
1118
|
writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
|
|
@@ -1139,12 +1182,12 @@ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
|
|
|
1139
1182
|
|
|
1140
1183
|
it('E6 域级联(分支①)+ ghost 域厂商诚实态', async () => {
|
|
1141
1184
|
const h = eHarness();
|
|
1142
|
-
writeIntegrations(h.app.appDir, {
|
|
1185
|
+
writeIntegrations(h.app.appDir, { modules: { 'module-parking': { provider: 'wanda' } }, services: { 'parking.query': {} } });
|
|
1143
1186
|
const view = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
|
|
1144
1187
|
expect(view.integrationSchemas).toEqual({ provider: 'wanda', configSchema: h.schemas.wQuery });
|
|
1145
1188
|
// ghost 域厂商(catalog 外)→ configSchema null(诊断归谓词 #11——富化非门控)
|
|
1146
1189
|
const h2 = eHarness();
|
|
1147
|
-
writeIntegrations(h2.app.appDir, {
|
|
1190
|
+
writeIntegrations(h2.app.appDir, { modules: { 'module-parking': { provider: 'ghost-vendor' } }, services: { 'parking.query': {} } });
|
|
1148
1191
|
const ghost = await h2.tk.loadService('parking.query', { include: ['integrationSchemas'] });
|
|
1149
1192
|
expect(ghost.integrationSchemas).toEqual({ provider: 'ghost-vendor', configSchema: null });
|
|
1150
1193
|
h.app.dispose();
|
|
@@ -1216,10 +1259,10 @@ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
|
|
|
1216
1259
|
});
|
|
1217
1260
|
|
|
1218
1261
|
it('E14 malformed → 整字段 null(不落域级联——分支回归锚)', async () => {
|
|
1219
|
-
// ① 坏对象(缺 credentialType)——配
|
|
1262
|
+
// ① 坏对象(缺 credentialType)——配 modules 佐证未走模块级联
|
|
1220
1263
|
const h = eHarness();
|
|
1221
1264
|
writeIntegrations(h.app.appDir, {
|
|
1222
|
-
|
|
1265
|
+
modules: { 'module-parking': { provider: 'wanda' } },
|
|
1223
1266
|
services: { 'parking.query': { provider: { provider: 'my-api' } } },
|
|
1224
1267
|
});
|
|
1225
1268
|
const bad = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
|
|
@@ -1227,7 +1270,7 @@ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
|
|
|
1227
1270
|
// ② provider: null(谓词 #5 同判 INLINE_PROVIDER_INVALID——坏形态非缺席)
|
|
1228
1271
|
const h2 = eHarness();
|
|
1229
1272
|
writeIntegrations(h2.app.appDir, {
|
|
1230
|
-
|
|
1273
|
+
modules: { 'module-parking': { provider: 'wanda' } },
|
|
1231
1274
|
services: { 'parking.query': { provider: null } },
|
|
1232
1275
|
});
|
|
1233
1276
|
const nul = await h2.tk.loadService('parking.query', { include: ['integrationSchemas'] });
|
|
@@ -1239,7 +1282,7 @@ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
|
|
|
1239
1282
|
it('E15 provider 空串 → 走域级联(非 null)', async () => {
|
|
1240
1283
|
const h = eHarness();
|
|
1241
1284
|
writeIntegrations(h.app.appDir, {
|
|
1242
|
-
|
|
1285
|
+
modules: { 'module-parking': { provider: 'wanda' } },
|
|
1243
1286
|
services: { 'parking.query': { provider: '' } },
|
|
1244
1287
|
});
|
|
1245
1288
|
const view = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
|
|
@@ -1248,6 +1291,170 @@ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
|
|
|
1248
1291
|
});
|
|
1249
1292
|
});
|
|
1250
1293
|
|
|
1294
|
+
/**
|
|
1295
|
+
* v5.1 D39 测试族(服务轴向假想绑定——?serviceProvider= 表单双投影重定向)。
|
|
1296
|
+
* 供给矩阵(eHarness):wanda 供给 {parking.query, parking.payment};joycity 仅供给
|
|
1297
|
+
* {parking.query}(provider-multi 双 impl 皆 query)——用例 (厂商 × 服务) 选型须对照本矩阵:
|
|
1298
|
+
* catalog-miss 形态(D39-7)与豁免链正例(D39-6)不得混淆供给边界。每用例独立 eHarness
|
|
1299
|
+
* (零 file-cache stat 指纹串扰);D39-8/D39-9 用 layerHarness(x-tbox-global 标记 fixture)。
|
|
1300
|
+
*/
|
|
1301
|
+
describe('v5.1 D39(服务轴向假想绑定 serviceProvider)', () => {
|
|
1302
|
+
it('D39-1 假想 happy + I1:双投影切 joycity 且 provider 一致 + 单键边界正例', async () => {
|
|
1303
|
+
const h = eHarness();
|
|
1304
|
+
writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
|
|
1305
|
+
const both = ['integrationSchemas', 'integrationFieldRegistry'];
|
|
1306
|
+
const v = await h.tk.loadService('parking.query', { include: both, serviceProvider: 'joycity' });
|
|
1307
|
+
// I1:双投影同源同口径(fields = joycity schema 字段——catalog 复合键声明序首个 = @1 的 joycityUrl)
|
|
1308
|
+
expect(v.integrationSchemas).toEqual({ provider: 'joycity', configSchema: h.schemas.joycity1 });
|
|
1309
|
+
expect(v.integrationFieldRegistry?.provider).toBe('joycity');
|
|
1310
|
+
expect(v.integrationFieldRegistry!.fields.map((f) => f.key)).toEqual(['joycityUrl']);
|
|
1311
|
+
// 单键边界正例:仅 integrationSchemas + 轴参 → 200 假想计算;registry 未请求 = 缺席(S7-1 尾部先例)
|
|
1312
|
+
const only = await h.tk.loadService('parking.query', { include: ['integrationSchemas'], serviceProvider: 'joycity' });
|
|
1313
|
+
expect(only.integrationSchemas).toEqual({ provider: 'joycity', configSchema: h.schemas.joycity1 });
|
|
1314
|
+
expect('integrationFieldRegistry' in only).toBe(false);
|
|
1315
|
+
h.app.dispose();
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
it('D39-2 I2 锁:integration/suppliedBy/defaults 恒实际态', async () => {
|
|
1319
|
+
const h = eHarness();
|
|
1320
|
+
writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda', config: { baseUrl: 'https://w.example' } } } });
|
|
1321
|
+
const base = await h.tk.loadService('parking.query');
|
|
1322
|
+
const v = await h.tk.loadService('parking.query', { include: ['integrationSchemas', 'integrationFieldRegistry'], serviceProvider: 'joycity' });
|
|
1323
|
+
expect(v.integration).toEqual(base.integration); // 文件逐字原值不被重定向(预览不产生「已保存」误导)
|
|
1324
|
+
expect(v.suppliedBy).toEqual(base.suppliedBy); // catalog 静态切片恒实际
|
|
1325
|
+
expect(v.defaults).toEqual(base.defaults); // manifest 预填恒实际
|
|
1326
|
+
expect(v.integrationSchemas?.provider).toBe('joycity'); // 对照:表单层已重定向
|
|
1327
|
+
h.app.dispose();
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
it('D39-3 I3 锁 + 错轴静默忽略:空串 ≡ 缺席 ≡ 实际;appProvider/moduleProvider 被无视', async () => {
|
|
1331
|
+
const h = eHarness();
|
|
1332
|
+
writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
|
|
1333
|
+
const empty = await h.tk.loadService('parking.query', { include: ['integrationFieldRegistry'], serviceProvider: '' });
|
|
1334
|
+
const absent = await h.tk.loadService('parking.query', { include: ['integrationFieldRegistry'] });
|
|
1335
|
+
expect(empty.integrationFieldRegistry).toEqual(absent.integrationFieldRegistry);
|
|
1336
|
+
// 错轴参数(变量绕过多余属性检查——运行时按名读取,appProvider/moduleProvider 被无视;S7-4 先例镜像)
|
|
1337
|
+
const opts = { include: ['integrationFieldRegistry'], serviceProvider: '', appProvider: 'joycity', moduleProvider: 'joycity' };
|
|
1338
|
+
const wrongAxis = await h.tk.loadService('parking.query', opts);
|
|
1339
|
+
expect(wrongAxis.integrationFieldRegistry).toEqual(absent.integrationFieldRegistry);
|
|
1340
|
+
h.app.dispose();
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
it('D39-4 轴参严检矩阵:400 先于 404(镜像 S7-1 形态)', async () => {
|
|
1344
|
+
const h = eHarness();
|
|
1345
|
+
await expect(h.tk.loadService('parking.query', { serviceProvider: 'x' })).rejects.toMatchObject({
|
|
1346
|
+
code: 'VALIDATION_FAILED', httpStatus: 400, fields: ['serviceProvider'],
|
|
1347
|
+
});
|
|
1348
|
+
await expect(h.tk.loadService('parking.query', { include: [], serviceProvider: 'x' })).rejects.toMatchObject({
|
|
1349
|
+
code: 'VALIDATION_FAILED', httpStatus: 400, fields: ['serviceProvider'],
|
|
1350
|
+
});
|
|
1351
|
+
// 先于 404:ghost 服务 + 无表单键轴参 → 400(非 SERVICE_NOT_FOUND);表单键在场 → 放行至 404
|
|
1352
|
+
await expect(h.tk.loadService('ghost', { serviceProvider: 'x' })).rejects.toMatchObject({
|
|
1353
|
+
code: 'VALIDATION_FAILED', httpStatus: 400, fields: ['serviceProvider'],
|
|
1354
|
+
});
|
|
1355
|
+
await expect(h.tk.loadService('ghost', { include: ['integrationSchemas'], serviceProvider: 'x' })).rejects.toMatchObject({
|
|
1356
|
+
code: 'SERVICE_NOT_FOUND', httpStatus: 404,
|
|
1357
|
+
});
|
|
1358
|
+
h.app.dispose();
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
it('D39-5 同构承诺锁:假想 ≡ PUT 改写 provider 后实读(双投影;S6 写通道先例)', async () => {
|
|
1362
|
+
const h = eHarness();
|
|
1363
|
+
const both = ['integrationSchemas', 'integrationFieldRegistry'];
|
|
1364
|
+
writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
|
|
1365
|
+
const hyp = await h.tk.loadService('parking.query', { include: both, serviceProvider: 'joycity' });
|
|
1366
|
+
// 状态演进全经 toolkit 写通道——写后本实例缓存即刻失效,免外部写缓存关注
|
|
1367
|
+
const cur = await h.tk.loadService('parking.query');
|
|
1368
|
+
const w = await h.tk.writeServiceIntegration('parking.query', { ...cur.integration!, provider: 'joycity' });
|
|
1369
|
+
expect(w.written).toBe(true);
|
|
1370
|
+
const act = await h.tk.loadService('parking.query', { include: both });
|
|
1371
|
+
expect(act.integrationSchemas).toEqual(hyp.integrationSchemas);
|
|
1372
|
+
expect(act.integrationFieldRegistry).toEqual(hyp.integrationFieldRegistry);
|
|
1373
|
+
h.app.dispose();
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
it('D39-6 假想豁免链三分支:malformed 不污染 / 内联不读取无 H9 / 未绑定仍非 null', async () => {
|
|
1377
|
+
// ① malformed 节点(E14 形态)+ 假想 → 非 null(短路在节点读取之前——坏形态只影响实际态投影)
|
|
1378
|
+
const h = eHarness();
|
|
1379
|
+
writeIntegrations(h.app.appDir, {
|
|
1380
|
+
modules: { 'module-parking': { provider: 'wanda' } },
|
|
1381
|
+
services: { 'parking.query': { provider: { provider: 'my-api' } } },
|
|
1382
|
+
});
|
|
1383
|
+
const bad = await h.tk.loadService('parking.query', { include: ['integrationFieldRegistry'], serviceProvider: 'joycity' });
|
|
1384
|
+
expect(bad.integrationFieldRegistry).not.toBeNull();
|
|
1385
|
+
expect(bad.integrationFieldRegistry!.provider).toBe('joycity');
|
|
1386
|
+
// 实际态对照:同坏节点无轴参 → 整字段 null(E14 双态互证——预览与坏文件解耦)
|
|
1387
|
+
const badActual = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
|
|
1388
|
+
expect(badActual.integrationSchemas).toBeNull();
|
|
1389
|
+
h.app.dispose();
|
|
1390
|
+
// ② 内联 ref 逃逸节点(E4 形态)+ 假想 → 无 H9 400(短路不读节点内联 ref——换厂商即离开内联)
|
|
1391
|
+
const h2 = eHarness();
|
|
1392
|
+
writeIntegrations(h2.app.appDir, {
|
|
1393
|
+
services: { 'parking.query': { provider: { provider: 'my-api', credentialType: '-', configSchema: 'config/schemas-evil/x.json' } } },
|
|
1394
|
+
});
|
|
1395
|
+
const inline = await h2.tk.loadService('parking.query', { include: ['integrationSchemas'], serviceProvider: 'joycity' });
|
|
1396
|
+
expect(inline.integrationSchemas).toEqual({ provider: 'joycity', configSchema: h2.schemas.joycity1 });
|
|
1397
|
+
h2.app.dispose();
|
|
1398
|
+
// ③ 全链未绑定(E8 形态)+ 假想 → 仍非 null(服务轴恒可写——无 module 轴零归属豁免形态);
|
|
1399
|
+
// wanda 供给 payment(供给矩阵)→ mallId 字段在场
|
|
1400
|
+
const h3 = eHarness();
|
|
1401
|
+
writeIntegrations(h3.app.appDir, { services: { 'parking.payment': {} } });
|
|
1402
|
+
const unbound = await h3.tk.loadService('parking.payment', { include: ['integrationFieldRegistry'], serviceProvider: 'wanda' });
|
|
1403
|
+
// fields[].schema = 字段子 schema(非整文件 schema——G 批锁 :1777 同型)
|
|
1404
|
+
expect(unbound.integrationFieldRegistry).toEqual({
|
|
1405
|
+
provider: 'wanda', schemaless: [], unsupplied: [], additionalPropertiesAllowed: true,
|
|
1406
|
+
fields: [{ key: 'mallId', schema: { type: 'string' }, sources: [{ service: 'parking.payment', required: false }], global: false }],
|
|
1407
|
+
});
|
|
1408
|
+
h3.app.dispose();
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
it('D39-7 catalog miss:假想未知厂商 → 非 null 空集(schemaless=[service])', async () => {
|
|
1412
|
+
const h = eHarness();
|
|
1413
|
+
writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
|
|
1414
|
+
const v = await h.tk.loadService('parking.query', { include: ['integrationSchemas', 'integrationFieldRegistry'], serviceProvider: 'ghost-vendor' });
|
|
1415
|
+
expect(v.integrationSchemas).toEqual({ provider: 'ghost-vendor', configSchema: null });
|
|
1416
|
+
expect(v.integrationFieldRegistry).toEqual({
|
|
1417
|
+
provider: 'ghost-vendor', schemaless: ['parking.query'], unsupplied: [], additionalPropertiesAllowed: true, fields: [],
|
|
1418
|
+
});
|
|
1419
|
+
h.app.dispose();
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
it('D39-8 global 旗标假想透传:标记位如实(layerHarness——true 正例 + 未标记 false 不过滤)', async () => {
|
|
1423
|
+
const h = layerHarness();
|
|
1424
|
+
// joycity 供给 parking.query → 假想后 fields = joycity query schema 字段(fixture 全标记 → global:true)
|
|
1425
|
+
const joy = await h.tk.loadService('parking.query', { include: ['integrationFieldRegistry'], serviceProvider: 'joycity' });
|
|
1426
|
+
expect(joy.integrationFieldRegistry!.provider).toBe('joycity');
|
|
1427
|
+
expect(joy.integrationFieldRegistry!.fields.length).toBeGreaterThan(0);
|
|
1428
|
+
expect(joy.integrationFieldRegistry!.fields.every((f) => f.global)).toBe(true);
|
|
1429
|
+
// wanda 供给 parking.payment → mallId global:true / parkLotId global:false 在场(服务轴不过滤——G 批语义假想不变);
|
|
1430
|
+
// payment 先配置入词汇(P9:契约包 ∪ 已配置——layerHarness 初始 integrations.json 未配置 payment;
|
|
1431
|
+
// 覆盖写后 payment 节点 {} 实际态级联双缺 → null,假想 wanda 短路仍非 null——豁免链③同型,S7-7 外部写先例)
|
|
1432
|
+
writeAppFile(h.app.appDir, 'config/integrations.json', JSON.stringify({ services: { 'parking.payment': {} } }));
|
|
1433
|
+
const wan = await h.tk.loadService('parking.payment', { include: ['integrationFieldRegistry'], serviceProvider: 'wanda' });
|
|
1434
|
+
expect(wan.integrationFieldRegistry!.fields.map((f) => f.key).sort()).toEqual(['mallId', 'parkLotId']);
|
|
1435
|
+
expect(wan.integrationFieldRegistry!.fields.find((f) => f.key === 'mallId')!.global).toBe(true);
|
|
1436
|
+
expect(wan.integrationFieldRegistry!.fields.find((f) => f.key === 'parkLotId')!.global).toBe(false);
|
|
1437
|
+
h.app.dispose();
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
it('D39-9 单源锁:loadServiceFieldRegistry(snap,s,X) ≡ buildIntegrationFieldRegistry over 链函数', async () => {
|
|
1441
|
+
const h = layerHarness();
|
|
1442
|
+
const { loadViewSnapshotSync } = await import('../src/views/context.js');
|
|
1443
|
+
const snap = loadViewSnapshotSync({ appDir: h.app.appDir }, await h.tk.resolveContracts());
|
|
1444
|
+
const X = 'joycity';
|
|
1445
|
+
const reg = loadServiceFieldRegistry(snap, 'parking.query', X);
|
|
1446
|
+
const schemas = loadServiceIntegrationSchemas(snap, 'parking.query', X);
|
|
1447
|
+
expect(schemas).not.toBeNull();
|
|
1448
|
+
// 结构性断言(依赖契约锁——registry 旁路重取 schema 破 I1 即刻红;S8 同族意图)
|
|
1449
|
+
const manual = buildIntegrationFieldRegistry({
|
|
1450
|
+
provider: schemas!.provider,
|
|
1451
|
+
services: { 'parking.query': { configSchema: schemas!.configSchema } },
|
|
1452
|
+
});
|
|
1453
|
+
expect(reg).toEqual(manual);
|
|
1454
|
+
h.app.dispose();
|
|
1455
|
+
});
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1251
1458
|
/**
|
|
1252
1459
|
* v4.6 D36/D37 测试族(S5 接续 S1-S4):S5a 纯代数(直调合成 layer 字面量,零 fs)/
|
|
1253
1460
|
* S5b 接线序 + 等价锁 / S6 同构(假想 ≡ 写后实读)/ S7 边界(400·null 两态·I2·空集)/
|
|
@@ -1264,15 +1471,15 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1264
1471
|
unsupplied: [],
|
|
1265
1472
|
additionalPropertiesAllowed: false,
|
|
1266
1473
|
fields: [
|
|
1267
|
-
{ key: 'baseUrl', schema: { type: 'string', format: 'uri' }, sources: [src('parking.query', true), src('parking.payment', false)] },
|
|
1268
|
-
{ key: 'lotId', schema: null, sources: [src('parking.query', true), src('parking.payment', false)] },
|
|
1269
|
-
{ key: 'env', schema: { type: 'string', enum: ['dev', 'staging', 'prod'] }, sources: [src('parking.query', false), src('parking.payment', false)] },
|
|
1270
|
-
{ key: 'envMixed', schema: null, sources: [src('parking.query', false), src('parking.payment', false)] },
|
|
1271
|
-
{ key: 'meta', schema: null, sources: [src('parking.query', false), src('parking.payment', false)] },
|
|
1272
|
-
{ key: 'options', schema: { type: 'object', properties: { x: { type: 'string' } } }, sources: [src('parking.query', false), src('parking.payment', true)] },
|
|
1273
|
-
{ key: 'untyped', schema: null, sources: [src('parking.query', false), src('parking.payment', false)] },
|
|
1274
|
-
{ key: 'timeout', schema: null, sources: [src('parking.query', false), src('parking.payment', false)] },
|
|
1275
|
-
{ key: 'apiKey', schema: { type: 'string' }, sources: [src('parking.payment', true)] },
|
|
1474
|
+
{ key: 'baseUrl', schema: { type: 'string', format: 'uri', 'x-tbox-global': true }, global: true, sources: [src('parking.query', true), src('parking.payment', false)] },
|
|
1475
|
+
{ key: 'lotId', schema: null, global: true, sources: [src('parking.query', true), src('parking.payment', false)] },
|
|
1476
|
+
{ key: 'env', schema: { type: 'string', enum: ['dev', 'staging', 'prod'], 'x-tbox-global': true }, global: true, sources: [src('parking.query', false), src('parking.payment', false)] },
|
|
1477
|
+
{ key: 'envMixed', schema: null, global: true, sources: [src('parking.query', false), src('parking.payment', false)] },
|
|
1478
|
+
{ key: 'meta', schema: null, global: true, sources: [src('parking.query', false), src('parking.payment', false)] },
|
|
1479
|
+
{ key: 'options', schema: { type: 'object', properties: { x: { type: 'string', 'x-tbox-global': true } }, 'x-tbox-global': true }, global: true, sources: [src('parking.query', false), src('parking.payment', true)] },
|
|
1480
|
+
{ key: 'untyped', schema: null, global: true, sources: [src('parking.query', false), src('parking.payment', false)] },
|
|
1481
|
+
{ key: 'timeout', schema: null, global: true, sources: [src('parking.query', false), src('parking.payment', false)] },
|
|
1482
|
+
{ key: 'apiKey', schema: { type: 'string', 'x-tbox-global': true }, global: true, sources: [src('parking.payment', true)] },
|
|
1276
1483
|
],
|
|
1277
1484
|
};
|
|
1278
1485
|
|
|
@@ -1283,15 +1490,15 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1283
1490
|
unsupplied: [],
|
|
1284
1491
|
additionalPropertiesAllowed: false,
|
|
1285
1492
|
fields: [
|
|
1286
|
-
{ key: 'apiKey', schema: { type: 'string' }, sources: [src('parking.payment', true)] },
|
|
1287
|
-
{ key: 'baseUrl', schema: { type: 'string' }, sources: [src('parking.payment', false), src('parking.query', true)] },
|
|
1288
|
-
{ key: 'lotId', schema: null, sources: [src('parking.payment', false), src('parking.query', true)] },
|
|
1289
|
-
{ key: 'env', schema: { type: 'string', enum: ['prod', 'dev', 'staging'] }, sources: [src('parking.payment', false), src('parking.query', false)] },
|
|
1290
|
-
{ key: 'envMixed', schema: null, sources: [src('parking.payment', false), src('parking.query', false)] },
|
|
1291
|
-
{ key: 'meta', schema: null, sources: [src('parking.payment', false), src('parking.query', false)] },
|
|
1292
|
-
{ key: 'options', schema: { type: 'object', properties: { x: { type: 'string' } } }, sources: [src('parking.payment', true), src('parking.query', false)] },
|
|
1293
|
-
{ key: 'untyped', schema: null, sources: [src('parking.payment', false), src('parking.query', false)] },
|
|
1294
|
-
{ key: 'timeout', schema: null, sources: [src('parking.payment', false), src('parking.query', false)] },
|
|
1493
|
+
{ key: 'apiKey', schema: { type: 'string', 'x-tbox-global': true }, global: true, sources: [src('parking.payment', true)] },
|
|
1494
|
+
{ key: 'baseUrl', schema: { type: 'string', 'x-tbox-global': true }, global: true, sources: [src('parking.payment', false), src('parking.query', true)] },
|
|
1495
|
+
{ key: 'lotId', schema: null, global: true, sources: [src('parking.payment', false), src('parking.query', true)] },
|
|
1496
|
+
{ key: 'env', schema: { type: 'string', enum: ['prod', 'dev', 'staging'], 'x-tbox-global': true }, global: true, sources: [src('parking.payment', false), src('parking.query', false)] },
|
|
1497
|
+
{ key: 'envMixed', schema: null, global: true, sources: [src('parking.payment', false), src('parking.query', false)] },
|
|
1498
|
+
{ key: 'meta', schema: null, global: true, sources: [src('parking.payment', false), src('parking.query', false)] },
|
|
1499
|
+
{ key: 'options', schema: { type: 'object', properties: { x: { type: 'string', 'x-tbox-global': true } }, 'x-tbox-global': true }, global: true, sources: [src('parking.payment', true), src('parking.query', false)] },
|
|
1500
|
+
{ key: 'untyped', schema: null, global: true, sources: [src('parking.payment', false), src('parking.query', false)] },
|
|
1501
|
+
{ key: 'timeout', schema: null, global: true, sources: [src('parking.payment', false), src('parking.query', false)] },
|
|
1295
1502
|
],
|
|
1296
1503
|
};
|
|
1297
1504
|
|
|
@@ -1301,15 +1508,15 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1301
1508
|
/** E4:module 轴 moduleProvider=ghost-x(未知厂商——非 null 空集 + 全 demand 回填,demand 序) */
|
|
1302
1509
|
const E4 = { provider: 'ghost-x', schemaless: [], unsupplied: ['parking.query', 'parking.payment'], additionalPropertiesAllowed: true, fields: [] };
|
|
1303
1510
|
|
|
1304
|
-
/** E5:实际态(无轴参,wanda;scope = [query, payment]
|
|
1511
|
+
/** E5:实际态(无轴参,wanda;scope = [query, payment])——parkLotId 未标记 global 被表单收窄(G 批负例) */
|
|
1305
1512
|
const E5 = {
|
|
1306
1513
|
provider: 'wanda',
|
|
1307
1514
|
schemaless: [],
|
|
1308
1515
|
unsupplied: [],
|
|
1309
1516
|
additionalPropertiesAllowed: true,
|
|
1310
1517
|
fields: [
|
|
1311
|
-
{ key: 'baseUrl', schema: { type: 'string', format: 'uri' }, sources: [src('parking.query', true)] },
|
|
1312
|
-
{ key: 'mallId', schema: { type: 'string' }, sources: [src('parking.payment', false)] },
|
|
1518
|
+
{ key: 'baseUrl', schema: { type: 'string', format: 'uri', 'x-tbox-global': true }, global: true, sources: [src('parking.query', true)] },
|
|
1519
|
+
{ key: 'mallId', schema: { type: 'string', 'x-tbox-global': true }, global: true, sources: [src('parking.payment', false)] },
|
|
1313
1520
|
],
|
|
1314
1521
|
};
|
|
1315
1522
|
|
|
@@ -1391,7 +1598,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1391
1598
|
|
|
1392
1599
|
it('S5a-8/9 单源直通 + required 聚合(some;conflict 照常聚合)', () => {
|
|
1393
1600
|
const single = reg({ 'a.one': { configSchema: { type: 'object', properties: { only: { type: 'string', enum: ['z'] } } } } });
|
|
1394
|
-
expect(single.fields[0]).toEqual({ key: 'only', schema: { type: 'string', enum: ['z'] }, sources: [src('a.one', false)] });
|
|
1601
|
+
expect(single.fields[0]).toEqual({ key: 'only', schema: { type: 'string', enum: ['z'] }, global: false, sources: [src('a.one', false)] });
|
|
1395
1602
|
const some = reg({
|
|
1396
1603
|
'a.one': { configSchema: { type: 'object', properties: { k: { type: 'string' } }, required: ['k'] } },
|
|
1397
1604
|
'a.two': { configSchema: { type: 'object', properties: { k: { type: 'string' } } } },
|
|
@@ -1447,7 +1654,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1447
1654
|
const appInc = await h2.tk.loadApp({ include: ['integrationFieldRegistry'], appProvider: 'joycity' });
|
|
1448
1655
|
expect(appInc.integrationFieldRegistry).toEqual(E2);
|
|
1449
1656
|
expect(appInc.integrationFieldRegistry?.fields[0].key).toBe('apiKey'); // 足迹字母序(vs E1 首键 baseUrl)
|
|
1450
|
-
expect(appInc.integrationFieldRegistry?.fields[1].schema).toEqual({ type: 'string' }); // 首源漂移(payment 先)
|
|
1657
|
+
expect(appInc.integrationFieldRegistry?.fields[1].schema).toEqual({ type: 'string', 'x-tbox-global': true }); // 首源漂移(payment 先)
|
|
1451
1658
|
const partial = await h2.tk.loadModule('module-parking', { include: ['integrationFieldRegistry'], moduleProvider: 'partial' });
|
|
1452
1659
|
expect(partial.integrationFieldRegistry).toEqual(E3);
|
|
1453
1660
|
const actual = await h2.tk.loadModule('module-parking', { include: ['integrationFieldRegistry'] });
|
|
@@ -1485,7 +1692,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1485
1692
|
const actJoy = await h2.tk.loadModule('module-parking', { include: both });
|
|
1486
1693
|
expect(actJoy.integrationFieldRegistry).toEqual(hypJoy.integrationFieldRegistry);
|
|
1487
1694
|
expect(actJoy.integrationSchemas).toEqual(hypJoy.integrationSchemas);
|
|
1488
|
-
// ② 跟随应用级(root.provider=wanda)≡
|
|
1695
|
+
// ② 跟随应用级(root.provider=wanda)≡ 清模块位(N1 删 modules['module-parking'] → root 兜底)后实读——非平凡:清位前实际态 = E1
|
|
1489
1696
|
const hypFollow = await h2.tk.loadModule('module-parking', { include: both, moduleProvider: 'wanda' });
|
|
1490
1697
|
expect(hypFollow.integrationFieldRegistry).toEqual(E5);
|
|
1491
1698
|
await h2.tk.writeModuleIntegration('module-parking', {});
|
|
@@ -1498,7 +1705,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1498
1705
|
h2.app.dispose();
|
|
1499
1706
|
});
|
|
1500
1707
|
|
|
1501
|
-
it('S6 app 轴同构:假想 ≡ 写 root {provider:joycity} 后实读(
|
|
1708
|
+
it('S6 app 轴同构:假想 ≡ 写 root {provider:joycity} 后实读(modules/services 不受扰)', async () => {
|
|
1502
1709
|
const h2 = layerHarness();
|
|
1503
1710
|
const hyp = await h2.tk.loadApp({ include: ['integrationFieldRegistry'], appProvider: 'joycity' });
|
|
1504
1711
|
const cur = await h2.tk.loadApp();
|
|
@@ -1557,7 +1764,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1557
1764
|
moduleProvider: 'joycity',
|
|
1558
1765
|
});
|
|
1559
1766
|
expect(v.integration).toEqual({ provider: 'wanda' });
|
|
1560
|
-
expect(v.
|
|
1767
|
+
expect(v.binding).toEqual({ provider: 'wanda', layer: 'module' });
|
|
1561
1768
|
expect(v.configEditable).toBe(true);
|
|
1562
1769
|
expect(v.integrationFieldRegistry?.provider).toBe('joycity'); // 表单层重定向
|
|
1563
1770
|
expect(v.integrationSchemas?.provider).toBe('joycity');
|
|
@@ -1573,7 +1780,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1573
1780
|
'config/integrations.json',
|
|
1574
1781
|
JSON.stringify({
|
|
1575
1782
|
instances: [{ id: 'mall-bj-01', name: '北京店' }],
|
|
1576
|
-
|
|
1783
|
+
modules: { 'module-parking': { provider: 'joycity' } },
|
|
1577
1784
|
services: { 'parking.query': { provider: 'wanda', implementation: 'wanda-parking@1' } },
|
|
1578
1785
|
}),
|
|
1579
1786
|
);
|
|
@@ -1583,7 +1790,7 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1583
1790
|
).toBeNull();
|
|
1584
1791
|
h3.app.dispose();
|
|
1585
1792
|
|
|
1586
|
-
// 可写未绑定(无
|
|
1793
|
+
// 可写未绑定(无 modules 位无 root.provider——无轴参 → null)
|
|
1587
1794
|
const h4 = layerHarness();
|
|
1588
1795
|
writeAppFile(
|
|
1589
1796
|
h4.app.appDir,
|
|
@@ -1599,29 +1806,39 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1599
1806
|
expect(unbound.configEditable).toBe(true);
|
|
1600
1807
|
h4.app.dispose();
|
|
1601
1808
|
|
|
1602
|
-
// D34
|
|
1809
|
+
// v6 形态(D34 三形态退役):零归属 → null(带 moduleProvider 仍 null——不豁免);
|
|
1810
|
+
// 多域/跨域归属合法化 → 非 null(override 可计算;unsupplied 回填非供给归属槽)
|
|
1603
1811
|
const d34 = createTempApp('tbox-v46-d34-');
|
|
1604
1812
|
invalidateContractsResolver();
|
|
1605
1813
|
const store34 = joinStore(d34.appDir, 'contracts');
|
|
1606
1814
|
writeFakeContractsPackage(store34, { version: '0.9.0' });
|
|
1607
1815
|
linkContracts(d34.appDir, store34, { subdir: 'apps/server' });
|
|
1608
1816
|
const need = (service: string): { service: string; optional: boolean } => ({ service, optional: false });
|
|
1609
|
-
const manifest34 = (
|
|
1610
|
-
JSON.stringify({ schemaVersion: 1,
|
|
1817
|
+
const manifest34 = (id: string, services: Array<{ service: string; optional: boolean }>): string =>
|
|
1818
|
+
JSON.stringify({ schemaVersion: 1, id, version: '0.1.0', kind: 'business', declare: { owns: services.map((x) => ({ service: x.service })), consumes: services }, dependencies: { modules: [] }, env: [] });
|
|
1611
1819
|
writeAppFile(d34.appDir, 'packages/module-multi/tbox.module.json', manifest34('module-multi', [need('parking.query'), need('member.account')]));
|
|
1612
1820
|
writeAppFile(
|
|
1613
1821
|
d34.appDir,
|
|
1614
1822
|
'packages/module-zero/tbox.module.json',
|
|
1615
|
-
JSON.stringify({ schemaVersion: 1,
|
|
1823
|
+
JSON.stringify({ schemaVersion: 1, id: 'module-zero', version: '0.1.0', kind: 'business', declare: {}, dependencies: { modules: [] }, env: [] }),
|
|
1616
1824
|
);
|
|
1617
1825
|
writeAppFile(d34.appDir, 'packages/module-a/tbox.module.json', manifest34('module-a', [need('parking.alpha')]));
|
|
1618
1826
|
writeAppFile(d34.appDir, 'packages/module-b/tbox.module.json', manifest34('module-b', [need('parking.beta')]));
|
|
1619
1827
|
const tk34 = createAppToolkit(d34.appDir);
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1828
|
+
// 零归属:null(唯一 null 形态——override 不豁免)
|
|
1829
|
+
const zero = await tk34.loadModule('module-zero', { include: ['integrationFieldRegistry', 'integrationSchemas'], moduleProvider: 'joycity' });
|
|
1830
|
+
expect(zero.integrationFieldRegistry).toBeNull();
|
|
1831
|
+
expect(zero.integrationSchemas).toBeNull();
|
|
1832
|
+
expect(zero.configEditable).toBe(false);
|
|
1833
|
+
// 多域归属:可写(owns>0)——override joycity 供给 parking.query(member.account → unsupplied)
|
|
1834
|
+
const multi = await tk34.loadModule('module-multi', { include: ['integrationFieldRegistry', 'integrationSchemas'], moduleProvider: 'joycity' });
|
|
1835
|
+
expect(multi.configEditable).toBe(true);
|
|
1836
|
+
expect(multi.integrationSchemas?.provider).toBe('joycity');
|
|
1837
|
+
// d34 fixture 无 provider 包 → joycity 零供给 → 双 owned 全回填(manifest owns 序)
|
|
1838
|
+
expect(multi.integrationFieldRegistry?.unsupplied).toEqual(['parking.query', 'member.account']);
|
|
1839
|
+
// 跨域归属:scope = owns ∩ 供给 = [](parking.alpha 非 joycity 供给)→ 非 null 空集 + unsupplied 全回填
|
|
1840
|
+
const a = await tk34.loadModule('module-a', { include: ['integrationFieldRegistry'], moduleProvider: 'joycity' });
|
|
1841
|
+
expect(a.integrationFieldRegistry).toEqual({ provider: 'joycity', schemaless: [], unsupplied: ['parking.alpha'], additionalPropertiesAllowed: true, fields: [] });
|
|
1625
1842
|
d34.dispose();
|
|
1626
1843
|
});
|
|
1627
1844
|
|
|
@@ -1682,7 +1899,13 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1682
1899
|
const props = c.properties;
|
|
1683
1900
|
const declared = props && typeof props === 'object' && !Array.isArray(props) ? Object.keys(props) : []; // 同款守卫
|
|
1684
1901
|
const rev = registry.fields.filter((f) => f.sources.some((x) => x.service === s)).map((f) => f.key);
|
|
1685
|
-
|
|
1902
|
+
// G 批收窄互证:表单键 ⊆ 声明键,且声明侧标记键 ≡ 表单键(未标记键落 ExtraFields——不进 fields)
|
|
1903
|
+
const marked = declared.filter((k) => {
|
|
1904
|
+
const sub = (props as Record<string, Record<string, unknown>>)[k];
|
|
1905
|
+
return !!sub && sub['x-tbox-global'] === true;
|
|
1906
|
+
});
|
|
1907
|
+
expect(rev.every((k) => declared.includes(k)), s).toBe(true);
|
|
1908
|
+
expect([...marked].sort(), s).toEqual([...rev].sort());
|
|
1686
1909
|
}
|
|
1687
1910
|
for (const s of registry.schemaless) expect(layer.services[s]?.configSchema).toBeNull();
|
|
1688
1911
|
// unsupplied ≡ demand − scope 键集
|
|
@@ -1692,6 +1915,39 @@ describe('v4.6 D36/D37(integrationFieldRegistry + 轴向假想绑定)', () =
|
|
|
1692
1915
|
h2.app.dispose();
|
|
1693
1916
|
});
|
|
1694
1917
|
|
|
1918
|
+
it('G 批收窄负例(P16 复合可见面):未标记键在 layer.properties 在场、registry.fields 不收(wanda 实际态 parkLotId)', async () => {
|
|
1919
|
+
const h2 = layerHarness();
|
|
1920
|
+
const v = await h2.tk.loadModule('module-parking', { include: ['integrationSchemas', 'integrationFieldRegistry'] });
|
|
1921
|
+
const layer = v.integrationSchemas!;
|
|
1922
|
+
const registry = v.integrationFieldRegistry!;
|
|
1923
|
+
expect(layer.provider).toBe('wanda');
|
|
1924
|
+
const parkingProps = (layer.services['parking.payment'].configSchema as Record<string, Record<string, unknown>>).properties as Record<string, Record<string, unknown>>;
|
|
1925
|
+
expect(parkingProps.mallId['x-tbox-global']).toBe(true);
|
|
1926
|
+
expect(parkingProps.parkLotId).toBeDefined(); // 原始投影在场
|
|
1927
|
+
expect(parkingProps.parkLotId['x-tbox-global']).toBeUndefined();
|
|
1928
|
+
expect(registry.fields.some((f) => f.key === 'parkLotId')).toBe(false); // 表单收窄不收
|
|
1929
|
+
expect(registry.fields.some((f) => f.key === 'mallId')).toBe(true);
|
|
1930
|
+
// app 轴同律(足迹含 query——baseUrl 标记在场)
|
|
1931
|
+
const appV = await h2.tk.loadApp({ include: ['integrationSchemas', 'integrationFieldRegistry'] });
|
|
1932
|
+
expect(appV.integrationFieldRegistry!.fields.some((f) => f.key === 'baseUrl')).toBe(true);
|
|
1933
|
+
h2.app.dispose();
|
|
1934
|
+
});
|
|
1935
|
+
|
|
1936
|
+
it('G 批服务轴不过滤锁:loadServiceFieldRegistry 全字段带旗标(unmarked → global:false 在场)', async () => {
|
|
1937
|
+
const h2 = layerHarness();
|
|
1938
|
+
const snap = (await import('../src/views/context.js')).loadViewSnapshotSync({ appDir: h2.app.appDir }, await h2.tk.resolveContracts());
|
|
1939
|
+
const reg = loadServiceFieldRegistry(snap, 'parking.payment');
|
|
1940
|
+
expect(reg).not.toBeNull();
|
|
1941
|
+
const keys = reg!.fields.map((f) => f.key).sort();
|
|
1942
|
+
expect(keys).toEqual(['mallId', 'parkLotId']); // 服务轴全量(未标记也入列——旗标 false 如实)
|
|
1943
|
+
const lot = reg!.fields.find((f) => f.key === 'parkLotId')!;
|
|
1944
|
+
expect(lot.global).toBe(false);
|
|
1945
|
+
expect(lot.schema).toEqual({ type: 'string', title: '车场组' }); // 无标记污染
|
|
1946
|
+
const mall = reg!.fields.find((f) => f.key === 'mallId')!;
|
|
1947
|
+
expect(mall.global).toBe(true);
|
|
1948
|
+
h2.app.dispose();
|
|
1949
|
+
});
|
|
1950
|
+
|
|
1695
1951
|
it('S8 app 轴:unsupplied ≡ [](结构性:足迹 ⊆ 供给)', async () => {
|
|
1696
1952
|
const h2 = layerHarness();
|
|
1697
1953
|
const v = await h2.tk.loadApp({ include: ['integrationFieldRegistry'], appProvider: 'joycity' });
|
|
@@ -1715,8 +1971,8 @@ describe('B1 展示名链', () => {
|
|
|
1715
1971
|
version: '0.9.0',
|
|
1716
1972
|
slots: ['parking.query', 'auth.alipay-login', 'mall.info'],
|
|
1717
1973
|
meta: {
|
|
1718
|
-
'parking.query': {
|
|
1719
|
-
'auth.alipay-login': {
|
|
1974
|
+
'parking.query': { name: '停车查询', description: '费用报价、车场信息、可享权益' },
|
|
1975
|
+
'auth.alipay-login': { name: '支付宝登录' },
|
|
1720
1976
|
},
|
|
1721
1977
|
});
|
|
1722
1978
|
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
@@ -1726,14 +1982,15 @@ describe('B1 展示名链', () => {
|
|
|
1726
1982
|
'packages/module-parking/tbox.module.json',
|
|
1727
1983
|
JSON.stringify({
|
|
1728
1984
|
schemaVersion: 1,
|
|
1729
|
-
|
|
1985
|
+
id: 'module-parking',
|
|
1730
1986
|
version: '0.1.0',
|
|
1731
1987
|
kind: 'business',
|
|
1732
|
-
|
|
1988
|
+
name: '停车缴费',
|
|
1733
1989
|
description: '车辆查询与绑定、停车费用查询、缴费支付与开票',
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1990
|
+
declare: {
|
|
1991
|
+
owns: [{ service: 'parking.query' }],
|
|
1992
|
+
// demand meta name=车位查询——应被词汇 meta 停车查询压过(词汇主源)
|
|
1993
|
+
consumes: [{ service: 'parking.query', optional: false, name: '车位查询' }],
|
|
1737
1994
|
},
|
|
1738
1995
|
dependencies: { modules: [] },
|
|
1739
1996
|
env: [],
|
|
@@ -1745,12 +2002,12 @@ describe('B1 展示名链', () => {
|
|
|
1745
2002
|
'packages/provider-demo/tbox.module.json',
|
|
1746
2003
|
JSON.stringify({
|
|
1747
2004
|
schemaVersion: 1,
|
|
1748
|
-
|
|
2005
|
+
id: 'provider-demo',
|
|
1749
2006
|
version: '0.1.0',
|
|
1750
2007
|
kind: 'third-party',
|
|
1751
|
-
|
|
2008
|
+
name: '万达',
|
|
1752
2009
|
description: '万达厂商集成',
|
|
1753
|
-
|
|
2010
|
+
declare: {
|
|
1754
2011
|
providers: {
|
|
1755
2012
|
slots: [{ service: 'parking.query', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'apikey' }],
|
|
1756
2013
|
},
|
|
@@ -1766,49 +2023,89 @@ describe('B1 展示名链', () => {
|
|
|
1766
2023
|
it('T1 resolveServiceDisplay 三态:词汇 > demand > id;逐字段回落(词汇仅 description 时 demand title 可达)', () => {
|
|
1767
2024
|
const walk = {
|
|
1768
2025
|
slotMeta: {
|
|
1769
|
-
'a.b': {
|
|
2026
|
+
'a.b': { name: '甲', description: '甲描述' },
|
|
1770
2027
|
'c.d': { description: '只有描述' },
|
|
1771
2028
|
},
|
|
1772
2029
|
catalog: {
|
|
1773
2030
|
services: {
|
|
1774
|
-
'a.b': {
|
|
1775
|
-
'c.d': {
|
|
2031
|
+
'a.b': { name: '乙', description: '乙描述' },
|
|
2032
|
+
'c.d': { name: '丙' },
|
|
1776
2033
|
},
|
|
1777
2034
|
},
|
|
1778
2035
|
} as unknown as WalkManifestsResult;
|
|
1779
2036
|
// 词汇全量在场 → 词汇胜出
|
|
1780
|
-
expect(resolveServiceDisplay(walk, 'a.b')).toEqual({
|
|
2037
|
+
expect(resolveServiceDisplay(walk, 'a.b')).toEqual({ name: '甲', description: '甲描述' });
|
|
1781
2038
|
// 词汇仅 description → title 回落 demand、description 取词汇(逐字段回落)
|
|
1782
|
-
expect(resolveServiceDisplay(walk, 'c.d')).toEqual({
|
|
2039
|
+
expect(resolveServiceDisplay(walk, 'c.d')).toEqual({ name: '丙', description: '只有描述' });
|
|
1783
2040
|
// 双缺席 → demand
|
|
1784
|
-
expect(resolveServiceDisplay(walk, 'e.f')).toEqual({
|
|
2041
|
+
expect(resolveServiceDisplay(walk, 'e.f')).toEqual({ name: 'e.f' });
|
|
1785
2042
|
// 双双缺席 → id 兜底(title 恒有值)
|
|
1786
2043
|
const bare = { slotMeta: {}, catalog: { services: {} } } as unknown as WalkManifestsResult;
|
|
1787
|
-
expect(resolveServiceDisplay(bare, 'x.y')).toEqual({
|
|
2044
|
+
expect(resolveServiceDisplay(bare, 'x.y')).toEqual({ name: 'x.y' });
|
|
1788
2045
|
});
|
|
1789
2046
|
|
|
1790
2047
|
it('T2 loadModulesView:模块 meta 填充;name 兜底', async () => {
|
|
1791
2048
|
const h2 = displayHarness();
|
|
1792
2049
|
const v = await h2.tk.loadModules();
|
|
1793
2050
|
const parking = v.modules.find((m) => m.id === 'module-parking');
|
|
1794
|
-
expect(parking?.
|
|
2051
|
+
expect(parking?.name).toBe('停车缴费');
|
|
1795
2052
|
expect(parking?.description).toBe('车辆查询与绑定、停车费用查询、缴费支付与开票');
|
|
1796
2053
|
// 无 meta 模块(provider 家族不在清单——用第二基座验证兜底)
|
|
1797
2054
|
const h3 = harnessWithContracts();
|
|
1798
2055
|
const bare = (await h3.tk.loadModules()).modules.find((m) => m.id === 'module-parking');
|
|
1799
|
-
expect(bare?.
|
|
2056
|
+
expect(bare?.name).toBe('module-parking');
|
|
1800
2057
|
expect(bare?.description).toBe('');
|
|
1801
2058
|
h3.app.dispose();
|
|
1802
2059
|
h2.app.dispose();
|
|
1803
2060
|
});
|
|
1804
2061
|
|
|
2062
|
+
it('T2b loadModulesView:hidden 模块默认过滤;includeHidden 可见 + 徽标;detail 恒 200 不滤', async () => {
|
|
2063
|
+
const app = createTempApp('tbox-hidden-');
|
|
2064
|
+
invalidateContractsResolver();
|
|
2065
|
+
const store = joinStore(app.appDir, 'contracts');
|
|
2066
|
+
writeFakeContractsPackage(store, { version: '0.9.0' });
|
|
2067
|
+
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
2068
|
+
const manifest = (id: string, name: string, extra: Record<string, unknown> = {}): string =>
|
|
2069
|
+
JSON.stringify({
|
|
2070
|
+
schemaVersion: 1,
|
|
2071
|
+
id,
|
|
2072
|
+
version: '0.1.0',
|
|
2073
|
+
kind: 'business',
|
|
2074
|
+
name,
|
|
2075
|
+
declare: { owns: [], consumes: [] },
|
|
2076
|
+
dependencies: { modules: [] },
|
|
2077
|
+
env: [],
|
|
2078
|
+
...extra,
|
|
2079
|
+
});
|
|
2080
|
+
mkdirSync(join(app.appDir, 'packages', 'module-visible'), { recursive: true });
|
|
2081
|
+
writeAppFile(app.appDir, 'packages/module-visible/tbox.module.json', manifest('module-visible', '可见模块'));
|
|
2082
|
+
mkdirSync(join(app.appDir, 'packages', 'module-secret'), { recursive: true });
|
|
2083
|
+
writeAppFile(app.appDir, 'packages/module-secret/tbox.module.json', manifest('module-secret', '隐藏模块', { hidden: true }));
|
|
2084
|
+
|
|
2085
|
+
const tk = createAppToolkit(app.appDir);
|
|
2086
|
+
// 默认:hidden 模块不进清单(可见模块无 hidden 键——additive 纪律)
|
|
2087
|
+
const def = await tk.loadModules();
|
|
2088
|
+
expect(def.modules.map((m) => m.id)).toEqual(['module-visible']);
|
|
2089
|
+
expect(def.modules[0]).not.toHaveProperty('hidden');
|
|
2090
|
+
// includeHidden = true:可见并携带 hidden: true 徽标
|
|
2091
|
+
const inc = await tk.loadModules({ includeHidden: true });
|
|
2092
|
+
expect(inc.modules.map((m) => m.id).sort()).toEqual(['module-secret', 'module-visible']);
|
|
2093
|
+
expect(inc.modules.find((m) => m.id === 'module-secret')?.hidden).toBe(true);
|
|
2094
|
+
expect(inc.modules.find((m) => m.id === 'module-visible')).not.toHaveProperty('hidden');
|
|
2095
|
+
// detail 端点恒 200 不滤——视图携带 hidden 徽标
|
|
2096
|
+
const detail = await tk.loadModule('module-secret');
|
|
2097
|
+
expect(detail.name).toBe('隐藏模块');
|
|
2098
|
+
expect(detail.hidden).toBe(true);
|
|
2099
|
+
app.dispose();
|
|
2100
|
+
});
|
|
2101
|
+
|
|
1805
2102
|
it('T3 loadModuleDetailView:meta 恒发;services[] 词汇优先(demand 车位查询被词汇停车查询压过——防漂移锁)', async () => {
|
|
1806
2103
|
const h2 = displayHarness();
|
|
1807
2104
|
const v = await h2.tk.loadModule('module-parking');
|
|
1808
|
-
expect(v.
|
|
2105
|
+
expect(v.name).toBe('停车缴费');
|
|
1809
2106
|
expect(v.description).toContain('停车费用查询');
|
|
1810
2107
|
const svc = v.services.find((s) => s.service === 'parking.query');
|
|
1811
|
-
expect(svc?.
|
|
2108
|
+
expect(svc?.name).toBe('停车查询');
|
|
1812
2109
|
expect(svc?.description).toBe('费用报价、车场信息、可享权益');
|
|
1813
2110
|
expect(svc?.required).toBe(true);
|
|
1814
2111
|
h2.app.dispose();
|
|
@@ -1821,22 +2118,63 @@ describe('B1 展示名链', () => {
|
|
|
1821
2118
|
h2.app.dispose();
|
|
1822
2119
|
});
|
|
1823
2120
|
|
|
2121
|
+
// ── v7 双键寻址(factory 级——resolve-key 单点经七方法入口;id 优先/展示名兜底/多命中 404)──
|
|
2122
|
+
|
|
2123
|
+
it('v7-DK1 模块双键:展示名「停车缴费」≡ id module-parking(同一视图);读侧 id 恒优先', async () => {
|
|
2124
|
+
const h2 = displayHarness();
|
|
2125
|
+
const byName = await h2.tk.loadModule('停车缴费');
|
|
2126
|
+
const byId = await h2.tk.loadModule('module-parking');
|
|
2127
|
+
expect(byName.name).toBe(byId.name);
|
|
2128
|
+
expect(byName.ownedServices).toEqual(byId.ownedServices);
|
|
2129
|
+
h2.app.dispose();
|
|
2130
|
+
});
|
|
2131
|
+
|
|
2132
|
+
it('v7-DK2 服务双键:展示名「停车查询」→ parking.query(词汇 meta 源);含 include 富化同键', async () => {
|
|
2133
|
+
const h2 = displayHarness();
|
|
2134
|
+
const byName = await h2.tk.loadService('停车查询');
|
|
2135
|
+
expect(byName.service).toBe('parking.query');
|
|
2136
|
+
expect(byName.name).toBe('停车查询');
|
|
2137
|
+
h2.app.dispose();
|
|
2138
|
+
});
|
|
2139
|
+
|
|
2140
|
+
it('v7-DK3 多展示名命中 → 404 带候选(两模块同名;fail-visible 不静默取首)', async () => {
|
|
2141
|
+
const app = createTempApp('tbox-dk3-');
|
|
2142
|
+
invalidateContractsResolver();
|
|
2143
|
+
const store = joinStore(app.appDir, 'contracts');
|
|
2144
|
+
writeFakeContractsPackage(store, { version: '0.9.0' });
|
|
2145
|
+
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
2146
|
+
const manifest = (id: string): string =>
|
|
2147
|
+
JSON.stringify({ schemaVersion: 1, id, name: '同名模块', version: '0.1.0', kind: 'business', declare: {}, dependencies: { modules: [] }, env: [] });
|
|
2148
|
+
writeAppFile(app.appDir, 'packages/module-a/tbox.module.json', manifest('module-a'));
|
|
2149
|
+
writeAppFile(app.appDir, 'packages/module-b/tbox.module.json', manifest('module-b'));
|
|
2150
|
+
const tk = createAppToolkit(app.appDir);
|
|
2151
|
+
await expect(tk.loadModule('同名模块')).rejects.toMatchObject({
|
|
2152
|
+
code: 'MODULE_NOT_FOUND',
|
|
2153
|
+
httpStatus: 404,
|
|
2154
|
+
message: expect.stringContaining('id=module-a'),
|
|
2155
|
+
});
|
|
2156
|
+
await expect(tk.loadModule('同名模块')).rejects.toMatchObject({
|
|
2157
|
+
message: expect.stringContaining('改用模块 id'),
|
|
2158
|
+
});
|
|
2159
|
+
app.dispose();
|
|
2160
|
+
});
|
|
2161
|
+
|
|
1824
2162
|
it('T4 loadAppView 平台条目:title 走链恒发;词汇无 meta 槽 = id 兜底', async () => {
|
|
1825
2163
|
const h2 = displayHarness();
|
|
1826
2164
|
const v = await h2.tk.loadApp();
|
|
1827
2165
|
const byKey = Object.fromEntries(v.services.map((s) => [s.service, s]));
|
|
1828
2166
|
// 平台条目 = 词汇 − 占用(parking.query 被模块占用)
|
|
1829
2167
|
expect(Object.keys(byKey).sort()).toEqual(['auth.alipay-login', 'mall.info']);
|
|
1830
|
-
expect(byKey['auth.alipay-login']?.
|
|
1831
|
-
expect(byKey['mall.info']?.
|
|
2168
|
+
expect(byKey['auth.alipay-login']?.name).toBe('支付宝登录');
|
|
2169
|
+
expect(byKey['mall.info']?.name).toBe('mall.info');
|
|
1832
2170
|
expect(byKey['auth.alipay-login']?.required).toBe(false);
|
|
1833
2171
|
h2.app.dispose();
|
|
1834
2172
|
});
|
|
1835
2173
|
|
|
1836
2174
|
it('T5 loadServiceDetailView:词汇优先 + id 兜底恒有值', async () => {
|
|
1837
2175
|
const h2 = displayHarness();
|
|
1838
|
-
expect((await h2.tk.loadService('parking.query')).
|
|
1839
|
-
expect((await h2.tk.loadService('mall.info')).
|
|
2176
|
+
expect((await h2.tk.loadService('parking.query')).name).toBe('停车查询');
|
|
2177
|
+
expect((await h2.tk.loadService('mall.info')).name).toBe('mall.info');
|
|
1840
2178
|
h2.app.dispose();
|
|
1841
2179
|
});
|
|
1842
2180
|
|
|
@@ -1844,11 +2182,11 @@ describe('B1 展示名链', () => {
|
|
|
1844
2182
|
const h2 = displayHarness();
|
|
1845
2183
|
const list = await h2.tk.loadProviders();
|
|
1846
2184
|
const wanda = list.providers.find((p) => p.provider === 'wanda');
|
|
1847
|
-
expect(wanda?.
|
|
2185
|
+
expect(wanda?.name).toBe('万达');
|
|
1848
2186
|
expect(wanda?.description).toBe('万达厂商集成');
|
|
1849
2187
|
const detail = await h2.tk.loadProvider('wanda');
|
|
1850
|
-
expect(detail.
|
|
1851
|
-
expect(detail.services['parking.query']?.
|
|
2188
|
+
expect(detail.name).toBe('万达');
|
|
2189
|
+
expect(detail.services['parking.query']?.name).toBe('停车查询');
|
|
1852
2190
|
expect(detail.services['parking.query']?.description).toBe('费用报价、车场信息、可享权益');
|
|
1853
2191
|
h2.app.dispose();
|
|
1854
2192
|
});
|