2121 * `@objectstack/metadata-protocol`'s `dist/`, so a change there is read here
2222 * after that package is rebuilt.
2323 */
24- import { afterEach , beforeEach , describe , expect , it } from 'vitest' ;
24+ import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest' ;
2525import type { ServiceObject } from '@objectstack/spec/data' ;
2626import {
2727 ObjectStackProtocolImplementation ,
2828 isServedOverrideRowOf ,
2929 resetEnvWritableMetadataTypes ,
3030 storedRowBindingOf ,
31+ SysMetadataRepository ,
3132} from '@objectstack/metadata-protocol' ;
3233import { SysMetadataAuditObject , SysMetadataCommitObject , SysMetadataHistoryObject } from '@objectstack/metadata-core' ;
3334import { ObjectQL } from './engine.js' ;
@@ -37,6 +38,8 @@ const PAIR = 'com.example.showcase.custom';
3738const DEPENDENT = 'com.default.acme-custom' ;
3839const FLOW = 'showcase_task_escalate' ;
3940const DECLARATION = { package : BASE , basedOn : '1.0.0' } as const ;
41+ const MAPPING = 'showcase_import_map' ;
42+ const VIEW = 'showcase_task.default' ;
4043
4144const sysMetadataObject : ServiceObject = {
4245 name : 'sys_metadata' ,
@@ -159,10 +162,15 @@ function baseManifest(): Record<string, unknown> {
159162 fields : { title : { name : 'title' , label : 'title' , type : 'text' } } ,
160163 } ] ,
161164 flows : [ flowBody ( 'SHIPPED' ) ] ,
165+ mappings : [ { name : MAPPING , label : 'Import' , sourceFormat : 'csv' , targetObject : 'showcase_task' , fieldMapping : [ ] } ] ,
166+ views : [ {
167+ object : 'showcase_task' ,
168+ list : { label : 'All Tasks' , type : 'grid' , data : { provider : 'object' , object : 'showcase_task' } , columns : [ { field : 'title' } ] } ,
169+ } ] ,
162170 } ;
163171}
164172
165- async function boot ( ) {
173+ async function boot ( environmentId ?: string ) {
166174 const engine = new ObjectQL ( ) ;
167175 const { driver, stores } = makeMemoryDriver ( ) ;
168176 engine . registerDriver ( driver , true ) ;
@@ -172,7 +180,7 @@ async function boot() {
172180 engine . registry . registerObject ( o as any , 'test-package' ) ;
173181 }
174182 engine . registerApp ( baseManifest ( ) ) ;
175- const protocol = new ObjectStackProtocolImplementation ( engine , ( ) => new Map ( ) ) ;
183+ const protocol = new ObjectStackProtocolImplementation ( engine , ( ) => new Map ( ) , environmentId ) ;
176184 const rows = ( ) => Array . from ( stores . get ( 'sys_metadata' ) ?. values ( ) ?? [ ] )
177185 . filter ( ( r ) => r . type === 'flow' && r . name === FLOW ) ;
178186 return { engine, protocol, rows } ;
@@ -286,3 +294,51 @@ describe('[#28412] a declared .custom flow override is the packaged flow, at eve
286294 expect ( await listed ( protocol , 'execution' ) ) . toEqual ( [ [ BASE , 'SHIPPED' ] ] ) ;
287295 } ) ;
288296} ) ;
297+
298+ /**
299+ * [#28412, contract review 6559358939] The pair-row removal is admitted for
300+ * `flow` alone — the one type whose ADR-0146 D9 conditions this change
301+ * delivers. Every other open override type keeps the removal verdict and the
302+ * receipt intent it has on main: a `mapping` (no overlay channel, outside the
303+ * #6960 carve-out) is still refused, and a `view` (overlay-capable) is still
304+ * removed as an `override-artifact` delete.
305+ */
306+ describe ( '[#28412] the pair-row removal is flow-only: other open types keep main\'s verdict' , ( ) => {
307+ const storePairRow = ( engine : ObjectQL , type : string , name : string , body : Record < string , unknown > ) =>
308+ engine . insert ( 'sys_metadata' , {
309+ type, name, organization_id : null , package_id : PAIR ,
310+ metadata : JSON . stringify ( { ...body , overrides : { ...DECLARATION } } ) , state : 'active' , version : 1 ,
311+ } , { context : { isSystem : true } } ) ;
312+ const rowsOf = async ( engine : ObjectQL , type : string , name : string ) =>
313+ ( ( await engine . find ( 'sys_metadata' , { where : { type, name } , context : { isSystem : true } } ) ) as any [ ] ) ;
314+
315+ for ( const environmentId of [ undefined , 'env_1' ] ) {
316+ const kernel = environmentId ? 'environment' : 'host-config' ;
317+ it ( `a mapping's pair-row delete naming the pair still answers 403 NOT_OVERRIDABLE, and the row stays (${ kernel } kernel)` , async ( ) => {
318+ const { protocol, engine } = await boot ( environmentId ) ;
319+ await install ( protocol , PAIR ) ;
320+ await storePairRow ( engine , 'mapping' , MAPPING , { name : MAPPING , label : 'Ours' , sourceFormat : 'csv' , targetObject : 'showcase_task' , fieldMapping : [ ] } ) ;
321+ await expect ( protocol . deleteMetaItem ( { type : 'mapping' , name : MAPPING , packageId : PAIR } as any ) )
322+ . rejects . toMatchObject ( { code : 'NOT_OVERRIDABLE' , status : 403 } ) ;
323+ expect ( ( await rowsOf ( engine , 'mapping' , MAPPING ) ) . map ( ( r ) => r . package_id ) ) . toEqual ( [ PAIR ] ) ;
324+ } ) ;
325+ }
326+
327+ it ( 'a view\'s pair-row delete naming the pair is removed as an override-artifact delete, as on main' , async ( ) => {
328+ const { protocol, engine } = await boot ( ) ;
329+ await install ( protocol , PAIR ) ;
330+ await storePairRow ( engine , 'view' , VIEW , {
331+ name : VIEW , object : 'showcase_task' , viewKind : 'list' , label : 'Ours' ,
332+ config : { type : 'grid' , data : { provider : 'object' , object : 'showcase_task' } , columns : [ { field : 'title' } ] } ,
333+ } ) ;
334+ const removal = vi . spyOn ( SysMetadataRepository . prototype , 'delete' ) ;
335+ try {
336+ await protocol . deleteMetaItem ( { type : 'view' , name : VIEW , packageId : PAIR } as any ) ;
337+ expect ( removal ) . toHaveBeenCalledTimes ( 1 ) ;
338+ expect ( removal . mock . calls [ 0 ] [ 1 ] ) . toMatchObject ( { intent : 'override-artifact' , packageId : PAIR } ) ;
339+ } finally {
340+ removal . mockRestore ( ) ;
341+ }
342+ expect ( ( await rowsOf ( engine , 'view' , VIEW ) ) . filter ( ( r ) => r . state === 'active' ) ) . toEqual ( [ ] ) ;
343+ } ) ;
344+ } ) ;
0 commit comments