This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/main by this push:
     new 9ddd7f3  Update tests and fix a bug with sending email to start a vote
9ddd7f3 is described below

commit 9ddd7f3f15f24f801d33d921e9888dcef8f9ded4
Author: Sean B. Palmer <[email protected]>
AuthorDate: Fri Dec 5 09:57:45 2025 +0000

    Update tests and fix a bug with sending email to start a vote
---
 atr/construct.py            |  2 +-
 atr/db/interaction.py       | 24 +++++-------------------
 atr/storage/writers/vote.py | 22 +++++++++++++++++++++-
 atr/tasks/vote.py           | 29 +++++++++++------------------
 atr/util.py                 | 15 +++++++++++++++
 playwright/test.py          | 20 ++++++++++----------
 6 files changed, 63 insertions(+), 49 deletions(-)

diff --git a/atr/construct.py b/atr/construct.py
index 027b3d8..70b3043 100644
--- a/atr/construct.py
+++ b/atr/construct.py
@@ -161,7 +161,7 @@ async def start_vote_body(body: str, options: 
StartVoteOptions) -> str:
     import atr.get.vote as vote
 
     async with db.session() as data:
-        # Do not limit by phase, as it may be at RELEASE_CANDIDATE here if 
called by the task
+        # Do not limit by phase, as it may be at RELEASE_CANDIDATE already
         release = await data.release(
             project_name=options.project_name,
             version=options.version_name,
diff --git a/atr/db/interaction.py b/atr/db/interaction.py
index e59aebb..d1b210f 100644
--- a/atr/db/interaction.py
+++ b/atr/db/interaction.py
@@ -19,7 +19,7 @@ import contextlib
 import datetime
 import enum
 from collections.abc import AsyncGenerator, Sequence
-from typing import Any, Final
+from typing import Any
 
 import packaging.version as version
 import sqlalchemy
@@ -36,20 +36,6 @@ import atr.user as user
 import atr.util as util
 import atr.web as web
 
-# TEST_MID: Final[str | None] = 
"CAH5JyZo8QnWmg9CwRSwWY=givhxw4nilyenjo71fkdk81j5...@mail.gmail.com"
-TEST_MID: Final[str | None] = None
-_THREAD_URLS_FOR_DEVELOPMENT: Final[dict[str, str]] = {
-    "CAH5JyZo8QnWmg9CwRSwWY=givhxw4nilyenjo71fkdk81j5...@mail.gmail.com": 
"https://lists.apache.org/thread/z0o7xnjnyw2o886rxvvq2ql4rdfn754w";,
-    "[email protected]": 
"https://lists.apache.org/thread/619hn4x796mh3hkk3kxg1xnl48dy2s64";,
-    "CAA9ykM+bMPNk=bof9hj0o+mjn1igppoj+pkdzhcam0ddvi+...@mail.gmail.com": 
"https://lists.apache.org/thread/x0m3p2xqjvflgtkb6oxqysm36cr9l5mg";,
-    "CAFHDsVzgtfboqYF+a3owaNf+55MUiENWd3g53mU4rD=whkx...@mail.gmail.com": 
"https://lists.apache.org/thread/brj0k3g8pq63g8f7xhmfg2rbt1240nts";,
-    "camomwmrvktqk7k2-otztreo0jjxzo2g5ynw3gsoks_pxwpz...@mail.gmail.com": 
"https://lists.apache.org/thread/y5rqp5qk6dmo08wlc3g20n862hznc9m8";,
-    "CANVKqzfLYj6TAVP_Sfsy5vFbreyhKskpRY-vs=f7aled+rl...@mail.gmail.com": 
"https://lists.apache.org/thread/oy969lhh6wlzd51ovckn8fly9rvpopwh";,
-    "cah4123zwgtkwszheu7qnmbyla-yvykz2w+djh_uchpmuzaa...@mail.gmail.com": 
"https://lists.apache.org/thread/7111mqyc25sfqxm6bf4ynwhs0bk0r4ys";,
-    "CADL1oArKFcXvNb1MJfjN=10-yrfkxgpltrurdmm1r7ygatk...@mail.gmail.com": 
"https://lists.apache.org/thread/d7119h2qm7jrd5zsbp8ghkk0lpvnnxnw";,
-    "[email protected]": 
"https://lists.apache.org/thread/gzjd2jv7yod5sk5rgdf4x33g5l3fdf5o";,
-}
-
 
 class ApacheUserMissingError(RuntimeError):
     def __init__(self, message: str, fingerprint: str | None, primary_uid: str 
| None) -> None:
@@ -338,11 +324,11 @@ async def releases_in_progress(project: sql.Project) -> 
list[sql.Release]:
 
 
 def task_mid_get(latest_vote_task: sql.Task) -> str | None:
-    if util.is_dev_environment():
-        import atr.db.interaction as interaction
+    # if util.is_dev_environment():
+    #     import atr.db.interaction as interaction
 
-        return interaction.TEST_MID
-    # TODO: Improve this
+    #     return interaction.TEST_MID
+    # # TODO: Improve this
 
     result = latest_vote_task.result
     if not isinstance(result, results.VoteInitiate):
diff --git a/atr/storage/writers/vote.py b/atr/storage/writers/vote.py
index 6dae337..eb41204 100644
--- a/atr/storage/writers/vote.py
+++ b/atr/storage/writers/vote.py
@@ -18,6 +18,7 @@
 # Removing this will cause circular imports
 from __future__ import annotations
 
+import datetime
 from typing import Literal
 
 import atr.construct as construct
@@ -168,6 +169,25 @@ class CommitteeParticipant(FoundationCommitter):
         # Presumably this sets the default, and the form takes precedence?
         # ReleasePolicy.min_hours can also be 0, though
 
+        # Calculate vote end time for template substitution
+        vote_start = datetime.datetime.now(datetime.UTC)
+        vote_end = vote_start + datetime.timedelta(hours=vote_duration_choice)
+        vote_end_str = vote_end.strftime("%Y-%m-%d %H:%M:%S UTC")
+
+        # Perform template substitutions in the body before passing to task
+        # This must be done here and not in the task because we need 
util.as_url
+        body_substituted = await construct.start_vote_body(
+            body_data,
+            construct.StartVoteOptions(
+                asfuid=asf_uid,
+                fullname=asf_fullname,
+                project_name=project_name,
+                version_name=version_name,
+                vote_duration=vote_duration_choice,
+                vote_end=vote_end_str,
+            ),
+        )
+
         # Create a task for vote initiation
         task = sql.Task(
             status=sql.TaskStatus.QUEUED,
@@ -179,7 +199,7 @@ class CommitteeParticipant(FoundationCommitter):
                 initiator_id=asf_uid,
                 initiator_fullname=asf_fullname,
                 subject=subject_data,
-                body=body_data,
+                body=body_substituted,
             ).model_dump(),
             asf_uid=asf_uid,
             project_name=project_name,
diff --git a/atr/tasks/vote.py b/atr/tasks/vote.py
index 0097671..f53906d 100644
--- a/atr/tasks/vote.py
+++ b/atr/tasks/vote.py
@@ -17,7 +17,6 @@
 
 import datetime
 
-import atr.construct as construct
 import atr.db as db
 import atr.db.interaction as interaction
 import atr.log as log
@@ -101,21 +100,9 @@ async def _initiate_core_logic(args: Initiate) -> 
results.Results | None:
         log.error(error_msg)
         raise VoteInitiationError(error_msg)
 
-    # Construct email
+    # The body has already been substituted by the route handler
     subject = args.subject
-
-    # Perform substitutions in the body
-    body = await construct.start_vote_body(
-        args.body,
-        construct.StartVoteOptions(
-            asfuid=args.initiator_id,
-            fullname=args.initiator_fullname,
-            project_name=release.project.name,
-            version_name=release.version,
-            vote_duration=args.vote_duration,
-            vote_end=vote_end_str,
-        ),
-    )
+    body = args.body
 
     permitted_recipients = util.permitted_voting_recipients(args.initiator_id, 
release.committee.name)
     if args.email_to not in permitted_recipients:
@@ -131,8 +118,14 @@ async def _initiate_core_logic(args: Initiate) -> 
results.Results | None:
         body=body,
     )
 
-    # Send the email
-    mid, mail_errors = await mail.send(message)
+    if util.is_dev_environment():
+        # Pretend to send the mail
+        log.info("Dev environment detected, pretending to send mail")
+        mid = util.DEV_TEST_MID
+        mail_errors = []
+    else:
+        # Send the mail
+        mid, mail_errors = await mail.send(message)
 
     # Original success message structure
     result = results.VoteInitiate(
@@ -146,7 +139,7 @@ async def _initiate_core_logic(args: Initiate) -> 
results.Results | None:
     )
 
     if mail_errors:
-        log.warning(f"Start vote for {args.release_name}: sending to 
{args.email_to}  gave errors: {mail_errors}")
+        log.warning(f"Start vote for {args.release_name}: sending to 
{args.email_to} gave errors: {mail_errors}")
     else:
         log.info(f"Vote email sent successfully to {args.email_to}")
     return result
diff --git a/atr/util.py b/atr/util.py
index 09e38ef..0745002 100644
--- a/atr/util.py
+++ b/atr/util.py
@@ -57,6 +57,18 @@ import atr.user as user
 T = TypeVar("T")
 
 USER_TESTS_ADDRESS: Final[str] = "[email protected]"
+DEV_TEST_MID: Final[str] = 
"CAH5JyZo8QnWmg9CwRSwWY=givhxw4nilyenjo71fkdk81j5...@mail.gmail.com"
+DEV_THREAD_URLS: Final[dict[str, str]] = {
+    "CAH5JyZo8QnWmg9CwRSwWY=givhxw4nilyenjo71fkdk81j5...@mail.gmail.com": 
"https://lists.apache.org/thread/z0o7xnjnyw2o886rxvvq2ql4rdfn754w";,
+    "[email protected]": 
"https://lists.apache.org/thread/619hn4x796mh3hkk3kxg1xnl48dy2s64";,
+    "CAA9ykM+bMPNk=bof9hj0o+mjn1igppoj+pkdzhcam0ddvi+...@mail.gmail.com": 
"https://lists.apache.org/thread/x0m3p2xqjvflgtkb6oxqysm36cr9l5mg";,
+    "CAFHDsVzgtfboqYF+a3owaNf+55MUiENWd3g53mU4rD=whkx...@mail.gmail.com": 
"https://lists.apache.org/thread/brj0k3g8pq63g8f7xhmfg2rbt1240nts";,
+    "camomwmrvktqk7k2-otztreo0jjxzo2g5ynw3gsoks_pxwpz...@mail.gmail.com": 
"https://lists.apache.org/thread/y5rqp5qk6dmo08wlc3g20n862hznc9m8";,
+    "CANVKqzfLYj6TAVP_Sfsy5vFbreyhKskpRY-vs=f7aled+rl...@mail.gmail.com": 
"https://lists.apache.org/thread/oy969lhh6wlzd51ovckn8fly9rvpopwh";,
+    "cah4123zwgtkwszheu7qnmbyla-yvykz2w+djh_uchpmuzaa...@mail.gmail.com": 
"https://lists.apache.org/thread/7111mqyc25sfqxm6bf4ynwhs0bk0r4ys";,
+    "CADL1oArKFcXvNb1MJfjN=10-yrfkxgpltrurdmm1r7ygatk...@mail.gmail.com": 
"https://lists.apache.org/thread/d7119h2qm7jrd5zsbp8ghkk0lpvnnxnw";,
+    "[email protected]": 
"https://lists.apache.org/thread/gzjd2jv7yod5sk5rgdf4x33g5l3fdf5o";,
+}
 
 
 class SshFingerprintError(ValueError):
@@ -834,6 +846,9 @@ async def task_archive_url(task_mid: str, recipient: str | 
None = None) -> str |
     if "@" not in task_mid:
         return None
 
+    if is_dev_environment() and (task_mid in DEV_THREAD_URLS):
+        return DEV_THREAD_URLS[task_mid]
+
     recipient_address = recipient or USER_TESTS_ADDRESS
     lid = recipient_address.replace("@", ".")
     url = 
f"https://lists.apache.org/api/email.json?id=%3C{task_mid}%3E&listid=%3C{lid}%3E";
diff --git a/playwright/test.py b/playwright/test.py
index df51509..dc8b12f 100755
--- a/playwright/test.py
+++ b/playwright/test.py
@@ -206,21 +206,21 @@ def lifecycle_05_resolve_vote(page: sync_api.Page, 
credentials: Credentials, ver
     logging.info("Vote page loaded successfully")
 
     # Wait until the vote initiation background task has completed
-    # When it finishes the page shows a banner that begins with "Vote thread 
started"
-    # We poll for that banner before moving on
+    # When it finishes the page shows a "view thread" link
+    # We poll for that link before moving on
     # Otherwise the subsequent Resolve step cannot find the completed 
VOTE_INITIATE task
-    # TODO: Make a poll_for_tasks_completion style function that can be used 
here
-    banner_locator = page.locator("p.text-success:has-text('Vote thread 
started')")
-    banner_found = False
+    # Note that this actually doesn't matter in dev testing
+    thread_link_locator = page.locator('a:has-text("view thread")')
+    link_found = False
     for _ in range(30):
-        if banner_locator.is_visible(timeout=500):
-            banner_found = True
-            logging.info("Vote initiation banner detected, task completed")
+        if thread_link_locator.is_visible(timeout=500):
+            link_found = True
+            logging.info("Vote thread link detected, task completed")
             break
         time.sleep(0.5)
         page.reload()
-    if not banner_found:
-        logging.warning("Vote initiation banner not detected after 15s, 
proceeding anyway")
+    if not link_found:
+        logging.warning("Vote thread link not detected after 15s, proceeding 
anyway")
 
     logging.info("Locating the 'Resolve vote' button")
     tabulate_form_locator = 
page.locator(f'form[action="/resolve/{TEST_PROJECT}/{version_name}"]')


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to