Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ jobs:
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5, 6, 7, 8]
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
steps:
- uses: actions/checkout@v6

Expand Down Expand Up @@ -255,7 +255,7 @@ jobs:
env:
BEADS_TEST_EMBEDDED_DOLT: "1"
BEADS_TEST_BD_BINARY: /tmp/bd-embedded-test
run: bash .github/scripts/embedded-test-shard.sh ${{ matrix.shard }} 8
run: bash .github/scripts/embedded-test-shard.sh ${{ matrix.shard }} 20

# Windows smoke tests only - full test suite times out (see bd-bmev)
# Linux/macOS run comprehensive tests; Windows just verifies binary works
Expand Down
7 changes: 7 additions & 0 deletions cmd/bd/ado.go
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,13 @@ func runADOSync(cmd *cobra.Command, _ []string) error {
_, _ = fmt.Fprintln(out, "Run without --dry-run to apply changes")
}

// Embedded mode: flush Dolt commit after sync writes.
if isEmbeddedDolt && !adoSyncDryRun && store != nil {
if _, commitErr := store.CommitPending(rootCtx, actor); commitErr != nil {
return fmt.Errorf("failed to commit: %w", commitErr)
}
}

return nil
}

Expand Down
124 changes: 124 additions & 0 deletions cmd/bd/blocked_embedded_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//go:build embeddeddolt

package main

import (
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
"sync"
"testing"
)

func TestEmbeddedBlocked(t *testing.T) {
if os.Getenv("BEADS_TEST_EMBEDDED_DOLT") != "1" {
t.Skip("set BEADS_TEST_EMBEDDED_DOLT=1 to run embedded dolt integration tests")
}
t.Parallel()

bd := buildEmbeddedBD(t)
dir, _, _ := bdInit(t, bd, "--prefix", "bl")

// ===== Default Empty =====

t.Run("blocked_default_empty", func(t *testing.T) {
cmd := exec.Command(bd, "blocked")
cmd.Dir = dir
cmd.Env = bdEnv(dir)
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("bd blocked failed: %v\n%s", err, out)
}
// No blocked issues on fresh db
_ = out
})

// ===== With Blocked Issue =====

t.Run("blocked_with_issue", func(t *testing.T) {
blocker := bdCreate(t, bd, dir, "Blocker for blocked test", "--type", "task")
blocked := bdCreate(t, bd, dir, "I am blocked", "--type", "task")

// blocked depends on blocker (blocker blocks blocked)
cmd := exec.Command(bd, "dep", "add", blocked.ID, blocker.ID)
cmd.Dir = dir
cmd.Env = bdEnv(dir)
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("dep add failed: %v\n%s", err, out)
}

cmd = exec.Command(bd, "blocked")
cmd.Dir = dir
cmd.Env = bdEnv(dir)
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("bd blocked failed: %v\n%s", err, out)
}
if !strings.Contains(string(out), blocked.ID) {
t.Errorf("expected %s in blocked output: %s", blocked.ID, out)
}
})

// ===== --json =====

t.Run("blocked_json", func(t *testing.T) {
cmd := exec.Command(bd, "blocked", "--json")
cmd.Dir = dir
cmd.Env = bdEnv(dir)
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("bd blocked --json failed: %v\n%s", err, out)
}
s := strings.TrimSpace(string(out))
start := strings.IndexAny(s, "[{")
if start >= 0 {
if !json.Valid([]byte(s[start:])) {
t.Errorf("invalid JSON in blocked output: %s", s[:min(200, len(s))])
}
}
Comment on lines +74 to +81
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the --json subtest, the test only validates JSON if it finds a '{' or '['. With --json, lack of any JSON should fail the test (as done in other embedded tests). Consider asserting start >= 0 (or specifically requiring an array/object) before calling json.Valid.

Copilot generated this review using guidance from repository custom instructions.
})
}

func TestEmbeddedBlockedConcurrent(t *testing.T) {
if os.Getenv("BEADS_TEST_EMBEDDED_DOLT") != "1" {
t.Skip("set BEADS_TEST_EMBEDDED_DOLT=1 to run embedded dolt integration tests")
}
t.Parallel()

bd := buildEmbeddedBD(t)
dir, _, _ := bdInit(t, bd, "--prefix", "bx")

bdCreate(t, bd, dir, "Blocked concurrent issue", "--type", "task")

const numWorkers = 8
type workerResult struct {
worker int
err error
}
results := make([]workerResult, numWorkers)
var wg sync.WaitGroup
wg.Add(numWorkers)

for w := 0; w < numWorkers; w++ {
go func(worker int) {
defer wg.Done()
r := workerResult{worker: worker}
cmd := exec.Command(bd, "blocked")
cmd.Dir = dir
cmd.Env = bdEnv(dir)
out, err := cmd.CombinedOutput()
if err != nil {
r.err = fmt.Errorf("blocked (worker %d): %v\n%s", worker, err, out)
}
results[worker] = r
}(w)
}
wg.Wait()
for _, r := range results {
if r.err != nil {
t.Errorf("worker %d failed: %v", r.worker, r.err)
}
}
}
7 changes: 7 additions & 0 deletions cmd/bd/defer.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@ Examples:
if jsonOutput && len(deferredIssues) > 0 {
outputJSON(deferredIssues)
}

// Embedded mode: flush Dolt commit.
if isEmbeddedDolt && len(args) > 0 && store != nil {
if _, err := store.CommitPending(ctx, actor); err != nil {
FatalError("failed to commit: %v", err)
}
}
},
}

Expand Down
202 changes: 202 additions & 0 deletions cmd/bd/defer_embedded_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
//go:build embeddeddolt

package main

import (
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
"sync"
"testing"
)

// bdDefer runs "bd defer" with the given args and returns stdout.
func bdDefer(t *testing.T, bd, dir string, args ...string) string {
t.Helper()
fullArgs := append([]string{"defer"}, args...)
cmd := exec.Command(bd, fullArgs...)
cmd.Dir = dir
cmd.Env = bdEnv(dir)
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("bd defer %s failed: %v\n%s", strings.Join(args, " "), err, out)
}
return string(out)
}

// bdUndefer runs "bd undefer" with the given args and returns stdout.
func bdUndefer(t *testing.T, bd, dir string, args ...string) string {
t.Helper()
fullArgs := append([]string{"undefer"}, args...)
cmd := exec.Command(bd, fullArgs...)
cmd.Dir = dir
cmd.Env = bdEnv(dir)
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("bd undefer %s failed: %v\n%s", strings.Join(args, " "), err, out)
}
return string(out)
}

// getIssueStatus returns the status of an issue via bd show --json.
func getIssueStatus(t *testing.T, bd, dir, id string) string {
t.Helper()
cmd := exec.Command(bd, "show", id, "--json")
cmd.Dir = dir
cmd.Env = bdEnv(dir)
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("bd show %s --json failed: %v\n%s", id, err, out)
}
s := strings.TrimSpace(string(out))
// show --json may return an array or object
start := strings.IndexAny(s, "[{")
if start < 0 {
t.Fatalf("no JSON in show output: %s", s)
}
if s[start] == '[' {
var arr []map[string]interface{}
if err := json.Unmarshal([]byte(s[start:]), &arr); err != nil {
t.Fatalf("parse show JSON array: %v\n%s", err, s)
}
if len(arr) == 0 {
t.Fatalf("empty JSON array in show output")
}
status, _ := arr[0]["status"].(string)
return status
}
var m map[string]interface{}
if err := json.Unmarshal([]byte(s[start:]), &m); err != nil {
t.Fatalf("parse show JSON: %v\n%s", err, s)
}
status, _ := m["status"].(string)
return status
}

func TestEmbeddedDefer(t *testing.T) {
if os.Getenv("BEADS_TEST_EMBEDDED_DOLT") != "1" {
t.Skip("set BEADS_TEST_EMBEDDED_DOLT=1 to run embedded dolt integration tests")
}
t.Parallel()

bd := buildEmbeddedBD(t)
dir, _, _ := bdInit(t, bd, "--prefix", "df")

// ===== Single Issue =====

t.Run("defer_single", func(t *testing.T) {
issue := bdCreate(t, bd, dir, "Defer single test", "--type", "task")
out := bdDefer(t, bd, dir, issue.ID)
if !strings.Contains(out, "Deferred") {
t.Errorf("expected 'Deferred' in output: %s", out)
}
status := getIssueStatus(t, bd, dir, issue.ID)
if status != "deferred" {
t.Errorf("expected status=deferred, got %q", status)
}
})

// ===== Multiple Issues =====

t.Run("defer_multiple", func(t *testing.T) {
issue1 := bdCreate(t, bd, dir, "Defer multi 1", "--type", "task")
issue2 := bdCreate(t, bd, dir, "Defer multi 2", "--type", "task")
out := bdDefer(t, bd, dir, issue1.ID, issue2.ID)
if !strings.Contains(out, issue1.ID) || !strings.Contains(out, issue2.ID) {
t.Errorf("expected both IDs in output: %s", out)
}
for _, id := range []string{issue1.ID, issue2.ID} {
status := getIssueStatus(t, bd, dir, id)
if status != "deferred" {
t.Errorf("expected %s status=deferred, got %q", id, status)
}
}
})

// ===== With --until =====

t.Run("defer_until", func(t *testing.T) {
issue := bdCreate(t, bd, dir, "Defer until test", "--type", "task")
out := bdDefer(t, bd, dir, issue.ID, "--until", "+1h")
if !strings.Contains(out, "Deferred") {
t.Errorf("expected 'Deferred' in output: %s", out)
}
status := getIssueStatus(t, bd, dir, issue.ID)
if status != "deferred" {
t.Errorf("expected status=deferred, got %q", status)
}
})

// ===== Already Deferred =====

t.Run("defer_already_deferred", func(t *testing.T) {
issue := bdCreate(t, bd, dir, "Defer idempotent", "--type", "task")
bdDefer(t, bd, dir, issue.ID)
// Defer again — should succeed
out := bdDefer(t, bd, dir, issue.ID)
if !strings.Contains(out, "Deferred") {
t.Errorf("expected 'Deferred' on second defer: %s", out)
}
})
}

// TestEmbeddedDeferConcurrent exercises defer operations concurrently.
func TestEmbeddedDeferConcurrent(t *testing.T) {
if os.Getenv("BEADS_TEST_EMBEDDED_DOLT") != "1" {
t.Skip("set BEADS_TEST_EMBEDDED_DOLT=1 to run embedded dolt integration tests")
}
t.Parallel()

bd := buildEmbeddedBD(t)
dir, _, _ := bdInit(t, bd, "--prefix", "dx")

// Pre-create issues
var issueIDs []string
for i := 0; i < 8; i++ {
issue := bdCreate(t, bd, dir, fmt.Sprintf("defer-concurrent-%d", i), "--type", "task")
issueIDs = append(issueIDs, issue.ID)
}

const numWorkers = 8
type workerResult struct {
worker int
err error
}
results := make([]workerResult, numWorkers)
var wg sync.WaitGroup
wg.Add(numWorkers)

for w := 0; w < numWorkers; w++ {
go func(worker int) {
defer wg.Done()
r := workerResult{worker: worker}
id := issueIDs[worker]

cmd := exec.Command(bd, "defer", id)
cmd.Dir = dir
cmd.Env = bdEnv(dir)
out, err := cmd.CombinedOutput()
if err != nil {
r.err = fmt.Errorf("defer %s (worker %d): %v\n%s", id, worker, err, out)
}
results[worker] = r
}(w)
}
wg.Wait()

for _, r := range results {
if r.err != nil {
t.Errorf("worker %d failed: %v", r.worker, r.err)
}
}

// Verify all are deferred
for i, id := range issueIDs {
status := getIssueStatus(t, bd, dir, id)
if status != "deferred" {
t.Errorf("issue %d (%s): expected status=deferred, got %q", i, id, status)
}
}
}
Loading
Loading