Skip to content

Commit 18bf83f

Browse files
sturleseclaude
andauthored
refactor(slack): resolve the reviewer through the org directory (#26)
The CLI resolves runners and reviewers to stable directory ids, but the Slack path recorded the raw Slack handle — the same person clicking in Slack and typing 'flightdeck feedback' became two reviewers to the KPIs (the asymmetry was even acknowledged in apply_interaction's docstring). apply_interaction now resolves the Slack user through org.directory with the same precedence as the CLI; no match keeps the raw string, so orgs without a directory are byte-identical. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent f6b2e61 commit 18bf83f

3 files changed

Lines changed: 45 additions & 8 deletions

File tree

‎docs/governance.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,9 @@ click back into the same measurement `flightdeck feedback` records.
108108
- **One feedback path.** The CLI command and the Slack handler both call a single
109109
`record_feedback(...)` function, so a button click lands the *identical* store row and the
110110
*identical* `feedback_recorded` ledger event (`{run_id, outcome, human_minutes, by}`) — the
111-
Slack `by` is the reviewer's Slack handle, with a `via slack` note on the store row for
112-
provenance. There is no second, weaker feedback API to keep in sync.
111+
Slack `by` is resolved through the org directory exactly like the CLI reviewer (the stable
112+
id when the handle matches, the raw Slack handle otherwise), with a `via slack` note on the
113+
store row for provenance. There is no second, weaker feedback API to keep in sync.
113114
- **Offline-first, no new dependency.** `flightdeck slack post <run_id>` renders a Slack Block
114115
Kit message and, by default, **prints the JSON** — fully demoable and pipeable to any poster.
115116
Only when `FLIGHTDECK_SLACK_WEBHOOK` is set does it actually POST, via stdlib `urllib`

‎src/flightdeck/integrations/slack.py‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -263,20 +263,23 @@ def apply_interaction(
263263
"""Parse a Slack interaction and record it through the shared feedback path.
264264
265265
An explicit ``minutes`` wins; otherwise a modal's minutes are used; otherwise
266-
``None`` (metrics fall back to ``org.config.default_review_minutes``). ``org``
267-
is accepted for parity with the CLI and future directory resolution — today
268-
``by`` is simply the Slack user string. Raises ``SlackError`` on a bad payload
269-
and ``FeedbackError`` (from ``record_feedback``) on an unknown run.
266+
``None`` (metrics fall back to ``org.config.default_review_minutes``). The
267+
Slack user is resolved through the org directory exactly like the CLI's
268+
reviewer — the stable id when the handle matches, the raw Slack string
269+
otherwise — so one person clicking in Slack and typing in a terminal stays
270+
ONE reviewer to the KPIs. Raises ``SlackError`` on a bad payload and
271+
``FeedbackError`` (from ``record_feedback``) on an unknown run.
270272
"""
271273
parsed = parse_interaction(payload)
272274
effective_minutes = minutes if minutes is not None else parsed.minutes
275+
resolved = org.directory.resolve(parsed.user)
273276
return record_feedback(
274277
store,
275278
ledger,
276279
parsed.run_id,
277280
parsed.outcome,
278281
human_minutes=effective_minutes,
279-
by=parsed.user,
282+
by=resolved.id if resolved is not None else parsed.user,
280283
note="via slack",
281284
)
282285

‎tests/test_slack.py‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from flightdeck.runner import execute
3131
from flightdeck.schemas import Run
3232
from flightdeck.store import Store
33-
from tests.conftest import NOW
33+
from tests.conftest import NOW, write_org
3434

3535
runner = CliRunner()
3636

@@ -231,6 +231,39 @@ def test_apply_interaction_uses_modal_minutes(org, store, ledger):
231231
assert fb.outcome == "edited" and fb.human_minutes == 9.0
232232

233233

234+
def test_apply_interaction_resolves_reviewer_through_the_directory(tmp_path):
235+
# A click by Slack handle and a CLI review by alias must land on the SAME
236+
# stable id — one person, one reviewer to the KPIs (same rule as `flightdeck
237+
# feedback`, which already resolves --by through the directory).
238+
directory = {
239+
"provider": "azure_ad",
240+
"users": [
241+
{
242+
"id": "AAD-42", "display_name": "Ana García",
243+
"email": "ana.garcia@example.com", "department": "Support",
244+
"aliases": ["ana", "ana.g"],
245+
}
246+
],
247+
}
248+
org = load_org(write_org(tmp_path / "org", directory=directory))
249+
with Store(org.db_path) as store:
250+
ledger = Ledger(org.ledger_path)
251+
run = _seed_run(org, store, ledger)
252+
payload = _button_payload(json.dumps({"run_id": run.id, "outcome": "accepted"}), user="ana.g")
253+
fb = apply_interaction(payload, store, ledger, org)
254+
255+
assert fb.by == "AAD-42" # the stable id, not the raw Slack handle
256+
assert ledger.entries()[-1]["data"]["by"] == "AAD-42"
257+
258+
259+
def test_apply_interaction_keeps_unknown_reviewers_raw(org, store, ledger):
260+
# No directory (or no match) → the raw Slack string, exactly as before.
261+
run = _seed_run(org, store, ledger)
262+
payload = _button_payload(json.dumps({"run_id": run.id, "outcome": "accepted"}), user="stranger")
263+
fb = apply_interaction(payload, store, ledger, org)
264+
assert fb.by == "stranger"
265+
266+
234267
# ------------------------------------------------------------------------ transport
235268

236269

0 commit comments

Comments
 (0)