@milaboratories/pl-middle-layer 1.59.15 → 1.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/debug/index.cjs +19 -0
- package/dist/debug/index.cjs.map +1 -1
- package/dist/debug/index.js +19 -0
- package/dist/debug/index.js.map +1 -1
- package/dist/middle_layer/middle_layer.cjs +1 -0
- package/dist/middle_layer/middle_layer.cjs.map +1 -1
- package/dist/middle_layer/middle_layer.js +1 -0
- package/dist/middle_layer/middle_layer.js.map +1 -1
- package/dist/middle_layer/ops.cjs +1 -1
- package/dist/middle_layer/ops.cjs.map +1 -1
- package/dist/middle_layer/ops.js +1 -1
- package/dist/middle_layer/ops.js.map +1 -1
- package/dist/middle_layer/project.cjs +29 -1
- package/dist/middle_layer/project.cjs.map +1 -1
- package/dist/middle_layer/project.d.ts +1 -0
- package/dist/middle_layer/project.d.ts.map +1 -1
- package/dist/middle_layer/project.js +30 -2
- package/dist/middle_layer/project.js.map +1 -1
- package/dist/middle_layer/project_list.cjs +3 -1
- package/dist/middle_layer/project_list.cjs.map +1 -1
- package/dist/middle_layer/project_list.js +4 -2
- package/dist/middle_layer/project_list.js.map +1 -1
- package/dist/middle_layer/types.d.ts +1 -1
- package/package.json +13 -13
- package/src/debug/index.ts +26 -0
- package/src/middle_layer/middle_layer.ts +6 -0
- package/src/middle_layer/ops.ts +1 -1
- package/src/middle_layer/project.ts +178 -2
- package/src/middle_layer/project_list.ts +5 -2
- package/src/middle_layer/project_pruning.test.ts +636 -0
- package/src/middle_layer/types.ts +1 -1
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parity tests verifying that the new ResourceTree field-filter + traversal-stop-rule
|
|
3
|
+
* predicates produce the same outcome as the legacy BFS pruning functions.
|
|
4
|
+
*
|
|
5
|
+
* §4.1 — pruning parity: projectTreePruning ⇄ projectTreeFieldFilter
|
|
6
|
+
* and ProjectsListTreePruningFunction ⇄ projectsListFieldFilter
|
|
7
|
+
* §4.2 — stop-rule isolation coverage for projectTreeTraverseStopRules
|
|
8
|
+
* §4.3 — final-predicate parity: DefaultFinalResourceDataPredicate ⇄ projectTreeTraverseStopRules
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, it } from "vitest";
|
|
12
|
+
import {
|
|
13
|
+
projectTreeFieldFilter,
|
|
14
|
+
projectTreePruning,
|
|
15
|
+
projectTreeTraverseStopRules,
|
|
16
|
+
} from "./project";
|
|
17
|
+
import { projectsListFieldFilter, ProjectsListTreePruningFunction } from "./project_list";
|
|
18
|
+
import type { Filter } from "@milaboratories/pl-client";
|
|
19
|
+
import {
|
|
20
|
+
DefaultFinalResourceDataPredicate,
|
|
21
|
+
FilterOperatorType,
|
|
22
|
+
FilterProperty,
|
|
23
|
+
NullSignedResourceId,
|
|
24
|
+
} from "@milaboratories/pl-client";
|
|
25
|
+
import type { ExtendedResourceData } from "@milaboratories/pl-tree";
|
|
26
|
+
import type { MiLogger } from "@milaboratories/ts-helpers";
|
|
27
|
+
|
|
28
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/** Minimal no-op logger satisfying MiLogger interface. */
|
|
31
|
+
const noopLogger: MiLogger = {
|
|
32
|
+
info: () => {},
|
|
33
|
+
warn: () => {},
|
|
34
|
+
error: () => {},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Build a minimal ExtendedResourceData for tests. */
|
|
38
|
+
function makeResource(typeName: string, fieldNames: string[]): ExtendedResourceData {
|
|
39
|
+
return {
|
|
40
|
+
id: "NG:0x1" as any,
|
|
41
|
+
type: { name: typeName, version: "1" },
|
|
42
|
+
kind: "Structural",
|
|
43
|
+
data: undefined,
|
|
44
|
+
resourceReady: false,
|
|
45
|
+
error: NullSignedResourceId,
|
|
46
|
+
originalResourceId: NullSignedResourceId,
|
|
47
|
+
final: false,
|
|
48
|
+
inputsLocked: false,
|
|
49
|
+
outputsLocked: false,
|
|
50
|
+
fields: fieldNames.map((name) => ({
|
|
51
|
+
name,
|
|
52
|
+
type: "Dynamic",
|
|
53
|
+
value: NullSignedResourceId,
|
|
54
|
+
error: NullSignedResourceId,
|
|
55
|
+
status: "Resolved",
|
|
56
|
+
valueIsFinal: false,
|
|
57
|
+
})),
|
|
58
|
+
kv: [],
|
|
59
|
+
} as unknown as ExtendedResourceData;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Build a resource where resourceReady=true (satisfies readyOrDuplicateOrError). */
|
|
63
|
+
function makeReadyResource(typeName: string, version = "1"): ExtendedResourceData {
|
|
64
|
+
return { ...makeResource(typeName, []), type: { name: typeName, version }, resourceReady: true };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Build a resource that satisfies readyAndHasAllOutputsFilled:
|
|
69
|
+
* resourceReady=true, outputsLocked=true, one field with valueIsFinal=true.
|
|
70
|
+
*/
|
|
71
|
+
function makeAllOutputsFinalResource(typeName: string): ExtendedResourceData {
|
|
72
|
+
const base = makeResource(typeName, ["output"]);
|
|
73
|
+
return {
|
|
74
|
+
...base,
|
|
75
|
+
resourceReady: true,
|
|
76
|
+
outputsLocked: true,
|
|
77
|
+
fields: [
|
|
78
|
+
{
|
|
79
|
+
name: "output",
|
|
80
|
+
type: "Dynamic",
|
|
81
|
+
value: "NG:0x99" as any,
|
|
82
|
+
error: NullSignedResourceId,
|
|
83
|
+
status: "Resolved",
|
|
84
|
+
valueIsFinal: true,
|
|
85
|
+
} as any,
|
|
86
|
+
],
|
|
87
|
+
} as unknown as ExtendedResourceData;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Evaluate a single Filter against a resource + optional field-name context. */
|
|
91
|
+
function evalFilter(
|
|
92
|
+
filter: Filter,
|
|
93
|
+
ctx: { resourceType: string; fieldName?: string; isFinal?: boolean; allOutputsFinal?: boolean },
|
|
94
|
+
): boolean {
|
|
95
|
+
const { resourceType, fieldName = "", isFinal = false, allOutputsFinal = false } = ctx;
|
|
96
|
+
|
|
97
|
+
if (filter.value.oneofKind === "filtersValue") {
|
|
98
|
+
const children = filter.value.filtersValue.filters;
|
|
99
|
+
switch (filter.operator) {
|
|
100
|
+
case FilterOperatorType.AND:
|
|
101
|
+
return children.every((c) => evalFilter(c, ctx));
|
|
102
|
+
case FilterOperatorType.OR:
|
|
103
|
+
return children.some((c) => evalFilter(c, ctx));
|
|
104
|
+
case FilterOperatorType.NOT:
|
|
105
|
+
return !evalFilter(children[0]!, ctx);
|
|
106
|
+
default:
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (filter.value.oneofKind === "stringValue") {
|
|
112
|
+
const pattern = filter.value.stringValue;
|
|
113
|
+
let subject: string;
|
|
114
|
+
switch (filter.key) {
|
|
115
|
+
case FilterProperty.RESOURCE_TYPE:
|
|
116
|
+
subject = resourceType;
|
|
117
|
+
break;
|
|
118
|
+
case FilterProperty.FIELD_NAME:
|
|
119
|
+
subject = fieldName;
|
|
120
|
+
break;
|
|
121
|
+
default:
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
switch (filter.operator) {
|
|
125
|
+
case FilterOperatorType.EQUAL:
|
|
126
|
+
return subject === pattern;
|
|
127
|
+
case FilterOperatorType.MATCH:
|
|
128
|
+
return new RegExp(pattern).test(subject);
|
|
129
|
+
default:
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (filter.value.oneofKind === "boolValue") {
|
|
135
|
+
const want = filter.value.boolValue;
|
|
136
|
+
switch (filter.key) {
|
|
137
|
+
case FilterProperty.IS_FINAL:
|
|
138
|
+
return isFinal === want;
|
|
139
|
+
case FilterProperty.ALL_OUTPUTS_FINAL:
|
|
140
|
+
return allOutputsFinal === want;
|
|
141
|
+
// readyOrDuplicateOrError() expands to OR(resourceReady, isDuplicate, hasErrors);
|
|
142
|
+
// model the "ready" branch as isFinal and the other two as always-false so
|
|
143
|
+
// the OR collapses to the test's isFinal flag.
|
|
144
|
+
case FilterProperty.RESOURCE_READY_FOR_CALCULATION:
|
|
145
|
+
return isFinal === want;
|
|
146
|
+
case FilterProperty.IS_DUPLICATE:
|
|
147
|
+
return want === false;
|
|
148
|
+
case FilterProperty.HAS_ERRORS:
|
|
149
|
+
return want === false;
|
|
150
|
+
case FilterProperty.OUTPUTS_LOCKED:
|
|
151
|
+
return allOutputsFinal === want;
|
|
152
|
+
default:
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Apply fieldFilter to a resource's fields.
|
|
162
|
+
* A field is kept when the filter evaluates true for that (resource, field) pair.
|
|
163
|
+
*/
|
|
164
|
+
function evaluateFieldFilter(
|
|
165
|
+
filter: Filter,
|
|
166
|
+
resource: { type: { name: string }; fields: { name: string }[] },
|
|
167
|
+
): string[] {
|
|
168
|
+
return resource.fields
|
|
169
|
+
.filter((f) => evalFilter(filter, { resourceType: resource.type.name, fieldName: f.name }))
|
|
170
|
+
.map((f) => f.name);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Evaluate traverseStopRules (a single filter) against a resource. */
|
|
174
|
+
function evaluateStopRule(
|
|
175
|
+
rule: Filter,
|
|
176
|
+
ctx: { resourceType: string; isFinal?: boolean; allOutputsFinal?: boolean },
|
|
177
|
+
): boolean {
|
|
178
|
+
return evalFilter(rule, ctx);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── §4.1 — Pruning parity: projectTreePruning ⇄ projectTreeFieldFilter ────
|
|
182
|
+
|
|
183
|
+
const projectPruningCases: Array<{
|
|
184
|
+
name: string;
|
|
185
|
+
typeName: string;
|
|
186
|
+
fields: string[];
|
|
187
|
+
expected: string[];
|
|
188
|
+
}> = [
|
|
189
|
+
{
|
|
190
|
+
name: "StreamWorkdir/* — all fields dropped",
|
|
191
|
+
typeName: "StreamWorkdir/run-42",
|
|
192
|
+
fields: ["cmd", "output"],
|
|
193
|
+
expected: [],
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
name: "BlockPackCustom — template dropped, other kept",
|
|
197
|
+
typeName: "BlockPackCustom",
|
|
198
|
+
fields: ["template", "output"],
|
|
199
|
+
expected: ["output"],
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: "UserProject — __serviceTemplate* dropped, others kept",
|
|
203
|
+
typeName: "UserProject",
|
|
204
|
+
fields: ["__serviceTemplateA", "__serviceTemplateB", "x"],
|
|
205
|
+
expected: ["x"],
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: "Blob — all fields dropped",
|
|
209
|
+
typeName: "Blob",
|
|
210
|
+
fields: ["data"],
|
|
211
|
+
expected: [],
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: "arbitrary other type — all fields kept",
|
|
215
|
+
typeName: "SomeStructure",
|
|
216
|
+
fields: ["a", "b", "c"],
|
|
217
|
+
expected: ["a", "b", "c"],
|
|
218
|
+
},
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
describe("§4.1 project tree pruning parity", () => {
|
|
222
|
+
const pruner = projectTreePruning(noopLogger);
|
|
223
|
+
const filter = projectTreeFieldFilter();
|
|
224
|
+
|
|
225
|
+
for (const c of projectPruningCases) {
|
|
226
|
+
it(`${c.name} — BFS keeps [${c.expected.join(",")}]`, () => {
|
|
227
|
+
const resource = makeResource(c.typeName, c.fields);
|
|
228
|
+
const kept = pruner(resource).map((f) => f.name);
|
|
229
|
+
expect(kept).toEqual(c.expected);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it(`${c.name} — fieldFilter keeps [${c.expected.join(",")}]`, () => {
|
|
233
|
+
const resource = makeResource(c.typeName, c.fields);
|
|
234
|
+
const kept = evaluateFieldFilter(filter, resource);
|
|
235
|
+
expect(kept).toEqual(c.expected);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// ── §4.1 — Pruning parity: ProjectsListTreePruningFunction ⇄ projectsListFieldFilter
|
|
241
|
+
|
|
242
|
+
const projectsListPruningCases: Array<{
|
|
243
|
+
name: string;
|
|
244
|
+
typeName: string;
|
|
245
|
+
fields: string[];
|
|
246
|
+
expected: string[];
|
|
247
|
+
}> = [
|
|
248
|
+
{
|
|
249
|
+
name: "Projects root — all fields kept",
|
|
250
|
+
typeName: "Projects",
|
|
251
|
+
fields: ["project-1", "project-2"],
|
|
252
|
+
expected: ["project-1", "project-2"],
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: "non-Projects (UserProject) — no fields traversed",
|
|
256
|
+
typeName: "UserProject",
|
|
257
|
+
fields: ["x", "y"],
|
|
258
|
+
expected: [],
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
name: "non-Projects (Blob) — no fields traversed",
|
|
262
|
+
typeName: "Blob",
|
|
263
|
+
fields: ["data"],
|
|
264
|
+
expected: [],
|
|
265
|
+
},
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
describe("§4.1 projects-list pruning parity", () => {
|
|
269
|
+
const filter = projectsListFieldFilter;
|
|
270
|
+
|
|
271
|
+
for (const c of projectsListPruningCases) {
|
|
272
|
+
it(`${c.name} — BFS keeps [${c.expected.join(",")}]`, () => {
|
|
273
|
+
const resource = makeResource(c.typeName, c.fields);
|
|
274
|
+
const kept = ProjectsListTreePruningFunction(resource).map((f) => f.name);
|
|
275
|
+
expect(kept).toEqual(c.expected);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it(`${c.name} — fieldFilter keeps [${c.expected.join(",")}]`, () => {
|
|
279
|
+
const resource = makeResource(c.typeName, c.fields);
|
|
280
|
+
const kept = evaluateFieldFilter(filter, resource);
|
|
281
|
+
expect(kept).toEqual(c.expected);
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// ── §4.2 — traverseStopRules coverage ────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
describe("§4.2 projectTreeTraverseStopRules", () => {
|
|
289
|
+
const rule = projectTreeTraverseStopRules();
|
|
290
|
+
|
|
291
|
+
// Always-terminal exact types
|
|
292
|
+
const alwaysTerminalTypes = [
|
|
293
|
+
"json/object",
|
|
294
|
+
"json-gz/object",
|
|
295
|
+
"json/string",
|
|
296
|
+
"json/array",
|
|
297
|
+
"json/number",
|
|
298
|
+
"binary",
|
|
299
|
+
"Frontend/FromUrl",
|
|
300
|
+
"Frontend/FromFolder",
|
|
301
|
+
"BObjectSpec",
|
|
302
|
+
"BContextEnd",
|
|
303
|
+
"Null",
|
|
304
|
+
"LSProvider",
|
|
305
|
+
"Blob",
|
|
306
|
+
];
|
|
307
|
+
for (const t of alwaysTerminalTypes) {
|
|
308
|
+
it(`always-terminal: ${t}`, () => {
|
|
309
|
+
expect(evaluateStopRule(rule, { resourceType: t })).toBe(true);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Always-terminal prefix types
|
|
314
|
+
const prefixTerminalCases: Array<{ desc: string; type: string }> = [
|
|
315
|
+
{ desc: "Blob/ prefix", type: "Blob/v2" },
|
|
316
|
+
{ desc: "LS/ prefix", type: "LS/remote" },
|
|
317
|
+
{ desc: "WorkingDirectory/ prefix", type: "WorkingDirectory/1" },
|
|
318
|
+
{ desc: "StorageSpaceAllocation/ prefix", type: "StorageSpaceAllocation/disk" },
|
|
319
|
+
];
|
|
320
|
+
for (const { desc, type } of prefixTerminalCases) {
|
|
321
|
+
it(`always-terminal: ${desc} (${type})`, () => {
|
|
322
|
+
expect(evaluateStopRule(rule, { resourceType: type })).toBe(true);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// isFinal=true conditional stops — StreamManager (and other container types)
|
|
327
|
+
it("StreamManager + isFinal=true → stops", () => {
|
|
328
|
+
expect(evaluateStopRule(rule, { resourceType: "StreamManager", isFinal: true })).toBe(true);
|
|
329
|
+
});
|
|
330
|
+
it("StreamManager + isFinal=false → continues", () => {
|
|
331
|
+
expect(evaluateStopRule(rule, { resourceType: "StreamManager", isFinal: false })).toBe(false);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// isFinal=true conditional stops — container types (representative sample)
|
|
335
|
+
it("StdMap + isFinal=true → stops", () => {
|
|
336
|
+
expect(evaluateStopRule(rule, { resourceType: "StdMap", isFinal: true })).toBe(true);
|
|
337
|
+
});
|
|
338
|
+
it("StdMap + isFinal=false → continues", () => {
|
|
339
|
+
expect(evaluateStopRule(rule, { resourceType: "StdMap", isFinal: false })).toBe(false);
|
|
340
|
+
});
|
|
341
|
+
it("BlockPackCustom + isFinal=true → stops", () => {
|
|
342
|
+
expect(evaluateStopRule(rule, { resourceType: "BlockPackCustom", isFinal: true })).toBe(true);
|
|
343
|
+
});
|
|
344
|
+
it("BlockPackCustom + isFinal=false → continues", () => {
|
|
345
|
+
expect(evaluateStopRule(rule, { resourceType: "BlockPackCustom", isFinal: false })).toBe(false);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// json/resourceError — unconditional stop (BFS treats v1 as always-final; impl
|
|
349
|
+
// matches the type regardless of version since the proto filter has no version
|
|
350
|
+
// property).
|
|
351
|
+
it("json/resourceError + isFinal=true → stops", () => {
|
|
352
|
+
expect(evaluateStopRule(rule, { resourceType: "json/resourceError", isFinal: true })).toBe(
|
|
353
|
+
true,
|
|
354
|
+
);
|
|
355
|
+
});
|
|
356
|
+
it("json/resourceError + isFinal=false → stops (unconditional)", () => {
|
|
357
|
+
expect(evaluateStopRule(rule, { resourceType: "json/resourceError", isFinal: false })).toBe(
|
|
358
|
+
true,
|
|
359
|
+
);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("PColumnData/Int32 + isFinal=true → stops", () => {
|
|
363
|
+
expect(evaluateStopRule(rule, { resourceType: "PColumnData/Int32", isFinal: true })).toBe(true);
|
|
364
|
+
});
|
|
365
|
+
it("PColumnData/Int32 + isFinal=false → continues", () => {
|
|
366
|
+
expect(evaluateStopRule(rule, { resourceType: "PColumnData/Int32", isFinal: false })).toBe(
|
|
367
|
+
false,
|
|
368
|
+
);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("StreamWorkdir/run-42 + isFinal=true → stops", () => {
|
|
372
|
+
expect(evaluateStopRule(rule, { resourceType: "StreamWorkdir/run-42", isFinal: true })).toBe(
|
|
373
|
+
true,
|
|
374
|
+
);
|
|
375
|
+
});
|
|
376
|
+
it("StreamWorkdir/run-42 + isFinal=false → continues", () => {
|
|
377
|
+
expect(evaluateStopRule(rule, { resourceType: "StreamWorkdir/run-42", isFinal: false })).toBe(
|
|
378
|
+
false,
|
|
379
|
+
);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// isFinal + allOutputsFinal stops
|
|
383
|
+
it("BlobUpload/v1 + isFinal=true + allOutputsFinal=true → stops", () => {
|
|
384
|
+
expect(
|
|
385
|
+
evaluateStopRule(rule, {
|
|
386
|
+
resourceType: "BlobUpload/v1",
|
|
387
|
+
isFinal: true,
|
|
388
|
+
allOutputsFinal: true,
|
|
389
|
+
}),
|
|
390
|
+
).toBe(true);
|
|
391
|
+
});
|
|
392
|
+
it("BlobUpload/v1 + isFinal=false → continues", () => {
|
|
393
|
+
expect(
|
|
394
|
+
evaluateStopRule(rule, {
|
|
395
|
+
resourceType: "BlobUpload/v1",
|
|
396
|
+
isFinal: false,
|
|
397
|
+
allOutputsFinal: true,
|
|
398
|
+
}),
|
|
399
|
+
).toBe(false);
|
|
400
|
+
});
|
|
401
|
+
it("BlobUpload/v1 + isFinal=true but allOutputsFinal=false → continues", () => {
|
|
402
|
+
expect(
|
|
403
|
+
evaluateStopRule(rule, {
|
|
404
|
+
resourceType: "BlobUpload/v1",
|
|
405
|
+
isFinal: true,
|
|
406
|
+
allOutputsFinal: false,
|
|
407
|
+
}),
|
|
408
|
+
).toBe(false);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("BlobIndex/v2 + isFinal=true + allOutputsFinal=true → stops", () => {
|
|
412
|
+
expect(
|
|
413
|
+
evaluateStopRule(rule, {
|
|
414
|
+
resourceType: "BlobIndex/v2",
|
|
415
|
+
isFinal: true,
|
|
416
|
+
allOutputsFinal: true,
|
|
417
|
+
}),
|
|
418
|
+
).toBe(true);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Non-matching type — traversal continues
|
|
422
|
+
it("arbitrary type SomeStructure → does not stop", () => {
|
|
423
|
+
expect(evaluateStopRule(rule, { resourceType: "SomeStructure" })).toBe(false);
|
|
424
|
+
});
|
|
425
|
+
it("UserProject → does not stop", () => {
|
|
426
|
+
expect(evaluateStopRule(rule, { resourceType: "UserProject" })).toBe(false);
|
|
427
|
+
});
|
|
428
|
+
it("Projects → does not stop", () => {
|
|
429
|
+
expect(evaluateStopRule(rule, { resourceType: "Projects" })).toBe(false);
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// ── §4.3 — Final-predicate parity ────────────────────────────────────────────
|
|
434
|
+
//
|
|
435
|
+
// For each case we assert:
|
|
436
|
+
// 1. DefaultFinalResourceDataPredicate(resource) === expectedPredicate
|
|
437
|
+
// 2. evaluateStopRule(rule, flags) === expectedStop
|
|
438
|
+
//
|
|
439
|
+
// `flags` encodes what the backend would emit for the same resource state:
|
|
440
|
+
// - isFinal = resource is "ready/error/duplicate" (or always-final type)
|
|
441
|
+
// - allOutputsFinal = outputsLocked && every field value is final
|
|
442
|
+
//
|
|
443
|
+
// The parity contract: when the predicate says "final" AND the type is one the
|
|
444
|
+
// stop rule covers, the stop rule must also fire. Types that are final only due
|
|
445
|
+
// to runtime state (StdMap, BlockPackCustom, …) are NOT in the stop rules —
|
|
446
|
+
// those are left to the backend's own isFinal flag (`item.final`) and are
|
|
447
|
+
// documented as intentional gaps below.
|
|
448
|
+
|
|
449
|
+
describe("§4.3 final-predicate parity: DefaultFinalResourceDataPredicate ⇄ projectTreeTraverseStopRules", () => {
|
|
450
|
+
const rule = projectTreeTraverseStopRules();
|
|
451
|
+
|
|
452
|
+
// ── Group A: Always-terminal (predicate=true always; stop fires regardless of isFinal) ──
|
|
453
|
+
|
|
454
|
+
const alwaysTerminal: Array<{ typeName: string; version?: string }> = [
|
|
455
|
+
{ typeName: "json/object" },
|
|
456
|
+
{ typeName: "json-gz/object" },
|
|
457
|
+
{ typeName: "json/string" },
|
|
458
|
+
{ typeName: "json/array" },
|
|
459
|
+
{ typeName: "json/number" },
|
|
460
|
+
{ typeName: "binary" },
|
|
461
|
+
{ typeName: "Frontend/FromUrl" },
|
|
462
|
+
{ typeName: "Frontend/FromFolder" },
|
|
463
|
+
{ typeName: "BObjectSpec" },
|
|
464
|
+
{ typeName: "BContextEnd" },
|
|
465
|
+
{ typeName: "Null" },
|
|
466
|
+
{ typeName: "LSProvider" },
|
|
467
|
+
{ typeName: "Blob" },
|
|
468
|
+
{ typeName: "Blob/v2" },
|
|
469
|
+
{ typeName: "LS/remote" },
|
|
470
|
+
{ typeName: "WorkingDirectory/1" },
|
|
471
|
+
{ typeName: "StorageSpaceAllocation/disk" },
|
|
472
|
+
];
|
|
473
|
+
|
|
474
|
+
for (const { typeName, version } of alwaysTerminal) {
|
|
475
|
+
it(`${typeName}: predicate=true, stop fires (even isFinal=false)`, () => {
|
|
476
|
+
const r = version
|
|
477
|
+
? { ...makeResource(typeName, []), type: { name: typeName, version } }
|
|
478
|
+
: makeResource(typeName, []);
|
|
479
|
+
expect(DefaultFinalResourceDataPredicate(r as any)).toBe(true);
|
|
480
|
+
// stop rule fires unconditionally for these types
|
|
481
|
+
expect(evaluateStopRule(rule, { resourceType: typeName, isFinal: false })).toBe(true);
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ── Group B: isFinal-conditional — readyOrDuplicateOrError maps to isFinal ──
|
|
486
|
+
|
|
487
|
+
it("StreamManager — ready resource (fields=undefined): predicate=true, stop fires with isFinal=true", () => {
|
|
488
|
+
// fields=undefined triggers the early-return path in the predicate (no getField call needed)
|
|
489
|
+
const r = { ...makeReadyResource("StreamManager"), fields: undefined };
|
|
490
|
+
expect(DefaultFinalResourceDataPredicate(r as any)).toBe(true);
|
|
491
|
+
expect(evaluateStopRule(rule, { resourceType: "StreamManager", isFinal: true })).toBe(true);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it("StreamManager — not-ready resource: predicate=false, stop does NOT fire with isFinal=false", () => {
|
|
495
|
+
const r = makeResource("StreamManager", []);
|
|
496
|
+
expect(DefaultFinalResourceDataPredicate(r as any)).toBe(false);
|
|
497
|
+
expect(evaluateStopRule(rule, { resourceType: "StreamManager", isFinal: false })).toBe(false);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it("StdMap — ready resource: predicate=true, stop fires with isFinal=true", () => {
|
|
501
|
+
const r = makeReadyResource("StdMap");
|
|
502
|
+
expect(DefaultFinalResourceDataPredicate(r as any)).toBe(true);
|
|
503
|
+
expect(evaluateStopRule(rule, { resourceType: "StdMap", isFinal: true })).toBe(true);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("StdMap — not-ready resource: predicate=false, stop does NOT fire with isFinal=false", () => {
|
|
507
|
+
const r = makeResource("StdMap", []);
|
|
508
|
+
expect(DefaultFinalResourceDataPredicate(r as any)).toBe(false);
|
|
509
|
+
expect(evaluateStopRule(rule, { resourceType: "StdMap", isFinal: false })).toBe(false);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it("BlockPackCustom — ready resource: predicate=true, stop fires with isFinal=true", () => {
|
|
513
|
+
const r = makeReadyResource("BlockPackCustom");
|
|
514
|
+
expect(DefaultFinalResourceDataPredicate(r as any)).toBe(true);
|
|
515
|
+
expect(evaluateStopRule(rule, { resourceType: "BlockPackCustom", isFinal: true })).toBe(true);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it("BlockPackCustom — not-ready resource: predicate=false, stop does NOT fire with isFinal=false", () => {
|
|
519
|
+
const r = makeResource("BlockPackCustom", []);
|
|
520
|
+
expect(DefaultFinalResourceDataPredicate(r as any)).toBe(false);
|
|
521
|
+
expect(evaluateStopRule(rule, { resourceType: "BlockPackCustom", isFinal: false })).toBe(false);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it("PColumnData/Int32 — ready resource: predicate=true, stop fires with isFinal=true", () => {
|
|
525
|
+
const r = makeReadyResource("PColumnData/Int32");
|
|
526
|
+
expect(DefaultFinalResourceDataPredicate(r as any)).toBe(true);
|
|
527
|
+
expect(evaluateStopRule(rule, { resourceType: "PColumnData/Int32", isFinal: true })).toBe(true);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it("PColumnData/Int32 — not-ready resource: predicate=false, stop does NOT fire with isFinal=false", () => {
|
|
531
|
+
const r = makeResource("PColumnData/Int32", []);
|
|
532
|
+
expect(DefaultFinalResourceDataPredicate(r as any)).toBe(false);
|
|
533
|
+
expect(evaluateStopRule(rule, { resourceType: "PColumnData/Int32", isFinal: false })).toBe(
|
|
534
|
+
false,
|
|
535
|
+
);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it("StreamWorkdir/run-42 — ready resource: predicate=true, stop fires with isFinal=true", () => {
|
|
539
|
+
const r = makeReadyResource("StreamWorkdir/run-42");
|
|
540
|
+
expect(DefaultFinalResourceDataPredicate(r as any)).toBe(true);
|
|
541
|
+
expect(evaluateStopRule(rule, { resourceType: "StreamWorkdir/run-42", isFinal: true })).toBe(
|
|
542
|
+
true,
|
|
543
|
+
);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it("StreamWorkdir/run-42 — not-ready: predicate=false, stop does NOT fire with isFinal=false", () => {
|
|
547
|
+
const r = makeResource("StreamWorkdir/run-42", []);
|
|
548
|
+
expect(DefaultFinalResourceDataPredicate(r as any)).toBe(false);
|
|
549
|
+
expect(evaluateStopRule(rule, { resourceType: "StreamWorkdir/run-42", isFinal: false })).toBe(
|
|
550
|
+
false,
|
|
551
|
+
);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// ── Group C: isFinal+allOutputsFinal-conditional — readyAndHasAllOutputsFilled ──
|
|
555
|
+
|
|
556
|
+
it("BlobUpload/v1 — all outputs final: predicate=true, stop fires with isFinal+allOutputsFinal=true", () => {
|
|
557
|
+
const r = makeAllOutputsFinalResource("BlobUpload/v1");
|
|
558
|
+
expect(DefaultFinalResourceDataPredicate(r as any)).toBe(true);
|
|
559
|
+
expect(
|
|
560
|
+
evaluateStopRule(rule, {
|
|
561
|
+
resourceType: "BlobUpload/v1",
|
|
562
|
+
isFinal: true,
|
|
563
|
+
allOutputsFinal: true,
|
|
564
|
+
}),
|
|
565
|
+
).toBe(true);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it("BlobUpload/v1 — not ready: predicate=false, stop does NOT fire with isFinal=false", () => {
|
|
569
|
+
const r = makeResource("BlobUpload/v1", []);
|
|
570
|
+
expect(DefaultFinalResourceDataPredicate(r as any)).toBe(false);
|
|
571
|
+
expect(
|
|
572
|
+
evaluateStopRule(rule, {
|
|
573
|
+
resourceType: "BlobUpload/v1",
|
|
574
|
+
isFinal: false,
|
|
575
|
+
allOutputsFinal: false,
|
|
576
|
+
}),
|
|
577
|
+
).toBe(false);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it("BlobIndex/v2 — all outputs final: predicate=true, stop fires with isFinal+allOutputsFinal=true", () => {
|
|
581
|
+
const r = makeAllOutputsFinalResource("BlobIndex/v2");
|
|
582
|
+
expect(DefaultFinalResourceDataPredicate(r as any)).toBe(true);
|
|
583
|
+
expect(
|
|
584
|
+
evaluateStopRule(rule, {
|
|
585
|
+
resourceType: "BlobIndex/v2",
|
|
586
|
+
isFinal: true,
|
|
587
|
+
allOutputsFinal: true,
|
|
588
|
+
}),
|
|
589
|
+
).toBe(true);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// ── Group D: json/resourceError — final for version "1" only ──
|
|
593
|
+
|
|
594
|
+
it("json/resourceError v1: predicate=true (version check), stop fires with isFinal=true", () => {
|
|
595
|
+
const r = makeReadyResource("json/resourceError", "1");
|
|
596
|
+
expect(DefaultFinalResourceDataPredicate(r as any)).toBe(true);
|
|
597
|
+
expect(evaluateStopRule(rule, { resourceType: "json/resourceError", isFinal: true })).toBe(
|
|
598
|
+
true,
|
|
599
|
+
);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// Intentional divergence: the predicate is version-aware (false for v2), but
|
|
603
|
+
// the ResourceTree filter proto has no version property — the stop rule fires
|
|
604
|
+
// for any json/resourceError regardless of version. Documented gap.
|
|
605
|
+
it("json/resourceError v2: predicate=false (version check), stop still fires (no version filter)", () => {
|
|
606
|
+
const r = {
|
|
607
|
+
...makeResource("json/resourceError", []),
|
|
608
|
+
type: { name: "json/resourceError", version: "2" },
|
|
609
|
+
};
|
|
610
|
+
expect(DefaultFinalResourceDataPredicate(r as any)).toBe(false);
|
|
611
|
+
expect(evaluateStopRule(rule, { resourceType: "json/resourceError", isFinal: false })).toBe(
|
|
612
|
+
true,
|
|
613
|
+
);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// ── Group E: never-final types — predicate=false, stop rule does not fire ──
|
|
617
|
+
//
|
|
618
|
+
// Covers the explicit `return false` cases in DefaultFinalResourceDataPredicate
|
|
619
|
+
// (UserProject, Projects, ClientRoot) and the `default` else-branch for unknown
|
|
620
|
+
// types. Both paths agree: not final, traversal continues.
|
|
621
|
+
|
|
622
|
+
const neverFinalCases: Array<{ typeName: string; label: string }> = [
|
|
623
|
+
{ typeName: "UserProject", label: "explicit false in predicate" },
|
|
624
|
+
{ typeName: "Projects", label: "explicit false in predicate" },
|
|
625
|
+
{ typeName: "ClientRoot", label: "explicit false in predicate" },
|
|
626
|
+
{ typeName: "SomeUnknownType", label: "default branch else → false" },
|
|
627
|
+
];
|
|
628
|
+
|
|
629
|
+
for (const { typeName, label } of neverFinalCases) {
|
|
630
|
+
it(`${typeName} (${label}): predicate=false, stop does NOT fire`, () => {
|
|
631
|
+
const r = makeResource(typeName, []);
|
|
632
|
+
expect(DefaultFinalResourceDataPredicate(r as any)).toBe(false);
|
|
633
|
+
expect(evaluateStopRule(rule, { resourceType: typeName, isFinal: false })).toBe(false);
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
});
|
|
@@ -3,7 +3,7 @@ import type { Computable } from "@milaboratories/computable";
|
|
|
3
3
|
|
|
4
4
|
export type TemporalSynchronizedTreeOps = Pick<
|
|
5
5
|
SynchronizedTreeOps,
|
|
6
|
-
"pollingInterval" | "stopPollingDelay" | "logStat" | "initialTreeLoadingTimeout"
|
|
6
|
+
"pollingInterval" | "stopPollingDelay" | "logStat" | "initialTreeLoadingTimeout" | "traversalMode"
|
|
7
7
|
>;
|
|
8
8
|
|
|
9
9
|
export interface TreeAndComputable<T, ST extends T = T> {
|