cleanup
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -11,9 +12,191 @@ import (
|
|||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/steveyegge/beads/internal/rpc"
|
"github.com/steveyegge/beads/internal/rpc"
|
||||||
|
"github.com/steveyegge/beads/internal/storage"
|
||||||
"github.com/steveyegge/beads/internal/types"
|
"github.com/steveyegge/beads/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// createFormRawInput holds the raw string values from the form UI.
|
||||||
|
// This struct encapsulates all form fields before parsing/conversion.
|
||||||
|
type createFormRawInput struct {
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
IssueType string
|
||||||
|
Priority string // String from select, e.g., "0", "1", "2"
|
||||||
|
Assignee string
|
||||||
|
Labels string // Comma-separated
|
||||||
|
Design string
|
||||||
|
Acceptance string
|
||||||
|
ExternalRef string
|
||||||
|
Deps string // Comma-separated, format: "type:id" or "id"
|
||||||
|
}
|
||||||
|
|
||||||
|
// createFormValues holds the parsed values from the create-form input.
|
||||||
|
// This struct is used to pass form data to the issue creation logic,
|
||||||
|
// allowing the creation logic to be tested independently of the form UI.
|
||||||
|
type createFormValues struct {
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
IssueType string
|
||||||
|
Priority int
|
||||||
|
Assignee string
|
||||||
|
Labels []string
|
||||||
|
Design string
|
||||||
|
AcceptanceCriteria string
|
||||||
|
ExternalRef string
|
||||||
|
Dependencies []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseCreateFormInput parses raw form input into a createFormValues struct.
|
||||||
|
// It handles comma-separated labels and dependencies, and converts priority strings.
|
||||||
|
func parseCreateFormInput(raw *createFormRawInput) *createFormValues {
|
||||||
|
// Parse priority
|
||||||
|
priority, err := strconv.Atoi(raw.Priority)
|
||||||
|
if err != nil {
|
||||||
|
priority = 2 // Default to medium if parsing fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse labels
|
||||||
|
var labels []string
|
||||||
|
if raw.Labels != "" {
|
||||||
|
for _, l := range strings.Split(raw.Labels, ",") {
|
||||||
|
l = strings.TrimSpace(l)
|
||||||
|
if l != "" {
|
||||||
|
labels = append(labels, l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse dependencies
|
||||||
|
var deps []string
|
||||||
|
if raw.Deps != "" {
|
||||||
|
for _, d := range strings.Split(raw.Deps, ",") {
|
||||||
|
d = strings.TrimSpace(d)
|
||||||
|
if d != "" {
|
||||||
|
deps = append(deps, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &createFormValues{
|
||||||
|
Title: raw.Title,
|
||||||
|
Description: raw.Description,
|
||||||
|
IssueType: raw.IssueType,
|
||||||
|
Priority: priority,
|
||||||
|
Assignee: raw.Assignee,
|
||||||
|
Labels: labels,
|
||||||
|
Design: raw.Design,
|
||||||
|
AcceptanceCriteria: raw.Acceptance,
|
||||||
|
ExternalRef: raw.ExternalRef,
|
||||||
|
Dependencies: deps,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateIssueFromFormValues creates an issue from the given form values.
|
||||||
|
// It returns the created issue and any error that occurred.
|
||||||
|
// This function handles labels, dependencies, and source_repo inheritance.
|
||||||
|
func CreateIssueFromFormValues(ctx context.Context, s storage.Storage, fv *createFormValues, actor string) (*types.Issue, error) {
|
||||||
|
var externalRefPtr *string
|
||||||
|
if fv.ExternalRef != "" {
|
||||||
|
externalRefPtr = &fv.ExternalRef
|
||||||
|
}
|
||||||
|
|
||||||
|
issue := &types.Issue{
|
||||||
|
Title: fv.Title,
|
||||||
|
Description: fv.Description,
|
||||||
|
Design: fv.Design,
|
||||||
|
AcceptanceCriteria: fv.AcceptanceCriteria,
|
||||||
|
Status: types.StatusOpen,
|
||||||
|
Priority: fv.Priority,
|
||||||
|
IssueType: types.IssueType(fv.IssueType),
|
||||||
|
Assignee: fv.Assignee,
|
||||||
|
ExternalRef: externalRefPtr,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any dependencies are discovered-from type
|
||||||
|
// If so, inherit source_repo from the parent issue
|
||||||
|
var discoveredFromParentID string
|
||||||
|
for _, depSpec := range fv.Dependencies {
|
||||||
|
depSpec = strings.TrimSpace(depSpec)
|
||||||
|
if depSpec == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(depSpec, ":") {
|
||||||
|
parts := strings.SplitN(depSpec, ":", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
depType := types.DependencyType(strings.TrimSpace(parts[0]))
|
||||||
|
dependsOnID := strings.TrimSpace(parts[1])
|
||||||
|
|
||||||
|
if depType == types.DepDiscoveredFrom && dependsOnID != "" {
|
||||||
|
discoveredFromParentID = dependsOnID
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we found a discovered-from dependency, inherit source_repo from parent
|
||||||
|
if discoveredFromParentID != "" {
|
||||||
|
parentIssue, err := s.GetIssue(ctx, discoveredFromParentID)
|
||||||
|
if err == nil && parentIssue != nil && parentIssue.SourceRepo != "" {
|
||||||
|
issue.SourceRepo = parentIssue.SourceRepo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.CreateIssue(ctx, issue, actor); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create issue: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add labels if specified
|
||||||
|
for _, label := range fv.Labels {
|
||||||
|
if err := s.AddLabel(ctx, issue.ID, label, actor); err != nil {
|
||||||
|
// Log warning but don't fail the entire operation
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: failed to add label %s: %v\n", label, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add dependencies if specified
|
||||||
|
for _, depSpec := range fv.Dependencies {
|
||||||
|
depSpec = strings.TrimSpace(depSpec)
|
||||||
|
if depSpec == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var depType types.DependencyType
|
||||||
|
var dependsOnID string
|
||||||
|
|
||||||
|
if strings.Contains(depSpec, ":") {
|
||||||
|
parts := strings.SplitN(depSpec, ":", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: invalid dependency format '%s', expected 'type:id' or 'id'\n", depSpec)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
depType = types.DependencyType(strings.TrimSpace(parts[0]))
|
||||||
|
dependsOnID = strings.TrimSpace(parts[1])
|
||||||
|
} else {
|
||||||
|
depType = types.DepBlocks
|
||||||
|
dependsOnID = depSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
if !depType.IsValid() {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: invalid dependency type '%s' (valid: blocks, related, parent-child, discovered-from)\n", depType)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dep := &types.Dependency{
|
||||||
|
IssueID: issue.ID,
|
||||||
|
DependsOnID: dependsOnID,
|
||||||
|
Type: depType,
|
||||||
|
}
|
||||||
|
if err := s.AddDependency(ctx, dep, actor); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: failed to add dependency %s -> %s: %v\n", issue.ID, dependsOnID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return issue, nil
|
||||||
|
}
|
||||||
|
|
||||||
var createFormCmd = &cobra.Command{
|
var createFormCmd = &cobra.Command{
|
||||||
Use: "create-form",
|
Use: "create-form",
|
||||||
Short: "Create a new issue using an interactive form",
|
Short: "Create a new issue using an interactive form",
|
||||||
@@ -34,19 +217,8 @@ The form uses keyboard navigation:
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runCreateForm(cmd *cobra.Command) {
|
func runCreateForm(cmd *cobra.Command) {
|
||||||
// Form field values
|
// Raw form input - will be populated by the form
|
||||||
var (
|
raw := &createFormRawInput{}
|
||||||
title string
|
|
||||||
description string
|
|
||||||
issueType string
|
|
||||||
priorityStr string
|
|
||||||
assignee string
|
|
||||||
labelsInput string
|
|
||||||
design string
|
|
||||||
acceptance string
|
|
||||||
externalRef string
|
|
||||||
depsInput string
|
|
||||||
)
|
|
||||||
|
|
||||||
// Issue type options
|
// Issue type options
|
||||||
typeOptions := []huh.Option[string]{
|
typeOptions := []huh.Option[string]{
|
||||||
@@ -73,7 +245,7 @@ func runCreateForm(cmd *cobra.Command) {
|
|||||||
Title("Title").
|
Title("Title").
|
||||||
Description("Brief summary of the issue (required)").
|
Description("Brief summary of the issue (required)").
|
||||||
Placeholder("e.g., Fix authentication bug in login handler").
|
Placeholder("e.g., Fix authentication bug in login handler").
|
||||||
Value(&title).
|
Value(&raw.Title).
|
||||||
Validate(func(s string) error {
|
Validate(func(s string) error {
|
||||||
if strings.TrimSpace(s) == "" {
|
if strings.TrimSpace(s) == "" {
|
||||||
return fmt.Errorf("title is required")
|
return fmt.Errorf("title is required")
|
||||||
@@ -89,19 +261,19 @@ func runCreateForm(cmd *cobra.Command) {
|
|||||||
Description("Detailed context about the issue").
|
Description("Detailed context about the issue").
|
||||||
Placeholder("Explain why this issue exists and what needs to be done...").
|
Placeholder("Explain why this issue exists and what needs to be done...").
|
||||||
CharLimit(5000).
|
CharLimit(5000).
|
||||||
Value(&description),
|
Value(&raw.Description),
|
||||||
|
|
||||||
huh.NewSelect[string]().
|
huh.NewSelect[string]().
|
||||||
Title("Type").
|
Title("Type").
|
||||||
Description("Categorize the kind of work").
|
Description("Categorize the kind of work").
|
||||||
Options(typeOptions...).
|
Options(typeOptions...).
|
||||||
Value(&issueType),
|
Value(&raw.IssueType),
|
||||||
|
|
||||||
huh.NewSelect[string]().
|
huh.NewSelect[string]().
|
||||||
Title("Priority").
|
Title("Priority").
|
||||||
Description("Set urgency level").
|
Description("Set urgency level").
|
||||||
Options(priorityOptions...).
|
Options(priorityOptions...).
|
||||||
Value(&priorityStr),
|
Value(&raw.Priority),
|
||||||
),
|
),
|
||||||
|
|
||||||
huh.NewGroup(
|
huh.NewGroup(
|
||||||
@@ -109,19 +281,19 @@ func runCreateForm(cmd *cobra.Command) {
|
|||||||
Title("Assignee").
|
Title("Assignee").
|
||||||
Description("Who should work on this? (optional)").
|
Description("Who should work on this? (optional)").
|
||||||
Placeholder("username or email").
|
Placeholder("username or email").
|
||||||
Value(&assignee),
|
Value(&raw.Assignee),
|
||||||
|
|
||||||
huh.NewInput().
|
huh.NewInput().
|
||||||
Title("Labels").
|
Title("Labels").
|
||||||
Description("Comma-separated tags (optional)").
|
Description("Comma-separated tags (optional)").
|
||||||
Placeholder("e.g., urgent, backend, needs-review").
|
Placeholder("e.g., urgent, backend, needs-review").
|
||||||
Value(&labelsInput),
|
Value(&raw.Labels),
|
||||||
|
|
||||||
huh.NewInput().
|
huh.NewInput().
|
||||||
Title("External Reference").
|
Title("External Reference").
|
||||||
Description("Link to external tracker (optional)").
|
Description("Link to external tracker (optional)").
|
||||||
Placeholder("e.g., gh-123, jira-ABC-456").
|
Placeholder("e.g., gh-123, jira-ABC-456").
|
||||||
Value(&externalRef),
|
Value(&raw.ExternalRef),
|
||||||
),
|
),
|
||||||
|
|
||||||
huh.NewGroup(
|
huh.NewGroup(
|
||||||
@@ -130,14 +302,14 @@ func runCreateForm(cmd *cobra.Command) {
|
|||||||
Description("Technical approach or design details (optional)").
|
Description("Technical approach or design details (optional)").
|
||||||
Placeholder("Describe the implementation approach...").
|
Placeholder("Describe the implementation approach...").
|
||||||
CharLimit(5000).
|
CharLimit(5000).
|
||||||
Value(&design),
|
Value(&raw.Design),
|
||||||
|
|
||||||
huh.NewText().
|
huh.NewText().
|
||||||
Title("Acceptance Criteria").
|
Title("Acceptance Criteria").
|
||||||
Description("How do we know this is done? (optional)").
|
Description("How do we know this is done? (optional)").
|
||||||
Placeholder("List the criteria for completion...").
|
Placeholder("List the criteria for completion...").
|
||||||
CharLimit(5000).
|
CharLimit(5000).
|
||||||
Value(&acceptance),
|
Value(&raw.Acceptance),
|
||||||
),
|
),
|
||||||
|
|
||||||
huh.NewGroup(
|
huh.NewGroup(
|
||||||
@@ -145,7 +317,7 @@ func runCreateForm(cmd *cobra.Command) {
|
|||||||
Title("Dependencies").
|
Title("Dependencies").
|
||||||
Description("Format: type:id or just id (optional)").
|
Description("Format: type:id or just id (optional)").
|
||||||
Placeholder("e.g., discovered-from:bd-20, blocks:bd-15").
|
Placeholder("e.g., discovered-from:bd-20, blocks:bd-15").
|
||||||
Value(&depsInput),
|
Value(&raw.Deps),
|
||||||
|
|
||||||
huh.NewConfirm().
|
huh.NewConfirm().
|
||||||
Title("Create this issue?").
|
Title("Create this issue?").
|
||||||
@@ -163,53 +335,22 @@ func runCreateForm(cmd *cobra.Command) {
|
|||||||
FatalError("form error: %v", err)
|
FatalError("form error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse priority
|
// Parse the form input
|
||||||
priority, err := strconv.Atoi(priorityStr)
|
fv := parseCreateFormInput(raw)
|
||||||
if err != nil {
|
|
||||||
priority = 2 // Default to medium if parsing fails
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse labels
|
|
||||||
var labels []string
|
|
||||||
if labelsInput != "" {
|
|
||||||
for _, l := range strings.Split(labelsInput, ",") {
|
|
||||||
l = strings.TrimSpace(l)
|
|
||||||
if l != "" {
|
|
||||||
labels = append(labels, l)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse dependencies
|
|
||||||
var deps []string
|
|
||||||
if depsInput != "" {
|
|
||||||
for _, d := range strings.Split(depsInput, ",") {
|
|
||||||
d = strings.TrimSpace(d)
|
|
||||||
if d != "" {
|
|
||||||
deps = append(deps, d)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the issue
|
|
||||||
var externalRefPtr *string
|
|
||||||
if externalRef != "" {
|
|
||||||
externalRefPtr = &externalRef
|
|
||||||
}
|
|
||||||
|
|
||||||
// If daemon is running, use RPC
|
// If daemon is running, use RPC
|
||||||
if daemonClient != nil {
|
if daemonClient != nil {
|
||||||
createArgs := &rpc.CreateArgs{
|
createArgs := &rpc.CreateArgs{
|
||||||
Title: title,
|
Title: fv.Title,
|
||||||
Description: description,
|
Description: fv.Description,
|
||||||
IssueType: issueType,
|
IssueType: fv.IssueType,
|
||||||
Priority: priority,
|
Priority: fv.Priority,
|
||||||
Design: design,
|
Design: fv.Design,
|
||||||
AcceptanceCriteria: acceptance,
|
AcceptanceCriteria: fv.AcceptanceCriteria,
|
||||||
Assignee: assignee,
|
Assignee: fv.Assignee,
|
||||||
ExternalRef: externalRef,
|
ExternalRef: fv.ExternalRef,
|
||||||
Labels: labels,
|
Labels: fv.Labels,
|
||||||
Dependencies: deps,
|
Dependencies: fv.Dependencies,
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := daemonClient.Create(createArgs)
|
resp, err := daemonClient.Create(createArgs)
|
||||||
@@ -229,101 +370,12 @@ func runCreateForm(cmd *cobra.Command) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direct mode
|
// Direct mode - use the extracted creation function
|
||||||
issue := &types.Issue{
|
issue, err := CreateIssueFromFormValues(rootCtx, store, fv, actor)
|
||||||
Title: title,
|
if err != nil {
|
||||||
Description: description,
|
|
||||||
Design: design,
|
|
||||||
AcceptanceCriteria: acceptance,
|
|
||||||
Status: types.StatusOpen,
|
|
||||||
Priority: priority,
|
|
||||||
IssueType: types.IssueType(issueType),
|
|
||||||
Assignee: assignee,
|
|
||||||
ExternalRef: externalRefPtr,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := rootCtx
|
|
||||||
|
|
||||||
// Check if any dependencies are discovered-from type
|
|
||||||
// If so, inherit source_repo from the parent issue
|
|
||||||
var discoveredFromParentID string
|
|
||||||
for _, depSpec := range deps {
|
|
||||||
depSpec = strings.TrimSpace(depSpec)
|
|
||||||
if depSpec == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(depSpec, ":") {
|
|
||||||
parts := strings.SplitN(depSpec, ":", 2)
|
|
||||||
if len(parts) == 2 {
|
|
||||||
depType := types.DependencyType(strings.TrimSpace(parts[0]))
|
|
||||||
dependsOnID := strings.TrimSpace(parts[1])
|
|
||||||
|
|
||||||
if depType == types.DepDiscoveredFrom && dependsOnID != "" {
|
|
||||||
discoveredFromParentID = dependsOnID
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we found a discovered-from dependency, inherit source_repo from parent
|
|
||||||
if discoveredFromParentID != "" {
|
|
||||||
parentIssue, err := store.GetIssue(ctx, discoveredFromParentID)
|
|
||||||
if err == nil && parentIssue.SourceRepo != "" {
|
|
||||||
issue.SourceRepo = parentIssue.SourceRepo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := store.CreateIssue(ctx, issue, actor); err != nil {
|
|
||||||
FatalError("%v", err)
|
FatalError("%v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add labels if specified
|
|
||||||
for _, label := range labels {
|
|
||||||
if err := store.AddLabel(ctx, issue.ID, label, actor); err != nil {
|
|
||||||
WarnError("failed to add label %s: %v", label, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add dependencies if specified
|
|
||||||
for _, depSpec := range deps {
|
|
||||||
depSpec = strings.TrimSpace(depSpec)
|
|
||||||
if depSpec == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var depType types.DependencyType
|
|
||||||
var dependsOnID string
|
|
||||||
|
|
||||||
if strings.Contains(depSpec, ":") {
|
|
||||||
parts := strings.SplitN(depSpec, ":", 2)
|
|
||||||
if len(parts) != 2 {
|
|
||||||
WarnError("invalid dependency format '%s', expected 'type:id' or 'id'", depSpec)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
depType = types.DependencyType(strings.TrimSpace(parts[0]))
|
|
||||||
dependsOnID = strings.TrimSpace(parts[1])
|
|
||||||
} else {
|
|
||||||
depType = types.DepBlocks
|
|
||||||
dependsOnID = depSpec
|
|
||||||
}
|
|
||||||
|
|
||||||
if !depType.IsValid() {
|
|
||||||
WarnError("invalid dependency type '%s' (valid: blocks, related, parent-child, discovered-from)", depType)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
dep := &types.Dependency{
|
|
||||||
IssueID: issue.ID,
|
|
||||||
DependsOnID: dependsOnID,
|
|
||||||
Type: depType,
|
|
||||||
}
|
|
||||||
if err := store.AddDependency(ctx, dep, actor); err != nil {
|
|
||||||
WarnError("failed to add dependency %s -> %s: %v", issue.ID, dependsOnID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schedule auto-flush
|
// Schedule auto-flush
|
||||||
markDirtyAndScheduleFlush()
|
markDirtyAndScheduleFlush()
|
||||||
|
|
||||||
|
|||||||
599
cmd/bd/create_form_test.go
Normal file
599
cmd/bd/create_form_test.go
Normal file
@@ -0,0 +1,599 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/steveyegge/beads/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseFormInput(t *testing.T) {
|
||||||
|
t.Run("BasicParsing", func(t *testing.T) {
|
||||||
|
fv := parseCreateFormInput(&createFormRawInput{
|
||||||
|
Title: "Test Title",
|
||||||
|
Description: "Test Description",
|
||||||
|
IssueType: "bug",
|
||||||
|
Priority: "1",
|
||||||
|
Assignee: "alice",
|
||||||
|
})
|
||||||
|
|
||||||
|
if fv.Title != "Test Title" {
|
||||||
|
t.Errorf("expected title 'Test Title', got %q", fv.Title)
|
||||||
|
}
|
||||||
|
if fv.Description != "Test Description" {
|
||||||
|
t.Errorf("expected description 'Test Description', got %q", fv.Description)
|
||||||
|
}
|
||||||
|
if fv.IssueType != "bug" {
|
||||||
|
t.Errorf("expected issue type 'bug', got %q", fv.IssueType)
|
||||||
|
}
|
||||||
|
if fv.Priority != 1 {
|
||||||
|
t.Errorf("expected priority 1, got %d", fv.Priority)
|
||||||
|
}
|
||||||
|
if fv.Assignee != "alice" {
|
||||||
|
t.Errorf("expected assignee 'alice', got %q", fv.Assignee)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("PriorityParsing", func(t *testing.T) {
|
||||||
|
// Valid priority
|
||||||
|
fv := parseCreateFormInput(&createFormRawInput{Title: "Title", IssueType: "task", Priority: "0"})
|
||||||
|
if fv.Priority != 0 {
|
||||||
|
t.Errorf("expected priority 0, got %d", fv.Priority)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalid priority defaults to 2
|
||||||
|
fv = parseCreateFormInput(&createFormRawInput{Title: "Title", IssueType: "task", Priority: "invalid"})
|
||||||
|
if fv.Priority != 2 {
|
||||||
|
t.Errorf("expected default priority 2 for invalid input, got %d", fv.Priority)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty priority defaults to 2
|
||||||
|
fv = parseCreateFormInput(&createFormRawInput{Title: "Title", IssueType: "task", Priority: ""})
|
||||||
|
if fv.Priority != 2 {
|
||||||
|
t.Errorf("expected default priority 2 for empty input, got %d", fv.Priority)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("LabelsParsing", func(t *testing.T) {
|
||||||
|
fv := parseCreateFormInput(&createFormRawInput{
|
||||||
|
Title: "Title",
|
||||||
|
IssueType: "task",
|
||||||
|
Priority: "2",
|
||||||
|
Labels: "bug, critical, needs-review",
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(fv.Labels) != 3 {
|
||||||
|
t.Fatalf("expected 3 labels, got %d", len(fv.Labels))
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []string{"bug", "critical", "needs-review"}
|
||||||
|
for i, label := range expected {
|
||||||
|
if fv.Labels[i] != label {
|
||||||
|
t.Errorf("expected label %q at index %d, got %q", label, i, fv.Labels[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("LabelsWithEmptyValues", func(t *testing.T) {
|
||||||
|
fv := parseCreateFormInput(&createFormRawInput{
|
||||||
|
Title: "Title",
|
||||||
|
IssueType: "task",
|
||||||
|
Priority: "2",
|
||||||
|
Labels: "bug, , critical, ",
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(fv.Labels) != 2 {
|
||||||
|
t.Fatalf("expected 2 non-empty labels, got %d: %v", len(fv.Labels), fv.Labels)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("DependenciesParsing", func(t *testing.T) {
|
||||||
|
fv := parseCreateFormInput(&createFormRawInput{
|
||||||
|
Title: "Title",
|
||||||
|
IssueType: "task",
|
||||||
|
Priority: "2",
|
||||||
|
Deps: "discovered-from:bd-20, blocks:bd-15",
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(fv.Dependencies) != 2 {
|
||||||
|
t.Fatalf("expected 2 dependencies, got %d", len(fv.Dependencies))
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []string{"discovered-from:bd-20", "blocks:bd-15"}
|
||||||
|
for i, dep := range expected {
|
||||||
|
if fv.Dependencies[i] != dep {
|
||||||
|
t.Errorf("expected dependency %q at index %d, got %q", dep, i, fv.Dependencies[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("AllFields", func(t *testing.T) {
|
||||||
|
fv := parseCreateFormInput(&createFormRawInput{
|
||||||
|
Title: "Full Issue",
|
||||||
|
Description: "Detailed description",
|
||||||
|
IssueType: "feature",
|
||||||
|
Priority: "1",
|
||||||
|
Assignee: "bob",
|
||||||
|
Labels: "frontend, urgent",
|
||||||
|
Design: "Use React hooks",
|
||||||
|
Acceptance: "Tests pass, UI works",
|
||||||
|
ExternalRef: "gh-123",
|
||||||
|
Deps: "blocks:bd-1",
|
||||||
|
})
|
||||||
|
|
||||||
|
if fv.Title != "Full Issue" {
|
||||||
|
t.Errorf("unexpected title: %q", fv.Title)
|
||||||
|
}
|
||||||
|
if fv.Description != "Detailed description" {
|
||||||
|
t.Errorf("unexpected description: %q", fv.Description)
|
||||||
|
}
|
||||||
|
if fv.IssueType != "feature" {
|
||||||
|
t.Errorf("unexpected issue type: %q", fv.IssueType)
|
||||||
|
}
|
||||||
|
if fv.Priority != 1 {
|
||||||
|
t.Errorf("unexpected priority: %d", fv.Priority)
|
||||||
|
}
|
||||||
|
if fv.Assignee != "bob" {
|
||||||
|
t.Errorf("unexpected assignee: %q", fv.Assignee)
|
||||||
|
}
|
||||||
|
if len(fv.Labels) != 2 {
|
||||||
|
t.Errorf("unexpected labels count: %d", len(fv.Labels))
|
||||||
|
}
|
||||||
|
if fv.Design != "Use React hooks" {
|
||||||
|
t.Errorf("unexpected design: %q", fv.Design)
|
||||||
|
}
|
||||||
|
if fv.AcceptanceCriteria != "Tests pass, UI works" {
|
||||||
|
t.Errorf("unexpected acceptance criteria: %q", fv.AcceptanceCriteria)
|
||||||
|
}
|
||||||
|
if fv.ExternalRef != "gh-123" {
|
||||||
|
t.Errorf("unexpected external ref: %q", fv.ExternalRef)
|
||||||
|
}
|
||||||
|
if len(fv.Dependencies) != 1 {
|
||||||
|
t.Errorf("unexpected dependencies count: %d", len(fv.Dependencies))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateIssueFromFormValues(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
testDB := filepath.Join(tmpDir, ".beads", "beads.db")
|
||||||
|
s := newTestStore(t, testDB)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
t.Run("BasicIssue", func(t *testing.T) {
|
||||||
|
fv := &createFormValues{
|
||||||
|
Title: "Test Form Issue",
|
||||||
|
Priority: 1,
|
||||||
|
IssueType: "bug",
|
||||||
|
}
|
||||||
|
|
||||||
|
issue, err := CreateIssueFromFormValues(ctx, s, fv, "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create issue: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if issue.Title != "Test Form Issue" {
|
||||||
|
t.Errorf("expected title 'Test Form Issue', got %q", issue.Title)
|
||||||
|
}
|
||||||
|
if issue.Priority != 1 {
|
||||||
|
t.Errorf("expected priority 1, got %d", issue.Priority)
|
||||||
|
}
|
||||||
|
if issue.IssueType != types.TypeBug {
|
||||||
|
t.Errorf("expected type bug, got %s", issue.IssueType)
|
||||||
|
}
|
||||||
|
if issue.Status != types.StatusOpen {
|
||||||
|
t.Errorf("expected status open, got %s", issue.Status)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("WithDescription", func(t *testing.T) {
|
||||||
|
fv := &createFormValues{
|
||||||
|
Title: "Issue with description",
|
||||||
|
Description: "This is a detailed description",
|
||||||
|
Priority: 2,
|
||||||
|
IssueType: "task",
|
||||||
|
}
|
||||||
|
|
||||||
|
issue, err := CreateIssueFromFormValues(ctx, s, fv, "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create issue: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if issue.Description != "This is a detailed description" {
|
||||||
|
t.Errorf("expected description, got %q", issue.Description)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("WithDesignAndAcceptance", func(t *testing.T) {
|
||||||
|
fv := &createFormValues{
|
||||||
|
Title: "Feature with design",
|
||||||
|
Design: "Use MVC pattern",
|
||||||
|
AcceptanceCriteria: "All tests pass",
|
||||||
|
IssueType: "feature",
|
||||||
|
Priority: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
issue, err := CreateIssueFromFormValues(ctx, s, fv, "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create issue: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if issue.Design != "Use MVC pattern" {
|
||||||
|
t.Errorf("expected design, got %q", issue.Design)
|
||||||
|
}
|
||||||
|
if issue.AcceptanceCriteria != "All tests pass" {
|
||||||
|
t.Errorf("expected acceptance criteria, got %q", issue.AcceptanceCriteria)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("WithAssignee", func(t *testing.T) {
|
||||||
|
fv := &createFormValues{
|
||||||
|
Title: "Assigned issue",
|
||||||
|
Assignee: "alice",
|
||||||
|
Priority: 1,
|
||||||
|
IssueType: "task",
|
||||||
|
}
|
||||||
|
|
||||||
|
issue, err := CreateIssueFromFormValues(ctx, s, fv, "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create issue: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if issue.Assignee != "alice" {
|
||||||
|
t.Errorf("expected assignee 'alice', got %q", issue.Assignee)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("WithExternalRef", func(t *testing.T) {
|
||||||
|
fv := &createFormValues{
|
||||||
|
Title: "Issue with external ref",
|
||||||
|
ExternalRef: "gh-123",
|
||||||
|
Priority: 2,
|
||||||
|
IssueType: "bug",
|
||||||
|
}
|
||||||
|
|
||||||
|
issue, err := CreateIssueFromFormValues(ctx, s, fv, "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create issue: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if issue.ExternalRef == nil {
|
||||||
|
t.Fatal("expected external ref to be set")
|
||||||
|
}
|
||||||
|
if *issue.ExternalRef != "gh-123" {
|
||||||
|
t.Errorf("expected external ref 'gh-123', got %q", *issue.ExternalRef)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("WithLabels", func(t *testing.T) {
|
||||||
|
fv := &createFormValues{
|
||||||
|
Title: "Issue with labels",
|
||||||
|
Priority: 0,
|
||||||
|
IssueType: "bug",
|
||||||
|
Labels: []string{"bug", "critical"},
|
||||||
|
}
|
||||||
|
|
||||||
|
issue, err := CreateIssueFromFormValues(ctx, s, fv, "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create issue: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
labels, err := s.GetLabels(ctx, issue.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get labels: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(labels) != 2 {
|
||||||
|
t.Errorf("expected 2 labels, got %d", len(labels))
|
||||||
|
}
|
||||||
|
|
||||||
|
labelMap := make(map[string]bool)
|
||||||
|
for _, l := range labels {
|
||||||
|
labelMap[l] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !labelMap["bug"] || !labelMap["critical"] {
|
||||||
|
t.Errorf("expected labels 'bug' and 'critical', got %v", labels)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("WithDependencies", func(t *testing.T) {
|
||||||
|
// Create a parent issue first
|
||||||
|
parentFv := &createFormValues{
|
||||||
|
Title: "Parent issue for deps",
|
||||||
|
Priority: 1,
|
||||||
|
IssueType: "task",
|
||||||
|
}
|
||||||
|
parent, err := CreateIssueFromFormValues(ctx, s, parentFv, "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create parent: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create child with dependency
|
||||||
|
childFv := &createFormValues{
|
||||||
|
Title: "Child issue",
|
||||||
|
Priority: 1,
|
||||||
|
IssueType: "task",
|
||||||
|
Dependencies: []string{parent.ID}, // Default blocks type
|
||||||
|
}
|
||||||
|
child, err := CreateIssueFromFormValues(ctx, s, childFv, "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create child: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deps, err := s.GetDependencies(ctx, child.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get dependencies: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(deps) == 0 {
|
||||||
|
t.Fatal("expected at least 1 dependency, got 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, d := range deps {
|
||||||
|
if d.ID == parent.ID {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
t.Errorf("expected dependency on %s, not found", parent.ID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("WithTypedDependencies", func(t *testing.T) {
|
||||||
|
// Create a parent issue
|
||||||
|
parentFv := &createFormValues{
|
||||||
|
Title: "Related parent",
|
||||||
|
Priority: 1,
|
||||||
|
IssueType: "task",
|
||||||
|
}
|
||||||
|
parent, err := CreateIssueFromFormValues(ctx, s, parentFv, "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create parent: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create child with typed dependency
|
||||||
|
childFv := &createFormValues{
|
||||||
|
Title: "Child with typed dep",
|
||||||
|
Priority: 1,
|
||||||
|
IssueType: "bug",
|
||||||
|
Dependencies: []string{"discovered-from:" + parent.ID},
|
||||||
|
}
|
||||||
|
child, err := CreateIssueFromFormValues(ctx, s, childFv, "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create child: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deps, err := s.GetDependencies(ctx, child.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get dependencies: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(deps) == 0 {
|
||||||
|
t.Fatal("expected at least 1 dependency, got 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, d := range deps {
|
||||||
|
if d.ID == parent.ID {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
t.Errorf("expected dependency on %s, not found", parent.ID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("AllIssueTypes", func(t *testing.T) {
|
||||||
|
issueTypes := []string{"bug", "feature", "task", "epic", "chore"}
|
||||||
|
expectedTypes := []types.IssueType{
|
||||||
|
types.TypeBug,
|
||||||
|
types.TypeFeature,
|
||||||
|
types.TypeTask,
|
||||||
|
types.TypeEpic,
|
||||||
|
types.TypeChore,
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, issueType := range issueTypes {
|
||||||
|
fv := &createFormValues{
|
||||||
|
Title: "Test " + issueType,
|
||||||
|
IssueType: issueType,
|
||||||
|
Priority: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
issue, err := CreateIssueFromFormValues(ctx, s, fv, "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create issue type %s: %v", issueType, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if issue.IssueType != expectedTypes[i] {
|
||||||
|
t.Errorf("expected type %s, got %s", expectedTypes[i], issue.IssueType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MultipleDependencies", func(t *testing.T) {
|
||||||
|
// Create two parent issues
|
||||||
|
parent1Fv := &createFormValues{
|
||||||
|
Title: "Multi-dep Parent 1",
|
||||||
|
Priority: 1,
|
||||||
|
IssueType: "task",
|
||||||
|
}
|
||||||
|
parent1, err := CreateIssueFromFormValues(ctx, s, parent1Fv, "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create parent1: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parent2Fv := &createFormValues{
|
||||||
|
Title: "Multi-dep Parent 2",
|
||||||
|
Priority: 1,
|
||||||
|
IssueType: "task",
|
||||||
|
}
|
||||||
|
parent2, err := CreateIssueFromFormValues(ctx, s, parent2Fv, "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create parent2: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create child with multiple dependencies
|
||||||
|
childFv := &createFormValues{
|
||||||
|
Title: "Multi-dep Child",
|
||||||
|
Priority: 1,
|
||||||
|
IssueType: "task",
|
||||||
|
Dependencies: []string{"blocks:" + parent1.ID, "related:" + parent2.ID},
|
||||||
|
}
|
||||||
|
child, err := CreateIssueFromFormValues(ctx, s, childFv, "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create child: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deps, err := s.GetDependencies(ctx, child.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get dependencies: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(deps) < 2 {
|
||||||
|
t.Fatalf("expected at least 2 dependencies, got %d", len(deps))
|
||||||
|
}
|
||||||
|
|
||||||
|
foundParents := make(map[string]bool)
|
||||||
|
for _, d := range deps {
|
||||||
|
if d.ID == parent1.ID || d.ID == parent2.ID {
|
||||||
|
foundParents[d.ID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(foundParents) != 2 {
|
||||||
|
t.Errorf("expected to find both parent dependencies, found %d", len(foundParents))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("DiscoveredFromInheritsSourceRepo", func(t *testing.T) {
|
||||||
|
// Create a parent issue with a custom source_repo
|
||||||
|
parent := &types.Issue{
|
||||||
|
Title: "Parent with source repo",
|
||||||
|
Priority: 1,
|
||||||
|
Status: types.StatusOpen,
|
||||||
|
IssueType: types.TypeTask,
|
||||||
|
SourceRepo: "/path/to/custom/repo",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.CreateIssue(ctx, parent, "test"); err != nil {
|
||||||
|
t.Fatalf("failed to create parent: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a discovered issue with discovered-from dependency
|
||||||
|
childFv := &createFormValues{
|
||||||
|
Title: "Discovered bug",
|
||||||
|
Priority: 1,
|
||||||
|
IssueType: "bug",
|
||||||
|
Dependencies: []string{"discovered-from:" + parent.ID},
|
||||||
|
}
|
||||||
|
child, err := CreateIssueFromFormValues(ctx, s, childFv, "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create discovered issue: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the discovered issue inherited the source_repo
|
||||||
|
retrievedIssue, err := s.GetIssue(ctx, child.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get discovered issue: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if retrievedIssue.SourceRepo != parent.SourceRepo {
|
||||||
|
t.Errorf("expected source_repo %q, got %q", parent.SourceRepo, retrievedIssue.SourceRepo)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("AllPriorities", func(t *testing.T) {
|
||||||
|
for priority := 0; priority <= 4; priority++ {
|
||||||
|
fv := &createFormValues{
|
||||||
|
Title: "Priority test",
|
||||||
|
IssueType: "task",
|
||||||
|
Priority: priority,
|
||||||
|
}
|
||||||
|
|
||||||
|
issue, err := CreateIssueFromFormValues(ctx, s, fv, "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create issue with priority %d: %v", priority, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if issue.Priority != priority {
|
||||||
|
t.Errorf("expected priority %d, got %d", priority, issue.Priority)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormValuesIntegration(t *testing.T) {
|
||||||
|
// Test the full flow: parseCreateFormInput -> CreateIssueFromFormValues
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
testDB := filepath.Join(tmpDir, ".beads", "beads.db")
|
||||||
|
s := newTestStore(t, testDB)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
t.Run("FullFlow", func(t *testing.T) {
|
||||||
|
// Simulate form input
|
||||||
|
fv := parseCreateFormInput(&createFormRawInput{
|
||||||
|
Title: "Integration Test Issue",
|
||||||
|
Description: "Testing the full flow from form to storage",
|
||||||
|
IssueType: "feature",
|
||||||
|
Priority: "1",
|
||||||
|
Assignee: "test-user",
|
||||||
|
Labels: "integration, test",
|
||||||
|
Design: "Design notes here",
|
||||||
|
Acceptance: "Should work end to end",
|
||||||
|
ExternalRef: "gh-999",
|
||||||
|
})
|
||||||
|
|
||||||
|
issue, err := CreateIssueFromFormValues(ctx, s, fv, "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create issue: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify issue was stored
|
||||||
|
retrieved, err := s.GetIssue(ctx, issue.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to retrieve issue: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if retrieved.Title != "Integration Test Issue" {
|
||||||
|
t.Errorf("unexpected title: %q", retrieved.Title)
|
||||||
|
}
|
||||||
|
if retrieved.Description != "Testing the full flow from form to storage" {
|
||||||
|
t.Errorf("unexpected description: %q", retrieved.Description)
|
||||||
|
}
|
||||||
|
if retrieved.IssueType != types.TypeFeature {
|
||||||
|
t.Errorf("unexpected type: %s", retrieved.IssueType)
|
||||||
|
}
|
||||||
|
if retrieved.Priority != 1 {
|
||||||
|
t.Errorf("unexpected priority: %d", retrieved.Priority)
|
||||||
|
}
|
||||||
|
if retrieved.Assignee != "test-user" {
|
||||||
|
t.Errorf("unexpected assignee: %q", retrieved.Assignee)
|
||||||
|
}
|
||||||
|
if retrieved.Design != "Design notes here" {
|
||||||
|
t.Errorf("unexpected design: %q", retrieved.Design)
|
||||||
|
}
|
||||||
|
if retrieved.AcceptanceCriteria != "Should work end to end" {
|
||||||
|
t.Errorf("unexpected acceptance criteria: %q", retrieved.AcceptanceCriteria)
|
||||||
|
}
|
||||||
|
if retrieved.ExternalRef == nil || *retrieved.ExternalRef != "gh-999" {
|
||||||
|
t.Errorf("unexpected external ref: %v", retrieved.ExternalRef)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check labels
|
||||||
|
labels, err := s.GetLabels(ctx, issue.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get labels: %v", err)
|
||||||
|
}
|
||||||
|
if len(labels) != 2 {
|
||||||
|
t.Errorf("expected 2 labels, got %d", len(labels))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user