@@ -1595,3 +1595,88 @@ describe('discovery — routes.mcp (ADR-0036, #152)', () => {
15951595 expect ( body . routes . mcp ) . toBeUndefined ( ) ;
15961596 } ) ;
15971597} ) ;
1598+
1599+ // ──────────────────────────────────────────────────────────────────────────
1600+ // Metadata translation — envelope unwrapping
1601+ //
1602+ // Regression guard for the Setup-app i18n gap: `getMetaItem` returns an
1603+ // envelope `{ type, name, item, ... }` whose translatable document (and its
1604+ // `navigation` tree) is nested at `.item`. Translating the envelope's top
1605+ // level instead of the inner doc left every nav label in English. The
1606+ // single-item app route bypasses the HTTP cache (for per-user RBAC
1607+ // filtering), so it only ever sees the envelope shape.
1608+ // ──────────────────────────────────────────────────────────────────────────
1609+ describe ( 'RestServer metadata translation — envelope unwrap' , ( ) => {
1610+ // Minimal i18n service exposing one zh-CN bundle for the `setup` app.
1611+ const fakeI18n = {
1612+ getLocales : ( ) => [ 'zh-CN' ] ,
1613+ getDefaultLocale : ( ) => 'zh-CN' ,
1614+ getTranslations : ( locale : string ) =>
1615+ locale === 'zh-CN'
1616+ ? {
1617+ apps : {
1618+ setup : {
1619+ label : '系统设置' ,
1620+ navigation : {
1621+ group_configuration : { label : '配置' } ,
1622+ nav_settings_storage : { label : '文件存储' } ,
1623+ } ,
1624+ } ,
1625+ } ,
1626+ }
1627+ : undefined ,
1628+ } ;
1629+ const zhReq = { headers : { 'accept-language' : 'zh-CN' } } ;
1630+ // A fresh app document each call (helpers must not mutate the input).
1631+ const makeDoc = ( ) => ( {
1632+ name : 'setup' ,
1633+ label : 'Setup' ,
1634+ navigation : [
1635+ {
1636+ id : 'group_configuration' ,
1637+ type : 'group' ,
1638+ label : 'Configuration' ,
1639+ children : [ { id : 'nav_settings_storage' , type : 'url' , label : 'File Storage' } ] ,
1640+ } ,
1641+ ] ,
1642+ } ) ;
1643+
1644+ it ( 'translates the inner document of a getMetaItem envelope' , async ( ) => {
1645+ const rest = new RestServer ( createMockServer ( ) as any , createMockProtocol ( ) as any ) ;
1646+ const envelope = { type : 'app' , name : 'setup' , item : makeDoc ( ) , lock : null } ;
1647+ const out = await ( rest as any ) . translateMetaItem ( zhReq , 'app' , undefined , envelope , fakeI18n ) ;
1648+ // Envelope shape preserved …
1649+ expect ( out . type ) . toBe ( 'app' ) ;
1650+ expect ( out . name ) . toBe ( 'setup' ) ;
1651+ expect ( out . lock ) . toBeNull ( ) ;
1652+ // … and the nested doc — not the envelope top level — is translated.
1653+ expect ( out . item . label ) . toBe ( '系统设置' ) ;
1654+ expect ( out . item . navigation [ 0 ] . label ) . toBe ( '配置' ) ;
1655+ expect ( out . item . navigation [ 0 ] . children [ 0 ] . label ) . toBe ( '文件存储' ) ;
1656+ } ) ;
1657+
1658+ it ( 'still translates a bare (already-unwrapped) document' , async ( ) => {
1659+ const rest = new RestServer ( createMockServer ( ) as any , createMockProtocol ( ) as any ) ;
1660+ const out = await ( rest as any ) . translateMetaItem ( zhReq , 'app' , undefined , makeDoc ( ) , fakeI18n ) ;
1661+ expect ( out . label ) . toBe ( '系统设置' ) ;
1662+ expect ( out . navigation [ 0 ] . children [ 0 ] . label ) . toBe ( '文件存储' ) ;
1663+ } ) ;
1664+
1665+ it ( 'translates list responses in the `{ items: [...] }` envelope shape' , async ( ) => {
1666+ const rest = new RestServer ( createMockServer ( ) as any , createMockProtocol ( ) as any ) ;
1667+ // translateMetaItems resolves the i18n service itself; stub the lookup.
1668+ ( rest as any ) . resolveI18nService = async ( ) => fakeI18n ;
1669+ const listEnvelope = { items : [ { type : 'app' , name : 'setup' , item : makeDoc ( ) } ] } ;
1670+ const out = await ( rest as any ) . translateMetaItems ( zhReq , 'app' , undefined , listEnvelope ) ;
1671+ expect ( Array . isArray ( out . items ) ) . toBe ( true ) ;
1672+ expect ( out . items [ 0 ] . item . navigation [ 0 ] . children [ 0 ] . label ) . toBe ( '文件存储' ) ;
1673+ } ) ;
1674+
1675+ it ( 'translates a bare array of unwrapped documents' , async ( ) => {
1676+ const rest = new RestServer ( createMockServer ( ) as any , createMockProtocol ( ) as any ) ;
1677+ ( rest as any ) . resolveI18nService = async ( ) => fakeI18n ;
1678+ const out = await ( rest as any ) . translateMetaItems ( zhReq , 'app' , undefined , [ makeDoc ( ) ] ) ;
1679+ expect ( Array . isArray ( out ) ) . toBe ( true ) ;
1680+ expect ( out [ 0 ] . navigation [ 0 ] . children [ 0 ] . label ) . toBe ( '文件存储' ) ;
1681+ } ) ;
1682+ } ) ;
0 commit comments