@tbox.cn/app-toolkit 0.5.0 → 0.7.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/README.md +2 -2
- package/dist/index.d.ts +101 -20
- package/dist/index.js +9 -9
- package/package.json +1 -1
- package/src/assembly/walk-manifests.ts +28 -12
- package/src/core/errors.ts +12 -2
- package/src/core/module-schema.ts +3 -0
- package/src/dto.ts +41 -5
- package/src/factory.ts +48 -28
- package/src/index.ts +4 -2
- package/src/integrations/write.ts +4 -0
- package/src/views/context.ts +4 -2
- package/src/views/integration-schemas.ts +17 -3
- package/src/views/modules.ts +66 -4
- package/src/views/service-resolutions.ts +113 -25
- package/tests/dev-manifest-catalog.test.ts +39 -1
- package/tests/module-resources.test.ts +148 -0
- package/tests/module-schema.test.ts +14 -0
- package/tests/views.test.ts +437 -120
- package/tests/write-core.test.ts +34 -0
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, loadServiceFieldRegistry } 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';
|
|
@@ -24,7 +24,7 @@ interface Harness {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function harnessWithContracts(
|
|
27
|
-
opts: { version?: string; withStrict?: boolean; missingEvaluationExports?: boolean } = {},
|
|
27
|
+
opts: { version?: string; withStrict?: boolean; missingEvaluationExports?: boolean; resolveBody?: string[] } = {},
|
|
28
28
|
): Harness {
|
|
29
29
|
const app = createTempApp('tbox-views-');
|
|
30
30
|
invalidateContractsResolver();
|
|
@@ -33,6 +33,7 @@ function harnessWithContracts(
|
|
|
33
33
|
version: opts.version ?? '0.9.0',
|
|
34
34
|
withStrict: opts.withStrict ?? true,
|
|
35
35
|
missingEvaluationExports: opts.missingEvaluationExports,
|
|
36
|
+
...(opts.resolveBody ? { resolveBody: opts.resolveBody.join(' ') } : {}),
|
|
36
37
|
});
|
|
37
38
|
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
38
39
|
// 应用结构:parking 模块(packages/ 约定发现)+ 注册表两实例 + integrations.json
|
|
@@ -240,6 +241,96 @@ function layerHarness(): {
|
|
|
240
241
|
return { app, tk: createAppToolkit(app.appDir), schemas, joycity };
|
|
241
242
|
}
|
|
242
243
|
|
|
244
|
+
/** E 系列基座(v4.7 D38 引入;v5.1 D39 提升至模块层共用——原 D38 describe 内定义):wanda 双槽
|
|
245
|
+
* (layerHarness 形态)+ joycity 双 impl(O6 形态)+ config/schemas/ok.json(O15 形态)+
|
|
246
|
+
* module-parking demand。schema 内容互异防断言串扰:
|
|
247
|
+
* wQuery={baseUrl} / wParking={mallId} / joycity@1={joycityUrl} / joycity@2={endpointV2} / ok={okEndpoint}。
|
|
248
|
+
* 每用例独立 eHarness()——fixture 全新 temp app,零 file-cache stat 指纹串扰(不取重写+bumpMtime 方案)。
|
|
249
|
+
* 供给矩阵:wanda 供给 {parking.query, parking.payment};joycity 仅供给 {parking.query}
|
|
250
|
+
* (provider-multi 双 impl 皆 query)——D39 用例 (厂商 × 服务) 选型须对照本矩阵。 */
|
|
251
|
+
function eHarness(): {
|
|
252
|
+
app: TempApp;
|
|
253
|
+
tk: AppToolkit;
|
|
254
|
+
schemas: { wQuery: object; wParking: object; joycity1: object; joycity2: object; ok: object };
|
|
255
|
+
} {
|
|
256
|
+
const app = createTempApp('tbox-d38-');
|
|
257
|
+
invalidateContractsResolver();
|
|
258
|
+
const store = join(app.appDir, 'contracts');
|
|
259
|
+
writeFakeContractsPackage(store, { version: '0.9.0' });
|
|
260
|
+
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
261
|
+
const schemas = {
|
|
262
|
+
wQuery: { type: 'object', properties: { baseUrl: { type: 'string', format: 'uri' } }, required: ['baseUrl'] },
|
|
263
|
+
wParking: { type: 'object', properties: { mallId: { type: 'string' } } },
|
|
264
|
+
joycity1: { type: 'object', properties: { joycityUrl: { type: 'string' } } },
|
|
265
|
+
joycity2: { type: 'object', properties: { endpointV2: { type: 'string' } } },
|
|
266
|
+
ok: { type: 'object', properties: { okEndpoint: { type: 'string' } } },
|
|
267
|
+
};
|
|
268
|
+
writeAppFile(
|
|
269
|
+
app.appDir,
|
|
270
|
+
'packages/provider-w/tbox.module.json',
|
|
271
|
+
JSON.stringify({
|
|
272
|
+
schemaVersion: 1,
|
|
273
|
+
id: 'provider-w',
|
|
274
|
+
version: '0.1.0',
|
|
275
|
+
kind: 'business',
|
|
276
|
+
declare: {
|
|
277
|
+
providers: {
|
|
278
|
+
slots: [
|
|
279
|
+
{ service: 'parking.query', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/query.json' },
|
|
280
|
+
{ service: 'parking.payment', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/parking.json' },
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
dependencies: { modules: [] },
|
|
285
|
+
env: [],
|
|
286
|
+
}),
|
|
287
|
+
);
|
|
288
|
+
writeAppFile(app.appDir, 'packages/provider-w/schemas/query.json', JSON.stringify(schemas.wQuery));
|
|
289
|
+
writeAppFile(app.appDir, 'packages/provider-w/schemas/parking.json', JSON.stringify(schemas.wParking));
|
|
290
|
+
writeAppFile(
|
|
291
|
+
app.appDir,
|
|
292
|
+
'packages/provider-multi/tbox.module.json',
|
|
293
|
+
JSON.stringify({
|
|
294
|
+
schemaVersion: 1,
|
|
295
|
+
id: 'provider-multi',
|
|
296
|
+
version: '0.1.0',
|
|
297
|
+
kind: 'business',
|
|
298
|
+
declare: {
|
|
299
|
+
providers: {
|
|
300
|
+
slots: [
|
|
301
|
+
{ service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c', configSchema: 'schemas/query.json' },
|
|
302
|
+
{ service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@2', credentialType: 'joycity-c', configSchema: 'schemas/query2.json' },
|
|
303
|
+
],
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
dependencies: { modules: [] },
|
|
307
|
+
env: [],
|
|
308
|
+
}),
|
|
309
|
+
);
|
|
310
|
+
writeAppFile(app.appDir, 'packages/provider-multi/schemas/query.json', JSON.stringify(schemas.joycity1));
|
|
311
|
+
writeAppFile(app.appDir, 'packages/provider-multi/schemas/query2.json', JSON.stringify(schemas.joycity2));
|
|
312
|
+
writeAppFile(app.appDir, 'config/schemas/ok.json', JSON.stringify(schemas.ok));
|
|
313
|
+
writeAppFile(
|
|
314
|
+
app.appDir,
|
|
315
|
+
'packages/module-parking/tbox.module.json',
|
|
316
|
+
JSON.stringify({
|
|
317
|
+
schemaVersion: 1,
|
|
318
|
+
id: 'module-parking',
|
|
319
|
+
version: '0.1.0',
|
|
320
|
+
kind: 'business',
|
|
321
|
+
declare: { owns: [{ service: 'parking.query' }], consumes: [{ service: 'parking.query', optional: false }] },
|
|
322
|
+
dependencies: { modules: [] },
|
|
323
|
+
env: [],
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
326
|
+
return { app, tk: createAppToolkit(app.appDir), schemas };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** integrations.json 单次写入(每用例独立 harness——零缓存串扰) */
|
|
330
|
+
function writeIntegrations(appDir: string, config: Record<string, unknown>): void {
|
|
331
|
+
writeAppFile(appDir, 'config/integrations.json', JSON.stringify(config));
|
|
332
|
+
}
|
|
333
|
+
|
|
243
334
|
describe('views/工厂(O 矩阵核心族)', () => {
|
|
244
335
|
let h: Harness;
|
|
245
336
|
|
|
@@ -267,7 +358,8 @@ describe('views/工厂(O 矩阵核心族)', () => {
|
|
|
267
358
|
const mod = await h.tk.loadModule('module-parking');
|
|
268
359
|
// v4.4 D34:domainKeys/providerBindings/configEditable 显式化(include 两键可选缺席——F4)
|
|
269
360
|
// v7 词汇统一:模块展示名 name 前置(恒发——兜底 id)
|
|
270
|
-
|
|
361
|
+
// resources 只读镜像(N2 读半边——基座 integrations modules 节无 resources → null 仍恒发)
|
|
362
|
+
expect(Object.keys(mod)).toEqual(['name', 'description', 'ownedServices', 'integration', 'resources', 'binding', 'configEditable', 'services']);
|
|
271
363
|
const svc = await h.tk.loadService('parking.query');
|
|
272
364
|
expect(Object.keys(svc).some((k) => ['resolution', 'issues', 'contracts'].includes(k))).toBe(false);
|
|
273
365
|
// 静态端点在 <0.9 下照常(永不 503——O13 反例联动)
|
|
@@ -287,6 +379,41 @@ describe('views/工厂(O 矩阵核心族)', () => {
|
|
|
287
379
|
expect(single.resolution.inheritance).toHaveLength(4);
|
|
288
380
|
});
|
|
289
381
|
|
|
382
|
+
it('O17 v6 ownerOf 视图轴透传(回归锁——fef9a71a 漏改点):求值单源带 walk.ownership 第 5 参;modules 级 config 不触发 fail-visible', async () => {
|
|
383
|
+
// 假 contracts 复刻 v6 fail-visible 语义 + 回显 ownerOf:缺席 + modules 绑定面 → binding-unresolved;
|
|
384
|
+
// 在场 → effective.provider 前缀回显 owner 模块 id(证明透传值 = walk.ownership 内容本身)
|
|
385
|
+
const ownerEcho = harnessWithContracts({
|
|
386
|
+
resolveBody: [
|
|
387
|
+
'const modBinding = integrations && integrations.modules',
|
|
388
|
+
' && Object.values(integrations.modules).some((m) => m.provider != null || m.config != null);',
|
|
389
|
+
'if ((ownerOf === undefined || ownerOf === null) && modBinding) return { status: "binding-unresolved", message: "OWNEROF_MISSING" };',
|
|
390
|
+
'const owner = ownerOf ? ownerOf[service] : undefined;',
|
|
391
|
+
'const slot = integrations && integrations.services && integrations.services[service];',
|
|
392
|
+
'const prov = (slot && slot.provider) || "root-mock";',
|
|
393
|
+
'return { status: "ok", effective: { provider: owner ? owner + ":" + prov : prov, implementation: "impl@1", config: {} } };',
|
|
394
|
+
],
|
|
395
|
+
});
|
|
396
|
+
// 触发形态(bug 实景):modules 仅 config(无 provider)+ 空 slot——依赖 ownerOf 注入才能过 fail-visible
|
|
397
|
+
writeAppFile(
|
|
398
|
+
ownerEcho.app.appDir,
|
|
399
|
+
'config/integrations.json',
|
|
400
|
+
JSON.stringify({
|
|
401
|
+
provider: 'root-mock',
|
|
402
|
+
modules: { 'module-parking': { config: { parkName: '示例车场' } } },
|
|
403
|
+
services: { 'parking.query': {}, 'auth.alipay-login': {} },
|
|
404
|
+
}),
|
|
405
|
+
);
|
|
406
|
+
const single = await ownerEcho.tk.loadServiceResolution('parking.query');
|
|
407
|
+
// ① fail-visible 未触发(ownerOf 已注入)
|
|
408
|
+
expect(single.resolution.status).toBe('ok');
|
|
409
|
+
// ② 透传值 = walk.ownership 内容(parking.query 归属 module-parking——harness manifest declare.owns)
|
|
410
|
+
expect(single.resolution.provider).toBe('module-parking:root-mock');
|
|
411
|
+
// ③ 无主服务回退无前缀(排除「任意 truthy 值即绿」的假阳性)
|
|
412
|
+
const orphan = await ownerEcho.tk.loadServiceResolution('auth.alipay-login');
|
|
413
|
+
expect(orphan.resolution.provider).toBe('root-mock');
|
|
414
|
+
ownerEcho.app.dispose();
|
|
415
|
+
});
|
|
416
|
+
|
|
290
417
|
it('O3 键集 = 服务词汇 ∪ 已配置;required echo(OR 聚合 + 平台 false)', async () => {
|
|
291
418
|
// vocabulary 空(无契约包 fixture 物化 slots——退化态回归锁;词汇在场态见 B1 族 T-O3)
|
|
292
419
|
const batch = await h.tk.loadServiceResolutions();
|
|
@@ -302,28 +429,29 @@ describe('views/工厂(O 矩阵核心族)', () => {
|
|
|
302
429
|
expect(batch.issues.length).toBeGreaterThanOrEqual(single.issues.length);
|
|
303
430
|
});
|
|
304
431
|
|
|
305
|
-
it('O13 门控正反例:<0.9
|
|
432
|
+
it('O13 门控正反例:<0.9 读求值轴降级骨架(v7 503 退役)+ 静态 200 + 写端点 503 保留;多实例无显式默认 → 通配口径', async () => {
|
|
306
433
|
// 正例:≥0.9 求值可用(harness 默认)
|
|
307
434
|
const batch = await h.tk.loadServiceResolutions();
|
|
308
435
|
expect(batch.contracts.strictValidation).toBe(true);
|
|
309
436
|
h.app.dispose();
|
|
310
437
|
|
|
311
|
-
// 反例:缺 zod 面(0.8.2)→
|
|
438
|
+
// 反例:缺 zod 面(0.8.2)→ 读求值轴降级骨架 + N1 升级指引入 statusMessage;静态照常
|
|
312
439
|
const low = harnessWithContracts({ version: '0.8.2', withStrict: false });
|
|
313
|
-
await
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
440
|
+
const lowBatch = await low.tk.loadServiceResolutions();
|
|
441
|
+
expect(lowBatch.contracts.strictValidation).toBe(false);
|
|
442
|
+
const lowEntries = Object.values(lowBatch.resolutions);
|
|
443
|
+
expect(lowEntries.length).toBeGreaterThan(0);
|
|
444
|
+
expect(lowEntries.every((e) => e.status === 'evaluation-unavailable')).toBe(true);
|
|
445
|
+
expect(lowEntries[0]?.statusMessage).toMatch(/升级应用 @tbox\.cn\/app-contracts/);
|
|
446
|
+
expect(lowBatch.issues).toEqual([]);
|
|
318
447
|
// 静态永不 503
|
|
319
448
|
const appView = await low.tk.loadApp();
|
|
320
449
|
expect(appView.services).toBeInstanceOf(Array);
|
|
321
|
-
//
|
|
450
|
+
// 写端点 503 门控保留(policy at edge 收窄至写/迁移面)
|
|
322
451
|
await expect(
|
|
323
452
|
low.tk.writeServiceIntegration('parking.query', { provider: 'mock' }),
|
|
324
453
|
).rejects.toMatchObject({ code: 'CONTRACTS_NOT_RESOLVED', httpStatus: 503 });
|
|
325
454
|
low.app.dispose();
|
|
326
|
-
|
|
327
455
|
// 默认实例委托:无 defaultInstanceId(多实例)→ '*' 通配口径(normalizeInstances null → '*')
|
|
328
456
|
const wild = harnessWithContracts();
|
|
329
457
|
writeAppFile(
|
|
@@ -607,41 +735,102 @@ describe('O6/O7/O10/O14(FX-3 补全)', () => {
|
|
|
607
735
|
});
|
|
608
736
|
|
|
609
737
|
describe('A2 错误面(P-4 notes 优先 / P-5 畸形包 503)', () => {
|
|
610
|
-
it('M1 导入失败 message 含构建指引且不再误报未安装(P-4 notes
|
|
738
|
+
it('M1 导入失败 message 含构建指引且不再误报未安装(P-4 notes 优先——v7 降级骨架 statusMessage 承载)', async () => {
|
|
611
739
|
const app = createTempApp('tbox-m1-');
|
|
612
740
|
invalidateContractsResolver();
|
|
613
741
|
const store = joinStore(app.appDir, 'contracts');
|
|
614
742
|
writeFakeContractsPackage(store, { version: '0.9.0', brokenDist: true });
|
|
615
743
|
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
744
|
+
writeAppFile(app.appDir, 'config/integrations.json', JSON.stringify({ provider: 'mock', services: { 'parking.query': {} } }));
|
|
745
|
+
const tk = createAppToolkit(app.appDir);
|
|
746
|
+
const batch = await tk.loadServiceResolutions();
|
|
747
|
+
const entry = batch.resolutions['parking.query'];
|
|
748
|
+
expect(entry.status).toBe('evaluation-unavailable');
|
|
749
|
+
expect(entry.statusMessage).toMatch(/构建产物导入失败/);
|
|
750
|
+
expect(entry.statusMessage).not.toMatch(/未安装/);
|
|
751
|
+
app.dispose();
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
it('M4 无契约包(未安装)→ 降级骨架 + 声明实例枚举 + T2 空键集诚实态(问题②主形态)', async () => {
|
|
755
|
+
// T1 形态:contracts 未安装,walk 静态面在场(manifest demand/owns + integrations 配置)
|
|
756
|
+
const app = createTempApp('tbox-m4-');
|
|
757
|
+
invalidateContractsResolver();
|
|
758
|
+
writeAppFile(app.appDir, 'packages/module-parking/tbox.module.json', JSON.stringify({
|
|
759
|
+
schemaVersion: 1, id: 'module-parking', version: '0.1.0', kind: 'business',
|
|
760
|
+
declare: { owns: [{ service: 'parking.query' }], consumes: [{ service: 'parking.query', optional: false }] },
|
|
761
|
+
dependencies: { modules: [] },
|
|
762
|
+
env: [],
|
|
763
|
+
}));
|
|
764
|
+
writeAppFile(app.appDir, 'config/integrations.json', JSON.stringify({
|
|
765
|
+
provider: 'mock', instances: [{ id: 'mall-1' }], defaultInstanceId: 'mall-1',
|
|
766
|
+
services: { 'parking.query': { instances: { 'mall-1': { provider: 'wanda', implementation: 'wanda-parking@1' } } } },
|
|
767
|
+
}));
|
|
616
768
|
const tk = createAppToolkit(app.appDir);
|
|
617
|
-
await
|
|
618
|
-
|
|
769
|
+
const batch = await tk.loadServiceResolutions();
|
|
770
|
+
const entry = batch.resolutions['parking.query'];
|
|
771
|
+
expect(entry.status).toBe('evaluation-unavailable');
|
|
772
|
+
expect(entry.statusMessage).toMatch(/未安装/);
|
|
773
|
+
// 静态可算字段:module = demand owners 首(与求值轴同源);required = OR 聚合
|
|
774
|
+
expect(entry.module).toBe('@app/module-parking');
|
|
775
|
+
expect(entry.required).toBe(true);
|
|
776
|
+
expect(batch.issues).toEqual([]);
|
|
777
|
+
// 单体:声明实例枚举(原始数据投影——root instances[].id)逐实例同标记;
|
|
778
|
+
// inheritance 四层静态投影(app=root.provider;instance=显式 defaultInstanceId 位声明——降级默认实例口径实证)
|
|
779
|
+
const detail = await tk.loadServiceResolution('parking.query');
|
|
780
|
+
expect(detail.resolution.instances.map((i) => i.instanceId)).toEqual(['mall-1']);
|
|
781
|
+
expect(detail.resolution.instances.every((i) => i.status === 'evaluation-unavailable')).toBe(true);
|
|
782
|
+
expect(detail.resolution.inheritance).toHaveLength(4);
|
|
783
|
+
expect(detail.resolution.inheritance.find((l) => l.layer === 'app')?.provider).toBe('mock');
|
|
784
|
+
const instLayer = detail.resolution.inheritance.find((l) => l.layer === 'instance');
|
|
785
|
+
expect(instLayer?.provider).toBe('wanda');
|
|
786
|
+
expect(instLayer?.implementation).toBe('wanda-parking@1');
|
|
787
|
+
// include 轴内嵌骨架(四面统一——P14 (b))
|
|
788
|
+
const appInc = await tk.loadApp({ include: ['serviceResolutions'] });
|
|
789
|
+
expect(appInc.serviceResolutions?.resolutions['parking.query']?.status).toBe('evaluation-unavailable');
|
|
619
790
|
app.dispose();
|
|
791
|
+
|
|
792
|
+
// T2 形态:无 manifest 无 integrations → 空键集诚实态(空列表非 503;envelope 携带原因)
|
|
793
|
+
const bare = createTempApp('tbox-m4b-');
|
|
794
|
+
invalidateContractsResolver();
|
|
795
|
+
const bareTk = createAppToolkit(bare.appDir);
|
|
796
|
+
const bareBatch = await bareTk.loadServiceResolutions();
|
|
797
|
+
expect(bareBatch.resolutions).toEqual({});
|
|
798
|
+
expect(bareBatch.contracts.version).toBeNull();
|
|
799
|
+
expect(bareBatch.contracts.notes.join()).toMatch(/未安装/);
|
|
800
|
+
bare.dispose();
|
|
620
801
|
});
|
|
621
802
|
|
|
622
|
-
it('M2 <0.9 message 对齐 api.md 信封(需 ≥0.9.0——resolver note
|
|
803
|
+
it('M2 <0.9 message 对齐 api.md 信封(需 ≥0.9.0——resolver note 文案入降级 statusMessage)', async () => {
|
|
623
804
|
const app = createTempApp('tbox-m2-');
|
|
624
805
|
invalidateContractsResolver();
|
|
625
806
|
const store = joinStore(app.appDir, 'contracts');
|
|
626
807
|
writeFakeContractsPackage(store, { version: '0.8.2', withStrict: false });
|
|
627
808
|
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
809
|
+
writeAppFile(app.appDir, 'config/integrations.json', JSON.stringify({ provider: 'mock', services: { 'parking.query': {} } }));
|
|
628
810
|
const tk = createAppToolkit(app.appDir);
|
|
629
|
-
await
|
|
811
|
+
const entry = (await tk.loadServiceResolutions()).resolutions['parking.query'];
|
|
812
|
+
expect(entry.status).toBe('evaluation-unavailable');
|
|
813
|
+
expect(entry.statusMessage).toMatch(/需 ≥0\.9\.0/);
|
|
630
814
|
app.dispose();
|
|
631
815
|
});
|
|
632
816
|
|
|
633
|
-
it('M3
|
|
817
|
+
it('M3 畸形包(严格面在场求值面缺席)→ 降级骨架非 5xx(v7——evaluationContracts throw 面不再可达;形态异常 note 入 statusMessage)', async () => {
|
|
634
818
|
const app = createTempApp('tbox-m3-');
|
|
635
819
|
invalidateContractsResolver();
|
|
636
820
|
const store = joinStore(app.appDir, 'contracts');
|
|
637
821
|
writeFakeContractsPackage(store, { version: '0.9.0', missingEvaluationExports: true, slots: ['parking.query'] });
|
|
638
822
|
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
823
|
+
writeAppFile(app.appDir, 'config/integrations.json', JSON.stringify({ provider: 'mock', services: { 'parking.query': {} } }));
|
|
824
|
+
const tk = createAppToolkit(app.appDir);
|
|
825
|
+
const batch = await tk.loadServiceResolutions();
|
|
826
|
+
const entry = batch.resolutions['parking.query'];
|
|
827
|
+
expect(entry.status).toBe('evaluation-unavailable');
|
|
828
|
+
expect(entry.statusMessage).toMatch(/求值面具名导出缺席/);
|
|
829
|
+
expect(entry.statusMessage).toMatch(/契约包形态异常/);
|
|
830
|
+
// 声明实例投影在场(降级 detail——原始数据非 normalizeInstances 派生)
|
|
831
|
+
const detail = await tk.loadServiceResolution('parking.query');
|
|
832
|
+
expect(detail.resolution.status).toBe('evaluation-unavailable');
|
|
833
|
+
expect(detail.resolution.inheritance).toHaveLength(4);
|
|
645
834
|
app.dispose();
|
|
646
835
|
});
|
|
647
836
|
});
|
|
@@ -758,7 +947,7 @@ describe('v6 所有权族(D34 多域钉死退役 → ownedServices/binding 单
|
|
|
758
947
|
app.dispose();
|
|
759
948
|
});
|
|
760
949
|
|
|
761
|
-
it('D6
|
|
950
|
+
it('D6 偏斜态分级(P1 标记门 v7):v5 contracts 假件 + v6 toolkit → 写端点 503 保留;读求值轴降级骨架(503 退役)', async () => {
|
|
762
951
|
const app = createTempApp('tbox-d6-');
|
|
763
952
|
invalidateContractsResolver();
|
|
764
953
|
const store = joinStore(app.appDir, 'contracts');
|
|
@@ -768,15 +957,21 @@ describe('v6 所有权族(D34 多域钉死退役 → ownedServices/binding 单
|
|
|
768
957
|
schemaVersion: 1, id: 'module-parking', version: '0.1.0', kind: 'business',
|
|
769
958
|
declare: { owns: [{ service: 'parking.query' }] },
|
|
770
959
|
}));
|
|
960
|
+
writeAppFile(app.appDir, 'config/integrations.json', JSON.stringify({ provider: 'mock', services: { 'parking.query': {} } }));
|
|
771
961
|
const tk = createAppToolkit(app.appDir);
|
|
772
|
-
//
|
|
962
|
+
// 写路径门控保留(assertContractsUsable——policy at edge 收窄至写/迁移面)
|
|
773
963
|
await expect(tk.writeModuleIntegration('module-parking', { provider: 'wanda' })).rejects.toMatchObject({
|
|
774
964
|
code: 'CONTRACTS_NOT_RESOLVED',
|
|
775
965
|
httpStatus: 503,
|
|
776
966
|
message: expect.stringContaining('OWNERSHIP_BINDING_V6'),
|
|
777
967
|
});
|
|
778
|
-
|
|
779
|
-
|
|
968
|
+
// 读求值轴降级(503 退役):statusMessage 与写门控同源(N1 标记分支单源)
|
|
969
|
+
const degraded = await tk.loadServiceResolutions();
|
|
970
|
+
expect(degraded.contracts.strictValidation).toBe(true);
|
|
971
|
+
const entry = degraded.resolutions['parking.query'];
|
|
972
|
+
expect(entry.status).toBe('evaluation-unavailable');
|
|
973
|
+
expect(entry.statusMessage).toMatch(/OWNERSHIP_BINDING_V6 标记缺席/);
|
|
974
|
+
// 静态端点照常(永不 503——门控 policy at edge 只拦写/迁移)
|
|
780
975
|
const mod = await tk.loadModule('module-parking');
|
|
781
976
|
expect(mod.ownedServices).toEqual(['parking.query']);
|
|
782
977
|
app.dispose();
|
|
@@ -830,15 +1025,20 @@ describe('v4.4 include 协议 + Entry.layer + 层级 schema(F4/F5)', () => {
|
|
|
830
1025
|
h2.app.dispose();
|
|
831
1026
|
});
|
|
832
1027
|
|
|
833
|
-
it('I3 degrade
|
|
1028
|
+
it('I3 degrade:内嵌轴同步降级(v7 P14 (b)——D31 null 分支退役):三条件不满足 → 骨架非 null', async () => {
|
|
834
1029
|
const low = harnessWithContracts({ version: '0.8.2', withStrict: false });
|
|
835
|
-
|
|
1030
|
+
const lowInc = await low.tk.loadApp({ include: ['serviceResolutions'] });
|
|
1031
|
+
expect(lowInc.serviceResolutions).not.toBeNull();
|
|
1032
|
+
const lowEntries = Object.values(lowInc.serviceResolutions!.resolutions);
|
|
1033
|
+
expect(lowEntries.length).toBeGreaterThan(0);
|
|
1034
|
+
expect(lowEntries.every((e) => e.status === 'evaluation-unavailable')).toBe(true);
|
|
836
1035
|
expect((await low.tk.loadApp({ include: ['integrationSchemas'] })).integrationSchemas).toEqual({ provider: 'mock', services: {} }); // 静态轴富化零 contracts
|
|
837
1036
|
low.app.dispose();
|
|
1037
|
+
// 畸形包(严格面在场求值面缺席)→ 内嵌轴同样降级(503/双 null 全退役)
|
|
838
1038
|
const malformed = harnessWithContracts({ missingEvaluationExports: true });
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1039
|
+
const malformedInc = await malformed.tk.loadApp({ include: ['serviceResolutions'] });
|
|
1040
|
+
expect(malformedInc.serviceResolutions).not.toBeNull();
|
|
1041
|
+
expect(Object.values(malformedInc.serviceResolutions!.resolutions)[0]?.status).toBe('evaluation-unavailable');
|
|
842
1042
|
malformed.app.dispose();
|
|
843
1043
|
});
|
|
844
1044
|
|
|
@@ -1022,93 +1222,6 @@ describe('模块清单 provider 家族排除(2026-09-16 双缺陷批——供
|
|
|
1022
1222
|
});
|
|
1023
1223
|
|
|
1024
1224
|
describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
|
|
1025
|
-
/** E 系列基座:wanda 双槽(layerHarness 形态)+ joycity 双 impl(O6 形态)+ config/schemas/ok.json
|
|
1026
|
-
* (O15 形态)+ module-parking demand。schema 内容互异防断言串扰:
|
|
1027
|
-
* wQuery={baseUrl} / wParking={mallId} / joycity@1={joycityUrl} / joycity@2={endpointV2} / ok={okEndpoint}。
|
|
1028
|
-
* 每用例独立 eHarness()——fixture 全新 temp app,零 file-cache stat 指纹串扰(不取重写+bumpMtime 方案)。 */
|
|
1029
|
-
function eHarness(): {
|
|
1030
|
-
app: TempApp;
|
|
1031
|
-
tk: AppToolkit;
|
|
1032
|
-
schemas: { wQuery: object; wParking: object; joycity1: object; joycity2: object; ok: object };
|
|
1033
|
-
} {
|
|
1034
|
-
const app = createTempApp('tbox-d38-');
|
|
1035
|
-
invalidateContractsResolver();
|
|
1036
|
-
const store = join(app.appDir, 'contracts');
|
|
1037
|
-
writeFakeContractsPackage(store, { version: '0.9.0' });
|
|
1038
|
-
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
1039
|
-
const schemas = {
|
|
1040
|
-
wQuery: { type: 'object', properties: { baseUrl: { type: 'string', format: 'uri' } }, required: ['baseUrl'] },
|
|
1041
|
-
wParking: { type: 'object', properties: { mallId: { type: 'string' } } },
|
|
1042
|
-
joycity1: { type: 'object', properties: { joycityUrl: { type: 'string' } } },
|
|
1043
|
-
joycity2: { type: 'object', properties: { endpointV2: { type: 'string' } } },
|
|
1044
|
-
ok: { type: 'object', properties: { okEndpoint: { type: 'string' } } },
|
|
1045
|
-
};
|
|
1046
|
-
writeAppFile(
|
|
1047
|
-
app.appDir,
|
|
1048
|
-
'packages/provider-w/tbox.module.json',
|
|
1049
|
-
JSON.stringify({
|
|
1050
|
-
schemaVersion: 1,
|
|
1051
|
-
id: 'provider-w',
|
|
1052
|
-
version: '0.1.0',
|
|
1053
|
-
kind: 'business',
|
|
1054
|
-
declare: {
|
|
1055
|
-
providers: {
|
|
1056
|
-
slots: [
|
|
1057
|
-
{ service: 'parking.query', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/query.json' },
|
|
1058
|
-
{ service: 'parking.payment', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c', configSchema: 'schemas/parking.json' },
|
|
1059
|
-
],
|
|
1060
|
-
},
|
|
1061
|
-
},
|
|
1062
|
-
dependencies: { modules: [] },
|
|
1063
|
-
env: [],
|
|
1064
|
-
}),
|
|
1065
|
-
);
|
|
1066
|
-
writeAppFile(app.appDir, 'packages/provider-w/schemas/query.json', JSON.stringify(schemas.wQuery));
|
|
1067
|
-
writeAppFile(app.appDir, 'packages/provider-w/schemas/parking.json', JSON.stringify(schemas.wParking));
|
|
1068
|
-
writeAppFile(
|
|
1069
|
-
app.appDir,
|
|
1070
|
-
'packages/provider-multi/tbox.module.json',
|
|
1071
|
-
JSON.stringify({
|
|
1072
|
-
schemaVersion: 1,
|
|
1073
|
-
id: 'provider-multi',
|
|
1074
|
-
version: '0.1.0',
|
|
1075
|
-
kind: 'business',
|
|
1076
|
-
declare: {
|
|
1077
|
-
providers: {
|
|
1078
|
-
slots: [
|
|
1079
|
-
{ service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@1', credentialType: 'joycity-c', configSchema: 'schemas/query.json' },
|
|
1080
|
-
{ service: 'parking.query', provider: 'joycity', implementation: 'joycity-parking@2', credentialType: 'joycity-c', configSchema: 'schemas/query2.json' },
|
|
1081
|
-
],
|
|
1082
|
-
},
|
|
1083
|
-
},
|
|
1084
|
-
dependencies: { modules: [] },
|
|
1085
|
-
env: [],
|
|
1086
|
-
}),
|
|
1087
|
-
);
|
|
1088
|
-
writeAppFile(app.appDir, 'packages/provider-multi/schemas/query.json', JSON.stringify(schemas.joycity1));
|
|
1089
|
-
writeAppFile(app.appDir, 'packages/provider-multi/schemas/query2.json', JSON.stringify(schemas.joycity2));
|
|
1090
|
-
writeAppFile(app.appDir, 'config/schemas/ok.json', JSON.stringify(schemas.ok));
|
|
1091
|
-
writeAppFile(
|
|
1092
|
-
app.appDir,
|
|
1093
|
-
'packages/module-parking/tbox.module.json',
|
|
1094
|
-
JSON.stringify({
|
|
1095
|
-
schemaVersion: 1,
|
|
1096
|
-
id: 'module-parking',
|
|
1097
|
-
version: '0.1.0',
|
|
1098
|
-
kind: 'business',
|
|
1099
|
-
declare: { owns: [{ service: 'parking.query' }], consumes: [{ service: 'parking.query', optional: false }] },
|
|
1100
|
-
dependencies: { modules: [] },
|
|
1101
|
-
env: [],
|
|
1102
|
-
}),
|
|
1103
|
-
);
|
|
1104
|
-
return { app, tk: createAppToolkit(app.appDir), schemas };
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
/** integrations.json 单次写入(每用例独立 harness——零缓存串扰) */
|
|
1108
|
-
function writeIntegrations(appDir: string, config: Record<string, unknown>): void {
|
|
1109
|
-
writeAppFile(appDir, 'config/integrations.json', JSON.stringify(config));
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
1225
|
it('E1 显式 string + catalog schema(分支②)', async () => {
|
|
1113
1226
|
const h = eHarness();
|
|
1114
1227
|
writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
|
|
@@ -1287,6 +1400,170 @@ describe('v4.7 服务级 include(D38——生效绑定单体)', () => {
|
|
|
1287
1400
|
});
|
|
1288
1401
|
});
|
|
1289
1402
|
|
|
1403
|
+
/**
|
|
1404
|
+
* v5.1 D39 测试族(服务轴向假想绑定——?serviceProvider= 表单双投影重定向)。
|
|
1405
|
+
* 供给矩阵(eHarness):wanda 供给 {parking.query, parking.payment};joycity 仅供给
|
|
1406
|
+
* {parking.query}(provider-multi 双 impl 皆 query)——用例 (厂商 × 服务) 选型须对照本矩阵:
|
|
1407
|
+
* catalog-miss 形态(D39-7)与豁免链正例(D39-6)不得混淆供给边界。每用例独立 eHarness
|
|
1408
|
+
* (零 file-cache stat 指纹串扰);D39-8/D39-9 用 layerHarness(x-tbox-global 标记 fixture)。
|
|
1409
|
+
*/
|
|
1410
|
+
describe('v5.1 D39(服务轴向假想绑定 serviceProvider)', () => {
|
|
1411
|
+
it('D39-1 假想 happy + I1:双投影切 joycity 且 provider 一致 + 单键边界正例', async () => {
|
|
1412
|
+
const h = eHarness();
|
|
1413
|
+
writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
|
|
1414
|
+
const both = ['integrationSchemas', 'integrationFieldRegistry'];
|
|
1415
|
+
const v = await h.tk.loadService('parking.query', { include: both, serviceProvider: 'joycity' });
|
|
1416
|
+
// I1:双投影同源同口径(fields = joycity schema 字段——catalog 复合键声明序首个 = @1 的 joycityUrl)
|
|
1417
|
+
expect(v.integrationSchemas).toEqual({ provider: 'joycity', configSchema: h.schemas.joycity1 });
|
|
1418
|
+
expect(v.integrationFieldRegistry?.provider).toBe('joycity');
|
|
1419
|
+
expect(v.integrationFieldRegistry!.fields.map((f) => f.key)).toEqual(['joycityUrl']);
|
|
1420
|
+
// 单键边界正例:仅 integrationSchemas + 轴参 → 200 假想计算;registry 未请求 = 缺席(S7-1 尾部先例)
|
|
1421
|
+
const only = await h.tk.loadService('parking.query', { include: ['integrationSchemas'], serviceProvider: 'joycity' });
|
|
1422
|
+
expect(only.integrationSchemas).toEqual({ provider: 'joycity', configSchema: h.schemas.joycity1 });
|
|
1423
|
+
expect('integrationFieldRegistry' in only).toBe(false);
|
|
1424
|
+
h.app.dispose();
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1427
|
+
it('D39-2 I2 锁:integration/suppliedBy/defaults 恒实际态', async () => {
|
|
1428
|
+
const h = eHarness();
|
|
1429
|
+
writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda', config: { baseUrl: 'https://w.example' } } } });
|
|
1430
|
+
const base = await h.tk.loadService('parking.query');
|
|
1431
|
+
const v = await h.tk.loadService('parking.query', { include: ['integrationSchemas', 'integrationFieldRegistry'], serviceProvider: 'joycity' });
|
|
1432
|
+
expect(v.integration).toEqual(base.integration); // 文件逐字原值不被重定向(预览不产生「已保存」误导)
|
|
1433
|
+
expect(v.suppliedBy).toEqual(base.suppliedBy); // catalog 静态切片恒实际
|
|
1434
|
+
expect(v.defaults).toEqual(base.defaults); // manifest 预填恒实际
|
|
1435
|
+
expect(v.integrationSchemas?.provider).toBe('joycity'); // 对照:表单层已重定向
|
|
1436
|
+
h.app.dispose();
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
it('D39-3 I3 锁 + 错轴静默忽略:空串 ≡ 缺席 ≡ 实际;appProvider/moduleProvider 被无视', async () => {
|
|
1440
|
+
const h = eHarness();
|
|
1441
|
+
writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
|
|
1442
|
+
const empty = await h.tk.loadService('parking.query', { include: ['integrationFieldRegistry'], serviceProvider: '' });
|
|
1443
|
+
const absent = await h.tk.loadService('parking.query', { include: ['integrationFieldRegistry'] });
|
|
1444
|
+
expect(empty.integrationFieldRegistry).toEqual(absent.integrationFieldRegistry);
|
|
1445
|
+
// 错轴参数(变量绕过多余属性检查——运行时按名读取,appProvider/moduleProvider 被无视;S7-4 先例镜像)
|
|
1446
|
+
const opts = { include: ['integrationFieldRegistry'], serviceProvider: '', appProvider: 'joycity', moduleProvider: 'joycity' };
|
|
1447
|
+
const wrongAxis = await h.tk.loadService('parking.query', opts);
|
|
1448
|
+
expect(wrongAxis.integrationFieldRegistry).toEqual(absent.integrationFieldRegistry);
|
|
1449
|
+
h.app.dispose();
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
it('D39-4 轴参严检矩阵:400 先于 404(镜像 S7-1 形态)', async () => {
|
|
1453
|
+
const h = eHarness();
|
|
1454
|
+
await expect(h.tk.loadService('parking.query', { serviceProvider: 'x' })).rejects.toMatchObject({
|
|
1455
|
+
code: 'VALIDATION_FAILED', httpStatus: 400, fields: ['serviceProvider'],
|
|
1456
|
+
});
|
|
1457
|
+
await expect(h.tk.loadService('parking.query', { include: [], serviceProvider: 'x' })).rejects.toMatchObject({
|
|
1458
|
+
code: 'VALIDATION_FAILED', httpStatus: 400, fields: ['serviceProvider'],
|
|
1459
|
+
});
|
|
1460
|
+
// 先于 404:ghost 服务 + 无表单键轴参 → 400(非 SERVICE_NOT_FOUND);表单键在场 → 放行至 404
|
|
1461
|
+
await expect(h.tk.loadService('ghost', { serviceProvider: 'x' })).rejects.toMatchObject({
|
|
1462
|
+
code: 'VALIDATION_FAILED', httpStatus: 400, fields: ['serviceProvider'],
|
|
1463
|
+
});
|
|
1464
|
+
await expect(h.tk.loadService('ghost', { include: ['integrationSchemas'], serviceProvider: 'x' })).rejects.toMatchObject({
|
|
1465
|
+
code: 'SERVICE_NOT_FOUND', httpStatus: 404,
|
|
1466
|
+
});
|
|
1467
|
+
h.app.dispose();
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
it('D39-5 同构承诺锁:假想 ≡ PUT 改写 provider 后实读(双投影;S6 写通道先例)', async () => {
|
|
1471
|
+
const h = eHarness();
|
|
1472
|
+
const both = ['integrationSchemas', 'integrationFieldRegistry'];
|
|
1473
|
+
writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
|
|
1474
|
+
const hyp = await h.tk.loadService('parking.query', { include: both, serviceProvider: 'joycity' });
|
|
1475
|
+
// 状态演进全经 toolkit 写通道——写后本实例缓存即刻失效,免外部写缓存关注
|
|
1476
|
+
const cur = await h.tk.loadService('parking.query');
|
|
1477
|
+
const w = await h.tk.writeServiceIntegration('parking.query', { ...cur.integration!, provider: 'joycity' });
|
|
1478
|
+
expect(w.written).toBe(true);
|
|
1479
|
+
const act = await h.tk.loadService('parking.query', { include: both });
|
|
1480
|
+
expect(act.integrationSchemas).toEqual(hyp.integrationSchemas);
|
|
1481
|
+
expect(act.integrationFieldRegistry).toEqual(hyp.integrationFieldRegistry);
|
|
1482
|
+
h.app.dispose();
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
it('D39-6 假想豁免链三分支:malformed 不污染 / 内联不读取无 H9 / 未绑定仍非 null', async () => {
|
|
1486
|
+
// ① malformed 节点(E14 形态)+ 假想 → 非 null(短路在节点读取之前——坏形态只影响实际态投影)
|
|
1487
|
+
const h = eHarness();
|
|
1488
|
+
writeIntegrations(h.app.appDir, {
|
|
1489
|
+
modules: { 'module-parking': { provider: 'wanda' } },
|
|
1490
|
+
services: { 'parking.query': { provider: { provider: 'my-api' } } },
|
|
1491
|
+
});
|
|
1492
|
+
const bad = await h.tk.loadService('parking.query', { include: ['integrationFieldRegistry'], serviceProvider: 'joycity' });
|
|
1493
|
+
expect(bad.integrationFieldRegistry).not.toBeNull();
|
|
1494
|
+
expect(bad.integrationFieldRegistry!.provider).toBe('joycity');
|
|
1495
|
+
// 实际态对照:同坏节点无轴参 → 整字段 null(E14 双态互证——预览与坏文件解耦)
|
|
1496
|
+
const badActual = await h.tk.loadService('parking.query', { include: ['integrationSchemas'] });
|
|
1497
|
+
expect(badActual.integrationSchemas).toBeNull();
|
|
1498
|
+
h.app.dispose();
|
|
1499
|
+
// ② 内联 ref 逃逸节点(E4 形态)+ 假想 → 无 H9 400(短路不读节点内联 ref——换厂商即离开内联)
|
|
1500
|
+
const h2 = eHarness();
|
|
1501
|
+
writeIntegrations(h2.app.appDir, {
|
|
1502
|
+
services: { 'parking.query': { provider: { provider: 'my-api', credentialType: '-', configSchema: 'config/schemas-evil/x.json' } } },
|
|
1503
|
+
});
|
|
1504
|
+
const inline = await h2.tk.loadService('parking.query', { include: ['integrationSchemas'], serviceProvider: 'joycity' });
|
|
1505
|
+
expect(inline.integrationSchemas).toEqual({ provider: 'joycity', configSchema: h2.schemas.joycity1 });
|
|
1506
|
+
h2.app.dispose();
|
|
1507
|
+
// ③ 全链未绑定(E8 形态)+ 假想 → 仍非 null(服务轴恒可写——无 module 轴零归属豁免形态);
|
|
1508
|
+
// wanda 供给 payment(供给矩阵)→ mallId 字段在场
|
|
1509
|
+
const h3 = eHarness();
|
|
1510
|
+
writeIntegrations(h3.app.appDir, { services: { 'parking.payment': {} } });
|
|
1511
|
+
const unbound = await h3.tk.loadService('parking.payment', { include: ['integrationFieldRegistry'], serviceProvider: 'wanda' });
|
|
1512
|
+
// fields[].schema = 字段子 schema(非整文件 schema——G 批锁 :1777 同型)
|
|
1513
|
+
expect(unbound.integrationFieldRegistry).toEqual({
|
|
1514
|
+
provider: 'wanda', schemaless: [], unsupplied: [], additionalPropertiesAllowed: true,
|
|
1515
|
+
fields: [{ key: 'mallId', schema: { type: 'string' }, sources: [{ service: 'parking.payment', required: false }], global: false }],
|
|
1516
|
+
});
|
|
1517
|
+
h3.app.dispose();
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1520
|
+
it('D39-7 catalog miss:假想未知厂商 → 非 null 空集(schemaless=[service])', async () => {
|
|
1521
|
+
const h = eHarness();
|
|
1522
|
+
writeIntegrations(h.app.appDir, { services: { 'parking.query': { provider: 'wanda' } } });
|
|
1523
|
+
const v = await h.tk.loadService('parking.query', { include: ['integrationSchemas', 'integrationFieldRegistry'], serviceProvider: 'ghost-vendor' });
|
|
1524
|
+
expect(v.integrationSchemas).toEqual({ provider: 'ghost-vendor', configSchema: null });
|
|
1525
|
+
expect(v.integrationFieldRegistry).toEqual({
|
|
1526
|
+
provider: 'ghost-vendor', schemaless: ['parking.query'], unsupplied: [], additionalPropertiesAllowed: true, fields: [],
|
|
1527
|
+
});
|
|
1528
|
+
h.app.dispose();
|
|
1529
|
+
});
|
|
1530
|
+
|
|
1531
|
+
it('D39-8 global 旗标假想透传:标记位如实(layerHarness——true 正例 + 未标记 false 不过滤)', async () => {
|
|
1532
|
+
const h = layerHarness();
|
|
1533
|
+
// joycity 供给 parking.query → 假想后 fields = joycity query schema 字段(fixture 全标记 → global:true)
|
|
1534
|
+
const joy = await h.tk.loadService('parking.query', { include: ['integrationFieldRegistry'], serviceProvider: 'joycity' });
|
|
1535
|
+
expect(joy.integrationFieldRegistry!.provider).toBe('joycity');
|
|
1536
|
+
expect(joy.integrationFieldRegistry!.fields.length).toBeGreaterThan(0);
|
|
1537
|
+
expect(joy.integrationFieldRegistry!.fields.every((f) => f.global)).toBe(true);
|
|
1538
|
+
// wanda 供给 parking.payment → mallId global:true / parkLotId global:false 在场(服务轴不过滤——G 批语义假想不变);
|
|
1539
|
+
// payment 先配置入词汇(P9:契约包 ∪ 已配置——layerHarness 初始 integrations.json 未配置 payment;
|
|
1540
|
+
// 覆盖写后 payment 节点 {} 实际态级联双缺 → null,假想 wanda 短路仍非 null——豁免链③同型,S7-7 外部写先例)
|
|
1541
|
+
writeAppFile(h.app.appDir, 'config/integrations.json', JSON.stringify({ services: { 'parking.payment': {} } }));
|
|
1542
|
+
const wan = await h.tk.loadService('parking.payment', { include: ['integrationFieldRegistry'], serviceProvider: 'wanda' });
|
|
1543
|
+
expect(wan.integrationFieldRegistry!.fields.map((f) => f.key).sort()).toEqual(['mallId', 'parkLotId']);
|
|
1544
|
+
expect(wan.integrationFieldRegistry!.fields.find((f) => f.key === 'mallId')!.global).toBe(true);
|
|
1545
|
+
expect(wan.integrationFieldRegistry!.fields.find((f) => f.key === 'parkLotId')!.global).toBe(false);
|
|
1546
|
+
h.app.dispose();
|
|
1547
|
+
});
|
|
1548
|
+
|
|
1549
|
+
it('D39-9 单源锁:loadServiceFieldRegistry(snap,s,X) ≡ buildIntegrationFieldRegistry over 链函数', async () => {
|
|
1550
|
+
const h = layerHarness();
|
|
1551
|
+
const { loadViewSnapshotSync } = await import('../src/views/context.js');
|
|
1552
|
+
const snap = loadViewSnapshotSync({ appDir: h.app.appDir }, await h.tk.resolveContracts());
|
|
1553
|
+
const X = 'joycity';
|
|
1554
|
+
const reg = loadServiceFieldRegistry(snap, 'parking.query', X);
|
|
1555
|
+
const schemas = loadServiceIntegrationSchemas(snap, 'parking.query', X);
|
|
1556
|
+
expect(schemas).not.toBeNull();
|
|
1557
|
+
// 结构性断言(依赖契约锁——registry 旁路重取 schema 破 I1 即刻红;S8 同族意图)
|
|
1558
|
+
const manual = buildIntegrationFieldRegistry({
|
|
1559
|
+
provider: schemas!.provider,
|
|
1560
|
+
services: { 'parking.query': { configSchema: schemas!.configSchema } },
|
|
1561
|
+
});
|
|
1562
|
+
expect(reg).toEqual(manual);
|
|
1563
|
+
h.app.dispose();
|
|
1564
|
+
});
|
|
1565
|
+
});
|
|
1566
|
+
|
|
1290
1567
|
/**
|
|
1291
1568
|
* v4.6 D36/D37 测试族(S5 接续 S1-S4):S5a 纯代数(直调合成 layer 字面量,零 fs)/
|
|
1292
1569
|
* S5b 接线序 + 等价锁 / S6 同构(假想 ≡ 写后实读)/ S7 边界(400·null 两态·I2·空集)/
|
|
@@ -1891,6 +2168,46 @@ describe('B1 展示名链', () => {
|
|
|
1891
2168
|
h2.app.dispose();
|
|
1892
2169
|
});
|
|
1893
2170
|
|
|
2171
|
+
it('T2b loadModulesView:hidden 模块默认过滤;includeHidden 可见 + 徽标;detail 恒 200 不滤', async () => {
|
|
2172
|
+
const app = createTempApp('tbox-hidden-');
|
|
2173
|
+
invalidateContractsResolver();
|
|
2174
|
+
const store = joinStore(app.appDir, 'contracts');
|
|
2175
|
+
writeFakeContractsPackage(store, { version: '0.9.0' });
|
|
2176
|
+
linkContracts(app.appDir, store, { subdir: 'apps/server' });
|
|
2177
|
+
const manifest = (id: string, name: string, extra: Record<string, unknown> = {}): string =>
|
|
2178
|
+
JSON.stringify({
|
|
2179
|
+
schemaVersion: 1,
|
|
2180
|
+
id,
|
|
2181
|
+
version: '0.1.0',
|
|
2182
|
+
kind: 'business',
|
|
2183
|
+
name,
|
|
2184
|
+
declare: { owns: [], consumes: [] },
|
|
2185
|
+
dependencies: { modules: [] },
|
|
2186
|
+
env: [],
|
|
2187
|
+
...extra,
|
|
2188
|
+
});
|
|
2189
|
+
mkdirSync(join(app.appDir, 'packages', 'module-visible'), { recursive: true });
|
|
2190
|
+
writeAppFile(app.appDir, 'packages/module-visible/tbox.module.json', manifest('module-visible', '可见模块'));
|
|
2191
|
+
mkdirSync(join(app.appDir, 'packages', 'module-secret'), { recursive: true });
|
|
2192
|
+
writeAppFile(app.appDir, 'packages/module-secret/tbox.module.json', manifest('module-secret', '隐藏模块', { hidden: true }));
|
|
2193
|
+
|
|
2194
|
+
const tk = createAppToolkit(app.appDir);
|
|
2195
|
+
// 默认:hidden 模块不进清单(可见模块无 hidden 键——additive 纪律)
|
|
2196
|
+
const def = await tk.loadModules();
|
|
2197
|
+
expect(def.modules.map((m) => m.id)).toEqual(['module-visible']);
|
|
2198
|
+
expect(def.modules[0]).not.toHaveProperty('hidden');
|
|
2199
|
+
// includeHidden = true:可见并携带 hidden: true 徽标
|
|
2200
|
+
const inc = await tk.loadModules({ includeHidden: true });
|
|
2201
|
+
expect(inc.modules.map((m) => m.id).sort()).toEqual(['module-secret', 'module-visible']);
|
|
2202
|
+
expect(inc.modules.find((m) => m.id === 'module-secret')?.hidden).toBe(true);
|
|
2203
|
+
expect(inc.modules.find((m) => m.id === 'module-visible')).not.toHaveProperty('hidden');
|
|
2204
|
+
// detail 端点恒 200 不滤——视图携带 hidden 徽标
|
|
2205
|
+
const detail = await tk.loadModule('module-secret');
|
|
2206
|
+
expect(detail.name).toBe('隐藏模块');
|
|
2207
|
+
expect(detail.hidden).toBe(true);
|
|
2208
|
+
app.dispose();
|
|
2209
|
+
});
|
|
2210
|
+
|
|
1894
2211
|
it('T3 loadModuleDetailView:meta 恒发;services[] 词汇优先(demand 车位查询被词汇停车查询压过——防漂移锁)', async () => {
|
|
1895
2212
|
const h2 = displayHarness();
|
|
1896
2213
|
const v = await h2.tk.loadModule('module-parking');
|