feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
533
server/supabase/tests/content-reporting.integration.sql
Normal file
533
server/supabase/tests/content-reporting.integration.sql
Normal file
|
|
@ -0,0 +1,533 @@
|
|||
\set ON_ERROR_STOP on
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE OR REPLACE FUNCTION pg_temp.assert_true(condition boolean, message text)
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF condition IS NOT TRUE THEN
|
||||
RAISE EXCEPTION 'assertion_failed: %', message;
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
INSERT INTO auth.users (
|
||||
id, aud, role, email, encrypted_password, email_confirmed_at,
|
||||
raw_app_meta_data, raw_user_meta_data, created_at, updated_at
|
||||
) VALUES
|
||||
(
|
||||
'29000000-0000-4000-8000-000000000001', 'authenticated', 'authenticated',
|
||||
'content-reporter@example.invalid', crypt('fixture-password', gen_salt('bf')), now(),
|
||||
'{"provider":"email","providers":["email"]}'::jsonb, '{}'::jsonb, now(), now()
|
||||
),
|
||||
(
|
||||
'29000000-0000-4000-8000-000000000002', 'authenticated', 'authenticated',
|
||||
'content-other@example.invalid', crypt('fixture-password', gen_salt('bf')), now(),
|
||||
'{"provider":"email","providers":["email"]}'::jsonb, '{}'::jsonb, now(), now()
|
||||
),
|
||||
(
|
||||
'29000000-0000-4000-8000-000000000003', 'authenticated', 'authenticated',
|
||||
'content-manager@example.invalid', crypt('fixture-password', gen_salt('bf')), now(),
|
||||
'{"provider":"email","providers":["email"],"role":"manager"}'::jsonb, '{}'::jsonb, now(), now()
|
||||
),
|
||||
(
|
||||
'29000000-0000-4000-8000-000000000004', 'authenticated', 'authenticated',
|
||||
'content-rate@example.invalid', crypt('fixture-password', gen_salt('bf')), now(),
|
||||
'{"provider":"email","providers":["email"]}'::jsonb, '{}'::jsonb, now(), now()
|
||||
);
|
||||
|
||||
UPDATE public.profiles
|
||||
SET role = 'manager'
|
||||
WHERE id = '29000000-0000-4000-8000-000000000003';
|
||||
|
||||
SELECT pg_temp.assert_true(
|
||||
NOT has_table_privilege('authenticated', 'public.content_generation_receipts', 'SELECT'),
|
||||
'authenticated users cannot read generation receipts'
|
||||
);
|
||||
SELECT pg_temp.assert_true(
|
||||
NOT has_table_privilege('authenticated', 'public.content_reports', 'SELECT'),
|
||||
'authenticated users cannot read moderation snapshots directly'
|
||||
);
|
||||
SELECT pg_temp.assert_true(
|
||||
NOT has_table_privilege('authenticated', 'public.content_reports', 'INSERT'),
|
||||
'authenticated users cannot bypass the submission RPC'
|
||||
);
|
||||
SELECT pg_temp.assert_true(
|
||||
NOT has_function_privilege(
|
||||
'authenticated',
|
||||
'public.submit_content_report_v1(uuid,uuid,text,text,uuid,text,text,text)',
|
||||
'EXECUTE'
|
||||
),
|
||||
'authenticated users cannot forge the service submission actor'
|
||||
);
|
||||
SELECT pg_temp.assert_true(
|
||||
NOT has_function_privilege(
|
||||
'authenticated',
|
||||
'public.purge_expired_content_reporting_data_v1()',
|
||||
'EXECUTE'
|
||||
),
|
||||
'authenticated users cannot purge moderation evidence'
|
||||
);
|
||||
SELECT pg_temp.assert_true(
|
||||
has_function_privilege(
|
||||
'authenticated',
|
||||
'public.admin_list_content_reports_v1(text,integer,timestamptz)',
|
||||
'EXECUTE'
|
||||
),
|
||||
'moderation reads cross an explicit role-checking RPC'
|
||||
);
|
||||
|
||||
SELECT pg_temp.assert_true(
|
||||
NOT has_column_privilege('authenticated', 'public.profiles', 'role', 'UPDATE'),
|
||||
'authenticated users cannot self-promote through profiles_update_own'
|
||||
);
|
||||
SELECT pg_temp.assert_true(
|
||||
NOT has_column_privilege('authenticated', 'public.profiles', 'tier', 'UPDATE'),
|
||||
'authenticated users cannot self-assign an entitlement tier'
|
||||
);
|
||||
SELECT pg_temp.assert_true(
|
||||
has_column_privilege('authenticated', 'public.profiles', 'name', 'UPDATE')
|
||||
AND has_column_privilege('authenticated', 'public.profiles', 'avatar_url', 'UPDATE')
|
||||
AND has_column_privilege('authenticated', 'public.profiles', 'locale', 'UPDATE'),
|
||||
'authenticated users retain the intended self-service profile fields'
|
||||
);
|
||||
|
||||
SET LOCAL ROLE authenticated;
|
||||
SELECT set_config('request.jwt.claim.role', 'authenticated', true);
|
||||
SELECT set_config('request.jwt.claim.sub', '29000000-0000-4000-8000-000000000002', true);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
BEGIN
|
||||
UPDATE public.profiles
|
||||
SET role = 'manager'
|
||||
WHERE id = '29000000-0000-4000-8000-000000000002';
|
||||
RAISE EXCEPTION 'expected_profile_role_update_denied';
|
||||
EXCEPTION WHEN insufficient_privilege THEN
|
||||
NULL;
|
||||
END;
|
||||
|
||||
BEGIN
|
||||
UPDATE public.profiles
|
||||
SET tier = 'pro_plus'
|
||||
WHERE id = '29000000-0000-4000-8000-000000000002';
|
||||
RAISE EXCEPTION 'expected_profile_tier_update_denied';
|
||||
EXCEPTION WHEN insufficient_privilege THEN
|
||||
NULL;
|
||||
END;
|
||||
|
||||
UPDATE public.profiles
|
||||
SET name = 'Safe self-service profile', locale = 'en'
|
||||
WHERE id = '29000000-0000-4000-8000-000000000002';
|
||||
END;
|
||||
$$;
|
||||
|
||||
RESET ROLE;
|
||||
SELECT pg_temp.assert_true(
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.profiles
|
||||
WHERE id = '29000000-0000-4000-8000-000000000002'
|
||||
AND name = 'Safe self-service profile'
|
||||
AND locale = 'en'
|
||||
AND role = 'user'
|
||||
AND tier = 'free'
|
||||
),
|
||||
'profile ACL preserves public edits while managed fields remain unchanged'
|
||||
);
|
||||
|
||||
SELECT set_config('request.jwt.claim.role', 'service_role', true);
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
receipt jsonb;
|
||||
first_result jsonb;
|
||||
replay_result jsonb;
|
||||
command_receipt jsonb;
|
||||
action_receipt jsonb;
|
||||
BEGIN
|
||||
receipt := public.issue_content_generation_receipt_v1(
|
||||
'29000000-0000-4000-8000-000000000001',
|
||||
'talk_response',
|
||||
'claude-haiku-4-5-20251001'
|
||||
);
|
||||
PERFORM pg_temp.assert_true(receipt->>'generationId' IS NOT NULL,
|
||||
'Talk generation receives a server-side receipt');
|
||||
|
||||
first_result := public.submit_content_report_v1(
|
||||
'29000000-0000-4000-8000-000000000001',
|
||||
'29100000-0000-4000-8000-000000000001',
|
||||
'ai_output',
|
||||
'talk_response',
|
||||
(receipt->>'generationId')::uuid,
|
||||
'harmful',
|
||||
'Please review this output',
|
||||
'Reporter-selected AI response evidence'
|
||||
);
|
||||
PERFORM pg_temp.assert_true(
|
||||
first_result->>'status' = 'submitted'
|
||||
AND NOT (first_result->>'idempotent')::boolean,
|
||||
'first submission creates a report'
|
||||
);
|
||||
|
||||
replay_result := public.submit_content_report_v1(
|
||||
'29000000-0000-4000-8000-000000000001',
|
||||
'29100000-0000-4000-8000-000000000001',
|
||||
'ai_output',
|
||||
'talk_response',
|
||||
(receipt->>'generationId')::uuid,
|
||||
'harmful',
|
||||
'Please review this output',
|
||||
'Reporter-selected AI response evidence'
|
||||
);
|
||||
PERFORM pg_temp.assert_true(
|
||||
(replay_result->>'idempotent')::boolean
|
||||
AND replay_result->>'reportId' = first_result->>'reportId',
|
||||
'same key and body replay without a duplicate row'
|
||||
);
|
||||
|
||||
BEGIN
|
||||
PERFORM public.submit_content_report_v1(
|
||||
'29000000-0000-4000-8000-000000000001',
|
||||
'29100000-0000-4000-8000-000000000001',
|
||||
'ai_output', 'talk_response', (receipt->>'generationId')::uuid,
|
||||
'privacy', NULL, 'Different body'
|
||||
);
|
||||
RAISE EXCEPTION 'expected_idempotency_conflict';
|
||||
EXCEPTION WHEN SQLSTATE 'PT409' THEN
|
||||
NULL;
|
||||
END;
|
||||
|
||||
BEGIN
|
||||
PERFORM public.submit_content_report_v1(
|
||||
'29000000-0000-4000-8000-000000000002',
|
||||
'29100000-0000-4000-8000-000000000002',
|
||||
'ai_output', 'talk_response', (receipt->>'generationId')::uuid,
|
||||
'privacy', NULL, 'Cross-user attempt'
|
||||
);
|
||||
RAISE EXCEPTION 'expected_source_not_found';
|
||||
EXCEPTION WHEN SQLSTATE 'P0002' THEN
|
||||
NULL;
|
||||
END;
|
||||
|
||||
command_receipt := public.issue_content_generation_receipt_v1(
|
||||
'29000000-0000-4000-8000-000000000001',
|
||||
'command_response',
|
||||
'claude-haiku-4-5-20251001'
|
||||
);
|
||||
action_receipt := public.issue_content_generation_receipt_v1(
|
||||
'29000000-0000-4000-8000-000000000001',
|
||||
'action_response',
|
||||
'claude-haiku-4-5-20251001'
|
||||
);
|
||||
PERFORM pg_temp.assert_true(
|
||||
command_receipt->>'generationId' IS NOT NULL
|
||||
AND action_receipt->>'generationId' IS NOT NULL,
|
||||
'Commands and Actions share the implemented receipt boundary'
|
||||
);
|
||||
|
||||
BEGIN
|
||||
PERFORM public.submit_content_report_v1(
|
||||
'29000000-0000-4000-8000-000000000001',
|
||||
'29100000-0000-4000-8000-000000000003',
|
||||
'ai_output', 'talk_response', (command_receipt->>'generationId')::uuid,
|
||||
'other', NULL, 'Surface mismatch'
|
||||
);
|
||||
RAISE EXCEPTION 'expected_surface_mismatch';
|
||||
EXCEPTION WHEN SQLSTATE 'P0002' THEN
|
||||
NULL;
|
||||
END;
|
||||
END;
|
||||
$$;
|
||||
|
||||
INSERT INTO public.meetings(id, user_id, title, status)
|
||||
VALUES (
|
||||
'29700000-0000-4000-8000-000000000001',
|
||||
'29000000-0000-4000-8000-000000000001',
|
||||
'Generated document report fixture',
|
||||
'completed'
|
||||
);
|
||||
|
||||
INSERT INTO public.meeting_documents(
|
||||
id, meeting_id, user_id, template_type, title, content, prompt_used,
|
||||
llm_model, llm_latency_ms, generation_idempotency_key
|
||||
) VALUES
|
||||
(
|
||||
'29800000-0000-4000-8000-000000000001',
|
||||
'29700000-0000-4000-8000-000000000001',
|
||||
'29000000-0000-4000-8000-000000000001',
|
||||
'minutes',
|
||||
'Generated minutes',
|
||||
'Generated meeting document evidence',
|
||||
'template:fixture@1',
|
||||
'claude-haiku-4-5-20251001',
|
||||
12,
|
||||
'29900000-0000-4000-8000-000000000001'
|
||||
),
|
||||
(
|
||||
'29800000-0000-4000-8000-000000000002',
|
||||
'29700000-0000-4000-8000-000000000001',
|
||||
'29000000-0000-4000-8000-000000000001',
|
||||
'custom',
|
||||
'Manual document',
|
||||
'This row has no generation provenance',
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL
|
||||
);
|
||||
|
||||
INSERT INTO public.meeting_document_generation_audit(
|
||||
user_id, meeting_id, document_id, idempotency_key, model,
|
||||
template_revision, transcript_hash, latency_ms
|
||||
) VALUES (
|
||||
'29000000-0000-4000-8000-000000000001',
|
||||
'29700000-0000-4000-8000-000000000001',
|
||||
'29800000-0000-4000-8000-000000000001',
|
||||
'29900000-0000-4000-8000-000000000001',
|
||||
'claude-haiku-4-5-20251001',
|
||||
1,
|
||||
repeat('a', 64),
|
||||
12
|
||||
);
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
result jsonb;
|
||||
BEGIN
|
||||
result := public.submit_content_report_v1(
|
||||
'29000000-0000-4000-8000-000000000001',
|
||||
'29100000-0000-4000-8000-000000000007',
|
||||
'ai_output',
|
||||
'meeting_document',
|
||||
'29800000-0000-4000-8000-000000000001',
|
||||
'misinformation',
|
||||
NULL,
|
||||
'Generated meeting document evidence'
|
||||
);
|
||||
PERFORM pg_temp.assert_true(
|
||||
result->>'status' = 'submitted'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM public.content_reports
|
||||
WHERE id = (result->>'reportId')::uuid
|
||||
AND source_type = 'meeting_document'
|
||||
AND generation_receipt_id IS NULL
|
||||
),
|
||||
'owned meeting document with generation audit is reportable'
|
||||
);
|
||||
|
||||
BEGIN
|
||||
PERFORM public.submit_content_report_v1(
|
||||
'29000000-0000-4000-8000-000000000002',
|
||||
'29100000-0000-4000-8000-000000000008',
|
||||
'ai_output', 'meeting_document',
|
||||
'29800000-0000-4000-8000-000000000001',
|
||||
'privacy', NULL, 'Cross-user generated document'
|
||||
);
|
||||
RAISE EXCEPTION 'expected_cross_user_document_not_found';
|
||||
EXCEPTION WHEN SQLSTATE 'P0002' THEN
|
||||
NULL;
|
||||
END;
|
||||
|
||||
BEGIN
|
||||
PERFORM public.submit_content_report_v1(
|
||||
'29000000-0000-4000-8000-000000000001',
|
||||
'29100000-0000-4000-8000-000000000009',
|
||||
'ai_output', 'meeting_document',
|
||||
'29800000-0000-4000-8000-000000000002',
|
||||
'other', NULL, 'Manual document masquerading as AI output'
|
||||
);
|
||||
RAISE EXCEPTION 'expected_generation_audit_required';
|
||||
EXCEPTION WHEN SQLSTATE 'P0002' THEN
|
||||
NULL;
|
||||
END;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
purge_result jsonb;
|
||||
BEGIN
|
||||
INSERT INTO public.content_generation_receipts(
|
||||
id, user_id, purpose, model, created_at, expires_at
|
||||
) VALUES (
|
||||
'29600000-0000-4000-8000-000000000001',
|
||||
'29000000-0000-4000-8000-000000000002',
|
||||
'talk_response',
|
||||
'expired-fixture',
|
||||
now() - interval '31 days',
|
||||
now() - interval '1 day'
|
||||
);
|
||||
purge_result := public.purge_expired_content_reporting_data_v1();
|
||||
PERFORM pg_temp.assert_true(
|
||||
(purge_result->>'purgedReceipts')::integer >= 1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM public.content_generation_receipts
|
||||
WHERE id = '29600000-0000-4000-8000-000000000001'
|
||||
),
|
||||
'service cleanup removes an expired content-free receipt'
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Fill the hourly window for a separate actor through service-only rows, then
|
||||
-- prove that the atomic RPC denies the next otherwise valid receipt.
|
||||
INSERT INTO public.content_generation_receipts(id, user_id, purpose, model)
|
||||
SELECT
|
||||
('29200000-0000-4000-8000-' || lpad(ordinal::text, 12, '0'))::uuid,
|
||||
'29000000-0000-4000-8000-000000000004'::uuid,
|
||||
'talk_response',
|
||||
'fixture-model'
|
||||
FROM generate_series(1, 11) AS ordinal;
|
||||
|
||||
INSERT INTO public.content_reports(
|
||||
reporter_id, idempotency_key, request_hash, kind, source_type, source_id,
|
||||
generation_receipt_id, reason, reported_snapshot, snapshot_sha256
|
||||
)
|
||||
SELECT
|
||||
'29000000-0000-4000-8000-000000000004'::uuid,
|
||||
gen_random_uuid(),
|
||||
encode(extensions.digest(('fixture-' || ordinal)::text, 'sha256'), 'hex'),
|
||||
'ai_output',
|
||||
'talk_response',
|
||||
('29200000-0000-4000-8000-' || lpad(ordinal::text, 12, '0'))::uuid,
|
||||
('29200000-0000-4000-8000-' || lpad(ordinal::text, 12, '0'))::uuid,
|
||||
'spam',
|
||||
'fixture evidence ' || ordinal,
|
||||
encode(extensions.digest(('fixture evidence ' || ordinal)::text, 'sha256'), 'hex')
|
||||
FROM generate_series(1, 10) AS ordinal;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
remaining_receipt uuid;
|
||||
BEGIN
|
||||
SELECT receipt.id INTO remaining_receipt
|
||||
FROM public.content_generation_receipts AS receipt
|
||||
WHERE receipt.user_id = '29000000-0000-4000-8000-000000000004'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM public.content_reports AS report
|
||||
WHERE report.generation_receipt_id = receipt.id
|
||||
)
|
||||
LIMIT 1;
|
||||
|
||||
BEGIN
|
||||
PERFORM public.submit_content_report_v1(
|
||||
'29000000-0000-4000-8000-000000000004',
|
||||
'29100000-0000-4000-8000-000000000004',
|
||||
'ai_output', 'talk_response', remaining_receipt,
|
||||
'spam', NULL, 'Rate-limited evidence'
|
||||
);
|
||||
RAISE EXCEPTION 'expected_rate_limit';
|
||||
EXCEPTION WHEN SQLSTATE 'PT429' THEN
|
||||
NULL;
|
||||
END;
|
||||
END;
|
||||
$$;
|
||||
|
||||
SELECT set_config('request.jwt.claim.role', 'authenticated', true);
|
||||
SELECT set_config('request.jwt.claim.sub', '29000000-0000-4000-8000-000000000003', true);
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
queued record;
|
||||
acted jsonb;
|
||||
replay jsonb;
|
||||
resolved jsonb;
|
||||
evidence_expiry timestamptz;
|
||||
BEGIN
|
||||
SELECT * INTO queued
|
||||
FROM public.admin_list_content_reports_v1('pending', 100, NULL)
|
||||
WHERE reporter_id = '29000000-0000-4000-8000-000000000001'
|
||||
LIMIT 1;
|
||||
PERFORM pg_temp.assert_true(
|
||||
queued.report_id IS NOT NULL
|
||||
AND queued.reported_snapshot = 'Reporter-selected AI response evidence',
|
||||
'manager can read the bounded moderation evidence through the RPC'
|
||||
);
|
||||
|
||||
acted := public.admin_act_on_content_report_v1(
|
||||
queued.report_id,
|
||||
'29100000-0000-4000-8000-000000000005',
|
||||
'begin_review',
|
||||
'Initial policy review'
|
||||
);
|
||||
PERFORM pg_temp.assert_true(
|
||||
acted->>'status' = 'reviewing' AND NOT (acted->>'idempotent')::boolean,
|
||||
'manager can start review through the action RPC'
|
||||
);
|
||||
|
||||
replay := public.admin_act_on_content_report_v1(
|
||||
queued.report_id,
|
||||
'29100000-0000-4000-8000-000000000005',
|
||||
'begin_review',
|
||||
'Initial policy review'
|
||||
);
|
||||
PERFORM pg_temp.assert_true(
|
||||
(replay->>'idempotent')::boolean,
|
||||
'moderation action replay is idempotent'
|
||||
);
|
||||
|
||||
resolved := public.admin_act_on_content_report_v1(
|
||||
queued.report_id,
|
||||
'29100000-0000-4000-8000-000000000006',
|
||||
'dismiss',
|
||||
'No policy violation after review'
|
||||
);
|
||||
SELECT evidence_expires_at INTO evidence_expiry
|
||||
FROM public.content_reports WHERE id = queued.report_id;
|
||||
PERFORM pg_temp.assert_true(
|
||||
resolved->>'status' = 'dismissed'
|
||||
AND evidence_expiry BETWEEN now() + interval '179 days' AND now() + interval '181 days',
|
||||
'terminal moderation sets the implemented 180-day evidence window'
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
SELECT set_config('request.jwt.claim.sub', '29000000-0000-4000-8000-000000000002', true);
|
||||
DO $$
|
||||
BEGIN
|
||||
BEGIN
|
||||
PERFORM public.admin_list_content_reports_v1(NULL, 10, NULL);
|
||||
RAISE EXCEPTION 'expected_manager_required';
|
||||
EXCEPTION WHEN insufficient_privilege THEN
|
||||
NULL;
|
||||
END;
|
||||
END;
|
||||
$$;
|
||||
|
||||
SELECT pg_temp.assert_true(
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.audit_log
|
||||
WHERE action = 'content_report.dismiss'
|
||||
AND target_type = 'content_report'
|
||||
AND admin_id = '29000000-0000-4000-8000-000000000003'
|
||||
),
|
||||
'moderation resolution records metadata-only audit evidence before manager deletion'
|
||||
);
|
||||
|
||||
DELETE FROM auth.users WHERE id = '29000000-0000-4000-8000-000000000003';
|
||||
SELECT pg_temp.assert_true(
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.audit_log
|
||||
WHERE action = 'content_report.dismiss'
|
||||
AND target_type = 'content_report'
|
||||
AND admin_id IS NULL
|
||||
),
|
||||
'manager deletion preserves audit evidence while unlinking the manager identity'
|
||||
);
|
||||
|
||||
-- Account deletion removes the generation ledger identity but retains a
|
||||
-- de-identified moderation record and its selected evidence.
|
||||
DELETE FROM auth.users WHERE id = '29000000-0000-4000-8000-000000000001';
|
||||
SELECT pg_temp.assert_true(
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.content_reports
|
||||
WHERE reporter_id IS NULL
|
||||
AND reported_snapshot = 'Reporter-selected AI response evidence'
|
||||
AND generation_receipt_id IS NULL
|
||||
),
|
||||
'moderation evidence survives account deletion without the reporter identity'
|
||||
);
|
||||
|
||||
ROLLBACK;
|
||||
Loading…
Add table
Add a link
Reference in a new issue