Skip to content

Commit 5e9c01f

Browse files
committed
test(spec-tooling): restore the liveness author-lint's array fan-out assertion (#7079)
`getNested` fans a dotted warn-map path out over an ARRAY container level, so `navigation.runAction` must check EVERY navigation entry, not just index 0. That reach lost its only test subject when #6774 flipped `dashboard.widgets.colorVariant` to `live`: the assertion "fans out over EVERY widget, not just the first" had nowhere left to stand, and was removed with a tombstone comment rather than downgraded into a silence check that would pass on a walk which never looks past `widgets[0]`. `app.props.navigation.children.runAction` (#4848's spec half — `planned` + `authorWarn`, because the declared deep-link auto-run slot is validated at authoring but no shipped shell reads it yet) is now a dotted warned path under an array container: `navigation` is an array in every authored app. Rebuild the assertion against it in the deleted test's construction — the warned key on `navigation[1]` and nowhere on `navigation[0]`, so a walk that stops at the first element finds `undefined`, emits nothing, and goes red. Verified by mutation, not by a green suite: with `getNested` temporarily taking only `c[0]` at an array level, this assertion is the ONLY failure across the package (1883 passed / 1 failed), so the red is attributable to the broken walk rather than to collateral damage. Reverting the mutation returns 1884 passed. Alongside it: a control pinning that index 0 is not a blind spot either, the `checkItem` one-finding-per-(item, path) contract the length assertion rests on, and a clean-navigation silence pin. The tombstone comment is replaced by a pointer to this resolution. Test-only; `getNested` and the lint's behaviour are untouched. Fixes #7079 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DveDnLVD1syPGfXryonczk
1 parent 5db0dca commit 5e9c01f

1 file changed

Lines changed: 99 additions & 9 deletions

File tree

packages/lint/src/lint-liveness-properties.test.ts

Lines changed: 99 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -441,15 +441,19 @@ describe('lintLivenessProperties', () => {
441441
expect(findings.map((f) => f.message).some((m) => m.includes('widgets.colorVariant'))).toBe(false);
442442
});
443443

444-
// ⚠️ What this flip COST, recorded so the next author does not read the
445-
// absence as an oversight: `widgets.colorVariant` was the only warned entry
446-
// in any ledger sitting under an array container, so it was the only subject
447-
// `getNested`'s array fan-out ever had. The assertion that used to live here
448-
// — "fans out over EVERY widget, not just the first" — cannot be written
449-
// against a warn-map that is empty for `dashboard`, and no other type offers
450-
// a dotted warned path today. The fan-out is now untested; filed as #7079
451-
// rather than replaced with a test that would pass on a lint which never
452-
// walks past `widgets[0]`.
444+
// ⚠️ What this flip COST, and where the debt was repaid. `widgets.colorVariant`
445+
// was the only warned entry in any ledger sitting under an array container,
446+
// so it was the only subject `getNested`'s array fan-out ever had; the
447+
// assertion that lived here — "fans out over EVERY widget, not just the
448+
// first" — could not be rewritten against a warn-map that is empty for
449+
// `dashboard`, and was filed as #7079 rather than downgraded into a silence
450+
// check that would pass on a lint which never walks past `widgets[0]`.
451+
//
452+
// #7079 is CLOSED: `app.props.navigation.children.runAction` (#4848's
453+
// spec half — `planned` + `authorWarn`) gave the fan-out a new dotted
454+
// subject under an array container, and the assertion was rewritten
455+
// against it in the `app navigation` block at the bottom of this file,
456+
// same construction (the warned key on index 1, never index 0).
453457

454458
// ── #5010: four of these keys are RETIRED, so this lint must go quiet ─────
455459
//
@@ -515,4 +519,90 @@ describe('lintLivenessProperties', () => {
515519
expect(findings).toEqual([]);
516520
});
517521
});
522+
523+
// ── #7079: the array fan-out gets its subject back ─────────────────────────
524+
//
525+
// `getNested` resolves a dotted warn-map path by fanning it out over an ARRAY
526+
// container level — `navigation.runAction` must check EVERY navigation entry,
527+
// not just `navigation[0]`. That reach is what the docblock promises ("each
528+
// flow node, each dataset measure") and it is the half a walk can lose
529+
// silently: a `getNested` that stopped at index 0 still warns on every
530+
// single-entry fixture, on every top-level warned key, and on the first item
531+
// of every real app — so nothing else in this file would go red.
532+
//
533+
// The subject is `app.props.navigation.children.runAction`, #4848's spec half:
534+
// `planned` + `authorWarn`, because the declared deep-link auto-run slot is
535+
// validated at authoring but no shipped shell reads it yet. It sits under an
536+
// array container in every authored app (`examples/app-crm` crm.app.ts:13,
537+
// `app-todo` todo.app.ts:15, showcase `ui/apps/index.ts:27` all open
538+
// `navigation: [`), which is precisely the subject class `widgets.colorVariant`
539+
// stopped being when #6774 flipped it live.
540+
//
541+
// Ledger-driven, like the rest of this file: it holds the real
542+
// `@objectstack/spec` ledger to its `authorWarn` on that row as well as the
543+
// walk to its fan-out. Because the discriminating assertion is a POSITIVE
544+
// warning, it cannot pass vacuously — `lintLivenessProperties` returns [] both
545+
// when the ledgers fail to load and when the walk is broken, and this block
546+
// fails in either case.
547+
describe('app navigation (#7079 — `getNested`\'s array fan-out)', () => {
548+
const navApp = (navigation: Record<string, unknown>[]) => ({
549+
apps: [{ name: 'crm_app', label: 'CRM', navigation }],
550+
});
551+
552+
const navItem = (id: string, extra: Record<string, unknown> = {}) => ({
553+
id,
554+
type: 'object',
555+
objectName: 'crm_lead',
556+
label: 'Leads',
557+
...extra,
558+
});
559+
560+
// The load-bearing assertion, rebuilt in the deleted widget test's shape:
561+
// the warned key sits on `navigation[1]` and NOWHERE on `navigation[0]`, so
562+
// a walk that only ever reads the first element of an array level finds
563+
// `undefined`, emits nothing, and this goes red. Putting the key on index 0
564+
// — or on a single-entry `navigation` — would pass on both walks and prove
565+
// nothing, which is the exact non-test #7079 was filed to avoid writing.
566+
it('fans out over EVERY navigation entry, not just the first', () => {
567+
const findings = lintLivenessProperties(navApp([
568+
navItem('nav_accounts', { objectName: 'crm_account', label: 'Accounts' }),
569+
navItem('nav_leads', { runAction: 'create_lead' }),
570+
]));
571+
const matched = paths(findings).filter((m) => m.includes('navigation.runAction'));
572+
expect(matched).toHaveLength(1);
573+
expect(findings.find((f) => f.message.includes('navigation.runAction'))?.where)
574+
.toBe("app 'crm_app'");
575+
});
576+
577+
// The control for the assertion above: index 0 is not a blind spot either,
578+
// so a red there means "the fan-out is broken", not "the walk moved".
579+
it('warns when the deep-link slot is authored on the first entry too', () => {
580+
const findings = lintLivenessProperties(navApp([
581+
navItem('nav_leads', { runAction: 'create_lead' }),
582+
navItem('nav_accounts', { objectName: 'crm_account', label: 'Accounts' }),
583+
]));
584+
expect(paths(findings).filter((m) => m.includes('navigation.runAction'))).toHaveLength(1);
585+
});
586+
587+
// `checkItem` breaks after the first hit per (item, path): the advisory is
588+
// about the app's authoring, not a per-entry tally, so three offending
589+
// entries are still one line. Pinned because it is the reason the assertion
590+
// above can say `toHaveLength(1)` without that number being an accident.
591+
it('reports one finding per app even when several entries author the slot', () => {
592+
const findings = lintLivenessProperties(navApp([
593+
navItem('nav_accounts', { objectName: 'crm_account', runAction: 'create_account' }),
594+
navItem('nav_leads', { runAction: 'create_lead' }),
595+
navItem('nav_contacts', { objectName: 'crm_contact', runAction: 'create_contact' }),
596+
]));
597+
expect(paths(findings).filter((m) => m.includes('navigation.runAction'))).toHaveLength(1);
598+
});
599+
600+
it('stays silent on navigation entries that author no warned key', () => {
601+
const findings = lintLivenessProperties(navApp([
602+
navItem('nav_accounts', { objectName: 'crm_account', label: 'Accounts' }),
603+
navItem('nav_leads', { viewName: 'hot_leads' }),
604+
]));
605+
expect(findings).toEqual([]);
606+
});
607+
});
518608
});

0 commit comments

Comments
 (0)