@flydocs/cli 0.6.0-alpha.20 → 0.6.0-alpha.22

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.
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env python3
2
+ """Tests for workflow enforcement logic.
3
+
4
+ Run: python3 test_enforcement.py
5
+ Tests the enforcement functions in issues.py, auto-approve.py, stop-gate.py,
6
+ and post-transition-check.py without requiring API access.
7
+ """
8
+
9
+ import json
10
+ import os
11
+ import sys
12
+ import tempfile
13
+ from pathlib import Path
14
+ from unittest.mock import patch
15
+
16
+ # Add parent dirs to path for imports
17
+ SCRIPT_DIR = Path(__file__).parent.resolve()
18
+ HOOKS_DIR = SCRIPT_DIR.parent.parent.parent / "hooks"
19
+ sys.path.insert(0, str(SCRIPT_DIR))
20
+ sys.path.insert(0, str(HOOKS_DIR))
21
+
22
+ passed = 0
23
+ failed = 0
24
+
25
+
26
+ def test(name: str):
27
+ """Decorator to register and run a test."""
28
+ def decorator(fn):
29
+ global passed, failed
30
+ try:
31
+ fn()
32
+ print(f" PASS: {name}")
33
+ passed += 1
34
+ except AssertionError as e:
35
+ print(f" FAIL: {name} — {e}")
36
+ failed += 1
37
+ except Exception as e:
38
+ print(f" ERROR: {name} — {type(e).__name__}: {e}")
39
+ failed += 1
40
+ return fn
41
+ return decorator
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # issues.py enforcement tests
46
+ # ---------------------------------------------------------------------------
47
+
48
+ print("\n## issues.py enforcement")
49
+
50
+
51
+ @test("create rejects empty description")
52
+ def _():
53
+ """issues.py create should fail with empty --description."""
54
+ import subprocess
55
+ result = subprocess.run(
56
+ [sys.executable, str(SCRIPT_DIR / "issues.py"), "create",
57
+ "--title", "Test", "--type", "feature", "--description", ""],
58
+ capture_output=True, text=True, timeout=10,
59
+ )
60
+ assert result.returncode != 0, f"Expected failure, got rc={result.returncode}"
61
+ assert "Description is required" in result.stderr, f"Expected error message, got: {result.stderr[:200]}"
62
+
63
+
64
+ @test("create rejects missing description")
65
+ def _():
66
+ """issues.py create should fail without --description flag."""
67
+ import subprocess
68
+ result = subprocess.run(
69
+ [sys.executable, str(SCRIPT_DIR / "issues.py"), "create",
70
+ "--title", "Test", "--type", "feature"],
71
+ capture_output=True, text=True, timeout=10,
72
+ )
73
+ assert result.returncode != 0, f"Expected failure, got rc={result.returncode}"
74
+
75
+
76
+ @test("create allows --triage without description")
77
+ def _():
78
+ """issues.py create with --triage should not fail on empty description."""
79
+ # This would succeed only if we have API access, so just check the
80
+ # enforcement bypass by testing the error message doesn't say "Description is required"
81
+ import subprocess
82
+ result = subprocess.run(
83
+ [sys.executable, str(SCRIPT_DIR / "issues.py"), "create",
84
+ "--title", "Quick test", "--type", "bug", "--triage"],
85
+ capture_output=True, text=True, timeout=10,
86
+ )
87
+ # It may fail for other reasons (no API), but NOT for missing description
88
+ assert "Description is required" not in result.stderr, \
89
+ f"--triage should bypass description check, got: {result.stderr[:200]}"
90
+
91
+
92
+ @test("transition rejects empty comment")
93
+ def _():
94
+ """issues.py transition should reject whitespace-only comment."""
95
+ import subprocess
96
+ result = subprocess.run(
97
+ [sys.executable, str(SCRIPT_DIR / "issues.py"), "transition",
98
+ "FLY-999", "REVIEW", " "],
99
+ capture_output=True, text=True, timeout=10,
100
+ )
101
+ assert result.returncode != 0, f"Expected failure, got rc={result.returncode}"
102
+ assert "comment cannot be empty" in result.stderr, f"Got: {result.stderr[:200]}"
103
+
104
+
105
+ # ---------------------------------------------------------------------------
106
+ # auto-approve.py enforcement tests
107
+ # ---------------------------------------------------------------------------
108
+
109
+ print("\n## auto-approve.py enforcement")
110
+
111
+
112
+ @test("should_approve matches workflow scripts")
113
+ def _():
114
+ # Import the function directly
115
+ sys.path.insert(0, str(HOOKS_DIR))
116
+ # We need to import carefully since it's a hook script
117
+ import importlib.util
118
+ spec = importlib.util.spec_from_file_location("auto_approve", HOOKS_DIR / "auto-approve.py")
119
+ mod = importlib.util.module_from_spec(spec)
120
+ spec.loader.exec_module(mod)
121
+
122
+ assert mod.should_approve("python3 .claude/skills/flydocs-workflow/scripts/issues.py create --title test")
123
+ assert mod.should_approve("python3 .claude/skills/flydocs-workflow/scripts/workspace.py validate")
124
+ assert not mod.should_approve("python3 malicious.py")
125
+ assert not mod.should_approve("rm -rf /")
126
+
127
+
128
+ @test("validate_create_args warns on missing description")
129
+ def _():
130
+ import importlib.util
131
+ spec = importlib.util.spec_from_file_location("auto_approve", HOOKS_DIR / "auto-approve.py")
132
+ mod = importlib.util.module_from_spec(spec)
133
+ spec.loader.exec_module(mod)
134
+
135
+ warnings = mod.validate_create_args(
136
+ 'python3 .claude/skills/flydocs-workflow/scripts/issues.py create --title "test" --type feature'
137
+ )
138
+ assert len(warnings) > 0, "Should warn about missing --description"
139
+ assert any("description" in w.lower() for w in warnings)
140
+
141
+
142
+ @test("validate_create_args no warning when description present")
143
+ def _():
144
+ import importlib.util
145
+ spec = importlib.util.spec_from_file_location("auto_approve", HOOKS_DIR / "auto-approve.py")
146
+ mod = importlib.util.module_from_spec(spec)
147
+ spec.loader.exec_module(mod)
148
+
149
+ warnings = mod.validate_create_args(
150
+ 'python3 .claude/skills/flydocs-workflow/scripts/issues.py create --title "test" --type feature --description "Full description here with enough content"'
151
+ )
152
+ desc_warnings = [w for w in warnings if "Missing --description" in w]
153
+ assert len(desc_warnings) == 0, f"Should not warn when description present, got: {warnings}"
154
+
155
+
156
+ @test("get_transition_comment_hint returns template for known status")
157
+ def _():
158
+ import importlib.util
159
+ spec = importlib.util.spec_from_file_location("auto_approve", HOOKS_DIR / "auto-approve.py")
160
+ mod = importlib.util.module_from_spec(spec)
161
+ spec.loader.exec_module(mod)
162
+
163
+ hint = mod.get_transition_comment_hint("issues.py transition FLY-123 IMPLEMENTING")
164
+ assert hint is not None, "Should return hint for IMPLEMENTING"
165
+ assert "Starting implementation" in hint
166
+
167
+
168
+ @test("get_transition_comment_hint returns None for non-transition")
169
+ def _():
170
+ import importlib.util
171
+ spec = importlib.util.spec_from_file_location("auto_approve", HOOKS_DIR / "auto-approve.py")
172
+ mod = importlib.util.module_from_spec(spec)
173
+ spec.loader.exec_module(mod)
174
+
175
+ hint = mod.get_transition_comment_hint("issues.py list --status BACKLOG")
176
+ assert hint is None, "Should return None for non-transition commands"
177
+
178
+
179
+ # ---------------------------------------------------------------------------
180
+ # Transition validation tests
181
+ # ---------------------------------------------------------------------------
182
+
183
+ print("\n## Transition validation")
184
+
185
+
186
+ @test("VALID_TRANSITIONS allows IMPLEMENTING -> REVIEW")
187
+ def _():
188
+ # Test the transition map directly from issues.py
189
+ import importlib.util
190
+ spec = importlib.util.spec_from_file_location("issues", SCRIPT_DIR / "issues.py")
191
+ mod = importlib.util.module_from_spec(spec)
192
+ spec.loader.exec_module(mod)
193
+
194
+ assert "REVIEW" in mod.VALID_TRANSITIONS["IMPLEMENTING"]
195
+ assert "BLOCKED" in mod.VALID_TRANSITIONS["IMPLEMENTING"]
196
+
197
+
198
+ @test("VALID_TRANSITIONS blocks BACKLOG -> REVIEW")
199
+ def _():
200
+ import importlib.util
201
+ spec = importlib.util.spec_from_file_location("issues", SCRIPT_DIR / "issues.py")
202
+ mod = importlib.util.module_from_spec(spec)
203
+ spec.loader.exec_module(mod)
204
+
205
+ assert "REVIEW" not in mod.VALID_TRANSITIONS["BACKLOG"]
206
+
207
+
208
+ @test("VALID_TRANSITIONS blocks READY -> REVIEW")
209
+ def _():
210
+ import importlib.util
211
+ spec = importlib.util.spec_from_file_location("issues", SCRIPT_DIR / "issues.py")
212
+ mod = importlib.util.module_from_spec(spec)
213
+ spec.loader.exec_module(mod)
214
+
215
+ assert "REVIEW" not in mod.VALID_TRANSITIONS["READY"]
216
+
217
+
218
+ # ---------------------------------------------------------------------------
219
+ # Summary
220
+ # ---------------------------------------------------------------------------
221
+
222
+ print(f"\n{'='*50}")
223
+ print(f"Results: {passed} passed, {failed} failed, {passed + failed} total")
224
+ if failed > 0:
225
+ sys.exit(1)
@@ -4,7 +4,17 @@
4
4
 
5
5
  When a conversation begins or the user returns after a gap:
6
6
 
7
- 1. **Query graph for issue context** (if an issue ID is known from the prompt or last session) —
7
+ 1. **Verify config completeness** Read `.flydocs/config.json` and check:
8
+ - `workspace.activeProjects` is non-empty (cloud tier) — warn if missing
9
+ - `workspace.defaultMilestoneId` is set — warn if null
10
+ - `issueLabels.category` has non-null values — warn if unconfigured
11
+ These are informational warnings, not blocking. Example:
12
+
13
+ ```
14
+ Config check: activeProjects OK | milestone OK | labels: 2 of 4 configured (idea, chore missing)
15
+ ```
16
+
17
+ 2. **Query graph for issue context** (if an issue ID is known from the prompt or last session) —
8
18
 
9
19
  ```
10
20
  python3 .claude/skills/flydocs-workflow/scripts/graph_query.py --node <issue-id> --depth 2
@@ -13,28 +23,28 @@ When a conversation begins or the user returns after a gap:
13
23
  This surfaces related decisions, blocking relationships, and recent session activity.
14
24
  Skip silently if the script is not installed or the graph has not been built.
15
25
 
16
- 2. **Check workspace context** — If `flydocs/context/service.json` exists, note the
26
+ 3. **Check workspace context** — If `flydocs/context/service.json` exists, note the
17
27
  repo's topology and dependencies. For multi-repo workspaces (topology type 3 or 4),
18
28
  briefly mention which sibling repos exist and any active cross-repo dependencies.
19
29
  This helps the user orient when switching between repos.
20
30
 
21
- 3. **Fetch all product issues in one call** — Run `issues.py list --active --limit 100`.
31
+ 4. **Fetch all product issues in one call** — Run `issues.py list --active --limit 100`.
22
32
  This is auto-scoped by the product cascade: `activeProjects` → `product.labelIds` → team-wide.
23
33
  Issues outside the product scope are never shown.
24
34
 
25
- 4. **Identify the active project** — Read `workspace.activeProjects` from config.
35
+ 5. **Identify the active project** — Read `workspace.activeProjects` from config.
26
36
  Separate issues into two buckets:
27
37
  - **Active project issues** — issues whose `projectId` matches an active project
28
38
  - **Other product issues** — issues in the product scope but in a different project
29
39
 
30
- 5. **Group active project issues by milestone** — Use the `milestone` and `milestoneSortOrder`
40
+ 6. **Group active project issues by milestone** — Use the `milestone` and `milestoneSortOrder`
31
41
  fields returned by the script. Sort milestones by `milestoneSortOrder` (lowest first = earliest).
32
42
  Within each milestone, group by status.
33
43
 
34
- 6. **Identify the current milestone** — The first milestone (by sort order) that still has
44
+ 7. **Identify the current milestone** — The first milestone (by sort order) that still has
35
45
  open issues. This is where work should focus.
36
46
 
37
- 7. **Present the dashboard:**
47
+ 8. **Present the dashboard:**
38
48
 
39
49
  ```
40
50
  Welcome back! [Product Name] status:
@@ -57,21 +67,21 @@ When a conversation begins or the user returns after a gap:
57
67
  Suggested starting point: [ISSUE-ID] — [reason: highest priority in current milestone / unblocks others / due soon]
58
68
  ```
59
69
 
60
- 8. **Suggest where to start** — Use this priority cascade:
70
+ 9. **Suggest where to start** — Use this priority cascade:
61
71
  - Blocked issues that need unblocking (always surface first)
62
72
  - In-progress issues (continue existing work)
63
73
  - Due date approaching (within 7 days)
64
74
  - Highest priority in the current milestone
65
75
  - Issues that unblock downstream milestones
66
76
 
67
- 9. **Surface other product issues briefly** — If there are issues in the product scope
68
- but outside the active project, mention the count with a one-line summary:
77
+ 10. **Surface other product issues briefly** — If there are issues in the product scope
78
+ but outside the active project, mention the count with a one-line summary:
69
79
 
70
- ```
71
- Also in [Product Name]: [N] issues across other projects (use --all to see)
72
- ```
80
+ ```
81
+ Also in [Product Name]: [N] issues across other projects (use --all to see)
82
+ ```
73
83
 
74
- 10. **Check for stale issues** — Flag issues exceeding staleness thresholds (see below).
84
+ 11. **Check for stale issues** — Flag issues exceeding staleness thresholds (see below).
75
85
 
76
86
  **Important:** Do NOT make separate API calls per status or per milestone. One call returns
77
87
  all issues with their status and milestone fields — group them in your response.
@@ -109,7 +119,17 @@ When the user indicates they're done for the session:
109
119
  Populate from the session data gathered in step 1. Use the Write tool or a script — the file
110
120
  must exist on disk so the prompt hook can read it on the next session start.
111
121
 
112
- 7. **Record session in context graph** — Call `graph_session.py` with summary and issues worked on:
122
+ 7. **Audit session issues** — Run `issues.py audit --limit 20` to check all recently
123
+ touched issues for compliance. Report findings as part of the wrap summary:
124
+
125
+ ```
126
+ Session audit: 5 issues checked, 1 finding
127
+ - FLY-484: missing_description
128
+ ```
129
+
130
+ This is informational — don't block the wrap, just surface gaps.
131
+
132
+ 8. **Record session in context graph** — Call `graph_session.py` with summary and issues worked on:
113
133
  ```
114
134
  python3 .claude/skills/flydocs-workflow/scripts/graph_session.py \
115
135
  --summary "Brief summary of session outcomes" \
@@ -117,8 +137,8 @@ When the user indicates they're done for the session:
117
137
  [--decision NNN]
118
138
  ```
119
139
  This creates a session node for cross-session continuity. Skip silently if the script is not installed.
120
- 8. **Verify** — Confirm the project update was posted (update ID returned).
121
- 9. **Ask about uncommitted changes** — If git shows uncommitted work, offer to commit.
140
+ 9. **Verify** — Confirm the project update was posted (update ID returned).
141
+ 10. **Ask about uncommitted changes** — If git shows uncommitted work, offer to commit.
122
142
 
123
143
  Do not just summarize in chat. Actually post the update. Do not skip if the user seems in a hurry.
124
144
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.6.0-alpha.20",
2
+ "version": "0.6.0-alpha.22",
3
3
  "sourceRepo": "github.com/plastrlab/flydocs-core",
4
4
  "tier": "local",
5
5
  "setupComplete": false,
@@ -0,0 +1,30 @@
1
+ <!-- AGENT: Use this template when creating bug issues. Fill all [bracketed] sections. -->
2
+
3
+ ## What
4
+
5
+ [One sentence describing the bug]
6
+
7
+ ## Expected Behavior
8
+
9
+ [What should happen]
10
+
11
+ ## Actual Behavior
12
+
13
+ [What actually happens]
14
+
15
+ ## Steps to Reproduce
16
+
17
+ 1. [Step 1]
18
+ 2. [Step 2]
19
+ 3. [Step 3]
20
+
21
+ ## Acceptance Criteria
22
+
23
+ - [ ] Bug is fixed and original behavior restored
24
+ - [ ] [Additional verification criteria]
25
+
26
+ ## Context
27
+
28
+ - **Frequency:** [Always / Sometimes / Rare]
29
+ - **Impact:** [Blocking / Degraded / Cosmetic]
30
+ - **Environment:** [Where it occurs — browser, OS, tier]
@@ -0,0 +1,22 @@
1
+ <!-- AGENT: Use this template when creating chore issues. Fill all [bracketed] sections. -->
2
+
3
+ ## What
4
+
5
+ [One sentence describing the task]
6
+
7
+ ## Why
8
+
9
+ [Why this maintenance/cleanup is needed now]
10
+
11
+ ## Acceptance Criteria
12
+
13
+ - [ ] [Specific, testable criterion 1]
14
+ - [ ] [Specific, testable criterion 2]
15
+
16
+ ## Scope
17
+
18
+ [What's included and what's explicitly excluded]
19
+
20
+ ## Technical Notes
21
+
22
+ [Approach, risks, or migration steps — remove if not applicable]
@@ -0,0 +1,27 @@
1
+ <!-- AGENT: Use this template when creating feature issues. Fill all [bracketed] sections. -->
2
+
3
+ ## What
4
+
5
+ [One sentence describing the feature]
6
+
7
+ ## Why
8
+
9
+ [Problem this solves or opportunity it creates]
10
+
11
+ ## User Story
12
+
13
+ As a [role], I want to [action] so that [benefit].
14
+
15
+ ## Acceptance Criteria
16
+
17
+ - [ ] [Specific, testable criterion 1]
18
+ - [ ] [Specific, testable criterion 2]
19
+ - [ ] [Specific, testable criterion 3]
20
+
21
+ ## Technical Notes
22
+
23
+ [Implementation approach, constraints, or dependencies — remove if not applicable]
24
+
25
+ ## Out of Scope
26
+
27
+ [Explicitly list what this does NOT include — remove if not applicable]
@@ -0,0 +1,22 @@
1
+ <!-- AGENT: Use this template when creating idea issues. Fill all [bracketed] sections. -->
2
+
3
+ ## What
4
+
5
+ [One sentence describing the idea]
6
+
7
+ ## Why
8
+
9
+ [Problem or opportunity this addresses]
10
+
11
+ ## Rough Shape
12
+
13
+ [Initial thinking on approach — does not need to be detailed]
14
+
15
+ ## Open Questions
16
+
17
+ - [Question 1]
18
+ - [Question 2]
19
+
20
+ ## Next Steps
21
+
22
+ - [ ] [First step to explore or validate this idea]
@@ -1 +1 @@
1
- 0.6.0-alpha.20
1
+ 0.6.0-alpha.22
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.6.0-alpha.20",
2
+ "version": "0.6.0-alpha.22",
3
3
  "description": "FlyDocs Core - Manifest of all managed files",
4
4
  "repository": "github.com/plastrlab/flydocs-core",
5
5