feat(cli): tapir report — Stage-0 usage gate query
ActiveWeeks computes per-user distinct active weeks (reads login_events UNION acts summary_actions) for the gate (VISION/ADR-016: usage in >=2 distinct weeks). Because the user-owned tables are FORCE RLS under a non-superuser owner, a single cross-user query is deny-all; instead it enumerates users from the un-RLS'd identity map and counts each inside withUser — no privilege escalation, no policy change. `tapir report` prints the per-user table and the pass/fail verdict (needs only TAPIR_DB_DSN). Pure formatter + store query are unit-tested, including the cross-table shared-week dedup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
package store_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// seedReportUser inserts a user + its identity mapping (the enumeration source
|
||||
// ActiveWeeks reads). display_name is optional.
|
||||
func seedReportUser(t *testing.T, p *pgxpool.Pool, userID, subject, name string) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
_, err := p.Exec(ctx,
|
||||
`INSERT INTO users (id, display_name) VALUES ($1, NULLIF($2, ''))`, userID, name)
|
||||
require.NoError(t, err)
|
||||
_, err = p.Exec(ctx,
|
||||
`INSERT INTO user_identities (dex_subject, user_id) VALUES ($1, $2)`, subject, userID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// TestActiveWeeksCountsDistinctWeeksAcrossReadsAndActs is the gate-query proof.
|
||||
// It seeds, with fixed timestamps in known ISO weeks:
|
||||
// - user A: reads in week of Jan 5 and Jan 12, acts in week of Jan 12 (dup) and
|
||||
// Jan 19 → the UNION across both tables collapses the shared week → 3 distinct.
|
||||
// - user B: a single read in the week of Jan 5 → 1 distinct (below the gate).
|
||||
//
|
||||
// It verifies the count is correct, dedups the cross-table shared week, and orders
|
||||
// most-active first.
|
||||
func TestActiveWeeksCountsDistinctWeeksAcrossReadsAndActs(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newStore(t)
|
||||
p := rawPool(t)
|
||||
resetDB(t, p) // TRUNCATE ... users CASCADE also clears user_identities
|
||||
|
||||
seedReportUser(t, p, userA, "subject-a", "Ada")
|
||||
seedReportUser(t, p, userB, "subject-b", "")
|
||||
|
||||
// Reads (login_events) — fixed dates in distinct ISO weeks.
|
||||
_, err := p.Exec(ctx,
|
||||
`INSERT INTO login_events (user_id, seen_at) VALUES
|
||||
($1, '2026-01-05T09:00:00Z'),
|
||||
($1, '2026-01-12T09:00:00Z'),
|
||||
($2, '2026-01-05T09:00:00Z')`, userA, userB)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Acts (summary_actions) — one in A's week-of-Jan-12 (shared with a read, must
|
||||
// dedup) and one in a new week (Jan 19).
|
||||
_, err = p.Exec(ctx,
|
||||
`INSERT INTO summary_actions (user_id, video_id, action, acted_at) VALUES
|
||||
($1, 'vid-1', 'watched', '2026-01-12T18:00:00Z'),
|
||||
($1, 'vid-2', 'saved', '2026-01-19T18:00:00Z')`, userA)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := s.ActiveWeeks(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 2, "both identity users must appear")
|
||||
|
||||
require.Equal(t, userA, got[0].UserID, "most-active user first")
|
||||
require.Equal(t, "Ada", got[0].DisplayName)
|
||||
require.Equal(t, 3, got[0].ActiveWeeks, "3 distinct weeks across reads+acts, shared week deduped")
|
||||
|
||||
require.Equal(t, userB, got[1].UserID)
|
||||
require.Equal(t, 1, got[1].ActiveWeeks, "single read = 1 distinct week (below gate)")
|
||||
}
|
||||
|
||||
// TestActiveWeeksEmptyWhenNoUsers: no identities → no rows (not an error).
|
||||
func TestActiveWeeksEmptyWhenNoUsers(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newStore(t)
|
||||
resetDB(t, rawPool(t))
|
||||
|
||||
got, err := s.ActiveWeeks(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, got)
|
||||
}
|
||||
Reference in New Issue
Block a user