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.
165 lines
5.7 KiB
Go
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")
|
|
}
|
|
}
|