Files
kandev/apps/backend/internal/task/repository/document_test.go
T
2026-05-16 20:07:57 +01:00

431 lines
13 KiB
Go

package repository
import (
"context"
"testing"
"time"
"github.com/kandev/kandev/internal/task/models"
v1 "github.com/kandev/kandev/pkg/api/v1"
)
// seedDocTask creates the prerequisite workspace, workflow, and task for document tests.
func seedDocTask(t *testing.T, ctx context.Context, repo interface {
CreateWorkspace(context.Context, *models.Workspace) error
CreateWorkflow(context.Context, *models.Workflow) error
CreateTask(context.Context, *models.Task) error
}, taskID string) {
t.Helper()
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-doc", Name: "Doc WS"})
_ = repo.CreateWorkflow(ctx, &models.Workflow{ID: "wf-doc", WorkspaceID: "ws-doc", Name: "WF"})
now := time.Now().UTC()
_ = repo.CreateTask(ctx, &models.Task{
ID: taskID, WorkspaceID: "ws-doc", WorkflowID: "wf-doc",
Title: "T", State: v1.TaskStateCreated, CreatedAt: now, UpdatedAt: now,
})
}
func TestDocument_CreateAndGet(t *testing.T) {
repo, cleanup := createTestSQLiteRepo(t)
defer cleanup()
ctx := context.Background()
seedDocTask(t, ctx, repo, "task-d1")
doc := &models.TaskDocument{
TaskID: "task-d1",
Key: "spec",
Type: "spec",
Title: "Feature Spec",
Content: "## Spec\nHello",
AuthorKind: "agent",
AuthorName: "Agent",
}
if err := repo.CreateDocument(ctx, doc); err != nil {
t.Fatalf("create: %v", err)
}
if doc.ID == "" {
t.Fatal("expected ID to be set after create")
}
got, err := repo.GetDocument(ctx, "task-d1", "spec")
if err != nil {
t.Fatalf("get: %v", err)
}
if got == nil {
t.Fatal("expected document, got nil")
}
if got.Key != "spec" || got.Type != "spec" || got.Title != "Feature Spec" {
t.Errorf("unexpected doc: %+v", got)
}
}
func TestDocument_GetNonExistent(t *testing.T) {
repo, cleanup := createTestSQLiteRepo(t)
defer cleanup()
ctx := context.Background()
seedDocTask(t, ctx, repo, "task-d2")
got, err := repo.GetDocument(ctx, "task-d2", "missing")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != nil {
t.Fatalf("expected nil for missing document, got %+v", got)
}
}
func TestDocument_Update(t *testing.T) {
repo, cleanup := createTestSQLiteRepo(t)
defer cleanup()
ctx := context.Background()
seedDocTask(t, ctx, repo, "task-d3")
doc := &models.TaskDocument{
TaskID: "task-d3", Key: "plan", Type: "plan",
Title: "Plan", Content: "v1", AuthorKind: "agent", AuthorName: "Agent",
}
_ = repo.CreateDocument(ctx, doc)
doc.Content = "v2"
doc.Title = "Updated Plan"
if err := repo.UpdateDocument(ctx, doc); err != nil {
t.Fatalf("update: %v", err)
}
got, _ := repo.GetDocument(ctx, "task-d3", "plan")
if got.Content != "v2" || got.Title != "Updated Plan" {
t.Errorf("update not reflected: %+v", got)
}
}
func TestDocument_Delete(t *testing.T) {
repo, cleanup := createTestSQLiteRepo(t)
defer cleanup()
ctx := context.Background()
seedDocTask(t, ctx, repo, "task-d4")
_ = repo.CreateDocument(ctx, &models.TaskDocument{
TaskID: "task-d4", Key: "notes", Type: "notes",
Title: "Notes", AuthorKind: "user", AuthorName: "User",
})
if err := repo.DeleteDocument(ctx, "task-d4", "notes"); err != nil {
t.Fatalf("delete: %v", err)
}
got, _ := repo.GetDocument(ctx, "task-d4", "notes")
if got != nil {
t.Errorf("expected nil after delete, got %+v", got)
}
}
func TestDocument_ListDocuments(t *testing.T) {
repo, cleanup := createTestSQLiteRepo(t)
defer cleanup()
ctx := context.Background()
seedDocTask(t, ctx, repo, "task-d5")
keys := []string{"plan", "notes", "spec"}
for _, k := range keys {
_ = repo.CreateDocument(ctx, &models.TaskDocument{
TaskID: "task-d5", Key: k, Type: k,
Title: k, AuthorKind: "agent", AuthorName: "Agent",
})
}
list, err := repo.ListDocuments(ctx, "task-d5")
if err != nil {
t.Fatalf("list: %v", err)
}
if len(list) != 3 {
t.Fatalf("expected 3 documents, got %d", len(list))
}
// Should be ordered by key alphabetically: notes, plan, spec
if list[0].Key != "notes" || list[1].Key != "plan" || list[2].Key != "spec" {
t.Errorf("unexpected order: %v, %v, %v", list[0].Key, list[1].Key, list[2].Key)
}
}
func TestDocument_Revisions_InsertAndList(t *testing.T) {
repo, cleanup := createTestSQLiteRepo(t)
defer cleanup()
ctx := context.Background()
seedDocTask(t, ctx, repo, "task-dr1")
_ = repo.CreateDocument(ctx, &models.TaskDocument{
TaskID: "task-dr1", Key: "spec", Type: "spec",
Title: "Spec", AuthorKind: "agent", AuthorName: "Agent",
})
for i := 1; i <= 3; i++ {
rev := &models.TaskDocumentRevision{
TaskID: "task-dr1",
DocumentKey: "spec",
RevisionNumber: i,
Title: "Spec",
Content: "rev " + string(rune('0'+i)),
AuthorKind: "agent",
AuthorName: "Agent",
}
if err := repo.InsertDocumentRevision(ctx, rev); err != nil {
t.Fatalf("insert rev %d: %v", i, err)
}
}
list, err := repo.ListDocumentRevisions(ctx, "task-dr1", "spec", 0)
if err != nil {
t.Fatalf("list: %v", err)
}
if len(list) != 3 {
t.Fatalf("expected 3 revisions, got %d", len(list))
}
if list[0].RevisionNumber != 3 || list[2].RevisionNumber != 1 {
t.Errorf("expected newest-first ordering, got %d..%d", list[0].RevisionNumber, list[2].RevisionNumber)
}
}
func TestDocument_Revisions_GetLatest(t *testing.T) {
repo, cleanup := createTestSQLiteRepo(t)
defer cleanup()
ctx := context.Background()
seedDocTask(t, ctx, repo, "task-dr2")
_ = repo.CreateDocument(ctx, &models.TaskDocument{
TaskID: "task-dr2", Key: "plan", Type: "plan",
Title: "Plan", AuthorKind: "agent", AuthorName: "Agent",
})
latest, _ := repo.GetLatestDocumentRevision(ctx, "task-dr2", "plan")
if latest != nil {
t.Errorf("expected nil before any revisions, got %+v", latest)
}
_ = repo.InsertDocumentRevision(ctx, &models.TaskDocumentRevision{
TaskID: "task-dr2", DocumentKey: "plan", RevisionNumber: 1,
Content: "a", AuthorKind: "agent", AuthorName: "Agent",
})
_ = repo.InsertDocumentRevision(ctx, &models.TaskDocumentRevision{
TaskID: "task-dr2", DocumentKey: "plan", RevisionNumber: 2,
Content: "b", AuthorKind: "user", AuthorName: "User",
})
latest, err := repo.GetLatestDocumentRevision(ctx, "task-dr2", "plan")
if err != nil {
t.Fatalf("get latest: %v", err)
}
if latest == nil || latest.RevisionNumber != 2 || latest.Content != "b" {
t.Errorf("expected rev 2 content 'b', got %+v", latest)
}
}
func TestDocument_Revisions_NextRevisionNumber(t *testing.T) {
repo, cleanup := createTestSQLiteRepo(t)
defer cleanup()
ctx := context.Background()
seedDocTask(t, ctx, repo, "task-dr3")
n, err := repo.NextDocumentRevisionNumber(ctx, "task-dr3", "spec")
if err != nil {
t.Fatalf("next: %v", err)
}
if n != 1 {
t.Errorf("expected 1, got %d", n)
}
_ = repo.CreateDocument(ctx, &models.TaskDocument{
TaskID: "task-dr3", Key: "spec", Type: "spec",
Title: "Spec", AuthorKind: "agent", AuthorName: "Agent",
})
_ = repo.InsertDocumentRevision(ctx, &models.TaskDocumentRevision{
TaskID: "task-dr3", DocumentKey: "spec", RevisionNumber: 1, AuthorKind: "agent",
})
_ = repo.InsertDocumentRevision(ctx, &models.TaskDocumentRevision{
TaskID: "task-dr3", DocumentKey: "spec", RevisionNumber: 2, AuthorKind: "agent",
})
n, _ = repo.NextDocumentRevisionNumber(ctx, "task-dr3", "spec")
if n != 3 {
t.Errorf("expected 3, got %d", n)
}
}
func TestDocument_WriteDocumentRevision_NewRevision(t *testing.T) {
repo, cleanup := createTestSQLiteRepo(t)
defer cleanup()
ctx := context.Background()
seedDocTask(t, ctx, repo, "task-dw1")
head := &models.TaskDocument{
TaskID: "task-dw1", Key: "spec", Type: "spec",
Title: "Spec", Content: "v1",
AuthorKind: "agent", AuthorName: "Agent",
}
rev := &models.TaskDocumentRevision{
TaskID: "task-dw1", DocumentKey: "spec",
Title: "Spec", Content: "v1",
AuthorKind: "agent", AuthorName: "Agent",
}
if err := repo.WriteDocumentRevision(ctx, head, rev, nil); err != nil {
t.Fatalf("write: %v", err)
}
if rev.RevisionNumber != 1 {
t.Errorf("expected revision_number=1, got %d", rev.RevisionNumber)
}
if rev.ID == "" {
t.Error("expected ID to be set")
}
// Second write creates revision 2.
head2 := &models.TaskDocument{
TaskID: "task-dw1", Key: "spec", Type: "spec",
Title: "Spec", Content: "v2",
AuthorKind: "agent", AuthorName: "Agent",
}
rev2 := &models.TaskDocumentRevision{
TaskID: "task-dw1", DocumentKey: "spec",
Title: "Spec", Content: "v2",
AuthorKind: "agent", AuthorName: "Agent",
}
if err := repo.WriteDocumentRevision(ctx, head2, rev2, nil); err != nil {
t.Fatalf("write 2: %v", err)
}
if rev2.RevisionNumber != 2 {
t.Errorf("expected revision_number=2, got %d", rev2.RevisionNumber)
}
// HEAD should reflect latest content.
got, _ := repo.GetDocument(ctx, "task-dw1", "spec")
if got == nil || got.Content != "v2" {
t.Errorf("expected HEAD content 'v2', got %+v", got)
}
}
func TestDocument_WriteDocumentRevision_Coalesce(t *testing.T) {
repo, cleanup := createTestSQLiteRepo(t)
defer cleanup()
ctx := context.Background()
seedDocTask(t, ctx, repo, "task-dw2")
head := &models.TaskDocument{
TaskID: "task-dw2", Key: "plan", Type: "plan",
Title: "Plan", Content: "v1",
AuthorKind: "agent", AuthorName: "Agent",
}
rev := &models.TaskDocumentRevision{
TaskID: "task-dw2", DocumentKey: "plan",
Title: "Plan", Content: "v1",
AuthorKind: "agent", AuthorName: "Agent",
}
_ = repo.WriteDocumentRevision(ctx, head, rev, nil)
firstRevID := rev.ID
// Coalesce into the first revision.
head2 := &models.TaskDocument{
TaskID: "task-dw2", Key: "plan", Type: "plan",
Title: "Plan", Content: "v2",
AuthorKind: "agent", AuthorName: "Agent",
}
rev2 := &models.TaskDocumentRevision{
TaskID: "task-dw2", DocumentKey: "plan",
Title: "Plan", Content: "v2",
AuthorKind: "agent", AuthorName: "Agent",
}
if err := repo.WriteDocumentRevision(ctx, head2, rev2, &firstRevID); err != nil {
t.Fatalf("coalesce write: %v", err)
}
if rev2.ID != firstRevID {
t.Errorf("expected coalesced revision to have original ID %q, got %q", firstRevID, rev2.ID)
}
// Still only one revision row.
list, _ := repo.ListDocumentRevisions(ctx, "task-dw2", "plan", 0)
if len(list) != 1 {
t.Errorf("coalesce should not add rows; have %d", len(list))
}
if list[0].Content != "v2" {
t.Errorf("expected merged content 'v2', got %q", list[0].Content)
}
}
func TestDocument_Revisions_RevertOf(t *testing.T) {
repo, cleanup := createTestSQLiteRepo(t)
defer cleanup()
ctx := context.Background()
seedDocTask(t, ctx, repo, "task-dr4")
_ = repo.CreateDocument(ctx, &models.TaskDocument{
TaskID: "task-dr4", Key: "spec", Type: "spec",
Title: "Spec", AuthorKind: "agent", AuthorName: "Agent",
})
target := &models.TaskDocumentRevision{
TaskID: "task-dr4", DocumentKey: "spec", RevisionNumber: 1,
Content: "original", AuthorKind: "agent", AuthorName: "Agent",
}
_ = repo.InsertDocumentRevision(ctx, target)
refID := target.ID
revertRev := &models.TaskDocumentRevision{
TaskID: "task-dr4", DocumentKey: "spec", RevisionNumber: 2,
Content: "original", AuthorKind: "user", AuthorName: "User",
RevertOfRevisionID: &refID,
}
_ = repo.InsertDocumentRevision(ctx, revertRev)
got, _ := repo.GetDocumentRevision(ctx, revertRev.ID)
if got.RevertOfRevisionID == nil || *got.RevertOfRevisionID != refID {
t.Errorf("expected revert_of_revision_id=%q, got %v", refID, got.RevertOfRevisionID)
}
}
func TestDocument_UniqueKeyConstraint(t *testing.T) {
repo, cleanup := createTestSQLiteRepo(t)
defer cleanup()
ctx := context.Background()
seedDocTask(t, ctx, repo, "task-du1")
doc := &models.TaskDocument{
TaskID: "task-du1", Key: "spec", Type: "spec",
Title: "Spec", AuthorKind: "agent", AuthorName: "Agent",
}
if err := repo.CreateDocument(ctx, doc); err != nil {
t.Fatalf("first create: %v", err)
}
// Second create with same key should fail.
doc2 := &models.TaskDocument{
TaskID: "task-du1", Key: "spec", Type: "spec",
Title: "Spec 2", AuthorKind: "agent", AuthorName: "Agent",
}
if err := repo.CreateDocument(ctx, doc2); err == nil {
t.Fatal("expected UNIQUE violation on duplicate (task_id, key)")
}
}
func TestDocument_IsolatedByTask(t *testing.T) {
repo, cleanup := createTestSQLiteRepo(t)
defer cleanup()
ctx := context.Background()
seedDocTask(t, ctx, repo, "task-di1")
seedDocTask(t, ctx, repo, "task-di2")
_ = repo.CreateDocument(ctx, &models.TaskDocument{
TaskID: "task-di1", Key: "plan", Type: "plan",
Title: "Plan A", AuthorKind: "agent", AuthorName: "Agent",
})
_ = repo.CreateDocument(ctx, &models.TaskDocument{
TaskID: "task-di2", Key: "plan", Type: "plan",
Title: "Plan B", AuthorKind: "agent", AuthorName: "Agent",
})
list1, _ := repo.ListDocuments(ctx, "task-di1")
list2, _ := repo.ListDocuments(ctx, "task-di2")
if len(list1) != 1 || list1[0].Title != "Plan A" {
t.Errorf("task-di1 should have Plan A: %+v", list1)
}
if len(list2) != 1 || list2[0].Title != "Plan B" {
t.Errorf("task-di2 should have Plan B: %+v", list2)
}
}