推送真发送:webhook/slack 内置真实发送端 + worker 终态自动通知

此前订阅与分发框架都在,但分发端点传 sender=None,
webhook/slack 永远报无 sender、从未真发过。本轮:
- push:send_webhook/send_slack(标准库 urllib POST,
  单次 10 秒超时,不重试,失败记 errors);sender=None
  时用内置端,显式 sender 优先;webpush/apns/fcm 仍报
  缺凭证(原因写明:webpush 需浏览器端三字段订阅与标准库
  没有的 P-256 ECDH + AES-GCM;apns 需 HTTP/2;fcm 需
  Firebase 服务账号,三者均无现成接收方,不造假发送)
- worker:run_one 终态(completed/failed,缺 key 与仓库
  失败两条早退路径也在内)后 best-effort 调分发,通知
  失败只记日志、绝不影响任务状态;取消(interrupted)
  不在事件集合里,不通知
- 测试:真实发送端 6 个(本地回环服务器)、worker 终态
  通知 2 个,旧无 sender 断言按新语义改写
This commit is contained in:
host committed 2026-10-26 17:31:30 +08:00
1 parent b7dace55b9
commit fb3a311da8
7 files changed
+429 -29

No files matched your search

+97
View File
@@ -126,6 +126,103 @@ class WorkerTestCase(unittest.TestCase):
# B 的依赖未完成,始终不可派,保持 queued。
self.assertEqual(_status(self.db_path, "B"), "queued")
# -- 终态推送 --------------------------------------------------------
def test_notify_on_completed_and_failed(self):
"""任务终态自动通知订阅:completed 与 failed 各发一次真 POST
到本地回环 webhook,body 含事件类型与任务 id;取消
(interrupted)不在事件集合里,不通知(见下个测试)。"""
import http.server
import threading
requests = []
class Handler(http.server.BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get("Content-Length") or 0)
body = self.rfile.read(length) if length else b""
requests.append(json.loads(body.decode("utf-8")))
self.send_response(200)
self.send_header("Content-Length", "2")
self.end_headers()
self.wfile.write(b"{}")
def log_message(self, *args):
pass
server = http.server.ThreadingHTTPServer(
("127.0.0.1", 0), Handler)
thread = threading.Thread(
target=server.serve_forever, daemon=True)
thread.start()
try:
from cloud.push import create_subscription
url = "http://127.0.0.1:%d/hook" % server.server_address[1]
create_subscription(
self.db_path, "p1", "user-1", "webhook", url, "instant")
_insert_task(self.db_path, "A")
_insert_task(self.db_path, "B")
def failing(cmd):
if "dispatch" in cmd and cmd[cmd.index("--name") + 1] == "B":
return SimpleNamespace(returncode=1, stdout="boom",
stderr="")
return _completed_runner(cmd)
worker = self._worker(runner=failing)
results = worker.run_all()
self.assertEqual(
sorted(r["status"] for r in results),
["completed", "failed"])
by_task = {b["task_id"]: b for b in requests}
self.assertEqual(set(by_task), {"A", "B"})
self.assertEqual(by_task["A"]["event_type"], "completed")
self.assertEqual(by_task["B"]["event_type"], "failed")
self.assertIn("boom", json.dumps(by_task["B"]["payload"]))
finally:
server.shutdown()
server.server_close()
thread.join(timeout=5)
def test_cancel_does_not_notify(self):
"""取消只标 interrupted,不在推送事件集合里,不发通知。"""
import http.server
import threading
requests = []
class Handler(http.server.BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get("Content-Length") or 0)
self.rfile.read(length) if length else b""
requests.append(True)
self.send_response(200)
self.send_header("Content-Length", "2")
self.end_headers()
self.wfile.write(b"{}")
def log_message(self, *args):
pass
server = http.server.ThreadingHTTPServer(
("127.0.0.1", 0), Handler)
thread = threading.Thread(
target=server.serve_forever, daemon=True)
thread.start()
try:
from cloud.push import create_subscription
url = "http://127.0.0.1:%d/hook" % server.server_address[1]
create_subscription(
self.db_path, "p1", "user-1", "webhook", url, "instant")
_insert_task(self.db_path, "A")
worker = self._worker(runner=_completed_runner)
self.assertTrue(worker.cancel("A"))
self.assertEqual(worker.run_all(), [])
self.assertEqual(requests, [])
finally:
server.shutdown()
server.server_close()
thread.join(timeout=5)
# -- 取消 ------------------------------------------------------------
def test_cancel_queued_task_marks_interrupted(self):
_insert_task(self.db_path, "A")