From 1f1a9322486f0b26802ea1409e1090fa7a5b0e03 Mon Sep 17 00:00:00 2001 From: ehsan shariati Date: Fri, 12 Jun 2026 18:33:39 -0400 Subject: [PATCH 1/2] test: #38 - fix flaky v7 paginated-listing test (seed across subdirs) test_v7_list_directory_paginated_round_trips seeded 64 files in ONE directory. Shard routing is dir-local (shard_for_path_v6 hashes the parent dir), so all 64 landed in ONE salt-random shard and the >=2-page assertion rode an artifact: page 2 was an empty tail drain of the shards after the hot one. Whenever the salt routed /big to the LAST shard (P = 1/16 per run) the cursor exhausted immediately, yielding a single page and the panic: expected >=2 pages, got 1. Measured ~1/10 locally; failed both of today's main pushes while all PR runs passed (luck). Latent since e98ad3d - NOT related to the 0.6.8/0.6.9 changes, and not a bug in shipped pagination code (page unions were always complete and correct). Fix: seed the 64 files across 8 subdirectories of /big/ (8 each). Each subdir routes to an independent salt-random shard, so the set spans >=2 of the 16 shards unless all eight collide (P = 16^-7, never). The test now also asserts >=2 NON-EMPTY pages, so it can never again pass on the tail artifact - it finally tests real multi-shard pagination, and stays valid if list_recursive_page later learns to skip trailing empty shards (follow-up noted on #38). Validation: 0 failures in 20 consecutive local runs (pre-fix: 1/10). Fixes #38 Co-Authored-By: Claude Fable 5 --- tests/v7_hamt_tests.rs | 41 ++++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/tests/v7_hamt_tests.rs b/tests/v7_hamt_tests.rs index 7382b49..c27585d 100644 --- a/tests/v7_hamt_tests.rs +++ b/tests/v7_hamt_tests.rs @@ -353,11 +353,23 @@ async fn test_v7_list_directory_paginated_round_trips() { let client = make_client(&base, encryption); client.create_bucket(bucket).await.expect("create bucket"); - // 64 files under /big/ — enough to span multiple v7 shards. + // 64 files spread across 8 SUBDIRECTORIES of /big/ (8 files each). + // + // Issue #38: shard routing is DIR-LOCAL — `shard_for_path_v6` hashes the + // parent directory — so the previous single-directory seed put all 64 + // files in ONE salt-random shard. The ≥2-page assertion below then + // depended on an artifact: page 2 was an EMPTY tail drain of the shards + // after the hot one, and whenever the salt routed /big to the LAST + // shard (P = 1/16 per run) the cursor exhausted immediately and the + // test failed with "got 1 page". Eight subdirectories route to eight + // independent salt-random shards, so the seeded set genuinely spans ≥2 + // of the 16 initial shards unless all eight collide on one + // (P = 16⁻⁷ ≈ 4e-9) — and the pagination assertions below test real + // multi-shard paging, not the tail artifact. let total = 64usize; let mut expected: std::collections::HashSet = std::collections::HashSet::new(); for i in 0..total { - let key = format!("/big/entry-{:03}.bin", i); + let key = format!("/big/d{}/entry-{:03}.bin", i % 8, i); client.put_object_flat(bucket, &key, format!("v{}", i).into_bytes(), None) .await.expect("seed"); expected.insert(key); @@ -370,17 +382,23 @@ async fn test_v7_list_directory_paginated_round_trips() { let mut seen: std::collections::HashSet = std::collections::HashSet::new(); let mut cursor: Option = None; let mut page_count = 0usize; + let mut nonempty_pages = 0usize; loop { let listing = client .list_directory_paginated(bucket, Some("/big"), cursor.as_deref(), Some(8)) .await .expect("paginated page"); page_count += 1; + let mut page_files = 0usize; for files in listing.directories.values() { for f in files { seen.insert(f.original_key.clone()); + page_files += 1; } } + if page_files > 0 { + nonempty_pages += 1; + } if listing.is_truncated { assert!( listing.next_continuation_token.is_some(), @@ -397,12 +415,21 @@ async fn test_v7_list_directory_paginated_round_trips() { // Safety guard — paging must terminate; bail if runaway. assert!(page_count < 1024, "pagination did not converge"); } - // Structural assumption: 64 files + max_keys=8 spans ≥2 v7 shards under the - // default shard count and BLAKE3 key distribution. If the shard config or - // hash distribution shifts enough to land all 64 in one shard, bump `total` - // rather than deleting this assertion — it guards the "pagination actually - // paginated" invariant that the test is supposed to prove. + // Structural invariant (issue #38): with 8 subdirectories on independent + // salt-random shards and max_keys=8 ≤ every populated shard's match + // count, each populated shard ends a page — so the walk must produce ≥2 + // pages AND ≥2 NON-EMPTY pages. The non-empty assertion is the one that + // proves real multi-shard pagination (a single-shard walk can still + // emit 2 pages via the empty tail-drain artifact); it also keeps this + // test correct if `list_recursive_page` ever learns to skip trailing + // empty shards (the follow-up noted on #38). assert!(page_count >= 2, "expected >=2 pages for {} entries with max_keys=8, got {}", total, page_count); + assert!( + nonempty_pages >= 2, + "expected >=2 NON-EMPTY pages (real multi-shard paging) for {} entries \ + across 8 subdirectories with max_keys=8, got {} non-empty of {} total", + total, nonempty_pages, page_count + ); assert_eq!(seen, expected, "pagination lost or duplicated entries"); // Sanity: the unpaginated API returns the whole set in one shot, matching From 72526cf693082af7be06940468528bef9e07b79e Mon Sep 17 00:00:00 2001 From: ehsan shariati Date: Fri, 12 Jun 2026 18:36:01 -0400 Subject: [PATCH 2/2] ci: trigger flutter-ci on workspace-root tests/ changes (#38) test-rust runs cargo test --workspace --all-targets, but the workflow path filters omitted the workspace-root tests/ directory - a PR touching only tests/ (like the #38 flake fix itself) triggered NO CI at all, so root-test changes merged unvalidated. Add tests/** to both push and pull_request filters. Co-Authored-By: Claude Fable 5 --- .github/workflows/flutter-ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index bd528a7..d2ba7a9 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -9,6 +9,11 @@ on: - 'crates/fula-client/**' - 'crates/fula-crypto/**' - 'packages/fula_client/**' + # Workspace-root integration tests (#38: test-rust runs + # `--workspace --all-targets`, but a PR touching only tests/ + # previously triggered NO CI at all — root-test changes merged + # unvalidated). + - 'tests/**' - 'flutter_rust_bridge.yaml' - '.github/workflows/flutter-ci.yml' pull_request: @@ -18,6 +23,8 @@ on: - 'crates/fula-client/**' - 'crates/fula-crypto/**' - 'packages/fula_client/**' + # See push.paths note (#38). + - 'tests/**' - 'flutter_rust_bridge.yaml' - '.github/workflows/flutter-ci.yml' workflow_dispatch: