cabloy 5.1.207 → 5.1.209

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +13 -0
  3. package/README.md +1 -0
  4. package/package.json +1 -1
  5. package/repo-agent-governance/managed-assets.json +9 -9
  6. package/repo-agent-governance/policies/repository.md +1 -1
  7. package/repo-agent-governance/skills/cabloy-workflow/SKILL.md +4 -2
  8. package/repo-agent-governance/skills/cabloy-workflow/evals/evals.json +13 -1
  9. package/repo-docs/backend/unit-testing.md +1 -1
  10. package/repo-docs/frontend/form-guide.md +17 -1
  11. package/repo-docs/frontend/model-architecture.md +4 -0
  12. package/repo-docs/frontend/model-state-guide.md +23 -2
  13. package/repo-docs/frontend/resource-entry-page-deep-dive.md +119 -0
  14. package/repo-docs/frontend/scripts.md +3 -3
  15. package/repo-docs/frontend/zova-form-under-the-hood.md +5 -0
  16. package/repo-docs/fullstack/parallel-worktree-environment.md +3 -3
  17. package/repo-docs/fullstack/quickstart.md +11 -1
  18. package/repo-docs/reference/repo-scripts.md +19 -5
  19. package/repo-e2e/specs/cabloy-basic.spec.ts +1577 -0
  20. package/vona/packages-vona/vona/package.json +1 -1
  21. package/vona/src/suite/a-home/modules/home-user/src/controller/passport.ts +1 -0
  22. package/vona/src/suite/a-home/modules/home-user/src/dto/passport.ts +4 -0
  23. package/vona/src/suite/a-home/modules/home-user/test/passport.test.ts +172 -0
  24. package/vona/src/suite/a-home/modules/home-user/test/role.test.ts +1 -0
  25. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
  26. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  27. package/vona/src/suite-vendor/a-vona/modules/a-openapi/package.json +1 -1
  28. package/vona/src/suite-vendor/a-vona/modules/a-openapi/src/types/rest.ts +1 -0
  29. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  30. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  31. package/zova/packages-cli/cli/package.json +2 -2
  32. package/zova/packages-cli/cli-set-front/cli/templates/rest/render.ts +2 -0
  33. package/zova/packages-cli/cli-set-front/cli/templates/rest/rest.ts +9 -0
  34. package/zova/packages-cli/cli-set-front/package.json +1 -1
  35. package/zova/packages-zova/zova/package.json +2 -2
  36. package/zova/src/suite/a-home/modules/home-api/src/.metadata/index.ts +17 -17
  37. package/zova/src/suite/a-home/modules/home-api/src/api/openapi/schemas.ts +68 -52
  38. package/zova/src/suite/a-home/modules/home-api/src/api/openapi/types.ts +524 -343
  39. package/zova/src/suite/a-home/modules/home-passport/src/model/passport.ts +45 -11
  40. package/zova/src/suite/a-home/modules/home-passport/test/lib/persistenceIdentity.test.ts +229 -0
  41. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/package.json +9 -1
  42. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/.metadata/index.ts +81 -5
  43. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/bean/bean.draftCoordinator.ts +158 -0
  44. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/component/blockForm/controller.tsx +11 -3
  45. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/component/blockPageEntry/controller.tsx +438 -8
  46. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/config/config.ts +11 -0
  47. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/config/locale/en-us.ts +9 -0
  48. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/config/locale/zh-cn.ts +9 -0
  49. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/lib/draftSchema.ts +649 -0
  50. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/model/draft.ts +59 -0
  51. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/monkey.ts +11 -0
  52. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/types/draft.ts +49 -0
  53. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftDate.test.ts +262 -0
  54. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftLifecycle.test.ts +1073 -0
  55. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftLoader.mjs +13 -0
  56. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftModel.test.ts +572 -0
  57. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftSchema.test.ts +427 -0
  58. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftSchemaSafety.test.ts +200 -0
  59. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  60. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/page/entry/controller.tsx +6 -1
  61. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  62. package/zova/src/suite-vendor/a-zova/modules/a-form/package.json +1 -1
  63. package/zova/src/suite-vendor/a-zova/modules/a-form/src/component/form/controller.tsx +86 -1
  64. package/zova/src/suite-vendor/a-zova/modules/a-form/src/component/formField/controller.tsx +88 -13
  65. package/zova/src/suite-vendor/a-zova/modules/a-form/src/lib/schemaRefresh.ts +751 -0
  66. package/zova/src/suite-vendor/a-zova/modules/a-form/test/lib/schemaRefresh.test.ts +637 -0
  67. package/zova/src/suite-vendor/a-zova/modules/a-form/test/lib/schemaRefreshLifecycle.test.ts +994 -0
  68. package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
  69. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.persister.ts +331 -87
  70. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.query.ts +11 -6
  71. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useQuery.ts +3 -1
  72. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useState.ts +17 -1
  73. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/index.ts +1 -0
  74. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/modelPersistence.ts +204 -0
  75. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/persistenceQueue.ts +50 -0
  76. package/zova/src/suite-vendor/a-zova/modules/a-model/src/monkey.ts +5 -0
  77. package/zova/src/suite-vendor/a-zova/modules/a-model/src/service/storage.ts +47 -2
  78. package/zova/src/suite-vendor/a-zova/modules/a-model/src/types/query.ts +3 -0
  79. package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/modelLoader.mjs +34 -0
  80. package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/modelPersistence.test.ts +727 -0
  81. package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/persistenceQueue.test.ts +137 -0
  82. package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/persistenceTransition.test.ts +129 -0
  83. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  84. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/monkey.ts +7 -5
  85. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/rest.ts +2 -0
  86. package/zova/src/suite-vendor/a-zova/modules/a-router/package.json +1 -1
  87. package/zova/src/suite-vendor/a-zova/modules/a-router/src/bean/bean.router.ts +14 -0
  88. package/zova/src/suite-vendor/a-zova/modules/a-router/src/lib/routeDiscard.ts +26 -0
  89. package/zova/src/suite-vendor/a-zova/modules/a-router/src/service/routerGuards.ts +6 -3
  90. package/zova/src/suite-vendor/a-zova/modules/a-router/src/types/index.ts +1 -0
  91. package/zova/src/suite-vendor/a-zova/modules/a-router/src/types/routeDiscard.ts +24 -0
  92. package/zova/src/suite-vendor/a-zova/modules/a-router/test/lib/routeDiscard.test.ts +105 -0
  93. package/zova/src/suite-vendor/a-zova/modules/a-routertabs/package.json +1 -1
  94. package/zova/src/suite-vendor/a-zova/modules/a-routertabs/src/model/tabs.ts +202 -30
  95. package/zova/src/suite-vendor/a-zova/modules/a-routertabs/test/lib/tabs.test.ts +524 -0
  96. package/zova/src/suite-vendor/a-zova/modules/a-routertabs/test/lib/tabsLoader.mjs +35 -0
  97. package/zova/src/suite-vendor/a-zova/package.json +6 -6
@@ -0,0 +1,637 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import type { ISchemaRefreshContext } from '../../src/lib/schemaRefresh.js';
5
+
6
+ import {
7
+ createSchemaRefreshContract,
8
+ schemaRefreshFingerprint,
9
+ SchemaRefreshTracker,
10
+ } from '../../src/lib/schemaRefresh.js';
11
+
12
+ function contract(
13
+ name: Record<string, unknown> = {},
14
+ level: Record<string, unknown> = {},
15
+ root: Record<string, unknown> = {},
16
+ ) {
17
+ const properties = { name: { type: 'string', ...name }, level: { type: 'number', ...level } };
18
+ return createSchemaRefreshContract(
19
+ { type: 'object', properties, ...root },
20
+ Object.entries(properties).map(([key, property]) => ({ key, ...property })),
21
+ () => undefined,
22
+ );
23
+ }
24
+
25
+ function context(overrides: Partial<ISchemaRefreshContext> = {}): ISchemaRefreshContext {
26
+ return {
27
+ locale: 'en-us',
28
+ identity: 'student:edit:1',
29
+ epoch: 1,
30
+ scene: 'form',
31
+ contract: contract({ title: 'Name', errorMessage: { minLength: 'Too short' } }),
32
+ configuration: 'default',
33
+ enabled: true,
34
+ eligible: true,
35
+ ...overrides,
36
+ };
37
+ }
38
+
39
+ const chinese = () => contract({ title: '姓名', errorMessage: { minLength: '太短' } });
40
+
41
+ test('normalizes only display strings on schema nodes, retaining message keys and raw validation', () => {
42
+ const before = contract({
43
+ title: 'Name',
44
+ description: 'Student name',
45
+ errorMessage: { minLength: 'Short' },
46
+ });
47
+ const after = contract({
48
+ title: '姓名',
49
+ description: '学生姓名',
50
+ errorMessage: { minLength: '太短' },
51
+ });
52
+ assert.equal(before.ready, true);
53
+ assert.equal(before.validation, after.validation);
54
+ assert.notEqual(before.validationRaw, after.validationRaw);
55
+ assert.notEqual(before.fields.name.raw, after.fields.name.raw);
56
+ assert.equal(before.fields.name.compatible, after.fields.name.compatible);
57
+ assert.notEqual(before.validation, contract({ errorMessage: { maximum: 'Short' } }).validation);
58
+ for (const keyword of ['default', 'const', 'enum', 'examples', 'options', 'x-unknown']) {
59
+ const a = contract({ [keyword]: { title: 'Name', description: 'English' } });
60
+ const b = contract({ [keyword]: { title: '姓名', description: '中文' } });
61
+ assert.notEqual(a.validation, b.validation, keyword);
62
+ assert.notEqual(a.fields.name.compatible, b.fields.name.compatible, keyword);
63
+ }
64
+ assert.notEqual(contract({ title: 1 }).validation, contract({ title: 2 }).validation);
65
+ assert.notEqual(
66
+ contract({ errorMessage: 'English' }).validation,
67
+ contract({ errorMessage: '中文' }).validation,
68
+ );
69
+ });
70
+
71
+ test('serialization is sorted, detached, and distinguishes undefined from absent or null', () => {
72
+ assert.equal(schemaRefreshFingerprint({ b: 2, a: 1 }), schemaRefreshFingerprint({ a: 1, b: 2 }));
73
+ for (const other of [{}, { a: null }, { a: 'undefined' }]) {
74
+ assert.notEqual(schemaRefreshFingerprint({ a: undefined }), schemaRefreshFingerprint(other));
75
+ }
76
+ const property = { key: 'name', type: 'string', default: { title: 'Original' } };
77
+ const schema = { type: 'object', properties: { name: property } };
78
+ const before = createSchemaRefreshContract(schema, [property], () => undefined);
79
+ const signature = before.fields.name.raw;
80
+ property.default.title = 'Changed';
81
+ assert.equal(before.fields.name.raw, signature);
82
+ assert.notEqual(
83
+ before.validation,
84
+ createSchemaRefreshContract(schema, [property], () => undefined).validation,
85
+ );
86
+ assert.notEqual(contract({ default: undefined }).validation, contract().validation);
87
+ });
88
+
89
+ test('parent required and unknown validation keywords invalidate every field', () => {
90
+ const baseline = contract({}, {}, { required: ['name'] });
91
+ assert.notEqual(baseline.validation, contract({}, {}, { required: ['level'] }).validation);
92
+ assert.notEqual(baseline.validationRaw, contract({}, {}, { required: ['level'] }).validationRaw);
93
+ assert.notEqual(
94
+ contract({}, {}, { 'x-rule': 1 }).validation,
95
+ contract({}, {}, { 'x-rule': 2 }).validation,
96
+ );
97
+ const tracker = new SchemaRefreshTracker();
98
+ tracker.consume('level', context({ contract: baseline }));
99
+ assert.deepEqual(
100
+ tracker.consume(
101
+ 'level',
102
+ context({ locale: 'zh-cn', contract: contract({}, {}, { required: ['level'] }) }),
103
+ ),
104
+ { changed: true, preserve: false },
105
+ );
106
+ });
107
+
108
+ function studentRest(title: string) {
109
+ return {
110
+ render: {
111
+ $$typeof: 'zova-jsx:component',
112
+ type: 'training-student:formFieldLevel',
113
+ props: {
114
+ items: [{ value: 1, title }],
115
+ placeholder: title,
116
+ },
117
+ },
118
+ };
119
+ }
120
+
121
+ test('Student static JSX rest is strict per-field and does not block an independent name', () => {
122
+ const before = contract({ title: 'Name' }, { rest: studentRest('Level') });
123
+ const after = contract({ title: '姓名' }, { rest: studentRest('级别') });
124
+ assert.equal(before.fields.name.eligible, true);
125
+ assert.equal(before.validation, after.validation);
126
+ assert.equal(before.behavior, after.behavior);
127
+ assert.notEqual(before.fields.level.compatible, after.fields.level.compatible);
128
+ const tracker = new SchemaRefreshTracker();
129
+ tracker.consume('name', context({ contract: before }));
130
+ tracker.consume('level', context({ contract: before }));
131
+ assert.deepEqual(tracker.consume('name', context({ locale: 'zh-cn', contract: after })), {
132
+ changed: true,
133
+ preserve: true,
134
+ });
135
+ assert.deepEqual(tracker.consume('level', context({ locale: 'zh-cn', contract: after })), {
136
+ changed: true,
137
+ preserve: false,
138
+ });
139
+ assert.notEqual(
140
+ contract({}, {}, { rest: { readonly: false } }).behavior,
141
+ contract({}, {}, { rest: { readonly: true } }).behavior,
142
+ );
143
+ const ancestor = (readonly: boolean) =>
144
+ createSchemaRefreshContract(
145
+ {
146
+ properties: {
147
+ content: { type: 'object', rest: { readonly }, properties: { name: { type: 'string' } } },
148
+ },
149
+ },
150
+ [{ key: 'content.name', type: 'string' }],
151
+ () => undefined,
152
+ );
153
+ assert.equal(ancestor(false).validation, ancestor(true).validation);
154
+ assert.notEqual(ancestor(false).behavior, ancestor(true).behavior);
155
+ });
156
+
157
+ test('opaque cross-field behavior or unknown rest fails preservation globally', () => {
158
+ const cases = [
159
+ { onEffect: 'handler' },
160
+ { onEffect: () => {} },
161
+ { evaluate: 'value + 1' },
162
+ { commands: [] },
163
+ { behaviors: {} },
164
+ { unknown: 'opaque' },
165
+ { visible: { unknown: 'opaque' } },
166
+ { options: { unknown: 'opaque' } },
167
+ { visible: 'cel://value.name != ""' },
168
+ { render: { type: 'training-student:formFieldLevel', props: { onChange: 'handler' } } },
169
+ { render: { type: 'component', props: { items: [{ title: 'cel://value.name' }] } } },
170
+ ];
171
+ for (const rest of cases) {
172
+ const result = contract({}, { rest });
173
+ assert.equal(result.ready, true);
174
+ assert.equal(result.fields.name.eligible, false);
175
+ assert.equal(result.fields.level.eligible, false);
176
+ }
177
+ });
178
+
179
+ test('resolves supported schema positions, retaining ref identity and target changes', () => {
180
+ const schema: Record<string, unknown> = {
181
+ type: 'object',
182
+ properties: { name: { $ref: '#/$defs/Name' } },
183
+ $defs: { Name: { type: 'string', title: 'Name' } },
184
+ };
185
+ const build = () =>
186
+ createSchemaRefreshContract(schema, [{ key: 'name', $ref: '#/$defs/Name' }], () => undefined);
187
+ const before = build();
188
+ (schema.$defs as any).Name.title = '姓名';
189
+ const after = build();
190
+ assert.equal(before.ready, true);
191
+ assert.equal(before.validation, after.validation);
192
+ assert.equal(before.fields.name.compatible, after.fields.name.compatible);
193
+ assert.notEqual(before.fields.name.raw, after.fields.name.raw);
194
+ (schema.$defs as any).Name.minLength = 3;
195
+ assert.notEqual(after.validation, build().validation);
196
+ const target = { type: 'string' };
197
+ const ref = (name: string) =>
198
+ createSchemaRefreshContract(
199
+ { properties: { name: { $ref: name } } },
200
+ [{ key: 'name', $ref: name }],
201
+ () => target,
202
+ );
203
+ assert.notEqual(ref('A').validation, ref('B').validation);
204
+ for (const keyword of [
205
+ 'items',
206
+ 'additionalProperties',
207
+ 'unevaluatedProperties',
208
+ 'contains',
209
+ 'not',
210
+ 'if',
211
+ 'then',
212
+ 'else',
213
+ 'propertyNames',
214
+ 'contentSchema',
215
+ ]) {
216
+ const result = contract({ [keyword]: { $ref: 'missing' } });
217
+ assert.equal(result.ready, false, keyword);
218
+ }
219
+ for (const keyword of ['allOf', 'anyOf', 'oneOf', 'prefixItems']) {
220
+ assert.equal(contract({ [keyword]: [{ $ref: 'missing' }] }).ready, false, keyword);
221
+ }
222
+ for (const keyword of [
223
+ 'properties',
224
+ 'patternProperties',
225
+ '$defs',
226
+ 'definitions',
227
+ 'dependentSchemas',
228
+ 'dependencies',
229
+ ]) {
230
+ assert.equal(contract({ [keyword]: { child: { $ref: 'missing' } } }).ready, false, keyword);
231
+ }
232
+ // A business object containing $ref/title is data, not another schema node.
233
+ assert.equal(contract({ default: { $ref: 'missing', title: 'Data' } }).ready, true);
234
+ });
235
+
236
+ test('unresolved refs defer, while cycles and runtime metadata remain changed but ineligible', () => {
237
+ assert.equal(createSchemaRefreshContract({ $ref: 'missing' }, [], () => undefined).ready, false);
238
+ const result = createSchemaRefreshContract(
239
+ { $ref: 'A' },
240
+ [{ key: 'name', type: 'string' }],
241
+ () => ({ $ref: 'A' }),
242
+ );
243
+ assert.equal(result.ready, true);
244
+ assert.equal(result.fields.name.eligible, false);
245
+ const cyclic: any = { type: 'object' };
246
+ cyclic.properties = { self: cyclic };
247
+ const cycle = createSchemaRefreshContract(
248
+ cyclic,
249
+ [{ key: 'name', type: 'string' }],
250
+ () => undefined,
251
+ );
252
+ assert.equal(cycle.ready, true);
253
+ assert.equal(cycle.fields.name.eligible, false);
254
+ const callback = () => {};
255
+ assert.equal(contract({ default: { callback } }).fields.name.eligible, false);
256
+ assert.equal(contract({ 'x-runtime': callback }).fields.name.eligible, false);
257
+ assert.equal(schemaRefreshFingerprint(callback), schemaRefreshFingerprint(callback));
258
+ assert.notEqual(
259
+ schemaRefreshFingerprint(callback),
260
+ schemaRefreshFingerprint(() => {}),
261
+ );
262
+ let invoked = false;
263
+ const getter = {
264
+ get title() {
265
+ invoked = true;
266
+ return 'Never read';
267
+ },
268
+ };
269
+ schemaRefreshFingerprint(getter);
270
+ const opaque = contract({ 'x-runtime': getter });
271
+ assert.equal(invoked, false);
272
+ assert.equal(opaque.fields.name.eligible, false);
273
+ });
274
+
275
+ test('canonical keys and aliases remain strict; duplicates fail closed', () => {
276
+ const build = (aliases: Record<string, unknown>) =>
277
+ createSchemaRefreshContract(
278
+ { type: 'object' },
279
+ [{ key: 'content.name', type: 'string', ...aliases }],
280
+ () => undefined,
281
+ );
282
+ const a = build({ schemaKey: '_name', schemaKeys: ['alias'] });
283
+ const b = build({ schemaKey: '_name', schemaKeys: ['other'] });
284
+ assert.deepEqual(Object.keys(a.fields), ['content.name']);
285
+ assert.notEqual(a.fields['content.name'].compatible, b.fields['content.name'].compatible);
286
+ const duplicate = createSchemaRefreshContract(
287
+ {},
288
+ [{ key: 'name' }, { key: 'name' }],
289
+ () => undefined,
290
+ );
291
+ assert.equal(duplicate.fields.name.eligible, false);
292
+ assert.deepEqual(Object.keys(build({ key: '__proto__.name' }).fields), []);
293
+ });
294
+
295
+ test('tracker preserves eligible locale refresh only and does not overwrite on a loading gap', () => {
296
+ const tracker = new SchemaRefreshTracker();
297
+ assert.equal(tracker.has('name'), false);
298
+ assert.deepEqual(tracker.consume('name', context()), { changed: false, preserve: false });
299
+ assert.equal(tracker.has('name'), true);
300
+ assert.deepEqual(
301
+ tracker.consume('name', context({ locale: 'zh-cn', contract: { ready: false, fields: {} } })),
302
+ { changed: false, preserve: false },
303
+ );
304
+ assert.deepEqual(tracker.consume('name', context({ locale: 'zh-cn', contract: chinese() })), {
305
+ changed: true,
306
+ preserve: true,
307
+ });
308
+ assert.deepEqual(tracker.consume('name', context({ locale: 'zh-cn', contract: chinese() })), {
309
+ changed: false,
310
+ preserve: false,
311
+ });
312
+ tracker.clear();
313
+ assert.equal(tracker.has('name'), false);
314
+ });
315
+
316
+ test('tracker rejects epoch drift, absent identities, owner config and eligibility changes', () => {
317
+ for (const overrides of [
318
+ { epoch: 2 },
319
+ { identity: undefined },
320
+ { identity: 'student:create' },
321
+ { scene: 'form-view' },
322
+ { configuration: 'custom' },
323
+ { enabled: false },
324
+ { eligible: false },
325
+ { contract: contract({ title: '姓名', minLength: 1 }) },
326
+ ]) {
327
+ const tracker = new SchemaRefreshTracker();
328
+ tracker.consume('name', context());
329
+ assert.deepEqual(
330
+ tracker.consume('name', context({ locale: 'zh-cn', contract: chinese(), ...overrides })),
331
+ { changed: true, preserve: false },
332
+ );
333
+ }
334
+ const tracker = new SchemaRefreshTracker();
335
+ tracker.consume('name', context());
336
+ assert.deepEqual(tracker.consume('name', context({ epoch: 2 })), {
337
+ changed: false,
338
+ preserve: false,
339
+ });
340
+ assert.deepEqual(
341
+ tracker.consume('name', context({ epoch: 2, locale: 'zh-cn', contract: chinese() })),
342
+ { changed: true, preserve: true },
343
+ );
344
+ });
345
+
346
+ test('tracker observes parent raw validation, field removal and manual field configuration', () => {
347
+ const tracker = new SchemaRefreshTracker();
348
+ tracker.consume(
349
+ 'level',
350
+ context({ contract: contract({}, {}, { errorMessage: { required: 'Required' } }) }),
351
+ );
352
+ assert.deepEqual(
353
+ tracker.consume(
354
+ 'level',
355
+ context({ contract: contract({}, {}, { errorMessage: { required: '必须' } }) }),
356
+ ),
357
+ { changed: true, preserve: false },
358
+ );
359
+ tracker.consume('name', context());
360
+ const empty = { ready: true, fields: {} };
361
+ assert.deepEqual(tracker.consume('name', context({ contract: empty })), {
362
+ changed: true,
363
+ preserve: false,
364
+ });
365
+ assert.deepEqual(tracker.consume('manual', context({ contract: empty })), {
366
+ changed: false,
367
+ preserve: false,
368
+ });
369
+ assert.deepEqual(
370
+ tracker.consume('manual', context({ contract: empty, configuration: 'other' })),
371
+ { changed: true, preserve: false },
372
+ );
373
+ });
374
+
375
+ function studentBlocks(title: string, permission = 'update') {
376
+ return [
377
+ {
378
+ $$typeof: 'zova-jsx:block',
379
+ render: 'basic-pageentry:blockPageEntry',
380
+ options: {
381
+ blocks: [
382
+ {
383
+ $$typeof: 'zova-jsx:block',
384
+ render: 'basic-pageentry:blockForm',
385
+ options: {
386
+ blocks: [
387
+ {
388
+ $$typeof: 'zova-jsx:block',
389
+ render: 'basic-form:blockFormLayout',
390
+ options: {
391
+ formLayout: {
392
+ children: [
393
+ {
394
+ type: 'tabs',
395
+ children: [
396
+ {
397
+ type: 'tab',
398
+ title,
399
+ children: [
400
+ {
401
+ type: 'group',
402
+ title,
403
+ children: [
404
+ {
405
+ type: 'section',
406
+ columns: { default: 1, md: 2 },
407
+ children: [{ type: 'field', name: 'name' }],
408
+ },
409
+ ],
410
+ },
411
+ ],
412
+ },
413
+ ],
414
+ },
415
+ ],
416
+ },
417
+ },
418
+ },
419
+ ],
420
+ },
421
+ },
422
+ {
423
+ $$typeof: 'zova-jsx:block',
424
+ render: 'basic-pageentry:blockToolbarRow',
425
+ options: {
426
+ actions: [
427
+ {
428
+ $$typeof: 'zova-jsx:actionRow',
429
+ name: 'submit',
430
+ render: 'basic-form:actionSubmit',
431
+ options: {
432
+ permission: { actionInherit: permission, formScene: ['create', 'edit'] },
433
+ },
434
+ },
435
+ ],
436
+ },
437
+ },
438
+ ],
439
+ },
440
+ },
441
+ ];
442
+ }
443
+
444
+ test('actual Student static blocks and table columnProps allow independent name across localized layout titles', () => {
445
+ const student = (title: string, permission = 'update') =>
446
+ contract(
447
+ { title },
448
+ {
449
+ rest: {
450
+ order: 5,
451
+ form: {
452
+ render: 'training-student:formFieldLevel',
453
+ options: { items: [{ value: 1, title }], placeholder: title },
454
+ },
455
+ table: {
456
+ render: 'training-student:level',
457
+ columnProps: { items: [{ value: 1, title }] },
458
+ },
459
+ },
460
+ },
461
+ { rest: { blocks: studentBlocks(title, permission) } },
462
+ );
463
+ const before = student('Name');
464
+ const after = student('姓名');
465
+ assert.equal(before.fields.name.eligible, true);
466
+ assert.equal(before.behavior, after.behavior);
467
+ assert.notEqual(before.behaviorRaw, after.behaviorRaw);
468
+ assert.notEqual(before.fields.level.compatible, after.fields.level.compatible);
469
+ const tracker = new SchemaRefreshTracker();
470
+ tracker.consume('name', context({ contract: before }));
471
+ assert.deepEqual(tracker.consume('name', context({ locale: 'zh-cn', contract: after })), {
472
+ changed: true,
473
+ preserve: true,
474
+ });
475
+ assert.notEqual(after.behavior, student('姓名', 'create').behavior);
476
+ const unknown = studentBlocks('Name');
477
+ (unknown[0].options as any).unknown = { title: 'Name' };
478
+ assert.equal(contract({}, {}, { rest: { blocks: unknown } }).fields.name.eligible, false);
479
+ const effect = studentBlocks('Name');
480
+ (effect[0].options as any).onEffect = 'callback';
481
+ assert.equal(contract({}, {}, { rest: { blocks: effect } }).fields.name.eligible, false);
482
+ const expression = studentBlocks('cel://value.name');
483
+ assert.equal(contract({}, {}, { rest: { blocks: expression } }).fields.name.eligible, false);
484
+ });
485
+
486
+ test('layout exception never normalizes descriptions, actions, field names, or arbitrary titles', () => {
487
+ const signature = (change: (blocks: any[]) => void) => {
488
+ const blocks = studentBlocks('Name');
489
+ change(blocks);
490
+ return contract({}, {}, { rest: { blocks } });
491
+ };
492
+ const base = signature(() => {});
493
+ const description = signature(blocks => {
494
+ blocks[0].options.blocks[0].options.blocks[0].options.formLayout.children[0].children[0].children[0].description =
495
+ 'Translated';
496
+ });
497
+ assert.notEqual(base.behavior, description.behavior);
498
+ const changedName = signature(blocks => {
499
+ blocks[0].options.blocks[0].options.blocks[0].options.formLayout.children[0].children[0].children[0].children[0].children[0].name =
500
+ 'level';
501
+ });
502
+ assert.notEqual(base.behavior, changedName.behavior);
503
+ const arbitraryTitle = signature(blocks => {
504
+ blocks[0].options.title = 'Unknown';
505
+ });
506
+ assert.equal(arbitraryTitle.fields.name.eligible, false);
507
+ });
508
+
509
+ test('prune removes vanished schema fields only after readiness, retaining manual fields', () => {
510
+ const tracker = new SchemaRefreshTracker();
511
+ tracker.consume('name', context());
512
+ tracker.consume('manual', context());
513
+ tracker.prune({ ready: false, fields: {} });
514
+ assert.equal(tracker.has('name'), true);
515
+ tracker.prune({ ready: true, fields: {} });
516
+ assert.equal(tracker.has('name'), false);
517
+ assert.equal(tracker.has('manual'), true);
518
+ });
519
+
520
+ test('full Student static image options and nested details effects preserve independent name only', () => {
521
+ const effect = () => {};
522
+ const build = (
523
+ title: string,
524
+ details = true,
525
+ direct = false,
526
+ auxiliary: Record<string, unknown> = {},
527
+ ) => {
528
+ const fields = {
529
+ _trainingRecords: {
530
+ type: ['array', 'null'],
531
+ rest: { visible: false },
532
+ items: { $ref: '#/components/schemas/DetailRecord' },
533
+ ...auxiliary,
534
+ },
535
+ name: {
536
+ type: 'string',
537
+ title,
538
+ rest: {
539
+ order: 1,
540
+ table: {
541
+ render: 'basic-table:actionView',
542
+ align: 'left',
543
+ width: 240,
544
+ fixed: 'left',
545
+ enableSorting: true,
546
+ sortDescFirst: true,
547
+ },
548
+ },
549
+ },
550
+ level: {
551
+ type: 'number',
552
+ title,
553
+ rest: {
554
+ form: {
555
+ render: 'training-student:formFieldLevel',
556
+ options: { items: [{ value: 1, title }], placeholder: title },
557
+ },
558
+ table: {
559
+ render: 'training-student:level',
560
+ columnProps: { items: [{ value: 1, title }] },
561
+ },
562
+ },
563
+ },
564
+ imageId: {
565
+ type: 'number',
566
+ rest: {
567
+ form: {
568
+ render: 'basic-image:formFieldImage',
569
+ options: {
570
+ imageScene: 'training-student:studentImage',
571
+ enableCrop: true,
572
+ cropAspectRatio: 1,
573
+ relationName: 'image',
574
+ resize: { width: 512, height: 512, fit: 'cover', format: 'jpeg', quality: 90 },
575
+ },
576
+ },
577
+ table: { render: 'basic-image:image', columnProps: { relationName: 'image' } },
578
+ },
579
+ },
580
+ trainingRecords: {
581
+ type: 'array',
582
+ rest: {
583
+ form: { render: details ? 'basic-details:formFieldDetails' : 'other:formFieldObject' },
584
+ ...(direct ? { onEffect: effect } : {}),
585
+ },
586
+ items: {
587
+ type: 'object',
588
+ properties: {
589
+ trainingRecordSubjects: {
590
+ type: 'array',
591
+ rest: { form: { render: 'basic-details:formFieldDetails' }, onEffect: effect },
592
+ items: { type: 'string' },
593
+ },
594
+ },
595
+ },
596
+ },
597
+ };
598
+ return createSchemaRefreshContract(
599
+ { type: 'object', properties: fields, rest: { blocks: studentBlocks(title) } },
600
+ Object.entries(fields).map(([key, property]) => ({ key, ...property })),
601
+ () => ({
602
+ type: 'object',
603
+ rest: { blocks: [{ render: 'basic-details:blockForm', options: { dialogOptions: {} } }] },
604
+ properties: {
605
+ nested: { type: 'string', rest: { onEffect: effect } },
606
+ },
607
+ }),
608
+ );
609
+ };
610
+ const before = build('Name');
611
+ const after = build('姓名');
612
+ assert.equal(before.fields.name.eligible, true);
613
+ assert.equal(before.fields.imageId.eligible, true);
614
+ assert.equal(before.fields.trainingRecords.eligible, false);
615
+ assert.equal(before.fields._trainingRecords.eligible, false);
616
+ assert.equal(build('Name', true, false, { type: 'array' }).fields.name.eligible, true);
617
+ assert.equal(build('Name', true, false, { type: ['null', 'array'] }).fields.name.eligible, true);
618
+ for (const type of [['array', 'string'], ['array', 'null', 'string'], ['array'], 'object']) {
619
+ assert.equal(build('Name', true, false, { type }).fields.name.eligible, false);
620
+ }
621
+ assert.notEqual(before.validation, build('Name', true, false, { type: 'array' }).validation);
622
+ assert.equal(build('Name', true, false, { rest: { visible: true } }).fields.name.eligible, false);
623
+ assert.equal(build('Name', true, false, { key: '_unrelated' }).fields.name.eligible, false);
624
+ assert.equal(
625
+ build('Name', true, false, { items: { type: 'object', rest: { onEffect: effect } } }).fields
626
+ .name.eligible,
627
+ false,
628
+ );
629
+ const tracker = new SchemaRefreshTracker();
630
+ tracker.consume('name', context({ contract: before }));
631
+ assert.deepEqual(tracker.consume('name', context({ locale: 'zh-cn', contract: after })), {
632
+ changed: true,
633
+ preserve: true,
634
+ });
635
+ assert.equal(build('Name', false).fields.name.eligible, false);
636
+ assert.equal(build('Name', true, true).fields.name.eligible, false);
637
+ });