|
1 | 1 | // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. |
2 | 2 |
|
3 | 3 | import { describe, it, expect } from 'vitest'; |
4 | | -import { lintLivenessProperties } from './lint-liveness-properties.js'; |
| 4 | +import { |
| 5 | + lintLivenessProperties, |
| 6 | + // #10262 test seam — package-internal (not re-exported by `src/index.ts`, not |
| 7 | + // in the package's `exports` map). See the block below `getNested` in the |
| 8 | + // source for why this ONE property is tested off the ledger. |
| 9 | + checkItemAgainstWarnMap, |
| 10 | + getNested, |
| 11 | +} from './lint-liveness-properties.js'; |
5 | 12 |
|
6 | 13 | /** |
7 | 14 | * These run against the REAL ledgers shipped by `@objectstack/spec` (the same |
@@ -645,3 +652,107 @@ describe('lintLivenessProperties', () => { |
645 | 652 | }); |
646 | 653 | }); |
647 | 654 | }); |
| 655 | + |
| 656 | +// ── #10262: the array fan-out, tested at the WALKER's own level ────────────── |
| 657 | +// |
| 658 | +// Everything above this line is deliberately ledger-driven: it asserts against |
| 659 | +// the REAL ledgers shipped by `@objectstack/spec`, which is what makes those |
| 660 | +// assertions contract tests. This block is the one exception, and the reason is |
| 661 | +// recorded twice over in the comments above. |
| 662 | +// |
| 663 | +// `getNested`'s array fan-out — a dotted warn-map path resolved over an ARRAY |
| 664 | +// container level must visit EVERY element, not just index 0 — is reachable |
| 665 | +// only from a DOTTED warned entry, because `checkItem` takes the |
| 666 | +// `path.includes('.') ? getNested(item, path) : [item[path]]` branch. Its |
| 667 | +// subject was therefore always "whichever row happens to carry `authorWarn` |
| 668 | +// under an array container today", and that is a ledger verdict: verdicts move. |
| 669 | +// Twice a row correctly flipping to `live` deleted this coverage — |
| 670 | +// `dashboard.widgets.colorVariant` (#6774, filed as #7079) and then |
| 671 | +// `app.…navigation.children.runAction` (#10068, filed as #10262) — and as of |
| 672 | +// #10262 every warned entry in all 30 shipped ledgers is top-level, so there is |
| 673 | +// nothing left to re-subject to and no reason to expect a third subject to last. |
| 674 | +// |
| 675 | +// So this block drives the walker with a SYNTHETIC warn map through the |
| 676 | +// package-internal seam (`checkItemAgainstWarnMap`, `getNested` — module |
| 677 | +// exports, not re-exported by `src/index.ts`, not in the package's `exports` |
| 678 | +// map). No ledger flip can empty it. The cost is honest and bounded: these |
| 679 | +// assertions say nothing about which properties the ledger warns on — that |
| 680 | +// stays the job of every other block in this file. |
| 681 | +describe('the array fan-out, against a synthetic warn map (#10262)', () => { |
| 682 | + const warnOn = (...paths: string[]) => |
| 683 | + new Map(paths.map((p) => [p, { authorWarn: true, authorHint: 'synthetic (#10262)' }] as const)); |
| 684 | + |
| 685 | + /** `n` navigation entries; those at `authored` set the warned key. */ |
| 686 | + const navItems = (n: number, authored: number[]) => |
| 687 | + Array.from({ length: n }, (_, i) => ({ |
| 688 | + id: `nav_${i}`, |
| 689 | + type: 'object', |
| 690 | + objectName: 'crm_lead', |
| 691 | + ...(authored.includes(i) ? { runAction: `create_${i}` } : {}), |
| 692 | + })); |
| 693 | + |
| 694 | + describe('getNested', () => { |
| 695 | + it('resolves one value per element of an array container, in order', () => { |
| 696 | + expect(getNested({ navigation: navItems(3, [0, 1, 2]) }, 'navigation.runAction')) |
| 697 | + .toEqual(['create_0', 'create_1', 'create_2']); |
| 698 | + }); |
| 699 | + |
| 700 | + // The load-bearing shape: a walk that stopped at index 0 returns |
| 701 | + // `[undefined]` here — one entry, not three — while every fixture that |
| 702 | + // authors the key on the FIRST element keeps passing. That asymmetry is |
| 703 | + // exactly why a positive assertion on a single-entry fixture is not a test |
| 704 | + // of the fan-out (#7079's original reasoning). |
| 705 | + it('visits elements that do NOT set the key rather than filtering them out', () => { |
| 706 | + expect(getNested({ navigation: navItems(3, [2]) }, 'navigation.runAction')) |
| 707 | + .toEqual([undefined, undefined, 'create_2']); |
| 708 | + }); |
| 709 | + |
| 710 | + it('flattens a trailing array container one step (`nodes.tags` → every tag)', () => { |
| 711 | + expect(getNested({ nodes: [{ tags: ['a', 'b'] }, { tags: ['c'] }] }, 'nodes.tags')) |
| 712 | + .toEqual(['a', 'b', 'c']); |
| 713 | + }); |
| 714 | + |
| 715 | + it('treats a missing parent level as absent instead of throwing', () => { |
| 716 | + expect(getNested({}, 'navigation.runAction')).toEqual([]); |
| 717 | + expect(getNested({ navigation: null }, 'navigation.runAction')).toEqual([]); |
| 718 | + }); |
| 719 | + }); |
| 720 | + |
| 721 | + describe('checkItem via the dotted branch', () => { |
| 722 | + // The anti-index-0 assertion, restored as a property of the walker: the |
| 723 | + // warned key is authored on exactly ONE entry of a four-entry container, |
| 724 | + // and the walk must find it wherever that entry sits. A `getNested` that |
| 725 | + // stopped at index 0 passes case 0 and fails 1, 2 and 3. |
| 726 | + it.each([0, 1, 2, 3])('finds a warned key authored on navigation[%i] alone', (index) => { |
| 727 | + const findings = checkItemAgainstWarnMap( |
| 728 | + 'app', |
| 729 | + { name: 'crm_app', navigation: navItems(4, [index]) }, |
| 730 | + "app 'crm_app'", |
| 731 | + warnOn('navigation.runAction'), |
| 732 | + ); |
| 733 | + expect(findings).toHaveLength(1); |
| 734 | + expect(findings[0].message).toContain('navigation.runAction'); |
| 735 | + expect(findings[0].where).toBe("app 'crm_app'"); |
| 736 | + }); |
| 737 | + |
| 738 | + it('reports once per (item, path) however many entries author the key', () => { |
| 739 | + const findings = checkItemAgainstWarnMap( |
| 740 | + 'app', |
| 741 | + { name: 'crm_app', navigation: navItems(4, [0, 1, 2, 3]) }, |
| 742 | + "app 'crm_app'", |
| 743 | + warnOn('navigation.runAction'), |
| 744 | + ); |
| 745 | + expect(findings).toHaveLength(1); |
| 746 | + }); |
| 747 | + |
| 748 | + it('stays silent when no entry authors the warned key', () => { |
| 749 | + const findings = checkItemAgainstWarnMap( |
| 750 | + 'app', |
| 751 | + { name: 'crm_app', navigation: navItems(4, []) }, |
| 752 | + "app 'crm_app'", |
| 753 | + warnOn('navigation.runAction'), |
| 754 | + ); |
| 755 | + expect(findings).toEqual([]); |
| 756 | + }); |
| 757 | + }); |
| 758 | +}); |
0 commit comments