- 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
657 lines
20 KiB
Go
657 lines
20 KiB
Go
package streamgate
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestFailureCauseChainCapsAtFour verifies that FailureCauseChain enforces the
|
|
// MaxFailureCauses (4) cap on construction and Append, that accessors return
|
|
// defensive copies, that distinct codes prove the latest-four retention order,
|
|
// and that invalid stage/code/consumer/filter/rule tokens are rejected.
|
|
func TestFailureCauseChainCapsAtFour(t *testing.T) {
|
|
// --- Empty chain ---
|
|
empty, err := NewFailureCauseChain(nil)
|
|
if err != nil {
|
|
t.Fatalf("NewFailureCauseChain(nil): %v", err)
|
|
}
|
|
if empty.Len() != 0 {
|
|
t.Errorf("empty chain Len() = %d, want 0", empty.Len())
|
|
}
|
|
if empty.All() != nil {
|
|
t.Errorf("empty chain All() = %v, want nil", empty.All())
|
|
}
|
|
|
|
// --- Chain with 4 causes: all retained, distinct codes ---
|
|
codes4 := []string{"code0", "code1", "code2", "code3"}
|
|
causes4 := make([]FailureCause, MaxFailureCauses)
|
|
for i, code := range codes4 {
|
|
c, err := NewFailureCause("stage", code, "", "", "")
|
|
if err != nil {
|
|
t.Fatalf("NewFailureCause: %v", err)
|
|
}
|
|
causes4[i] = c
|
|
}
|
|
chain4, err := NewFailureCauseChain(causes4)
|
|
if err != nil {
|
|
t.Fatalf("NewFailureCauseChain(4): %v", err)
|
|
}
|
|
if chain4.Len() != 4 {
|
|
t.Errorf("4-cause chain Len() = %d, want 4", chain4.Len())
|
|
}
|
|
// Verify order and distinct codes.
|
|
for i := 0; i < 4; i++ {
|
|
c, err := chain4.At(i)
|
|
if err != nil {
|
|
t.Fatalf("At(%d): %v", i, err)
|
|
}
|
|
if c.Code() != codes4[i] {
|
|
t.Errorf("At(%d) Code() = %q, want %q", i, c.Code(), codes4[i])
|
|
}
|
|
}
|
|
|
|
// --- Chain with 5 causes: oldest dropped, latest 4 retained in order ---
|
|
codes5 := []string{"code0", "code1", "code2", "code3", "code4"}
|
|
causes5 := make([]FailureCause, MaxFailureCauses+1)
|
|
for i, code := range codes5 {
|
|
c, err := NewFailureCause("stage", code, "", "", "")
|
|
if err != nil {
|
|
t.Fatalf("NewFailureCause: %v", err)
|
|
}
|
|
causes5[i] = c
|
|
}
|
|
chain5, err := NewFailureCauseChain(causes5)
|
|
if err != nil {
|
|
t.Fatalf("NewFailureCauseChain(5): %v", err)
|
|
}
|
|
if chain5.Len() != 4 {
|
|
t.Errorf("5-cause chain Len() = %d, want 4", chain5.Len())
|
|
}
|
|
// Latest 4 are code1..code4 in order.
|
|
for i := 0; i < 4; i++ {
|
|
c, err := chain5.At(i)
|
|
if err != nil {
|
|
t.Fatalf("At(%d): %v", i, err)
|
|
}
|
|
want := codes5[i+1]
|
|
if c.Code() != want {
|
|
t.Errorf("At(%d) Code() = %q, want %q (retention order wrong)", i, c.Code(), want)
|
|
}
|
|
}
|
|
|
|
// --- Constructor input mutation does not affect stored chain ---
|
|
t.Run("constructor_input_mutation_isolation", func(t *testing.T) {
|
|
input := make([]FailureCause, MaxFailureCauses+1)
|
|
for i, code := range codes5 {
|
|
c, err := NewFailureCause("stage", code, "", "", "")
|
|
if err != nil {
|
|
t.Fatalf("NewFailureCause: %v", err)
|
|
}
|
|
input[i] = c
|
|
}
|
|
chain, err := NewFailureCauseChain(input)
|
|
if err != nil {
|
|
t.Fatalf("NewFailureCauseChain: %v", err)
|
|
}
|
|
// Replace the element with a different cause.
|
|
mutated, err := NewFailureCause("stage", "mutated", "", "", "")
|
|
if err != nil {
|
|
t.Fatalf("NewFailureCause: %v", err)
|
|
}
|
|
input[0] = mutated
|
|
input[1] = mutated
|
|
input = append(input, mutated)
|
|
|
|
// Stored chain should still be code1..code4.
|
|
if chain.Len() != 4 {
|
|
t.Errorf("chain Len() = %d, want 4 (input mutation leaked)", chain.Len())
|
|
}
|
|
for i := 0; i < 4; i++ {
|
|
c, err := chain.At(i)
|
|
if err != nil {
|
|
t.Fatalf("At(%d): %v", i, err)
|
|
}
|
|
want := codes5[i+1]
|
|
if c.Code() != want {
|
|
t.Errorf("At(%d) Code() = %q, want %q (input mutation leaked)", i, c.Code(), want)
|
|
}
|
|
}
|
|
})
|
|
|
|
// --- Append to 4-cause chain: oldest dropped, latest 4 retained in order ---
|
|
appendCause, err := NewFailureCause("stage", "code4", "", "", "")
|
|
if err != nil {
|
|
t.Fatalf("NewFailureCause: %v", err)
|
|
}
|
|
appended, err := chain4.Append(appendCause)
|
|
if err != nil {
|
|
t.Fatalf("Append: %v", err)
|
|
}
|
|
if appended.Len() != 4 {
|
|
t.Errorf("appended chain Len() = %d, want 4", appended.Len())
|
|
}
|
|
// Latest 4 are code1..code4 in order.
|
|
for i := 0; i < 4; i++ {
|
|
c, err := appended.At(i)
|
|
if err != nil {
|
|
t.Fatalf("At(%d): %v", i, err)
|
|
}
|
|
want := codes5[i+1]
|
|
if c.Code() != want {
|
|
t.Errorf("At(%d) Code() = %q, want %q (Append retention order wrong)", i, c.Code(), want)
|
|
}
|
|
}
|
|
|
|
// --- Append returned chain mutation does not affect original ---
|
|
t.Run("append_returned_chain_mutation_isolation", func(t *testing.T) {
|
|
appendCause2, err := NewFailureCause("stage", "code4", "", "", "")
|
|
if err != nil {
|
|
t.Fatalf("NewFailureCause: %v", err)
|
|
}
|
|
appended2, err := chain4.Append(appendCause2)
|
|
if err != nil {
|
|
t.Fatalf("Append: %v", err)
|
|
}
|
|
// Replace retained elements in the returned chain directly.
|
|
mutated, err := NewFailureCause("stage", "mutated", "", "", "")
|
|
if err != nil {
|
|
t.Fatalf("NewFailureCause: %v", err)
|
|
}
|
|
appended2.causes[0] = mutated
|
|
appended2.causes = append(appended2.causes, mutated)
|
|
|
|
// Original chain4 should be unchanged.
|
|
if chain4.Len() != 4 {
|
|
t.Errorf("chain4 Len() = %d, want 4 (Append mutation leaked)", chain4.Len())
|
|
}
|
|
for i := 0; i < 4; i++ {
|
|
c, err := chain4.At(i)
|
|
if err != nil {
|
|
t.Fatalf("At(%d): %v", i, err)
|
|
}
|
|
if c.Code() != codes4[i] {
|
|
t.Errorf("At(%d) Code() = %q, want %q (Append mutation leaked)", i, c.Code(), codes4[i])
|
|
}
|
|
}
|
|
})
|
|
|
|
// --- Append to 3-cause chain: 4 retained ---
|
|
chain3, err := NewFailureCauseChain(causes4[:3])
|
|
if err != nil {
|
|
t.Fatalf("NewFailureCauseChain(3): %v", err)
|
|
}
|
|
appended3, err := chain3.Append(appendCause)
|
|
if err != nil {
|
|
t.Fatalf("Append: %v", err)
|
|
}
|
|
if appended3.Len() != 4 {
|
|
t.Errorf("appended 3-cause chain Len() = %d, want 4", appended3.Len())
|
|
}
|
|
|
|
// --- At() returns defensive copy ---
|
|
orig, err := chain4.At(0)
|
|
if err != nil {
|
|
t.Fatalf("At(0): %v", err)
|
|
}
|
|
if orig.Stage() != "stage" {
|
|
t.Errorf("At(0) Stage() = %q, want %q", orig.Stage(), "stage")
|
|
}
|
|
|
|
// --- At() out of range returns error ---
|
|
if _, err := chain4.At(-1); err == nil {
|
|
t.Error("At(-1) should return error")
|
|
}
|
|
if _, err := chain4.At(4); err == nil {
|
|
t.Error("At(4) should return error")
|
|
}
|
|
|
|
// --- All() returns defensive copy ---
|
|
all := chain4.All()
|
|
if len(all) != 4 {
|
|
t.Errorf("All() length = %d, want 4", len(all))
|
|
}
|
|
all = append(all, orig)
|
|
if chain4.Len() != 4 {
|
|
t.Errorf("chain4 Len() after All() mutation = %d, want 4", chain4.Len())
|
|
}
|
|
|
|
// --- All() element replacement does not affect stored ---
|
|
all2 := chain4.All()
|
|
mutated, err := NewFailureCause("stage", "mutated", "", "", "")
|
|
if err != nil {
|
|
t.Fatalf("NewFailureCause: %v", err)
|
|
}
|
|
all2[0] = mutated
|
|
c, err := chain4.At(0)
|
|
if err != nil {
|
|
t.Fatalf("At(0): %v", err)
|
|
}
|
|
if c.Code() != codes4[0] {
|
|
t.Errorf("chain4 At(0) Code() = %q, want %q (All mutation leaked)", c.Code(), codes4[0])
|
|
}
|
|
|
|
// --- Copy() returns defensive copy ---
|
|
cp := chain4.Copy()
|
|
if cp.Len() != 4 {
|
|
t.Errorf("Copy() Len() = %d, want 4", cp.Len())
|
|
}
|
|
cpCauses := cp.All()
|
|
cpCauses = append(cpCauses, orig)
|
|
if chain4.Len() != 4 {
|
|
t.Errorf("chain4 Len() after Copy() mutation = %d, want 4", chain4.Len())
|
|
}
|
|
|
|
// --- Copy() element replacement does not affect original ---
|
|
cp2 := chain4.Copy()
|
|
cp2.causes[0] = mutated
|
|
c, err = chain4.At(0)
|
|
if err != nil {
|
|
t.Fatalf("At(0): %v", err)
|
|
}
|
|
if c.Code() != codes4[0] {
|
|
t.Errorf("chain4 At(0) Code() = %q, want %q (Copy mutation leaked)", c.Code(), codes4[0])
|
|
}
|
|
|
|
// --- Invalid cause in chain is rejected by constructor ---
|
|
invalidCause := FailureCause{
|
|
stage: StableToken{value: "INVALID"},
|
|
code: mustToken("code"),
|
|
}
|
|
if _, err := NewFailureCauseChain([]FailureCause{invalidCause}); err == nil {
|
|
t.Error("NewFailureCauseChain with invalid cause should return error")
|
|
}
|
|
|
|
// --- Append rejects invalid cause ---
|
|
if _, err := chain4.Append(invalidCause); err == nil {
|
|
t.Error("Append with invalid cause should return error")
|
|
}
|
|
|
|
// --- Invalid token table ---
|
|
t.Run("invalid_stage_empty", func(t *testing.T) {
|
|
_, err := NewFailureCause("", "code", "", "", "")
|
|
if err == nil {
|
|
t.Fatal("empty stage should be rejected")
|
|
}
|
|
})
|
|
t.Run("invalid_stage_uppercase", func(t *testing.T) {
|
|
_, err := NewFailureCause("STAGE", "code", "", "", "")
|
|
if err == nil {
|
|
t.Fatal("uppercase stage should be rejected")
|
|
}
|
|
})
|
|
t.Run("invalid_code_empty", func(t *testing.T) {
|
|
_, err := NewFailureCause("stage", "", "", "", "")
|
|
if err == nil {
|
|
t.Fatal("empty code should be rejected")
|
|
}
|
|
})
|
|
t.Run("invalid_code_uppercase", func(t *testing.T) {
|
|
_, err := NewFailureCause("stage", "CODE", "", "", "")
|
|
if err == nil {
|
|
t.Fatal("uppercase code should be rejected")
|
|
}
|
|
})
|
|
t.Run("invalid_consumer_uppercase", func(t *testing.T) {
|
|
_, err := NewFailureCause("stage", "code", "CONSUMER", "", "")
|
|
if err == nil {
|
|
t.Fatal("uppercase consumer should be rejected")
|
|
}
|
|
})
|
|
t.Run("invalid_filter_uppercase", func(t *testing.T) {
|
|
_, err := NewFailureCause("stage", "code", "", "FILTER", "")
|
|
if err == nil {
|
|
t.Fatal("uppercase filter should be rejected")
|
|
}
|
|
})
|
|
t.Run("invalid_rule_id_uppercase", func(t *testing.T) {
|
|
_, err := NewFailureCause("stage", "code", "", "", "RULE")
|
|
if err == nil {
|
|
t.Fatal("uppercase rule id should be rejected")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestTerminalResultValidation verifies that TerminalResult enforces the
|
|
// success/error mutual exclusion, required channel/timestamp, required
|
|
// external descriptor for error results, cause cap, and that accessors return
|
|
// defensive copies.
|
|
func TestTerminalResultValidation(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)
|
|
}
|
|
|
|
// --- Success terminal: valid ---
|
|
success, err := NewSuccessTerminalResult("ch", ts)
|
|
if err != nil {
|
|
t.Fatalf("NewSuccessTerminalResult: %v", err)
|
|
}
|
|
if !success.Success() {
|
|
t.Error("success terminal should be success")
|
|
}
|
|
if success.Error() {
|
|
t.Error("success terminal should not be error")
|
|
}
|
|
if success.ExternalDesc() != nil {
|
|
t.Error("success terminal should not have external descriptor")
|
|
}
|
|
if success.FailureCauses().Len() != 0 {
|
|
t.Errorf("success terminal FailureCauses().Len() = %d, want 0", success.FailureCauses().Len())
|
|
}
|
|
if err := success.Validate(); err != nil {
|
|
t.Errorf("success terminal Validate() = %v, want nil", err)
|
|
}
|
|
|
|
// --- Error terminal with descriptor: valid ---
|
|
errorResult, err := NewErrorTerminalResult("ch", desc, FailureCauseChain{}, ts)
|
|
if err != nil {
|
|
t.Fatalf("NewErrorTerminalResult: %v", err)
|
|
}
|
|
if errorResult.Success() {
|
|
t.Error("error terminal should not be success")
|
|
}
|
|
if !errorResult.Error() {
|
|
t.Error("error terminal should be error")
|
|
}
|
|
if errorResult.ExternalDesc() == nil {
|
|
t.Error("error terminal should have external descriptor")
|
|
}
|
|
if err := errorResult.Validate(); err != nil {
|
|
t.Errorf("error terminal Validate() = %v, want nil", err)
|
|
}
|
|
|
|
// --- Error terminal with descriptor + 4 causes: valid ---
|
|
codes4 := []string{"code0", "code1", "code2", "code3"}
|
|
causes4 := make([]FailureCause, MaxFailureCauses)
|
|
for i, code := range codes4 {
|
|
c, err := NewFailureCause("stage", code, "", "", "")
|
|
if err != nil {
|
|
t.Fatalf("NewFailureCause: %v", err)
|
|
}
|
|
causes4[i] = c
|
|
}
|
|
chain4, err := NewFailureCauseChain(causes4)
|
|
if err != nil {
|
|
t.Fatalf("NewFailureCauseChain: %v", err)
|
|
}
|
|
errorWithCauses, err := NewErrorTerminalResult("ch", desc, chain4, ts)
|
|
if err != nil {
|
|
t.Fatalf("NewErrorTerminalResult with causes: %v", err)
|
|
}
|
|
if err := errorWithCauses.Validate(); err != nil {
|
|
t.Errorf("error terminal with 4 causes Validate() = %v, want nil", err)
|
|
}
|
|
|
|
// --- Error terminal without descriptor: rejected ---
|
|
if _, err := NewErrorTerminalResult("ch", ExternalDescriptor{}, FailureCauseChain{}, ts); err == nil {
|
|
t.Fatal("error terminal without descriptor should return error")
|
|
}
|
|
|
|
// --- Error terminal with nil descriptor: rejected ---
|
|
badErr := TerminalResult{
|
|
channel: "ch",
|
|
err: true,
|
|
occurredAt: ts,
|
|
}
|
|
if err := badErr.Validate(); err == nil {
|
|
t.Fatal("error terminal with nil descriptor should fail Validate")
|
|
}
|
|
|
|
// --- Success terminal with descriptor: rejected ---
|
|
badSuccess := TerminalResult{
|
|
channel: "ch",
|
|
success: true,
|
|
externalDesc: &desc,
|
|
occurredAt: ts,
|
|
}
|
|
if err := badSuccess.Validate(); err == nil {
|
|
t.Fatal("success terminal with descriptor should fail Validate")
|
|
}
|
|
|
|
// --- Success terminal with causes: rejected ---
|
|
badSuccessCauses := TerminalResult{
|
|
channel: "ch",
|
|
success: true,
|
|
failureCauses: chain4,
|
|
occurredAt: ts,
|
|
}
|
|
if err := badSuccessCauses.Validate(); err == nil {
|
|
t.Fatal("success terminal with causes should fail Validate")
|
|
}
|
|
|
|
// --- Neither success nor error: rejected ---
|
|
neither := TerminalResult{
|
|
channel: "ch",
|
|
occurredAt: ts,
|
|
}
|
|
if err := neither.Validate(); err == nil {
|
|
t.Fatal("terminal with neither success nor error should fail Validate")
|
|
}
|
|
|
|
// --- Both success and error: rejected ---
|
|
both := TerminalResult{
|
|
channel: "ch",
|
|
success: true,
|
|
err: true,
|
|
externalDesc: &desc,
|
|
occurredAt: ts,
|
|
}
|
|
if err := both.Validate(); err == nil {
|
|
t.Fatal("terminal with both success and error should fail Validate")
|
|
}
|
|
|
|
// --- Empty channel: rejected ---
|
|
if _, err := NewSuccessTerminalResult("", ts); err == nil {
|
|
t.Fatal("success terminal with empty channel should return error")
|
|
}
|
|
if _, err := NewErrorTerminalResult("", desc, FailureCauseChain{}, ts); err == nil {
|
|
t.Fatal("error terminal with empty channel should return error")
|
|
}
|
|
|
|
// --- Zero timestamp: rejected ---
|
|
if _, err := NewSuccessTerminalResult("ch", time.Time{}); err == nil {
|
|
t.Fatal("success terminal with zero timestamp should return error")
|
|
}
|
|
if _, err := NewErrorTerminalResult("ch", desc, FailureCauseChain{}, time.Time{}); err == nil {
|
|
t.Fatal("error terminal with zero timestamp should return error")
|
|
}
|
|
|
|
// --- Error with 5 causes: rejected ---
|
|
causes5 := make([]FailureCause, MaxFailureCauses+1)
|
|
for i := range causes5 {
|
|
c, err := NewFailureCause("stage", "code", "", "", "")
|
|
if err != nil {
|
|
t.Fatalf("NewFailureCause: %v", err)
|
|
}
|
|
causes5[i] = c
|
|
}
|
|
chain5Raw := FailureCauseChain{causes: causes5}
|
|
badTr := TerminalResult{
|
|
channel: "ch",
|
|
err: true,
|
|
externalDesc: &desc,
|
|
failureCauses: chain5Raw,
|
|
occurredAt: ts,
|
|
}
|
|
if err := badTr.Validate(); err == nil {
|
|
t.Fatal("error terminal with 5 causes should fail Validate")
|
|
}
|
|
|
|
// --- ExternalDesc accessor returns defensive copy ---
|
|
ed := errorResult.ExternalDesc()
|
|
if ed == nil {
|
|
t.Fatal("ExternalDesc() returned nil")
|
|
}
|
|
// Mutate the returned descriptor.
|
|
*ed = ExternalDescriptor{}
|
|
// The stored descriptor should be unchanged.
|
|
ed2 := errorResult.ExternalDesc()
|
|
if ed2 == nil {
|
|
t.Fatal("ExternalDesc() returned nil after mutation")
|
|
}
|
|
if ed2.Type() != "error.type" {
|
|
t.Errorf("ExternalDesc() Type() = %q, want %q (mutation leaked)", ed2.Type(), "error.type")
|
|
}
|
|
|
|
// --- FailureCauses accessor returns defensive copy ---
|
|
fc := errorWithCauses.FailureCauses()
|
|
if fc.Len() != 4 {
|
|
t.Errorf("FailureCauses().Len() = %d, want 4", fc.Len())
|
|
}
|
|
fcCauses := fc.All()
|
|
fcCauses = append(fcCauses, fcCauses[0])
|
|
if errorWithCauses.FailureCauses().Len() != 4 {
|
|
t.Error("FailureCauses() mutated (accessor mutation leaked)")
|
|
}
|
|
|
|
// --- FailureCauses accessor element mutation does not affect stored ---
|
|
fc2 := errorWithCauses.FailureCauses()
|
|
mutated, err := NewFailureCause("stage", "mutated", "", "", "")
|
|
if err != nil {
|
|
t.Fatalf("NewFailureCause: %v", err)
|
|
}
|
|
fc2.causes[0] = mutated
|
|
fc3 := errorWithCauses.FailureCauses()
|
|
if fc3.Len() != 4 {
|
|
t.Errorf("FailureCauses().Len() after mutation = %d, want 4", fc3.Len())
|
|
}
|
|
c, err := fc3.At(0)
|
|
if err != nil {
|
|
t.Fatalf("At(0): %v", err)
|
|
}
|
|
if c.Code() != codes4[0] {
|
|
t.Errorf("FailureCauses().At(0) Code() = %q, want %q (accessor mutation leaked)", c.Code(), codes4[0])
|
|
}
|
|
|
|
// --- FailureCause invalid token table ---
|
|
t.Run("invalid_stage_empty", func(t *testing.T) {
|
|
_, err := NewFailureCause("", "code", "", "", "")
|
|
if err == nil {
|
|
t.Fatal("empty stage should be rejected")
|
|
}
|
|
})
|
|
t.Run("invalid_stage_uppercase", func(t *testing.T) {
|
|
_, err := NewFailureCause("STAGE", "code", "", "", "")
|
|
if err == nil {
|
|
t.Fatal("uppercase stage should be rejected")
|
|
}
|
|
})
|
|
t.Run("invalid_code_empty", func(t *testing.T) {
|
|
_, err := NewFailureCause("stage", "", "", "", "")
|
|
if err == nil {
|
|
t.Fatal("empty code should be rejected")
|
|
}
|
|
})
|
|
t.Run("invalid_code_uppercase", func(t *testing.T) {
|
|
_, err := NewFailureCause("stage", "CODE", "", "", "")
|
|
if err == nil {
|
|
t.Fatal("uppercase code should be rejected")
|
|
}
|
|
})
|
|
t.Run("invalid_consumer_uppercase", func(t *testing.T) {
|
|
_, err := NewFailureCause("stage", "code", "CONSUMER", "", "")
|
|
if err == nil {
|
|
t.Fatal("uppercase consumer should be rejected")
|
|
}
|
|
})
|
|
t.Run("invalid_filter_uppercase", func(t *testing.T) {
|
|
_, err := NewFailureCause("stage", "code", "", "FILTER", "")
|
|
if err == nil {
|
|
t.Fatal("uppercase filter should be rejected")
|
|
}
|
|
})
|
|
t.Run("invalid_rule_id_uppercase", func(t *testing.T) {
|
|
_, err := NewFailureCause("stage", "code", "", "", "RULE")
|
|
if err == nil {
|
|
t.Fatal("uppercase rule id should be rejected")
|
|
}
|
|
})
|
|
}
|
|
|
|
// fakeReleaseSink is a test-only implementation of ReleaseSink that records
|
|
// all committed payloads. It is used solely for the compile-time interface
|
|
// assertion; single-terminal behavior is verified in the host fixture.
|
|
type fakeReleaseSink struct {
|
|
responseStarts []ResponseStart
|
|
releases []ReleaseEvent
|
|
terminals []TerminalResult
|
|
}
|
|
|
|
// Compile-time assertion that fakeReleaseSink implements ReleaseSink.
|
|
var _ ReleaseSink = (*fakeReleaseSink)(nil)
|
|
|
|
func (f *fakeReleaseSink) CommitResponseStart(ctx context.Context, rs ResponseStart) (CommitState, error) {
|
|
f.responseStarts = append(f.responseStarts, rs)
|
|
return CommitStateStreamOpen, nil
|
|
}
|
|
|
|
func (f *fakeReleaseSink) Release(ctx context.Context, ev ReleaseEvent) (CommitState, error) {
|
|
f.releases = append(f.releases, ev)
|
|
return CommitStateStreamOpen, nil
|
|
}
|
|
|
|
func (f *fakeReleaseSink) CommitTerminal(ctx context.Context, tr TerminalResult) (CommitState, error) {
|
|
f.terminals = append(f.terminals, tr)
|
|
return CommitStateTerminalCommitted, nil
|
|
}
|
|
|
|
// TestReleaseSinkContractCompiles verifies that the ReleaseSink interface
|
|
// contract is satisfied by a test-only fake sink via a compile-time assertion,
|
|
// and that all three methods can be invoked through the interface.
|
|
func TestReleaseSinkContractCompiles(t *testing.T) {
|
|
ctx := context.Background()
|
|
sink := &fakeReleaseSink{}
|
|
|
|
// Verify the interface is usable through the interface type.
|
|
var iface ReleaseSink = sink
|
|
|
|
// CommitResponseStart
|
|
rs, err := NewResponseStart("ch", 200, map[string]string{"x-custom": "val"}, time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC))
|
|
if err != nil {
|
|
t.Fatalf("NewResponseStart: %v", err)
|
|
}
|
|
state, err := iface.CommitResponseStart(ctx, rs)
|
|
if err != nil {
|
|
t.Fatalf("CommitResponseStart: %v", err)
|
|
}
|
|
if state != CommitStateStreamOpen {
|
|
t.Errorf("CommitResponseStart state = %q, want %q", state, CommitStateStreamOpen)
|
|
}
|
|
if len(sink.responseStarts) != 1 {
|
|
t.Errorf("responseStarts length = %d, want 1", len(sink.responseStarts))
|
|
}
|
|
|
|
// Release
|
|
releaseEv, err := NewReleaseTextDeltaEvent("ch", "hello", time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC))
|
|
if err != nil {
|
|
t.Fatalf("NewReleaseTextDeltaEvent: %v", err)
|
|
}
|
|
state, err = iface.Release(ctx, releaseEv)
|
|
if err != nil {
|
|
t.Fatalf("Release: %v", err)
|
|
}
|
|
if state != CommitStateStreamOpen {
|
|
t.Errorf("Release state = %q, want %q", state, CommitStateStreamOpen)
|
|
}
|
|
if len(sink.releases) != 1 {
|
|
t.Errorf("releases length = %d, want 1", len(sink.releases))
|
|
}
|
|
|
|
// CommitTerminal
|
|
terminal, err := NewSuccessTerminalResult("ch", time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC))
|
|
if err != nil {
|
|
t.Fatalf("NewSuccessTerminalResult: %v", err)
|
|
}
|
|
state, err = iface.CommitTerminal(ctx, terminal)
|
|
if err != nil {
|
|
t.Fatalf("CommitTerminal: %v", err)
|
|
}
|
|
if state != CommitStateTerminalCommitted {
|
|
t.Errorf("CommitTerminal state = %q, want %q", state, CommitStateTerminalCommitted)
|
|
}
|
|
if len(sink.terminals) != 1 {
|
|
t.Errorf("terminals length = %d, want 1", len(sink.terminals))
|
|
}
|
|
}
|