@@ -36,14 +36,20 @@ describe('showcase stack', () => {
3636} ) ;
3737
3838/**
39- * Static shadow of what SeedLoader + validation do at boot (#3415): for every
40- * object whose state_machine gates INSERT (`initialStates`), replay the seed
41- * datasets in declaration order and assert each record enters through a legal
42- * initial state and only moves along declared transitions. The fixture that
43- * silently lost 4/5 projects (target status written directly on insert) can
44- * never come back green.
39+ * Static shadow of the seed contract after #3433: a seed write is a curated
40+ * end-state fact, so the platform EXEMPTS it from the object's `state_machine`
41+ * rule — a project is seeded directly `active` / `on_hold` / `completed`
42+ * without walking the FSM up from `planned` (the three-phase walk workaround
43+ * of #3415 is gone). This guard pins the new contract for every object whose
44+ * state_machine gates INSERT (`initialStates`):
45+ * 1. every seeded value is still a state the FSM DECLARES (a curated fact,
46+ * not a typo — the exemption is not a license to write garbage); and
47+ * 2. the fixture actually EXERCISES the exemption by seeding ≥1 non-initial
48+ * state, so a regression back to "all rows enter as the initial state"
49+ * (a re-introduced walk, or a fixture that collapses the board to one
50+ * column) fails here. The #3433 failure was 1/5 projects surviving.
4551 */
46- describe ( 'seed data vs state machines (#3415 )' , ( ) => {
52+ describe ( 'seed data vs state machines (#3433 )' , ( ) => {
4753 const gated = ( stack . objects ?? [ ] ) . flatMap ( ( o : any ) =>
4854 ( o . validations ?? [ ] )
4955 . filter (
@@ -56,40 +62,46 @@ describe('seed data vs state machines (#3415)', () => {
5662 . map ( ( v : any ) => ( { object : o , rule : v } ) ) ,
5763 ) ;
5864
59- it ( 'covers the project status flow (the #3415 gate)' , ( ) => {
65+ it ( 'covers the project status flow (the #3433 gate)' , ( ) => {
6066 expect ( gated . map ( ( g : any ) => `${ g . object . name } .${ g . rule . field } ` ) ) . toContain ( 'showcase_project.status' ) ;
6167 } ) ;
6268
6369 for ( const { object, rule } of gated ) {
64- it ( `${ object . name } : seeded '${ rule . field } ' respects initialStates and transitions — including on replay ` , ( ) => {
70+ it ( `${ object . name } : seeded '${ rule . field } ' is FSM-exempt but stays within declared states (#3433) ` , ( ) => {
6571 const datasets = ShowcaseSeedData . filter ( ( d : any ) => d . object === object . name ) ;
6672 expect ( datasets . length ) . toBeGreaterThan ( 0 ) ;
67- const current = new Map < string , string > ( ) ;
68- // Round 1 = fresh boot; round 2 = replay against the walked state.
69- // Replay must also be violation-free (#3415 follow-up): `ignore`
70- // datasets skip existing rows wholesale, and re-walked hops must be
71- // legal transitions (which is what the reopen edge guarantees).
72- for ( const round of [ 1 , 2 ] ) {
73- for ( const ds of datasets as any [ ] ) {
74- for ( const rec of ds . records as any [ ] ) {
75- const key = String ( rec [ ds . externalId ?? 'name' ] ) ;
76- const next = rec [ rule . field ] ;
77- if ( ! current . has ( key ) ) {
78- // First appearance = INSERT. Seed inserts do NOT apply select
79- // defaults, so a gated field must be explicit AND legal.
80- expect ( next , `'${ key } ' (round ${ round } ) must seed '${ rule . field } ' explicitly` ) . toBeDefined ( ) ;
81- expect ( rule . initialStates , `'${ key } ' enters as '${ next } '` ) . toContain ( next ) ;
82- current . set ( key , next ) ;
83- } else {
84- if ( ds . mode === 'ignore' ) continue ; // existing rows untouched
85- if ( next === undefined || next === current . get ( key ) ) continue ; // no-op replay skips
86- const from = current . get ( key ) ! ;
87- expect ( rule . transitions ?. [ from ] ?? [ ] , `'${ key } ' (round ${ round } ) ${ from } → ${ next } ` ) . toContain ( next ) ;
88- current . set ( key , next ) ;
89- }
90- }
73+
74+ // Every state the FSM knows about — the legal value universe, derived
75+ // from the rule itself (no dependency on the field's option shape).
76+ const fsmStates = new Set < string > (
77+ [
78+ ...rule . initialStates ,
79+ ...Object . keys ( rule . transitions ?? { } ) ,
80+ ...Object . values ( rule . transitions ?? { } ) . flat ( ) ,
81+ ] . map ( String ) ,
82+ ) ;
83+
84+ const seeded = new Set < string > ( ) ;
85+ for ( const ds of datasets as any [ ] ) {
86+ for ( const rec of ds . records as any [ ] ) {
87+ const v = rec [ rule . field ] ;
88+ if ( v === undefined || v === null ) continue ;
89+ const key = String ( rec [ ds . externalId ?? 'name' ] ) ;
90+ // #3433: a seed value need NOT be an initialState (the FSM entry
91+ // guard is exempt), but it must be a state the machine declares.
92+ expect ( fsmStates , `'${ key } ' seeds '${ rule . field } =${ String ( v ) } '` ) . toContain ( String ( v ) ) ;
93+ seeded . add ( String ( v ) ) ;
9194 }
9295 }
96+
97+ // The exemption must actually be used: seed at least one state the FSM
98+ // entry point would reject on INSERT. Guards against a silent regression
99+ // to a planned-only fixture (or a re-introduced FSM walk).
100+ const nonInitial = [ ...seeded ] . filter ( ( v ) => ! rule . initialStates . includes ( v ) ) ;
101+ expect (
102+ nonInitial . length ,
103+ `${ object . name } seeds only initial states (${ [ ...seeded ] . join ( ', ' ) } ) — #3433 exemption unused` ,
104+ ) . toBeGreaterThan ( 0 ) ;
93105 } ) ;
94106 }
95107} ) ;
0 commit comments