fix(hitl): resume 记录 KV 键不可含冒号(NATS: invalid key)

live NATS 联调发现:resume 记录键用 "pending:"+taskID,冒号是 NATS JetStream KV
非法字符(仅允许 [-/_=.a-zA-Z0-9])→ 中断时 persistResume 的 Put 静默失败、决定
到达时 loadResume 报 "nats: invalid key",任务永卡 waiting、无法恢复。内存桩接受
任意键,故单测漏过——正是只有 live NATS 才暴露的那类。

- pendingKey: "pending:"+id → "pending_"+id(合法键)。
- persistResume: Put 失败改 log.Printf 大声告警(不止 exec 轨迹),关键失败可见。
- 回归测试 TestPendingKeyIsNATSValid:直接钉键形匹配 NATS KV 字符集,绕开内存桩盲区。

live 验证(devnats + 真链路):提交 HITL 任务→waiting→杀 dispatcher→离线批准→
重启→1s waiting→2s running→3s done,deepseek 真实出稿。证明 checkpoint 抗重启 +
决定经 JetStream 抗离线 + 断点恢复。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-29 14:25:47 +08:00
parent 515cf7f87a
commit 0378a770ca
2 changed files with 18 additions and 1 deletions
@@ -2,12 +2,25 @@ package eino
import (
"context"
"regexp"
"sync"
"testing"
"github.com/sundynix/sundynix-shared/bus"
)
// natsKVKey 是 NATS JetStream KV 允许的键字符集(首字符不可为 . 或 _)。
// resume 记录键曾用冒号分隔 → 真 NATS 报 "invalid key",内存桩测不到,故在此直接钉键形。
var natsKVKey = regexp.MustCompile(`^[a-zA-Z0-9][-/_=.a-zA-Z0-9]*$`)
func TestPendingKeyIsNATSValid(t *testing.T) {
for _, id := range []string{"task_4c47de5ce1a29e16", "abc123", "t_0"} {
if k := pendingKey(id); !natsKVKey.MatchString(k) {
t.Fatalf("pendingKey(%q)=%q 不是合法 NATS KV 键(冒号等字符会被拒)", id, k)
}
}
}
// memKV 是 CheckpointKV 的内存桩(并发安全),用于不依赖 NATS 的单测。
type memKV struct {
mu sync.Mutex