forge-workflow 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/dev.md +6 -1
- package/.claude/commands/plan.md +59 -14
- package/.claude/commands/premerge.md +10 -0
- package/.claude/commands/review.md +7 -1
- package/.claude/commands/ship.md +95 -47
- package/.claude/commands/status.md +42 -0
- package/.claude/commands/validate.md +7 -1
- package/.claude/commands/verify.md +52 -4
- package/.claude/rules/workflow.md +16 -0
- package/.claude/scripts/greptile-resolve.sh +32 -0
- package/.cline/workflows/dev.md +6 -1
- package/.cline/workflows/plan.md +59 -14
- package/.cline/workflows/premerge.md +10 -0
- package/.cline/workflows/review.md +7 -1
- package/.cline/workflows/ship.md +95 -47
- package/.cline/workflows/status.md +42 -0
- package/.cline/workflows/validate.md +7 -1
- package/.cline/workflows/verify.md +52 -4
- package/.codex/skills/dev/SKILL.md +6 -1
- package/.codex/skills/plan/SKILL.md +59 -14
- package/.codex/skills/premerge/SKILL.md +10 -0
- package/.codex/skills/review/SKILL.md +7 -1
- package/.codex/skills/ship/SKILL.md +95 -47
- package/.codex/skills/status/SKILL.md +42 -0
- package/.codex/skills/validate/SKILL.md +7 -1
- package/.codex/skills/verify/SKILL.md +52 -4
- package/.cursor/commands/dev.md +6 -1
- package/.cursor/commands/plan.md +59 -14
- package/.cursor/commands/premerge.md +10 -0
- package/.cursor/commands/review.md +7 -1
- package/.cursor/commands/ship.md +95 -47
- package/.cursor/commands/status.md +42 -0
- package/.cursor/commands/validate.md +7 -1
- package/.cursor/commands/verify.md +52 -4
- package/.cursorrules +149 -0
- package/.github/prompts/dev.prompt.md +6 -1
- package/.github/prompts/plan.prompt.md +59 -14
- package/.github/prompts/premerge.prompt.md +10 -0
- package/.github/prompts/review.prompt.md +7 -1
- package/.github/prompts/ship.prompt.md +95 -47
- package/.github/prompts/status.prompt.md +42 -0
- package/.github/prompts/validate.prompt.md +7 -1
- package/.github/prompts/verify.prompt.md +52 -4
- package/.kilocode/workflows/dev.md +6 -1
- package/.kilocode/workflows/plan.md +59 -14
- package/.kilocode/workflows/premerge.md +10 -0
- package/.kilocode/workflows/review.md +7 -1
- package/.kilocode/workflows/ship.md +95 -47
- package/.kilocode/workflows/status.md +42 -0
- package/.kilocode/workflows/validate.md +7 -1
- package/.kilocode/workflows/verify.md +52 -4
- package/.opencode/commands/dev.md +6 -1
- package/.opencode/commands/plan.md +59 -14
- package/.opencode/commands/premerge.md +10 -0
- package/.opencode/commands/review.md +7 -1
- package/.opencode/commands/ship.md +95 -47
- package/.opencode/commands/status.md +42 -0
- package/.opencode/commands/validate.md +7 -1
- package/.opencode/commands/verify.md +52 -4
- package/.roo/commands/dev.md +6 -1
- package/.roo/commands/plan.md +59 -14
- package/.roo/commands/premerge.md +10 -0
- package/.roo/commands/review.md +7 -1
- package/.roo/commands/ship.md +95 -47
- package/.roo/commands/status.md +42 -0
- package/.roo/commands/validate.md +7 -1
- package/.roo/commands/verify.md +52 -4
- package/AGENTS.md +97 -0
- package/CLAUDE.md +10 -0
- package/README.md +2 -2
- package/bin/forge-cmd.js +5 -1
- package/bin/forge-preflight.js +15 -2
- package/bin/forge.js +211 -9
- package/docs/ENHANCED_ONBOARDING.md +96 -86
- package/docs/ROADMAP.md +2 -2
- package/docs/TOOLCHAIN.md +23 -0
- package/docs/VALIDATION.md +1 -1
- package/lefthook.yml +11 -0
- package/lib/agents/README.md +46 -1
- package/lib/agents/cline.plugin.json +11 -4
- package/lib/agents/codex.plugin.json +2 -2
- package/lib/agents/copilot.plugin.json +5 -5
- package/lib/agents/cursor.plugin.json +1 -1
- package/lib/agents/kilocode.plugin.json +1 -1
- package/lib/agents/opencode.plugin.json +7 -4
- package/lib/agents/roo.plugin.json +10 -3
- package/lib/agents-config.js +129 -81
- package/lib/codex-skills.js +50 -0
- package/lib/commands/_registry.js +173 -0
- package/lib/commands/clean.js +181 -0
- package/lib/commands/commands-reset.js +147 -0
- package/lib/commands/dev.js +84 -0
- package/lib/commands/plan.js +18 -0
- package/lib/commands/push.js +196 -0
- package/lib/commands/recommend.js +1 -1
- package/lib/commands/setup.js +4295 -0
- package/lib/commands/ship.js +20 -0
- package/lib/commands/status.js +210 -44
- package/lib/commands/sync.js +71 -0
- package/lib/commands/team.js +37 -0
- package/lib/commands/test.js +207 -0
- package/lib/commands/validate.js +13 -0
- package/lib/commands/worktree.js +310 -0
- package/lib/detect-agent.js +38 -8
- package/lib/detection-utils.js +405 -0
- package/lib/docs-command.js +51 -0
- package/lib/docs-copy.js +50 -0
- package/lib/file-utils.js +260 -0
- package/lib/forge-context.js +42 -0
- package/lib/freshness-token.js +148 -0
- package/lib/frontmatter.js +79 -0
- package/lib/greptile-match.js +80 -0
- package/lib/husky-migration.js +113 -12
- package/lib/lefthook-check.js +27 -6
- package/lib/plugin-manager.js +225 -72
- package/lib/project-discovery.js +39 -5
- package/lib/reset.js +309 -0
- package/lib/runtime-health.js +305 -0
- package/lib/shell-utils.js +50 -0
- package/lib/task-ownership.js +117 -0
- package/lib/ui-utils.js +43 -0
- package/lib/validation-utils.js +163 -0
- package/lib/workflow/enforce-stage.js +179 -0
- package/lib/workflow/stages.js +201 -0
- package/lib/workflow/state.js +332 -0
- package/opencode.json +67 -0
- package/package.json +16 -6
- package/scripts/beads-context.sh +165 -22
- package/scripts/beads-context.test.js +5 -1
- package/scripts/check-agents.js +103 -0
- package/scripts/check-forge-token.js +98 -0
- package/scripts/conflict-detect.sh +2 -2
- package/scripts/dep-guard.sh +6 -28
- package/scripts/file-index.sh +117 -23
- package/scripts/forge-team/index.sh +86 -0
- package/scripts/forge-team/lib/agent-prompt.sh +52 -0
- package/scripts/forge-team/lib/claim.sh +256 -0
- package/scripts/forge-team/lib/dashboard.sh +341 -0
- package/scripts/forge-team/lib/epic.sh +332 -0
- package/scripts/forge-team/lib/hooks.sh +253 -0
- package/scripts/forge-team/lib/identity.sh +235 -0
- package/scripts/forge-team/lib/sync-github.sh +317 -0
- package/scripts/forge-team/lib/verify.sh +284 -0
- package/scripts/forge-team/lib/workload.sh +296 -0
- package/scripts/forge-team/tests/agent-prompt.test.sh +72 -0
- package/scripts/forge-team/tests/claim.test.sh +179 -0
- package/scripts/forge-team/tests/dashboard.test.sh +170 -0
- package/scripts/forge-team/tests/dispatcher.test.sh +79 -0
- package/scripts/forge-team/tests/epic.test.sh +176 -0
- package/scripts/forge-team/tests/hooks.test.sh +239 -0
- package/scripts/forge-team/tests/identity.test.sh +176 -0
- package/scripts/forge-team/tests/integration.test.sh +371 -0
- package/scripts/forge-team/tests/sync-github.test.sh +209 -0
- package/scripts/forge-team/tests/verify.test.sh +314 -0
- package/scripts/forge-team/tests/workflow-integration.test.sh +43 -0
- package/scripts/forge-team/tests/workload.test.sh +209 -0
- package/scripts/lib/eval-runner.js +39 -0
- package/scripts/lib/jsonl-lock.sh +48 -0
- package/scripts/lib/sanitize.sh +116 -0
- package/scripts/pr-coordinator.sh +756 -0
- package/scripts/smart-status.sh +58 -21
- package/scripts/sync-commands.js +49 -20
- package/scripts/sync-utils.sh +24 -29
- package/scripts/test.js +18 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
|
|
6
|
+
# ── Setup temp dir for test isolation ────────────────────────────────────
|
|
7
|
+
TEST_TMP="$(mktemp -d)"
|
|
8
|
+
trap 'rm -rf "$TEST_TMP"' EXIT
|
|
9
|
+
|
|
10
|
+
# ── Create mock bd ───────────────────────────────────────────────────────
|
|
11
|
+
mock_dir="$(mktemp -d)"
|
|
12
|
+
|
|
13
|
+
cat > "$mock_dir/bd" << 'MOCK'
|
|
14
|
+
#!/usr/bin/env bash
|
|
15
|
+
case "$1" in
|
|
16
|
+
list)
|
|
17
|
+
if echo "$*" | grep -q -- "--status=open,in_progress"; then
|
|
18
|
+
if [[ -n "${BD_MOCK_EMPTY:-}" ]]; then
|
|
19
|
+
# No output for empty test
|
|
20
|
+
:
|
|
21
|
+
elif [[ -n "${BD_MOCK_SINGLE:-}" ]]; then
|
|
22
|
+
echo "◐ forge-aaa · Feature A"
|
|
23
|
+
else
|
|
24
|
+
echo "◐ forge-aaa · Feature A"
|
|
25
|
+
echo "○ forge-bbb · Feature B"
|
|
26
|
+
echo "◐ forge-ccc · Feature C"
|
|
27
|
+
fi
|
|
28
|
+
fi
|
|
29
|
+
;;
|
|
30
|
+
show)
|
|
31
|
+
case "$2" in
|
|
32
|
+
forge-aaa)
|
|
33
|
+
echo "◐ forge-aaa · Feature A [● P2 · IN_PROGRESS]"
|
|
34
|
+
echo "Owner: devone"
|
|
35
|
+
echo "Updated: 2026-03-27T10:00:00Z"
|
|
36
|
+
;;
|
|
37
|
+
forge-bbb)
|
|
38
|
+
echo "○ forge-bbb · Feature B [● P2 · OPEN]"
|
|
39
|
+
echo "Owner: devtwo"
|
|
40
|
+
echo "Updated: 2026-03-24T10:00:00Z"
|
|
41
|
+
;;
|
|
42
|
+
forge-ccc)
|
|
43
|
+
echo "◐ forge-ccc · Feature C [● P1 · IN_PROGRESS]"
|
|
44
|
+
echo "Owner: devone"
|
|
45
|
+
echo "Updated: 2026-03-27T09:00:00Z"
|
|
46
|
+
echo ""
|
|
47
|
+
echo "DEPENDS ON"
|
|
48
|
+
echo " → ◐ forge-aaa: Feature A"
|
|
49
|
+
;;
|
|
50
|
+
esac
|
|
51
|
+
;;
|
|
52
|
+
esac
|
|
53
|
+
MOCK
|
|
54
|
+
chmod +x "$mock_dir/bd"
|
|
55
|
+
|
|
56
|
+
export BD_CMD="$mock_dir/bd"
|
|
57
|
+
|
|
58
|
+
# Source the library under test
|
|
59
|
+
source "$SCRIPT_DIR/lib/dashboard.sh"
|
|
60
|
+
|
|
61
|
+
PASS=0
|
|
62
|
+
FAIL=0
|
|
63
|
+
|
|
64
|
+
assert_eq() {
|
|
65
|
+
local label="$1" expected="$2" actual="$3"
|
|
66
|
+
if [[ "$actual" == "$expected" ]]; then
|
|
67
|
+
PASS=$((PASS + 1)); echo " PASS: $label"
|
|
68
|
+
else
|
|
69
|
+
FAIL=$((FAIL + 1)); echo " FAIL: $label (expected '$expected', got '$actual')"
|
|
70
|
+
fi
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
assert_contains() {
|
|
74
|
+
local label="$1" expected="$2" actual="$3"
|
|
75
|
+
if [[ "$actual" == *"$expected"* ]]; then
|
|
76
|
+
PASS=$((PASS + 1)); echo " PASS: $label"
|
|
77
|
+
else
|
|
78
|
+
FAIL=$((FAIL + 1)); echo " FAIL: $label (expected to contain '$expected', got '$actual')"
|
|
79
|
+
fi
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
assert_not_contains() {
|
|
83
|
+
local label="$1" unexpected="$2" actual="$3"
|
|
84
|
+
if [[ "$actual" != *"$unexpected"* ]]; then
|
|
85
|
+
PASS=$((PASS + 1)); echo " PASS: $label"
|
|
86
|
+
else
|
|
87
|
+
FAIL=$((FAIL + 1)); echo " FAIL: $label (expected NOT to contain '$unexpected')"
|
|
88
|
+
fi
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
assert_exit() {
|
|
92
|
+
local label="$1" expected="$2" actual="$3"
|
|
93
|
+
if [[ "$actual" -eq "$expected" ]]; then
|
|
94
|
+
PASS=$((PASS + 1)); echo " PASS: $label"
|
|
95
|
+
else
|
|
96
|
+
FAIL=$((FAIL + 1)); echo " FAIL: $label (expected exit $expected, got $actual)"
|
|
97
|
+
fi
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# ── Test 1: Full dashboard with multiple developers ──────────────────────
|
|
101
|
+
echo "── Test 1: Full dashboard with multiple developers ──"
|
|
102
|
+
|
|
103
|
+
# Override _now to a fixed time: 2026-03-27T12:00:00Z = 1774836000
|
|
104
|
+
export FORGE_NOW=1774836000
|
|
105
|
+
|
|
106
|
+
rc=0
|
|
107
|
+
output="$(cmd_dashboard 2>&1)" || rc=$?
|
|
108
|
+
assert_exit "dashboard exits 0" 0 "$rc"
|
|
109
|
+
assert_contains "shows Team Dashboard header" "Team Dashboard" "$output"
|
|
110
|
+
assert_contains "shows devone stats" "devone" "$output"
|
|
111
|
+
assert_contains "shows devtwo stats" "devtwo" "$output"
|
|
112
|
+
assert_contains "shows total summary" "Total:" "$output"
|
|
113
|
+
|
|
114
|
+
# ── Test 2: Stale issue flagged (forge-bbb >48h old) ────────────────────
|
|
115
|
+
echo ""
|
|
116
|
+
echo "── Test 2: Stale issue flagged ──"
|
|
117
|
+
|
|
118
|
+
# forge-bbb updated 2026-03-24T10:00:00Z, now is 2026-03-27T12:00:00Z => ~74h
|
|
119
|
+
assert_contains "stale section shown" "Stale assignments" "$output"
|
|
120
|
+
assert_contains "forge-bbb flagged as stale" "forge-bbb" "$output"
|
|
121
|
+
assert_contains "stale issue shows devtwo" "devtwo" "$output"
|
|
122
|
+
|
|
123
|
+
# ── Test 3: Blocked issue detected (forge-ccc depends on forge-aaa) ─────
|
|
124
|
+
echo ""
|
|
125
|
+
echo "── Test 3: Blocked issue detected ──"
|
|
126
|
+
|
|
127
|
+
assert_contains "blocked section shown" "Blocked issues" "$output"
|
|
128
|
+
assert_contains "forge-ccc flagged as blocked" "forge-ccc" "$output"
|
|
129
|
+
assert_contains "blocked shows dependency" "forge-aaa" "$output"
|
|
130
|
+
|
|
131
|
+
# ── Test 4: No issues → "No active issues" ──────────────────────────────
|
|
132
|
+
echo ""
|
|
133
|
+
echo "── Test 4: No issues → clear message ──"
|
|
134
|
+
|
|
135
|
+
export BD_MOCK_EMPTY=1
|
|
136
|
+
rc=0
|
|
137
|
+
output_empty="$(cmd_dashboard 2>&1)" || rc=$?
|
|
138
|
+
assert_exit "empty dashboard exits 0" 0 "$rc"
|
|
139
|
+
assert_contains "shows no active issues" "No active issues" "$output_empty"
|
|
140
|
+
unset BD_MOCK_EMPTY
|
|
141
|
+
|
|
142
|
+
# ── Test 5: Single developer → works correctly ──────────────────────────
|
|
143
|
+
echo ""
|
|
144
|
+
echo "── Test 5: Single developer ──"
|
|
145
|
+
|
|
146
|
+
export BD_MOCK_SINGLE=1
|
|
147
|
+
rc=0
|
|
148
|
+
output_single="$(cmd_dashboard 2>&1)" || rc=$?
|
|
149
|
+
assert_exit "single dev dashboard exits 0" 0 "$rc"
|
|
150
|
+
assert_contains "shows devone" "devone" "$output_single"
|
|
151
|
+
assert_not_contains "no devtwo in single mode" "devtwo" "$output_single"
|
|
152
|
+
unset BD_MOCK_SINGLE
|
|
153
|
+
|
|
154
|
+
# ── Test 6: --format=json → valid JSON ──────────────────────────────────
|
|
155
|
+
echo ""
|
|
156
|
+
echo "── Test 6: --format=json → valid JSON ──"
|
|
157
|
+
|
|
158
|
+
rc=0
|
|
159
|
+
output_json="$(cmd_dashboard --format=json 2>&1)" || rc=$?
|
|
160
|
+
assert_exit "json format exits 0" 0 "$rc"
|
|
161
|
+
# Check it starts with { and contains expected keys
|
|
162
|
+
assert_contains "json has developers key" "developers" "$output_json"
|
|
163
|
+
assert_contains "json has total key" "total" "$output_json"
|
|
164
|
+
assert_contains "json has stale key" "stale" "$output_json"
|
|
165
|
+
assert_contains "json has blocked key" "blocked" "$output_json"
|
|
166
|
+
|
|
167
|
+
# ── Results ──────────────────────────────────────────────────────────────
|
|
168
|
+
echo ""
|
|
169
|
+
echo "Results: $PASS passed, $FAIL failed"
|
|
170
|
+
[[ $FAIL -eq 0 ]] && exit 0 || exit 1
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Tests for forge-team CLI dispatcher (index.sh)
|
|
3
|
+
# Exit codes: 0=all pass, 1=any failure
|
|
4
|
+
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
DISPATCHER="$SCRIPT_DIR/../index.sh"
|
|
9
|
+
|
|
10
|
+
PASS=0
|
|
11
|
+
FAIL=0
|
|
12
|
+
|
|
13
|
+
assert_exit() {
|
|
14
|
+
local desc="$1" expected="$2"
|
|
15
|
+
shift 2
|
|
16
|
+
"$@" >/dev/null 2>&1
|
|
17
|
+
local actual=$?
|
|
18
|
+
if [[ "$actual" -eq "$expected" ]]; then
|
|
19
|
+
echo " PASS: $desc"
|
|
20
|
+
((PASS++))
|
|
21
|
+
else
|
|
22
|
+
echo " FAIL: $desc (expected exit=$expected, got exit=$actual)"
|
|
23
|
+
((FAIL++))
|
|
24
|
+
fi
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
assert_output_contains() {
|
|
28
|
+
local desc="$1" pattern="$2"
|
|
29
|
+
shift 2
|
|
30
|
+
local output
|
|
31
|
+
output=$("$@" 2>&1) || true
|
|
32
|
+
if echo "$output" | grep -q "$pattern"; then
|
|
33
|
+
echo " PASS: $desc"
|
|
34
|
+
((PASS++))
|
|
35
|
+
else
|
|
36
|
+
echo " FAIL: $desc (expected output containing '$pattern')"
|
|
37
|
+
((FAIL++))
|
|
38
|
+
fi
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
echo "=== forge-team dispatcher tests ==="
|
|
42
|
+
|
|
43
|
+
# Test 1: help exits 0 and shows usage
|
|
44
|
+
echo ""
|
|
45
|
+
echo "-- help subcommand --"
|
|
46
|
+
assert_exit "help exits 0" 0 bash "$DISPATCHER" help
|
|
47
|
+
assert_output_contains "help shows usage" "Usage: forge team" bash "$DISPATCHER" help
|
|
48
|
+
|
|
49
|
+
# Test 2: No args exits 1
|
|
50
|
+
echo ""
|
|
51
|
+
echo "-- no args --"
|
|
52
|
+
assert_exit "no args exits 1" 1 bash "$DISPATCHER"
|
|
53
|
+
|
|
54
|
+
# Test 3: Unknown subcommand exits 1 with error
|
|
55
|
+
echo ""
|
|
56
|
+
echo "-- unknown subcommand --"
|
|
57
|
+
assert_exit "unknown subcommand exits 1" 1 bash "$DISPATCHER" bogus
|
|
58
|
+
assert_output_contains "unknown subcommand shows error" "unknown subcommand" bash "$DISPATCHER" bogus
|
|
59
|
+
|
|
60
|
+
# Test 4: Each stub subcommand is reachable (exits 0)
|
|
61
|
+
echo ""
|
|
62
|
+
echo "-- stub subcommands reachable --"
|
|
63
|
+
for cmd in workload epic dashboard add verify sync claim; do
|
|
64
|
+
assert_exit "$cmd exits 0" 0 bash "$DISPATCHER" "$cmd"
|
|
65
|
+
assert_output_contains "$cmd outputs stub message" "not implemented" bash "$DISPATCHER" "$cmd"
|
|
66
|
+
done
|
|
67
|
+
|
|
68
|
+
# Test 5: Sources libs without error (script runs successfully)
|
|
69
|
+
echo ""
|
|
70
|
+
echo "-- sources libs gracefully --"
|
|
71
|
+
assert_exit "dispatcher sources libs without error" 0 bash "$DISPATCHER" help
|
|
72
|
+
|
|
73
|
+
echo ""
|
|
74
|
+
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
75
|
+
|
|
76
|
+
if [[ "$FAIL" -gt 0 ]]; then
|
|
77
|
+
exit 1
|
|
78
|
+
fi
|
|
79
|
+
exit 0
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
|
|
6
|
+
# ── Setup temp dir for test isolation ────────────────────────────────────
|
|
7
|
+
TEST_TMP="$(mktemp -d)"
|
|
8
|
+
trap 'rm -rf "$TEST_TMP"' EXIT
|
|
9
|
+
|
|
10
|
+
# ── Create mock bd ───────────────────────────────────────────────────────
|
|
11
|
+
mock_dir="$(mktemp -d)"
|
|
12
|
+
|
|
13
|
+
cat > "$mock_dir/bd" << 'MOCK'
|
|
14
|
+
#!/usr/bin/env bash
|
|
15
|
+
case "$1 $2" in
|
|
16
|
+
"show forge-epic1")
|
|
17
|
+
cat << 'SHOW'
|
|
18
|
+
◐ forge-epic1 · Test Epic [● P2 · IN_PROGRESS]
|
|
19
|
+
Owner: devone
|
|
20
|
+
|
|
21
|
+
BLOCKS
|
|
22
|
+
← ✓ forge-child1: Child 1 (closed)
|
|
23
|
+
← ◐ forge-child2: Child 2 (in_progress)
|
|
24
|
+
← ○ forge-child3: Child 3 (open)
|
|
25
|
+
SHOW
|
|
26
|
+
;;
|
|
27
|
+
"show forge-child1")
|
|
28
|
+
echo "✓ forge-child1 · Child 1 [● P2 · CLOSED]"
|
|
29
|
+
echo "Owner: devone"
|
|
30
|
+
;;
|
|
31
|
+
"show forge-child2")
|
|
32
|
+
echo "◐ forge-child2 · Child 2 [● P2 · IN_PROGRESS]"
|
|
33
|
+
echo "Owner: devtwo"
|
|
34
|
+
;;
|
|
35
|
+
"show forge-child3")
|
|
36
|
+
echo "○ forge-child3 · Child 3 [● P3 · OPEN]"
|
|
37
|
+
echo "Owner: devone"
|
|
38
|
+
;;
|
|
39
|
+
"show forge-empty")
|
|
40
|
+
cat << 'SHOW'
|
|
41
|
+
○ forge-empty · Empty Epic [● P2 · OPEN]
|
|
42
|
+
Owner: devone
|
|
43
|
+
SHOW
|
|
44
|
+
;;
|
|
45
|
+
"show forge-blocked")
|
|
46
|
+
cat << 'SHOW'
|
|
47
|
+
◐ forge-blocked · Blocked Epic [● P2 · IN_PROGRESS]
|
|
48
|
+
Owner: devone
|
|
49
|
+
|
|
50
|
+
BLOCKS
|
|
51
|
+
← ✓ forge-bchild1: Bchild 1 (closed)
|
|
52
|
+
← ◐ forge-bchild2: Bchild 2 (in_progress)
|
|
53
|
+
← ○ forge-bchild3: Bchild 3 (open)
|
|
54
|
+
SHOW
|
|
55
|
+
;;
|
|
56
|
+
"show forge-bchild1")
|
|
57
|
+
echo "✓ forge-bchild1 · Bchild 1 [● P2 · CLOSED]"
|
|
58
|
+
echo "Owner: devone"
|
|
59
|
+
;;
|
|
60
|
+
"show forge-bchild2")
|
|
61
|
+
cat << 'SHOW'
|
|
62
|
+
◐ forge-bchild2 · Bchild 2 [● P2 · IN_PROGRESS]
|
|
63
|
+
Owner: devtwo
|
|
64
|
+
|
|
65
|
+
BLOCKED_BY
|
|
66
|
+
→ forge-bchild1: Bchild 1
|
|
67
|
+
SHOW
|
|
68
|
+
;;
|
|
69
|
+
"show forge-bchild3")
|
|
70
|
+
echo "○ forge-bchild3 · Bchild 3 [● P3 · OPEN]"
|
|
71
|
+
echo "Owner: devone"
|
|
72
|
+
;;
|
|
73
|
+
esac
|
|
74
|
+
MOCK
|
|
75
|
+
chmod +x "$mock_dir/bd"
|
|
76
|
+
|
|
77
|
+
export BD_CMD="$mock_dir/bd"
|
|
78
|
+
|
|
79
|
+
# Source the library under test
|
|
80
|
+
source "$SCRIPT_DIR/lib/epic.sh"
|
|
81
|
+
|
|
82
|
+
PASS=0
|
|
83
|
+
FAIL=0
|
|
84
|
+
|
|
85
|
+
assert_eq() {
|
|
86
|
+
local label="$1" expected="$2" actual="$3"
|
|
87
|
+
if [[ "$actual" == "$expected" ]]; then
|
|
88
|
+
PASS=$((PASS + 1)); echo " PASS: $label"
|
|
89
|
+
else
|
|
90
|
+
FAIL=$((FAIL + 1)); echo " FAIL: $label (expected '$expected', got '$actual')"
|
|
91
|
+
fi
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
assert_contains() {
|
|
95
|
+
local label="$1" expected="$2" actual="$3"
|
|
96
|
+
if [[ "$actual" == *"$expected"* ]]; then
|
|
97
|
+
PASS=$((PASS + 1)); echo " PASS: $label"
|
|
98
|
+
else
|
|
99
|
+
FAIL=$((FAIL + 1)); echo " FAIL: $label (expected to contain '$expected', got '$actual')"
|
|
100
|
+
fi
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
assert_exit() {
|
|
104
|
+
local label="$1" expected="$2" actual="$3"
|
|
105
|
+
if [[ "$actual" -eq "$expected" ]]; then
|
|
106
|
+
PASS=$((PASS + 1)); echo " PASS: $label"
|
|
107
|
+
else
|
|
108
|
+
FAIL=$((FAIL + 1)); echo " FAIL: $label (expected exit $expected, got $actual)"
|
|
109
|
+
fi
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# ── Test 1: Epic with mixed status children → correct progress count ─────
|
|
113
|
+
echo "── Test 1: Epic with mixed status children ──"
|
|
114
|
+
rc=0
|
|
115
|
+
output="$(cmd_epic "forge-epic1" 2>&1)" || rc=$?
|
|
116
|
+
assert_exit "exits 0" 0 "$rc"
|
|
117
|
+
assert_contains "shows epic id" "forge-epic1" "$output"
|
|
118
|
+
assert_contains "progress shows 1/3" "1/3" "$output"
|
|
119
|
+
assert_contains "shows closed child marker" "forge-child1" "$output"
|
|
120
|
+
assert_contains "shows in_progress child" "forge-child2" "$output"
|
|
121
|
+
assert_contains "shows open child" "forge-child3" "$output"
|
|
122
|
+
|
|
123
|
+
# ── Test 2: Per-developer breakdown correct ──────────────────────────────
|
|
124
|
+
echo ""
|
|
125
|
+
echo "── Test 2: Per-developer breakdown correct ──"
|
|
126
|
+
assert_contains "devone breakdown" "devone" "$output"
|
|
127
|
+
assert_contains "devtwo breakdown" "devtwo" "$output"
|
|
128
|
+
|
|
129
|
+
# ── Test 3: Completion percentage correct ────────────────────────────────
|
|
130
|
+
echo ""
|
|
131
|
+
echo "── Test 3: Completion percentage correct ──"
|
|
132
|
+
assert_contains "shows 33%" "33%" "$output"
|
|
133
|
+
|
|
134
|
+
# ── Test 4: Empty epic → No child issues ─────────────────────────────────
|
|
135
|
+
echo ""
|
|
136
|
+
echo "── Test 4: Empty epic ──"
|
|
137
|
+
rc=0
|
|
138
|
+
output_empty="$(cmd_epic "forge-empty" 2>&1)" || rc=$?
|
|
139
|
+
assert_exit "exits 0 for empty epic" 0 "$rc"
|
|
140
|
+
assert_contains "shows no child issues" "No child issues" "$output_empty"
|
|
141
|
+
|
|
142
|
+
# ── Test 5: --format=json → valid JSON with all fields ───────────────────
|
|
143
|
+
echo ""
|
|
144
|
+
echo "── Test 5: --format=json ──"
|
|
145
|
+
rc=0
|
|
146
|
+
output_json="$(cmd_epic "forge-epic1" "--format=json" 2>&1)" || rc=$?
|
|
147
|
+
assert_exit "json exits 0" 0 "$rc"
|
|
148
|
+
# Validate it's actual JSON (check for opening brace and key fields)
|
|
149
|
+
assert_contains "json has epic_id" "epic_id" "$output_json"
|
|
150
|
+
assert_contains "json has total" "total" "$output_json"
|
|
151
|
+
assert_contains "json has done" "done" "$output_json"
|
|
152
|
+
assert_contains "json has percentage" "percentage" "$output_json"
|
|
153
|
+
assert_contains "json has children" "children" "$output_json"
|
|
154
|
+
assert_contains "json has by_developer" "by_developer" "$output_json"
|
|
155
|
+
|
|
156
|
+
# ── Test 6: Missing issue-id → error ────────────────────────────────────
|
|
157
|
+
echo ""
|
|
158
|
+
echo "── Test 6: Missing issue-id ──"
|
|
159
|
+
rc=0
|
|
160
|
+
output_missing="$(cmd_epic 2>&1)" || rc=$?
|
|
161
|
+
assert_exit "missing id returns 1" 1 "$rc"
|
|
162
|
+
assert_contains "error about usage" "Usage" "$output_missing"
|
|
163
|
+
|
|
164
|
+
# ── Test 7: Blocked children shown ───────────────────────────────────────
|
|
165
|
+
echo ""
|
|
166
|
+
echo "── Test 7: Blocked children shown ──"
|
|
167
|
+
rc=0
|
|
168
|
+
output_blocked="$(cmd_epic "forge-blocked" 2>&1)" || rc=$?
|
|
169
|
+
assert_exit "blocked epic exits 0" 0 "$rc"
|
|
170
|
+
assert_contains "blocked section present" "Blocked" "$output_blocked"
|
|
171
|
+
assert_contains "blocked child mentioned" "forge-bchild2" "$output_blocked"
|
|
172
|
+
|
|
173
|
+
# ── Results ──────────────────────────────────────────────────────────────
|
|
174
|
+
echo ""
|
|
175
|
+
echo "Results: $PASS passed, $FAIL failed"
|
|
176
|
+
[[ $FAIL -eq 0 ]] && exit 0 || exit 1
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
|
|
6
|
+
# ── Setup temp dir for test isolation ────────────────────────────────────
|
|
7
|
+
TEST_TMP="$(mktemp -d)"
|
|
8
|
+
trap 'rm -rf "$TEST_TMP"' EXIT
|
|
9
|
+
|
|
10
|
+
# ── Create mock gh (authenticated) ──────────────────────────────────────
|
|
11
|
+
mock_dir="$(mktemp -d)"
|
|
12
|
+
|
|
13
|
+
cat > "$mock_dir/gh" << 'MOCK'
|
|
14
|
+
#!/usr/bin/env bash
|
|
15
|
+
case "$*" in
|
|
16
|
+
"auth status")
|
|
17
|
+
echo "Logged in to github.com as devone"
|
|
18
|
+
exit 0
|
|
19
|
+
;;
|
|
20
|
+
"issue edit"*)
|
|
21
|
+
# Accept any issue edit (sync_issue_status)
|
|
22
|
+
exit 0
|
|
23
|
+
;;
|
|
24
|
+
esac
|
|
25
|
+
MOCK
|
|
26
|
+
chmod +x "$mock_dir/gh"
|
|
27
|
+
|
|
28
|
+
# ── Create mock gh (not authenticated) ──────────────────────────────────
|
|
29
|
+
cat > "$mock_dir/gh-noauth" << 'MOCK'
|
|
30
|
+
#!/usr/bin/env bash
|
|
31
|
+
case "$*" in
|
|
32
|
+
"auth status")
|
|
33
|
+
echo "You are not logged in" >&2
|
|
34
|
+
exit 1
|
|
35
|
+
;;
|
|
36
|
+
esac
|
|
37
|
+
MOCK
|
|
38
|
+
chmod +x "$mock_dir/gh-noauth"
|
|
39
|
+
|
|
40
|
+
# ── Create mock bd (with in_progress issues having github_issue state) ──
|
|
41
|
+
cat > "$mock_dir/bd" << 'MOCK'
|
|
42
|
+
#!/usr/bin/env bash
|
|
43
|
+
case "$1 $2" in
|
|
44
|
+
"list --status=in_progress")
|
|
45
|
+
echo "◐ forge-aaa · Feature A"
|
|
46
|
+
echo "◐ forge-bbb · Feature B"
|
|
47
|
+
;;
|
|
48
|
+
"show forge-aaa")
|
|
49
|
+
echo "◐ forge-aaa · Feature A [● P2 · IN_PROGRESS]"
|
|
50
|
+
echo "Owner: devone"
|
|
51
|
+
echo "Updated: 2026-03-27T10:00:00Z"
|
|
52
|
+
echo ""
|
|
53
|
+
echo "STATE"
|
|
54
|
+
echo " github_issue:42"
|
|
55
|
+
;;
|
|
56
|
+
"show forge-bbb")
|
|
57
|
+
echo "◐ forge-bbb · Feature B [● P2 · IN_PROGRESS]"
|
|
58
|
+
echo "Owner: devtwo"
|
|
59
|
+
echo "Updated: 2026-03-27T09:00:00Z"
|
|
60
|
+
echo ""
|
|
61
|
+
echo "STATE"
|
|
62
|
+
echo " github_issue:55"
|
|
63
|
+
;;
|
|
64
|
+
esac
|
|
65
|
+
MOCK
|
|
66
|
+
chmod +x "$mock_dir/bd"
|
|
67
|
+
|
|
68
|
+
# ── Create mock bd (no in_progress issues) ──────────────────────────────
|
|
69
|
+
cat > "$mock_dir/bd-empty" << 'MOCK'
|
|
70
|
+
#!/usr/bin/env bash
|
|
71
|
+
case "$1 $2" in
|
|
72
|
+
"list --status=in_progress")
|
|
73
|
+
echo ""
|
|
74
|
+
;;
|
|
75
|
+
esac
|
|
76
|
+
MOCK
|
|
77
|
+
chmod +x "$mock_dir/bd-empty"
|
|
78
|
+
|
|
79
|
+
# ── Create mock bd (issue without github_issue state) ───────────────────
|
|
80
|
+
cat > "$mock_dir/bd-no-gh" << 'MOCK'
|
|
81
|
+
#!/usr/bin/env bash
|
|
82
|
+
case "$1 $2" in
|
|
83
|
+
"list --status=in_progress")
|
|
84
|
+
echo "◐ forge-xxx · Feature X"
|
|
85
|
+
;;
|
|
86
|
+
"show forge-xxx")
|
|
87
|
+
echo "◐ forge-xxx · Feature X [● P2 · IN_PROGRESS]"
|
|
88
|
+
echo "Owner: devone"
|
|
89
|
+
echo "Updated: 2026-03-27T10:00:00Z"
|
|
90
|
+
;;
|
|
91
|
+
esac
|
|
92
|
+
MOCK
|
|
93
|
+
chmod +x "$mock_dir/bd-no-gh"
|
|
94
|
+
|
|
95
|
+
# ── Create .beads/config.yaml for auto-sync tests ──────────────────────
|
|
96
|
+
mkdir -p "$TEST_TMP/.beads"
|
|
97
|
+
cat > "$TEST_TMP/.beads/config.yaml" << 'YAML'
|
|
98
|
+
team:
|
|
99
|
+
auto-sync: true
|
|
100
|
+
YAML
|
|
101
|
+
|
|
102
|
+
export GH_CMD="$mock_dir/gh"
|
|
103
|
+
export BD_CMD="$mock_dir/bd"
|
|
104
|
+
export BEADS_ROOT="$TEST_TMP"
|
|
105
|
+
|
|
106
|
+
# Source the library under test
|
|
107
|
+
source "$SCRIPT_DIR/lib/hooks.sh"
|
|
108
|
+
|
|
109
|
+
PASS=0
|
|
110
|
+
FAIL=0
|
|
111
|
+
|
|
112
|
+
assert_eq() {
|
|
113
|
+
local label="$1" expected="$2" actual="$3"
|
|
114
|
+
if [[ "$actual" == "$expected" ]]; then
|
|
115
|
+
PASS=$((PASS + 1)); echo " PASS: $label"
|
|
116
|
+
else
|
|
117
|
+
FAIL=$((FAIL + 1)); echo " FAIL: $label (expected '$expected', got '$actual')"
|
|
118
|
+
fi
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
assert_contains() {
|
|
122
|
+
local label="$1" expected="$2" actual="$3"
|
|
123
|
+
if [[ "$actual" == *"$expected"* ]]; then
|
|
124
|
+
PASS=$((PASS + 1)); echo " PASS: $label"
|
|
125
|
+
else
|
|
126
|
+
FAIL=$((FAIL + 1)); echo " FAIL: $label (expected to contain '$expected', got '$actual')"
|
|
127
|
+
fi
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
assert_not_contains() {
|
|
131
|
+
local label="$1" unexpected="$2" actual="$3"
|
|
132
|
+
if [[ "$actual" != *"$unexpected"* ]]; then
|
|
133
|
+
PASS=$((PASS + 1)); echo " PASS: $label"
|
|
134
|
+
else
|
|
135
|
+
FAIL=$((FAIL + 1)); echo " FAIL: $label (expected NOT to contain '$unexpected', got '$actual')"
|
|
136
|
+
fi
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
assert_exit() {
|
|
140
|
+
local label="$1" expected="$2" actual="$3"
|
|
141
|
+
if [[ "$actual" -eq "$expected" ]]; then
|
|
142
|
+
PASS=$((PASS + 1)); echo " PASS: $label"
|
|
143
|
+
else
|
|
144
|
+
FAIL=$((FAIL + 1)); echo " FAIL: $label (expected exit $expected, got $actual)"
|
|
145
|
+
fi
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# ── Test 1: forge_team_sync with mock gh + bd → syncs in_progress issues ──
|
|
149
|
+
echo "── Test 1: forge_team_sync syncs in_progress issues ──"
|
|
150
|
+
export GH_CMD="$mock_dir/gh"
|
|
151
|
+
export BD_CMD="$mock_dir/bd"
|
|
152
|
+
rc=0
|
|
153
|
+
output="$(forge_team_sync 2>&1)" || rc=$?
|
|
154
|
+
assert_exit "exits 0" 0 "$rc"
|
|
155
|
+
assert_contains "reports synced issues" "Synced 2 issues" "$output"
|
|
156
|
+
|
|
157
|
+
# ── Test 2: forge_team_sync with gh not authenticated → error, exit 1 ──
|
|
158
|
+
echo ""
|
|
159
|
+
echo "── Test 2: forge_team_sync with gh not authenticated → error, exit 1 ──"
|
|
160
|
+
export GH_CMD="$mock_dir/gh-noauth"
|
|
161
|
+
export BD_CMD="$mock_dir/bd"
|
|
162
|
+
rc=0
|
|
163
|
+
output="$(forge_team_sync 2>&1)" || rc=$?
|
|
164
|
+
assert_exit "exits 1" 1 "$rc"
|
|
165
|
+
assert_contains "error message" "FORGE_AGENT_7f3a:ERROR:" "$output"
|
|
166
|
+
|
|
167
|
+
# ── Test 3: forge_team_sync with auto-sync: false → skip, exit 0 ────────
|
|
168
|
+
echo ""
|
|
169
|
+
echo "── Test 3: forge_team_sync with auto-sync: false → skip, exit 0 ──"
|
|
170
|
+
export GH_CMD="$mock_dir/gh"
|
|
171
|
+
export BD_CMD="$mock_dir/bd"
|
|
172
|
+
cat > "$TEST_TMP/.beads/config.yaml" << 'YAML'
|
|
173
|
+
team:
|
|
174
|
+
auto-sync: false
|
|
175
|
+
YAML
|
|
176
|
+
rc=0
|
|
177
|
+
output="$(forge_team_sync 2>&1)" || rc=$?
|
|
178
|
+
assert_exit "exits 0" 0 "$rc"
|
|
179
|
+
assert_not_contains "does not sync" "Synced" "$output"
|
|
180
|
+
# Restore config
|
|
181
|
+
cat > "$TEST_TMP/.beads/config.yaml" << 'YAML'
|
|
182
|
+
team:
|
|
183
|
+
auto-sync: true
|
|
184
|
+
YAML
|
|
185
|
+
|
|
186
|
+
# ── Test 4: forge_team_sync with no in_progress issues → nothing to sync ──
|
|
187
|
+
echo ""
|
|
188
|
+
echo "── Test 4: forge_team_sync with no in_progress issues → nothing to sync ──"
|
|
189
|
+
export GH_CMD="$mock_dir/gh"
|
|
190
|
+
export BD_CMD="$mock_dir/bd-empty"
|
|
191
|
+
rc=0
|
|
192
|
+
output="$(forge_team_sync 2>&1)" || rc=$?
|
|
193
|
+
assert_exit "exits 0" 0 "$rc"
|
|
194
|
+
assert_contains "nothing to sync message" "Nothing to sync" "$output"
|
|
195
|
+
|
|
196
|
+
# ── Test 5: forge_team_sync --quiet → no output on success ──────────────
|
|
197
|
+
echo ""
|
|
198
|
+
echo "── Test 5: forge_team_sync --quiet → no output on success ──"
|
|
199
|
+
export GH_CMD="$mock_dir/gh"
|
|
200
|
+
export BD_CMD="$mock_dir/bd"
|
|
201
|
+
rc=0
|
|
202
|
+
output="$(forge_team_sync --quiet 2>&1)" || rc=$?
|
|
203
|
+
assert_exit "exits 0" 0 "$rc"
|
|
204
|
+
assert_eq "no output" "" "$output"
|
|
205
|
+
|
|
206
|
+
# ── Test 6: forge_team_sync_on_stage triggers correct status sync ────────
|
|
207
|
+
echo ""
|
|
208
|
+
echo "── Test 6: forge_team_sync_on_stage triggers correct status sync per stage ──"
|
|
209
|
+
export GH_CMD="$mock_dir/gh"
|
|
210
|
+
export BD_CMD="$mock_dir/bd"
|
|
211
|
+
# dev transition → in_progress
|
|
212
|
+
rc=0
|
|
213
|
+
output="$(forge_team_sync_on_stage forge-aaa plan dev 2>&1)" || rc=$?
|
|
214
|
+
assert_exit "dev transition exits 0" 0 "$rc"
|
|
215
|
+
# ship transition → in_progress (still active)
|
|
216
|
+
rc=0
|
|
217
|
+
output="$(forge_team_sync_on_stage forge-aaa dev ship 2>&1)" || rc=$?
|
|
218
|
+
assert_exit "ship transition exits 0" 0 "$rc"
|
|
219
|
+
# verify transition → skip (closed by /verify)
|
|
220
|
+
rc=0
|
|
221
|
+
output="$(forge_team_sync_on_stage forge-aaa ship verify 2>&1)" || rc=$?
|
|
222
|
+
assert_exit "verify transition exits 0" 0 "$rc"
|
|
223
|
+
assert_contains "verify skips sync" "skipped" "$output"
|
|
224
|
+
|
|
225
|
+
# ── Test 7: Sync failure doesn't block (non-fatal) ──────────────────────
|
|
226
|
+
echo ""
|
|
227
|
+
echo "── Test 7: Sync failure doesn't block (non-fatal warning) ──"
|
|
228
|
+
export GH_CMD="$mock_dir/gh"
|
|
229
|
+
export BD_CMD="$mock_dir/bd-no-gh"
|
|
230
|
+
rc=0
|
|
231
|
+
output="$(forge_team_sync 2>&1)" || rc=$?
|
|
232
|
+
# Should still exit 0 even with sync failures (non-blocking)
|
|
233
|
+
assert_exit "exits 0 despite sync failure" 0 "$rc"
|
|
234
|
+
assert_contains "warns about failure" "warn" "$output"
|
|
235
|
+
|
|
236
|
+
# ── Results ──────────────────────────────────────────────────────────────
|
|
237
|
+
echo ""
|
|
238
|
+
echo "Results: $PASS passed, $FAIL failed"
|
|
239
|
+
[[ $FAIL -eq 0 ]] && exit 0 || exit 1
|