@@ -65,6 +65,16 @@ const T = 1_790_000_000_000;
6565/** A reading of the real clock, used only as a seed: the drained walk reads the real clock itself. */
6666const SEED_NOW = Date . now ( ) ;
6767
68+ /**
69+ * A folder at the top of this machine's file system, spelled as a scan root is spelled here: `/fz` on
70+ * macOS and Linux, `D:\fz` on Windows (the working folder's drive). Every scan root on Windows has
71+ * a drive or a share — sanitizePath resolves it — and a store reads its separator off that
72+ * (separatorOf). `path.join(path.sep, …)` gave Windows the drive-less `\fz`, which a store reads as
73+ * "/" while the trees here join their paths with "\": lookups below the top missed, and the packed
74+ * store spelled paths no rule names (Windows CI run 38345032466).
75+ */
76+ const absRoot = ( ...names : string [ ] ) : string => path . resolve ( path . sep , ...names ) ;
77+
6878const STORES : ReadonlyArray < readonly [ string , ( tree : FileNode ) => ScanStore ] > = [
6979 [ 'object store' , ( tree ) => ObjectScanStore . wrap ( tree ) ] ,
7080 [ 'packed store' , ( tree ) => buildStoreFromTree ( tree ) ] ,
@@ -265,11 +275,14 @@ const immediate = (): Promise<void> => Promise.resolve();
265275/* ──────────────────────── the same answer, byte for byte ──────────────────────── */
266276
267277test ( 'the sliced walk and the drained walk give the pre-change answer, observer order included, on fuzz trees with every rule kind' , async ( ) => {
268- const fired = new Set < string > ( ) ;
278+ // The rules each kind of store fired, apart: a store whose paths no rule names (a drive-less root
279+ // read with the wrong separator, as on the Windows runner) passed the differential while the other
280+ // store fired every rule for it.
281+ const fired = new Map < string , Set < string > > ( STORES . map ( ( [ kind ] ) => [ kind , new Set < string > ( ) ] ) ) ;
269282 for ( const platform of [ 'linux' , 'darwin' , 'win32' ] as const ) {
270283 await withPlatform ( platform , async ( ) => {
271284 for ( let seed = 1 ; seed <= 24 ; seed ++ ) {
272- const root = path . join ( path . sep , 'fz' , `root${ seed } ` ) ;
285+ const root = absRoot ( 'fz' , `root${ seed } ` ) ;
273286 const catalog = testCatalog ( root ) ;
274287
275288 // The sliced walk, on a clock of its own: thresholds land on files.
@@ -279,7 +292,7 @@ test('the sliced walk and the drained walk give the pre-change answer, observer
279292 const label = `${ platform } , seed ${ seed } , ${ v . label } , ${ kind } ` ;
280293 const want = recorder ( ) ;
281294 const oracle = oracleCleanupSuggestions ( make ( exact ) , v . ignore , catalog , want . observe , v . suppressed , T ) ;
282- for ( const g of oracle ) fired . add ( g . id ) ;
295+ for ( const g of oracle ) fired . get ( kind ) ?. add ( g . id ) ;
283296 const got = recorder ( ) ;
284297 const sliced = await collectCleanupSuggestionsAsync ( make ( exact ) , v . ignore , catalog , got . observe , v . suppressed , {
285298 now : ( ) => T , sliceNodes : 5 , yieldTurn : immediate ,
@@ -296,7 +309,7 @@ test('the sliced walk and the drained walk give the pre-change answer, observer
296309 const label = `${ platform } , seed ${ seed + 1000 } , ${ v . label } , ${ kind } ` ;
297310 const want = recorder ( ) ;
298311 const oracle = oracleCleanupSuggestions ( make ( loose ) , v . ignore , catalog , want . observe , v . suppressed ) ;
299- for ( const g of oracle ) fired . add ( g . id ) ;
312+ for ( const g of oracle ) fired . get ( kind ) ?. add ( g . id ) ;
300313 const got = recorder ( ) ;
301314 const drained = collectCleanupSuggestions ( make ( loose ) , v . ignore , catalog , got . observe , v . suppressed ) ;
302315 assert . equal ( JSON . stringify ( drained ) , JSON . stringify ( oracle ) , `${ label } : the drained walk answers what the old walk answered` ) ;
@@ -307,7 +320,9 @@ test('the sliced walk and the drained walk give the pre-change answer, observer
307320 } ) ;
308321 }
309322 // A differential that never fires a rule proves nothing about that rule.
310- assert . deepEqual ( [ ...fired ] . sort ( ) , [ ...RULE_IDS ] . sort ( ) , 'the fuzz trees fired every rule of the test catalog' ) ;
323+ for ( const [ kind , ids ] of fired ) {
324+ assert . deepEqual ( [ ...ids ] . sort ( ) , [ ...RULE_IDS ] . sort ( ) , `${ kind } : the fuzz trees fired every rule of the test catalog` ) ;
325+ }
311326} ) ;
312327
313328test ( 'the shipped rule packs give the pre-change answer through the rewritten walk' , async ( ) => {
@@ -327,11 +342,11 @@ test('the shipped rule packs give the pre-change answer through the rewritten wa
327342} ) ;
328343
329344test ( 'a U+0130 location is found wherever case folds, and only there' , async ( ) => {
330- const root = path . join ( path . sep , 'dotted' ) ;
345+ const root = absRoot ( 'dotted' ) ;
331346 const catalog = testCatalog ( root ) ;
332347 // The rule says "İstanbul/Cache"; the disk says "i̇stanbul/cache": one code unit longer, the same folded.
333348 const at = path . join ( root , 'i̇stanbul' ) ;
334- const tree = dirAt ( path . sep , 'dotted' , [ dirAt ( root , 'i̇stanbul' , [ dirAt ( at , 'cache' , [ fileAt ( path . join ( at , 'cache' ) , 'x.bin' , 10 , 0 ) ] ) ] ) ] ) ;
349+ const tree = dirAt ( path . dirname ( root ) , 'dotted' , [ dirAt ( root , 'i̇stanbul' , [ dirAt ( at , 'cache' , [ fileAt ( path . join ( at , 'cache' ) , 'x.bin' , 10 , 0 ) ] ) ] ) ] ) ;
335350 for ( const [ platform , expected ] of [ [ 'darwin' , 1 ] , [ 'win32' , 1 ] , [ 'linux' , 0 ] ] as const ) {
336351 await withPlatform ( platform , async ( ) => {
337352 for ( const [ kind , make ] of STORES ) {
@@ -350,7 +365,7 @@ test('the Autopilot selects through the rewritten matcher exactly what it select
350365 { kind : 'suggestion' , groupIds : [ 's-old' , 's-small' , 'l-browser' , 'l-dotted' , 'p-rust' , 'p-maven' , 'p-web' ] } ,
351366 ] ;
352367 for ( let seed = 1 ; seed <= 16 ; seed ++ ) {
353- const root = path . join ( path . sep , 'ap' , `root${ seed } ` ) ;
368+ const root = absRoot ( 'ap' , `root${ seed } ` ) ;
354369 const catalog = testCatalog ( root ) ;
355370 const tree = fuzzTree ( seed , root , 700 , nearThresholds ( T ) ) ;
356371 for ( const [ kind , make ] of STORES ) {
@@ -380,7 +395,7 @@ function wideTree(root: string, folderCount: number, filesEach: number): { tree:
380395
381396test ( 'the walk hands the loop back once per slice of 16,384 children, and a turn queued before it runs before it answers' , async ( ) => {
382397 assert . equal ( CLEANUP_SLICE_NODES , 16_384 , 'the slice the plan fixes' ) ;
383- const { tree, examined } = wideTree ( path . join ( path . sep , 'wide' ) , 50 , 1_000 ) ;
398+ const { tree, examined } = wideTree ( absRoot ( 'wide' ) , 50 , 1_000 ) ;
384399 for ( const [ kind , make ] of STORES ) {
385400 let yields = 0 ;
386401 const yieldTurn = ( ) : Promise < void > => new Promise < void > ( ( resolve ) => { yields ++ ; setImmediate ( resolve ) ; } ) ;
@@ -414,10 +429,10 @@ test('a store other than the packed one answers its own child paths, as a legacy
414429} ) ;
415430
416431test ( 'each name is read once, and a folder\'s names again only when a sibling-gated rule names one of its children' , ( ) => {
417- const root = path . join ( path . sep , 'names' ) ;
432+ const root = absRoot ( 'names' ) ;
418433 const a = path . join ( root , 'a' ) ;
419434 const b = path . join ( root , 'b' ) ;
420- const tree = dirAt ( path . sep , 'names' , [
435+ const tree = dirAt ( path . dirname ( root ) , 'names' , [
421436 dirAt ( root , 'a' , Array . from ( { length : 5 } , ( _ , i ) => fileAt ( a , `f${ i } .dat` , 10 , 0 ) ) ) ,
422437 dirAt ( root , 'b' , [ dirAt ( b , 'target' , [ fileAt ( path . join ( b , 'target' ) , 'o.rlib' , 10 , 0 ) ] ) , fileAt ( b , 'Cargo.toml' , 1 , 0 ) , fileAt ( b , 'x.dat' , 1 , 0 ) , fileAt ( b , 'y.dat' , 1 , 0 ) ] ) ,
423438 ] ) ;
@@ -497,7 +512,7 @@ const MUTABLE_STORES: ReadonlyArray<readonly [string, (tree: FileNode) => ScanSt
497512
498513test ( 'a store changed during a walk is walked again, and the answer is the changed store\'s' , async ( ) => {
499514 for ( const [ kind , make ] of MUTABLE_STORES ) {
500- const store = make ( projectsTree ( path . join ( path . sep , 'changing' ) ) ) ;
515+ const store = make ( projectsTree ( absRoot ( 'changing' ) ) ) ;
501516 const { result, yieldsPerAttempt, heard } = await walkWhileChanging ( store , growFirstClaim , ( turn ) => turn === 1 ) ;
502517 assert . equal ( result . attempts , 2 , `${ kind } : the change on the first turn started a second walk` ) ;
503518 assert . equal ( yieldsPerAttempt . length , 2 , `${ kind } : two attempts began` ) ;
@@ -512,7 +527,7 @@ test('a store changed during a walk is walked again, and the answer is the chang
512527test ( 'a store that keeps changing is walked at most three times, the last in one turn' , async ( ) => {
513528 assert . equal ( CLEANUP_MAX_ATTEMPTS , 3 , 'U-D9: at most 3 attempts' ) ;
514529 for ( const [ kind , make ] of MUTABLE_STORES ) {
515- const store = make ( projectsTree ( path . join ( path . sep , 'storm' ) ) ) ;
530+ const store = make ( projectsTree ( absRoot ( 'storm' ) ) ) ;
516531 const { result, yieldsPerAttempt } = await walkWhileChanging ( store , growFirstClaim , ( ) => true ) ;
517532 assert . equal ( result . attempts , CLEANUP_MAX_ATTEMPTS , `${ kind } : a change on every turn ends at the third attempt` ) ;
518533 assert . deepEqual ( yieldsPerAttempt , [ 1 , 1 , 0 ] , `${ kind } : the first two gave up at their first turn; the last took none` ) ;
@@ -524,7 +539,7 @@ test('a wrapped object store counts its edits: the memo answers it once per vers
524539 resetCleanupMemoForTests ( ) ;
525540 setCleanupMemoOptionsForTests ( { yieldTurn : immediate , sliceNodes : 4 } ) ;
526541 try {
527- const store = ObjectScanStore . wrap ( projectsTree ( path . join ( path . sep , 'wrapped' ) ) ) ;
542+ const store = ObjectScanStore . wrap ( projectsTree ( absRoot ( 'wrapped' ) ) ) ;
528543 const catalog = loadRuleCatalog ( ) ;
529544 for ( let edits = 0 ; edits <= 3 ; edits ++ ) {
530545 if ( edits > 0 ) growFirstClaim ( store , edits ) ;
@@ -543,7 +558,7 @@ test('a second wrap of an edited tree starts at version 0 again, and is answered
543558 resetCleanupMemoForTests ( ) ;
544559 setCleanupMemoOptionsForTests ( { yieldTurn : immediate , sliceNodes : 4 } ) ;
545560 try {
546- const tree = projectsTree ( path . join ( path . sep , 'rewrapped' ) ) ;
561+ const tree = projectsTree ( absRoot ( 'rewrapped' ) ) ;
547562 const catalog = loadRuleCatalog ( ) ;
548563 const first = ObjectScanStore . wrap ( tree ) ;
549564 const before = JSON . stringify ( await cleanupSuggestionsFor ( first , [ ] , catalog , [ ] ) ) ;
@@ -567,7 +582,7 @@ test('two requests at once make one walk; a changed size, ignore list, note list
567582 resetCleanupMemoForTests ( ) ;
568583 setCleanupMemoOptionsForTests ( { yieldTurn : immediate , sliceNodes : 3 } ) ;
569584 try {
570- const root = path . join ( path . sep , 'memo' ) ;
585+ const root = absRoot ( 'memo' ) ;
571586 const tree = projectsTree ( root ) ;
572587 const store = make ( tree ) ;
573588 const catalog = loadRuleCatalog ( ) ;
@@ -615,12 +630,12 @@ test('a stale-files threshold crossed between two requests gives the fresh answe
615630 let clock = T ;
616631 setCleanupMemoOptionsForTests ( { yieldTurn : immediate , now : ( ) => clock } ) ;
617632 try {
618- const root = path . join ( path . sep , 'stale' ) ;
633+ const root = absRoot ( 'stale' ) ;
619634 const catalog = testCatalog ( root ) ;
620635 const dl = path . join ( root , 'Downloads' ) ;
621636 // 30.5 days and 2 seconds from crossing the small-file rule; the 90-day rule needs far longer.
622637 const young = T - 30.5 * DAY + 2_000 ;
623- const tree = dirAt ( path . sep , 'stale' , [ dirAt ( root , 'Downloads' , [ fileAt ( dl , 'setup.dmg' , 5_000 , young ) , fileAt ( dl , 'old.iso' , 5_000 , T - 400 * DAY ) ] ) ] ) ;
638+ const tree = dirAt ( path . dirname ( root ) , 'stale' , [ dirAt ( root , 'Downloads' , [ fileAt ( dl , 'setup.dmg' , 5_000 , young ) , fileAt ( dl , 'old.iso' , 5_000 , T - 400 * DAY ) ] ) ] ) ;
624639 for ( const [ kind , make ] of STORES ) {
625640 resetCleanupMemoForTests ( ) ;
626641 clock = T ;
@@ -645,10 +660,10 @@ test('a request that joins a walk begun before a threshold it is already past ge
645660 let clock = T ;
646661 setCleanupMemoOptionsForTests ( { yieldTurn : immediate , sliceNodes : 1 , now : ( ) => clock } ) ;
647662 try {
648- const root = path . join ( path . sep , 'joined' ) ;
663+ const root = absRoot ( 'joined' ) ;
649664 const catalog = testCatalog ( root ) ;
650665 const dl = path . join ( root , 'Downloads' ) ;
651- const tree = dirAt ( path . sep , 'joined' , [ dirAt ( root , 'Downloads' , [ fileAt ( dl , 'setup.dmg' , 5_000 , T - 30.5 * DAY + 2_000 ) , fileAt ( dl , 'old.iso' , 5_000 , T - 400 * DAY ) ] ) ] ) ;
666+ const tree = dirAt ( path . dirname ( root ) , 'joined' , [ dirAt ( root , 'Downloads' , [ fileAt ( dl , 'setup.dmg' , 5_000 , T - 30.5 * DAY + 2_000 ) , fileAt ( dl , 'old.iso' , 5_000 , T - 400 * DAY ) ] ) ] ) ;
652667 for ( const [ kind , make ] of STORES ) {
653668 resetCleanupMemoForTests ( ) ;
654669 clock = T ;
@@ -678,12 +693,12 @@ test('a look at the git repos does not cost the next suggestions request a walk'
678693 resetCleanupMemoForTests ( ) ;
679694 setCleanupMemoOptionsForTests ( { yieldTurn : immediate } ) ;
680695 try {
681- const root = path . join ( path . sep , 'repos' ) ;
696+ const root = absRoot ( 'repos' ) ;
682697 const repo = ( name : string , flagged : boolean ) : FileNode => {
683698 const at = path . join ( root , name ) ;
684699 return { ...dirAt ( root , name , [ dirAt ( at , '.git' , [ fileAt ( path . join ( at , '.git' ) , 'HEAD' , 23 , 0 ) ] ) , dirAt ( at , 'node_modules' , [ fileAt ( path . join ( at , 'node_modules' ) , 'i.js' , 40 , 0 ) ] ) ] ) , ...( flagged ? { gitRepo : true } : { } ) } ;
685700 } ;
686- const tree = dirAt ( path . sep , 'repos' , [ repo ( 'marked' , true ) , repo ( 'unmarked' , false ) ] ) ;
701+ const tree = dirAt ( path . dirname ( root ) , 'repos' , [ repo ( 'marked' , true ) , repo ( 'unmarked' , false ) ] ) ;
687702 const store = buildStoreFromTree ( tree ) ;
688703 const catalog = loadRuleCatalog ( ) ;
689704 await cleanupSuggestionsFor ( store , [ ] , catalog , [ ] ) ;
@@ -704,7 +719,7 @@ test('a walk that fails is not remembered', async () => {
704719 resetCleanupMemoForTests ( ) ;
705720 setCleanupMemoOptionsForTests ( { yieldTurn : immediate } ) ;
706721 try {
707- const store = ObjectScanStore . wrap ( projectsTree ( path . join ( path . sep , 'failing' ) ) ) ;
722+ const store = ObjectScanStore . wrap ( projectsTree ( absRoot ( 'failing' ) ) ) ;
708723 const childIds = store . childIds . bind ( store ) ;
709724 let fail = true ;
710725 store . childIds = ( id : number ) : number [ ] => {
@@ -724,7 +739,7 @@ test('the rule claims are every non-advisory match in walk order, uncapped, and
724739 setCleanupMemoOptionsForTests ( { yieldTurn : immediate , sliceNodes : 50 } ) ;
725740 try {
726741 // 250 node_modules: past the 200 items a group lists, so a capped list would lose 50 claims.
727- const root = path . join ( path . sep , 'claims' ) ;
742+ const root = absRoot ( 'claims' ) ;
728743 const kids : FileNode [ ] = [ ] ;
729744 for ( let p = 0 ; p < 250 ; p ++ ) {
730745 const at = path . join ( root , `p${ p } ` ) ;
@@ -758,7 +773,7 @@ test('the rule claims are every non-advisory match in walk order, uncapped, and
758773
759774test ( 'the Reclaim Score\'s rule claims come from the memo, walked once for the same store' , async ( ) => {
760775 resetCleanupMemoForTests ( ) ;
761- const store = buildStoreFromTree ( projectsTree ( path . join ( path . sep , 'reclaim' ) ) ) ;
776+ const store = buildStoreFromTree ( projectsTree ( absRoot ( 'reclaim' ) ) ) ;
762777 const first = await computeRuleClaims ( store ) ;
763778 const second = await computeRuleClaims ( store ) ;
764779 assert . equal ( cleanupMemoCountsForTests ( ) . claims , 1 , 'two score batches, one walk' ) ;
0 commit comments