simplify SaaS SIP configuration response
This commit is contained in:
@@ -38,10 +38,6 @@ type Snapshot struct {
|
||||
TenantID string
|
||||
TenantKey string
|
||||
SIPRevision int64
|
||||
SIPSnapshotSHA256 string
|
||||
SIPCellID string
|
||||
SIPArtifactRevision int64
|
||||
SIPArtifactConfigSHA256 string
|
||||
TaskRevision int64
|
||||
TaskStatus string
|
||||
TaskMaxConcurrentCalls int64
|
||||
@@ -122,10 +118,23 @@ func (c *Client) ReadTask(ctx context.Context, taskID, tenantID string) (Snapsho
|
||||
if err := json.Unmarshal(sipBody, &sip); err != nil {
|
||||
return Snapshot{}, fmt.Errorf("decode SIP configuration identity: %w", err)
|
||||
}
|
||||
if sip.Resource != resourceSIPConfig || sip.DispatcherID != c.dispatcherID || sip.Revision <= 0 || sip.SnapshotSHA256 == "" ||
|
||||
sip.Artifact.CellID == "" || sip.Artifact.Revision <= 0 || sip.Artifact.ConfigSHA256 == "" {
|
||||
if sip.Resource != resourceSIPConfig || sip.DispatcherID != c.dispatcherID || sip.Revision <= 0 {
|
||||
return Snapshot{}, errors.New("SIP configuration identity or revision does not match the request")
|
||||
}
|
||||
seenTrunks := make(map[string]bool, len(sip.Trunks))
|
||||
for _, trunk := range sip.Trunks {
|
||||
if seenTrunks[trunk.TrunkID] {
|
||||
return Snapshot{}, fmt.Errorf("SIP configuration repeats trunk %q", trunk.TrunkID)
|
||||
}
|
||||
seenTrunks[trunk.TrunkID] = true
|
||||
seenCallers := make(map[string]bool, len(trunk.CallerProfiles))
|
||||
for _, caller := range trunk.CallerProfiles {
|
||||
if seenCallers[caller.CallerProfileID] {
|
||||
return Snapshot{}, fmt.Errorf("SIP trunk %q repeats caller profile %q", trunk.TrunkID, caller.CallerProfileID)
|
||||
}
|
||||
seenCallers[caller.CallerProfileID] = true
|
||||
}
|
||||
}
|
||||
|
||||
taskPath := configReadPath + "/task/" + url.PathEscape(taskID)
|
||||
taskBody, err := c.getConfig(ctx, taskPath)
|
||||
@@ -165,10 +174,8 @@ func (c *Client) ReadTask(ctx context.Context, taskID, tenantID string) (Snapsho
|
||||
|
||||
return Snapshot{
|
||||
TaskID: taskID, TenantID: tenantID, TenantKey: task.TenantKey,
|
||||
SIPRevision: sip.Revision, SIPSnapshotSHA256: sip.SnapshotSHA256,
|
||||
SIPCellID: sip.Artifact.CellID, SIPArtifactRevision: sip.Artifact.Revision,
|
||||
SIPArtifactConfigSHA256: sip.Artifact.ConfigSHA256,
|
||||
TaskRevision: task.TaskRevision, TaskStatus: task.Status,
|
||||
SIPRevision: sip.Revision,
|
||||
TaskRevision: task.TaskRevision, TaskStatus: task.Status,
|
||||
TaskMaxConcurrentCalls: int64(task.MaxConcurrentCalls), TaskRingTimeoutMS: int64(task.RingTimeoutMS),
|
||||
TaskMaxCallDurationMS: int64(task.MaxCallDurationMS), TaskRoutePolicyID: task.RoutePolicyID,
|
||||
TaskCallerProfileID: task.CallerProfileID, TaskAllowedTrunkIDs: append([]string(nil), task.AllowedTrunkIDs...),
|
||||
@@ -307,15 +314,15 @@ func responseErrorCode(body []byte) string {
|
||||
}
|
||||
|
||||
type sipConfigResponse struct {
|
||||
Resource string `json:"resource"`
|
||||
DispatcherID string `json:"dispatcher_id"`
|
||||
Revision int64 `json:"revision"`
|
||||
SnapshotSHA256 string `json:"snapshot_sha256"`
|
||||
Artifact struct {
|
||||
CellID string `json:"cell_id"`
|
||||
Revision int64 `json:"revision"`
|
||||
ConfigSHA256 string `json:"config_sha256"`
|
||||
} `json:"artifact"`
|
||||
Resource string `json:"resource"`
|
||||
DispatcherID string `json:"dispatcher_id"`
|
||||
Revision int64 `json:"revision"`
|
||||
Trunks []struct {
|
||||
TrunkID string `json:"trunk_id"`
|
||||
CallerProfiles []struct {
|
||||
CallerProfileID string `json:"caller_profile_id"`
|
||||
} `json:"caller_profiles"`
|
||||
} `json:"trunks"`
|
||||
}
|
||||
|
||||
type taskConfigResponse struct {
|
||||
|
||||
@@ -67,6 +67,38 @@ func TestClientReadTaskRequestsAndValidatesThreeConfigEndpoints(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientReadTaskRejectsDuplicateSIPTrunksAndCallers(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
mutate func(map[string]any)
|
||||
}{
|
||||
{"duplicate trunk", func(sip map[string]any) {
|
||||
trunks := sip["trunks"].([]any)
|
||||
sip["trunks"] = append(trunks, trunks[0])
|
||||
}},
|
||||
{"duplicate caller", func(sip map[string]any) {
|
||||
trunk := sip["trunks"].([]any)[0].(map[string]any)
|
||||
profiles := trunk["caller_profiles"].([]any)
|
||||
trunk["caller_profiles"] = append(profiles, profiles[0])
|
||||
}},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fixtures := validConfigFixtures(t)
|
||||
var sip map[string]any
|
||||
if err := json.Unmarshal(fixtures[configReadPath+"/sip"], &sip); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tc.mutate(sip)
|
||||
fixtures[configReadPath+"/sip"] = marshalDiscoveryResponse(t, sip)
|
||||
server := configFixtureServer(t, fixtures)
|
||||
defer server.Close()
|
||||
if _, err := newMockClient(t, server).ReadTask(context.Background(), mockTaskID, mockTenantID); err == nil {
|
||||
t.Fatal("duplicate SIP configuration identity accepted")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientReadTaskRejectsSchemaInvalidResponse(t *testing.T) {
|
||||
fixtures := validConfigFixtures(t)
|
||||
fixtures[configReadPath+"/sip"] = readConfigFixture(t, "config-read-invalid-extra-property-v0.1.json")
|
||||
@@ -139,7 +171,7 @@ func newMockClient(t *testing.T, server *httptest.Server) *Client {
|
||||
func validConfigFixtures(t *testing.T) map[string][]byte {
|
||||
t.Helper()
|
||||
return map[string][]byte{
|
||||
configReadPath + "/sip": readConfigFixture(t, "config-read-sip-v0.1.json"),
|
||||
configReadPath + "/sip": readConfigFixture(t, "config-read-sip-v0.2.json"),
|
||||
configReadPath + "/tasks": readConfigFixture(t, "task-discovery-snapshot-v0.2.json"),
|
||||
configReadPath + "/task/" + mockTaskID: readConfigFixture(t, "config-read-task-v0.1.json"),
|
||||
configReadPath + "/tenant/" + mockTenantID + "/quota": readConfigFixture(t, "config-read-tenant-quota-v0.1.json"),
|
||||
|
||||
@@ -69,7 +69,7 @@ func ValidateEvent(raw []byte) error {
|
||||
}
|
||||
|
||||
func ValidateLocalConfigRead(raw []byte) error {
|
||||
return validateLocalSchema("config-read-v0.1.schema.json", raw)
|
||||
return validateLocalSchema("config-read-v0.2.schema.json", raw)
|
||||
}
|
||||
|
||||
func ValidateLocalTaskDiscoveryV04(raw []byte) error {
|
||||
|
||||
@@ -62,6 +62,8 @@ func localSchemaVersion(name string) string {
|
||||
switch name {
|
||||
case "config-read-v0.1.schema.json", "command-next-v0.1-proposal.schema.json", "call-result-v0.1-proposal.schema.json", "local-mock-recording-failure-v0.1.schema.json":
|
||||
return "v0.1"
|
||||
case "config-read-v0.2.schema.json":
|
||||
return "v0.2"
|
||||
case "task-discovery-v0.3-proposal.schema.json":
|
||||
return "v0.3"
|
||||
case "task-discovery-v0.4-proposal.schema.json", "task-control-v0.4-proposal.schema.json", "call-execute-v0.4-proposal.schema.json":
|
||||
|
||||
@@ -2,8 +2,6 @@ package dispatcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
@@ -37,22 +35,18 @@ func (v *AgentSIPConfigVerifier) VerifyAppliedSIPConfig(ctx context.Context, sna
|
||||
if v == nil || v.Probe == nil || v.AgentID == "" || v.CellID == "" {
|
||||
return errors.New("Agent status probe, agent ID, and configured cell ID are required")
|
||||
}
|
||||
if snapshot.SIPCellID != v.CellID {
|
||||
return fmt.Errorf("SIP config targets cell %q, configured cell is %q", snapshot.SIPCellID, v.CellID)
|
||||
}
|
||||
status, err := v.Probe.Probe(ctx, v.AgentID, v.CellID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("probe Agent applied SIP config: %w", err)
|
||||
}
|
||||
return verifyAppliedSIPConfigStatus(status, v.AgentID, snapshot)
|
||||
return verifyAppliedSIPConfigStatus(status, v.AgentID, v.CellID, snapshot)
|
||||
}
|
||||
|
||||
func verifyAppliedSIPConfigStatus(status *agentv1.AgentStatus, agentID string, snapshot configread.Snapshot) error {
|
||||
digest, digestErr := hex.DecodeString(snapshot.SIPArtifactConfigSHA256)
|
||||
if snapshot.SIPCellID == "" || snapshot.SIPArtifactRevision <= 0 || digestErr != nil || len(digest) != sha256.Size {
|
||||
return errors.New("SIP config artifact identity is incomplete")
|
||||
func verifyAppliedSIPConfigStatus(status *agentv1.AgentStatus, agentID, cellID string, snapshot configread.Snapshot) error {
|
||||
if snapshot.SIPRevision <= 0 {
|
||||
return errors.New("SIP config revision is missing")
|
||||
}
|
||||
if status == nil || status.AgentId != agentID || status.CellId != snapshot.SIPCellID || status.BootId == "" {
|
||||
if status == nil || status.AgentId != agentID || status.CellId != cellID || status.BootId == "" {
|
||||
return errors.New("Agent status identity or boot ID is invalid")
|
||||
}
|
||||
var appliedSIP *agentv1.AppliedConfig
|
||||
@@ -68,9 +62,9 @@ func verifyAppliedSIPConfigStatus(status *agentv1.AgentStatus, agentID string, s
|
||||
if appliedSIP == nil || appliedSIP.State != AppliedConfigStateApplied || appliedSIP.ObservedAtUnixMs <= 0 {
|
||||
return errors.New("Agent has not reported one observed, applied SIP config")
|
||||
}
|
||||
revision := strconv.FormatInt(snapshot.SIPArtifactRevision, 10)
|
||||
if appliedSIP.Revision != revision || appliedSIP.ConfigSha256 != snapshot.SIPArtifactConfigSHA256 {
|
||||
return fmt.Errorf("Agent applied SIP config does not match revision %s and expected SHA-256", revision)
|
||||
revision := strconv.FormatInt(snapshot.SIPRevision, 10)
|
||||
if appliedSIP.Revision != revision {
|
||||
return fmt.Errorf("Agent applied SIP config does not match revision %s", revision)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -102,10 +102,6 @@ func (d *Dispatcher) LoadProjectConfig(ctx context.Context, client *configread.C
|
||||
for key, current := range d.projectConfigs {
|
||||
current.SIP = append(current.SIP[:0], snapshot.SIP...)
|
||||
current.SIPRevision = snapshot.SIPRevision
|
||||
current.SIPSnapshotSHA256 = snapshot.SIPSnapshotSHA256
|
||||
current.SIPCellID = snapshot.SIPCellID
|
||||
current.SIPArtifactRevision = snapshot.SIPArtifactRevision
|
||||
current.SIPArtifactConfigSHA256 = snapshot.SIPArtifactConfigSHA256
|
||||
current.ExpiresAt = minTime(current.ExpiresAt, sharedExpiry)
|
||||
if key.tenantID == snapshot.TenantID {
|
||||
current.TenantQuota = append(current.TenantQuota[:0], snapshot.TenantQuota...)
|
||||
|
||||
@@ -40,16 +40,8 @@ type dialTaskConfig struct {
|
||||
}
|
||||
|
||||
type dialSIPConfig struct {
|
||||
Artifact struct {
|
||||
Mode string `json:"mode"`
|
||||
AllowedTargets []string `json:"allowed_targets"`
|
||||
Trunks []struct {
|
||||
TrunkID string `json:"trunk_id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
CallerProfileIDs []string `json:"caller_profile_ids"`
|
||||
} `json:"trunks"`
|
||||
} `json:"artifact"`
|
||||
TrunkDetails []struct {
|
||||
Trunks []struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
TrunkID string `json:"trunk_id"`
|
||||
MaxConcurrentCalls *int64 `json:"max_concurrent_calls"`
|
||||
CallerProfiles []struct {
|
||||
@@ -57,7 +49,7 @@ type dialSIPConfig struct {
|
||||
CallerID string `json:"caller_id"`
|
||||
} `json:"caller_profiles"`
|
||||
Schedule callwindow.WeeklySchedule `json:"schedule"`
|
||||
} `json:"trunk_details"`
|
||||
} `json:"trunks"`
|
||||
}
|
||||
|
||||
// A line is chosen once at admission, in the task's listed order. The second
|
||||
@@ -74,8 +66,8 @@ func checkSelectedDialPolicy(snapshot configread.Snapshot, callee, trunkID strin
|
||||
}
|
||||
|
||||
func evaluateDialPolicy(snapshot configread.Snapshot, callee, selectedTrunk string, at time.Time, newAdmission bool) (dialDecision, error) {
|
||||
// This project-wide allowlist is never inferred from an artifact or rewritten
|
||||
// using a provider prefix. Provider targets further restrict this set.
|
||||
// The project-wide allowlist is local, never inferred from SaaS or
|
||||
// rewritten using a provider prefix.
|
||||
if callee != "15003164745" && callee != "15830461047" {
|
||||
return dialDecision{}, errors.New("callee is not on the approved outbound whitelist")
|
||||
}
|
||||
@@ -96,9 +88,6 @@ func evaluateDialPolicy(snapshot configread.Snapshot, callee, selectedTrunk stri
|
||||
if task.Status != "running" || len(task.AllowedTrunkIDs) == 0 || task.CallerProfileID == "" {
|
||||
return dialDecision{}, errors.New("task is not authorized for dialing")
|
||||
}
|
||||
if !slices.Contains(sip.Artifact.AllowedTargets, callee) {
|
||||
return dialDecision{}, errors.New("callee is not in the approved SIP artifact")
|
||||
}
|
||||
if task.RingTimeoutMS <= 0 || task.MaxCallDurationMS <= 0 || task.Agent.Config.Conversation.MaxDurationMS <= 0 {
|
||||
return dialDecision{}, errors.New("task or AI call duration is missing")
|
||||
}
|
||||
@@ -118,25 +107,14 @@ func evaluateDialPolicy(snapshot configread.Snapshot, callee, selectedTrunk stri
|
||||
if selectedTrunk != "" && candidate != selectedTrunk {
|
||||
continue
|
||||
}
|
||||
var artifactTrunk *struct {
|
||||
TrunkID string `json:"trunk_id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
CallerProfileIDs []string `json:"caller_profile_ids"`
|
||||
}
|
||||
for i := range sip.Artifact.Trunks {
|
||||
if sip.Artifact.Trunks[i].TrunkID == candidate {
|
||||
artifactTrunk = &sip.Artifact.Trunks[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if artifactTrunk == nil || !artifactTrunk.Enabled || !slices.Contains(artifactTrunk.CallerProfileIDs, task.CallerProfileID) {
|
||||
lastReason = fmt.Errorf("trunk %s is disabled or has no approved caller profile", candidate)
|
||||
continue
|
||||
}
|
||||
for _, detail := range sip.TrunkDetails {
|
||||
for _, detail := range sip.Trunks {
|
||||
if detail.TrunkID != candidate {
|
||||
continue
|
||||
}
|
||||
if !detail.Enabled {
|
||||
lastReason = fmt.Errorf("trunk %s is disabled", candidate)
|
||||
break
|
||||
}
|
||||
if detail.MaxConcurrentCalls == nil || *detail.MaxConcurrentCalls <= 0 {
|
||||
lastReason = fmt.Errorf("trunk %s has no authorized capacity", candidate)
|
||||
break
|
||||
|
||||
@@ -20,13 +20,13 @@ func policyAt(t *testing.T, value string) time.Time {
|
||||
|
||||
func policySnapshot(t *testing.T) configread.Snapshot {
|
||||
t.Helper()
|
||||
sip := localConfigFixture(t, "config-read-sip-v0.1.json")
|
||||
sip := localConfigFixture(t, "config-read-sip-v0.2.json")
|
||||
var document map[string]any
|
||||
if err := json.Unmarshal(sip, &document); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Explicit local Mock capacity; the documented null remains an unknown real limit.
|
||||
document["trunk_details"].([]any)[0].(map[string]any)["max_concurrent_calls"] = 3
|
||||
document["trunks"].([]any)[0].(map[string]any)["max_concurrent_calls"] = 3
|
||||
sip, err := json.Marshal(document)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -77,11 +77,11 @@ func TestSelectDialPolicyRequiresKnownCapacityCallerAndWhitelist(t *testing.T) {
|
||||
name string
|
||||
alter func()
|
||||
}{
|
||||
{"unknown trunk capacity", func() { sip["trunk_details"].([]any)[0].(map[string]any)["max_concurrent_calls"] = nil }},
|
||||
{"unknown trunk capacity", func() { sip["trunks"].([]any)[0].(map[string]any)["max_concurrent_calls"] = nil }},
|
||||
{"no matching caller", func() {
|
||||
sip["artifact"].(map[string]any)["trunks"].([]any)[0].(map[string]any)["caller_profile_ids"] = []string{"other"}
|
||||
sip["trunks"].([]any)[0].(map[string]any)["caller_profiles"].([]any)[0].(map[string]any)["caller_profile_id"] = "other"
|
||||
}},
|
||||
{"trunk disabled", func() { sip["artifact"].(map[string]any)["trunks"].([]any)[0].(map[string]any)["enabled"] = false }},
|
||||
{"trunk disabled", func() { sip["trunks"].([]any)[0].(map[string]any)["enabled"] = false }},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var item map[string]any
|
||||
@@ -121,21 +121,13 @@ func TestSelectDialPolicyUsesFirstEligibleTrunkWithoutPostSelectionSwitch(t *tes
|
||||
if err := json.Unmarshal(snapshot.SIP, &sip); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
artifact := sip["artifact"].(map[string]any)
|
||||
first := artifact["trunks"].([]any)[0].(map[string]any)
|
||||
first := sip["trunks"].([]any)[0].(map[string]any)
|
||||
second := make(map[string]any)
|
||||
for k, v := range first {
|
||||
second[k] = v
|
||||
}
|
||||
second["trunk_id"] = "trunk-second"
|
||||
artifact["trunks"] = append(artifact["trunks"].([]any), second)
|
||||
details := sip["trunk_details"].([]any)[0].(map[string]any)
|
||||
secondDetails := make(map[string]any)
|
||||
for k, v := range details {
|
||||
secondDetails[k] = v
|
||||
}
|
||||
secondDetails["trunk_id"] = "trunk-second"
|
||||
sip["trunk_details"] = append(sip["trunk_details"].([]any), secondDetails)
|
||||
sip["trunks"] = append(sip["trunks"].([]any), second)
|
||||
snapshot.TaskAllowedTrunkIDs = []string{"trunk-mock", "trunk-second"}
|
||||
var task map[string]any
|
||||
if err := json.Unmarshal(snapshot.Task, &task); err != nil {
|
||||
@@ -143,9 +135,9 @@ func TestSelectDialPolicyUsesFirstEligibleTrunkWithoutPostSelectionSwitch(t *tes
|
||||
}
|
||||
task["allowed_trunk_ids"] = snapshot.TaskAllowedTrunkIDs
|
||||
snapshot.Task, _ = json.Marshal(task)
|
||||
firstWindow := details["schedule"].(map[string]any)["weekly_windows"].(map[string]any)
|
||||
firstWindow := first["schedule"].(map[string]any)["weekly_windows"].(map[string]any)
|
||||
firstWindow["monday"] = []any{map[string]any{"start": "09:00", "end": "10:00"}}
|
||||
secondDetails["schedule"] = map[string]any{"time_zone": "Asia/Shanghai", "weekly_windows": map[string]any{
|
||||
second["schedule"] = map[string]any{"time_zone": "Asia/Shanghai", "weekly_windows": map[string]any{
|
||||
"monday": []any{map[string]any{"start": "10:00", "end": "11:00"}},
|
||||
"tuesday": []any{}, "wednesday": []any{}, "thursday": []any{}, "friday": []any{}, "saturday": []any{}, "sunday": []any{},
|
||||
}}
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestLocalDispatcherMockSaaSEndToEndWithOutboxRecovery(t *testing.T) {
|
||||
// The published example intentionally leaves supplier capacity unknown;
|
||||
// only this isolated Mock grants a positive, explicit trunk limit.
|
||||
fixtures := map[string][]byte{
|
||||
"/internal/v1/dispatcher/sip": localConfigFixture(t, "config-read-sip-v0.1.json"),
|
||||
"/internal/v1/dispatcher/sip": localConfigFixture(t, "config-read-sip-v0.2.json"),
|
||||
"/internal/v1/dispatcher/task/" + localTestTaskID: readLocalFixture(t, "config-read-task-v0.1.json"),
|
||||
"/internal/v1/dispatcher/tenant/" + localTestTenantID + "/quota": readLocalFixture(t, "config-read-tenant-quota-v0.1.json"),
|
||||
}
|
||||
|
||||
@@ -541,7 +541,7 @@ func localExecuteTaskCommandBody(t *testing.T, commandID, taskID string, referen
|
||||
func newLocalV01TestDispatcher(t *testing.T, now time.Time) (*Dispatcher, *store.Store, *httptest.Server) {
|
||||
t.Helper()
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/internal/v1/dispatcher/sip", localConfigResponse(string(localConfigFixture(t, "config-read-sip-v0.1.json"))))
|
||||
mux.HandleFunc("/internal/v1/dispatcher/sip", localConfigResponse(string(localConfigFixture(t, "config-read-sip-v0.2.json"))))
|
||||
mux.HandleFunc("/internal/v1/dispatcher/tasks", func(w http.ResponseWriter, r *http.Request) {
|
||||
after := r.URL.Query().Get("after")
|
||||
if after == "" || after == "0" {
|
||||
@@ -639,7 +639,7 @@ func newLocalV01MultiTaskConfigServer(t *testing.T, taskIDs []string, quotaRevis
|
||||
t.Fatal(err)
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/internal/v1/dispatcher/sip", localConfigResponse(string(localConfigFixture(t, "config-read-sip-v0.1.json"))))
|
||||
mux.HandleFunc("/internal/v1/dispatcher/sip", localConfigResponse(string(localConfigFixture(t, "config-read-sip-v0.2.json"))))
|
||||
mux.HandleFunc("/internal/v1/dispatcher/tasks", localConfigResponse(string(discoveryBody)))
|
||||
mux.HandleFunc("/internal/v1/dispatcher/task/", func(w http.ResponseWriter, r *http.Request) {
|
||||
taskID := r.URL.Path[len("/internal/v1/dispatcher/task/"):]
|
||||
@@ -657,13 +657,13 @@ func newLocalV01MultiTaskConfigServer(t *testing.T, taskIDs []string, quotaRevis
|
||||
func localTestSIPConfigVerifier() SIPConfigVerifier {
|
||||
return SIPConfigVerifierFunc(func(_ context.Context, snapshot configread.Snapshot) error {
|
||||
status := &agentv1.AgentStatus{
|
||||
AgentId: "agent-mock", CellId: snapshot.SIPCellID, BootId: "boot-mock",
|
||||
AgentId: "agent-mock", CellId: "cell-mock", BootId: "boot-mock",
|
||||
AppliedConfigs: []*agentv1.AppliedConfig{{
|
||||
Kind: AppliedConfigKindSIP, Revision: fmt.Sprint(snapshot.SIPArtifactRevision),
|
||||
ConfigSha256: snapshot.SIPArtifactConfigSHA256, State: AppliedConfigStateApplied, ObservedAtUnixMs: 1,
|
||||
Kind: AppliedConfigKindSIP, Revision: fmt.Sprint(snapshot.SIPRevision),
|
||||
State: AppliedConfigStateApplied, ObservedAtUnixMs: 1,
|
||||
}},
|
||||
}
|
||||
return verifyAppliedSIPConfigStatus(status, "agent-mock", snapshot)
|
||||
return verifyAppliedSIPConfigStatus(status, "agent-mock", "cell-mock", snapshot)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -673,14 +673,14 @@ func localConfigFixture(t *testing.T, name string) []byte {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if name == "config-read-sip-v0.1.json" {
|
||||
if name == "config-read-sip-v0.2.json" {
|
||||
var response map[string]any
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Only the isolated Mock is granted explicit capacity; the documented null
|
||||
// remains unknown and must not authorize a real supplier trunk.
|
||||
response["trunk_details"].([]any)[0].(map[string]any)["max_concurrent_calls"] = 3
|
||||
response["trunks"].([]any)[0].(map[string]any)["max_concurrent_calls"] = 3
|
||||
body, err = json.Marshal(response)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -22,17 +22,14 @@ func (p *staticAgentStatusProbe) Probe(_ context.Context, agentID, cellID string
|
||||
return p.status, p.err
|
||||
}
|
||||
|
||||
func TestAgentSIPConfigVerifierRequiresExactAppliedArtifact(t *testing.T) {
|
||||
snapshot := configread.Snapshot{
|
||||
SIPCellID: "cell-a", SIPArtifactRevision: 7,
|
||||
SIPArtifactConfigSHA256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
}
|
||||
func TestAgentSIPConfigVerifierRequiresExactAppliedRevision(t *testing.T) {
|
||||
snapshot := configread.Snapshot{SIPRevision: 7}
|
||||
newStatus := func() *agentv1.AgentStatus {
|
||||
return &agentv1.AgentStatus{
|
||||
AgentId: "agent-a", CellId: snapshot.SIPCellID, BootId: "boot-a",
|
||||
AgentId: "agent-a", CellId: "cell-a", BootId: "boot-a",
|
||||
AppliedConfigs: []*agentv1.AppliedConfig{{
|
||||
Kind: AppliedConfigKindSIP, Revision: fmt.Sprint(snapshot.SIPArtifactRevision),
|
||||
ConfigSha256: snapshot.SIPArtifactConfigSHA256, State: AppliedConfigStateApplied,
|
||||
Kind: AppliedConfigKindSIP, Revision: fmt.Sprint(snapshot.SIPRevision),
|
||||
State: AppliedConfigStateApplied,
|
||||
ObservedAtUnixMs: 1000,
|
||||
}},
|
||||
}
|
||||
@@ -47,7 +44,7 @@ func TestAgentSIPConfigVerifierRequiresExactAppliedArtifact(t *testing.T) {
|
||||
}
|
||||
wrongCellProbe := &staticAgentStatusProbe{status: newStatus()}
|
||||
wrongCellVerifier := &AgentSIPConfigVerifier{Probe: wrongCellProbe, AgentID: "agent-a", CellID: "cell-b"}
|
||||
if err := wrongCellVerifier.VerifyAppliedSIPConfig(context.Background(), snapshot); err == nil || wrongCellProbe.gotCellID != "" {
|
||||
if err := wrongCellVerifier.VerifyAppliedSIPConfig(context.Background(), snapshot); err == nil || wrongCellProbe.gotCellID != "cell-b" {
|
||||
t.Fatal("SIP config for a different configured cell was probed or accepted")
|
||||
}
|
||||
var nilVerifier *AgentSIPConfigVerifier
|
||||
@@ -69,7 +66,6 @@ func TestAgentSIPConfigVerifierRequiresExactAppliedArtifact(t *testing.T) {
|
||||
{name: "wrong kind", status: newStatus()},
|
||||
{name: "not applied", status: newStatus()},
|
||||
{name: "wrong revision", status: newStatus()},
|
||||
{name: "wrong digest", status: newStatus()},
|
||||
{name: "missing observation time", status: newStatus()},
|
||||
{name: "probe failure", status: newStatus(), err: errors.New("status unavailable")},
|
||||
}
|
||||
@@ -83,15 +79,13 @@ func TestAgentSIPConfigVerifierRequiresExactAppliedArtifact(t *testing.T) {
|
||||
case "missing boot ID":
|
||||
c.status.BootId = ""
|
||||
case "multiple SIP applied configs":
|
||||
c.status.AppliedConfigs = append(c.status.AppliedConfigs, &agentv1.AppliedConfig{Kind: AppliedConfigKindSIP, Revision: "7", ConfigSha256: snapshot.SIPArtifactConfigSHA256, State: AppliedConfigStateApplied, ObservedAtUnixMs: 2000})
|
||||
c.status.AppliedConfigs = append(c.status.AppliedConfigs, &agentv1.AppliedConfig{Kind: AppliedConfigKindSIP, Revision: "7", State: AppliedConfigStateApplied, ObservedAtUnixMs: 2000})
|
||||
case "wrong kind":
|
||||
c.status.AppliedConfigs[0].Kind = "media"
|
||||
case "not applied":
|
||||
c.status.AppliedConfigs[0].State = "pending"
|
||||
case "wrong revision":
|
||||
c.status.AppliedConfigs[0].Revision = "6"
|
||||
case "wrong digest":
|
||||
c.status.AppliedConfigs[0].ConfigSha256 = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
||||
case "missing observation time":
|
||||
c.status.AppliedConfigs[0].ObservedAtUnixMs = 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user