Skip to content

Commit 4149a5a

Browse files
Prithvi-Webclaude
andcommitted
test(store): the slice tests root their trees as a scan root is spelled on the machine running them - on Windows a drive's, never the drive-less "\x" a store reads "/" off (Windows CI)
cleanupSlices (8) and collectorSlices (1) failed on the Windows runner only (CI run 38345032466): "ObjectScanStore: no node -1" in five store/memo/version tests, "packed store: which (now) offers the file that crossed" in two threshold tests, "darwin, packed store: the dotted location is found 0 !== 1", and "PackedScanStore: no node -1" in the container-listing test. They passed in the run before. Cause: the tests rooted their trees at path.join(path.sep, ...), which on Windows is the drive-less "\changing". Since 49635c4 (POL item 7 review, F2.2) a store reads its separator off its root with separatorOf, which says "\" only for a drive's or a share's path, so it read "/" off "\changing" while the trees joined every child with "\"; and ObjectScanStore's lookup now uses the store's own separator, not one guessed from each child. Lookups below the top missed (no node -1), and the packed store spelled paths ("\stale/Downloads/setup.dmg") that no location or stale-files rule names. A test assumption, not a product defect: every scan root on Windows has a drive or a share (sanitizePath resolves it with path.win32; the live walk passes path.sep), and the store reads "\" off those. The roots are now path.resolve(path.sep, ...) - "/x" on macOS and Linux, "D:\x" on the runner - the convention notes.test.ts already uses. No assertion changed. The fuzz differential's "fired every rule of the test catalog" is now checked per store kind: on Windows the packed store fired no location or stale-files rule and the differential stayed green, because the object store fired them for it. Evidence: with Windows path resolution stood in on macOS for these test files and the store/cleanup modules (scratch shim, path.win32 with a C: working drive) the base fails exactly CI's 8 + 1 tests with CI's messages, and passes after (22/0, 7/0); natively 22/0 and 7/0. Mutants, all RED for the right reason and restored by SHA-256: - stand-in, roots drive-less again: "packed store: the fuzz trees fired every rule of the test catalog" (the old union check was green); - stand-in, buildStoreFromTree takes "/" whatever its root: "darwin, packed store: the dotted location is found", "packed store: which now offers the file that crossed", "PackedScanStore: no node -1" (cleanupSlices and collectorSlices); - stand-in, ObjectScanStore.wrap takes "/", or its lookup reads "/": "ObjectScanStore: no node -1"; - natively, buildStoreFromTree joins with "\": the fuzz test fails only on "packed store: the fuzz trees fired every rule". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent c2b1325 commit 4149a5a

2 files changed

Lines changed: 54 additions & 30 deletions

File tree

‎tests/cleanupSlices.test.ts‎

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -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. */
6666
const 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+
6878
const 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

267277
test('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

313328
test('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

329344
test('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

381396
test('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

416431
test('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

498513
test('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
512527
test('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

759774
test('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');

‎tests/collectorSlices.test.ts‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ const LIMITS = [1, 10, 20, 500];
4848
const MINIMUMS = [0, 1, MIB];
4949
const immediate = (): Promise<void> => Promise.resolve();
5050

51+
/**
52+
* A folder at the top of this machine's file system, spelled as a scan root is spelled here: `/shapes`
53+
* on macOS and Linux, `D:\shapes` on Windows (the working folder's drive). Every scan root on Windows
54+
* has a drive or a share, and a store reads its separator off that (separatorOf). `path.join(path.sep,
55+
* …)` gave Windows the drive-less `\shapes`, which a store reads as "/" while the trees here join their
56+
* paths with "\": a lookup below the top missed (Windows CI run 38345032466).
57+
*/
58+
const absRoot = (...names: string[]): string => path.resolve(path.sep, ...names);
59+
5160
/** A fuzz pair whose files share a handful of sizes, so the lists' ties decide their order. */
5261
function tiedPair(seed: number, mutate: boolean): { obj: ObjectScanStore; packed: PackedScanStore } {
5362
const { obj, packed } = buildPair(seed, seed, 1_500);
@@ -102,7 +111,7 @@ test('a container\'s listing, a file root and an empty root are walked as eachFi
102111
// Review of U-17, finding 2: no fuzz tree held these, so a walk that went
103112
// into a container's listing (whose entries are not files on disk) would
104113
// have stayed green.
105-
const root = path.join(path.sep, 'shapes');
114+
const root = absRoot('shapes');
106115
const file = (at: string, name: string, size: number): FileNode => ({ name, path: path.join(at, name), size, type: 'file', modifiedAt: 0, isHidden: false, extension: name.split('.').pop() });
107116
const zipAt = path.join(root, 'archive.zip');
108117
const listing: FileNode[] = [
@@ -122,9 +131,9 @@ test('a container\'s listing, a file root and an empty root are walked as eachFi
122131
const shapes: Array<[string, ScanStore]> = [
123132
['an expanded container, object store', withListing(ObjectScanStore.wrap(structuredClone(tree)))],
124133
['an expanded container, packed store', withListing(buildStoreFromTree(tree))],
125-
['a file root, object store', ObjectScanStore.wrap(file(path.sep, 'solo.bin', 7))],
126-
['a file root, packed store', buildStoreFromTree(file(path.sep, 'solo.bin', 7))],
127-
['an empty root, packed store', buildStoreFromTree({ name: 'none', path: path.join(path.sep, 'none'), type: 'dir', size: 0, modifiedAt: 0, isHidden: false, children: [] })],
134+
['a file root, object store', ObjectScanStore.wrap(file(absRoot(), 'solo.bin', 7))],
135+
['a file root, packed store', buildStoreFromTree(file(absRoot(), 'solo.bin', 7))],
136+
['an empty root, packed store', buildStoreFromTree({ name: 'none', path: absRoot('none'), type: 'dir', size: 0, modifiedAt: 0, isHidden: false, children: [] })],
128137
];
129138
for (const [what, store] of shapes) {
130139
const options = { sliceNodes: 2, yieldTurn: immediate };
@@ -160,7 +169,7 @@ test('a sliced walk of the files visits them in the order eachFile does, tombsto
160169

161170
/** 50 folders of 1,000 files under one root. */
162171
function wideStore(): ScanStore {
163-
const root = path.join(path.sep, 'wide');
172+
const root = absRoot('wide');
164173
const kids: FileNode[] = [];
165174
for (let d = 0; d < 50; d++) {
166175
const at = path.join(root, `d${d}`);

0 commit comments

Comments
 (0)