1- import { test as base , describe , expect } from 'vitest'
1+ import { afterEach , test as base , beforeEach , describe , expect , vi } from 'vitest'
22
33import type { QueryExecutor } from '@crowd/database'
44import { withQx } from '@crowd/test-kit/db'
@@ -40,6 +40,18 @@ const test = withQx(base)
4040
4141const EPOCH = new Date ( '1970-01-01' ) . toISOString ( )
4242
43+ /** Frozen "now" so prepare() dateEnd boundaries stay deterministic. */
44+ const NOW = '2024-06-15T00:00:00.000Z'
45+
46+ beforeEach ( ( ) => {
47+ vi . useFakeTimers ( { toFake : [ 'Date' ] } )
48+ vi . setSystemTime ( new Date ( NOW ) )
49+ } )
50+
51+ afterEach ( ( ) => {
52+ vi . useRealTimers ( )
53+ } )
54+
4355const SEGMENT_TREES = withSegmentDefaults ( [
4456 {
4557 name : 'Cloud Native Computing Foundation' ,
@@ -104,7 +116,7 @@ function expectTimeline(
104116 expected : Array < {
105117 organizationId : string | null
106118 dateStart : string
107- dateEnd : unknown
119+ dateEnd : string | null
108120 segmentId ?: string | null
109121 matchEmailDomain ?: string | null
110122 } > ,
@@ -149,9 +161,7 @@ describe('prepareMemberOrganizationAffiliationTimeline', () => {
149161
150162 const timeline = await prepareMemberOrganizationAffiliationTimeline ( qx , member . id )
151163
152- expectTimeline ( baseItems ( timeline ) , [
153- { organizationId : null , dateStart : EPOCH , dateEnd : expect . any ( String ) } ,
154- ] )
164+ expectTimeline ( baseItems ( timeline ) , [ { organizationId : null , dateStart : EPOCH , dateEnd : NOW } ] )
155165 expect ( manualItems ( timeline ) ) . toHaveLength ( 0 )
156166 expect ( emailItems ( timeline ) ) . toHaveLength ( 0 )
157167 } )
@@ -176,7 +186,7 @@ describe('prepareMemberOrganizationAffiliationTimeline', () => {
176186 const timeline = await prepareMemberOrganizationAffiliationTimeline ( qx , member . id )
177187
178188 expectTimeline ( baseItems ( timeline ) , [
179- { organizationId : org . id , dateStart : EPOCH , dateEnd : expect . any ( String ) } ,
189+ { organizationId : org . id , dateStart : EPOCH , dateEnd : NOW } ,
180190 ] )
181191 } )
182192
@@ -206,17 +216,24 @@ describe('prepareMemberOrganizationAffiliationTimeline', () => {
206216 ] )
207217
208218 const timeline = await prepareMemberOrganizationAffiliationTimeline ( qx , member . id )
209- const base = baseItems ( timeline )
210- const orgIds = base . map ( ( item ) => item . organizationId )
211219
212- expect ( orgIds [ 0 ] ) . toBe ( undated . id )
213- expect ( base [ 0 ] . dateStart ) . toBe ( EPOCH )
214- expect ( orgIds ) . toContain ( employer . id )
215- expect ( base . find ( ( item ) => item . organizationId === employer . id ) ?. dateStart ) . toBe (
216- '2017-07-01T00:00:00.000Z' ,
217- )
218- // Dated stint ended — undated fills the post-career gap through now as well.
219- expect ( orgIds [ orgIds . length - 1 ] ) . toBe ( undated . id )
220+ expectTimeline ( baseItems ( timeline ) , [
221+ {
222+ organizationId : undated . id ,
223+ dateStart : EPOCH ,
224+ dateEnd : '2017-06-30T00:00:00.000Z' ,
225+ } ,
226+ {
227+ organizationId : employer . id ,
228+ dateStart : '2017-07-01T00:00:00.000Z' ,
229+ dateEnd : '2020-01-01T00:00:00.000Z' ,
230+ } ,
231+ {
232+ organizationId : undated . id ,
233+ dateStart : '2020-01-02T00:00:00.000Z' ,
234+ dateEnd : null ,
235+ } ,
236+ ] )
220237 } )
221238
222239 test ( 'gap between jobs is filled by the undated fallback' , async ( { qx } ) => {
@@ -386,9 +403,7 @@ describe('prepareMemberOrganizationAffiliationTimeline', () => {
386403
387404 const timeline = await prepareMemberOrganizationAffiliationTimeline ( qx , member . id )
388405
389- expectTimeline ( baseItems ( timeline ) , [
390- { organizationId : null , dateStart : EPOCH , dateEnd : expect . any ( String ) } ,
391- ] )
406+ expectTimeline ( baseItems ( timeline ) , [ { organizationId : null , dateStart : EPOCH , dateEnd : NOW } ] )
392407 } )
393408
394409 test ( 'primary undated MO wins over other undated as fallback' , async ( { qx } ) => {
@@ -426,7 +441,7 @@ describe('prepareMemberOrganizationAffiliationTimeline', () => {
426441 const timeline = await prepareMemberOrganizationAffiliationTimeline ( qx , member . id )
427442
428443 expectTimeline ( baseItems ( timeline ) , [
429- { organizationId : orgB . id , dateStart : EPOCH , dateEnd : expect . any ( String ) } ,
444+ { organizationId : orgB . id , dateStart : EPOCH , dateEnd : NOW } ,
430445 ] )
431446 expect ( baseItems ( timeline ) . every ( ( item ) => item . organizationId !== orgA . id ) ) . toBe ( true )
432447 } )
@@ -753,7 +768,7 @@ describe('prepareMemberOrganizationAffiliationTimeline', () => {
753768 {
754769 organizationId : null ,
755770 dateStart : '2024-02-02T00:00:00.000Z' ,
756- dateEnd : expect . any ( String ) ,
771+ dateEnd : NOW ,
757772 } ,
758773 ] )
759774 } )
@@ -800,7 +815,7 @@ describe('prepareMemberOrganizationAffiliationTimeline', () => {
800815 {
801816 organizationId : null ,
802817 dateStart : '2023-01-02T00:00:00.000Z' ,
803- dateEnd : expect . any ( String ) ,
818+ dateEnd : NOW ,
804819 } ,
805820 ] )
806821 } )
@@ -880,7 +895,7 @@ describe('prepareMemberOrganizationAffiliationTimeline', () => {
880895 {
881896 organizationId : null ,
882897 dateStart : '2023-01-02T00:00:00.000Z' ,
883- dateEnd : expect . any ( String ) ,
898+ dateEnd : NOW ,
884899 } ,
885900 ] )
886901 } )
@@ -959,7 +974,7 @@ describe('prepareMemberOrganizationAffiliationTimeline', () => {
959974 {
960975 organizationId : null ,
961976 dateStart : '2022-01-02T00:00:00.000Z' ,
962- dateEnd : expect . any ( String ) ,
977+ dateEnd : NOW ,
963978 } ,
964979 ] )
965980 } )
@@ -1347,4 +1362,32 @@ describe('refreshMemberOrganizationAffiliations', () => {
13471362 expect ( await countByOrg ( qx , member . id , kubernetes . id , company . id ) ) . toBe ( 16 ) // 10 GH + 6 @intel.com
13481363 expect ( await countByOrg ( qx , member . id , kubernetes . id , university . id ) ) . toBe ( 8 ) // @nyu .edu
13491364 } )
1365+
1366+ test ( 'clears stale organizationId when timeline resolves to null' , async ( { qx } ) => {
1367+ const [ staleOrg ] = await createOrganizations (
1368+ qx ,
1369+ withOrganizationDefaults ( [ { displayName : 'Stale Co' } ] ) ,
1370+ )
1371+ const { member, identities } = await createMemberWithIdentity ( qx , { value : 'no-employer' } )
1372+ const { kubernetes } = await createLeafSegments ( qx )
1373+
1374+ // No member organizations → prepare yields a null fallback for the whole timeline.
1375+ const seeded = await createActivityRelations (
1376+ qx ,
1377+ generateActivityRelations ( {
1378+ memberId : member . id ,
1379+ segmentId : kubernetes . id ,
1380+ identities,
1381+ count : 20 ,
1382+ timestamp : { from : '2021-01-01T00:00:00.000Z' , to : '2021-06-01T00:00:00.000Z' } ,
1383+ } ) . map ( ( row ) => ( { ...row , organizationId : staleOrg . id } ) ) ,
1384+ )
1385+
1386+ expect ( await countByOrg ( qx , member . id , kubernetes . id , staleOrg . id ) ) . toBe ( seeded . length )
1387+
1388+ await refreshMemberOrganizationAffiliations ( qx , member . id )
1389+
1390+ expect ( await countByOrg ( qx , member . id , kubernetes . id , staleOrg . id ) ) . toBe ( 0 )
1391+ expect ( await countByOrg ( qx , member . id , kubernetes . id , null ) ) . toBe ( seeded . length )
1392+ } )
13501393} )
0 commit comments