From c3f3fb6df885273de39fbff43321849cb22c2609 Mon Sep 17 00:00:00 2001 From: Soushi888 Date: Sat, 15 Aug 2026 18:12:02 -0400 Subject: [PATCH 1/2] test(e2e): tear down leaked guard clone cell and assert empty-lobby precondition --- ui/tests/e2e/specs/core-flows.spec.ts | 33 ++++++++++++++++++++------ ui/tests/e2e/utils/e2e-helpers.ts | 34 ++++++++++++++++++++++++++- ui/tests/setup/harness.ts | 7 +++++- 3 files changed, 65 insertions(+), 9 deletions(-) diff --git a/ui/tests/e2e/specs/core-flows.spec.ts b/ui/tests/e2e/specs/core-flows.spec.ts index 05031f2..f3ea648 100644 --- a/ui/tests/e2e/specs/core-flows.spec.ts +++ b/ui/tests/e2e/specs/core-flows.spec.ts @@ -22,6 +22,7 @@ import { createGroup, createNdo, ensureLobbyProfile, + expectEmptyLobby, expectEventually, gotoAgent } from '../utils/e2e-helpers.js'; @@ -82,13 +83,27 @@ test.describe.serial('nondominium core flows', () => { modifiers: { network_seed: `e2e-clone-guard-${Date.now()}` } }); await authorizeWithRetry(seed.admin, clone.cell_id); - const myGroup = await seed.app.callZome({ - cell_id: clone.cell_id, - zome_name: 'zome_group', - fn_name: 'get_my_group', - payload: null - }); - expect(myGroup).toBeNull(); + try { + const myGroup = await seed.app.callZome({ + cell_id: clone.cell_id, + zome_name: 'zome_group', + fn_name: 'get_my_group', + payload: null + }); + expect(myGroup).toBeNull(); + } finally { + // The UI enumerates group clone cells straight off appInfo, so a leftover + // guard clone shows up in the sidebar as a real group and makes the next + // test's "empty lobby" precondition false. Tear it down here — the guard + // owns this cell, nothing downstream should see it. + await seed.app.disableCloneCell({ + clone_cell_id: { type: 'dna_hash', value: clone.cell_id[0] } + }); + await seed.admin.deleteCloneCell({ + app_id: seed.appId, + clone_cell_id: { type: 'dna_hash', value: clone.cell_id[0] } + }); + } }); // ── Phase 1: single-agent core flows ────────────────────────────────────── @@ -103,6 +118,10 @@ test.describe.serial('nondominium core flows', () => { }); test('empty lobby shows the create-or-join onboarding CTA', async () => { + // The CTA only renders when the agent has no groups, so the emptiness is a + // precondition rather than part of what is under test. Assert it first — + // when it breaks, the message should say so. + await expectEmptyLobby(page); await expect(page.getByText('Create or join a group to see NDOs')).toBeVisible(); }); diff --git a/ui/tests/e2e/utils/e2e-helpers.ts b/ui/tests/e2e/utils/e2e-helpers.ts index 2a64f3e..187b48c 100644 --- a/ui/tests/e2e/utils/e2e-helpers.ts +++ b/ui/tests/e2e/utils/e2e-helpers.ts @@ -119,6 +119,31 @@ export async function saveGroupProfileIfPrompted(page: Page): Promise { } } +/** + * Asserts the agent has no groups yet. + * + * Several lobby behaviours (the create-or-join onboarding CTA above all) are + * only reachable from a genuinely empty lobby, and that emptiness is a property + * of shared conductor state rather than of the test itself. Any earlier test — + * or any spec file that happens to sort earlier, since the suite runs + * `workers: 1`, `fullyParallel: false` — can quietly invalidate it. + * + * Assert it explicitly so the failure names the broken precondition instead of + * surfacing ten seconds later as an inscrutable "element not found" on whatever + * the empty state was supposed to render. + */ +export async function expectEmptyLobby(page: Page): Promise { + const groupLinks = page.locator('nav a[href^="/group/"]'); + const count = await groupLinks.count(); + const names = count > 0 ? await groupLinks.allInnerTexts() : []; + expect( + count, + `precondition failed: expected an empty lobby, found ${count} group(s) in the sidebar ` + + `(${names.join(', ')}). Some earlier test leaked conductor state — check for a ` + + `clone cell or group that was created and never torn down.` + ).toBe(0); +} + /** * Creates a group through the sidebar and lands on /group/{seed}. Handles the * first-visit GroupProfileModal. @@ -140,7 +165,14 @@ export async function createGroup(page: Page, name: string): Promise { export interface NdoFormInput { name: string; - regime?: 'Private' | 'Commons' | 'Nondominium' | 'CommonPool'; + regime?: + | 'Private' + | 'Commons' + | 'Collective' + | 'Pool' + | 'CommonPool' + | 'Public' + | 'Nondominium'; nature?: 'Physical' | 'Digital' | 'Service' | 'Hybrid' | 'Information'; stage?: string; description?: string; diff --git a/ui/tests/setup/harness.ts b/ui/tests/setup/harness.ts index 787cf26..9d81d81 100644 --- a/ui/tests/setup/harness.ts +++ b/ui/tests/setup/harness.ts @@ -123,6 +123,8 @@ export async function authorizeWithRetry( export interface SeedClient { app: AppWebsocket; admin: AdminWebsocket; + /** Installed app id — needed for admin calls scoped to the app (e.g. deleteCloneCell). */ + appId: string; close: () => Promise; } @@ -152,8 +154,10 @@ export async function createSeedClient(agent = 1): Promise { await authorizeWithRetry(admin, cellId); } + const appId = ready.appId || APP_ID; + const { token } = await admin.issueAppAuthenticationToken({ - installed_app_id: ready.appId || APP_ID, + installed_app_id: appId, single_use: false, expiry_seconds: 3600 }); @@ -168,6 +172,7 @@ export async function createSeedClient(agent = 1): Promise { return { app, admin, + appId, close: async () => { try { await app.client.close(); From bfdad4c17cc64b1347984918e1dd2056d07b46a8 Mon Sep 17 00:00:00 2001 From: Soushi888 Date: Sat, 15 Aug 2026 18:12:02 -0400 Subject: [PATCH 2/2] ci: shard Sweettest into a five-target matrix and gate e2e behind it --- .github/workflows/build.yml | 81 +++++++++++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index da74ad6..63cd712 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,12 +35,85 @@ jobs: - name: Build happ run: nix develop --command bun run build:happ - # Browser-level e2e against real conductors (ui/tests/e2e). Gated behind the - # build job; when a Sweettest CI job lands, gate this behind that instead so - # backend regressions fail fast before the slower browser suite runs. - e2e: + # Primary backend suite (CLAUDE.md): pure predicate unit tests in + # crates/shared, then Sweettest against real conductors. Runs before e2e so a + # backend regression fails fast instead of surfacing as a confusing browser + # failure ten minutes later. + # + # Sharded one job per [[test]] target. Measured on the first CI run of the + # whole suite in a single job (run 31736241424): 61 min total — 16 min of + # compile plus 40 min of tests executed back-to-back. Sharding overlaps the + # test time so wall clock tracks the slowest target rather than their sum: + # + # nondominium 16m · resource 13m · governance 6m · person 4m · misc 1.5m + # + # Two details make this work and are easy to undo by accident: + # + # * NO custom CARGO_TARGET_DIR. `CLAUDE.md` documents + # `CARGO_TARGET_DIR=target/native-tests` for local runs, which keeps the + # native test artifacts away from the wasm build. In CI that path falls + # outside what Swatinem/rust-cache saves, so every run recompiled + # holochain's test_utils from scratch — the 16 min. The default `target/` + # is cached, and cargo already separates wasm by target triple. + # * `shared-key` so all shards restore ONE cache rather than fighting over + # five. The first run on a new lockfile still pays the compile in + # parallel; later runs restore it. + # + # --test-threads 2: each Sweettest spawns conductors, and 6 threads with 11 + # conductor tests in flight gets the runner OOM-killed (SIGTERM mid-suite). + sweettest: runs-on: ubuntu-latest needs: build + strategy: + # Report every failing target, not just the first — a shared regression + # usually breaks several, and seeing which ones is the diagnosis. + fail-fast: false + matrix: + target: [misc, person, governance, resource, nondominium] + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - uses: cachix/install-nix-action@v26 + with: + github_access_token: ${{ secrets.GITHUB_TOKEN }} + + - uses: cachix/cachix-action@v14 + with: + name: holochain-ci + skipPush: true + + - uses: Swatinem/rust-cache@v2 + with: + shared-key: sweettest + workspaces: | + . -> target + vendor/hrea -> vendor/hrea/target + + - name: Install dependencies + run: nix develop --command bun install + + # Sweettest loads the packaged .happ, so the WASM build is a prerequisite. + - name: Build happ + run: nix develop --command bun run build:happ + + # Pure predicate tests — no conductors, seconds to run. Only needs to run + # once, so it rides the cheapest shard. + - name: Run shared-crate unit tests + if: matrix.target == 'misc' + run: nix develop --command cargo test --package nondominium_shared + + - name: Run Sweettest target + run: | + nix develop --command cargo test --package nondominium_sweettest \ + --test ${{ matrix.target }} -- --test-threads 2 + + # Browser-level e2e against real conductors (ui/tests/e2e). Gated behind + # sweettest so backend regressions fail before the slower browser suite runs. + e2e: + runs-on: ubuntu-latest + needs: sweettest steps: - uses: actions/checkout@v4 with: