Repository navigation
Expand file tree
/
Copy pathruntime-gate.package-closure.test.ts
More file actions
436 lines (386 loc) · 20.8 KB
/
Copy pathruntime-gate.package-closure.test.ts
File metadata and controls
436 lines (386 loc) · 20.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
/**
* #9612 — the publish gate judges a write against its PACKAGE's closure.
*
* The maintainer's ruling is the product decision under test, verbatim:
*
* > 大客户(420 个对象),就不应该出现在一个软件包中啊,这就是划分软件包的价值。
* > 客户开发开发,校验是否也应该基于软件包
* > 当然这里面要考虑系统对象
*
* So this file is NOT a performance test. It pins the four limbs of the
* closure, the fallback direction when a package cannot be resolved, and — the
* part that matters — that narrowing does not change the gate's ANSWER.
*
* ## Why the equivalence assertions are paired with ablations
*
* "The verdict is unchanged" is worth nothing from a fixture where nothing
* could have changed it. Measured on the shipped rules: the OBJECT door has no
* objects×objects coupling at all — none of its seven rules builds a
* name→object index — so on that door a verdict-equivalence assertion passes
* for a reason that has nothing to do with the closure being right. The FLOW
* door does have the coupling, and that is where the ablations below live: drop
* the written item's own package and a phantom reference finding appears; drop
* system objects and real findings disappear. Both directions are asserted, so
* an equivalence test that stopped being able to fail would be caught here
* rather than read as a pass.
*/
import { describe, expect, it } from 'vitest';
import {
narrowObjectsToPackageClosure,
runRuntimeAuthoringRules,
type RuntimePackageScope,
} from './runtime-gate.js';
import { validateListViewFieldRefs } from './validate-list-view-field-refs.js';
type AnyRec = Record<string, any>;
const OWN = 'com.acme.crm';
const DEP = 'com.acme.core';
const STRANGER = 'com.other.analytics';
const PLATFORM = 'com.objectstack.platform';
const SCOPE: RuntimePackageScope = { packageId: OWN, dependencies: [DEP] };
/** A minimal, spec-shaped object declaration. */
function obj(name: string, packageId?: string | null, extra: AnyRec = {}): AnyRec {
return {
name,
label: name,
sharingModel: 'private',
fields: { name: { type: 'text', label: 'Name' } },
...(packageId === undefined ? {} : { _packageId: packageId }),
...extra,
};
}
describe('narrowObjectsToPackageClosure — the four limbs (#9612)', () => {
const collection = [
obj('crm_account', OWN),
obj('core_user', DEP),
obj('analytics_cube', STRANGER),
obj('sys_audit_log', PLATFORM),
obj('tenant_overlay', undefined),
obj('rehydrated_overlay', 'sys_metadata'),
obj('flagged_system', STRANGER, { isSystem: true }),
];
const kept = () =>
(narrowObjectsToPackageClosure(collection, SCOPE) as AnyRec[]).map((o) => o.name);
it('keeps the written package and its declared dependency', () => {
expect(kept()).toContain('crm_account');
expect(kept()).toContain('core_user');
});
it('drops a package that is neither the written one nor a declared dependency', () => {
expect(kept()).not.toContain('analytics_cube');
});
it('keeps system objects unconditionally — by name prefix AND by isSystem flag', () => {
// The card's named hazard: a package references a platform object it never
// declares a dependency on. Judged against a closure that omitted it, the
// gate would report an unresolved reference that is not there.
expect(kept()).toContain('sys_audit_log');
expect(kept()).toContain('flagged_system');
});
it('keeps unpackaged rows, including the `sys_metadata` rehydration sentinel', () => {
// Nothing declares what a tenant-authored overlay row may reference, so
// nothing bounds it. Keeping it is the conservative direction.
expect(kept()).toContain('tenant_overlay');
expect(kept()).toContain('rehydrated_overlay');
});
it('narrows NOTHING when no scope is stated', () => {
expect(narrowObjectsToPackageClosure(collection, undefined)).toBe(collection);
});
it('narrows NOTHING when the scope carries an empty package id', () => {
// The host returns `undefined` rather than an empty id, but the guard is
// here too: an unresolvable package must buy MORE validation input, never
// less. ⛔ There is no branch anywhere that skips rules or skips them past
// a size — that is the fail-open at scale this card was forbidden to build.
const empty = { packageId: '', dependencies: [] } as RuntimePackageScope;
expect(narrowObjectsToPackageClosure(collection, empty)).toBe(collection);
});
it('keeps a non-object member rather than inspecting it', () => {
const ragged = [null, 'not-an-object', obj('analytics_cube', STRANGER)];
expect(narrowObjectsToPackageClosure(ragged, SCOPE)).toEqual([null, 'not-an-object']);
});
it('follows the dependency set the host resolved, not a graph of its own', () => {
// The transitive walk lives in the host (it holds the package registry).
// This function reads the set it is given and must not try to extend it.
const transitive: RuntimePackageScope = { packageId: OWN, dependencies: [DEP, STRANGER] };
const names = (narrowObjectsToPackageClosure(collection, transitive) as AnyRec[]).map((o) => o.name);
expect(names).toContain('analytics_cube');
});
});
// ─────────────────────────────────────────────────────────────────────────────
// The gate's ANSWER under narrowing — flow door, where objects×objects coupling
// actually exists.
// ─────────────────────────────────────────────────────────────────────────────
/**
* A flow that fires on a record trigger against `objectName`.
*
* The shape is the shipped one (`nodes[0].type: 'start'`, the object named in
* `config.objectName`) — that is what `flow-trigger-unknown-object` reads, and
* a fixture in any other shape produces no findings at all and would make every
* assertion in this file pass vacuously.
*/
function flowOn(name: string, objectName: string): AnyRec {
return {
name,
label: name,
status: 'active',
type: 'autolaunched',
nodes: [
{
id: 'start',
type: 'start',
label: 'On update',
config: { objectName, triggerType: 'record-after-update' },
},
{ id: 'end', type: 'end', label: 'End' },
],
edges: [{ id: 'e1', source: 'start', target: 'end', type: 'default', isDefault: false }],
};
}
/** Targets an object in the written package — resolves under the closure. */
const FLOW_INTO_OWN_PACKAGE = flowOn('crm_account_touched', 'crm_account');
/**
* Targets a PLATFORM object no package declares — the card's named hazard.
*
* `platform_ledger` rather than `sys_audit_log` on purpose:
* `validateFlowTriggerReadiness` exempts any target whose NAME starts with
* `sys_` (its own header calls such targets legitimate), so a `sys_`-named
* fixture would make the ablation below unable to fail for a reason that has
* nothing to do with the closure. `platform_ledger` carries `isSystem: true`
* instead — the OTHER limb of the same predicate — so the rule still judges it
* while the closure still keeps it.
*/
const FLOW_INTO_SYSTEM_OBJECT = flowOn('platform_ledger_touched', 'platform_ledger');
/** Targets an object that exists in NO package — a genuine finding, always. */
const FLOW_INTO_GHOST = flowOn('ghost_touched', 'ghost_object');
/** Targets a package the written one does NOT declare a dependency on. */
const FLOW_INTO_UNDECLARED = flowOn('stranger_touched', 'other_0_0');
/** A tenant of many packages — the shape the ruling says a big tenant really is. */
function packagedTenant(strangerPackages: number): AnyRec[] {
const out: AnyRec[] = [
obj('crm_account', OWN),
obj('crm_contact', OWN),
obj('core_user', DEP),
obj('sys_audit_log', PLATFORM),
obj('platform_ledger', PLATFORM, { isSystem: true }),
];
for (let i = 0; i < strangerPackages; i++) {
for (let k = 0; k < 5; k++) out.push(obj(`other_${i}_${k}`, `com.other.pkg_${i}`));
}
return out;
}
/** Every finding the write ADDED, as stable text. Array indices normalised. */
function verdict(result: { errors: AnyRec[]; advisories: AnyRec[] }): string[] {
return [...result.errors, ...result.advisories]
.map((f) => `${f.rule}|${f.where}|${String(f.path ?? '').replace(/\[\d+\]/g, '[i]')}|${f.message}`)
.sort();
}
describe('the differential verdict survives package narrowing (#9612)', () => {
const objects = packagedTenant(20);
const scoped = (type: string, item: AnyRec) =>
verdict(runRuntimeAuthoringRules({ type, item, context: { objects }, packageScope: SCOPE }));
const whole = (type: string, item: AnyRec) =>
verdict(runRuntimeAuthoringRules({ type, item, context: { objects } }));
it('a REAL finding survives narrowing — the equivalence is not vacuous', () => {
// Asserted first and by content: every other case in this block compares
// two empty verdicts, which would also be equal if the gate had stopped
// working. This one pins that the narrowed gate still FINDS things.
expect(whole('flow', FLOW_INTO_GHOST)).toHaveLength(1);
expect(scoped('flow', FLOW_INTO_GHOST)).toEqual(whole('flow', FLOW_INTO_GHOST));
});
it('reaches the whole-stack answer exactly for a flow into its own package', () => {
expect(scoped('flow', FLOW_INTO_OWN_PACKAGE)).toEqual(whole('flow', FLOW_INTO_OWN_PACKAGE));
expect(whole('flow', FLOW_INTO_OWN_PACKAGE)).toEqual([]);
});
it('reaches the whole-stack answer exactly for a flow into a PLATFORM object', () => {
// The closure keeps `sys_audit_log` although no package declares it. The
// ablation below is what proves this assertion could have failed.
expect(scoped('flow', FLOW_INTO_SYSTEM_OBJECT)).toEqual(whole('flow', FLOW_INTO_SYSTEM_OBJECT));
expect(whole('flow', FLOW_INTO_SYSTEM_OBJECT)).toEqual([]);
});
it('reaches the whole-stack answer exactly, object door', () => {
const written = obj('crm_opportunity', OWN, {
validations: [
{ name: 'shape', type: 'json_schema', schema: { type: 'not-a-type' }, message: 'bad' },
],
});
// Non-vacuous in the same way: the broken schema is a finding the narrowed
// gate must still produce.
expect(whole('object', written).length).toBeGreaterThan(0);
expect(scoped('object', written)).toEqual(whole('object', written));
});
it('narrows nothing — byte-identical to the pre-#9612 gate — with no scope', () => {
const written = obj('crm_opportunity', OWN);
const before = runRuntimeAuthoringRules({ type: 'object', item: written, context: { objects } });
const after = runRuntimeAuthoringRules({
type: 'object',
item: written,
context: { objects },
packageScope: undefined,
});
expect(verdict(after)).toEqual(verdict(before));
});
it('DOES judge a reference outside the declared dependencies — the ruling\'s intended consequence', () => {
// ⭐ The one place the answer legitimately differs, pinned so it is a
// decision on the record rather than a surprise. A package's declared
// `dependencies` bound what it MAY reference; a flow reaching into a
// package this one never declared is not resolvable BY DECLARATION, and
// the closure says so. Measured severity here is advisory (`warning`), so
// it reports rather than refuses.
expect(whole('flow', FLOW_INTO_UNDECLARED)).toEqual([]);
const narrowed = scoped('flow', FLOW_INTO_UNDECLARED);
expect(narrowed).toHaveLength(1);
expect(narrowed[0]).toContain('flow-trigger-unknown-object');
});
});
describe('ablations — the equivalence assertions above can fail (#9612)', () => {
const objects = packagedTenant(20);
it('DROPPING the written package manufactures a phantom the closure prevents', () => {
// The #7886 mechanism reproduced on `objects` rather than `permissions`.
// ⭐ #7886's phantoms came from narrowing `permissions`; the PM's original
// premise that narrowing PER SE manufactures them was falsified on #9612.
// What manufactures them is a closure missing a limb — this one.
const broken = objects.filter((o) => o._packageId !== OWN);
const mutilated = verdict(
runRuntimeAuthoringRules({ type: 'flow', item: FLOW_INTO_OWN_PACKAGE, context: { objects: broken } }),
);
expect(mutilated).toHaveLength(1);
expect(mutilated[0]).toContain('flow-trigger-unknown-object');
});
it('DROPPING system objects changes the answer — which is why that limb is unconditional', () => {
const broken = objects.filter((o) => o.isSystem !== true && !String(o.name).startsWith('sys_'));
const mutilated = verdict(
runRuntimeAuthoringRules({ type: 'flow', item: FLOW_INTO_SYSTEM_OBJECT, context: { objects: broken } }),
);
expect(mutilated).toHaveLength(1);
expect(mutilated[0]).toContain('flow-trigger-unknown-object');
});
});
// ─────────────────────────────────────────────────────────────────────────────
// #24563 — the VIEW door under a package closure: a list view bound outside the
// closure is reported, never skipped; one bound inside keeps its field verdict.
// ─────────────────────────────────────────────────────────────────────────────
/**
* A flattened list overlay — the shape `PUT /api/v1/meta/view` carries and the
* gate snapshots as `views: [item]` — bound to `object` with one known
* (`name`, every fixture object declares it) and optionally one unknown column.
*/
function listViewOn(object: string, columns: readonly string[] = ['name', 'zzz_nope']): AnyRec {
return {
name: `${object}.probe_list`,
object,
viewKind: 'list',
label: 'Probe',
type: 'grid',
data: { provider: 'object', object },
columns: columns.map((field) => ({ field })),
};
}
describe('the view door under a package closure (#24563)', () => {
const OUTSIDE = 'list-view-object-outside-closure';
const FIELD_UNKNOWN = 'list-view-field-unknown';
const objects = [...packagedTenant(3), obj('tenant_overlay', undefined)];
const scoped = (item: AnyRec, scope: RuntimePackageScope = SCOPE) =>
runRuntimeAuthoringRules({ type: 'view', item, context: { objects }, packageScope: scope });
const whole = (item: AnyRec) => runRuntimeAuthoringRules({ type: 'view', item, context: { objects } });
const rules = (fs: AnyRec[]) => fs.map((f) => f.rule);
it('REFUSES a binding into a package the written one never declared — at the binding, with the remedy', () => {
const result = scoped(listViewOn('other_0_0'));
// An `error`, so the door refuses the write: the dataset door's verdict
// for an out-of-closure base object under the same closure. The flow
// door's `flow-trigger-unknown-object` (pinned above) stays advisory.
expect(result.advisories).toEqual([]);
expect(result.errors).toHaveLength(1);
expect(result.errors[0]).toMatchObject({ severity: 'error', rule: OUTSIDE });
expect(result.errors[0].path).toMatch(/^views(\[0\]|\.other_0_0\.probe_list)\.data\.object$/);
// The fix names the remedy and the package it applies to.
expect(result.errors[0].hint).toContain(OWN);
expect(result.errors[0].hint).toContain('dependencies');
// ⛔ The field positions stay unjudged: judging them against the whole
// collection would judge the package against what ruling #9612 says it may
// not reference.
expect(rules(result.errors)).not.toContain(FIELD_UNKNOWN);
});
it('REFUSES a binding to an object that exists nowhere the same way', () => {
const result = scoped(listViewOn('nosuch_obj'));
expect(result.advisories).toEqual([]);
expect(rules(result.errors)).toEqual([OUTSIDE]);
});
it('REFUSES it under a closure that carries no object at all — an empty graph is still a verdict', () => {
const result = runRuntimeAuthoringRules({
type: 'view',
item: listViewOn('nosuch_obj'),
context: { objects: [] },
packageScope: SCOPE,
});
expect(rules(result.errors)).toEqual([OUTSIDE]);
});
it('REFUSES an unknown column on an object of the written package — the field verdict is unchanged', () => {
const result = scoped(listViewOn('crm_account'));
expect(rules(result.errors)).toEqual([FIELD_UNKNOWN]);
expect(result.errors[0].path).toMatch(/columns\[1\]\.field$/);
expect(result.advisories).toEqual([]);
});
it('admits a known column on an object of the written package', () => {
const result = scoped(listViewOn('crm_account', ['name']));
expect(result.errors).toEqual([]);
expect(result.advisories).toEqual([]);
});
it('keeps a DECLARED DEPENDENCY inside the closure — known column admitted, unknown refused', () => {
// The discriminating case: a refusal of the binding here would mean the
// dependency limb never reached the rule.
expect(scoped(listViewOn('core_user', ['name']))).toMatchObject({ errors: [], advisories: [] });
expect(rules(scoped(listViewOn('core_user')).errors)).toEqual([FIELD_UNKNOWN]);
});
it('keeps system objects inside the closure — by name prefix and by isSystem flag', () => {
for (const name of ['sys_audit_log', 'platform_ledger']) {
expect(scoped(listViewOn(name, ['name'])), name).toMatchObject({ errors: [], advisories: [] });
expect(rules(scoped(listViewOn(name)).errors), name).toEqual([FIELD_UNKNOWN]);
}
});
it('keeps a provenance-less overlay object inside the closure — its field verdict is unchanged', () => {
expect(rules(scoped(listViewOn('tenant_overlay')).errors)).toEqual([FIELD_UNKNOWN]);
expect(scoped(listViewOn('tenant_overlay')).advisories).toEqual([]);
});
it('a write that names no package is judged against the whole collection — field verdict unchanged, no closure verdict', () => {
// ⛔ The overlay door: the stranger's object is in the whole collection, so
// its unknown column is refused exactly as before. An object that exists
// nowhere gets no closure verdict (no scope); [#24668] it is refused as
// existing nowhere instead (`runtime-gate.whole-collection.test.ts`).
expect(rules(whole(listViewOn('other_0_0')).errors)).toEqual([FIELD_UNKNOWN]);
expect(whole(listViewOn('other_0_0')).advisories).toEqual([]);
expect(rules(whole(listViewOn('nosuch_obj')).errors)).toEqual(['list-view-object-unknown']);
// An empty package id narrows nothing, so it states no closure either.
const empty = { packageId: '', dependencies: [] } as RuntimePackageScope;
expect(rules(scoped(listViewOn('nosuch_obj'), empty).errors)).toEqual(['list-view-object-unknown']);
});
it('the flow verdict is unchanged, and a stored object\'s out-of-closure list view is not charged to it', () => {
// A context object of the written package whose built-in list view
// retargets an object outside the closure: the rule now refuses that list
// view, in the baseline and the candidate alike, so it cancels — a flow
// publish is not refused for a stored sibling's binding (#4463 D4).
const retargeting = obj('crm_board', OWN, {
listViews: { foreign: { label: 'Foreign', data: { provider: 'object', object: 'other_0_0' }, columns: ['name'] } },
});
const withSibling = [...objects, retargeting];
const flow = (item: AnyRec) =>
verdict(runRuntimeAuthoringRules({ type: 'flow', item, context: { objects: withSibling }, packageScope: SCOPE }));
// Non-vacuous: written itself, that object IS refused — the list view
// rides the object door too, at the same verdict.
const asWritten = runRuntimeAuthoringRules({ type: 'object', item: retargeting, context: { objects }, packageScope: SCOPE });
expect(rules(asWritten.errors)).toContain(OUTSIDE);
expect(rules(asWritten.advisories)).not.toContain(OUTSIDE);
expect(flow(FLOW_INTO_OWN_PACKAGE)).toEqual([]);
const undeclared = flow(FLOW_INTO_UNDECLARED);
expect(undeclared).toHaveLength(1);
expect(undeclared[0]).toContain('flow-trigger-unknown-object');
});
it('the CLI reading is unchanged: no scope stated, skip 1 judges nothing', () => {
// `os validate` / `os build` / `os lint` hand the rule no closure.
expect(validateListViewFieldRefs({ objects, views: [listViewOn('nosuch_obj')] })).toEqual([]);
// And the same stack under a stated closure is the verdict above.
expect(
validateListViewFieldRefs(
{ objects, views: [listViewOn('nosuch_obj')] },
{ packageScope: SCOPE },
).map((f) => f.rule),
).toEqual([OUTSIDE]);
});
});