Files
Assistant/cmd/commit-runtime/notification_executor_test.go
2026-08-31 16:28:57 +03:00

134 lines
7.9 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"os"
"testing"
"git.cin.su/m/Assistant/adapters/notificationexecutor"
"git.cin.su/m/Assistant/contracts/capability"
commitcontract "git.cin.su/m/Assistant/contracts/commit"
"git.cin.su/m/Assistant/contracts/identity"
capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain"
)
func TestCandidateNotificationCredentialsDoNotFallbackToProductionCredential(t *testing.T) {
path := t.TempDir() + "/candidate-token"
if err := os.WriteFile(path, []byte("candidate-only-token"), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("CANDIDATE_NOTIFICATION_WRITE_TOKEN_FILE", path)
t.Setenv("CANDIDATE_NOTIFICATION_WRITE_SECRET_REF", "credential://domovoy/candidate/notification-write/v1")
ref, secret, err := notificationCredentialsFromRuntime(true)
if err != nil || ref == "" || secret != "candidate-only-token" {
t.Fatalf("candidate credential resolution failed: ref=%q err=%v", ref, err)
}
t.Setenv("CANDIDATE_NOTIFICATION_WRITE_TOKEN_FILE", "")
if _, _, err := notificationCredentialsFromRuntime(true); err == nil {
t.Fatal("candidate path fell back to production notification credential")
}
}
type fakeNotificationBindings struct {
exact capability.Binding
active capability.Binding
exactErr error
activeErr error
candidate capability.CandidateBindingResolution
candidateErr error
}
func (f fakeNotificationBindings) CapabilityBinding(context.Context, capability.CapabilityBindingID, int64) (capability.Binding, error) {
return f.exact, f.exactErr
}
func (f fakeNotificationBindings) ActiveCapabilityBinding(context.Context, capability.CapabilityID, identity.PrincipalID) (capability.Binding, error) {
return f.active, f.activeErr
}
func (f fakeNotificationBindings) ResolveCandidateCapabilityBinding(context.Context, capability.CandidateReleaseContext) (capability.CandidateBindingResolution, error) {
return f.candidate, f.candidateErr
}
func dispatcherAction(t *testing.T, binding capability.Binding) commitcontract.AuthorizedAction {
t.Helper()
payload := notificationexecutor.Payload{ActionIntentID: "intent-1", TemplateCode: commitcontract.ProspectiveDeliveredTemplateV1, ProspectiveItemID: "item-1", ExpectedRevision: 1, Title: "Домовой", Message: "Пора вернуться"}
digest, err := notificationexecutor.Digest(payload)
if err != nil {
t.Fatal(err)
}
return commitcontract.AuthorizedAction{PrincipalID: binding.OwnerPrincipalID, Proposal: commitcontract.ActionProposal{IntentID: payload.ActionIntentID, CapabilityID: commitcontract.CapabilityProspectiveNotification, Operation: commitcontract.OperationDeliverProspectiveNotification, NotificationSubject: commitcontract.ProspectiveNotificationSubject{ProspectiveItemID: "item-1", ExpectedRevision: 1, TemplateCode: payload.TemplateCode, BindingID: binding.ID, BindingRevision: binding.Revision, RenderedTitle: payload.Title, RenderedMessage: payload.Message, RequestDigest: digest}}}
}
func dispatcherBinding(origin string) capability.Binding {
return capability.Binding{ID: "binding-1", CapabilityID: commitcontract.CapabilityProspectiveNotification, OwnerPrincipalID: "owner-1", Revision: 1, State: capability.BindingActive, HTTPSOrigin: origin, DeliveryPath: "/delivery/v1/deliveries", ReceiptPath: "/delivery/v1/receipts", Protocol: capability.ProtocolIdempotentReceiptV1, SecretRef: "write-v1", VerifiedAtUnixMillis: 1, Health: capability.HealthAvailable}
}
func TestOperationDispatcherRequiresExactCurrentBinding(t *testing.T) {
var action commitcontract.AuthorizedAction
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(notificationexecutor.Receipt{ReceiptID: "receipt-1", ActionIntentID: action.Proposal.IntentID, RequestDigest: action.Proposal.NotificationSubject.RequestDigest, AcceptedAt: 1})
}))
defer server.Close()
binding := dispatcherBinding(server.URL)
action = dispatcherAction(t, binding)
fallback := &knownExecutor{}
dispatcher := operationDispatcher{continuityExecutor: fallback, continuityReconciler: fallback, bindings: fakeNotificationBindings{exact: binding, active: binding}, secretRef: "write-v1", secret: "secret", client: server.Client()}
if result := dispatcher.Execute(action); !result.Known || !result.Succeeded {
t.Fatalf("execute=%+v", result)
}
stale := binding
stale.Revision = 2
dispatcher.bindings = fakeNotificationBindings{exact: binding, active: stale}
if result := dispatcher.Execute(action); !result.Known || result.Succeeded {
t.Fatalf("stale binding=%+v", result)
}
}
func TestOperationDispatcherReconcilesRetiredHistoricalBinding(t *testing.T) {
var action commitcontract.AuthorizedAction
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Fatal("reconciliation mutated receiver")
}
_ = json.NewEncoder(w).Encode(notificationexecutor.Receipt{ReceiptID: "receipt-1", ActionIntentID: action.Proposal.IntentID, RequestDigest: action.Proposal.NotificationSubject.RequestDigest, AcceptedAt: 1})
}))
defer server.Close()
binding := dispatcherBinding(server.URL)
action = dispatcherAction(t, binding)
binding.State = capability.BindingRetired
binding.Revision = 1
fallback := &knownExecutor{}
dispatcher := operationDispatcher{continuityExecutor: fallback, continuityReconciler: fallback, bindings: fakeNotificationBindings{exact: binding}, secretRef: "write-v1", secret: "secret", client: server.Client()}
if result := dispatcher.Reconcile(action); !result.Known || !result.Succeeded {
t.Fatalf("reconcile=%+v", result)
}
}
func TestCandidateDispatcherUsesShadowBindingWithoutActiveFallback(t *testing.T) {
var action commitcontract.AuthorizedAction
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Candidate-Release") != "assistant-slot-b" || r.Header.Get("X-Candidate-Task") != "GEN2-CAPABILITY-001" {
t.Fatalf("candidate headers missing")
}
_ = json.NewEncoder(w).Encode(notificationexecutor.Receipt{ReceiptID: "shadow-1", ActionIntentID: action.Proposal.IntentID, RequestDigest: action.Proposal.NotificationSubject.RequestDigest, AcceptedAt: 1, DeliveryMode: "SHADOW"})
}))
defer server.Close()
stable := dispatcherBinding("https://stable.invalid")
action = dispatcherAction(t, stable)
context := capability.CandidateReleaseContext{ReleaseSetID: "assistant-slot-b", Slot: "b", TaskID: "GEN2-CAPABILITY-001", TaskRevision: 7, PrincipalID: stable.OwnerPrincipalID, CapabilityID: stable.CapabilityID, ResourceScope: "synthetic-shadow-notification-v1", Mode: capability.CandidateBindingModeShadow, TargetKind: "NOTIFICATION_SHADOW_RECEIVER", TargetRef: "notification-receiver-candidate", AuthorityGrantID: "grant"}
candidate := capability.CandidateBinding{ID: "candidate-1", Revision: 1, BaseActiveBindingID: stable.ID, BaseActiveBindingRevision: stable.Revision, Context: context, TargetOrigin: server.URL, DeliveryPath: "/delivery/v1/deliveries", ReceiptPath: "/delivery/v1/receipts", Protocol: capability.ProtocolIdempotentReceiptV1, CreatedAtUnixMillis: 1, ExpiresAtUnixMillis: 100, Status: capability.CandidateBindingActive}
digest, err := capabilitydomain.CandidateTargetDigest(candidate)
if err != nil {
t.Fatal(err)
}
context.TargetDigest, candidate.Context.TargetDigest = digest, digest
dispatcher := operationDispatcher{continuityExecutor: &knownExecutor{}, continuityReconciler: &knownExecutor{}, bindings: fakeNotificationBindings{activeErr: errors.New("stable fallback must not run"), candidate: capability.CandidateBindingResolution{Binding: candidate, Mode: capability.CandidateBindingModeShadow}}, secret: "shadow-secret", client: server.Client(), candidateContext: &context}
result := dispatcher.Execute(action)
if !result.Known || !result.Succeeded || result.Receipt == nil || result.Receipt.DeliveryMode != "SHADOW" {
t.Fatalf("candidate result=%+v", result)
}
}