@milaboratories/pl-middle-layer 1.73.3 → 1.73.5

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.
@@ -18,6 +18,7 @@ import { projectsListFieldFilter, ProjectsListTreePruningFunction } from "./proj
18
18
  import type { Filter } from "@milaboratories/pl-client";
19
19
  import {
20
20
  DefaultFinalResourceDataPredicate,
21
+ TreeFinality,
21
22
  FilterOperatorType,
22
23
  FilterProperty,
23
24
  NullSignedResourceId,
@@ -65,7 +66,7 @@ function makeReadyResource(typeName: string, version = "1"): ExtendedResourceDat
65
66
  }
66
67
 
67
68
  /**
68
- * Build a resource that satisfies readyAndHasAllOutputsFilled:
69
+ * Build a resource that satisfies readyAndAllOutputsFilled:
69
70
  * resourceReady=true, outputsLocked=true, one field with valueIsFinal=true.
70
71
  */
71
72
  function makeAllOutputsFinalResource(typeName: string): ExtendedResourceData {
@@ -285,6 +286,23 @@ describe("§4.1 projects-list pruning parity", () => {
285
286
 
286
287
  // ── §4.2 — traverseStopRules coverage ────────────────────────────────────────
287
288
 
289
+ describe("the pruning the finality table relies on", () => {
290
+ for (const e of TreeFinality.entries) {
291
+ if (e.requiresPruning === undefined) continue;
292
+ const { type, fields } = e.requiresPruning;
293
+ it(`${type}: projectTreePruning and projectTreeFieldFilter drop ${fields === "all" ? "every field" : fields.join(", ")}`, () => {
294
+ const names = fields === "all" ? ["incarnation", "any"] : [...fields];
295
+ const r = makeResource(type, names);
296
+ const kept = projectTreePruning(noopLogger)(r).map((f) => f.name);
297
+ const keptByFilter = evaluateFieldFilter(projectTreeFieldFilter(), r);
298
+ for (const name of names) {
299
+ expect(kept).not.toContain(name);
300
+ expect(keptByFilter).not.toContain(name);
301
+ }
302
+ });
303
+ }
304
+ });
305
+
288
306
  describe("§4.2 projectTreeTraverseStopRules", () => {
289
307
  const rule = projectTreeTraverseStopRules();
290
308
 
@@ -301,8 +319,11 @@ describe("§4.2 projectTreeTraverseStopRules", () => {
301
319
  "BObjectSpec",
302
320
  "BContextEnd",
303
321
  "Null",
304
- "LSProvider",
305
322
  "Blob",
323
+ "Frontend/FromLocalTgz",
324
+ "json/bool",
325
+ "json/null",
326
+ "json/errorTrace",
306
327
  ];
307
328
  for (const t of alwaysTerminalTypes) {
308
329
  it(`always-terminal: ${t}`, () => {
@@ -436,15 +457,14 @@ describe("§4.2 projectTreeTraverseStopRules", () => {
436
457
  // 1. DefaultFinalResourceDataPredicate(resource) === expectedPredicate
437
458
  // 2. evaluateStopRule(rule, flags) === expectedStop
438
459
  //
439
- // `flags` encodes what the backend would emit for the same resource state:
460
+ // `flags` compress several independent backend properties into two:
440
461
  // - isFinal = resource is "ready/error/duplicate" (or always-final type)
441
462
  // - allOutputsFinal = outputsLocked && every field value is final
442
463
  //
443
464
  // 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.
465
+ // stop rule covers, the stop rule must also fire. Where the backend's filter
466
+ // differs from the predicate, finality_stop_rules.test.ts checks it against the
467
+ // backend's semantics.
448
468
 
449
469
  describe("§4.3 final-predicate parity: DefaultFinalResourceDataPredicate ⇄ projectTreeTraverseStopRules", () => {
450
470
  const rule = projectTreeTraverseStopRules();
@@ -463,8 +483,11 @@ describe("§4.3 final-predicate parity: DefaultFinalResourceDataPredicate ⇄ pr
463
483
  { typeName: "BObjectSpec" },
464
484
  { typeName: "BContextEnd" },
465
485
  { typeName: "Null" },
466
- { typeName: "LSProvider" },
467
486
  { typeName: "Blob" },
487
+ { typeName: "Frontend/FromLocalTgz" },
488
+ { typeName: "json/bool" },
489
+ { typeName: "json/null" },
490
+ { typeName: "json/errorTrace" },
468
491
  { typeName: "Blob/v2" },
469
492
  { typeName: "LS/remote" },
470
493
  { typeName: "WorkingDirectory/1" },
@@ -551,7 +574,7 @@ describe("§4.3 final-predicate parity: DefaultFinalResourceDataPredicate ⇄ pr
551
574
  );
552
575
  });
553
576
 
554
- // ── Group C: isFinal+allOutputsFinal-conditional — readyAndHasAllOutputsFilled ──
577
+ // ── Group C: isFinal+allOutputsFinal-conditional — readyAndAllOutputsFilled ──
555
578
 
556
579
  it("BlobUpload/v1 — all outputs final: predicate=true, stop fires with isFinal+allOutputsFinal=true", () => {
557
580
  const r = makeAllOutputsFinalResource("BlobUpload/v1");
@@ -615,15 +638,13 @@ describe("§4.3 final-predicate parity: DefaultFinalResourceDataPredicate ⇄ pr
615
638
 
616
639
  // ── Group E: never-final types — predicate=false, stop rule does not fire ──
617
640
  //
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.
641
+ // Types declared `never`, and types no entry matches: not final, and no stop clause.
621
642
 
622
643
  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" },
644
+ { typeName: "UserProject", label: "declared never" },
645
+ { typeName: "Projects", label: "declared never" },
646
+ { typeName: "ClientRoot", label: "declared never" },
647
+ { typeName: "SomeUnknownType", label: "unmatched type" },
627
648
  ];
628
649
 
629
650
  for (const { typeName, label } of neverFinalCases) {
@@ -633,4 +654,26 @@ describe("§4.3 final-predicate parity: DefaultFinalResourceDataPredicate ⇄ pr
633
654
  expect(evaluateStopRule(rule, { resourceType: typeName, isFinal: false })).toBe(false);
634
655
  });
635
656
  }
657
+
658
+ it("LSProvider: never final, stop does NOT fire even when ready", () => {
659
+ expect(DefaultFinalResourceDataPredicate(makeReadyResource("LSProvider"))).toBe(false);
660
+ expect(
661
+ evaluateStopRule(rule, { resourceType: "LSProvider", isFinal: true, allOutputsFinal: true }),
662
+ ).toBe(false);
663
+ });
664
+
665
+ // For these ready fixtures, locked outputs and settled fields satisfy both checks.
666
+ for (const typeName of ["BResolveSingle", "BResolveChoice", "BlobCopy/aToB"]) {
667
+ it(`${typeName}: final with outputs locked and settled, stop fires only then`, () => {
668
+ expect(DefaultFinalResourceDataPredicate(makeAllOutputsFinalResource(typeName))).toBe(true);
669
+ // a synthetic unlocked snapshot: schema-created resolvers and copies have locked outputs
670
+ expect(DefaultFinalResourceDataPredicate(makeReadyResource(typeName))).toBe(false);
671
+ expect(
672
+ evaluateStopRule(rule, { resourceType: typeName, isFinal: true, allOutputsFinal: true }),
673
+ ).toBe(true);
674
+ expect(
675
+ evaluateStopRule(rule, { resourceType: typeName, isFinal: true, allOutputsFinal: false }),
676
+ ).toBe(false);
677
+ });
678
+ }
636
679
  });
@@ -325,8 +325,8 @@ const SharingOutboxPruningFunction: PruningFunction = (resource) => {
325
325
 
326
326
  // Server-side traversal scope (modern resourceTree path). Pruning is client-side ONLY and does
327
327
  // NOT stop the backend walk — without a fieldFilter the backend descends through the envelope's
328
- // project/{uuid} snapshots into the whole project graph (StreamManager etc.), whose field-driven
329
- // finality predicate then throws on the pruned-to-[] fields. Following fields only FROM the outbox
328
+ // project/{uuid} snapshots into the whole project graph (StreamManager etc.), which the tree would
329
+ // then hold, with its fields pruned to [], and re-poll for nothing. Following fields only FROM the outbox
330
330
  // and the envelope stops the walk at the project snapshots (UserProject), which we never traverse.
331
331
  const SharingOutboxFieldFilter: Filter = treeFilter.or(
332
332
  treeFilter.resourceTypeEq(SharingOutboxResourceType.name),