@@ -1185,6 +1185,163 @@ describe('#21334 a container on another package\'s object never takes that packa
11851185 expect ( answers [ 1 ] , 'the unscoped kernel answers as env_local does' ) . toEqual ( answers [ 0 ] ) ;
11861186 } ) ;
11871187 } ) ;
1188+
1189+ /**
1190+ * #21558 — the save door refuses a view container saved under a name its
1191+ * own expansion produces.
1192+ *
1193+ * Measured on `origin/main` before this change, with this harness: the
1194+ * card's save, `{ name: 'showcase_task.default', object: 'showcase_task',
1195+ * list }` under `showcase_task.default`, was accepted on both kernels; the
1196+ * object door then listed nothing under that name (the container is the
1197+ * name's own row, so #21510's predicate keeps its expansion out, and a
1198+ * container is never enumerated) while the by-name read answered the raw
1199+ * container — no door answered a view item for the name. Triage's ruling:
1200+ * refuse it at the write door with a named error and a prescription (save
1201+ * the container under its object's name, or a view item under the
1202+ * expanded name); ⛔ no second own-row test in the readers.
1203+ *
1204+ * The refusal asks the readers' own expansion, so every member kind is
1205+ * covered as the expander places it. The two controls are the ruling's: a
1206+ * container under its object's name saves and expands as before, and a
1207+ * view item under an expanded name saves, as the sanctioned override.
1208+ */
1209+ describe ( '#21558 the save door refuses a view container saved under a name its own expansion produces' , ( ) => {
1210+ const withoutDiagnostics = ( item : any ) => {
1211+ if ( ! item || typeof item !== 'object' ) return item ;
1212+ const { _diagnostics : _drop , ...rest } = item ;
1213+ return rest ;
1214+ } ;
1215+ const saveIn = (
1216+ protocol : Protocol , name : string , item : unknown , organizationId ?: string , mode ?: 'draft' | 'publish' ,
1217+ ) => protocol . saveMetaItem ( { type : 'view' , name, item, ...scoped ( organizationId ) , ...( mode ? { mode } : { } ) } as any ) ;
1218+ const refusalOf = ( write : Promise < unknown > ) => write . then ( ( ) => null , ( e : any ) => e ) ;
1219+ /** The minimum a rejection pin asserts — the ADR-0112 envelope — plus the subjects it names. */
1220+ const expectRefused = ( error : any , saveName : string ) => {
1221+ expect ( error ) . toBeInstanceOf ( Error ) ;
1222+ expect ( { code : error ?. code , status : error ?. status } ) . toEqual ( { code : 'VALIDATION_ERROR' , status : 400 } ) ;
1223+ expect ( error . message , 'the refusal names the save name' ) . toContain ( `'${ saveName } '` ) ;
1224+ expect ( error . message , 'the prescription names the object\'s own name' ) . toContain ( `'${ TASK } '` ) ;
1225+ } ;
1226+ /** The doors answer `name` with the one item `expectItem` names, and the same item on both. */
1227+ const expectBothDoors = async (
1228+ protocol : Protocol , name : string , organizationId : string | undefined , expectItem : ( v : any ) => void ,
1229+ ) => {
1230+ const listed = named ( await objectDoor ( protocol , organizationId ) , name ) ;
1231+ expect ( listed , `exactly one item answers ${ name } on the object door` ) . toHaveLength ( 1 ) ;
1232+ expectItem ( listed [ 0 ] ) ;
1233+ const read = await byNameDoor ( protocol , name , organizationId ) ;
1234+ expectItem ( read ) ;
1235+ expect ( withoutDiagnostics ( read ) , `${ name } : the by-name read answers the item the object door lists` )
1236+ . toEqual ( withoutDiagnostics ( listed [ 0 ] ) ) ;
1237+ } ;
1238+
1239+ for ( const [ kernel , environmentId ] of KERNELS ) {
1240+ describe ( `on ${ kernel } ` , ( ) => {
1241+ for ( const organizationId of [ undefined , ORG ] ) {
1242+ const scope = organizationId ? 'organization-scoped' : 'environment-wide' ;
1243+ for ( const [ kind , m ] of Object . entries ( MEMBER_CASES ) ) {
1244+ it ( `${ scope } , member ${ kind } : a container saved under ${ m . shadows } , a name its own expansion produces, is refused VALIDATION_ERROR / 400; nothing is stored or registered` , async ( ) => {
1245+ const { protocol, rows, registry } = showcaseHarness ( environmentId ) ;
1246+ const body = { name : m . shadows , object : TASK , ...m . member } ;
1247+ expect ( expandViewContainer ( TASK , body ) . map ( ( vi ) => vi . name ) , 'the save name is its own expansion\'s' )
1248+ . toEqual ( [ m . shadows ] ) ;
1249+
1250+ expectRefused ( await refusalOf ( saveIn ( protocol , m . shadows , body , organizationId ) ) , m . shadows ) ;
1251+ expect ( [ ...rows . values ( ) ] . filter ( ( r ) => r . type === 'view' ) , 'no row is stored' ) . toEqual ( [ ] ) ;
1252+ expect (
1253+ registry . listItems ( 'view' ) . filter ( ( it ) => isAggregatedViewContainer ( it ) && it . name === m . shadows ) ,
1254+ 'no container is registered under the name' ,
1255+ ) . toEqual ( [ ] ) ;
1256+ // The doors answer exactly what they answered before the save.
1257+ await expectEveryPackagedNameIntact ( protocol , organizationId ) ;
1258+ } ) ;
1259+ }
1260+
1261+ it ( `${ scope } : the card's save is refused as a draft too, and no draft is stored` , async ( ) => {
1262+ const { protocol, rows } = showcaseHarness ( environmentId ) ;
1263+ const body = { name : DEFAULT , object : TASK , list : { label : 'SelfNamed' , type : 'grid' , columns : [ { field : 'title' } ] } } ;
1264+ expectRefused ( await refusalOf ( saveIn ( protocol , DEFAULT , body , organizationId , 'draft' ) ) , DEFAULT ) ;
1265+ expect ( [ ...rows . values ( ) ] . filter ( ( r ) => r . type === 'view' ) , 'no draft row is stored' ) . toEqual ( [ ] ) ;
1266+ } ) ;
1267+
1268+ it ( `${ scope } : a container saved under its object's name saves and expands as before` , async ( ) => {
1269+ const { protocol } = showcaseHarness ( environmentId ) ;
1270+ const overlay = {
1271+ name : TASK ,
1272+ list : { label : 'Overlay' , type : 'grid' , data, columns : [ { field : 'title' } ] } ,
1273+ listViews : { in_progress : { label : 'Overlay In Progress' , type : 'grid' , data, columns : [ { field : 'title' } ] } } ,
1274+ } ;
1275+ const saved = ( await saveIn ( protocol , TASK , overlay , organizationId ) ) as any ;
1276+ expect ( saved ?. success ) . toBe ( true ) ;
1277+ await expectBothDoors ( protocol , DEFAULT , organizationId , ( v ) => expect ( v ?. label ) . toBe ( 'Overlay' ) ) ;
1278+ await expectBothDoors ( protocol , `${ TASK } .in_progress` , organizationId , ( v ) => expect ( v ?. label ) . toBe ( 'Overlay In Progress' ) ) ;
1279+ } ) ;
1280+
1281+ it ( `${ scope } : a view item saved under an expanded name saves, as that name's sanctioned override` , async ( ) => {
1282+ const { protocol } = showcaseHarness ( environmentId ) ;
1283+ const item = {
1284+ name : DEFAULT , object : TASK , viewKind : 'list' , label : 'ByNameRow' ,
1285+ config : { type : 'grid' , data, columns : [ { field : 'title' } , { field : 'status' } ] } ,
1286+ } ;
1287+ const saved = ( await saveIn ( protocol , DEFAULT , item , organizationId ) ) as any ;
1288+ expect ( saved ?. success ) . toBe ( true ) ;
1289+ await expectBothDoors ( protocol , DEFAULT , organizationId , ( v ) => expect ( v ?. label ) . toBe ( 'ByNameRow' ) ) ;
1290+ } ) ;
1291+ }
1292+ } ) ;
1293+ }
1294+ } ) ;
1295+ } ) ;
1296+
1297+ /**
1298+ * #21558 on an object no code package ships: the refusal follows the readers'
1299+ * expansion, the expander's de-duplication included, so a container saved
1300+ * under the de-duplicated name its own expansion gave a member is refused
1301+ * too, and a container under its object's name still saves.
1302+ */
1303+ describe ( '#21558 a container under its own expanded name, on a runtime-authored object' , ( ) => {
1304+ async function saveCrm ( name : string , item : unknown ) {
1305+ const harness = makeStubEngine ( ) ;
1306+ const protocol = new ObjectStackProtocolImplementation ( harness . engine ) ;
1307+ const error = await protocol . saveMetaItem ( { type : 'view' , name, item } ) . then ( ( ) => null , ( e : any ) => e ) ;
1308+ const viewRows = Array . from ( harness . rows . values ( ) ) . filter ( ( r ) => r . type === 'view' ) ;
1309+ return { ...harness , protocol, error, viewRows } ;
1310+ }
1311+ const list = { label : 'All Leads' , type : 'grid' , columns : [ { field : 'name' } ] } ;
1312+ const other = { label : 'Other' , type : 'grid' , columns : [ { field : 'company' } ] } ;
1313+
1314+ for ( const [ saveName , member ] of [
1315+ [ 'crm_lead.default' , { list } ] ,
1316+ [ 'crm_lead.pipeline' , { listViews : { pipeline : list } } ] ,
1317+ // `listViews.default` takes `crm_lead.default` first, so the expander
1318+ // renames the bare `list` to `crm_lead.default_2`.
1319+ [ 'crm_lead.default_2' , { listViews : { default : list } , list : other } ] ,
1320+ ] as const ) {
1321+ it ( `saved under ${ saveName } : refused VALIDATION_ERROR / 400, nothing stored or registered` , async ( ) => {
1322+ const body = { name : saveName , object : 'crm_lead' , ...member } ;
1323+ expect ( expandViewContainer ( 'crm_lead' , body ) . map ( ( vi ) => vi . name ) , 'the save name is its own expansion\'s' )
1324+ . toContain ( saveName ) ;
1325+ const { error, viewRows, registered } = await saveCrm ( saveName , body ) ;
1326+ expect ( error ) . toBeInstanceOf ( Error ) ;
1327+ expect ( { code : error ?. code , status : error ?. status } ) . toEqual ( { code : 'VALIDATION_ERROR' , status : 400 } ) ;
1328+ expect ( viewRows ) . toEqual ( [ ] ) ;
1329+ expect ( registered . get ( 'view' ) ?. size ?? 0 ) . toBe ( 0 ) ;
1330+ } ) ;
1331+ }
1332+
1333+ it ( 'a container with no `name` is judged under the name the door stamps on it: refused under crm_lead.default' , async ( ) => {
1334+ const { error, viewRows } = await saveCrm ( 'crm_lead.default' , { object : 'crm_lead' , list } ) ;
1335+ expect ( { code : error ?. code , status : error ?. status } ) . toEqual ( { code : 'VALIDATION_ERROR' , status : 400 } ) ;
1336+ expect ( viewRows ) . toEqual ( [ ] ) ;
1337+ } ) ;
1338+
1339+ it ( 'CONTROL: the same container under its object\'s name saves, and its expansion fills crm_lead.default' , async ( ) => {
1340+ const { error, protocol } = await saveCrm ( 'crm_lead' , { name : 'crm_lead' , object : 'crm_lead' , list } ) ;
1341+ expect ( error ) . toBeNull ( ) ;
1342+ const listed : any = await protocol . getMetaItems ( { type : 'view' } ) ;
1343+ expect ( switcherMatches ( listed . items , 'crm_lead' ) . map ( ( v : any ) => [ v . name , v . label ] ) ) . toEqual ( [ [ 'crm_lead.default' , 'All Leads' ] ] ) ;
1344+ } ) ;
11881345} ) ;
11891346
11901347/**
0 commit comments