mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-10-30 22:07:28 +00:00
## 缺陷(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":` 本身的长度再找。
177 lines
6.4 KiB
Go
177 lines
6.4 KiB
Go
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)
|
||
}
|
||
}
|
||
}
|