Files
ARTEX/db/intent_intervention_lifecycle_test.go
T
2026-08-29 21:55:27 +08:00

205 lines
7.7 KiB
Go

package db
import (
"encoding/json"
"errors"
"sync"
"testing"
)
func newRunningInterventionIntent(t *testing.T, d *DB, label string) (*ExplorationStore, int64, func()) {
t.Helper()
task, err := d.CreateTask("worker intervention "+label, "verify intervention protocol", nil, 0, 0)
if err != nil {
t.Fatal(err)
}
store := d.Exploration(task.ExplorationID)
intentID, err := store.AddIntent(map[string]any{"summary": label}, 1, nil, "planner")
if err != nil {
_ = d.DeleteTask(task.ID)
t.Fatal(err)
}
if claimed, err := store.ClaimIntent(intentID, "work#1"); err != nil || !claimed {
_ = d.DeleteTask(task.ID)
t.Fatalf("claim intervention intent: claimed=%v err=%v", claimed, err)
}
return store, intentID, func() { _ = d.DeleteTask(task.ID) }
}
func TestIntentInterventionReservationAndAcceptanceAreIdempotent(t *testing.T) {
d, err := Open(testDSN(t))
if err != nil {
t.Skipf("postgres unavailable (%v) - skipping", err)
}
defer d.Close()
store, intentID, cleanup := newRunningInterventionIntent(t, d, "idempotent")
defer cleanup()
const requestID = "intervention-idempotent-1"
const message = "停止枚举,优先验证登录越权"
reserved, err := store.ReserveIntentIntervention(intentID, requestID, message)
if err != nil {
t.Fatal(err)
}
if reserved.Status != IntentInterventionPending || reserved.ActivityID <= 0 || reserved.IntentState != "running" {
t.Fatalf("unexpected reservation: %+v", reserved)
}
replayed, err := store.ReserveIntentIntervention(intentID, requestID, message)
if err != nil {
t.Fatal(err)
}
if replayed.ActivityID != reserved.ActivityID || replayed.Status != IntentInterventionPending {
t.Fatalf("reservation retry created a different operation: first=%+v retry=%+v", reserved, replayed)
}
if _, err := store.ReserveIntentIntervention(intentID, requestID, "改成另一条 Worker 消息"); !errors.Is(err, ErrIntentInterventionConflict) {
t.Fatalf("same request_id with different message error=%v, want intervention conflict", err)
}
if _, err := store.ReserveIntentIntervention(intentID, "intervention-other", message); !errors.Is(err, ErrIntentInterventionConflict) {
t.Fatalf("second pending request error=%v, want intervention conflict", err)
}
if changed, err := store.CompareAndSetIntentState(intentID, "running", "paused"); err != nil || !changed {
t.Fatalf("settle intent before acceptance: changed=%v err=%v", changed, err)
}
activity, duplicate, err := store.AcceptIntentIntervention(intentID, requestID, message)
if err != nil || duplicate {
t.Fatalf("accept intervention: duplicate=%v err=%v", duplicate, err)
}
if activity.ID <= reserved.ActivityID || activity.NodeID == nil || *activity.NodeID != intentID ||
activity.Worker != "user" || activity.Kind != "user" || activity.Summary != message || activity.Detail != message {
t.Fatalf("accepted audit mismatch: %+v", activity)
}
var metadata map[string]any
if err := json.Unmarshal(activity.Metadata, &metadata); err != nil {
t.Fatal(err)
}
if metadata["intervention_request_id"] != requestID || metadata["intervention_status"] != IntentInterventionAccepted {
t.Fatalf("accepted metadata=%v", metadata)
}
node, err := store.GetNode(intentID)
if err != nil || node == nil || node.State != "open" || node.Owner != "" {
t.Fatalf("accepted intent=%+v err=%v, want reopened and unowned", node, err)
}
again, duplicate, err := store.AcceptIntentIntervention(intentID, requestID, message)
if err != nil || !duplicate || again.ID != activity.ID {
t.Fatalf("accept retry: activity=%+v duplicate=%v err=%v", again, duplicate, err)
}
lookup, err := store.IntentInterventionByRequest(intentID, requestID)
if err != nil || lookup == nil || lookup.Status != IntentInterventionAccepted || lookup.ActivityID != activity.ID {
t.Fatalf("accepted lookup=%+v err=%v", lookup, err)
}
var rows int
if err := d.QueryRow(`SELECT COUNT(*) FROM activity
WHERE exploration_id=$1 AND node_id=$2
AND metadata->>'intervention_request_id'=$3`, store.ID(), intentID, requestID).Scan(&rows); err != nil {
t.Fatal(err)
}
if rows != 2 {
t.Fatalf("Worker message rows=%d, want one hidden control row and one visible user row", rows)
}
activities, cursor, err := store.ActivityList(&intentID, 0, 10)
if err != nil {
t.Fatal(err)
}
if len(activities) != 1 || activities[0].ID != activity.ID || cursor != activity.ID {
t.Fatalf("public activities leaked control row: activities=%+v cursor=%d", activities, cursor)
}
maxID, err := store.ActivityMaxID()
if err != nil || maxID != activity.ID {
t.Fatalf("public activity cursor=%d err=%v, want visible user row %d", maxID, err, activity.ID)
}
if detail, err := store.ActivityDetail(reserved.ActivityID); err != nil || detail != "" {
t.Fatalf("hidden control detail=%q err=%v", detail, err)
}
}
func TestIntentInterventionConcurrentReservationHasSingleDurableWinner(t *testing.T) {
d, err := Open(testDSN(t))
if err != nil {
t.Skipf("postgres unavailable (%v) - skipping", err)
}
defer d.Close()
store, intentID, cleanup := newRunningInterventionIntent(t, d, "concurrent")
defer cleanup()
const contenders = 12
const requestID = "intervention-concurrent-1"
const message = "只保留被动请求并验证当前假设"
start := make(chan struct{})
results := make(chan *IntentIntervention, contenders)
errs := make(chan error, contenders)
var wg sync.WaitGroup
for range contenders {
wg.Add(1)
go func() {
defer wg.Done()
<-start
reservation, reserveErr := store.ReserveIntentIntervention(intentID, requestID, message)
results <- reservation
errs <- reserveErr
}()
}
close(start)
wg.Wait()
close(results)
close(errs)
for err := range errs {
if err != nil {
t.Fatalf("concurrent reservation: %v", err)
}
}
var activityID int64
for result := range results {
if result == nil || result.ActivityID <= 0 {
t.Fatalf("invalid concurrent reservation: %+v", result)
}
if activityID == 0 {
activityID = result.ActivityID
} else if result.ActivityID != activityID {
t.Fatalf("concurrent requests produced activity ids %d and %d", activityID, result.ActivityID)
}
}
var rows int
if err := d.QueryRow(`SELECT COUNT(*) FROM activity
WHERE exploration_id=$1 AND node_id=$2
AND metadata->>'intervention_request_id'=$3`, store.ID(), intentID, requestID).Scan(&rows); err != nil {
t.Fatal(err)
}
if rows != 1 {
t.Fatalf("concurrent intervention rows=%d, want 1", rows)
}
}
func TestIntentInterventionAcceptanceCanRetryAfterStateSettlement(t *testing.T) {
d, err := Open(testDSN(t))
if err != nil {
t.Skipf("postgres unavailable (%v) - skipping", err)
}
defer d.Close()
store, intentID, cleanup := newRunningInterventionIntent(t, d, "settlement retry")
defer cleanup()
const requestID = "intervention-settle-1"
const message = "立刻停止当前动作并检查权限边界"
reserved, err := store.ReserveIntentIntervention(intentID, requestID, message)
if err != nil {
t.Fatal(err)
}
if _, _, err := store.AcceptIntentIntervention(intentID, requestID, message); !errors.Is(err, ErrIntentInterventionConflict) {
t.Fatalf("accept before worker settlement error=%v, want conflict", err)
}
pending, err := store.IntentInterventionByRequest(intentID, requestID)
if err != nil || pending == nil || pending.Status != IntentInterventionPending || pending.ActivityID != reserved.ActivityID {
t.Fatalf("failed acceptance lost reservation: pending=%+v err=%v", pending, err)
}
if changed, err := store.CompareAndSetIntentState(intentID, "running", "paused"); err != nil || !changed {
t.Fatalf("settle intent: changed=%v err=%v", changed, err)
}
activity, duplicate, err := store.AcceptIntentIntervention(intentID, requestID, message)
if err != nil || duplicate || activity.ID <= reserved.ActivityID {
t.Fatalf("retry acceptance: activity=%+v duplicate=%v err=%v", activity, duplicate, err)
}
}