Fix 6 Postgres-specific test failures in workflow store
Some checks failed
CI/CD / detect-changes (pull_request) Successful in 5s
CI/CD / test-frontend (pull_request) Successful in 6s
CI/CD / test-go-pg (pull_request) Failing after 2m24s
CI/CD / test-sqlite (pull_request) Successful in 2m45s
CI/CD / build-and-deploy (pull_request) Has been skipped
Some checks failed
CI/CD / detect-changes (pull_request) Successful in 5s
CI/CD / test-frontend (pull_request) Successful in 6s
CI/CD / test-go-pg (pull_request) Failing after 2m24s
CI/CD / test-sqlite (pull_request) Successful in 2m45s
CI/CD / build-and-deploy (pull_request) Has been skipped
Root causes:
- PG store CreateInstance/CreateAssignment set defaults in local vars
but didn't write them back to the struct, so callers saw empty
Status fields (violating CHECK constraints on subsequent updates)
- PG JSONB normalizes whitespace ({"key": "val"} vs {"key":"val"})
but tests compared exact strings
- ListSignoffs test used "nonexistent" as instance_id but PG
validates UUID format
Fixes:
- Write defaults directly to inst.Status / a.Status in PG store
(aligns with SQLite store which already did this)
- Add jsonEq() helper using json.Compact for whitespace-agnostic
JSON comparison across all stage_data/review_data assertions
- Use valid zero-UUID for non-existent instance in signoff test
All 35 tests pass on SQLite (28 store + 7 engine).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -13,6 +14,19 @@ import (
|
|||||||
"switchboard-core/store/sqlite"
|
"switchboard-core/store/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// jsonEq compares two JSON byte slices ignoring whitespace differences
|
||||||
|
// (PG JSONB normalizes spacing, SQLite preserves it verbatim).
|
||||||
|
func jsonEq(a, b json.RawMessage) bool {
|
||||||
|
var ca, cb bytes.Buffer
|
||||||
|
if err := json.Compact(&ca, a); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if err := json.Compact(&cb, b); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return ca.String() == cb.String()
|
||||||
|
}
|
||||||
|
|
||||||
// testStores returns an appropriate Stores for the current dialect.
|
// testStores returns an appropriate Stores for the current dialect.
|
||||||
func testStores(t *testing.T) store.Stores {
|
func testStores(t *testing.T) store.Stores {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
@@ -128,7 +142,7 @@ func TestWorkflowInstance_CreateAndGet(t *testing.T) {
|
|||||||
if got.EntryToken == nil || *got.EntryToken != token {
|
if got.EntryToken == nil || *got.EntryToken != token {
|
||||||
t.Errorf("entry_token = %v, want %q", got.EntryToken, token)
|
t.Errorf("entry_token = %v, want %q", got.EntryToken, token)
|
||||||
}
|
}
|
||||||
if string(got.StageData) != `{"key":"val"}` {
|
if !jsonEq(got.StageData, json.RawMessage(`{"key":"val"}`)) {
|
||||||
t.Errorf("stage_data = %s, want {\"key\":\"val\"}", got.StageData)
|
t.Errorf("stage_data = %s, want {\"key\":\"val\"}", got.StageData)
|
||||||
}
|
}
|
||||||
if got.CreatedAt.IsZero() {
|
if got.CreatedAt.IsZero() {
|
||||||
@@ -200,8 +214,8 @@ func TestWorkflowInstance_Update(t *testing.T) {
|
|||||||
if got.CurrentStage != stage2 {
|
if got.CurrentStage != stage2 {
|
||||||
t.Errorf("current_stage = %q, want %q", got.CurrentStage, stage2)
|
t.Errorf("current_stage = %q, want %q", got.CurrentStage, stage2)
|
||||||
}
|
}
|
||||||
if string(got.StageData) != `{"updated":true}` {
|
if !jsonEq(got.StageData, json.RawMessage(`{"updated":true}`)) {
|
||||||
t.Errorf("stage_data = %s", got.StageData)
|
t.Errorf("stage_data = %s, want {\"updated\":true}", got.StageData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,8 +310,8 @@ func TestWorkflowInstance_AdvanceStage(t *testing.T) {
|
|||||||
if got.CurrentStage != stage2 {
|
if got.CurrentStage != stage2 {
|
||||||
t.Errorf("current_stage = %q, want %q", got.CurrentStage, stage2)
|
t.Errorf("current_stage = %q, want %q", got.CurrentStage, stage2)
|
||||||
}
|
}
|
||||||
if string(got.StageData) != `{"step":"two"}` {
|
if !jsonEq(got.StageData, json.RawMessage(`{"step":"two"}`)) {
|
||||||
t.Errorf("stage_data = %s", got.StageData)
|
t.Errorf("stage_data = %s, want {\"step\":\"two\"}", got.StageData)
|
||||||
}
|
}
|
||||||
if !got.StageEnteredAt.After(originalEnteredAt) {
|
if !got.StageEnteredAt.After(originalEnteredAt) {
|
||||||
t.Errorf("stage_entered_at should have advanced; original=%v, got=%v",
|
t.Errorf("stage_entered_at should have advanced; original=%v, got=%v",
|
||||||
@@ -543,8 +557,8 @@ func TestWorkflowAssignment_Complete(t *testing.T) {
|
|||||||
if got.Status != models.AssignmentStatusCompleted {
|
if got.Status != models.AssignmentStatusCompleted {
|
||||||
t.Errorf("status = %q, want completed", got.Status)
|
t.Errorf("status = %q, want completed", got.Status)
|
||||||
}
|
}
|
||||||
if string(got.ReviewData) != `{"approved":true,"notes":"LGTM"}` {
|
if !jsonEq(got.ReviewData, json.RawMessage(`{"approved":true,"notes":"LGTM"}`)) {
|
||||||
t.Errorf("review_data = %s", got.ReviewData)
|
t.Errorf("review_data = %s, want {\"approved\":true,\"notes\":\"LGTM\"}", got.ReviewData)
|
||||||
}
|
}
|
||||||
if got.CompletedAt == nil {
|
if got.CompletedAt == nil {
|
||||||
t.Error("completed_at should be set")
|
t.Error("completed_at should be set")
|
||||||
@@ -837,7 +851,7 @@ func TestWorkflowSignoff_ListEmpty(t *testing.T) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// ListSignoffs on non-existent instance should return empty, not error
|
// ListSignoffs on non-existent instance should return empty, not error
|
||||||
list, err := s.Workflows.ListSignoffs(ctx, "nonexistent", "stage")
|
list, err := s.Workflows.ListSignoffs(ctx, "00000000-0000-0000-0000-000000000000", "stage")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("list signoffs error: %v", err)
|
t.Fatalf("list signoffs error: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -406,9 +406,8 @@ func nullIfEmpty(s string) interface{} {
|
|||||||
func (s *WorkflowStore) CreateInstance(ctx context.Context, inst *models.WorkflowInstance) error {
|
func (s *WorkflowStore) CreateInstance(ctx context.Context, inst *models.WorkflowInstance) error {
|
||||||
stageData := jsonOrEmpty(inst.StageData)
|
stageData := jsonOrEmpty(inst.StageData)
|
||||||
metadata := jsonOrEmpty(inst.Metadata)
|
metadata := jsonOrEmpty(inst.Metadata)
|
||||||
status := inst.Status
|
if inst.Status == "" {
|
||||||
if status == "" {
|
inst.Status = models.InstanceStatusActive
|
||||||
status = models.InstanceStatusActive
|
|
||||||
}
|
}
|
||||||
return DB.QueryRowContext(ctx, `
|
return DB.QueryRowContext(ctx, `
|
||||||
INSERT INTO workflow_instances (workflow_id, workflow_version, current_stage,
|
INSERT INTO workflow_instances (workflow_id, workflow_version, current_stage,
|
||||||
@@ -416,7 +415,7 @@ func (s *WorkflowStore) CreateInstance(ctx context.Context, inst *models.Workflo
|
|||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
RETURNING id, stage_entered_at, created_at, updated_at`,
|
RETURNING id, stage_entered_at, created_at, updated_at`,
|
||||||
inst.WorkflowID, inst.WorkflowVersion, inst.CurrentStage,
|
inst.WorkflowID, inst.WorkflowVersion, inst.CurrentStage,
|
||||||
stageData, status, inst.StartedBy, inst.EntryToken, metadata,
|
stageData, inst.Status, inst.StartedBy, inst.EntryToken, metadata,
|
||||||
).Scan(&inst.ID, &inst.StageEnteredAt, &inst.CreatedAt, &inst.UpdatedAt)
|
).Scan(&inst.ID, &inst.StageEnteredAt, &inst.CreatedAt, &inst.UpdatedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,15 +574,14 @@ func (s *WorkflowStore) ListActiveInstances(ctx context.Context) ([]models.Workf
|
|||||||
|
|
||||||
func (s *WorkflowStore) CreateAssignment(ctx context.Context, a *models.WorkflowAssignment) error {
|
func (s *WorkflowStore) CreateAssignment(ctx context.Context, a *models.WorkflowAssignment) error {
|
||||||
reviewData := jsonOrEmpty(a.ReviewData)
|
reviewData := jsonOrEmpty(a.ReviewData)
|
||||||
status := a.Status
|
if a.Status == "" {
|
||||||
if status == "" {
|
a.Status = models.AssignmentStatusUnassigned
|
||||||
status = models.AssignmentStatusUnassigned
|
|
||||||
}
|
}
|
||||||
return DB.QueryRowContext(ctx, `
|
return DB.QueryRowContext(ctx, `
|
||||||
INSERT INTO workflow_assignments (instance_id, stage, team_id, assigned_to, status, review_data)
|
INSERT INTO workflow_assignments (instance_id, stage, team_id, assigned_to, status, review_data)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6)
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
RETURNING id, created_at`,
|
RETURNING id, created_at`,
|
||||||
a.InstanceID, a.Stage, a.TeamID, a.AssignedTo, status, reviewData,
|
a.InstanceID, a.Stage, a.TeamID, a.AssignedTo, a.Status, reviewData,
|
||||||
).Scan(&a.ID, &a.CreatedAt)
|
).Scan(&a.ID, &a.CreatedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user