The Socket Mode inbound listener was unit-tested against a SYNTHETIC payload
shape that does not match what slack_bolt actually delivers, so the suite was
green while a real Slack thread reply was silently dropped (the clarifier
question stayed `open`). Real slack_bolt delivers an Events API message /
app_mention as `{"type":"event_callback","event":{"type":"message",...}}` and
a free-text thread reply carries NO callback_id/question_id/metadata.
Three breaks fixed (all on the free-text reply path):
1. Type gate — handle_event gated on the OUTER `type`, which is
"event_callback" for a real message/app_mention, so the event fell outside
_ANSWER_BEARING_TYPES and was dropped. Now collapsed to the discriminating
INNER `event.type` via _discriminating_type / _inner_event.
2. question_id recovery — a real reply has no callback_id/question_id/metadata
(the bot's metadata is on the QUESTION message, not the reply). When explicit
id recovery fails, the listener now resolves the question by the inner
event's `thread_ts` against the OPEN ledger row whose `channel_ref` equals it
(new schema helper find_open_question_by_channel_ref, constrained to
status='open' as anti-replay). Explicit id recovery still takes precedence.
3. answer extraction — a real message event carries its text at `event.text`,
not a top-level `answer`/`text`. The thread-reply path now takes the inner
`event.text` (stripped) as the answer value.
AUTHZ-01 is unchanged and still runs FIRST: authorization gates on the sender's
Slack user id (`event.user` for the Events API shape) and fails closed on an
empty/unknown allowlist or unrecoverable sender. The new mapping only resolves
WHICH question is answered, never WHO may answer. Answers stay opaque DATA
(parameterized SQL + json.dumps; never eval/exec/interpolate).
Tests: replaced the synthetic events-API fixtures with REAL Bolt envelopes and
added regression coverage — real thread reply maps via channel_ref and is
accepted, text is stripped, non-owner reply rejected (row stays open), thread_ts
matching no open row is a no-op, reply to an already-answered row is a no-op
(anti-replay), and app_mention is normalized identically. block_actions /
slash_command paths retained.
|
||
|---|---|---|
| .. | ||
| sim | ||
| __init__.py | ||
| conftest.py | ||
| test_apply_verify_workflow_hardening.py | ||
| test_billing.py | ||
| test_build_verify_subgraph.py | ||
| test_builders.py | ||
| test_builders_llm.py | ||
| test_checker_intake.py | ||
| test_ci_fetcher.py | ||
| test_ci_gate.py | ||
| test_ci_gate_workflow.py | ||
| test_clarifier.py | ||
| test_clarifier_llm.py | ||
| test_claude_code_adapter.py | ||
| test_claude_code_live.py | ||
| test_coordinator.py | ||
| test_deadline_timer.py | ||
| test_fixer.py | ||
| test_github_adapter.py | ||
| test_github_intake.py | ||
| test_github_live.py | ||
| test_graph.py | ||
| test_invoker.py | ||
| test_ledger.py | ||
| test_operator_cli.py | ||
| test_planner.py | ||
| test_recovery.py | ||
| test_responder.py | ||
| test_resume_worker.py | ||
| test_review_loop.py | ||
| test_review_loop_llm.py | ||
| test_run_team.py | ||
| test_schema.py | ||
| test_slack_adapter.py | ||
| test_slack_listener.py | ||
| test_slack_live.py | ||
| test_state_store.py | ||
| test_task_model.py | ||
| test_transport_base.py | ||
| test_verifier.py | ||
| test_verifier_llm.py | ||