193 lines
7.9 KiB
Go
193 lines
7.9 KiB
Go
package store
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
agentv1 "git.ipao.vip/rogee/go-sip/gen/agent/v1"
|
|
)
|
|
|
|
func localTestOriginationBinding(t *testing.T, st *Store, executionID string) *agentv1.ExecutionBinding {
|
|
t.Helper()
|
|
binding := &agentv1.ExecutionBinding{ExecutionId: executionID, AttemptId: executionID}
|
|
if err := st.DB().QueryRow(`SELECT tenant_id,tenant_key,task_id,task_item_id,task_revision,agent_version_id,route_policy_id,caller_profile_id FROM tasks WHERE execution_id=?`, executionID).Scan(
|
|
&binding.TenantId, &binding.TenantKey, &binding.TaskId, &binding.TaskItemId, &binding.TaskRevision,
|
|
&binding.AgentVersionId, &binding.RoutePolicyId, &binding.CallerProfileId,
|
|
); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return binding
|
|
}
|
|
|
|
func readyLocalOrigination(t *testing.T) (*Store, LocalCommandRecord) {
|
|
t.Helper()
|
|
st := openLocalDiscoveryTestStore(t)
|
|
at := time.Date(2026, 9, 21, 2, 0, 0, 0, time.UTC)
|
|
bundle := localConfigBundle("d-1", "tenant-a", "tenant-key-a", "task-a", 1, 1, 1, 1, 1, at)
|
|
if err := st.SaveLocalConfigBundle(bundle); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
record := localAdmissionCommand("command-a", "execution-a", "task-a", bundle, "d-1", "tenant-a", "tenant-key-a")
|
|
seedLocalCommandAssignments(t, st, record)
|
|
if duplicate, err := st.PersistLocalCommand(record); err != nil || duplicate {
|
|
t.Fatalf("persist accepted command: duplicate=%v err=%v", duplicate, err)
|
|
}
|
|
return st, record
|
|
}
|
|
|
|
func TestLocalOriginationDecisionsAreDurableAndOneShot(t *testing.T) {
|
|
for _, decision := range []string{"issued", "refused"} {
|
|
t.Run(decision, func(t *testing.T) {
|
|
st, record := readyLocalOrigination(t)
|
|
decisionAt := time.Date(2026, 9, 21, 2, 0, 0, 0, time.UTC)
|
|
pending, err := st.LocalPendingOriginationForCommand(record.Body)
|
|
if err != nil || pending != "execution-a" {
|
|
t.Fatalf("pending execution=%q err=%v", pending, err)
|
|
}
|
|
candidate, err := st.LoadLocalOrigination("d-1", pending)
|
|
if err != nil || candidate.CommandID != "command-a" || candidate.Callee != "15003164745" ||
|
|
candidate.Snapshot.SelectedTrunkID != "mock-trunk" || len(candidate.SnapshotSHA256) != 64 {
|
|
t.Fatalf("bound origination=%+v err=%v", candidate, err)
|
|
}
|
|
binding := localTestOriginationBinding(t, st, pending)
|
|
if decision == "issued" {
|
|
if err := st.ClaimLocalOrigination("d-1", pending, "agent-1", binding, decisionAt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := st.ClaimLocalOrigination("d-1", pending, "agent-1", binding, decisionAt); !errors.Is(err, ErrLocalOriginationNotAuthorized) {
|
|
t.Fatalf("second claim error=%v", err)
|
|
}
|
|
if err := st.RefuseLocalOrigination("d-1", pending, "control_closed", decisionAt); !errors.Is(err, ErrLocalOriginationNotAuthorized) {
|
|
t.Fatalf("issued instruction was overwritten by refusal: %v", err)
|
|
}
|
|
} else {
|
|
if err := st.RefuseLocalOrigination("d-1", pending, "policy_denied", decisionAt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := st.RefuseLocalOrigination("d-1", pending, "control_closed", decisionAt); err != nil {
|
|
t.Fatalf("replayed refusal error=%v", err)
|
|
}
|
|
if err := st.ClaimLocalOrigination("d-1", pending, "agent-1", binding, decisionAt); !errors.Is(err, ErrLocalOriginationNotAuthorized) {
|
|
t.Fatalf("refused instruction was revived: %v", err)
|
|
}
|
|
}
|
|
got, err := st.LocalOriginationDecision(pending)
|
|
if err != nil || got != decision {
|
|
t.Fatalf("durable decision=%q want=%q err=%v", got, decision, err)
|
|
}
|
|
var bound int
|
|
if err := st.DB().QueryRow(`SELECT COUNT(*) FROM execution_agents WHERE execution_id=?`, pending).Scan(&bound); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantBound := 0
|
|
if decision == "issued" {
|
|
wantBound = 1
|
|
}
|
|
if bound != wantBound {
|
|
t.Fatalf("claim and Agent control binding were not atomic: decision=%s bindings=%d", decision, bound)
|
|
}
|
|
if _, err := st.LocalPendingOriginationForCommand(record.Body); !errors.Is(err, sql.ErrNoRows) {
|
|
t.Fatalf("decided execution still pending: %v", err)
|
|
}
|
|
assertLocalQuota(t, st, LocalTenantQuotaScope("d-1", "tenant-a"), 1, 1) // F08 owns release.
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLocalOriginationFailClosedOnCorruptOrPausedFacts(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
change func(t *testing.T, st *Store)
|
|
}{
|
|
{"modified command hash", func(t *testing.T, st *Store) {
|
|
t.Helper()
|
|
if _, err := st.db.Exec(`UPDATE inbox SET body_hash='bad' WHERE command_id='command-a'`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}},
|
|
{"modified snapshot hash", func(t *testing.T, st *Store) {
|
|
t.Helper()
|
|
if _, err := st.db.Exec(`UPDATE local_v01_execution_configs SET content_sha256=? WHERE execution_id='execution-a'`, strings.Repeat("0", 64)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}},
|
|
{"snapshot identity mismatch with valid hash", func(t *testing.T, st *Store) {
|
|
t.Helper()
|
|
var body []byte
|
|
if err := st.db.QueryRow(`SELECT body FROM local_v01_execution_configs WHERE execution_id='execution-a'`).Scan(&body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var snapshot LocalExecutionConfigSnapshot
|
|
if err := json.Unmarshal(body, &snapshot); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
snapshot.TaskID = "different-task"
|
|
changed, err := json.Marshal(snapshot)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
hash := sha256.Sum256(changed)
|
|
if _, err := st.db.Exec(`UPDATE local_v01_execution_configs SET body=?,content_sha256=? WHERE execution_id='execution-a'`, changed, hex.EncodeToString(hash[:])); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}},
|
|
{"paused assignment", func(t *testing.T, st *Store) {
|
|
t.Helper()
|
|
if _, err := st.SetLocalTaskAdmissionBarrier("d-1", "task-a", "tenant-a", "tenant-key-a", "paused"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
st, record := readyLocalOrigination(t)
|
|
tc.change(t, st)
|
|
if _, err := st.LoadLocalOrigination("d-1", "execution-a"); !errors.Is(err, ErrLocalOriginationNotAuthorized) {
|
|
t.Fatalf("invalid facts did not block origination: %v", err)
|
|
}
|
|
if tc.name == "paused assignment" {
|
|
if err := st.ClaimLocalOrigination("d-1", "execution-a", "agent-1", localTestOriginationBinding(t, st, "execution-a"), time.Date(2026, 9, 21, 2, 0, 0, 0, time.UTC)); !errors.Is(err, ErrLocalOriginationNotAuthorized) {
|
|
t.Fatalf("pause crossed atomic claim: %v", err)
|
|
}
|
|
var bound int
|
|
if err := st.DB().QueryRow(`SELECT COUNT(*) FROM execution_agents WHERE execution_id='execution-a'`).Scan(&bound); err != nil || bound != 0 {
|
|
t.Fatalf("failed claim left an Agent binding: count=%d err=%v", bound, err)
|
|
}
|
|
if err := st.RefuseLocalOrigination("d-1", "execution-a", "control_closed", time.Date(2026, 9, 21, 2, 0, 0, 0, time.UTC)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := st.LocalPendingOriginationForCommand(record.Body); !errors.Is(err, sql.ErrNoRows) {
|
|
t.Fatalf("paused refusal reentered queue: %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLocalOriginationRejectsMissingOrInvalidIdentity(t *testing.T) {
|
|
st, record := readyLocalOrigination(t)
|
|
if _, err := st.LocalPendingOriginationForCommand(nil); !errors.Is(err, sql.ErrNoRows) {
|
|
t.Fatalf("empty command lookup error=%v", err)
|
|
}
|
|
if _, err := st.LocalPendingOriginationForCommand(append(append([]byte(nil), record.Body...), ' ')); !errors.Is(err, sql.ErrNoRows) {
|
|
t.Fatalf("changed command bytes matched a pending execution: %v", err)
|
|
}
|
|
if _, err := st.LoadLocalOrigination("other-dispatcher", "execution-a"); !errors.Is(err, ErrLocalOriginationNotAuthorized) {
|
|
t.Fatalf("wrong Dispatcher identity error=%v", err)
|
|
}
|
|
if err := st.RefuseLocalOrigination("d-1", "execution-a", "unrecognized", time.Date(2026, 9, 21, 2, 0, 0, 0, time.UTC)); err == nil {
|
|
t.Fatal("invalid refusal reason was persisted")
|
|
}
|
|
if _, err := st.LocalOriginationDecision(""); !errors.Is(err, ErrLocalOriginationNotAuthorized) {
|
|
t.Fatalf("empty execution ID error=%v", err)
|
|
}
|
|
if _, err := st.LocalOriginationDecision("missing"); !errors.Is(err, sql.ErrNoRows) {
|
|
t.Fatalf("missing decision error=%v", err)
|
|
}
|
|
}
|