feat(tempo): blue team Tempo time from pick-up, not queue entry
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

Previously blue_started_at was set when the RED team submitted evidence
(= queue open time), so Tempo was getting total queue wait time instead
of actual work time.

Changes:
- DB: add blue_work_started_at column (migration b045), set when a blue
  tech explicitly picks up the test (mirrors red_started_at for red team)
- Workflow: new start_blue_work() function + POST /tests/{id}/start-blue-work
  endpoint (blue_tech / blue_lead roles). Cannot be called twice.
- submit_blue_evidence: uses blue_work_started_at (when available) as the
  phase start for the Tempo worklog, falls back to blue_started_at
- reopen_test: clears blue_work_started_at alongside other timing fields
- Tempo: both red_team_execution and blue_team_evaluation now synced;
  correct work_date and description per activity type
- Frontend: "Start Evaluation" button shown in blue_evaluating state when
  blue_work_started_at is null; live timer shows from pick-up time

What each timestamp tracks:
  blue_started_at      = queue entry (SLA / internal tracking)
  blue_work_started_at = pick-up by blue tech (Tempo start)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-05-27 11:50:15 +02:00
parent 0e6cec4d07
commit 398e279116
10 changed files with 153 additions and 31 deletions

View File

@@ -11,10 +11,11 @@ all Tempo calls are silently skipped regardless of whether users have tokens.
What goes to Tempo
------------------
Only **red team execution** time is logged to Tempo. This reflects the time
the operator (red_tech) spends executing the attack technique — from the moment
they click "Start" to when they click "Done". Blue team evaluation time is
tracked internally in Aegis but is NOT sent to Tempo.
Both **red team execution** and **blue team evaluation** time are logged to
Tempo. Red team time runs from when the red_tech clicks "Start" to "Done".
Blue team time runs from when the blue_tech clicks "Start Evaluation" (sets
blue_work_started_at) to when they submit, so it reflects actual working time
rather than queue time.
"""
import logging
@@ -28,10 +29,8 @@ from app.models.jira_link import JiraLink, JiraLinkEntityType
logger = logging.getLogger(__name__)
# Only these activity types are forwarded to Tempo.
# "blue_team_evaluation" is intentionally excluded — it is tracked in Aegis
# but does not represent operator execution work.
_TEMPO_ACTIVITY_TYPES = {"red_team_execution"}
# Activity types forwarded to Tempo.
_TEMPO_ACTIVITY_TYPES = {"red_team_execution", "blue_team_evaluation"}
def has_tempo_configured(user) -> bool:
@@ -104,7 +103,7 @@ def auto_log_test_worklog(
Returns the Tempo worklog response dict, or ``None`` if skipped.
Completely non-fatal — errors are logged and swallowed.
"""
# Only operator execution time goes to Tempo
# Only whitelisted activity types go to Tempo
if activity_type not in _TEMPO_ACTIVITY_TYPES:
logger.debug(
"Skipping Tempo sync for activity_type=%s (not in whitelist)", activity_type
@@ -152,19 +151,27 @@ def auto_log_test_worklog(
return None
try:
# Use red_started_at date as the worklog date so it matches when the
# Use the phase start timestamp as the worklog date so it matches when
# work actually happened (not the submission timestamp).
work_date = (
(test.red_started_at or getattr(test, "updated_at", None) or test.created_at)
.strftime("%Y-%m-%d")
)
if activity_type == "blue_team_evaluation":
work_date = (
(test.blue_work_started_at or test.blue_started_at or test.created_at)
.strftime("%Y-%m-%d")
)
description = f"[Aegis] Blue Team evaluation: {test.name}"
else:
work_date = (
(test.red_started_at or getattr(test, "updated_at", None) or test.created_at)
.strftime("%Y-%m-%d")
)
description = f"[Aegis] Red Team execution: {test.name}"
result = log_worklog(
user=user,
jira_issue_id=int(link.jira_issue_id),
author_account_id=jira_account_id,
date=work_date,
time_spent_seconds=duration_seconds,
description=f"[Aegis] Red Team execution: {test.name}",
description=description,
)
logger.info(
"Tempo worklog created for test %s by user %s: %ds on %s",