Verify production support and legal queues

This commit is contained in:
SimpleTest 2026-08-21 01:11:49 +03:00
parent cd9a21af85
commit ccbef9cd4f
8 changed files with 122 additions and 29 deletions

View File

@ -1274,8 +1274,9 @@ WNH_PRODUCTION_E2E_CONFIRM='VALUE_PRINTED_BY_PLAN' \
The run creates six uniquely prefixed synthetic users and only their associated The run creates six uniquely prefixed synthetic users and only their associated
mutual-aid, activity, notification, audit, support, and legal fixture records. mutual-aid, activity, notification, audit, support, and legal fixture records.
The support and legal records are inserted directly without email jobs and are The support record, general-removal notice, and separate TAKE IT DOWN notice are
only read through the staff UI; the browser does not submit or moderate them. inserted directly without email jobs and are only read through the staff UI;
the browser does not submit or moderate them.
Cleanup uses the mode-`0600` manifest of exact IDs on success, failure, or Cleanup uses the mode-`0600` manifest of exact IDs on success, failure, or
interrupt, refuses cross-fixture relationships, deletes only matching jobs and interrupt, refuses cross-fixture relationships, deletes only matching jobs and
records, and verifies that the run prefix is absent. It never resets the records, and verifies that the run prefix is absent. It never resets the

View File

@ -197,11 +197,13 @@ as forward-only rather than receiving an invented database rollback.
approval, and reporting. approval, and reporting.
- [ ] Support, privacy/data, account-deletion, general content-removal, and - [ ] Support, privacy/data, account-deletion, general content-removal, and
TAKE IT DOWN submissions reach the correct production operator queues. TAKE IT DOWN submissions reach the correct production operator queues.
A 2026-08-09 run-scoped check proved one authenticated support case and A 2026-08-21 run-scoped check proved authenticated support, privacy,
one authenticated general content-removal notice, then removed only those data-export and account-deletion submissions plus separate general
exact records. Privacy/data, account deletion, anonymous verification, content-removal and TAKE IT DOWN records. Staff found every record in its
and TAKE IT DOWN still require an exact production smoke before this permission-scoped production queue, and exact cleanup left zero records
combined item can be checked. with the run prefix. Anonymous contact verification still requires a
controlled production mail smoke before this combined item can be
checked.
- [ ] Database, application, worker, email, push, backup, and edge monitoring - [ ] Database, application, worker, email, push, backup, and edge monitoring
are visible to the responsible operator. are visible to the responsible operator.

View File

@ -3265,3 +3265,26 @@ promoted.
`support_confirmation` deliveries, two successful `mail` queue jobs, and no `support_confirmation` deliveries, two successful `mail` queue jobs, and no
delivery exception in the selected metric set. These counters describe only delivery exception in the selected metric set. These counters describe only
the current application process, not historical delivery volume. the current application process, not historical delivery volume.
# 2026-08-21 production support and legal queue coverage
- Temporary production run `codex-support-legal-20260821-r3` passed all three
Chromium scenarios against exact deployed revision
`bb7eb58c8f14d8936cae0e968b50ae721516d213`: activity moderation, the complete
two-person medicine-help flow, and support/legal queue coverage.
- The support/legal scenario submitted authenticated support, privacy,
data-export and account-deletion requests through the public UI. Staff found
the resulting records in the permission-scoped production queue. The same
staff session found and opened distinct run-scoped general-removal and TAKE
IT DOWN records without submitting an operator decision.
- The fixture manifest used schema version 3, recorded the exact support,
general-removal and TAKE IT DOWN UUIDs, and ended with
`cleanup_verified=true`. Post-run inspection found
`fixture_prefix_count=0`; no run-owned fixture user or relationship remained.
- Anonymous contact verification was not submitted in this run because it
intentionally enqueues a real verification email. That path remains an open
controlled production-mail check rather than an inferred success.
- Non-secret evidence is retained under
`output/production-full-e2e/codex-support-legal-20260821-r3/`. The frozen
hackathon test deployment, Caddy, public Git remote and production release
were not changed by this browser run.

View File

@ -55,6 +55,10 @@ test("run-scoped support intake and legal fixture reach production staff", async
const accountDeletionSubject = "Delete my Who Need Help account"; const accountDeletionSubject = "Delete my Who Need Help account";
const removalExplanation = const removalExplanation =
"Run-scoped read-only browser fixture for the production legal review queue."; "Run-scoped read-only browser fixture for the production legal review queue.";
const takeItDownExplanation =
"Run-scoped read-only browser fixture for the production TAKE IT DOWN queue.";
const removalReference = `REM-E2E-${runID.toUpperCase()}`;
const takeItDownReference = `REM-E2E-TID-${runID.toUpperCase()}`;
const requester = await loginWithPassword( const requester = await loginWithPassword(
browser, browser,
@ -133,14 +137,29 @@ test("run-scoped support intake and legal fixture reach production staff", async
await admin.page await admin.page
.locator("#legal-case-filters") .locator("#legal-case-filters")
.getByLabel("Search") .getByLabel("Search")
.fill(requesterEmail); .fill(removalReference);
const legalRow = admin.page.locator("main tbody tr"); const generalLegalRow = admin.page.locator("main tbody tr");
await expect(legalRow).toHaveCount(1); await expect(generalLegalRow).toHaveCount(1);
await legalRow.getByRole("link", { name: "Open" }).click(); await expect(generalLegalRow).toContainText("general");
await generalLegalRow.getByRole("link", { name: "Open" }).click();
await expect( await expect(
admin.page.getByText(removalExplanation, { exact: true }), admin.page.getByText(removalExplanation, { exact: true }),
).toBeVisible(); ).toBeVisible();
await gotoLiveView(admin.page, "/support/operations?queue=legal");
await admin.page
.locator("#legal-case-filters")
.getByLabel("Search")
.fill(takeItDownReference);
const takeItDownRow = admin.page.locator("main tbody tr");
await expect(takeItDownRow).toHaveCount(1);
await expect(takeItDownRow).toContainText(/take it down/i);
await expect(takeItDownRow).toContainText("urgent review");
await takeItDownRow.getByRole("link", { name: "Open" }).click();
await expect(
admin.page.getByText(takeItDownExplanation, { exact: true }),
).toBeVisible();
assertRequesterClean(); assertRequesterClean();
assertAdminClean(); assertAdminClean();
await requester.context.close(); await requester.context.close();

View File

@ -142,7 +142,13 @@ defmodule Mix.Tasks.Wnh.StagingFullE2e do
password_hash = Bcrypt.hash_pwd_salt(password) password_hash = Bcrypt.hash_pwd_salt(password)
now = DateTime.utc_now(:second) now = DateTime.utc_now(:second)
{:ok, %{users: users, support_request: support_request, removal_notice: removal_notice}} = {:ok,
%{
users: users,
support_request: support_request,
removal_notice: removal_notice,
take_it_down_notice: take_it_down_notice
}} =
Repo.transaction(fn -> Repo.transaction(fn ->
users = users =
Map.new(@precreated_roles, fn role -> Map.new(@precreated_roles, fn role ->
@ -167,22 +173,25 @@ defmodule Mix.Tasks.Wnh.StagingFullE2e do
support_request = insert_support_fixture!(context, requester, now) support_request = insert_support_fixture!(context, requester, now)
removal_notice = insert_removal_fixture!(context, requester, now) removal_notice = insert_removal_fixture!(context, requester, now)
take_it_down_notice = insert_take_it_down_fixture!(context, requester, now)
%{ %{
users: users, users: users,
support_request: support_request, support_request: support_request,
removal_notice: removal_notice removal_notice: removal_notice,
take_it_down_notice: take_it_down_notice
} }
end) end)
manifest = %{ manifest = %{
"schema_version" => 2, "schema_version" => 3,
"run_id" => context.run_id, "run_id" => context.run_id,
"database" => context.database, "database" => context.database,
"precreated_users" => users, "precreated_users" => users,
"precreated_records" => %{ "precreated_records" => %{
"support_request" => support_request.id, "support_request" => support_request.id,
"content_removal_notice" => removal_notice.id "content_removal_notice" => removal_notice.id,
"take_it_down_notice" => take_it_down_notice.id
}, },
"allowed_emails" => context.emails |> Map.values() |> Enum.sort() "allowed_emails" => context.emails |> Map.values() |> Enum.sort()
} }
@ -468,15 +477,17 @@ defmodule Mix.Tasks.Wnh.StagingFullE2e do
case manifest["precreated_records"] do case manifest["precreated_records"] do
%{ %{
"support_request" => support_request_id, "support_request" => support_request_id,
"content_removal_notice" => removal_notice_id "content_removal_notice" => removal_notice_id,
"take_it_down_notice" => take_it_down_notice_id
} -> } ->
uuid?(support_request_id) and uuid?(removal_notice_id) uuid?(support_request_id) and uuid?(removal_notice_id) and
uuid?(take_it_down_notice_id) and removal_notice_id != take_it_down_notice_id
_other -> _other ->
false false
end end
unless manifest["schema_version"] == 2 and manifest["run_id"] == context.run_id and unless manifest["schema_version"] == 3 and manifest["run_id"] == context.run_id and
manifest["database"] == context.database and manifest["database"] == context.database and
manifest["allowed_emails"] == expected_emails and valid_precreated? and manifest["allowed_emails"] == expected_emails and valid_precreated? and
valid_precreated_records? do valid_precreated_records? do
@ -499,7 +510,9 @@ defmodule Mix.Tasks.Wnh.StagingFullE2e do
records = manifest["precreated_records"] records = manifest["precreated_records"]
unless records["support_request"] in support_request_ids and unless records["support_request"] in support_request_ids and
records["content_removal_notice"] in removal_notice_ids do records["content_removal_notice"] in removal_notice_ids and
records["take_it_down_notice"] in removal_notice_ids and
records["content_removal_notice"] != records["take_it_down_notice"] do
Mix.raise("full staging E2E precreated records do not match the manifest") Mix.raise("full staging E2E precreated records do not match the manifest")
end end
end end
@ -810,6 +823,32 @@ defmodule Mix.Tasks.Wnh.StagingFullE2e do
|> Repo.insert!() |> Repo.insert!()
end end
defp insert_take_it_down_fixture!(context, requester, now) do
%Notice{
reference: "REM-E2E-TID-#{String.upcase(context.run_id)}",
requester_id: requester["id"],
contact_verified_at: now,
regime: :take_it_down,
status: :urgent_review,
response_due_at: DateTime.add(now, 48, :hour)
}
|> Notice.submission_changeset(%{
"category" => "non_consensual_intimate_media",
"submitter_name" => "Production E2E Requester",
"contact_email" => requester["email"],
"relationship" => "self",
"content_locations" =>
"https://whoneedhelp.com/requests/production-e2e-take-it-down-#{context.run_id}",
"explanation" =>
"Run-scoped read-only browser fixture for the production TAKE IT DOWN queue.",
"legal_basis" => "Production E2E fixture only.",
"electronic_signature" => "Production E2E Requester",
"good_faith" => "true",
"accurate_complete" => "true"
})
|> Repo.insert!()
end
defp emails(run_id) do defp emails(run_id) do
(@precreated_roles ++ @registered_roles) (@precreated_roles ++ @registered_roles)
|> Map.new(fn role -> {role, "#{prefix(run_id)}#{role}@example.invalid"} end) |> Map.new(fn role -> {role, "#{prefix(run_id)}#{role}@example.invalid"} end)

View File

@ -1,6 +1,6 @@
%{ %{
"assent": {:hex, :assent, "0.3.1", "7e7b04f4ab5d07b497b80c04f009a0f1efc409787e68b8e086512b9098f3095b", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: true]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "3597b31f9eb556d97e64cf60c00d3451f7353d7b465a71d33530b870ebed1ff1"}, "assent": {:hex, :assent, "0.3.1", "7e7b04f4ab5d07b497b80c04f009a0f1efc409787e68b8e086512b9098f3095b", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: true]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "3597b31f9eb556d97e64cf60c00d3451f7353d7b465a71d33530b870ebed1ff1"},
"bandit": {:hex, :bandit, "1.12.4", "10bbab488edf8162318d736c19c5837077b8fca2bf5d95b07b33830387124f62", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.5", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "84513318c5752a2a8017664450f889b47fae5d53d64698ddf1e4fb09a7449e8d"}, "bandit": {:hex, :bandit, "1.12.5", "af205a8e550f304caae09a97d29fd3c79a7f337526ea7cd772d2ff11d2f7c800", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.5", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "c5684ca062fa407cac115aec3256383f3e2ec9fdced7904d59cf5a7bb7ed6181"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"},
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},

View File

@ -209,8 +209,8 @@ Exact temporary mutation scope:
- their activity, participation, group chat, report and category proposal; - their activity, participation, group chat, report and category proposal;
- one directly inserted support row plus three authenticated privacy, - one directly inserted support row plus three authenticated privacy,
data-export and account-deletion requests submitted through the public UI; data-export and account-deletion requests submitted through the public UI;
- one directly inserted legal row, read through the staff UI without - one directly inserted general-removal row and one separate TAKE IT DOWN
submitting or moderating a content-removal notice; row, read through the staff UI without submitting or moderating either;
- their notifications, audit events and associated Oban jobs; - their notifications, audit events and associated Oban jobs;
- no database reset, migration, real-user role/status change, email delivery, - no database reset, migration, real-user role/status change, email delivery,
Caddy change, test-project change, payment, iOS, or KYC action. Caddy change, test-project change, payment, iOS, or KYC action.
@ -327,10 +327,10 @@ cleanup() {
.cleanup_verified == true and .cleanup_verified == true and
(.cleanup_targets.users | length) >= 6 and (.cleanup_targets.users | length) >= 6 and
(.cleanup_targets.support_requests | length) >= 1 and (.cleanup_targets.support_requests | length) >= 1 and
(.cleanup_targets.content_removal_notices | length) == 1 and (.cleanup_targets.content_removal_notices | length) == 2 and
(.cleanup_deleted_counts.users | type) == "number" and (.cleanup_deleted_counts.users | type) == "number" and
.cleanup_deleted_counts.support_requests == (.cleanup_targets.support_requests | length) and .cleanup_deleted_counts.support_requests == (.cleanup_targets.support_requests | length) and
.cleanup_deleted_counts.content_removal_notices == 1 and .cleanup_deleted_counts.content_removal_notices == 2 and
.cleanup_verified_at != null .cleanup_verified_at != null
' "$output_dir/fixture.json" >/dev/null; then ' "$output_dir/fixture.json" >/dev/null; then
echo "Production E2E cleanup manifest lacks exact run-scoped verification." >&2 echo "Production E2E cleanup manifest lacks exact run-scoped verification." >&2

View File

@ -53,6 +53,12 @@ defmodule WhoNeedHelp.StagingFullE2ECleanupTest do
manifest["precreated_records"]["content_removal_notice"] manifest["precreated_records"]["content_removal_notice"]
) )
prepared_take_it_down_notice =
Repo.get!(
WhoNeedHelp.ContentRemoval.Notice,
manifest["precreated_records"]["take_it_down_notice"]
)
browser_created_support_request = browser_created_support_request =
%WhoNeedHelp.Support.SupportRequest{ %WhoNeedHelp.Support.SupportRequest{
reference: "SUP-BROWSER-#{String.upcase(run_id)}", reference: "SUP-BROWSER-#{String.upcase(run_id)}",
@ -69,11 +75,15 @@ defmodule WhoNeedHelp.StagingFullE2ECleanupTest do
}) })
|> Repo.insert!() |> Repo.insert!()
assert manifest["schema_version"] == 2 assert manifest["schema_version"] == 3
assert prepared_support_request.subject == "Production E2E support #{run_id}" assert prepared_support_request.subject == "Production E2E support #{run_id}"
assert prepared_support_request.contact_verified_at assert prepared_support_request.contact_verified_at
assert prepared_removal_notice.regime == :general assert prepared_removal_notice.regime == :general
assert prepared_removal_notice.contact_verified_at assert prepared_removal_notice.contact_verified_at
assert prepared_take_it_down_notice.regime == :take_it_down
assert prepared_take_it_down_notice.status == :urgent_review
assert prepared_take_it_down_notice.response_due_at
assert prepared_take_it_down_notice.contact_verified_at
refute_enqueued( refute_enqueued(
worker: WhoNeedHelp.Mail.SupportOperatorAlertWorker, worker: WhoNeedHelp.Mail.SupportOperatorAlertWorker,
@ -108,12 +118,11 @@ defmodule WhoNeedHelp.StagingFullE2ECleanupTest do
assert Enum.sort(manifest["cleanup_targets"]["support_requests"]) == assert Enum.sort(manifest["cleanup_targets"]["support_requests"]) ==
Enum.sort([prepared_support_request.id, browser_created_support_request.id]) Enum.sort([prepared_support_request.id, browser_created_support_request.id])
assert manifest["cleanup_targets"]["content_removal_notices"] == [ assert Enum.sort(manifest["cleanup_targets"]["content_removal_notices"]) ==
prepared_removal_notice.id Enum.sort([prepared_removal_notice.id, prepared_take_it_down_notice.id])
]
assert manifest["cleanup_deleted_counts"]["support_requests"] == 2 assert manifest["cleanup_deleted_counts"]["support_requests"] == 2
assert manifest["cleanup_deleted_counts"]["content_removal_notices"] == 1 assert manifest["cleanup_deleted_counts"]["content_removal_notices"] == 2
assert support_job.id in cleaned_job_ids assert support_job.id in cleaned_job_ids
assert legal_job.id in cleaned_job_ids assert legal_job.id in cleaned_job_ids
refute unrelated_job.id in cleaned_job_ids refute unrelated_job.id in cleaned_job_ids