300 lines
8.9 KiB
Go
300 lines
8.9 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"testing"
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
_ "github.com/mattn/go-sqlite3"
|
|
|
|
"github.com/kandev/kandev/internal/workflow/models"
|
|
)
|
|
|
|
func setupTestRepo(t *testing.T) *Repository {
|
|
repo, _ := setupTestRepoWithDB(t)
|
|
return repo
|
|
}
|
|
|
|
func setupTestRepoWithDB(t *testing.T) (*Repository, *sqlx.DB) {
|
|
t.Helper()
|
|
rawDB, err := sql.Open("sqlite3", ":memory:?_foreign_keys=on")
|
|
if err != nil {
|
|
t.Fatalf("failed to open sqlite: %v", err)
|
|
}
|
|
sqlxDB := sqlx.NewDb(rawDB, "sqlite3")
|
|
t.Cleanup(func() { _ = sqlxDB.Close() })
|
|
// Enable FK enforcement explicitly so workflow_step_participants
|
|
// ON DELETE CASCADE behaves as designed in the cascade test.
|
|
if _, err := sqlxDB.Exec(`PRAGMA foreign_keys = ON`); err != nil {
|
|
t.Fatalf("enable foreign keys: %v", err)
|
|
}
|
|
|
|
// Create workflows table (normally created by task repo)
|
|
_, err = sqlxDB.Exec(`CREATE TABLE IF NOT EXISTS workflows (
|
|
id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL DEFAULT '',
|
|
workflow_template_id TEXT DEFAULT '', name TEXT NOT NULL,
|
|
description TEXT DEFAULT '', created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL
|
|
)`)
|
|
if err != nil {
|
|
t.Fatalf("failed to create workflows table: %v", err)
|
|
}
|
|
|
|
// Create task_sessions table (referenced by session_step_history FK)
|
|
_, err = sqlxDB.Exec(`CREATE TABLE IF NOT EXISTS task_sessions (
|
|
id TEXT PRIMARY KEY
|
|
)`)
|
|
if err != nil {
|
|
t.Fatalf("failed to create task_sessions table: %v", err)
|
|
}
|
|
|
|
// Insert a test workflow
|
|
_, err = sqlxDB.Exec(`INSERT INTO workflows (id, workspace_id, name, created_at, updated_at)
|
|
VALUES ('wf-test', '', 'Test', datetime('now'), datetime('now'))`)
|
|
if err != nil {
|
|
t.Fatalf("failed to insert test workflow: %v", err)
|
|
}
|
|
|
|
repo, err := NewWithDB(sqlxDB, sqlxDB, nil)
|
|
if err != nil {
|
|
t.Fatalf("failed to create repo: %v", err)
|
|
}
|
|
return repo, sqlxDB
|
|
}
|
|
|
|
func TestStepAgentProfileID_CreateAndGet(t *testing.T) {
|
|
repo := setupTestRepo(t)
|
|
ctx := context.Background()
|
|
|
|
step := &models.WorkflowStep{
|
|
WorkflowID: "wf-test",
|
|
Name: "Test Step",
|
|
Position: 0,
|
|
Color: "#000000",
|
|
AgentProfileID: "agent-profile-abc",
|
|
}
|
|
|
|
if err := repo.CreateStep(ctx, step); err != nil {
|
|
t.Fatalf("failed to create step: %v", err)
|
|
}
|
|
if step.ID == "" {
|
|
t.Fatal("expected step ID to be set")
|
|
}
|
|
|
|
retrieved, err := repo.GetStep(ctx, step.ID)
|
|
if err != nil {
|
|
t.Fatalf("failed to get step: %v", err)
|
|
}
|
|
if retrieved.AgentProfileID != "agent-profile-abc" {
|
|
t.Errorf("expected agent_profile_id 'agent-profile-abc', got %q", retrieved.AgentProfileID)
|
|
}
|
|
}
|
|
|
|
func TestStepAgentProfileID_Update(t *testing.T) {
|
|
repo := setupTestRepo(t)
|
|
ctx := context.Background()
|
|
|
|
step := &models.WorkflowStep{
|
|
WorkflowID: "wf-test",
|
|
Name: "Update Step",
|
|
Position: 0,
|
|
AgentProfileID: "profile-original",
|
|
}
|
|
if err := repo.CreateStep(ctx, step); err != nil {
|
|
t.Fatalf("failed to create step: %v", err)
|
|
}
|
|
|
|
// Update agent_profile_id
|
|
step.AgentProfileID = "profile-updated"
|
|
if err := repo.UpdateStep(ctx, step); err != nil {
|
|
t.Fatalf("failed to update step: %v", err)
|
|
}
|
|
|
|
retrieved, err := repo.GetStep(ctx, step.ID)
|
|
if err != nil {
|
|
t.Fatalf("failed to get step: %v", err)
|
|
}
|
|
if retrieved.AgentProfileID != "profile-updated" {
|
|
t.Errorf("expected agent_profile_id 'profile-updated', got %q", retrieved.AgentProfileID)
|
|
}
|
|
|
|
// Clear agent_profile_id
|
|
step.AgentProfileID = ""
|
|
if err := repo.UpdateStep(ctx, step); err != nil {
|
|
t.Fatalf("failed to update step: %v", err)
|
|
}
|
|
retrieved, _ = repo.GetStep(ctx, step.ID)
|
|
if retrieved.AgentProfileID != "" {
|
|
t.Errorf("expected empty agent_profile_id, got %q", retrieved.AgentProfileID)
|
|
}
|
|
}
|
|
|
|
func TestStepAgentProfileID_ListByWorkflow(t *testing.T) {
|
|
repo := setupTestRepo(t)
|
|
ctx := context.Background()
|
|
|
|
step1 := &models.WorkflowStep{
|
|
WorkflowID: "wf-test",
|
|
Name: "Step 1",
|
|
Position: 0,
|
|
AgentProfileID: "profile-a",
|
|
}
|
|
step2 := &models.WorkflowStep{
|
|
WorkflowID: "wf-test",
|
|
Name: "Step 2",
|
|
Position: 1,
|
|
AgentProfileID: "profile-b",
|
|
}
|
|
if err := repo.CreateStep(ctx, step1); err != nil {
|
|
t.Fatalf("failed to create step1: %v", err)
|
|
}
|
|
if err := repo.CreateStep(ctx, step2); err != nil {
|
|
t.Fatalf("failed to create step2: %v", err)
|
|
}
|
|
|
|
steps, err := repo.ListStepsByWorkflow(ctx, "wf-test")
|
|
if err != nil {
|
|
t.Fatalf("failed to list steps: %v", err)
|
|
}
|
|
// Filter out any seeded steps (migration may have seeded defaults)
|
|
var testSteps []*models.WorkflowStep
|
|
for _, s := range steps {
|
|
if s.AgentProfileID == "profile-a" || s.AgentProfileID == "profile-b" {
|
|
testSteps = append(testSteps, s)
|
|
}
|
|
}
|
|
if len(testSteps) != 2 {
|
|
t.Fatalf("expected 2 steps with agent profiles, got %d", len(testSteps))
|
|
}
|
|
if testSteps[0].AgentProfileID != "profile-a" {
|
|
t.Errorf("expected first step agent_profile_id 'profile-a', got %q", testSteps[0].AgentProfileID)
|
|
}
|
|
if testSteps[1].AgentProfileID != "profile-b" {
|
|
t.Errorf("expected second step agent_profile_id 'profile-b', got %q", testSteps[1].AgentProfileID)
|
|
}
|
|
}
|
|
|
|
func TestStepAgentProfileID_EmptyByDefault(t *testing.T) {
|
|
repo := setupTestRepo(t)
|
|
ctx := context.Background()
|
|
|
|
step := &models.WorkflowStep{
|
|
WorkflowID: "wf-test",
|
|
Name: "No Profile Step",
|
|
Position: 0,
|
|
}
|
|
if err := repo.CreateStep(ctx, step); err != nil {
|
|
t.Fatalf("failed to create step: %v", err)
|
|
}
|
|
|
|
retrieved, err := repo.GetStep(ctx, step.ID)
|
|
if err != nil {
|
|
t.Fatalf("failed to get step: %v", err)
|
|
}
|
|
if retrieved.AgentProfileID != "" {
|
|
t.Errorf("expected empty agent_profile_id by default, got %q", retrieved.AgentProfileID)
|
|
}
|
|
}
|
|
|
|
func TestInitSchema_NormalizesDuplicateStartSteps(t *testing.T) {
|
|
rawDB, err := sql.Open("sqlite3", ":memory:?_foreign_keys=on")
|
|
if err != nil {
|
|
t.Fatalf("failed to open sqlite: %v", err)
|
|
}
|
|
db := sqlx.NewDb(rawDB, "sqlite3")
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
ctx := context.Background()
|
|
|
|
_, err = db.Exec(`
|
|
CREATE TABLE workflows (
|
|
id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL DEFAULT '',
|
|
workflow_template_id TEXT DEFAULT '', name TEXT NOT NULL,
|
|
description TEXT DEFAULT '', created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL
|
|
);
|
|
CREATE TABLE task_sessions (id TEXT PRIMARY KEY);
|
|
CREATE TABLE workflow_steps (
|
|
id TEXT PRIMARY KEY,
|
|
workflow_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
position INTEGER NOT NULL,
|
|
color TEXT,
|
|
prompt TEXT,
|
|
events TEXT,
|
|
allow_manual_move INTEGER DEFAULT 1,
|
|
is_start_step INTEGER DEFAULT 0,
|
|
show_in_command_panel INTEGER DEFAULT 1,
|
|
auto_archive_after_hours INTEGER DEFAULT 0,
|
|
agent_profile_id TEXT DEFAULT '',
|
|
stage_type TEXT NOT NULL DEFAULT 'custom',
|
|
auto_advance_requires_signal INTEGER NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMP NOT NULL,
|
|
updated_at TIMESTAMP NOT NULL,
|
|
FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON DELETE CASCADE
|
|
);
|
|
INSERT INTO workflows (id, workspace_id, name, created_at, updated_at)
|
|
VALUES ('wf-test', '', 'Test', datetime('now'), datetime('now'));
|
|
INSERT INTO workflow_steps (
|
|
id, workflow_id, name, position, is_start_step, created_at, updated_at
|
|
) VALUES
|
|
('latest-position-start', 'wf-test', 'Latest Position Start', 2, 1, datetime('2026-01-01T00:00:00Z'), datetime('2026-01-01T00:00:00Z')),
|
|
('latest-updated-start', 'wf-test', 'Latest Updated Start', 0, 1, datetime('2026-01-01T00:00:00Z'), datetime('2026-01-02T00:00:00Z'));
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("seed pre-repair database: %v", err)
|
|
}
|
|
|
|
reopened, err := NewWithDB(db, db, nil)
|
|
if err != nil {
|
|
t.Fatalf("reopen repo: %v", err)
|
|
}
|
|
steps, err := reopened.ListStepsByWorkflow(ctx, "wf-test")
|
|
if err != nil {
|
|
t.Fatalf("list steps: %v", err)
|
|
}
|
|
|
|
var starts []string
|
|
for _, step := range steps {
|
|
if step.IsStartStep {
|
|
starts = append(starts, step.Name)
|
|
}
|
|
}
|
|
if len(starts) != 1 || starts[0] != "Latest Updated Start" {
|
|
t.Fatalf("expected only Latest Updated Start to remain start, got %v", starts)
|
|
}
|
|
}
|
|
|
|
func TestCreateStep_RollsBackStartDemotionWhenInsertFails(t *testing.T) {
|
|
repo, db := setupTestRepoWithDB(t)
|
|
ctx := context.Background()
|
|
|
|
if _, err := db.Exec(`
|
|
DELETE FROM workflow_steps WHERE workflow_id = 'wf-test';
|
|
INSERT INTO workflow_steps (
|
|
id, workflow_id, name, position, is_start_step, created_at, updated_at
|
|
) VALUES
|
|
('old-start', 'wf-test', 'Old Start', 0, 1, datetime('now'), datetime('now')),
|
|
('duplicate-target', 'wf-test', 'Duplicate Target', 1, 0, datetime('now'), datetime('now'));
|
|
`); err != nil {
|
|
t.Fatalf("seed rollback workflow steps: %v", err)
|
|
}
|
|
|
|
err := repo.CreateStep(ctx, &models.WorkflowStep{
|
|
ID: "duplicate-target",
|
|
WorkflowID: "wf-test",
|
|
Name: "Duplicate ID Start",
|
|
Position: 99,
|
|
IsStartStep: true,
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected duplicate ID insert to fail")
|
|
}
|
|
|
|
start, err := repo.GetStartStep(ctx, "wf-test")
|
|
if err != nil {
|
|
t.Fatalf("get start step: %v", err)
|
|
}
|
|
if start == nil || start.ID != "old-start" {
|
|
t.Fatalf("expected original start step %q to remain after rollback, got %#v", "old-start", start)
|
|
}
|
|
}
|