Files
weichen 5a5a91dd7f feat(docrevision): keep candidate estimates separate from committed reports
A candidate used the same report later attached to the version, so a preview could be treated as the formal result. Proposed reports now close on replace or reject, and only the commit writes an authoritative successor.
2026-09-20 05:15:41 +00:00

151 lines
5.0 KiB
Go

package docrevision_test
import (
"context"
"testing"
"chat-agent-system/internal/docrevision"
)
func TestProposedReportStaysInactiveUntilCommit(t *testing.T) {
svc := docrevision.NewService(nil)
svc.Notify = true
var events []string
committed := false
svc.OnOutbox = func(_ context.Context, eventType string, payload map[string]any) error {
events = append(events, eventType)
if !committed && (eventType == "document.downstream_impact_detected" || eventType == "document.agent_version_committed") {
t.Fatalf("candidate path emitted %s payload=%v", eventType, payload)
}
return nil
}
ctx := context.Background()
docID := "doc_proposed_split"
st, err := svc.OpenTemplateVersion(ctx, docID, "# base\n\nbody\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, "")
if err != nil {
t.Fatal(err)
}
first, firstRep, err := svc.CreateAgentCandidate(ctx, docrevision.AgentCandidateRequest{
DocID: docID, RunID: "run-a", InputSnapshotID: st.HeadSnapshotID,
OutputMarkdown: "# base\n\nbody changed\n",
ActorKind: docrevision.ActorAgent,
})
if err != nil || first == nil || firstRep == nil {
t.Fatalf("first candidate: cand=%v rep=%v err=%v", first, firstRep, err)
}
if firstRep.Authority != docrevision.ReportProposed || firstRep.Lifecycle != docrevision.ReportActive || !firstRep.AllowsSuggestedActions() {
t.Fatalf("first report = %+v", firstRep)
}
for _, ev := range events {
if ev != "document.agent_candidate_ready" {
t.Fatalf("unexpected event before commit: %s", ev)
}
}
second, secondRep, err := svc.CreateAgentCandidate(ctx, docrevision.AgentCandidateRequest{
DocID: docID, RunID: "run-b", InputSnapshotID: st.HeadSnapshotID,
OutputMarkdown: "# base\n\nbody changed again\n",
ActorKind: docrevision.ActorAgent,
})
if err != nil || second == nil {
t.Fatalf("second candidate: %v err=%v", second, err)
}
again, err := svc.GetChangeReport(ctx, docID, firstRep.ID)
if err != nil {
t.Fatal(err)
}
if again.Lifecycle != docrevision.ReportSuperseded || again.AllowsSuggestedActions() {
t.Fatalf("older estimate must be closed: %+v", again)
}
if secondRep.Lifecycle != docrevision.ReportActive || !secondRep.AllowsSuggestedActions() {
t.Fatalf("newer estimate must stay active: %+v", secondRep)
}
events = nil
committed = true
st2, err := svc.CommitAgentVersion(ctx, docID, second.ID, docrevision.CommitAgentDecision{
Decision: "commit", ActorUserID: 1, ExpectedInputSHA: st.HeadSHA256,
})
if err != nil {
t.Fatal(err)
}
if st2.CurrentVersionNo != 2 {
t.Fatalf("version = %+v", st2)
}
committedProposed, err := svc.GetChangeReport(ctx, docID, secondRep.ID)
if err != nil {
t.Fatal(err)
}
if committedProposed.Lifecycle != docrevision.ReportCommitted || committedProposed.AllowsSuggestedActions() {
t.Fatalf("proposed report must stop accepting actions: %+v", committedProposed)
}
if committedProposed.SuccessorReportID == nil || *committedProposed.SuccessorReportID == secondRep.ID {
t.Fatalf("missing distinct successor: %+v", committedProposed)
}
auth, err := svc.GetChangeReport(ctx, docID, *committedProposed.SuccessorReportID)
if err != nil {
t.Fatal(err)
}
if auth.Authority != docrevision.ReportAuthoritative || !auth.AllowsSuggestedActions() {
t.Fatalf("authoritative report = %+v", auth)
}
versions, err := svc.ListVersions(ctx, docID)
if err != nil {
t.Fatal(err)
}
var sawAuth bool
for _, v := range versions {
if v.ChangeReportID == nil {
continue
}
if *v.ChangeReportID == secondRep.ID {
t.Fatal("version must not point at the proposed estimate")
}
if *v.ChangeReportID == auth.ID {
sawAuth = true
}
}
if !sawAuth {
t.Fatalf("version missing authoritative report id %d in %+v", auth.ID, versions)
}
for _, ev := range events {
if ev == "document.agent_version_committed" {
t.Fatal("docrevision must not emit the formal commit event itself")
}
}
}
func TestRejectedProposedReportCannotSuggest(t *testing.T) {
svc := docrevision.NewService(nil)
ctx := context.Background()
docID := "doc_proposed_reject"
st, err := svc.OpenTemplateVersion(ctx, docID, "# base\n\n法规引用 GB9706\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, "")
if err != nil {
t.Fatal(err)
}
cand, rep, err := svc.CreateAgentCandidate(ctx, docrevision.AgentCandidateRequest{
DocID: docID, RunID: "run-reject", InputSnapshotID: st.HeadSnapshotID,
OutputMarkdown: "# base\n\n",
ActorKind: docrevision.ActorAgent,
})
if cand == nil || rep == nil {
t.Fatalf("cand=%v rep=%v err=%v", cand, rep, err)
}
if _, err := svc.CommitAgentVersion(ctx, docID, cand.ID, docrevision.CommitAgentDecision{Decision: "reject", ActorUserID: 1}); err != nil {
t.Fatal(err)
}
got, err := svc.GetChangeReport(ctx, docID, rep.ID)
if err != nil {
t.Fatal(err)
}
if got.Lifecycle != docrevision.ReportRejected || got.AllowsSuggestedActions() {
t.Fatalf("rejected report = %+v", got)
}
versions, _ := svc.ListVersions(ctx, docID)
for _, v := range versions {
if v.VersionNo > 1 {
t.Fatalf("reject must not open a new version: %+v", versions)
}
}
}