- 5 independent flags (MULTI_INTENT_U11_LIVE_REVIEW/FINAL/PROPOSAL, U12_LIVE_SWITCH/RESTART), default OFF; no full rollout - DocGovLiveActionKeys/RunControlLiveActionKeys become flag-driven functions; baseline keys unchanged - DocGovOwner routes review.approve/reject, final.approve/reject, proposal.decide to real engine with slot-driven reason/approve parsing and idempotent receipts - RunControlOwner routes workflow.switch -> ActivateWorkflow and run.restart -> fresh CreateAndStart run - policy: 7 keys promoted from TierFull to TierConfirm (real execution still gated per-key) - tests: default-off isolation, per-key independence, live state advance reviewing->approving, dry-run zero-advance when flag off
200 lines
6.3 KiB
Go
200 lines
6.3 KiB
Go
package app
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
|
||
"chat-agent-system/internal/app/intentflow"
|
||
"chat-agent-system/internal/docgov"
|
||
)
|
||
|
||
// P8 逐键放量:默认 flag 全关时,新键不得进入 live 集合(仍 dry-run validated_only)。
|
||
func TestP8LiveKeysDefaultOff(t *testing.T) {
|
||
intentflow.SetFeatureFlagsForTest(intentflow.FeatureFlags{
|
||
PlanTier: intentflow.TierConfirm,
|
||
})
|
||
defer intentflow.ResetFeatureFlagsForTest()
|
||
|
||
for _, key := range []string{
|
||
"document.review.approve", "document.review.reject",
|
||
"document.final.approve", "document.final.reject",
|
||
"document.proposal.decide",
|
||
} {
|
||
if intentflow.IsDocGovLiveKey(key) {
|
||
t.Fatalf("docgov key %s must stay dry-run when LIVE flags off", key)
|
||
}
|
||
}
|
||
for _, key := range []string{"workflow.switch", "run.restart"} {
|
||
if intentflow.IsRunControlLiveKey(key) {
|
||
t.Fatalf("run-control key %s must stay dry-run when LIVE flags off", key)
|
||
}
|
||
}
|
||
// 基线键不受 per-key flag 影响。
|
||
for _, key := range []string{"document.claim", "document.review.submit"} {
|
||
if !intentflow.IsDocGovLiveKey(key) {
|
||
t.Fatalf("baseline docgov key %s must stay live", key)
|
||
}
|
||
}
|
||
for _, key := range []string{"workflow.run", "run.resume"} {
|
||
if !intentflow.IsRunControlLiveKey(key) {
|
||
t.Fatalf("baseline run-control key %s must stay live", key)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 逐键 flag 打开后对应键进入 live 集合,其余仍关闭(禁 full 一次放量)。
|
||
func TestP8LiveKeysPerKeyFlags(t *testing.T) {
|
||
intentflow.SetFeatureFlagsForTest(intentflow.FeatureFlags{
|
||
PlanTier: intentflow.TierConfirm,
|
||
U11LiveReview: true,
|
||
U11LiveFinal: true,
|
||
U11LiveProposal: true,
|
||
U12LiveSwitch: true,
|
||
U12LiveRestart: true,
|
||
})
|
||
defer intentflow.ResetFeatureFlagsForTest()
|
||
|
||
for _, key := range []string{
|
||
"document.review.approve", "document.review.reject",
|
||
"document.final.approve", "document.final.reject",
|
||
"document.proposal.decide",
|
||
} {
|
||
if !intentflow.IsDocGovLiveKey(key) {
|
||
t.Fatalf("docgov key %s must be live when flag on", key)
|
||
}
|
||
}
|
||
for _, key := range []string{"workflow.switch", "run.restart"} {
|
||
if !intentflow.IsRunControlLiveKey(key) {
|
||
t.Fatalf("run-control key %s must be live when flag on", key)
|
||
}
|
||
}
|
||
|
||
// 只开 review:final/proposal 仍 dry-run。
|
||
intentflow.SetFeatureFlagsForTest(intentflow.FeatureFlags{
|
||
PlanTier: intentflow.TierConfirm,
|
||
U11LiveReview: true,
|
||
})
|
||
if !intentflow.IsDocGovLiveKey("document.review.approve") {
|
||
t.Fatal("review.approve should be live")
|
||
}
|
||
if intentflow.IsDocGovLiveKey("document.final.approve") {
|
||
t.Fatal("final.approve must stay dry-run when only review flag on")
|
||
}
|
||
if intentflow.IsDocGovLiveKey("document.proposal.decide") {
|
||
t.Fatal("proposal.decide must stay dry-run when only review flag on")
|
||
}
|
||
}
|
||
|
||
// review.approve 打开后走真实引擎:文档从 in_review → pending_approval。
|
||
func TestP8DocGovReviewApproveLive(t *testing.T) {
|
||
intentflow.SetFeatureFlagsForTest(intentflow.FeatureFlags{
|
||
PlanTier: intentflow.TierConfirm,
|
||
U11DocGov: true,
|
||
U11LiveReview: true,
|
||
})
|
||
defer intentflow.ResetFeatureFlagsForTest()
|
||
|
||
engine := docgov.NewGovernanceEngine(nil)
|
||
ctx := context.Background()
|
||
doc, err := engine.CreateOrGetDocument(ctx, "doc_p8_review", 9, "intake", "t", "V0.5", "", 1, "A", 2, "R", 3, "P")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := engine.SubmitForReview(ctx, doc.DocID, 1, docgov.RoleAuthor, "A"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
cur, err := engine.GetDocument(ctx, doc.DocID)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
owner := NewDocGovOwner(engine, func(context.Context, int64, int64) error { return nil }, nil)
|
||
rec, err := owner.Execute(ctx, intentflow.StepExec{
|
||
ActionKey: "document.review.approve",
|
||
IdempotencyKey: "p8-review-approve",
|
||
ActionDigest: "d1",
|
||
ExpectedResourceVersion: itoa(cur.Version),
|
||
Principal: intentflow.Principal{UserID: 2, ProjectID: 9, SessionID: "s"},
|
||
SafeSummary: doc.DocID,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("review approve live: %v", err)
|
||
}
|
||
if rec.ResultRef != "docgov:"+doc.DocID {
|
||
t.Fatalf("unexpected ref %s", rec.ResultRef)
|
||
}
|
||
after, _ := engine.GetDocument(ctx, doc.DocID)
|
||
if after.CurrentState != docgov.StateApproving {
|
||
t.Fatalf("state after approve = %s, want %s", after.CurrentState, docgov.StateApproving)
|
||
}
|
||
|
||
// 幂等重放同键返回同一收据。
|
||
rec2, err := owner.Execute(ctx, intentflow.StepExec{
|
||
ActionKey: "document.review.approve",
|
||
IdempotencyKey: "p8-review-approve",
|
||
ActionDigest: "d1",
|
||
ExpectedResourceVersion: itoa(cur.Version),
|
||
Principal: intentflow.Principal{UserID: 2, ProjectID: 9, SessionID: "s"},
|
||
SafeSummary: doc.DocID,
|
||
})
|
||
if err != nil || rec2.IdempotencyKey != rec.IdempotencyKey {
|
||
t.Fatalf("idempotent replay: rec=%+v err=%v", rec2, err)
|
||
}
|
||
}
|
||
|
||
// flag 关闭时 review.approve 落 dry-run(Forbidden 而非真实状态推进)。
|
||
func TestP8DocGovReviewApproveDryRunWhenFlagOff(t *testing.T) {
|
||
intentflow.SetFeatureFlagsForTest(intentflow.FeatureFlags{
|
||
PlanTier: intentflow.TierConfirm,
|
||
U11DocGov: true,
|
||
})
|
||
defer intentflow.ResetFeatureFlagsForTest()
|
||
|
||
engine := docgov.NewGovernanceEngine(nil)
|
||
ctx := context.Background()
|
||
doc, err := engine.CreateOrGetDocument(ctx, "doc_p8_dry", 9, "intake", "t", "V0.5", "", 1, "A", 2, "R", 3, "P")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
owner := NewDocGovOwner(engine, func(context.Context, int64, int64) error { return nil }, nil)
|
||
rec, err := owner.Execute(ctx, intentflow.StepExec{
|
||
ActionKey: "document.review.approve",
|
||
IdempotencyKey: "p8-dry",
|
||
ActionDigest: "d1",
|
||
ExpectedResourceVersion: "1",
|
||
Principal: intentflow.Principal{UserID: 2, ProjectID: 9, SessionID: "s"},
|
||
SafeSummary: doc.DocID,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("dry-run execute: %v", err)
|
||
}
|
||
if rec.ResultRef == "docgov:"+doc.DocID {
|
||
t.Fatal("flag off must not hit real engine receipt")
|
||
}
|
||
after, _ := engine.GetDocument(ctx, doc.DocID)
|
||
if after.CurrentState == docgov.StateApproving {
|
||
t.Fatal("dry-run must not advance document state")
|
||
}
|
||
}
|
||
|
||
func itoa(v int) string {
|
||
if v == 0 {
|
||
return "0"
|
||
}
|
||
neg := v < 0
|
||
if neg {
|
||
v = -v
|
||
}
|
||
var b [20]byte
|
||
i := len(b)
|
||
for v > 0 {
|
||
i--
|
||
b[i] = byte('0' + v%10)
|
||
v /= 10
|
||
}
|
||
if neg {
|
||
i--
|
||
b[i] = '-'
|
||
}
|
||
return string(b[i:])
|
||
}
|