Files
Agent-System/internal/bizdev/quality_projection_test.go
T
weichenandCursor f4aa0da14f test(docgov): lock quality projection, stage halt, and uncontrolled auto-repair
补齐 QualityReport 拦截投影与阶段停链断言,并证明 CLI 非受控路径 AutoRepair=false 不会追加 repair 步。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 15:20:42 +00:00

198 lines
6.8 KiB
Go

package bizdev
import (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"time"
"chat-agent-system/internal/config"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/docrevision"
"chat-agent-system/internal/squad"
)
func TestAutoRepairForControlledSquad(t *testing.T) {
if autoRepairForControlledSquad(false, "", nil) {
t.Fatal("uncontrolled must respect AutoRepair=false")
}
if !autoRepairForControlledSquad(true, "", nil) {
t.Fatal("uncontrolled AutoRepair=true must stay true")
}
if !autoRepairForControlledSquad(false, "doc_proj1_intake", &docgov.DocumentRecord{DocID: "doc_proj1_intake"}) {
t.Fatal("resolved controlled doc must force AutoRepair")
}
if autoRepairForControlledSquad(false, "doc_proj1_intake", nil) {
t.Fatal("docID without docRec must not force AutoRepair")
}
}
func TestQualityBlocksControlledProjection(t *testing.T) {
if qualityBlocksControlledProjection(nil) {
t.Fatal("nil report must not block")
}
if qualityBlocksControlledProjection(&squad.QualityReport{Passed: true}) {
t.Fatal("passed report must not block")
}
if !qualityBlocksControlledProjection(&squad.QualityReport{Passed: false, Score: 40}) {
t.Fatal("failed report must block")
}
}
// 敌意输入:Success=true 但 QualityReport.Passed=false —— 仍不得投影到正式受控事实源。
func TestFinalizeSquadDeliverable_QualityFailed_NoDocProjectionSideEffects(t *testing.T) {
dataDir := t.TempDir()
svc := MustNewService(t, &config.Config{DataDir: dataDir, AgentBackend: "stub"})
ctx := context.Background()
projectID := int64(6098)
docID := "doc_proj6098_intake"
officialWD := filepath.Join(dataDir, "official-head")
if err := os.MkdirAll(officialWD, 0o755); err != nil {
t.Fatal(err)
}
base := "# 正式受控 head\n\n本正文为账本事实源,质量失败后不得被未过门禁稿覆盖。\n"
if _, err := svc.DocRevision().OpenTemplateVersion(ctx, docID, base, docrevision.ActorSnapshot{DisplayName: "baseline"}, projectID, officialWD); err != nil {
t.Fatal(err)
}
docRec, err := svc.DocGov().CreateOrGetDocument(ctx, docID, projectID, "intake", "用户初始需求收集表", "V0.5", officialWD, 1, "a", 2, "r", 3, "p")
if err != nil {
t.Fatal(err)
}
stBefore, err := svc.DocRevision().GetRevisionState(ctx, docID)
if err != nil {
t.Fatal(err)
}
versBefore, err := svc.DocRevision().ListVersions(ctx, docID)
if err != nil {
t.Fatal(err)
}
beforeSHA := docRec.ContentSHA256
beforeWD := docRec.WorkDir
beforeHead := stBefore.HeadSHA256
beforeRev := stBefore.HeadRevisionNo
beforeVer := stBefore.CurrentVersionNo
if stBefore.PendingCandidateID != nil {
t.Fatalf("baseline must not already have pending candidate: %v", *stBefore.PendingCandidateID)
}
isolatedWD := filepath.Join(dataDir, "isolated-quality-fail")
if err := os.MkdirAll(isolatedWD, 0o755); err != nil {
t.Fatal(err)
}
failBody := "# 未过门禁草稿\n\n此处篇幅有限,不做展开。\n"
out, err := svc.finalizeSquadDeliverable(ctx, TaskRunRequest{
TaskKey: "intake", ProjectID: projectID, ProjectName: "P",
CustomerName: "C", DeviceType: "医疗器械", UserID: 1, DocID: docID,
}, &squad.SquadRunResult{
TaskKey: "intake",
ArtifactTitle: "用户初始需求收集表",
MarkdownBody: failBody,
WorkDir: isolatedWD,
BackendUsed: "stub",
Duration: time.Second,
// 敌意:Success 误为 true,旧路径若只看 Success 会 Materialize
Success: true,
QualityReport: &squad.QualityReport{
Passed: false,
Score: 40,
ProhibitedPhrases: []string{"不做展开", "篇幅有限"},
Violations: []string{"检测到正文中的省略/敷衍写法(须实质展开;自检勿复述禁用原词)"},
},
}, docID, docRec, "", nil)
if err != nil {
t.Fatalf("quality fail must return nil error, got %v", err)
}
if out == nil || out.Success {
t.Fatalf("want Success=false, got %+v", out)
}
if out.TerminalStatus() != TaskStatusQualityFailed {
t.Fatalf("status=%q want quality_failed", out.TerminalStatus())
}
if out.WorkDir != isolatedWD {
t.Fatalf("must keep isolated workspace, got %q", out.WorkDir)
}
if !strings.Contains(out.ErrorReason, "质量门禁未通过") {
t.Fatalf("ErrorReason=%q", out.ErrorReason)
}
if out.MarkdownBody != failBody {
t.Fatal("failure summary body must remain visible")
}
afterDoc, err := svc.DocGov().GetDocument(ctx, docID)
if err != nil {
t.Fatal(err)
}
if afterDoc.WorkDir != beforeWD {
t.Fatalf("formal work_dir mutated: before=%q after=%q", beforeWD, afterDoc.WorkDir)
}
if afterDoc.ContentSHA256 != beforeSHA {
t.Fatalf("formal content hash mutated: before=%q after=%q", beforeSHA, afterDoc.ContentSHA256)
}
stAfter, err := svc.DocRevision().GetRevisionState(ctx, docID)
if err != nil {
t.Fatal(err)
}
if stAfter.HeadSHA256 != beforeHead || stAfter.HeadRevisionNo != beforeRev || stAfter.CurrentVersionNo != beforeVer {
t.Fatalf("ledger head mutated: before=%s rev=%d ver=%d after=%s rev=%d ver=%d",
beforeHead, beforeRev, beforeVer, stAfter.HeadSHA256, stAfter.HeadRevisionNo, stAfter.CurrentVersionNo)
}
if stAfter.PendingCandidateID != nil {
t.Fatalf("quality_failed must not create pending candidate: %v", *stAfter.PendingCandidateID)
}
versAfter, err := svc.DocRevision().ListVersions(ctx, docID)
if err != nil {
t.Fatal(err)
}
if len(versAfter) != len(versBefore) {
t.Fatalf("version count changed: before=%d after=%d", len(versBefore), len(versAfter))
}
for _, title := range activityTitles(t, svc, projectID) {
if strings.Contains(title, "完成交付物") || strings.Contains(title, "待编制人确认") {
t.Fatalf("quality_failed must not record delivery activity: %q", title)
}
}
}
func TestExecuteTask_Uncontrolled_DoesNotForceAutoRepair(t *testing.T) {
dataDir := t.TempDir()
svc := MustNewService(t, &config.Config{DataDir: dataDir, AgentBackend: "stub"})
res, err := svc.ExecuteTask(context.Background(), TaskRunRequest{
TaskKey: "intake",
ProjectID: 0,
DocID: "",
AutoRepair: false,
ProjectName: "CLI非受控",
CustomerName: "C",
DeviceType: "医疗器械",
Backend: "stub",
Timeout: 10 * time.Second,
})
if err != nil || res == nil || !res.Success {
t.Fatalf("uncontrolled ExecuteTask should succeed, err=%v res=%+v", err, res)
}
if res.DocRecord != nil {
t.Fatal("uncontrolled path must not create DocRecord")
}
raw, readErr := os.ReadFile(filepath.Join(res.WorkDir, "workflow.json"))
if readErr != nil {
t.Fatalf("read workflow.json: %v", readErr)
}
var wf struct {
Steps []struct {
ID string `json:"id"`
} `json:"steps"`
}
if err := json.Unmarshal(raw, &wf); err != nil {
t.Fatalf("workflow.json: %v", err)
}
for _, s := range wf.Steps {
if strings.HasPrefix(s.ID, "step_repair_") {
t.Fatalf("AutoRepair=false uncontrolled must not append repair step %q", s.ID)
}
}
}