feat: centralize send message production gate
This commit is contained in:
@@ -58,7 +58,7 @@ N13/N14/N15 are pre-business validation results. They can help identify UI eleme
|
|||||||
|
|
||||||
## Current loop
|
## Current loop
|
||||||
|
|
||||||
Current loop: `R6g send audit and idempotency replay diagnostics complete; next R6h central production gate policy for send message`.
|
Current loop: `R6h central production gate policy for send message complete; next R6i B-route connector adapter shell`.
|
||||||
|
|
||||||
Active continuous execution plan: `docs/superpowers/plans/2026-07-10-r6f-r14-continuous-execution-plan.md`.
|
Active continuous execution plan: `docs/superpowers/plans/2026-07-10-r6f-r14-continuous-execution-plan.md`.
|
||||||
|
|
||||||
@@ -239,8 +239,8 @@ Continuous execution plan R6f-R14 is approved and execution has started:
|
|||||||
- Plan file: `docs/superpowers/plans/2026-07-10-r6f-r14-continuous-execution-plan.md`.
|
- Plan file: `docs/superpowers/plans/2026-07-10-r6f-r14-continuous-execution-plan.md`.
|
||||||
- Scope: 15 ordered rounds covering send-message connector/gate hardening, send-file preview/idempotency/package, receive-file download preview, receive-message reconciliation, end-to-end business smoke, and release-candidate report.
|
- Scope: 15 ordered rounds covering send-message connector/gate hardening, send-file preview/idempotency/package, receive-file download preview, receive-message reconciliation, end-to-end business smoke, and release-candidate report.
|
||||||
- Execution rule after approval: one round at a time, status-card update, verification, commit, push, then continue automatically until an evidence-only blocker requires user action.
|
- Execution rule after approval: one round at a time, status-card update, verification, commit, push, then continue automatically until an evidence-only blocker requires user action.
|
||||||
- Last completed round: R6g send audit and idempotency replay diagnostics.
|
- Last completed round: R6h central production gate policy for send message.
|
||||||
- Next active round: R6h central production gate policy for send message.
|
- Next active round: R6i B-route connector adapter shell.
|
||||||
|
|
||||||
R6g send audit and idempotency replay diagnostics is complete:
|
R6g send audit and idempotency replay diagnostics is complete:
|
||||||
|
|
||||||
@@ -250,3 +250,12 @@ R6g send audit and idempotency replay diagnostics is complete:
|
|||||||
- Added `TestISphereSendMessageAuditRedactionIsStable` proving file-backed audit JSONL does not store raw message body or raw idempotency key.
|
- Added `TestISphereSendMessageAuditRedactionIsStable` proving file-backed audit JSONL does not store raw message body or raw idempotency key.
|
||||||
- Standard verification still reports 9 tools and `production_send_enabled=false`.
|
- Standard verification still reports 9 tools and `production_send_enabled=false`.
|
||||||
- Next node: R6h central production gate policy for send message.
|
- Next node: R6h central production gate policy for send message.
|
||||||
|
|
||||||
|
R6h central production gate policy for send message is complete:
|
||||||
|
|
||||||
|
- Added `SendMessageGatePolicy` and `LoadSendMessageGatePolicy`.
|
||||||
|
- Gate reason codes are now explicit: `send_disabled_by_default`, `send_evidence_missing`, `send_connector_missing`, and `send_gate_passed`.
|
||||||
|
- Default production-blocked `isphere_send_message` responses and audit events include `gate_reason_code` and `gate_reason_message`.
|
||||||
|
- `ISPHERE_SEND_PRODUCTION_ENABLED=1` is recognized by policy tests, but it is not enough by itself; evidence and connector gates must also pass.
|
||||||
|
- Standard verification still reports 9 tools and `production_send_enabled=false`.
|
||||||
|
- Next node: R6i B-route connector adapter shell.
|
||||||
|
|||||||
@@ -285,6 +285,15 @@ powershell -NoProfile -ExecutionPolicy Bypass -File scripts\verify-send-audit-id
|
|||||||
|
|
||||||
The diagnostic prints sanitized counts and booleans: `audit_records`, `idempotency_records`, `duplicate_detected`, `conflict_detected`, `raw_body_present`, and `raw_idempotency_key_present`. It exits non-zero if a raw message body field or raw idempotency-key field is found.
|
The diagnostic prints sanitized counts and booleans: `audit_records`, `idempotency_records`, `duplicate_detected`, `conflict_detected`, `raw_body_present`, and `raw_idempotency_key_present`. It exits non-zero if a raw message body field or raw idempotency-key field is found.
|
||||||
|
|
||||||
|
R6h centralizes the production-send gate. Default `execution_mode="production"` responses include machine-readable `gate_reason_code` and `gate_reason_message`. The valid gate codes are:
|
||||||
|
|
||||||
|
- `send_disabled_by_default`
|
||||||
|
- `send_evidence_missing`
|
||||||
|
- `send_connector_missing`
|
||||||
|
- `send_gate_passed`
|
||||||
|
|
||||||
|
`ISPHERE_SEND_PRODUCTION_ENABLED=1` is recognized by the gate policy, but it is not sufficient by itself: the evidence gate and connector gate must also pass before production can be allowed. In the default MCP server path, `production_send_enabled` remains `false`.
|
||||||
|
|
||||||
## 10. Current tool surface and next-stage route
|
## 10. Current tool surface and next-stage route
|
||||||
|
|
||||||
The current runbook verifies nine tools:
|
The current runbook verifies nine tools:
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ git push gitea main
|
|||||||
- Produces: `LoadSendMessageGatePolicy(env map[string]string, evidenceGatePass bool, connectorConfigured bool) SendMessageGatePolicy`.
|
- Produces: `LoadSendMessageGatePolicy(env map[string]string, evidenceGatePass bool, connectorConfigured bool) SendMessageGatePolicy`.
|
||||||
- Valid reason codes: `send_evidence_missing`, `send_connector_missing`, `send_disabled_by_default`, `send_gate_passed`.
|
- Valid reason codes: `send_evidence_missing`, `send_connector_missing`, `send_disabled_by_default`, `send_gate_passed`.
|
||||||
|
|
||||||
- [ ] **Step 1: Write table tests**
|
- [x] **Step 1: Write table tests**
|
||||||
|
|
||||||
Create `TestLoadSendMessageGatePolicy` with cases for default disabled, env set but evidence missing, evidence pass but connector missing, and all pass.
|
Create `TestLoadSendMessageGatePolicy` with cases for default disabled, env set but evidence missing, evidence pass but connector missing, and all pass.
|
||||||
|
|
||||||
@@ -249,15 +249,15 @@ go test ./internal/tools -run TestLoadSendMessageGatePolicy -v
|
|||||||
|
|
||||||
Expected before implementation: FAIL because the gate type does not exist.
|
Expected before implementation: FAIL because the gate type does not exist.
|
||||||
|
|
||||||
- [ ] **Step 2: Implement gate policy**
|
- [x] **Step 2: Implement gate policy**
|
||||||
|
|
||||||
Create `send_message_gate.go` with the exact type/function and reason-code order from the test.
|
Create `send_message_gate.go` with the exact type/function and reason-code order from the test.
|
||||||
|
|
||||||
- [ ] **Step 3: Route blocked production through the gate**
|
- [x] **Step 3: Route blocked production through the gate**
|
||||||
|
|
||||||
Modify `isphere_send_message.go` so blocked production responses include `gate_reason_code` and `gate_reason_message` from `SendMessageGatePolicy`.
|
Modify `isphere_send_message.go` so blocked production responses include `gate_reason_code` and `gate_reason_message` from `SendMessageGatePolicy`.
|
||||||
|
|
||||||
- [ ] **Step 4: Verify and commit**
|
- [x] **Step 4: Verify and commit**
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
git diff --check
|
git diff --check
|
||||||
@@ -272,6 +272,13 @@ git push gitea main
|
|||||||
|
|
||||||
**Acceptance gate:** The default runtime stays blocked, but the response explains the exact reason in machine-readable fields.
|
**Acceptance gate:** The default runtime stays blocked, but the response explains the exact reason in machine-readable fields.
|
||||||
|
|
||||||
|
**R6h Result:**
|
||||||
|
|
||||||
|
- Added `internal/tools/send_message_gate.go` with `SendMessageGatePolicy` and `LoadSendMessageGatePolicy`.
|
||||||
|
- Added table tests for `send_disabled_by_default`, `send_evidence_missing`, `send_connector_missing`, and `send_gate_passed`.
|
||||||
|
- Default `isphere_send_message` production-blocked responses and audit events now include `gate_reason_code` and `gate_reason_message`.
|
||||||
|
- `ISPHERE_SEND_PRODUCTION_ENABLED=1` is recognized by policy tests, but the default MCP runtime still has no real connector and reports `production_send_enabled=false`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### R6i: B-route connector adapter shell
|
### R6i: B-route connector adapter shell
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ type SendMessageAuditEvent struct {
|
|||||||
AckRef string `json:"ack_ref,omitempty"`
|
AckRef string `json:"ack_ref,omitempty"`
|
||||||
ErrorCode string `json:"error_code,omitempty"`
|
ErrorCode string `json:"error_code,omitempty"`
|
||||||
ErrorMessage string `json:"error_message,omitempty"`
|
ErrorMessage string `json:"error_message,omitempty"`
|
||||||
|
GateReasonCode string `json:"gate_reason_code,omitempty"`
|
||||||
|
GateReasonMessage string `json:"gate_reason_message,omitempty"`
|
||||||
AuditRef string `json:"audit_ref"`
|
AuditRef string `json:"audit_ref"`
|
||||||
StartedAt string `json:"started_at"`
|
StartedAt string `json:"started_at"`
|
||||||
FinishedAt string `json:"finished_at"`
|
FinishedAt string `json:"finished_at"`
|
||||||
@@ -130,7 +132,8 @@ func RegisterISphereSendMessageToolWithStateAndConnector(server *mcp.Server, aud
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
finished := time.Now().UTC()
|
finished := time.Now().UTC()
|
||||||
response, event := sendMessagePreviewResponse(normalized, started, finished, connector != nil)
|
gatePolicy := currentSendMessageGatePolicy(connector != nil)
|
||||||
|
response, event := sendMessagePreviewResponse(normalized, started, finished, connector != nil, gatePolicy)
|
||||||
decision, err := idempotencyStore.ReserveSendMessageIdempotency(ctx, sendMessageIdempotencyRecordFromAuditEvent(event))
|
decision, err := idempotencyStore.ReserveSendMessageIdempotency(ctx, sendMessageIdempotencyRecordFromAuditEvent(event))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("%s idempotency reserve failed: %w", ToolNameSendMessage, err)
|
return nil, nil, fmt.Errorf("%s idempotency reserve failed: %w", ToolNameSendMessage, err)
|
||||||
@@ -161,6 +164,12 @@ func defaultSendMessageIdempotencyStore() SendMessageIdempotencyStore {
|
|||||||
return fileSendMessageIdempotencyStore{path: filepath.Join("runs", "send-audit", "send-message-idempotency.jsonl")}
|
return fileSendMessageIdempotencyStore{path: filepath.Join("runs", "send-audit", "send-message-idempotency.jsonl")}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func currentSendMessageGatePolicy(connectorConfigured bool) SendMessageGatePolicy {
|
||||||
|
return LoadSendMessageGatePolicy(map[string]string{
|
||||||
|
EnvSendMessageProductionEnabled: os.Getenv(EnvSendMessageProductionEnabled),
|
||||||
|
}, false, connectorConfigured)
|
||||||
|
}
|
||||||
|
|
||||||
func (s fileSendMessageAuditSink) AppendSendMessageAudit(_ context.Context, event SendMessageAuditEvent) error {
|
func (s fileSendMessageAuditSink) AppendSendMessageAudit(_ context.Context, event SendMessageAuditEvent) error {
|
||||||
if strings.TrimSpace(s.path) == "" {
|
if strings.TrimSpace(s.path) == "" {
|
||||||
return nil
|
return nil
|
||||||
@@ -343,12 +352,14 @@ func normalizeSendMessageExecutionMode(value string) (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendMessagePreviewResponse(input normalizedSendMessageArgs, started time.Time, finished time.Time, connectorAvailable bool) (map[string]any, SendMessageAuditEvent) {
|
func sendMessagePreviewResponse(input normalizedSendMessageArgs, started time.Time, finished time.Time, connectorAvailable bool, gatePolicy SendMessageGatePolicy) (map[string]any, SendMessageAuditEvent) {
|
||||||
sideEffects := sendMessageNoSideEffects()
|
sideEffects := sendMessageNoSideEffects()
|
||||||
status := "planned"
|
status := "planned"
|
||||||
stage := sendMessagePreviewStage
|
stage := sendMessagePreviewStage
|
||||||
ok := true
|
ok := true
|
||||||
blockedReason := any(nil)
|
blockedReason := any(nil)
|
||||||
|
gateReasonCode := ""
|
||||||
|
gateReasonMessage := ""
|
||||||
if input.ExecutionMode == sendMessageProductionMode {
|
if input.ExecutionMode == sendMessageProductionMode {
|
||||||
if connectorAvailable {
|
if connectorAvailable {
|
||||||
stage = sendMessagePreviewStage
|
stage = sendMessagePreviewStage
|
||||||
@@ -356,7 +367,9 @@ func sendMessagePreviewResponse(input normalizedSendMessageArgs, started time.Ti
|
|||||||
ok = false
|
ok = false
|
||||||
status = "blocked"
|
status = "blocked"
|
||||||
stage = sendMessageBlockedStage
|
stage = sendMessageBlockedStage
|
||||||
blockedReason = "production send is blocked until dynamic observation, idempotency audit, and one approved sandbox send gate pass"
|
gateReasonCode = gatePolicy.ReasonCode
|
||||||
|
gateReasonMessage = gatePolicy.ReasonMessage
|
||||||
|
blockedReason = gateReasonMessage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
auditRef := sendMessageAuditRef(input.ContentSHA256, input.IdempotencyKeySHA256)
|
auditRef := sendMessageAuditRef(input.ContentSHA256, input.IdempotencyKeySHA256)
|
||||||
@@ -374,6 +387,8 @@ func sendMessagePreviewResponse(input normalizedSendMessageArgs, started time.Ti
|
|||||||
IdempotencyKeySHA256: input.IdempotencyKeySHA256,
|
IdempotencyKeySHA256: input.IdempotencyKeySHA256,
|
||||||
SendStatus: status,
|
SendStatus: status,
|
||||||
Result: status,
|
Result: status,
|
||||||
|
GateReasonCode: gateReasonCode,
|
||||||
|
GateReasonMessage: gateReasonMessage,
|
||||||
AuditRef: auditRef,
|
AuditRef: auditRef,
|
||||||
StartedAt: started.Format(time.RFC3339Nano),
|
StartedAt: started.Format(time.RFC3339Nano),
|
||||||
FinishedAt: finished.Format(time.RFC3339Nano),
|
FinishedAt: finished.Format(time.RFC3339Nano),
|
||||||
@@ -387,6 +402,8 @@ func sendMessagePreviewResponse(input normalizedSendMessageArgs, started time.Ti
|
|||||||
"connector": sendMessageConnector,
|
"connector": sendMessageConnector,
|
||||||
"connector_mode": sendMessageConnectorMode,
|
"connector_mode": sendMessageConnectorMode,
|
||||||
"connector_stage": stage,
|
"connector_stage": stage,
|
||||||
|
"gate_reason_code": gateReasonCode,
|
||||||
|
"gate_reason_message": gateReasonMessage,
|
||||||
"production_send_enabled": false,
|
"production_send_enabled": false,
|
||||||
"target": map[string]any{
|
"target": map[string]any{
|
||||||
"target_type": input.TargetType,
|
"target_type": input.TargetType,
|
||||||
@@ -406,18 +423,20 @@ func sendMessagePreviewResponse(input normalizedSendMessageArgs, started time.Ti
|
|||||||
},
|
},
|
||||||
"side_effects": sideEffects,
|
"side_effects": sideEffects,
|
||||||
"audit": map[string]any{
|
"audit": map[string]any{
|
||||||
"tool": ToolNameSendMessage,
|
"tool": ToolNameSendMessage,
|
||||||
"source": sendMessageConnector,
|
"source": sendMessageConnector,
|
||||||
"execution_mode": input.ExecutionMode,
|
"execution_mode": input.ExecutionMode,
|
||||||
"connector": sendMessageConnector,
|
"connector": sendMessageConnector,
|
||||||
"connector_mode": sendMessageConnectorMode,
|
"connector_mode": sendMessageConnectorMode,
|
||||||
"connector_stage": stage,
|
"connector_stage": stage,
|
||||||
"content_sha256": input.ContentSHA256,
|
"gate_reason_code": gateReasonCode,
|
||||||
"target_ref": input.TargetRef,
|
"gate_reason_message": gateReasonMessage,
|
||||||
"audit_ref": auditRef,
|
"content_sha256": input.ContentSHA256,
|
||||||
"started_at": started.Format(time.RFC3339Nano),
|
"target_ref": input.TargetRef,
|
||||||
"finished_at": finished.Format(time.RFC3339Nano),
|
"audit_ref": auditRef,
|
||||||
"result": status,
|
"started_at": started.Format(time.RFC3339Nano),
|
||||||
|
"finished_at": finished.Format(time.RFC3339Nano),
|
||||||
|
"result": status,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return response, event
|
return response, event
|
||||||
|
|||||||
@@ -147,6 +147,47 @@ func TestISphereSendMessageProductionIsBlockedWithoutSideEffects(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestISphereSendMessageProductionBlockedIncludesGateReason(t *testing.T) {
|
||||||
|
audit := &fakeSendMessageAuditSink{}
|
||||||
|
session, cleanup := connectToolsTestSession(t, func(server *mcp.Server) {
|
||||||
|
RegisterISphereSendMessageToolWithState(server, audit, newFakeSendMessageIdempotencyStore())
|
||||||
|
})
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
content := "blocked gate reason"
|
||||||
|
callResult, err := session.CallTool(context.Background(), &mcp.CallToolParams{
|
||||||
|
Name: ToolNameSendMessage,
|
||||||
|
Arguments: map[string]any{
|
||||||
|
"target_type": "direct",
|
||||||
|
"target_id": "alice@imopenfire1-lanzhou",
|
||||||
|
"content_text": content,
|
||||||
|
"content_sha256": sha256HexForSendMessageTest(content),
|
||||||
|
"idempotency_key": "idem-gate-reason-1",
|
||||||
|
"execution_mode": "production",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("call production blocked gate reason: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var decoded struct {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
SendStatus string `json:"send_status"`
|
||||||
|
GateReasonCode string `json:"gate_reason_code"`
|
||||||
|
GateReasonMessage string `json:"gate_reason_message"`
|
||||||
|
}
|
||||||
|
payload, _ := json.Marshal(callResult.StructuredContent)
|
||||||
|
if err := json.Unmarshal(payload, &decoded); err != nil {
|
||||||
|
t.Fatalf("decode payload %s: %v", payload, err)
|
||||||
|
}
|
||||||
|
if decoded.OK || decoded.SendStatus != "blocked" || decoded.GateReasonCode != "send_disabled_by_default" || decoded.GateReasonMessage == "" {
|
||||||
|
t.Fatalf("production blocked response missing gate reason: %s", payload)
|
||||||
|
}
|
||||||
|
if len(audit.events) != 1 || audit.events[0].GateReasonCode != "send_disabled_by_default" || audit.events[0].GateReasonMessage == "" {
|
||||||
|
t.Fatalf("audit event missing gate reason: %+v", audit.events)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestISphereSendMessageFakeConnectorAcceptedAudit(t *testing.T) {
|
func TestISphereSendMessageFakeConnectorAcceptedAudit(t *testing.T) {
|
||||||
audit := &fakeSendMessageAuditSink{}
|
audit := &fakeSendMessageAuditSink{}
|
||||||
idempotency := newFakeSendMessageIdempotencyStore()
|
idempotency := newFakeSendMessageIdempotencyStore()
|
||||||
|
|||||||
47
internal/tools/send_message_gate.go
Normal file
47
internal/tools/send_message_gate.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
const EnvSendMessageProductionEnabled = "ISPHERE_SEND_PRODUCTION_ENABLED"
|
||||||
|
|
||||||
|
type SendMessageGatePolicy struct {
|
||||||
|
ProductionAllowed bool
|
||||||
|
ReasonCode string
|
||||||
|
ReasonMessage string
|
||||||
|
EvidenceGatePass bool
|
||||||
|
ConnectorConfigured bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadSendMessageGatePolicy(env map[string]string, evidenceGatePass bool, connectorConfigured bool) SendMessageGatePolicy {
|
||||||
|
policy := SendMessageGatePolicy{
|
||||||
|
EvidenceGatePass: evidenceGatePass,
|
||||||
|
ConnectorConfigured: connectorConfigured,
|
||||||
|
}
|
||||||
|
if !sendMessageProductionEnabled(env) {
|
||||||
|
policy.ReasonCode = "send_disabled_by_default"
|
||||||
|
policy.ReasonMessage = "production send is disabled by default; set ISPHERE_SEND_PRODUCTION_ENABLED=1 only after the evidence and connector gates pass"
|
||||||
|
return policy
|
||||||
|
}
|
||||||
|
if !evidenceGatePass {
|
||||||
|
policy.ReasonCode = "send_evidence_missing"
|
||||||
|
policy.ReasonMessage = "production send is blocked until dynamic observation, idempotency audit, and one approved sandbox send gate pass"
|
||||||
|
return policy
|
||||||
|
}
|
||||||
|
if !connectorConfigured {
|
||||||
|
policy.ReasonCode = "send_connector_missing"
|
||||||
|
policy.ReasonMessage = "production send is blocked because no validated send connector is configured"
|
||||||
|
return policy
|
||||||
|
}
|
||||||
|
policy.ProductionAllowed = true
|
||||||
|
policy.ReasonCode = "send_gate_passed"
|
||||||
|
policy.ReasonMessage = "production send gate passed"
|
||||||
|
return policy
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendMessageProductionEnabled(env map[string]string) bool {
|
||||||
|
if env == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
value := strings.TrimSpace(strings.ToLower(env[EnvSendMessageProductionEnabled]))
|
||||||
|
return value == "1" || value == "true" || value == "yes"
|
||||||
|
}
|
||||||
33
internal/tools/send_message_gate_test.go
Normal file
33
internal/tools/send_message_gate_test.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestLoadSendMessageGatePolicy(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
env map[string]string
|
||||||
|
evidence bool
|
||||||
|
connector bool
|
||||||
|
wantAllowed bool
|
||||||
|
wantReason string
|
||||||
|
}{
|
||||||
|
{name: "default disabled", env: map[string]string{}, evidence: false, connector: false, wantAllowed: false, wantReason: "send_disabled_by_default"},
|
||||||
|
{name: "env set but evidence missing", env: map[string]string{"ISPHERE_SEND_PRODUCTION_ENABLED": "1"}, evidence: false, connector: true, wantAllowed: false, wantReason: "send_evidence_missing"},
|
||||||
|
{name: "evidence pass but connector missing", env: map[string]string{"ISPHERE_SEND_PRODUCTION_ENABLED": "1"}, evidence: true, connector: false, wantAllowed: false, wantReason: "send_connector_missing"},
|
||||||
|
{name: "all pass", env: map[string]string{"ISPHERE_SEND_PRODUCTION_ENABLED": "1"}, evidence: true, connector: true, wantAllowed: true, wantReason: "send_gate_passed"},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got := LoadSendMessageGatePolicy(tc.env, tc.evidence, tc.connector)
|
||||||
|
if got.ProductionAllowed != tc.wantAllowed || got.ReasonCode != tc.wantReason {
|
||||||
|
t.Fatalf("LoadSendMessageGatePolicy() = %+v, want allowed=%v reason=%q", got, tc.wantAllowed, tc.wantReason)
|
||||||
|
}
|
||||||
|
if got.ReasonMessage == "" {
|
||||||
|
t.Fatalf("ReasonMessage is empty for %+v", got)
|
||||||
|
}
|
||||||
|
if got.EvidenceGatePass != tc.evidence || got.ConnectorConfigured != tc.connector {
|
||||||
|
t.Fatalf("evidence/connector fields = %+v, want evidence=%v connector=%v", got, tc.evidence, tc.connector)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user