UI side:
* DesktopSidebar / BottomTabBar filter their entries by $enabledSections,
tag each entry with data-section, and BottomTabBar exposes a
data-section-count attribute so the grid is observable from tests.
* "lists" is force-shown unconditionally — the §12.3.4 always-one-
landing rule covers the edge case where every layer says OFF.
* (app)/+layout.svelte adds a $effect that, when the URL matches a
disabled section (/tasks, /notes, /search), goto's /lists and shows
a transient `section_disabled_for_collective` toast.
* Root +layout.svelte exposes the supabase singleton on window.__sb
in dev so the new Playwright spec can patch rows without a parallel
client; dead-code-eliminated in production builds.
DB side:
* Migration 023 grows by ALTER PUBLICATION supabase_realtime ADD TABLE
public.users + public.collectives + REPLICA IDENTITY FULL on both.
Without this membership the features.ts subscriptions get zero
events and SV-02 (collective toggle → realtime → member's nav
updates) silently fails. Caught while running the spec.
* pgTAP 015 grows from 16 to 20 assertions to cover publication
membership + REPLICA IDENTITY for both new realtime tables.
Paraglide messages: section_disabled_for_collective, the visibility
section titles + blurbs, section_label_* per SectionKey. Both en + es.
Playwright tests/e2e/section-visibility.test.ts (SV-01..SV-03):
SV-01 user toggle → nav reflects → /tasks redirects to /lists
SV-02 admin collective toggle → member sees it disappear in realtime
SV-03 collective ON beats user OFF — per-collective resolution
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
205 lines
7.1 KiB
PL/PgSQL
205 lines
7.1 KiB
PL/PgSQL
-- pgTAP: section visibility (feature_flags) — Fase 12.1
|
|
-- Run with: psql -U postgres -d postgres -f supabase/tests/015_section_visibility.sql
|
|
--
|
|
-- Verifies:
|
|
-- * `users.feature_flags` and `collectives.feature_flags` JSONB columns exist
|
|
-- with NOT NULL + default '{}'.
|
|
-- * `public.known_sections()` is IMMUTABLE and returns the canonical 4 sections.
|
|
-- * `public.section_enabled(section, user, collective)` is STABLE, returns
|
|
-- boolean, and applies the documented precedence:
|
|
-- collective override > user override > default ON.
|
|
-- The forward-compat server layer (Fase 13) adds another layer ABOVE the
|
|
-- collective one; nothing here should make that diff awkward.
|
|
--
|
|
-- These pgTAP assertions run as postgres which bypasses RLS — the RLS write
|
|
-- path for `users.feature_flags` / `collectives.feature_flags` is covered by
|
|
-- the Vitest integration suite (tests/section-visibility.test.ts).
|
|
|
|
CREATE EXTENSION IF NOT EXISTS pgtap;
|
|
|
|
BEGIN;
|
|
SELECT plan(20);
|
|
|
|
-- ── Schema invariants ───────────────────────────────────────────────────────
|
|
|
|
SELECT has_column(
|
|
'public', 'users', 'feature_flags',
|
|
'SV-T01: public.users has feature_flags column'
|
|
);
|
|
|
|
SELECT col_type_is(
|
|
'public', 'users', 'feature_flags', 'jsonb',
|
|
'SV-T02: public.users.feature_flags is jsonb'
|
|
);
|
|
|
|
SELECT col_not_null(
|
|
'public', 'users', 'feature_flags',
|
|
'SV-T03: public.users.feature_flags is NOT NULL'
|
|
);
|
|
|
|
SELECT has_column(
|
|
'public', 'collectives', 'feature_flags',
|
|
'SV-T04: public.collectives has feature_flags column'
|
|
);
|
|
|
|
SELECT col_type_is(
|
|
'public', 'collectives', 'feature_flags', 'jsonb',
|
|
'SV-T05: public.collectives.feature_flags is jsonb'
|
|
);
|
|
|
|
SELECT col_not_null(
|
|
'public', 'collectives', 'feature_flags',
|
|
'SV-T06: public.collectives.feature_flags is NOT NULL'
|
|
);
|
|
|
|
-- ── known_sections() ────────────────────────────────────────────────────────
|
|
|
|
SELECT has_function(
|
|
'public', 'known_sections',
|
|
'SV-T07: public.known_sections() exists'
|
|
);
|
|
|
|
SELECT volatility_is(
|
|
'public', 'known_sections', ARRAY[]::text[], 'immutable',
|
|
'SV-T08: known_sections() is IMMUTABLE'
|
|
);
|
|
|
|
SELECT results_eq(
|
|
$$ SELECT public.known_sections() $$,
|
|
$$ VALUES (ARRAY['lists','tasks','notes','search']) $$,
|
|
'SV-T09: known_sections() returns the canonical 4 sections in order'
|
|
);
|
|
|
|
-- ── section_enabled() ───────────────────────────────────────────────────────
|
|
|
|
SELECT has_function(
|
|
'public', 'section_enabled', ARRAY['text', 'uuid', 'uuid'],
|
|
'SV-T10: public.section_enabled(text, uuid, uuid) exists'
|
|
);
|
|
|
|
SELECT function_returns(
|
|
'public', 'section_enabled', ARRAY['text', 'uuid', 'uuid'], 'boolean',
|
|
'SV-T11: section_enabled() returns boolean'
|
|
);
|
|
|
|
SELECT volatility_is(
|
|
'public', 'section_enabled', ARRAY['text', 'uuid', 'uuid'], 'stable',
|
|
'SV-T12: section_enabled() is STABLE (planner-cacheable in a statement)'
|
|
);
|
|
|
|
-- ── Precedence semantics ────────────────────────────────────────────────────
|
|
-- Uses Ana (seed admin) + the seed collective. Reset both flag columns to {}
|
|
-- at the top of each assertion so the previous one doesn't leak.
|
|
|
|
UPDATE public.users
|
|
SET feature_flags = '{}'::jsonb
|
|
WHERE id = '11111111-1111-1111-1111-111111111111';
|
|
UPDATE public.collectives
|
|
SET feature_flags = '{}'::jsonb
|
|
WHERE id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
|
|
|
|
-- SV-T13: default ON — empty flags on both rows → tasks enabled.
|
|
SELECT is(
|
|
public.section_enabled(
|
|
'tasks',
|
|
'11111111-1111-1111-1111-111111111111',
|
|
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
|
|
),
|
|
true,
|
|
'SV-T13: with empty flag JSON on both rows the section defaults to ON'
|
|
);
|
|
|
|
-- SV-T14: user override OFF wins when collective has no opinion.
|
|
UPDATE public.users
|
|
SET feature_flags = '{"tasks": false}'::jsonb
|
|
WHERE id = '11111111-1111-1111-1111-111111111111';
|
|
SELECT is(
|
|
public.section_enabled(
|
|
'tasks',
|
|
'11111111-1111-1111-1111-111111111111',
|
|
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
|
|
),
|
|
false,
|
|
'SV-T14: user OFF + collective absent → OFF'
|
|
);
|
|
|
|
-- SV-T15: collective override OFF wins even when user explicitly enables.
|
|
UPDATE public.users
|
|
SET feature_flags = '{"tasks": true}'::jsonb
|
|
WHERE id = '11111111-1111-1111-1111-111111111111';
|
|
UPDATE public.collectives
|
|
SET feature_flags = '{"tasks": false}'::jsonb
|
|
WHERE id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
|
|
SELECT is(
|
|
public.section_enabled(
|
|
'tasks',
|
|
'11111111-1111-1111-1111-111111111111',
|
|
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
|
|
),
|
|
false,
|
|
'SV-T15: collective OFF beats user ON (collective is the higher layer)'
|
|
);
|
|
|
|
-- SV-T16: unknown sections default ON (forward-compat — new code can call
|
|
-- section_enabled() before a migration adds the key to known_sections()).
|
|
UPDATE public.users
|
|
SET feature_flags = '{}'::jsonb
|
|
WHERE id = '11111111-1111-1111-1111-111111111111';
|
|
UPDATE public.collectives
|
|
SET feature_flags = '{}'::jsonb
|
|
WHERE id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
|
|
SELECT is(
|
|
public.section_enabled(
|
|
'foo-unknown-section',
|
|
'11111111-1111-1111-1111-111111111111',
|
|
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
|
|
),
|
|
true,
|
|
'SV-T16: an unknown section key returns true (default ON, no migration race)'
|
|
);
|
|
|
|
-- ── Realtime publication membership ─────────────────────────────────────────
|
|
-- features.ts subscribes to UPDATE events on users (own row) + collectives
|
|
-- (active row). Without the publication membership those subscriptions
|
|
-- receive zero events. REPLICA IDENTITY FULL keeps OLD-row DELETE payloads
|
|
-- intact for the same callers down the line.
|
|
|
|
SELECT ok(
|
|
EXISTS (
|
|
SELECT 1 FROM pg_publication_tables
|
|
WHERE pubname = 'supabase_realtime'
|
|
AND schemaname = 'public'
|
|
AND tablename = 'users'
|
|
),
|
|
'SV-T17: public.users is part of the supabase_realtime publication'
|
|
);
|
|
|
|
SELECT ok(
|
|
EXISTS (
|
|
SELECT 1 FROM pg_publication_tables
|
|
WHERE pubname = 'supabase_realtime'
|
|
AND schemaname = 'public'
|
|
AND tablename = 'collectives'
|
|
),
|
|
'SV-T18: public.collectives is part of the supabase_realtime publication'
|
|
);
|
|
|
|
SELECT results_eq(
|
|
$$ SELECT relreplident::text FROM pg_class c
|
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
WHERE n.nspname = 'public' AND c.relname = 'users' $$,
|
|
$$ VALUES ('f') $$,
|
|
'SV-T19: public.users has REPLICA IDENTITY FULL'
|
|
);
|
|
|
|
SELECT results_eq(
|
|
$$ SELECT relreplident::text FROM pg_class c
|
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
WHERE n.nspname = 'public' AND c.relname = 'collectives' $$,
|
|
$$ VALUES ('f') $$,
|
|
'SV-T20: public.collectives has REPLICA IDENTITY FULL'
|
|
);
|
|
|
|
SELECT * FROM finish();
|
|
ROLLBACK;
|