Bind strict Dispatcher Mock environment to current SaaS read client

This commit is contained in:
2026-09-30 03:33:01 +08:00
parent 6f3404bd09
commit 6cc9357dd8
3 changed files with 92 additions and 0 deletions
@@ -0,0 +1,22 @@
package main
import (
"net/http"
"git.ipao.vip/rogee/go-sip/internal/config"
"git.ipao.vip/rogee/go-sip/internal/configread"
)
// dispatcherConfigurationClient binds the deployment-owned Dispatcher identity
// to the sole read-only SaaS configuration client. It opens no external resource.
func dispatcherConfigurationClient(mode string, httpClient *http.Client) (config.DispatcherEnvironment, *configread.Client, error) {
settings, err := config.LoadDispatcherEnvironment(mode)
if err != nil {
return config.DispatcherEnvironment{}, nil, err
}
reader, err := configread.NewClient(settings.SaaSBaseURL, settings.DispatcherID, settings.SecretKey, httpClient)
if err != nil {
return config.DispatcherEnvironment{}, nil, err
}
return settings, reader, nil
}
@@ -0,0 +1,69 @@
package main
import (
"context"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"sync/atomic"
"testing"
)
func TestDispatcherConfigurationClientReadsApprovedSIPWithExplicitIdentity(t *testing.T) {
const dispatcherID = "c046b893-8628-4589-ae50-619d049248a6"
const secret = "synthetic-placeholder-not-a-credential"
fixture, err := os.ReadFile(filepath.Join("..", "..", "contracts", "local", "examples", "config-read-sip.json"))
if err != nil {
t.Fatal(err)
}
var requests atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests.Add(1)
if r.Method != http.MethodGet || r.URL.Path != "/internal/v1/dispatcher/sip" || r.Header.Get("X-DISPATCHER-id") != dispatcherID || r.Header.Get("X-DISPATCHER-SECRET-KEY") != secret {
t.Errorf("unexpected SaaS read method, path or Dispatcher headers: %s %s", r.Method, r.URL.Path)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(fixture)
}))
defer server.Close()
path := filepath.Join(t.TempDir(), "dispatcher.sqlite")
t.Setenv("DISPATCHER_ID", dispatcherID)
t.Setenv("DISPATCHER_SECRET_KEY", secret)
t.Setenv("SAAS_BASE_URL", server.URL)
t.Setenv("RABBITMQ_URL", "amqp://127.0.0.1:5672/%2Fmock")
t.Setenv("DISPATCHER_SQLITE_PATH", path)
settings, reader, err := dispatcherConfigurationClient("mock", server.Client())
if err != nil {
t.Fatal(err)
}
if settings.DispatcherID != dispatcherID || settings.SQLitePath != path {
t.Fatal("dispatcher startup changed approved deployment identity or SQLite path")
}
sip, err := reader.ReadCurrentSIP(context.Background())
if err != nil {
t.Fatal(err)
}
if sip.DispatcherID != dispatcherID || sip.Revision != 8 || requests.Load() != 1 {
t.Fatalf("wrong approved SIP snapshot or HTTP request count: revision=%d requests=%d", sip.Revision, requests.Load())
}
if _, err := os.Stat(path); !os.IsNotExist(err) {
t.Fatalf("HTTP client assembly opened SQLite: %v", err)
}
}
func TestDispatcherConfigurationClientNeverUsesLegacyHTTPSetting(t *testing.T) {
t.Setenv("DISPATCHER_ID", "c046b893-8628-4589-ae50-619d049248a6")
t.Setenv("DISPATCHER_SECRET_KEY", "synthetic-placeholder-not-a-credential")
t.Setenv("SAAS_BASE_URL", "")
t.Setenv("DISPATCHER_CONFIG_READ_BASE_URL", "http://127.0.0.1:8080")
t.Setenv("RABBITMQ_URL", "amqp://127.0.0.1:5672/%2Fmock")
t.Setenv("DISPATCHER_SQLITE_PATH", filepath.Join(t.TempDir(), "dispatcher.sqlite"))
if _, _, err := dispatcherConfigurationClient("mock", nil); err == nil || !strings.Contains(err.Error(), "SAAS_BASE_URL") {
t.Fatalf("legacy HTTP source silently became a fallback: %v", err)
}
}
@@ -37,6 +37,7 @@
## P03:HTTP 读取分批改造(未整体签收)
- `contract.ValidateCurrent` 与 `configread` 按当前 Schema 读取 SIP、provider、task、quota 和 cursor 任务发现;严格检查数字 tenant_id、本 D 归属及不可变配置。provider 凭据原值只保留在内存快照,不写日志;Agent 参数中的显式 0/false 保真;无旧 Schema/旧配置回退。
- CLI 组装辅助 `dispatcherConfigurationClient` 将严格 Mock 环境预检与当前 HTTP Client 绑定;本机 HTTP 隔离测试实际读取 SIP,核对固定 `/internal/v1/dispatcher/sip`、D 身份/密钥 Header、revision 与不打开 SQLite;旧 `DISPATCHER_CONFIG_READ_BASE_URL` 不可充当缺失的当前地址。`go test ./cmd/sip-go-agent -run '^TestDispatcherConfigurationClient' -count=1` 通过;主 `dispatcher` 命令尚未调用该辅助,不能声称启动已切换。
- `store.OpenCurrent` 新建数字租户 SQLite 状态;旧表、旧版当前布局、残缺布局均在写入前拒绝并保留原记录;不执行旧数据迁移或自动清理。启动时完整发现同一快照一次提交,分页增量逐页持久提交后才推进**内存** cursor;失败关闭准入,重启重新取完整快照。HTTP 的旧 running 不能解除 MQ 暂停/终止,同 revision 异内容及跨任务 SIP/租户额度冲突拒绝。
- `CurrentBootstrap` 先关闭准入,核验 SIP 全量与 Agent/Asterisk 已加载 revision、读取任务和 provider/额度,再排空 MQ 控制积压,最后依据已验证 SIP revision 开准入;有更新的持久 SIP 通知时保持关闭但控制与结果处理仍可继续。`CurrentDiscoveryFollower` 逐页绑定任务快照;HTTP 错误、失效或授权不一致只失败,不回退旧读取。**目前只在隔离运行组件中调用,尚未接入 `cmd/sip-go-agent/main.go`;provider 向真实 Agent 交付及真实加载尚待 P05/P07。**
- TDD 与回归:`go test ./internal/configread ./internal/tenant ./internal/store ./internal/dispatcher -count=1`、`bash scripts/check-current-contracts.sh`、已提交 `a0118e3` 的干净归档测试通过;旧布局行/表原样保留由 `TestCurrentStoreRefusesPreviousCurrentLayoutBeforeModifyingDatabase` 覆盖。