Files
homeagent-sdk/example/commondev/delivery_backfill_test.go
JianFeeeee c9b7a6534c fix(commondev): 完成通知回填投递台账(原先永远停在 accepted)
## 缺陷(2026-10-18 本机实例日志实测)

异步投递上线后,台账里的记录全是 accepted、result 永远为空:

	07:39:09 accepted result=None
	07:40:09 accepted result=None
	07:41:18 accepted result=None     ← 13 条全是

而回投本身**是正常的**([plugin] 日志有 5 次 `回投任务结果…event=prompt_done`)。
缺的是:完成通知到达后没人把它写回台账。

实测后果(同一天日志,agent 自己起的轮询):

	[timer] firing: 【轮询轮次 2/6】…检查是否有新 result 回填…

agent 为此安排了 6 轮 timer 去等一个**永远不会出现**的回填,白等 30 分钟。
更根本的是:看板与任何查询方都拿不到任务结果。

## 修法

`completeDelivery`:把完成/失败结果回填到该 branch 下**最新一条**
未回填的 accepted。`recordDelivery` 改为返回 key(追溯用)。

匹配用"最新未完成"而不是按 key:完成通知里没有 key(sidecar 不知道它),
而同一 branch 的投递是串行投给同一个 pi 会话的,最新未完成那条就是它。

## 判据(6 条)

回填最新 / 不串 branch / 不覆盖已回填 / 无匹配不 panic / 返回 key /
**完成通知真的调用了回填**(接线判据)

## ★ 写判据时踩的两个坑(都记在注释里)

1. **判据没覆盖接线 → 变异不咬**:前 5 条测的是 completeDelivery 自身行为。
   把 sidecar.go 里**那一行调用删掉**,它们依然全绿 —— 而"函数写得对但
   没人调"正是原始缺陷的形态。补了第 6 条读源码验接线。

2. **偏移错位导致假绿**:分段用 `seg := block[k:]` 后找 "case ",会命中
   k 自己(n=0)⇒ seg 不截断、含整个函数剩余部分 ⇒ done 分支删掉回填也照样
   "找到" error 分支那一行。改成跳过 `case "x":` 本身的长度再找。
2026-10-18 07:53:45 +08:00

177 lines
6.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
// 投递台账**结果回填**的判据(2026-10-18 生产实测缺陷)。
//
// ## 缺陷
//
// 异步投递后,投递那一刻只记 "accepted";真实结果几十秒后才由完成通知
// 送回。但原先**没人把它写回台账** ⇒ 台账永远停在 accepted:
//
// 2026-10-18T07:39:09 accepted result=None
// 2026-10-18T07:40:09 accepted result=None
// 2026-10-18T07:41:18 accepted result=None ← 13 条全是 None
//
// 实测后果(同一天的日志):
//
// [timer] firing: 【轮询轮次 2/6】…检查是否有新 result 回填…
//
// agent 自己起了 6 轮 timer 去轮询"有没有新 result"——而那件事永远不会发生。
//
// ## 判据
//
// 1. 完成通知必须回填最新那条 accepted
// 2. 只回填该 branch 的,不串到别的工程线
// 3. 已回填的不会被第二条通知重复覆盖(幂等边界)
// 4. 无匹配时不能 panic / 不能误改别的记录
import (
"os"
"strings"
"testing"
)
func newLedgerPlugin(t *testing.T) *Plugin {
t.Helper()
dir := t.TempDir()
p := &Plugin{path: dir + "/plugin.bin", ledger: &Ledger{}}
p.ledger.Branches = map[string]*Branch{}
return p
}
// ★★★ 判据:完成通知回填最新 accepted。
func TestCompleteDeliveryBackfillsLatestAccepted(t *testing.T) {
p := newLedgerPlugin(t)
p.recordDelivery("br-a", "sess-1", "任务一", "accepted", "")
p.recordDelivery("br-a", "sess-1", "任务二", "accepted", "")
p.completeDelivery("br-a", "sess-2", "done", "改好了 multiply")
ds := p.ledger.Deliveries
if len(ds) != 2 {
t.Fatalf("投递条数应不变(2),实际 %d", len(ds))
}
// 最新那条被回填
if ds[1].Status != "done" || ds[1].Result != "改好了 multiply" {
t.Fatalf("★ 最新 accepted 未回填:status=%q result=%q", ds[1].Status, ds[1].Result)
}
if ds[1].SessionID != "sess-2" {
t.Errorf("session 应更新为 sess-2,实际 %q", ds[1].SessionID)
}
// 更早那条不能被误改
if ds[0].Status != "accepted" || ds[0].Result != "" {
t.Fatalf("★ 误改了更早的记录:status=%q result=%q", ds[0].Status, ds[0].Result)
}
}
// ★★★ 判据:只回填同一 branch,不串线。
func TestCompleteDeliveryDoesNotCrossBranches(t *testing.T) {
p := newLedgerPlugin(t)
p.recordDelivery("br-a", "s1", "a 的任务", "accepted", "")
p.recordDelivery("br-b", "s2", "b 的任务", "accepted", "")
p.completeDelivery("br-a", "s1", "done", "a 完成")
if p.ledger.Deliveries[0].Status != "done" {
t.Fatal("br-a 的记录应被回填")
}
if p.ledger.Deliveries[1].Status != "accepted" {
t.Fatalf("★ br-b 的记录被误改:status=%q", p.ledger.Deliveries[1].Status)
}
}
// ★★★ 判据:已回填的记录不会被第二条通知覆盖(同一投递只认第一次结果)。
func TestCompleteDeliveryIsNotOverwritten(t *testing.T) {
p := newLedgerPlugin(t)
p.recordDelivery("br-a", "s1", "任务", "accepted", "")
p.completeDelivery("br-a", "s1", "done", "第一次结果")
p.completeDelivery("br-a", "s1", "failed", "第二次结果")
d := p.ledger.Deliveries[0]
if d.Status != "done" || d.Result != "第一次结果" {
t.Fatalf("★ 已回填的记录被第二条通知覆盖:status=%q result=%q", d.Status, d.Result)
}
}
// ★★★ 判据:没有待回填记录时不 panic、不改任何东西。
func TestCompleteDeliveryNoMatchIsSafe(t *testing.T) {
p := newLedgerPlugin(t)
p.recordDelivery("br-a", "s1", "任务", "failed", "同步就失败了")
// 这条是 failed,不是 accepted ⇒ 不该被回填
p.completeDelivery("br-a", "s1", "done", "迟到的通知")
if p.ledger.Deliveries[0].Status != "failed" {
t.Fatalf("★ 非 accepted 的记录被回填了:%q", p.ledger.Deliveries[0].Status)
}
// 完全无记录也不该 panic
p.completeDelivery("no-such-branch", "", "done", "x")
}
// ★★★ 判据:recordDelivery 必须返回 key(回填/追溯的前提)。
func TestRecordDeliveryReturnsKey(t *testing.T) {
p := newLedgerPlugin(t)
k := p.recordDelivery("br-a", "s1", "任务", "accepted", "")
if k == "" {
t.Fatal("recordDelivery 应返回非空 key(否则无法追溯某次投递)")
}
if p.ledger.Deliveries[0].Key != k {
t.Fatalf("返回的 key 与台账记录不一致:%q vs %q", k, p.ledger.Deliveries[0].Key)
}
}
// ★★★ 判据:**完成通知必须真的调用回填**(接线,不只是函数存在)。
//
// 为什么需要这条(2026-10-18 实测判据漏洞):
// 上面四条测的是 `completeDelivery` 自身行为。但我把 sidecar.go 里
// **那一行调用删掉**之后,它们依然全绿 —— 因为没人再调它了。
// 而这正是原始缺陷的形态:函数写得对,但没人用。
//
// ⇒ 判据必须覆盖接线。这里读 sidecar.go 的 `makePromptNotifier` 块,
//
// 要求 prompt_done / prompt_error 两条分支里都出现 completeDelivery 调用。
//
// ★ 用**行扫描界定块边界**而不是正则:正则遇到嵌套大括号容易提前结束
//
// (本仓在别处踩过这个坑)。
func TestPromptNotifierWiresCompleteDelivery(t *testing.T) {
b, err := os.ReadFile("sidecar.go")
if err != nil {
t.Fatal(err)
}
src := string(b)
i := strings.Index(src, "func (p *Plugin) makePromptNotifier")
if i < 0 {
t.Fatal("找不到 makePromptNotifier")
}
block := src[i:]
if j := strings.Index(block[1:], "\nfunc "); j > 0 {
block = block[:j+1]
}
for _, ev := range []string{"prompt_done", "prompt_error"} {
k := strings.Index(block, `case "`+ev+`":`)
if k < 0 {
t.Fatalf("★ makePromptNotifier 里没有 %s 分支", ev)
}
// 该分支到**下一个** case 之间必须出现回填调用。
//
// ★ 偏移必须跳过当前 "case " 本身:从 k 开始找 "case " 会命中
// k 自己(n=0),于是 seg 不被截断、包含整个函数剩余部分 ——
// 那样 prompt_done 分支即便**删掉**回填调用也照样能"找到"
// prompt_error 分支里的那一行 ⇒ 判据假绿。
// 实测踩过:变异删掉 done 分支的回填,判据仍然 PASS。
seg := block[k:]
lead := len(`case "` + ev + `":`)
if n := strings.Index(seg[lead:], "case "); n >= 0 {
seg = seg[:lead+n]
}
if !strings.Contains(seg, "p.completeDelivery(") {
t.Fatalf("★ %s 分支**没有调用 completeDelivery** —— "+
"结果是:台账永远停在 accepted,看板与 agent 都查不到任何结果\n"+
"(实测后果:agent 起了 6 轮 timer 空等一个永远不会出现的结果)", ev)
}
}
}