adopt 的冪等改靠「先讀 /dependencies」:真 Gitea 對重複相依回 500 不是 409(inkstone/ISEP#133)

真 Gitea 實跑(hub=InkStoneCo#44、子票=ISEP#133):第一次 POST 成功、/blocks 回 #44、
清空快取後 has 靠 /blocks 判「在」;第二次 POST 回 500——假伺服器原本猜 409,adopt 第二次會炸。
add_dependency() 改成先讀 parent 的 /dependencies,邊在就不 POST(判準是 Gitea 的事實,不是回應碼);
假伺服器改照真的回 500;A41 56→57 條。測完 DELETE 復原(201,/blocks 空)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0178ef1fGw3XeZtpN7LaZrm4
This commit is contained in:
isep-hand
2026-09-07 10:46:43 +00:00
parent d4bf172990
commit 19face1c41
3 changed files with 20 additions and 9 deletions
+5 -4
View File
@@ -1019,12 +1019,12 @@ bash scripts/test-worktree.sh
- (31)(32) 紅 ⇒ 把「離線」當成「沒推」——拿雜訊懲罰謹慎。
### A41 — 主線成員由相依邊決定,不靠同名里程碑:56
### A41 — 主線成員由相依邊決定,不靠同名里程碑:57
```
bash hooks/tests/mainline-members-by-dependency.test.sh
```
**該看到**`通過 56 條,失敗 0 條`。**全程離線**:Gitea 是本機一個假伺服器(`TICKET_HOST` 指過去,
照真 Gitea 回:相依 POST 201、已存在 409、issue 列表用 `milestones=` 名字查),狀態走
**該看到**`通過 57 條,失敗 0 條`。**全程離線**:Gitea 是本機一個假伺服器(`TICKET_HOST` 指過去,
照真 Gitea 回:相依 POST 201、**已存在回 500**2026-09-07 實測,不是想像中的 409、issue 列表用 `milestones=` 名字查),狀態走
`ISEP_COUNTDOWN_STATE_DIR`、repo 那份走 TMP 底下的假薄殼、`HOME` 也指到 TMP。
**不碰真家目錄、不碰真 Gitea、不碰真 InkStoneCo。**
@@ -1048,7 +1048,7 @@ adopt 同 repo ⇒ 掛進 hub 里程碑;別 repo ⇒ POST 相依到 hub 票
- A 群 ⑥⑦ 紅 ⇒ **同名里程碑裡的票又被當成成員**——被推翻的判準回來了
- ⑪ 紅 ⇒ `set` 又去別的 repo 找同名的了
- ⑮⑯⑰ 任一紅 ⇒ `adopt` 別 repo 的票不是加相依:要嘛改了那張票的 milestone、要嘛建了里程碑
- ⑳ 紅 ⇒ 重複 adopt 炸了(Gitea 409 沒被當成「邊本來就在」
-⑳′ 紅 ⇒ 重複 adopt 炸了或重複 POST 了(冪等要靠「先讀 /dependencies」,真 Gitea 對重複 POST 回 500
- ㉔–㉙ 任一紅 ⇒ 沒 hub 票時沒擋、沒給路、或給的路**貼上去跑不動**(㉙ 把訊息裡那行原樣執行)
- ㉛–㉞ 任一紅 ⇒ **票上驗法第 1 條破了**:同名里程碑關掉後成員掉出主線,或 `pick` 抓不到靠邊進來的票
- ㊱㊴㊵ 任一紅 ⇒ `has`focus guard 沒去問 `/blocks`,只看 milestone 就下結論(那是「只知一半」)
@@ -1061,6 +1061,7 @@ adopt 同 repo ⇒ 掛進 hub 里程碑;別 repo ⇒ POST 相依到 hub 票
(里程碑裡 4 + 相依收到 20),其中 `mira#6``ISEP#130``ISEP#140``arcrun-rag#104` 都在
——**不必新增任何邊**,現有的相依圖已經連得到;`has` 四張都回「在」;
`ticket pick --all` 主線組回 `Arcrun#197``mira#5`(後者靠 `Arcrun#86` 的邊,沒掛任何 hub 里程碑)。
**寫入那半也真跑過一次**`adopt inkstone/ISEP#133`hubInkStoneCo#44POST 成功、`/blocks``InkStoneCo#44`、清空快取後 `has``/blocks` 判「在」;重複 POST 真 Gitea 回 **500**(假伺服器原本猜 409——這就是「離線全綠之後一定要真跑一次」),修成先讀 `/dependencies`;測完 DELETE 復原(回 201`/blocks` 空)。
這一走 19 秒(約 30 次序列 GET)⇒ SessionStart 的 `refresh` **不帶** `--members`(成本不變),
`set``adopt``refresh --members` 才重收;`pick``has`/閘每次現問 Gitea,快取只餵 `show`
@@ -14,7 +14,7 @@
#
# 用法:bash hooks/tests/mainline-members-by-dependency.test.sh
# 🔴 全程離線:Gitea 是本機一個假伺服器(TICKET_HOST 指過去),它照真 Gitea 回:相依 POST 201、
# 已存在 409、issue 列表用 `milestones=` 名字查。狀態走 ISEP_COUNTDOWN_STATE_DIR、repo 那份走
# 已存在 **500**2026-09-07 實測,不是 409、issue 列表用 `milestones=` 名字查。狀態走 ISEP_COUNTDOWN_STATE_DIR、repo 那份走
# TMP 底下的假薄殼、HOME 也指到 TMP。不碰真家目錄、不碰真 Gitea、不碰真 InkStoneCo。
set -u
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
@@ -105,7 +105,7 @@ class H(BaseHTTPRequestHandler):
m = re.match(r"^/api/v1/repos/([^/]+)/([^/]+)/issues/(\d+)/dependencies$", p)
if m:
parent = "%s/%s#%s" % m.groups(); child = "%s/%s#%s" % (body["owner"], body["repo"], body["index"])
if child in w["deps"].get(parent, []): return self._send(409, {"message": "dependency already exists"})
if child in w["deps"].get(parent, []): return self._send(500, {"message": "", "url": "…/api/swagger"}) # 真 Gitea 2026-09-07 實測:重複相依回 500,不是 409
w["deps"].setdefault(parent, []).append(child); save(w)
return self._send(201, issue_obj(w, parent))
if re.match(r"^/api/v1/repos/([^/]+)/([^/]+)/milestones$", p):
@@ -209,7 +209,8 @@ M=$(members)
case "$M" in *"inkstone/ISEP#130"*"inkstone/ISEP#140"*) ok "⑱ 成員多了 ISEP#130**還有它的相依 ISEP#140**(邊會一路走下去)" ;; *) no "⑱ 成員沒更新到 #130/#140" "$M" ;; esac
case "$OUT" in *"沒有多出任何里程碑"*) ok "⑲ 訊息明講那個 repo 沒有多出里程碑" ;; *) no "⑲ 訊息沒講" "$OUT" ;; esac
OUT=$(ml adopt inkstone/ISEP#130 --via inkstone/Arcrun#86); RC=$?
[ "$RC" = 0 ] && ok "⑳ 同一張再 adopt 一次(Gitea 回 409)⇒ 仍是離開碼 0(冪等,不炸" || no "⑳ 重複 adopt 炸了" "$OUT"
[ "$RC" = 0 ] && ok "⑳ 同一張再 adopt 一次 ⇒ 仍是離開碼 0(冪等:先讀 /dependencies,邊在就不 POST;真 Gitea 對重複 POST 回 500" || no "⑳ 重複 adopt 炸了" "$OUT"
grep -c '^POST /api/v1/repos/inkstone/Arcrun/issues/86/dependencies' "$TMP/log" | grep -qx 1 && ok "⑳′ 第二次沒有再 POST(判準是 Gitea 有沒有這條邊,不是回應碼)" || no "⑳′ 重複 POST 了" "$(grep POST "$TMP/log")"
: > "$TMP/log"
OUT=$(ml adopt inkstone/arcrun-rag#37); RC=$?
[ "$RC" = 0 ] && grep -q '^POST /api/v1/repos/inkstone/InkStoneCo/issues/44/dependencies' "$TMP/log" \
+11 -2
View File
@@ -670,13 +670,22 @@ def _deps(owner, repo, num):
def add_dependency(parent, child):
"""讓 `child` 成為 `parent` 的相依(兩個都是 (owner, repo, num))。**冪等**邊已經在
Gitea 回 409)就當成功,回 False——`mainline adopt` 補收同一張票兩次不該炸。
"""讓 `child` 成為 `parent` 的相依(兩個都是 (owner, repo, num))。**冪等**
先讀 parent 的 `/dependencies`,邊已經在就不 POST、回 False——`mainline adopt`
補收同一張票兩次不該炸。
🔴 冪等**不能靠回應碼**2026-09-07 對真 Gitea 實測,重複相依回的是 **500**(不是想像中的 409
——判準改成「Gitea 現在有沒有這條邊」這個事實;409 仍容忍(別的版本可能這樣回)。
`subtask` 與 `mainline adopt`inkstone/ISEP#133)都走這一支:掛相依只有一條路,
而且走的是 `api()`——離線測試把 `api()` 換成錄音機時才錄得到它。"""
global _TOLERATE
po, pr, pn = parent
co, cr, cn = child
want = f"{co}/{cr}#{cn}"
have = api(f"/repos/{po}/{pr}/issues/{pn}/dependencies?limit=100") or []
for d in have:
full = ((d.get("repository") or {}).get("full_name")) if isinstance(d, dict) else None
if full and f"{full}#{d.get('number')}" == want:
return False # 已經是相依
_TOLERATE = (409,)
try:
r = api(f"/repos/{po}/{pr}/issues/{pn}/dependencies", {"owner": co, "repo": cr, "index": cn})