2222
2323import { describe , expect , it , vi } from 'vitest' ;
2424import { ObjectKernel } from '@objectstack/core' ;
25+ import { ObjectQL } from '@objectstack/objectql' ;
26+ import { SETUP_APP , SETUP_NAV_CONTRIBUTIONS } from '@objectstack/platform-objects' ;
2527import type { IHttpRequest , IHttpResponse , IHttpServer , RouteHandler } from '@objectstack/spec/contracts' ;
2628import { SettingsServicePlugin } from './settings-service-plugin.js' ;
2729import type { SettingsService } from './settings-service.js' ;
@@ -64,6 +66,10 @@ async function boot(tenancy: Tenancy) {
6466 throw new Error ( 'tenancy backend unavailable' ) ;
6567 } ) ;
6668 }
69+ // Every manifest the plugin registers — its own, and the Setup entries it
70+ // contributes with an offered namespace.
71+ const registered : unknown [ ] = [ ] ;
72+ kernel . registerService ( 'manifest' , { register : ( m : unknown ) => { registered . push ( m ) ; } } ) ;
6773 const getService = kernel . getService . bind ( kernel ) ;
6874 const registerService = kernel . registerService . bind ( kernel ) ;
6975 const warnings : string [ ] = [ ] ;
@@ -109,7 +115,35 @@ async function boot(tenancy: Tenancy) {
109115 const getBranding = ( ) =>
110116 call ( 'GET /api/settings/:namespace' , { method : 'GET' , params : { namespace : 'branding' } as any } ) ;
111117
112- return { service, listed, putBranding, getBranding, warnings } ;
118+ return { service, listed, putBranding, getBranding, warnings, setupNavIds : ( ) => mergedSetupNavIds ( registered ) } ;
119+ }
120+
121+ /**
122+ * The Setup app's navigation as the registry serves it: the shell and
123+ * `@objectstack/platform-objects`' own contributions, plus whatever this boot
124+ * registered, folded by the real `applyNavContributions`. So the static half
125+ * (`SETUP_NAV_CONTRIBUTIONS` no longer carrying the entry) and the contributed
126+ * half are judged together, on the merged tree the console reads.
127+ */
128+ function mergedSetupNavIds ( registered : unknown [ ] ) : { ids : string [ ] ; configuration : string [ ] } {
129+ const engine = new ObjectQL ( ) ;
130+ engine . registry . logLevel = 'silent' ;
131+ engine . registerApp ( {
132+ id : 'com.objectstack.test.setup-shell' , namespace : 'sys' , version : '0.0.0' , type : 'plugin' , scope : 'system' ,
133+ name : 'Setup shell (fixture)' , apps : [ SETUP_APP ] , navigationContributions : SETUP_NAV_CONTRIBUTIONS ,
134+ } ) ;
135+ for ( const m of registered ) engine . registerApp ( m ) ;
136+ const ids : string [ ] = [ ] ;
137+ const walk = ( items : any [ ] | undefined ) => {
138+ for ( const item of items ?? [ ] ) {
139+ if ( item ?. id ) ids . push ( item . id ) ;
140+ walk ( item ?. children ) ;
141+ }
142+ } ;
143+ const navigation = ( engine . registry . getApp ( 'setup' ) as { navigation ?: any [ ] } | undefined ) ?. navigation ;
144+ walk ( navigation ) ;
145+ const group = ( navigation ?? [ ] ) . find ( ( item ) => item ?. id === 'group_configuration' ) ;
146+ return { ids, configuration : ( group ?. children ?? [ ] ) . map ( ( item : any ) => item ?. id ) } ;
113147}
114148
115149describe ( 'the Branding namespace under a walled posture (#24766)' , ( ) => {
@@ -137,6 +171,13 @@ describe('the Branding namespace under a walled posture (#24766)', () => {
137171 expect ( state . status ) . toBe ( 404 ) ;
138172 expect ( state . body . error . code ) . toBe ( 'UNKNOWN_NAMESPACE' ) ;
139173 } ) ;
174+
175+ it ( 'Setup serves no Branding entry, while its sibling settings entries stay' , async ( ) => {
176+ const { ids } = ( await boot ( { kind : 'service' , posture } ) ) . setupNavIds ( ) ;
177+ expect ( ids ) . not . toContain ( 'nav_settings_branding' ) ;
178+ // Control: the fold is real and the Configuration group is populated.
179+ expect ( ids ) . toEqual ( expect . arrayContaining ( [ 'nav_settings_company' , 'nav_settings_localization' ] ) ) ;
180+ } ) ;
140181 } ) ;
141182 }
142183} ) ;
@@ -158,6 +199,14 @@ describe('the Branding namespace where no wall stands (#24766)', () => {
158199 expect ( read . status ) . toBe ( 200 ) ;
159200 expect ( read . body . data . values . workspace_name ) . toMatchObject ( { value : 'Acme' , source : 'tenant' } ) ;
160201 } ) ;
202+
203+ it ( `${ label } : Setup serves the Branding entry exactly once, in Configuration` , async ( ) => {
204+ const { ids, configuration } = ( await boot ( tenancy ) ) . setupNavIds ( ) ;
205+ expect ( ids . filter ( ( id ) => id === 'nav_settings_branding' ) ) . toHaveLength ( 1 ) ;
206+ // In the Configuration group, after the platform-objects entries.
207+ expect ( configuration ) . toContain ( 'nav_settings_branding' ) ;
208+ expect ( configuration . indexOf ( 'nav_settings_branding' ) ) . toBeGreaterThan ( configuration . indexOf ( 'nav_settings_feature_flags' ) ) ;
209+ } ) ;
161210 }
162211} ) ;
163212
0 commit comments