Files
Agent-System-Backend/internal/app/human_decision_test.go
T
weichenandCursor b740f8b6f6 Add optional proofs for an exact human structure decision
Why: A model-visible flag cannot prove that a person confirmed one proposal revision and payload hash.
Evidence: go test ./pkg/extension ./internal/app ./internal/server -run 'HumanDecision|SurfaceSnapshot|ResolveHuman|NaturalLanguage' passed locally.
Impact: SurfaceVersion 2.22.0-experimental adds HumanDecisionVerifier. Existing Host methods stay unchanged, and the base does not consume the proof.
Refs: W4b human confirmation design r1
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-15 02:33:37 +00:00

171 lines
6.3 KiB
Go

package app
import (
"testing"
"time"
"agent-system-backend/internal/cmdbus"
"agent-system-backend/internal/store"
"agent-system-backend/pkg/extension"
)
func openDecisionStore(t *testing.T) *store.Store {
t.Helper()
st, err := store.OpenSQLite(t.TempDir() + "/decision.db")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { st.Close() })
return st
}
func decisionHost(st *store.Store, plugin, actor, entrance string, now time.Time, grant *cmdbus.HumanGrant) inprocHost {
return inprocHost{
caps: hostcaps{st: st, now: func() time.Time { return now }},
pluginID: plugin, owner: actor, actor: actor, entrance: entrance, humanGrant: grant,
}
}
func binding(decision, hash string, revision int) extension.HumanDecisionBinding {
return extension.HumanDecisionBinding{
Audience: extension.HumanDecisionAudienceStructure, Actor: "alice", ProjectID: "p1",
ProposalID: "prop-1", ProposalRevision: revision, PayloadHash: hash, Decision: decision,
}
}
func TestHumanDecisionAuthenticatedResponse(t *testing.T) {
st := openDecisionStore(t)
now := time.Date(2026, 10, 15, 1, 0, 0, 0, time.UTC)
host := decisionHost(st, "rdm", "alice", "button", now, nil)
card, err := host.IssueHumanDecision(binding("", "hash-1", 1), "确认结构变更")
if err != nil {
t.Fatal(err)
}
answered, err := st.RespondHumanDecision("alice", card.CardID, "accept", now)
if err != nil {
t.Fatal(err)
}
host.humanGrant = &cmdbus.HumanGrant{CardID: answered.ID, ProofID: answered.ProofID, Actor: "alice", Decision: "accept"}
host.entrance = "card"
proof, err := host.VerifyHumanDecision(binding("accept", "hash-1", 1))
if err != nil {
t.Fatal(err)
}
if proof.ProofID == "" || proof.Issuer != "agent-system-backend" || proof.HumanActor != "alice" ||
proof.HITLResponseRef != card.CardID || proof.PayloadHash != "hash-1" {
t.Fatalf("proof = %+v", proof)
}
again, err := host.VerifyHumanDecision(binding("accept", "hash-1", 1))
if err != nil || again.ProofID != proof.ProofID {
t.Fatalf("second verify consumed or changed the proof: %v %+v", err, again)
}
stored, err := st.HumanDecision(card.CardID)
if err != nil || stored.Status != "responded" {
t.Fatalf("host consumed the proof, status=%s err=%v", stored.Status, err)
}
}
func TestHumanDecisionRejectsUntrustedDispatch(t *testing.T) {
st := openDecisionStore(t)
now := time.Date(2026, 10, 15, 1, 0, 0, 0, time.UTC)
host := decisionHost(st, "rdm", "alice", "button", now, nil)
card, err := host.IssueHumanDecision(binding("", "hash-1", 1), "确认")
if err != nil {
t.Fatal(err)
}
answered, err := st.RespondHumanDecision("alice", card.CardID, "accept", now)
if err != nil {
t.Fatal(err)
}
grant := &cmdbus.HumanGrant{CardID: answered.ID, ProofID: answered.ProofID, Actor: "alice", Decision: "accept"}
expect := binding("accept", "hash-1", 1)
agent := decisionHost(st, "rdm", "alice", "agent", now, grant)
if _, err := agent.VerifyHumanDecision(expect); err == nil {
t.Fatal("model tool dispatch verified a human decision")
}
forged := decisionHost(st, "rdm", "alice", "card", now, nil)
if _, err := forged.VerifyHumanDecision(expect); err == nil {
t.Fatal("forged entrance verified without a host grant")
}
if _, err := decisionHost(st, "rdm", "alice", "agent", now, nil).IssueHumanDecision(binding("", "hash-2", 1), "x"); err == nil {
t.Fatal("model tool issued a human decision card")
}
if _, err := decisionHost(st, "other", "alice", "card", now, grant).VerifyHumanDecision(expect); err == nil {
t.Fatal("another plugin reused the proof")
}
other := expect
other.ProposalID = "prop-2"
if _, err := decisionHost(st, "rdm", "alice", "card", now, grant).VerifyHumanDecision(other); err == nil {
t.Fatal("proof authorized a second proposal")
}
wrongActor := expect
wrongActor.Actor = "bob"
if _, err := decisionHost(st, "rdm", "bob", "card", now, grant).VerifyHumanDecision(wrongActor); err == nil {
t.Fatal("wrong actor verified")
}
wrongProject := expect
wrongProject.ProjectID = "p2"
if _, err := decisionHost(st, "rdm", "alice", "card", now, grant).VerifyHumanDecision(wrongProject); err == nil {
t.Fatal("wrong project verified")
}
wrongRev := expect
wrongRev.ProposalRevision = 2
if _, err := decisionHost(st, "rdm", "alice", "card", now, grant).VerifyHumanDecision(wrongRev); err == nil {
t.Fatal("wrong revision verified")
}
wrongHash := expect
wrongHash.PayloadHash = "hash-changed"
if _, err := decisionHost(st, "rdm", "alice", "card", now, grant).VerifyHumanDecision(wrongHash); err == nil {
t.Fatal("wrong payload hash verified")
}
if _, err := decisionHost(st, "rdm", "alice", "button", now, nil).IssueHumanDecision(
extension.HumanDecisionBinding{Audience: "other.plugin", Actor: "alice", ProjectID: "p1", ProposalID: "prop-1", ProposalRevision: 1, PayloadHash: "h"},
"x"); err == nil {
t.Fatal("foreign audience was issued")
}
}
func TestHumanDecisionCardChangeExpiryAndRevoke(t *testing.T) {
st := openDecisionStore(t)
now := time.Date(2026, 10, 15, 1, 0, 0, 0, time.UTC)
host := decisionHost(st, "rdm", "alice", "api", now, nil)
first, err := host.IssueHumanDecision(binding("", "hash-1", 1), "旧卡")
if err != nil {
t.Fatal(err)
}
if _, err := host.IssueHumanDecision(binding("", "hash-2", 2), "新卡"); err != nil {
t.Fatal(err)
}
if _, err := st.RespondHumanDecision("alice", first.CardID, "accept", now); err == nil {
t.Fatal("changed card still accepted the old confirmation")
}
current, err := host.IssueHumanDecision(binding("", "hash-3", 3), "待撤销")
if err != nil {
t.Fatal(err)
}
if err := host.RevokeHumanDecision(current.CardID); err != nil {
t.Fatal(err)
}
if _, err := st.RespondHumanDecision("alice", current.CardID, "accept", now); err == nil {
t.Fatal("revoked card was answered")
}
live, err := host.IssueHumanDecision(binding("", "hash-4", 4), "会过期")
if err != nil {
t.Fatal(err)
}
if _, err := st.RespondHumanDecision("alice", live.CardID, "accept", now.Add(16*time.Minute)); err == nil {
t.Fatal("expired card was answered")
}
}
func TestMissingHumanDecisionStoreFailsClosed(t *testing.T) {
host := decisionHost(nil, "rdm", "alice", "button", time.Now(), nil)
if _, err := host.IssueHumanDecision(binding("", "hash-1", 1), "x"); err == nil {
t.Fatal("missing store issued a card")
}
if _, err := host.VerifyHumanDecision(binding("accept", "hash-1", 1)); err == nil {
t.Fatal("missing store verified a decision")
}
}