Files
WeiCHEN 9c7a5be209 feat(arch): clear G1 U3/U4 sql_write and path exceptions
Move document_records duty-slot writes into docgov, project ledger
heads through docgov/freedoc ports, and workspace deliverable I/O
through DeliverableStore so arch-check baseline drops to sql_write:1
path:13.
2026-09-22 21:40:12 +08:00

165 lines
5.7 KiB
Go

package docrevision_test
import (
"context"
"crypto/sha256"
"encoding/hex"
"os"
"path/filepath"
"testing"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/docrevision"
"chat-agent-system/internal/workspace"
)
func TestPendingHighRisk_DoesNotAdvanceFactSource_UntilConfirm(t *testing.T) {
ctx := context.Background()
dataDir := t.TempDir()
workDir := filepath.Join(dataDir, "ws-pending")
if err := os.MkdirAll(workDir, 0o755); err != nil {
t.Fatal(err)
}
docID := "doc_fact_pending"
oldMD := "# base\n\n法规引用 GB9706\n"
newMD := "# base\n\n" // deletes 法规 → high risk
// Simulate squad already writing unconfirmed body to fact source.
if err := os.WriteFile(filepath.Join(workDir, "output.md"), []byte(newMD), 0o644); err != nil {
t.Fatal(err)
}
oldSHA := sha256.Sum256([]byte(docrevision.CanonicalizeMarkdown(oldMD)))
oldSHAStr := hex.EncodeToString(oldSHA[:])
newSHA := sha256.Sum256([]byte(docrevision.CanonicalizeMarkdown(newMD)))
newSHAStr := hex.EncodeToString(newSHA[:])
eng := docgov.NewGovernanceEngine(nil)
if _, err := eng.CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", workDir, 1, "a", 2, "r", 3, "p"); err != nil {
t.Fatal(err)
}
_ = eng.MarkDocumentContentUpdated(ctx, docID, oldSHAStr, false)
rev := docrevision.NewService(nil)
rev.DataDir = dataDir
rev.SetDeliverableStore(workspace.DirDeliverable{})
st, err := rev.OpenTemplateVersion(ctx, docID, oldMD, docrevision.ActorSnapshot{DisplayName: "t"}, 1, workDir)
if err != nil {
t.Fatal(err)
}
mat, err := rev.MaterializeAgentOutput(ctx, docrevision.AgentMaterializeRequest{
DocID: docID, RunID: "run-pending-fact", OutputMarkdown: newMD,
ProjectID: 1, WorkDir: workDir, ActorKind: docrevision.ActorAgentSquad,
AutoCommitUserID: 1,
})
if err != nil {
t.Fatalf("materialize: %v", err)
}
if mat.Candidate == nil || mat.Candidate.Status != docrevision.CandidateNeedsConfirmation {
t.Fatalf("want needs_confirmation, got %+v", mat.Candidate)
}
// Restore fact source from ledger head (bizdev contract).
if err := rev.ProjectOutputMarkdown(docID, dataDir); err != nil {
t.Fatal(err)
}
if err := eng.MarkDocumentContentUpdated(ctx, docID, st.HeadSHA256, false); err != nil {
t.Fatal(err)
}
gotMD, err := os.ReadFile(filepath.Join(workDir, "output.md"))
if err != nil {
t.Fatal(err)
}
if docrevision.CanonicalizeMarkdown(string(gotMD)) != docrevision.CanonicalizeMarkdown(oldMD) {
t.Fatalf("pending must restore output.md to ledger head; got %q want old", string(gotMD))
}
doc, err := eng.GetDocument(ctx, docID)
if err != nil {
t.Fatal(err)
}
if doc.ContentSHA256 != st.HeadSHA256 {
t.Fatalf("document_records hash must stay on head while pending: got %s want %s", doc.ContentSHA256, st.HeadSHA256)
}
if doc.ContentSHA256 == newSHAStr {
t.Fatal("document_records must not carry unconfirmed candidate hash")
}
// Confirm → project new head.
st2, err := rev.CommitAgentVersion(ctx, docID, mat.Candidate.ID, docrevision.CommitAgentDecision{
Decision: "commit", Confirmed: true, ActorUserID: 1, ExpectedInputSHA: st.HeadSHA256,
})
if err != nil {
t.Fatal(err)
}
if err := rev.ProjectOutputMarkdown(docID, dataDir); err != nil {
t.Fatal(err)
}
if err := eng.MarkDocumentContentUpdated(ctx, docID, st2.HeadSHA256, false); err != nil {
t.Fatal(err)
}
gotAfter, _ := os.ReadFile(filepath.Join(workDir, "output.md"))
if docrevision.CanonicalizeMarkdown(string(gotAfter)) != docrevision.CanonicalizeMarkdown(newMD) {
t.Fatalf("after confirm output.md must be candidate body, got %q", string(gotAfter))
}
doc2, _ := eng.GetDocument(ctx, docID)
if doc2.ContentSHA256 != st2.HeadSHA256 {
t.Fatalf("after confirm hash want %s got %s", st2.HeadSHA256, doc2.ContentSHA256)
}
}
func TestPendingHighRisk_RejectKeepsOldHead(t *testing.T) {
ctx := context.Background()
dataDir := t.TempDir()
workDir := filepath.Join(dataDir, "ws-reject")
_ = os.MkdirAll(workDir, 0o755)
docID := "doc_fact_reject"
oldMD := "# base\n\n法规引用 YY0505\n"
newMD := "# base\n\n"
_ = os.WriteFile(filepath.Join(workDir, "output.md"), []byte(newMD), 0o644)
eng := docgov.NewGovernanceEngine(nil)
_, _ = eng.CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", workDir, 1, "a", 2, "r", 3, "p")
rev := docrevision.NewService(nil)
rev.DataDir = dataDir
rev.SetDeliverableStore(workspace.DirDeliverable{})
st, err := rev.OpenTemplateVersion(ctx, docID, oldMD, docrevision.ActorSnapshot{}, 1, workDir)
if err != nil {
t.Fatal(err)
}
mat, err := rev.MaterializeAgentOutput(ctx, docrevision.AgentMaterializeRequest{
DocID: docID, RunID: "run-rej", OutputMarkdown: newMD, WorkDir: workDir,
ActorKind: docrevision.ActorAgentSquad, AutoCommitUserID: 1,
})
if err != nil {
t.Fatal(err)
}
if mat.Candidate.Status != docrevision.CandidateNeedsConfirmation {
t.Fatalf("want needs_confirmation got %s", mat.Candidate.Status)
}
_ = rev.ProjectOutputMarkdown(docID, dataDir)
_ = eng.MarkDocumentContentUpdated(ctx, docID, st.HeadSHA256, false)
st2, err := rev.CommitAgentVersion(ctx, docID, mat.Candidate.ID, docrevision.CommitAgentDecision{
Decision: "reject", Confirmed: true, ActorUserID: 1,
})
if err != nil {
t.Fatal(err)
}
if err := rev.ProjectOutputMarkdown(docID, dataDir); err != nil {
t.Fatal(err)
}
_ = eng.MarkDocumentContentUpdated(ctx, docID, st2.HeadSHA256, false)
got, _ := os.ReadFile(filepath.Join(workDir, "output.md"))
if docrevision.CanonicalizeMarkdown(string(got)) != docrevision.CanonicalizeMarkdown(oldMD) {
t.Fatalf("reject must keep old head on disk, got %q", string(got))
}
if st2.CurrentVersionNo != 1 {
t.Fatalf("reject must not advance version, got V%d", st2.CurrentVersionNo)
}
if st2.PendingCandidateID != nil {
t.Fatalf("pending must clear after reject")
}
}