test(coordinator): unit + integration coverage across every layer
- domain: NewJobWithTasks, DeriveStatus, NewUploadedJob, NewShardTask, NewWorker, NewArtifact/SetContent (domain 47% -> 88%). - internal/memstore: in-memory implementations of every usecase port, so orchestration can be tested without Postgres or a filesystem. - usecase: claim/renew/complete/fail/create/register/upload/submit-dataset flows over memstore, including rule-10 cross-task rejection, idempotent replay, lease sweep-on-claim, and dataset chunking (usecase 0% -> 73%). - transport: httptest end-to-end over real use cases + memstore — auth, readiness, full lifecycle, multipart upload + shard input, error mappings (0% -> 70%). - postgres integration: fix the tests broken by the artifact_id switch and add worker-repo, artifact-repo, and shard-task (nullable input_uri) round-trips. go test -race ./... is clean; golangci-lint (incl. integration tag) reports 0.
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func TestNewArtifact(t *testing.T) {
|
||||
jobID := uuid.New()
|
||||
taskID := uuid.New()
|
||||
a, err := NewArtifact(jobID, &taskID, ArtifactPartialResult, "result.csv", "text/csv", testNow)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if a.JobID != jobID || a.TaskID == nil || *a.TaskID != taskID {
|
||||
t.Error("ownership not recorded")
|
||||
}
|
||||
// Storage key is derived from the artifact id, never the filename — no path
|
||||
// traversal from a hostile "../.." name.
|
||||
if a.StorageKey != a.ID.String() {
|
||||
t.Errorf("storage key = %q, want the artifact id", a.StorageKey)
|
||||
}
|
||||
if a.SizeBytes != 0 || a.SHA256 != "" {
|
||||
t.Error("size and checksum are unknown until SetContent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewArtifactDefaultsContentType(t *testing.T) {
|
||||
a, err := NewArtifact(uuid.New(), nil, ArtifactInput, "data", "", testNow)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if a.ContentType != "application/octet-stream" {
|
||||
t.Errorf("content type = %q, want the default", a.ContentType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewArtifactRejectsBadInput(t *testing.T) {
|
||||
if _, err := NewArtifact(uuid.New(), nil, ArtifactInput, "", "text/csv", testNow); !errors.Is(err, ErrInvalidInput) {
|
||||
t.Errorf("empty filename: err = %v, want ErrInvalidInput", err)
|
||||
}
|
||||
if _, err := NewArtifact(uuid.New(), nil, "", "f", "text/csv", testNow); !errors.Is(err, ErrInvalidInput) {
|
||||
t.Errorf("empty kind: err = %v, want ErrInvalidInput", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArtifactSetContent(t *testing.T) {
|
||||
a, _ := NewArtifact(uuid.New(), nil, ArtifactShard, "shard-0.tsv", "text/csv", testNow)
|
||||
a.SetContent("deadbeef", 42)
|
||||
if a.SHA256 != "deadbeef" || a.SizeBytes != 42 {
|
||||
t.Error("SetContent must record checksum and size")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func TestNewJobWithTasksBuildsBoth(t *testing.T) {
|
||||
job, tasks, err := NewJobWithTasks("similarity_search", "s3://in", nil, []ChunkSpec{
|
||||
{ChunkIndex: 0, InputURI: "s3://c0", InputSHA256: "a"},
|
||||
{ChunkIndex: 1, InputURI: "s3://c1", InputSHA256: "b"},
|
||||
}, testNow)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(tasks) != 2 {
|
||||
t.Fatalf("got %d tasks, want 2", len(tasks))
|
||||
}
|
||||
for _, tk := range tasks {
|
||||
if tk.JobID != job.ID {
|
||||
t.Error("task not linked to job")
|
||||
}
|
||||
if tk.Workload != "similarity_search" {
|
||||
t.Error("task should inherit the job workload")
|
||||
}
|
||||
}
|
||||
if job.Status != JobPending {
|
||||
t.Errorf("status = %q, want pending", job.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewJobWithTasksRejectsBadInput(t *testing.T) {
|
||||
good := []ChunkSpec{{ChunkIndex: 0, InputURI: "s3://c0", InputSHA256: "a"}}
|
||||
cases := map[string]struct {
|
||||
workload string
|
||||
inputURI string
|
||||
chunks []ChunkSpec
|
||||
}{
|
||||
"empty workload": {"", "s3://in", good},
|
||||
"empty input": {"w", "", good},
|
||||
"no chunks": {"w", "s3://in", nil},
|
||||
"duplicate index": {"w", "s3://in", []ChunkSpec{
|
||||
{ChunkIndex: 0, InputURI: "a", InputSHA256: "x"},
|
||||
{ChunkIndex: 0, InputURI: "b", InputSHA256: "y"},
|
||||
}},
|
||||
}
|
||||
for name, c := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if _, _, err := NewJobWithTasks(c.workload, c.inputURI, nil, c.chunks, testNow); !errors.Is(err, ErrInvalidInput) {
|
||||
t.Errorf("err = %v, want ErrInvalidInput", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewJobWithTasksInheritsAndOverridesWorkload(t *testing.T) {
|
||||
_, tasks, err := NewJobWithTasks("base", "s3://in", nil, []ChunkSpec{
|
||||
{ChunkIndex: 0, InputURI: "a", InputSHA256: "x"},
|
||||
{ChunkIndex: 1, InputURI: "b", InputSHA256: "y", Workload: "special"},
|
||||
}, testNow)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tasks[0].Workload != "base" || tasks[1].Workload != "special" {
|
||||
t.Errorf("workloads = %q, %q", tasks[0].Workload, tasks[1].Workload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeriveStatus(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
p JobProgress
|
||||
want JobStatus
|
||||
}{
|
||||
{"empty", JobProgress{Total: 0}, JobPending},
|
||||
{"all pending", JobProgress{Total: 3, Pending: 3}, JobPending},
|
||||
{"one leased", JobProgress{Total: 3, Pending: 2, Leased: 1}, JobRunning},
|
||||
{"partly done", JobProgress{Total: 3, Pending: 1, Done: 2}, JobRunning},
|
||||
{"all done", JobProgress{Total: 3, Done: 3}, JobCompleted},
|
||||
{"done and failed", JobProgress{Total: 3, Done: 2, Failed: 1}, JobFailed},
|
||||
{"failed but work remains", JobProgress{Total: 3, Pending: 1, Failed: 2}, JobRunning},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
if got := c.p.DeriveStatus(); got != c.want {
|
||||
t.Errorf("DeriveStatus() = %q, want %q", got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewUploadedJob(t *testing.T) {
|
||||
job, err := NewUploadedJob("w", map[string]any{"k": 1}, testNow)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if job.Status != JobPending || job.InputURI != "" {
|
||||
t.Error("uploaded job should be pending with no input URI")
|
||||
}
|
||||
if _, err := NewUploadedJob("", nil, testNow); !errors.Is(err, ErrInvalidInput) {
|
||||
t.Errorf("empty workload: err = %v, want ErrInvalidInput", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewShardTask(t *testing.T) {
|
||||
art := uuid.New()
|
||||
task, err := NewShardTask(uuid.New(), 2, "w", art, "sha", nil, 0, testNow)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if task.InputArtifactID == nil || *task.InputArtifactID != art {
|
||||
t.Error("shard task must reference its input artifact")
|
||||
}
|
||||
if task.InputURI != "" {
|
||||
t.Error("shard task must not carry a URI")
|
||||
}
|
||||
if task.MaxAttempts != DefaultMaxAttempts {
|
||||
t.Errorf("maxAttempts = %d, want default %d", task.MaxAttempts, DefaultMaxAttempts)
|
||||
}
|
||||
|
||||
bad := []struct {
|
||||
name string
|
||||
art uuid.UUID
|
||||
sha string
|
||||
idx int
|
||||
}{
|
||||
{"nil artifact", uuid.Nil, "sha", 0},
|
||||
{"empty sha", art, "", 0},
|
||||
{"negative index", art, "sha", -1},
|
||||
}
|
||||
for _, c := range bad {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
if _, err := NewShardTask(uuid.New(), c.idx, "w", c.art, c.sha, nil, 0, testNow); !errors.Is(err, ErrInvalidInput) {
|
||||
t.Errorf("err = %v, want ErrInvalidInput", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewWorker(t *testing.T) {
|
||||
w, err := NewWorker("lab-01", []string{"similarity_search"}, testNow)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if w.Status != WorkerOnline {
|
||||
t.Errorf("status = %q, want online", w.Status)
|
||||
}
|
||||
if w.ID.String() == "" {
|
||||
t.Error("worker must get an id")
|
||||
}
|
||||
if !w.LastHeartbeatAt.Equal(testNow) || !w.CreatedAt.Equal(testNow) {
|
||||
t.Error("timestamps must be stamped")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewWorkerRejectsNoCapabilities(t *testing.T) {
|
||||
if _, err := NewWorker("lab-01", nil, testNow); !errors.Is(err, ErrInvalidInput) {
|
||||
t.Errorf("err = %v, want ErrInvalidInput", err)
|
||||
}
|
||||
if _, err := NewWorker("lab-01", []string{}, testNow); !errors.Is(err, ErrInvalidInput) {
|
||||
t.Errorf("empty slice: err = %v, want ErrInvalidInput", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user