- Refactor plan, code-review, finalize-task-routing, refine-local-plans, router skills - Add agent-workflow-loop-orchestration skill and plan agent configs - Update roadmap: knowledge-tool-optimization milestones, stream-evidence-gate-core SDD - Add stream-evidence-gate-core task, archive, and Go streamgate package - Update dev-test inventory (edge/node smoke), agent-contract, edge-local-dev-guide - Deprecate USER_REVIEW for output-validation-filters SDD
1182 lines
35 KiB
Go
1182 lines
35 KiB
Go
package streamgate
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestNormalizedConstructorsRejectEmptyPayload verifies that text, reasoning,
|
|
// and tool call fragment constructors reject empty payloads by running the
|
|
// assembled value through Validate.
|
|
func TestNormalizedConstructorsRejectEmptyPayload(t *testing.T) {
|
|
ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
|
|
// Empty text delta
|
|
if _, err := NewTextDeltaEvent("ch", "", ts); err == nil {
|
|
t.Fatal("NewTextDeltaEvent with empty text should return error")
|
|
}
|
|
|
|
// Empty reasoning delta
|
|
if _, err := NewReasoningDeltaEvent("ch", "", ts); err == nil {
|
|
t.Fatal("NewReasoningDeltaEvent with empty reasoning should return error")
|
|
}
|
|
|
|
// Empty tool call args
|
|
if _, err := NewToolCallFragmentEvent("ch", "id", "name", "", ts); err == nil {
|
|
t.Fatal("NewToolCallFragmentEvent with empty args should return error")
|
|
}
|
|
|
|
// Non-empty variants should succeed and pass Validate
|
|
ev, err := NewTextDeltaEvent("ch", "hello", ts)
|
|
if err != nil {
|
|
t.Fatalf("NewTextDeltaEvent with non-empty text: %v", err)
|
|
}
|
|
if err := ev.Validate(); err != nil {
|
|
t.Fatalf("valid text delta failed Validate: %v", err)
|
|
}
|
|
|
|
ev, err = NewReasoningDeltaEvent("ch", "thinking...", ts)
|
|
if err != nil {
|
|
t.Fatalf("NewReasoningDeltaEvent with non-empty reasoning: %v", err)
|
|
}
|
|
if err := ev.Validate(); err != nil {
|
|
t.Fatalf("valid reasoning delta failed Validate: %v", err)
|
|
}
|
|
|
|
ev, err = NewToolCallFragmentEvent("ch", "id", "name", `{"key":"val"}`, ts)
|
|
if err != nil {
|
|
t.Fatalf("NewToolCallFragmentEvent with non-empty args: %v", err)
|
|
}
|
|
if err := ev.Validate(); err != nil {
|
|
t.Fatalf("valid tool call fragment failed Validate: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestFilterValueValidationRejectsMalformedPrivateState verifies that directly
|
|
// assembled FilterOutcome, RecoveryDirective, FilterDecision, SanitizedEvidence,
|
|
// and RecoveryIntent values with opposite payloads, invalid tokens, or unknown
|
|
// enums are rejected by Validate.
|
|
func TestFilterValueValidationRejectsMalformedPrivateState(t *testing.T) {
|
|
ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
|
|
// --- FilterOutcome: evaluated with errorCode ---
|
|
badEvaluated := FilterOutcome{
|
|
kind: FilterOutcomeKindEvaluated,
|
|
decision: buildValidDecision(t),
|
|
errorCode: mustToken("err.code"),
|
|
}
|
|
if err := badEvaluated.Validate(); err == nil {
|
|
t.Fatal("evaluated outcome with errorCode should be rejected")
|
|
}
|
|
|
|
// --- FilterOutcome: evaluation_error with decision ---
|
|
badError := FilterOutcome{
|
|
kind: FilterOutcomeKindEvaluationError,
|
|
decision: buildValidDecision(t),
|
|
errorCode: mustToken("err.code"),
|
|
}
|
|
if err := badError.Validate(); err == nil {
|
|
t.Fatal("evaluation_error outcome with decision should be rejected")
|
|
}
|
|
|
|
// --- FilterOutcome: not_applicable with decision ---
|
|
badNA := FilterOutcome{
|
|
kind: FilterOutcomeKindNotApplicableForEpoch,
|
|
decision: buildValidDecision(t),
|
|
}
|
|
if err := badNA.Validate(); err == nil {
|
|
t.Fatal("not_applicable outcome with decision should be rejected")
|
|
}
|
|
|
|
// --- FilterOutcome: deferred with errorCode ---
|
|
badDeferred := FilterOutcome{
|
|
kind: FilterOutcomeKindDeferredByRequirement,
|
|
errorCode: mustToken("err.code"),
|
|
}
|
|
if err := badDeferred.Validate(); err == nil {
|
|
t.Fatal("deferred outcome with errorCode should be rejected")
|
|
}
|
|
|
|
// --- FilterOutcome: unknown kind ---
|
|
badKind := FilterOutcome{
|
|
kind: FilterOutcomeKind("unknown"),
|
|
}
|
|
if err := badKind.Validate(); err == nil {
|
|
t.Fatal("unknown outcome kind should be rejected")
|
|
}
|
|
|
|
// --- RecoveryDirective: exact with continuation fields ---
|
|
badExact := RecoveryDirective{
|
|
kind: RecoveryDirectiveKindExact,
|
|
requestRef: mustToken("req.ref"),
|
|
cursor: 5,
|
|
snapshotRef: mustToken("snap.ref"),
|
|
}
|
|
if err := badExact.Validate(); err == nil {
|
|
t.Fatal("exact directive with cursor/snapshotRef should be rejected")
|
|
}
|
|
|
|
// --- RecoveryDirective: continuation with exact fields ---
|
|
badCont := RecoveryDirective{
|
|
kind: RecoveryDirectiveKindContinuation,
|
|
requestRef: mustToken("req.ref"),
|
|
snapshotRef: mustToken("snap.ref"),
|
|
cursor: 0,
|
|
}
|
|
if err := badCont.Validate(); err == nil {
|
|
t.Fatal("continuation directive with requestRef should be rejected")
|
|
}
|
|
|
|
// --- RecoveryDirective: schema with exact fields ---
|
|
badSchema := RecoveryDirective{
|
|
kind: RecoveryDirectiveKindSchema,
|
|
requestRef: mustToken("req.ref"),
|
|
schemaRef: mustToken("schema.ref"),
|
|
patchCode: mustToken("patch.code"),
|
|
}
|
|
if err := badSchema.Validate(); err == nil {
|
|
t.Fatal("schema directive with requestRef should be rejected")
|
|
}
|
|
|
|
// --- RecoveryDirective: unknown kind ---
|
|
badDirKind := RecoveryDirective{
|
|
kind: RecoveryDirectiveKind("unknown"),
|
|
}
|
|
if err := badDirKind.Validate(); err == nil {
|
|
t.Fatal("unknown directive kind should be rejected")
|
|
}
|
|
|
|
// --- FilterDecision: invalid consumerID token ---
|
|
badDecision := FilterDecision{
|
|
kind: FilterDecisionKindPass,
|
|
consumerID: StableToken{value: "INVALID"},
|
|
filterID: mustToken("filter.id"),
|
|
ruleID: mustToken("rule.id"),
|
|
evidence: buildValidEvidence(t, ts),
|
|
}
|
|
if err := badDecision.Validate(); err == nil {
|
|
t.Fatal("decision with invalid consumerID should be rejected")
|
|
}
|
|
|
|
// --- SanitizedEvidence: unknown event kind ---
|
|
badEvidence := SanitizedEvidence{
|
|
eventKind: EventKind("unknown_kind"),
|
|
channel: "ch",
|
|
filterRule: mustToken("filter.rule"),
|
|
outcome: FilterOutcomeKindEvaluated,
|
|
descriptorCode: mustToken("desc.code"),
|
|
fingerprint: [32]byte{1},
|
|
count: 0,
|
|
offset: 0,
|
|
timestamp: ts,
|
|
}
|
|
if err := badEvidence.Validate(); err == nil {
|
|
t.Fatal("evidence with unknown event kind should be rejected")
|
|
}
|
|
|
|
// --- SanitizedEvidence: unknown outcome kind ---
|
|
badEvidence2 := SanitizedEvidence{
|
|
eventKind: EventKindTextDelta,
|
|
channel: "ch",
|
|
filterRule: mustToken("filter.rule"),
|
|
outcome: FilterOutcomeKind("unknown_outcome"),
|
|
descriptorCode: mustToken("desc.code"),
|
|
fingerprint: [32]byte{1},
|
|
count: 0,
|
|
offset: 0,
|
|
timestamp: ts,
|
|
}
|
|
if err := badEvidence2.Validate(); err == nil {
|
|
t.Fatal("evidence with unknown outcome kind should be rejected")
|
|
}
|
|
|
|
// --- SanitizedEvidence: invalid filterRule token ---
|
|
badEvidence3 := SanitizedEvidence{
|
|
eventKind: EventKindTextDelta,
|
|
channel: "ch",
|
|
filterRule: StableToken{value: "INVALID"},
|
|
outcome: FilterOutcomeKindEvaluated,
|
|
descriptorCode: mustToken("desc.code"),
|
|
fingerprint: [32]byte{1},
|
|
count: 0,
|
|
offset: 0,
|
|
timestamp: ts,
|
|
}
|
|
if err := badEvidence3.Validate(); err == nil {
|
|
t.Fatal("evidence with invalid filterRule token should be rejected")
|
|
}
|
|
|
|
// --- RecoveryIntent: invalid reason token ---
|
|
badIntent := RecoveryIntent{
|
|
strategy: RecoveryStrategyExactReplay,
|
|
directive: mustBuildExactDirective(t),
|
|
reason: StableToken{value: "INVALID"},
|
|
priority: 0,
|
|
}
|
|
if err := badIntent.Validate(); err == nil {
|
|
t.Fatal("intent with invalid reason token should be rejected")
|
|
}
|
|
}
|
|
|
|
// TestTerminalValueValidationRejectsMalformedPrivateState verifies that directly
|
|
// assembled FailureCause, ExternalDescriptor, and TerminalResult values with
|
|
// invalid required tokens, malformed nested values, or over-long cause chains
|
|
// are rejected by Validate.
|
|
func TestTerminalValueValidationRejectsMalformedPrivateState(t *testing.T) {
|
|
ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
|
|
// --- FailureCause: invalid stage token ---
|
|
badCause := FailureCause{
|
|
stage: StableToken{value: "INVALID"},
|
|
code: mustToken("code"),
|
|
consumer: mustTokenOptional("consumer"),
|
|
filter: mustTokenOptional("filter"),
|
|
ruleID: mustTokenOptional("rule"),
|
|
}
|
|
if err := badCause.Validate(); err == nil {
|
|
t.Fatal("cause with invalid stage should be rejected")
|
|
}
|
|
|
|
// --- FailureCause: invalid code token ---
|
|
badCause2 := FailureCause{
|
|
stage: mustToken("stage"),
|
|
code: StableToken{value: "INVALID"},
|
|
consumer: mustTokenOptional("consumer"),
|
|
filter: mustTokenOptional("filter"),
|
|
ruleID: mustTokenOptional("rule"),
|
|
}
|
|
if err := badCause2.Validate(); err == nil {
|
|
t.Fatal("cause with invalid code should be rejected")
|
|
}
|
|
|
|
// --- ExternalDescriptor: invalid errorType token ---
|
|
badDesc := ExternalDescriptor{
|
|
errorType: StableToken{value: "INVALID"},
|
|
code: mustTokenOptional("code"),
|
|
message: mustToken("message"),
|
|
param: mustTokenOptional("param"),
|
|
}
|
|
if err := badDesc.Validate(); err == nil {
|
|
t.Fatal("descriptor with invalid errorType should be rejected")
|
|
}
|
|
|
|
// --- ExternalDescriptor: invalid message token ---
|
|
badDesc2 := ExternalDescriptor{
|
|
errorType: mustToken("errortype"),
|
|
code: mustTokenOptional("code"),
|
|
message: StableToken{value: "INVALID"},
|
|
param: mustTokenOptional("param"),
|
|
}
|
|
if err := badDesc2.Validate(); err == nil {
|
|
t.Fatal("descriptor with invalid message should be rejected")
|
|
}
|
|
|
|
// --- TerminalResult: error with invalid descriptor ---
|
|
badTr := TerminalResult{
|
|
channel: "ch",
|
|
err: true,
|
|
externalDesc: badDescPtr(),
|
|
occurredAt: ts,
|
|
}
|
|
if err := badTr.Validate(); err == nil {
|
|
t.Fatal("error terminal with invalid descriptor should be rejected")
|
|
}
|
|
|
|
// --- TerminalResult: error with over-long cause chain ---
|
|
overLongCauses := make([]FailureCause, MaxFailureCauses+1)
|
|
for i := range overLongCauses {
|
|
overLongCauses[i] = FailureCause{
|
|
stage: mustToken("stage"),
|
|
code: mustToken("code"),
|
|
}
|
|
}
|
|
badTr2 := TerminalResult{
|
|
channel: "ch",
|
|
err: true,
|
|
externalDesc: validDescPtr(),
|
|
failureCauses: FailureCauseChain{causes: overLongCauses},
|
|
occurredAt: ts,
|
|
}
|
|
if err := badTr2.Validate(); err == nil {
|
|
t.Fatal("error terminal with over-long cause chain should be rejected")
|
|
}
|
|
|
|
// --- TerminalResult: error with malformed cause ---
|
|
badCauseChains := []FailureCause{
|
|
{
|
|
stage: mustToken("stage"),
|
|
code: mustToken("code"),
|
|
},
|
|
{
|
|
stage: StableToken{value: "INVALID"},
|
|
code: mustToken("code"),
|
|
},
|
|
}
|
|
badTr3 := TerminalResult{
|
|
channel: "ch",
|
|
err: true,
|
|
externalDesc: validDescPtr(),
|
|
failureCauses: FailureCauseChain{causes: badCauseChains},
|
|
occurredAt: ts,
|
|
}
|
|
if err := badTr3.Validate(); err == nil {
|
|
t.Fatal("error terminal with malformed nested cause should be rejected")
|
|
}
|
|
|
|
// --- Valid 4-entry error chain should pass ---
|
|
validCauses := make([]FailureCause, MaxFailureCauses)
|
|
for i := range validCauses {
|
|
validCauses[i] = FailureCause{
|
|
stage: mustToken("stage"),
|
|
code: mustToken("code"),
|
|
}
|
|
}
|
|
validTr := TerminalResult{
|
|
channel: "ch",
|
|
err: true,
|
|
externalDesc: validDescPtr(),
|
|
failureCauses: FailureCauseChain{causes: validCauses},
|
|
occurredAt: ts,
|
|
}
|
|
if err := validTr.Validate(); err != nil {
|
|
t.Fatalf("valid 4-entry error chain failed Validate: %v", err)
|
|
}
|
|
|
|
// --- Valid success terminal without descriptor should pass ---
|
|
validSuccess := TerminalResult{
|
|
channel: "ch",
|
|
success: true,
|
|
occurredAt: ts,
|
|
}
|
|
if err := validSuccess.Validate(); err != nil {
|
|
t.Fatalf("valid success terminal failed Validate: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestEvidenceBatchValidationRejectsMalformedStagedStart verifies that EvidenceBatch
|
|
// rejects invalid HTTP status codes in staged response-start regardless of
|
|
// terminal flag, and enforces transport_uncommitted for staged starts.
|
|
func TestEvidenceBatchValidationRejectsMalformedStagedStart(t *testing.T) {
|
|
ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
|
|
// --- Private status 99 (below min) ---
|
|
badStart99 := ResponseStart{
|
|
channel: "ch",
|
|
status: 99,
|
|
timestamp: ts,
|
|
}
|
|
if _, err := NewEvidenceBatch(
|
|
[]NormalizedEvent{},
|
|
nil, nil,
|
|
&badStart99, false,
|
|
CommitStateTransportUncommitted, ts,
|
|
); err == nil {
|
|
t.Fatal("evidence batch with status 99 should be rejected")
|
|
}
|
|
|
|
// --- Private status 600 (above max) ---
|
|
badStart600 := ResponseStart{
|
|
channel: "ch",
|
|
status: 600,
|
|
timestamp: ts,
|
|
}
|
|
if _, err := NewEvidenceBatch(
|
|
[]NormalizedEvent{},
|
|
nil, nil,
|
|
&badStart600, false,
|
|
CommitStateTransportUncommitted, ts,
|
|
); err == nil {
|
|
t.Fatal("evidence batch with status 600 should be rejected")
|
|
}
|
|
|
|
// --- Direct Validate on batch with bad staged start ---
|
|
badBatch := EvidenceBatch{
|
|
capturedAt: ts,
|
|
commitState: CommitStateTransportUncommitted,
|
|
stagedStart: &badStart99,
|
|
terminalFlag: false,
|
|
}
|
|
if err := badBatch.Validate(); err == nil {
|
|
t.Fatal("batch with status 99 staged start should be rejected by Validate")
|
|
}
|
|
|
|
// --- Valid status 100 with transport_uncommitted should pass ---
|
|
validStart := ResponseStart{
|
|
channel: "ch",
|
|
status: 100,
|
|
timestamp: ts,
|
|
}
|
|
if _, err := NewEvidenceBatch(
|
|
[]NormalizedEvent{},
|
|
nil, nil,
|
|
&validStart, false,
|
|
CommitStateTransportUncommitted, ts,
|
|
); err != nil {
|
|
t.Fatalf("valid staged start with transport_uncommitted failed: %v", err)
|
|
}
|
|
|
|
// --- Valid status 599 with transport_uncommitted should pass ---
|
|
validStart2 := ResponseStart{
|
|
channel: "ch",
|
|
status: 599,
|
|
timestamp: ts,
|
|
}
|
|
if _, err := NewEvidenceBatch(
|
|
[]NormalizedEvent{},
|
|
nil, nil,
|
|
&validStart2, false,
|
|
CommitStateTransportUncommitted, ts,
|
|
); err != nil {
|
|
t.Fatalf("valid staged start with status 599 failed: %v", err)
|
|
}
|
|
|
|
// --- Staged start with stream_open should be rejected ---
|
|
if _, err := NewEvidenceBatch(
|
|
[]NormalizedEvent{},
|
|
nil, nil,
|
|
&validStart, false,
|
|
CommitStateStreamOpen, ts,
|
|
); err == nil {
|
|
t.Fatal("staged start with stream_open should be rejected")
|
|
}
|
|
}
|
|
|
|
// --- Helper functions ---
|
|
|
|
func buildValidDecision(t *testing.T) *FilterDecision {
|
|
t.Helper()
|
|
d, err := NewFilterDecision(
|
|
FilterDecisionKindPass,
|
|
"consumer.id", "filter.id", "rule.id",
|
|
buildValidEvidence(t, time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)),
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("buildValidDecision: %v", err)
|
|
}
|
|
return &d
|
|
}
|
|
|
|
func buildValidEvidence(t *testing.T, ts time.Time) SanitizedEvidence {
|
|
t.Helper()
|
|
e, err := NewSanitizedEvidence(
|
|
EventKindTextDelta,
|
|
"ch", "filter.rule", "desc.code",
|
|
[32]byte{1},
|
|
0, 0,
|
|
FilterOutcomeKindEvaluated,
|
|
ts,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("buildValidEvidence: %v", err)
|
|
}
|
|
return e
|
|
}
|
|
|
|
func mustToken(v string) StableToken {
|
|
tk, err := NewStableTokenRequired("test", v)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return tk
|
|
}
|
|
|
|
func mustTokenOptional(v string) StableToken {
|
|
tk, err := NewStableTokenOptional("test", v)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return tk
|
|
}
|
|
|
|
func mustBuildExactDirective(t *testing.T) RecoveryDirective {
|
|
t.Helper()
|
|
d, err := NewRecoveryDirectiveExact("req.ref")
|
|
if err != nil {
|
|
t.Fatalf("mustBuildExactDirective: %v", err)
|
|
}
|
|
return d
|
|
}
|
|
|
|
func badDescPtr() *ExternalDescriptor {
|
|
d := ExternalDescriptor{
|
|
errorType: mustToken("errortype"),
|
|
code: mustTokenOptional("code"),
|
|
message: StableToken{value: "INVALID"},
|
|
param: mustTokenOptional("param"),
|
|
}
|
|
return &d
|
|
}
|
|
|
|
func validDescPtr() *ExternalDescriptor {
|
|
d, err := NewExternalDescriptor("error.type", "code", "message", "")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return &d
|
|
}
|
|
|
|
// TestNormalizedEventValidationEnforcesCauseBound verifies that the Validate
|
|
// method on NormalizedEvent rejects provider_error events whose failure cause
|
|
// chain exceeds MaxFailureCauses (4), while accepting chains of exactly 4.
|
|
// Invalid nested causes remain rejected as before.
|
|
func TestNormalizedEventValidationEnforcesCauseBound(t *testing.T) {
|
|
ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
desc, err := NewExternalDescriptor("error.type", "code", "message", "")
|
|
if err != nil {
|
|
t.Fatalf("NewExternalDescriptor: %v", err)
|
|
}
|
|
|
|
// Build 4 valid causes (at the boundary).
|
|
causes4 := make([]FailureCause, MaxFailureCauses)
|
|
for i := range causes4 {
|
|
causes4[i] = FailureCause{
|
|
stage: mustToken("stage"),
|
|
code: mustToken("code"),
|
|
}
|
|
}
|
|
chain4 := FailureCauseChain{causes: causes4}
|
|
|
|
// Build 5 causes (exceeds boundary).
|
|
causes5 := make([]FailureCause, MaxFailureCauses+1)
|
|
for i := range causes5 {
|
|
causes5[i] = FailureCause{
|
|
stage: mustToken("stage"),
|
|
code: mustToken("code"),
|
|
}
|
|
}
|
|
chain5 := FailureCauseChain{causes: causes5}
|
|
|
|
// 4 causes must pass Validate.
|
|
ev4 := NormalizedEvent{
|
|
kind: EventKindProviderError,
|
|
channel: "ch",
|
|
timestamp: ts,
|
|
disposition: BaseDispositionTerminalErrorCandidate,
|
|
externalDesc: &desc,
|
|
failureCauses: chain4,
|
|
}
|
|
if err := ev4.Validate(); err != nil {
|
|
t.Fatalf("4-cause provider error event should pass Validate: %v", err)
|
|
}
|
|
|
|
// 5 causes must fail Validate.
|
|
ev5 := NormalizedEvent{
|
|
kind: EventKindProviderError,
|
|
channel: "ch",
|
|
timestamp: ts,
|
|
disposition: BaseDispositionTerminalErrorCandidate,
|
|
externalDesc: &desc,
|
|
failureCauses: chain5,
|
|
}
|
|
if err := ev5.Validate(); err == nil {
|
|
t.Fatal("5-cause provider error event should fail Validate")
|
|
}
|
|
|
|
// Invalid nested cause must still fail.
|
|
badCauses := []FailureCause{
|
|
{
|
|
stage: mustToken("stage"),
|
|
code: mustToken("code"),
|
|
},
|
|
{
|
|
stage: StableToken{value: "INVALID"},
|
|
code: mustToken("code"),
|
|
},
|
|
}
|
|
badChain := FailureCauseChain{causes: badCauses}
|
|
evBad := NormalizedEvent{
|
|
kind: EventKindProviderError,
|
|
channel: "ch",
|
|
timestamp: ts,
|
|
disposition: BaseDispositionTerminalErrorCandidate,
|
|
externalDesc: &desc,
|
|
failureCauses: badChain,
|
|
}
|
|
if err := evBad.Validate(); err == nil {
|
|
t.Fatal("provider error with invalid nested cause should fail Validate")
|
|
}
|
|
}
|
|
|
|
// TestFilterDecisionConstructorCopiesRecoveryIntent verifies that NewFilterDecision
|
|
// defensively copies the caller-owned *RecoveryIntent so that subsequent
|
|
// variable reassignment of the original intent does not affect the returned
|
|
// FilterDecision or any previously created evaluated FilterOutcome.
|
|
func TestFilterDecisionConstructorCopiesRecoveryIntent(t *testing.T) {
|
|
ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
evidence := buildValidEvidence(t, ts)
|
|
|
|
directive, err := NewRecoveryDirectiveExact("req.ref")
|
|
if err != nil {
|
|
t.Fatalf("NewRecoveryDirectiveExact: %v", err)
|
|
}
|
|
intent, err := NewRecoveryIntent(RecoveryStrategyExactReplay, directive, "reason", 0)
|
|
if err != nil {
|
|
t.Fatalf("NewRecoveryIntent: %v", err)
|
|
}
|
|
|
|
decision, err := NewFilterDecision(
|
|
FilterDecisionKindViolation,
|
|
"consumer.id", "filter.id", "rule.id",
|
|
evidence,
|
|
&intent,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewFilterDecision: %v", err)
|
|
}
|
|
|
|
// Create an evaluated outcome from the decision before any reassignment.
|
|
outcome, err := NewFilterOutcomeEvaluated(decision)
|
|
if err != nil {
|
|
t.Fatalf("NewFilterOutcomeEvaluated: %v", err)
|
|
}
|
|
|
|
// Reassign the original intent variable to a zero value.
|
|
intent = RecoveryIntent{}
|
|
|
|
// The decision must still validate and retain its original intent values.
|
|
if err := decision.Validate(); err != nil {
|
|
t.Fatalf("decision should survive caller intent reassignment: %v", err)
|
|
}
|
|
d := decision.Intent()
|
|
if d == nil {
|
|
t.Fatal("decision Intent() returned nil")
|
|
}
|
|
if d.Strategy() != RecoveryStrategyExactReplay {
|
|
t.Fatalf("decision Intent strategy should be exact_replay, got %s", d.Strategy())
|
|
}
|
|
if d.Reason() != "reason" {
|
|
t.Fatalf("decision Intent reason should be 'reason', got %s", d.Reason())
|
|
}
|
|
if d.Priority() != 0 {
|
|
t.Fatalf("decision Intent priority should be 0, got %d", d.Priority())
|
|
}
|
|
|
|
// The evaluated outcome must also survive and retain nested intent values.
|
|
if err := outcome.Validate(); err != nil {
|
|
t.Fatalf("outcome should survive caller intent reassignment: %v", err)
|
|
}
|
|
oDecision := outcome.Decision()
|
|
if oDecision == nil {
|
|
t.Fatal("outcome Decision() returned nil")
|
|
}
|
|
oIntent := oDecision.Intent()
|
|
if oIntent == nil {
|
|
t.Fatal("outcome decision Intent() returned nil")
|
|
}
|
|
if oIntent.Strategy() != RecoveryStrategyExactReplay {
|
|
t.Fatalf("outcome decision Intent strategy should be exact_replay, got %s", oIntent.Strategy())
|
|
}
|
|
if oIntent.Reason() != "reason" {
|
|
t.Fatalf("outcome decision Intent reason should be 'reason', got %s", oIntent.Reason())
|
|
}
|
|
if oIntent.Priority() != 0 {
|
|
t.Fatalf("outcome decision Intent priority should be 0, got %d", oIntent.Priority())
|
|
}
|
|
}
|
|
|
|
// TestFilterOutcomeValidationRejectsEachForbiddenField verifies that each
|
|
// FilterOutcome variant rejects exactly the forbidden field set for its kind
|
|
// when injected independently.
|
|
func TestFilterOutcomeValidationRejectsEachForbiddenField(t *testing.T) {
|
|
ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
evidence := buildValidEvidence(t, ts)
|
|
decision, err := NewFilterDecision(
|
|
FilterDecisionKindPass,
|
|
"consumer.id", "filter.id", "rule.id",
|
|
evidence, nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewFilterDecision: %v", err)
|
|
}
|
|
errCode := mustToken("err.code")
|
|
|
|
tests := []struct {
|
|
name string
|
|
make func() FilterOutcome
|
|
}{
|
|
{
|
|
name: "evaluated_with_errorCode",
|
|
make: func() FilterOutcome {
|
|
return FilterOutcome{
|
|
kind: FilterOutcomeKindEvaluated,
|
|
decision: &decision,
|
|
errorCode: errCode,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "evaluation_error_with_decision",
|
|
make: func() FilterOutcome {
|
|
return FilterOutcome{
|
|
kind: FilterOutcomeKindEvaluationError,
|
|
decision: &decision,
|
|
errorCode: errCode,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "not_applicable_with_decision",
|
|
make: func() FilterOutcome {
|
|
return FilterOutcome{
|
|
kind: FilterOutcomeKindNotApplicableForEpoch,
|
|
decision: &decision,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "not_applicable_with_errorCode",
|
|
make: func() FilterOutcome {
|
|
return FilterOutcome{
|
|
kind: FilterOutcomeKindNotApplicableForEpoch,
|
|
errorCode: errCode,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "deferred_with_decision",
|
|
make: func() FilterOutcome {
|
|
return FilterOutcome{
|
|
kind: FilterOutcomeKindDeferredByRequirement,
|
|
decision: &decision,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "deferred_with_errorCode",
|
|
make: func() FilterOutcome {
|
|
return FilterOutcome{
|
|
kind: FilterOutcomeKindDeferredByRequirement,
|
|
errorCode: errCode,
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if err := tc.make().Validate(); err == nil {
|
|
t.Fatalf("%s should be rejected", tc.name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestRecoveryDirectiveValidationRejectsEachForbiddenField verifies that each
|
|
// RecoveryDirective variant rejects exactly the forbidden fields for its kind
|
|
// when injected independently.
|
|
func TestRecoveryDirectiveValidationRejectsEachForbiddenField(t *testing.T) {
|
|
exactRef := mustToken("req.ref")
|
|
snapRef := mustToken("snap.ref")
|
|
schemaRef := mustToken("schema.ref")
|
|
patchCode := mustToken("patch.code")
|
|
|
|
tests := []struct {
|
|
name string
|
|
make func() RecoveryDirective
|
|
}{
|
|
{
|
|
name: "exact_with_cursor",
|
|
make: func() RecoveryDirective {
|
|
return RecoveryDirective{
|
|
kind: RecoveryDirectiveKindExact,
|
|
requestRef: exactRef,
|
|
cursor: 5,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "exact_with_snapshotRef",
|
|
make: func() RecoveryDirective {
|
|
return RecoveryDirective{
|
|
kind: RecoveryDirectiveKindExact,
|
|
requestRef: exactRef,
|
|
snapshotRef: snapRef,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "exact_with_schemaRef",
|
|
make: func() RecoveryDirective {
|
|
return RecoveryDirective{
|
|
kind: RecoveryDirectiveKindExact,
|
|
requestRef: exactRef,
|
|
schemaRef: schemaRef,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "exact_with_patchCode",
|
|
make: func() RecoveryDirective {
|
|
return RecoveryDirective{
|
|
kind: RecoveryDirectiveKindExact,
|
|
requestRef: exactRef,
|
|
patchCode: patchCode,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "continuation_with_requestRef",
|
|
make: func() RecoveryDirective {
|
|
return RecoveryDirective{
|
|
kind: RecoveryDirectiveKindContinuation,
|
|
requestRef: exactRef,
|
|
snapshotRef: snapRef,
|
|
cursor: 0,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "continuation_with_schemaRef",
|
|
make: func() RecoveryDirective {
|
|
return RecoveryDirective{
|
|
kind: RecoveryDirectiveKindContinuation,
|
|
snapshotRef: snapRef,
|
|
schemaRef: schemaRef,
|
|
cursor: 0,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "continuation_with_patchCode",
|
|
make: func() RecoveryDirective {
|
|
return RecoveryDirective{
|
|
kind: RecoveryDirectiveKindContinuation,
|
|
snapshotRef: snapRef,
|
|
patchCode: patchCode,
|
|
cursor: 0,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "schema_with_requestRef",
|
|
make: func() RecoveryDirective {
|
|
return RecoveryDirective{
|
|
kind: RecoveryDirectiveKindSchema,
|
|
requestRef: exactRef,
|
|
schemaRef: schemaRef,
|
|
patchCode: patchCode,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "schema_with_cursor",
|
|
make: func() RecoveryDirective {
|
|
return RecoveryDirective{
|
|
kind: RecoveryDirectiveKindSchema,
|
|
schemaRef: schemaRef,
|
|
patchCode: patchCode,
|
|
cursor: 5,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "schema_with_snapshotRef",
|
|
make: func() RecoveryDirective {
|
|
return RecoveryDirective{
|
|
kind: RecoveryDirectiveKindSchema,
|
|
snapshotRef: snapRef,
|
|
schemaRef: schemaRef,
|
|
patchCode: patchCode,
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if err := tc.make().Validate(); err == nil {
|
|
t.Fatalf("%s should be rejected", tc.name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestFilterAndTerminalConstructorsProduceValidValues verifies that all public
|
|
// constructors in the filter/terminal domain produce values that pass their
|
|
// own Validate, covering every normal variant matrix.
|
|
func TestFilterAndTerminalConstructorsProduceValidValues(t *testing.T) {
|
|
ts := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
|
|
// --- FilterDecision: all kinds with valid evidence ---
|
|
evidence := buildValidEvidence(t, ts)
|
|
kinds := []FilterDecisionKind{
|
|
FilterDecisionKindPass,
|
|
FilterDecisionKindObserve,
|
|
FilterDecisionKindViolation,
|
|
FilterDecisionKindFatal,
|
|
FilterDecisionKindReplacement,
|
|
}
|
|
for _, k := range kinds {
|
|
t.Run("FilterDecision_"+string(k), func(t *testing.T) {
|
|
d, err := NewFilterDecision(k, "consumer.id", "filter.id", "rule.id", evidence, nil)
|
|
if err != nil {
|
|
t.Fatalf("NewFilterDecision %s: %v", k, err)
|
|
}
|
|
if err := d.Validate(); err != nil {
|
|
t.Fatalf("FilterDecision %s failed Validate: %v", k, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- FilterDecision with violation + recovery intent ---
|
|
directive, err := NewRecoveryDirectiveExact("req.ref")
|
|
if err != nil {
|
|
t.Fatalf("NewRecoveryDirectiveExact: %v", err)
|
|
}
|
|
intent, err := NewRecoveryIntent(RecoveryStrategyExactReplay, directive, "reason", 0)
|
|
if err != nil {
|
|
t.Fatalf("NewRecoveryIntent: %v", err)
|
|
}
|
|
d, err := NewFilterDecision(FilterDecisionKindViolation, "consumer.id", "filter.id", "rule.id", evidence, &intent)
|
|
if err != nil {
|
|
t.Fatalf("NewFilterDecision violation+intent: %v", err)
|
|
}
|
|
if err := d.Validate(); err != nil {
|
|
t.Fatalf("FilterDecision violation+intent failed Validate: %v", err)
|
|
}
|
|
|
|
// --- FilterOutcome: all kinds ---
|
|
t.Run("FilterOutcome_evaluated", func(t *testing.T) {
|
|
o, err := NewFilterOutcomeEvaluated(d)
|
|
if err != nil {
|
|
t.Fatalf("NewFilterOutcomeEvaluated: %v", err)
|
|
}
|
|
if err := o.Validate(); err != nil {
|
|
t.Fatalf("FilterOutcome evaluated failed Validate: %v", err)
|
|
}
|
|
})
|
|
t.Run("FilterOutcome_evaluation_error", func(t *testing.T) {
|
|
o, err := NewFilterOutcomeEvaluationError("err.code")
|
|
if err != nil {
|
|
t.Fatalf("NewFilterOutcomeEvaluationError: %v", err)
|
|
}
|
|
if err := o.Validate(); err != nil {
|
|
t.Fatalf("FilterOutcome evaluation_error failed Validate: %v", err)
|
|
}
|
|
})
|
|
t.Run("FilterOutcome_not_applicable", func(t *testing.T) {
|
|
o := NewFilterOutcomeNotApplicableForEpoch()
|
|
if err := o.Validate(); err != nil {
|
|
t.Fatalf("FilterOutcome not_applicable failed Validate: %v", err)
|
|
}
|
|
})
|
|
t.Run("FilterOutcome_deferred", func(t *testing.T) {
|
|
o := NewFilterOutcomeDeferredByRequirement()
|
|
if err := o.Validate(); err != nil {
|
|
t.Fatalf("FilterOutcome deferred failed Validate: %v", err)
|
|
}
|
|
})
|
|
|
|
// --- RecoveryDirective: all kinds ---
|
|
t.Run("RecoveryDirective_exact", func(t *testing.T) {
|
|
dir, err := NewRecoveryDirectiveExact("req.ref")
|
|
if err != nil {
|
|
t.Fatalf("NewRecoveryDirectiveExact: %v", err)
|
|
}
|
|
if err := dir.Validate(); err != nil {
|
|
t.Fatalf("RecoveryDirective exact failed Validate: %v", err)
|
|
}
|
|
})
|
|
t.Run("RecoveryDirective_continuation", func(t *testing.T) {
|
|
dir, err := NewRecoveryDirectiveContinuation(10, "snap.ref")
|
|
if err != nil {
|
|
t.Fatalf("NewRecoveryDirectiveContinuation: %v", err)
|
|
}
|
|
if err := dir.Validate(); err != nil {
|
|
t.Fatalf("RecoveryDirective continuation failed Validate: %v", err)
|
|
}
|
|
})
|
|
t.Run("RecoveryDirective_schema", func(t *testing.T) {
|
|
dir, err := NewRecoveryDirectiveSchema("schema.ref", "patch.code")
|
|
if err != nil {
|
|
t.Fatalf("NewRecoveryDirectiveSchema: %v", err)
|
|
}
|
|
if err := dir.Validate(); err != nil {
|
|
t.Fatalf("RecoveryDirective schema failed Validate: %v", err)
|
|
}
|
|
})
|
|
|
|
// --- RecoveryIntent: all strategies ---
|
|
strategies := []struct {
|
|
strategy RecoveryStrategy
|
|
directive RecoveryDirective
|
|
}{
|
|
{RecoveryStrategyExactReplay, mustBuildExactDirective(t)},
|
|
{RecoveryStrategyContinuationRepair, mustBuildContinuationDirective(t)},
|
|
{RecoveryStrategySchemaRepair, mustBuildSchemaDirective(t)},
|
|
}
|
|
for _, s := range strategies {
|
|
t.Run("RecoveryIntent_"+string(s.strategy), func(t *testing.T) {
|
|
intent, err := NewRecoveryIntent(s.strategy, s.directive, "reason", 0)
|
|
if err != nil {
|
|
t.Fatalf("NewRecoveryIntent %s: %v", s.strategy, err)
|
|
}
|
|
if err := intent.Validate(); err != nil {
|
|
t.Fatalf("RecoveryIntent %s failed Validate: %v", s.strategy, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- SanitizedEvidence: valid variant ---
|
|
t.Run("SanitizedEvidence", func(t *testing.T) {
|
|
e, err := NewSanitizedEvidence(
|
|
EventKindTextDelta, "ch", "filter.rule", "desc.code",
|
|
[32]byte{1}, 0, 0,
|
|
FilterOutcomeKindEvaluated, ts,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewSanitizedEvidence: %v", err)
|
|
}
|
|
if err := e.Validate(); err != nil {
|
|
t.Fatalf("SanitizedEvidence failed Validate: %v", err)
|
|
}
|
|
})
|
|
|
|
// --- FailureCause: required-only (stage + code, optional fields empty) ---
|
|
t.Run("FailureCause_required_only", func(t *testing.T) {
|
|
fc, err := NewFailureCause("stage", "code", "", "", "")
|
|
if err != nil {
|
|
t.Fatalf("NewFailureCause required-only: %v", err)
|
|
}
|
|
if err := fc.Validate(); err != nil {
|
|
t.Fatalf("FailureCause required-only failed Validate: %v", err)
|
|
}
|
|
})
|
|
|
|
// --- FailureCause: optional-full (all fields provided) ---
|
|
t.Run("FailureCause_optional_full", func(t *testing.T) {
|
|
fc, err := NewFailureCause("stage", "code", "consumer", "filter", "rule")
|
|
if err != nil {
|
|
t.Fatalf("NewFailureCause optional-full: %v", err)
|
|
}
|
|
if err := fc.Validate(); err != nil {
|
|
t.Fatalf("FailureCause optional-full failed Validate: %v", err)
|
|
}
|
|
})
|
|
|
|
// --- ExternalDescriptor: required-only (errorType + message, optional fields empty) ---
|
|
t.Run("ExternalDescriptor_required_only", func(t *testing.T) {
|
|
ed, err := NewExternalDescriptor("error.type", "", "message", "")
|
|
if err != nil {
|
|
t.Fatalf("NewExternalDescriptor required-only: %v", err)
|
|
}
|
|
if err := ed.Validate(); err != nil {
|
|
t.Fatalf("ExternalDescriptor required-only failed Validate: %v", err)
|
|
}
|
|
})
|
|
|
|
// --- ExternalDescriptor: optional-full (all fields provided) ---
|
|
t.Run("ExternalDescriptor_optional_full", func(t *testing.T) {
|
|
ed, err := NewExternalDescriptor("error.type", "code", "message", "param")
|
|
if err != nil {
|
|
t.Fatalf("NewExternalDescriptor optional-full: %v", err)
|
|
}
|
|
if err := ed.Validate(); err != nil {
|
|
t.Fatalf("ExternalDescriptor optional-full failed Validate: %v", err)
|
|
}
|
|
})
|
|
|
|
// --- Invalid inputs must be rejected by constructors ---
|
|
t.Run("NewFilterDecision_unknownKind", func(t *testing.T) {
|
|
_, err := NewFilterDecision(FilterDecisionKind("bad"), "consumer.id", "filter.id", "rule.id", evidence, nil)
|
|
if err == nil {
|
|
t.Fatal("NewFilterDecision unknown kind should return error")
|
|
}
|
|
})
|
|
t.Run("NewFilterDecision_nonViolationWithIntent", func(t *testing.T) {
|
|
_, err := NewFilterDecision(FilterDecisionKindPass, "consumer.id", "filter.id", "rule.id", evidence, &intent)
|
|
if err == nil {
|
|
t.Fatal("NewFilterDecision non-violation with intent should return error")
|
|
}
|
|
})
|
|
t.Run("NewFilterOutcomeEvaluated_invalidDecision", func(t *testing.T) {
|
|
badDecision := FilterDecision{
|
|
kind: FilterDecisionKindPass,
|
|
consumerID: StableToken{value: "INVALID"},
|
|
filterID: mustToken("filter.id"),
|
|
ruleID: mustToken("rule.id"),
|
|
evidence: evidence,
|
|
}
|
|
_, err := NewFilterOutcomeEvaluated(badDecision)
|
|
if err == nil {
|
|
t.Fatal("NewFilterOutcomeEvaluated with invalid decision should return error")
|
|
}
|
|
})
|
|
t.Run("NewFilterOutcomeEvaluationError_emptyCode", func(t *testing.T) {
|
|
_, err := NewFilterOutcomeEvaluationError("")
|
|
if err == nil {
|
|
t.Fatal("NewFilterOutcomeEvaluationError with empty code should return error")
|
|
}
|
|
})
|
|
t.Run("NewRecoveryDirectiveExact_emptyRequestID", func(t *testing.T) {
|
|
_, err := NewRecoveryDirectiveExact("")
|
|
if err == nil {
|
|
t.Fatal("NewRecoveryDirectiveExact with empty requestID should return error")
|
|
}
|
|
})
|
|
t.Run("NewRecoveryDirectiveContinuation_negativeCursor", func(t *testing.T) {
|
|
_, err := NewRecoveryDirectiveContinuation(-1, "snap.ref")
|
|
if err == nil {
|
|
t.Fatal("NewRecoveryDirectiveContinuation with negative cursor should return error")
|
|
}
|
|
})
|
|
t.Run("NewRecoveryDirectiveSchema_emptySchemaRef", func(t *testing.T) {
|
|
_, err := NewRecoveryDirectiveSchema("", "patch.code")
|
|
if err == nil {
|
|
t.Fatal("NewRecoveryDirectiveSchema with empty schemaRef should return error")
|
|
}
|
|
})
|
|
t.Run("NewRecoveryIntent_emptyReason", func(t *testing.T) {
|
|
exactDir, err := NewRecoveryDirectiveExact("req.ref")
|
|
if err != nil {
|
|
t.Fatalf("NewRecoveryDirectiveExact: %v", err)
|
|
}
|
|
_, err = NewRecoveryIntent(RecoveryStrategyExactReplay, exactDir, "", 0)
|
|
if err == nil {
|
|
t.Fatal("NewRecoveryIntent with empty reason should return error")
|
|
}
|
|
})
|
|
t.Run("NewSanitizedEvidence_zeroFingerprint", func(t *testing.T) {
|
|
_, err := NewSanitizedEvidence(
|
|
EventKindTextDelta, "ch", "filter.rule", "desc.code",
|
|
[32]byte{}, 0, 0,
|
|
FilterOutcomeKindEvaluated, ts,
|
|
)
|
|
if err == nil {
|
|
t.Fatal("NewSanitizedEvidence with zero fingerprint should return error")
|
|
}
|
|
})
|
|
t.Run("NewFailureCause_invalidStage", func(t *testing.T) {
|
|
_, err := NewFailureCause("INVALID", "code", "", "", "")
|
|
if err == nil {
|
|
t.Fatal("NewFailureCause invalid stage should return error")
|
|
}
|
|
})
|
|
t.Run("NewExternalDescriptor_invalidErrorType", func(t *testing.T) {
|
|
_, err := NewExternalDescriptor("INVALID", "code", "message", "")
|
|
if err == nil {
|
|
t.Fatal("NewExternalDescriptor invalid errorType should return error")
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- Additional helpers for TestFilterAndTerminalConstructorsProduceValidValues ---
|
|
|
|
func mustBuildContinuationDirective(t *testing.T) RecoveryDirective {
|
|
t.Helper()
|
|
d, err := NewRecoveryDirectiveContinuation(10, "snap.ref")
|
|
if err != nil {
|
|
t.Fatalf("mustBuildContinuationDirective: %v", err)
|
|
}
|
|
return d
|
|
}
|
|
|
|
func mustBuildSchemaDirective(t *testing.T) RecoveryDirective {
|
|
t.Helper()
|
|
d, err := NewRecoveryDirectiveSchema("schema.ref", "patch.code")
|
|
if err != nil {
|
|
t.Fatalf("mustBuildSchemaDirective: %v", err)
|
|
}
|
|
return d
|
|
}
|