@@ -411,13 +411,21 @@ describe('validateTranslationReferences — the canonical view-record shape', ()
411411 // Reading `view.name` / `view.data.object` at the record root resolves
412412 // nothing here, drops the record, and reports every view key the app ships —
413413 // ~40 correct keys on the real corpus.
414+ //
415+ // The default list carries a `label` (#6038): without one it is
416+ // signature-identical to `listViews.my_leads` (`{type,label,columns}` all
417+ // equal), the composer collapses the two, and `all_leads` is not a runtime
418+ // view name at all — so the fixture would be asserting that a key nothing
419+ // resolves is legal. The label makes it the distinct default list this test
420+ // says it is. The collapse itself is pinned separately below.
414421 const leadViews = {
415422 objects : [ { name : 'crm_lead' , fields : { name : { type : 'text' } } } ] ,
416423 views : [
417424 {
418425 list : {
419426 type : 'grid' ,
420427 name : 'all_leads' ,
428+ label : 'All Leads' ,
421429 data : { provider : 'object' , object : 'crm_lead' } ,
422430 } ,
423431 listViews : {
@@ -472,6 +480,159 @@ describe('validateTranslationReferences — the canonical view-record shape', ()
472480 expect ( findings [ 0 ] . hint ) . toContain ( 'all_leads' ) ;
473481 } ) ;
474482
483+ // ── #6038 / #5164 leg 2: the default list's key is the RUNTIME's ─────────
484+ //
485+ // The composer (`expandViewContainer`) is the single producer of a view's
486+ // runtime identity, and these pin that this rule reads the key from it
487+ // instead of re-deriving one. Every fixture below is driven through the real
488+ // `validateTranslationReferences`, and every "legal" assertion is paired with
489+ // a planted bad key on the SAME fixture — a `toEqual([])` that passes because
490+ // the rule produced nothing at all would prove nothing.
491+ describe ( 'the default list is keyed by the runtime identity, single spelling' , ( ) => {
492+ /** The showcase shape: a container declaring ONLY a nameless default list. */
493+ const namelessDefaultList = {
494+ objects : [ { name : 'crm_lead' , fields : { name : { type : 'text' } } } ] ,
495+ views : [
496+ {
497+ list : { type : 'grid' , label : 'All Leads' , data : { provider : 'object' , object : 'crm_lead' } } ,
498+ } ,
499+ ] ,
500+ } ;
501+
502+ const bundle = ( views : Record < string , unknown > ) => ( {
503+ translations : [ { en : { objects : { crm_lead : { label : 'Lead' , _views : views } } } } ] ,
504+ } ) ;
505+
506+ it ( 'accepts `default` for a nameless default list — the key the registry holds' , ( ) => {
507+ const findings = validateTranslationReferences ( {
508+ ...namelessDefaultList ,
509+ ...bundle ( { default : { label : '全部线索' } } ) ,
510+ } ) ;
511+ expect ( findings ) . toEqual ( [ ] ) ;
512+ } ) ;
513+
514+ it ( 'the same fixture still reports a key nothing declares (the green above is not an empty run)' , ( ) => {
515+ const findings = validateTranslationReferences ( {
516+ ...namelessDefaultList ,
517+ ...bundle ( { default : { label : '全部线索' } , hot_leads : { label : 'Hot' } } ) ,
518+ } ) ;
519+ expect ( findings ) . toHaveLength ( 1 ) ;
520+ expect ( findings [ 0 ] . path ) . toBe ( 'translations[0].en.objects.crm_lead._views.hot_leads' ) ;
521+ } ) ;
522+
523+ it ( 'rejects the old `list` spelling — one key per view, and it is the runtime one' , ( ) => {
524+ const findings = validateTranslationReferences ( {
525+ ...namelessDefaultList ,
526+ ...bundle ( { list : { label : '全部线索' } } ) ,
527+ } ) ;
528+ expect ( findings ) . toHaveLength ( 1 ) ;
529+ expect ( findings [ 0 ] . path ) . toBe ( 'translations[0].en.objects.crm_lead._views.list' ) ;
530+ expect ( findings [ 0 ] . hint ) . toContain ( 'default' ) ;
531+ } ) ;
532+
533+ it ( 'a named default list keeps the author\'s `name`' , ( ) => {
534+ const stack = {
535+ objects : [ { name : 'crm_lead' , fields : { name : { type : 'text' } } } ] ,
536+ views : [
537+ {
538+ list : {
539+ type : 'grid' ,
540+ name : 'all_leads' ,
541+ label : 'All Leads' ,
542+ data : { provider : 'object' , object : 'crm_lead' } ,
543+ } ,
544+ listViews : { my_leads : { type : 'grid' , data : { provider : 'object' , object : 'crm_lead' } } } ,
545+ } ,
546+ ] ,
547+ } ;
548+ expect (
549+ validateTranslationReferences ( { ...stack , ...bundle ( { all_leads : { label : 'A' } , my_leads : { label : 'M' } } ) } ) ,
550+ ) . toEqual ( [ ] ) ;
551+ // …and `default` is NOT legal here: the author named the view, so the
552+ // composer never falls back to `default`.
553+ const planted = validateTranslationReferences ( { ...stack , ...bundle ( { default : { label : 'D' } } ) } ) ;
554+ expect ( planted ) . toHaveLength ( 1 ) ;
555+ expect ( planted [ 0 ] . path ) . toBe ( 'translations[0].en.objects.crm_lead._views.default' ) ;
556+ } ) ;
557+
558+ it ( 'a default list collapsed into a `listViews` entry contributes that entry\'s key, not its own `name`' , ( ) => {
559+ // Composer fact 2 — the `examples/app-crm` shape: `list` is
560+ // signature-identical to `listViews.all` (`{type,label,columns}` equal),
561+ // so the two are ONE registry entry named `all`. `list.name` resolves to
562+ // nothing and must not be a legal bundle key.
563+ const collapsed = {
564+ objects : [ { name : 'crm_lead' , fields : { name : { type : 'text' } } } ] ,
565+ views : [
566+ {
567+ list : { type : 'grid' , name : 'all_leads' , data : { provider : 'object' , object : 'crm_lead' } } ,
568+ listViews : { all : { type : 'grid' , data : { provider : 'object' , object : 'crm_lead' } } } ,
569+ } ,
570+ ] ,
571+ } ;
572+ expect ( validateTranslationReferences ( { ...collapsed , ...bundle ( { all : { label : '全部' } } ) } ) ) . toEqual ( [ ] ) ;
573+ const findings = validateTranslationReferences ( { ...collapsed , ...bundle ( { all_leads : { label : '全部' } } ) } ) ;
574+ expect ( findings ) . toHaveLength ( 1 ) ;
575+ expect ( findings [ 0 ] . path ) . toBe ( 'translations[0].en.objects.crm_lead._views.all_leads' ) ;
576+ } ) ;
577+
578+ it ( 'a collision-renamed default list is legal under the renamed key' , ( ) => {
579+ // Composer fact 3: `listViews.default` claims `crm_lead.default` first,
580+ // so the nameless default list is renamed `crm_lead.default_2` — and the
581+ // rename IS the registry key, so it is what a bundle must spell.
582+ const collided = {
583+ objects : [ { name : 'crm_lead' , fields : { name : { type : 'text' } } } ] ,
584+ views : [
585+ {
586+ list : { type : 'grid' , data : { provider : 'object' , object : 'crm_lead' } } ,
587+ listViews : { default : { type : 'kanban' , data : { provider : 'object' , object : 'crm_lead' } } } ,
588+ } ,
589+ ] ,
590+ } ;
591+ expect (
592+ validateTranslationReferences ( { ...collided , ...bundle ( { default : { label : 'D' } , default_2 : { label : 'D2' } } ) } ) ,
593+ ) . toEqual ( [ ] ) ;
594+ const findings = validateTranslationReferences ( { ...collided , ...bundle ( { default_3 : { label : 'D3' } } ) } ) ;
595+ expect ( findings ) . toHaveLength ( 1 ) ;
596+ expect ( findings [ 0 ] . path ) . toBe ( 'translations[0].en.objects.crm_lead._views.default_3' ) ;
597+ } ) ;
598+
599+ it ( 'the default FORM contributes sections but no `_views` name — `_views.*` is a list convention' , ( ) => {
600+ // The composer does name the default form `crm_lead.form`, but the i18n
601+ // walker emits no `_views` entry for any form view, so a `_views.form`
602+ // key would be one nothing reads. Its `_sections` still resolve (#5415).
603+ const withForm = {
604+ objects : [ { name : 'crm_lead' , fields : { name : { type : 'text' } } } ] ,
605+ views : [
606+ {
607+ list : { type : 'grid' , label : 'All' , data : { provider : 'object' , object : 'crm_lead' } } ,
608+ form : {
609+ type : 'simple' ,
610+ data : { provider : 'object' , object : 'crm_lead' } ,
611+ sections : [ { name : 'contact_info' , label : 'Contact Info' } ] ,
612+ } ,
613+ } ,
614+ ] ,
615+ } ;
616+ expect (
617+ validateTranslationReferences ( {
618+ ...withForm ,
619+ translations : [
620+ {
621+ en : {
622+ objects : {
623+ crm_lead : { label : 'Lead' , _views : { default : { label : 'All' } } , _sections : { contact_info : { label : '联系方式' } } } ,
624+ } ,
625+ } ,
626+ } ,
627+ ] ,
628+ } ) ,
629+ ) . toEqual ( [ ] ) ;
630+ const findings = validateTranslationReferences ( { ...withForm , ...bundle ( { form : { label : 'Form' } } ) } ) ;
631+ expect ( findings ) . toHaveLength ( 1 ) ;
632+ expect ( findings [ 0 ] . path ) . toBe ( 'translations[0].en.objects.crm_lead._views.form' ) ;
633+ } ) ;
634+ } ) ;
635+
475636 it ( 'resolves views embedded on the object itself' , ( ) => {
476637 const findings = validateTranslationReferences ( {
477638 objects : [
@@ -646,6 +807,33 @@ describe('validateTranslationReferences — the showcase contact surface (#5415)
646807 expect ( findings ) . toEqual ( [ ] ) ;
647808 } , 60_000 ) ;
648809
810+ it ( 'accepts `_views.default` — the key this very surface ships, and the one it was told to ship (#6038)' , ( ) => {
811+ // The specimen behind #5164/#6038, on the real metadata rather than a
812+ // reduction: `ContactViews` declares a nameless default `list`, the CLI
813+ // i18n walker demands `objects.showcase_contact._views.default.label`
814+ // (#6124), and `examples/app-showcase` ships exactly that key. Before this
815+ // rule read the key from the composer it answered "no view of object
816+ // showcase_contact declares `default`" — one `os lint` run, two rules, no
817+ // author action that satisfied both. The control below keeps this honest:
818+ // `list`, the spelling the walker used to demand, is NOT legal.
819+ expect (
820+ validateTranslationReferences (
821+ showcaseContactStack ( [
822+ { 'zh-CN' : { objects : { showcase_contact : { _views : { default : { label : '联系人' } } } } } } ,
823+ ] ) ,
824+ ) ,
825+ ) . toEqual ( [ ] ) ;
826+
827+ const stale = validateTranslationReferences (
828+ showcaseContactStack ( [
829+ { 'zh-CN' : { objects : { showcase_contact : { _views : { list : { label : '联系人' } } } } } } ,
830+ ] ) ,
831+ ) ;
832+ expect ( stale ) . toHaveLength ( 1 ) ;
833+ expect ( stale [ 0 ] . path ) . toBe ( 'translations[0]["zh-CN"].objects.showcase_contact._views.list' ) ;
834+ expect ( stale [ 0 ] . hint ) . toContain ( 'default' ) ;
835+ } , 60_000 ) ;
836+
649837 it ( 'still reports a section name nothing declares, and names the real ones' , ( ) => {
650838 // The over-widening control: `contract` is a typo of `contact`, and
651839 // `who_is_this` is the LABEL of `formViews.create`'s unnamed section — an
0 commit comments