spec-and-loop 1.0.5 → 1.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/QUICKSTART.md CHANGED
@@ -213,12 +213,49 @@ openspec new another-feature
213
213
  | **Auto-Resume** | Interrupted? Run again—picks up where left off |
214
214
  | **Context Injection** | Inject custom instructions during execution |
215
215
 
216
+ ## Testing
217
+
218
+ Spec-and-loop includes a comprehensive test suite to ensure reliability and cross-platform compatibility.
219
+
220
+ ### Running Tests
221
+
222
+ ```bash
223
+ # Run all tests
224
+ npm test
225
+
226
+ # Run unit tests only
227
+ npm run test:unit
228
+
229
+ # Run integration tests only
230
+ npm run test:integration
231
+
232
+ # Run tests with coverage
233
+ npm run test:coverage
234
+
235
+ # Run shellcheck linting
236
+ npm run lint
237
+ ```
238
+
239
+ ### Test Requirements
240
+
241
+ To run tests, you'll need:
242
+ - **Node.js** (>= 24.0.0)
243
+ - **Bats** (Bash testing framework): `apt install bats-core` or `brew install bats-core`
244
+ - **Shellcheck** (Bash linting): `apt install shellcheck` or `brew install shellcheck`
245
+
246
+ ### CI/CD
247
+
248
+ Tests run automatically on every push and pull request via GitHub Actions on both Linux and macOS.
249
+
250
+ **For more details, see [TESTING.md](./TESTING.md)**
251
+
216
252
  ## Next Steps
217
253
 
218
254
  1. **Read the full README.md** for detailed documentation
219
255
  2. **Try a real feature** in your project
220
256
  3. **Explore the .ralph/** directory to see internal state
221
257
  4. **Check out .hidden/** directory for advanced guides
258
+ 5. **Review TESTING.md** for testing guidelines
222
259
 
223
260
  ## Resources
224
261
 
package/README.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  OpenSpec + Ralph Loop integration for iterative development with opencode.
4
4
 
5
+ ![CI Status](https://img.shields.io/github/actions/workflow/status/ncheaz/spec-and-loop/test.yml)
6
+ ![Coverage](https://img.shields.io/badge/coverage-0%25-red)
7
+ [![npm version](https://badge.fury.io/js/spec-and-loop.svg)](https://badge.fury.io/js/spec-and-loop)
8
+
5
9
  **[🚀 Quick Start Guide](./QUICKSTART.md)** - Get up and running in 5 minutes!
6
10
 
7
11
  ## Why This Exists
@@ -56,6 +60,114 @@ For detailed step-by-step instructions, see [QUICKSTART.md](./QUICKSTART.md).
56
60
 
57
61
  <!-- Duplicate Quick Start removed; see QUICKSTART.md for full instructions -->
58
62
 
63
+ ## Testing
64
+
65
+ Spec-and-loop includes a comprehensive test suite to ensure reliability and cross-platform compatibility.
66
+
67
+ **[📋 Testing Guide](./TESTING.md)** - Detailed instructions for running tests
68
+
69
+ ### Quick Test Commands
70
+
71
+ ```bash
72
+ # Run all tests
73
+ npm test
74
+
75
+ # Run unit tests only
76
+ npm run test:unit
77
+
78
+ # Run integration tests only
79
+ npm run test:integration
80
+
81
+ # Run tests with coverage
82
+ npm run test:coverage
83
+
84
+ # Run shellcheck linting
85
+ npm run lint
86
+ ```
87
+
88
+ ### CI/CD Status
89
+
90
+ - **Linux**: Tests run on Ubuntu (latest)
91
+ - **macOS**: Tests run on macOS (latest)
92
+ - **Node.js**: Tested on Node.js 24
93
+
94
+ All tests are run automatically via GitHub Actions on every push and pull request.
95
+
96
+ ### CI/CD Workflow
97
+
98
+ The CI/CD pipeline is defined in `.github/workflows/test.yml` and performs the following steps:
99
+
100
+ 1. **Checkout Code**: Pulls the latest code from the repository
101
+ 2. **Setup Node.js**: Installs Node.js version 24 with npm caching
102
+ 3. **Install System Dependencies**:
103
+ - Linux: `apt-get install bats-core jq shellcheck`
104
+ - macOS: `brew install bats-core jq shellcheck`
105
+ 4. **Install npm Dependencies**: Runs `npm ci` to install dependencies
106
+ 5. **Install Global CLIs**: Installs openspec, ralph, and opencode globally
107
+ 6. **Run Shellcheck Linting**: Checks bash scripts for errors and best practices
108
+ 7. **Run Unit Tests**: Executes bash and JavaScript unit tests
109
+ 8. **Run Integration Tests**: Validates full workflow end-to-end
110
+ 9. **Upload Artifacts**: Uploads test logs and coverage reports
111
+
112
+ ### Triggering CI/CD
113
+
114
+ The workflow runs automatically on:
115
+ - Push to `main` or `develop` branches
116
+ - Pull requests to `main` or `develop` branches
117
+ - Manual trigger via GitHub Actions UI
118
+
119
+ To manually trigger:
120
+ 1. Go to Actions tab in GitHub
121
+ 2. Select "Test Suite" workflow
122
+ 3. Click "Run workflow"
123
+ 4. Select branch and test suite (all/unit/integration)
124
+
125
+ ### Troubleshooting CI/CD
126
+
127
+ **Tests Failing on One Platform**
128
+
129
+ If tests pass on Linux but fail on macOS (or vice versa):
130
+ - Check for platform-specific command differences (GNU vs BSD tools)
131
+ - Review platform-specific tests in `test-symlink-linux.bats`, `test-symlink-macos.bats`, etc.
132
+ - Verify stat, md5sum/md5, and other commands use correct flags
133
+
134
+ **Coverage Below Threshold**
135
+
136
+ If coverage drops below 80%:
137
+ - Review coverage reports uploaded as artifacts
138
+ - Identify which functions lost coverage
139
+ - Add tests to cover the missing code paths
140
+
141
+ **Linting Failures**
142
+
143
+ If shellcheck finds issues:
144
+ - Review the warnings in the CI logs
145
+ - Fix the issues locally: `npm run lint`
146
+ - Commit the fixes
147
+
148
+ **Timeout Issues**
149
+
150
+ If tests timeout:
151
+ - Integration tests may take longer than expected
152
+ - Check for infinite loops or hanging processes
153
+ - Review test fixture setup/teardown
154
+
155
+ **Artifact Access**
156
+
157
+ Download test logs and coverage reports:
158
+ 1. Go to the failed workflow run
159
+ 2. Scroll to "Artifacts" section
160
+ 3. Download relevant artifacts (test logs, coverage reports)
161
+ 4. Analyze locally to identify issues
162
+
163
+ ### Test Coverage
164
+
165
+ Critical functions have >80% test coverage. View detailed coverage reports:
166
+ ```bash
167
+ npm run test:coverage
168
+ open coverage/index.html
169
+ ```
170
+
59
171
  ## Prerequisites
60
172
 
61
173
  Before using spec-and-loop, ensure you have:
package/package.json CHANGED
@@ -1,17 +1,51 @@
1
1
  {
2
2
  "name": "spec-and-loop",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "OpenSpec + Ralph Loop integration for iterative development with opencode",
5
5
  "main": "index.js",
6
6
  "bin": {
7
7
  "ralph-run": "bin/ralph-run"
8
8
  },
9
9
  "scripts": {
10
- "postinstall": "node scripts/setup.js"
10
+ "postinstall": "node scripts/setup.js",
11
+ "test": "npm run test:unit && npm run test:integration",
12
+ "test:unit": "bats tests/unit/bash/*.bats && npm run test:js",
13
+ "test:js": "jest tests/unit/javascript",
14
+ "test:integration": "bats tests/integration/*.bats",
15
+ "test:watch": "npm run test:js -- --watch",
16
+ "test:coverage": "npm run test:unit -- --coverage",
17
+ "lint": "shellcheck scripts/*.sh"
11
18
  },
12
19
  "dependencies": {
13
- "@fission-ai/openspec": "latest",
14
- "@th0rgal/ralph-wiggum": "latest"
20
+ "@fission-ai/openspec": "1.2.0",
21
+ "@th0rgal/ralph-wiggum": "1.2.2"
22
+ },
23
+ "devDependencies": {
24
+ "@types/jest": "^29.5.0",
25
+ "bats": "^1.13.0",
26
+ "jest": "^29.7.0"
27
+ },
28
+ "jest": {
29
+ "testEnvironment": "node",
30
+ "collectCoverage": true,
31
+ "coverageDirectory": "coverage",
32
+ "coverageReporters": [
33
+ "text",
34
+ "lcov",
35
+ "html",
36
+ "json"
37
+ ],
38
+ "coverageThreshold": {
39
+ "global": {
40
+ "branches": 80,
41
+ "functions": 80,
42
+ "lines": 80,
43
+ "statements": 80
44
+ }
45
+ },
46
+ "testMatch": [
47
+ "**/tests/unit/javascript/**/*.test.js"
48
+ ]
15
49
  },
16
50
  "keywords": [
17
51
  "openspec",
@@ -2,6 +2,27 @@
2
2
 
3
3
  set -e
4
4
 
5
+ # Detect OS for cross-platform compatibility
6
+ detect_os() {
7
+ case "$(uname -s)" in
8
+ Linux*) OS="Linux";;
9
+ Darwin*) OS="macOS";;
10
+ *) OS="Unknown";;
11
+ esac
12
+ }
13
+
14
+ detect_os
15
+
16
+ # Cross-platform file modification time display
17
+ get_file_mtime_display() {
18
+ local file="$1"
19
+ if [[ "$OS" == "macOS" ]]; then
20
+ stat -f "%Sm" -t "%Y-%m-%d %H:%M:%S" "$file" 2>/dev/null || echo "Unknown"
21
+ else
22
+ stat -c "%y" "$file" 2>/dev/null | cut -d'.' -f1 || echo "Unknown"
23
+ fi
24
+ }
25
+
5
26
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
27
  CHANGE_NAME="${1:-auto-detect}"
7
28
 
@@ -85,7 +106,7 @@ while true; do
85
106
  echo ""
86
107
 
87
108
  # File status
88
- echo " Last Updated: $(stat -c "%y" "$TASKS_FILE" 2>/dev/null | cut -d'.' -f1)"
109
+ echo " Last Updated: $(get_file_mtime_display "$TASKS_FILE")"
89
110
  echo ""
90
111
 
91
112
  echo "======================================================================="
@@ -9,6 +9,58 @@ if [[ -d "$HOME/.bun/bin" ]]; then
9
9
  export PATH="$HOME/.bun/bin:$PATH"
10
10
  fi
11
11
 
12
+ # Detect OS for cross-platform compatibility
13
+ detect_os() {
14
+ case "$(uname -s)" in
15
+ Linux*) OS="Linux";;
16
+ Darwin*) OS="macOS";;
17
+ *) OS="Unknown";;
18
+ esac
19
+ }
20
+
21
+ detect_os
22
+
23
+ # Cross-platform file modification time
24
+ get_file_mtime() {
25
+ local file="$1"
26
+ if [[ "$OS" == "macOS" ]]; then
27
+ stat -f %m "$file" 2>/dev/null || echo 0
28
+ else
29
+ stat -c %Y "$file" 2>/dev/null || echo 0
30
+ fi
31
+ }
32
+
33
+ # Cross-platform MD5 hash
34
+ get_file_md5() {
35
+ local file="$1"
36
+ if command -v md5sum >/dev/null 2>&1; then
37
+ md5sum "$file" | cut -d' ' -f1
38
+ elif command -v md5 >/dev/null 2>&1; then
39
+ md5 -q "$file"
40
+ else
41
+ echo "0"
42
+ fi
43
+ }
44
+
45
+ # Cross-platform realpath with fallback
46
+ get_realpath() {
47
+ local path="$1"
48
+ if command -v realpath >/dev/null 2>&1; then
49
+ realpath "$path" 2>/dev/null || echo ""
50
+ elif readlink -f / >/dev/null 2>&1; then
51
+ readlink -f "$path" 2>/dev/null || echo ""
52
+ else
53
+ # Fallback for systems without realpath
54
+ local dir
55
+ dir=$(cd "$(dirname "$path")" 2>/dev/null && pwd -P || echo "")
56
+ if [[ -n "$dir" ]]; then
57
+ echo "$dir/$(basename "$path")"
58
+ else
59
+ echo ""
60
+ fi
61
+ fi
62
+ }
63
+
12
64
  CHANGE_NAME=""
13
65
  MAX_ITERATIONS=""
14
66
  ERROR_OCCURRED=false
@@ -219,7 +271,7 @@ auto_detect_change() {
219
271
  if [[ -d "$change_dir" ]]; then
220
272
  local tasks_file="$change_dir/tasks.md"
221
273
  if [[ -f "$tasks_file" ]]; then
222
- local mod_time=$(stat -c %Y "$tasks_file" 2>/dev/null || echo 0)
274
+ local mod_time=$(get_file_mtime "$tasks_file")
223
275
  if [[ $mod_time -gt $latest_time ]]; then
224
276
  latest_time=$mod_time
225
277
  latest_change=$(basename "$change_dir")
@@ -323,7 +375,7 @@ parse_tasks() {
323
375
  TASKS_MD5=""
324
376
 
325
377
  if [[ -f "$tasks_file" ]]; then
326
- TASKS_MD5=$(md5sum "$tasks_file" | cut -d' ' -f1)
378
+ TASKS_MD5=$(get_file_md5 "$tasks_file")
327
379
  fi
328
380
 
329
381
  log_verbose "Parsing tasks from tasks.md..."
@@ -356,7 +408,7 @@ check_tasks_modified() {
356
408
  fi
357
409
 
358
410
  local current_md5
359
- current_md5=$(md5sum "$tasks_file" | cut -d' ' -f1)
411
+ current_md5=$(get_file_md5 "$tasks_file")
360
412
 
361
413
  if [[ "$current_md5" != "$original_md5" ]]; then
362
414
  return 0
@@ -566,15 +618,7 @@ sync_tasks_to_ralph() {
566
618
 
567
619
  # Resolve absolute path to tasks file (portable across Linux/macOS)
568
620
  local abs_tasks_file=""
569
- if command -v realpath >/dev/null 2>&1; then
570
- abs_tasks_file=$(realpath "$tasks_file" 2>/dev/null || true)
571
- elif readlink -f / >/dev/null 2>&1; then
572
- abs_tasks_file=$(readlink -f "$tasks_file" 2>/dev/null || true)
573
- else
574
- local _tdir
575
- _tdir=$(cd "$(dirname "$tasks_file")" 2>/dev/null && pwd -P || echo "")
576
- abs_tasks_file="$_tdir/$(basename "$tasks_file")"
577
- fi
621
+ abs_tasks_file=$(get_realpath "$tasks_file")
578
622
 
579
623
  # Clean up old Ralph tasks file in change directory if exists
580
624
  if [[ -f "$old_ralph_tasks_file" ]]; then
@@ -589,16 +633,7 @@ sync_tasks_to_ralph() {
589
633
  if [[ -L "$ralph_tasks_file" ]]; then
590
634
  log_verbose "Symlink exists, ensuring it points to correct location"
591
635
  local current_target=""
592
- if command -v realpath >/dev/null 2>&1; then
593
- current_target=$(realpath "$ralph_tasks_file" 2>/dev/null || echo "")
594
- elif readlink -f / >/dev/null 2>&1; then
595
- current_target=$(readlink -f "$ralph_tasks_file" 2>/dev/null || echo "")
596
- else
597
- current_target=$(readlink "$ralph_tasks_file" 2>/dev/null || echo "")
598
- if [[ -n "$current_target" && "$current_target" != /* ]]; then
599
- current_target="$(cd "$(dirname "$ralph_tasks_file")" && pwd -P)/$current_target"
600
- fi
601
- fi
636
+ current_target=$(get_realpath "$ralph_tasks_file")
602
637
 
603
638
  if [[ "$current_target" != "$abs_tasks_file" ]]; then
604
639
  log_verbose "Updating symlink to point to new change directory"
@@ -625,7 +660,7 @@ create_prompt_template() {
625
660
  log_verbose "Creating custom prompt template..."
626
661
 
627
662
  local abs_change_dir
628
- abs_change_dir=$(realpath "$change_dir" 2>/dev/null)
663
+ abs_change_dir=$(get_realpath "$change_dir")
629
664
 
630
665
  cat > "$template_file" << 'EOF'
631
666
  # Ralph Wiggum Task Execution - Iteration {{iteration}} / {{max_iterations}}
@@ -715,7 +750,10 @@ Tasks completed:
715
750
  {{context}}
716
751
  EOF
717
752
 
718
- sed -i "s|{{change_dir}}|$abs_change_dir|g" "$template_file"
753
+ # Use a portable inplace replace: write to temp file then move into place
754
+ local _tmpfile
755
+ _tmpfile=$(mktemp 2>/dev/null || mktemp -t ralph-template)
756
+ sed "s|{{change_dir}}|$abs_change_dir|g" "$template_file" > "$_tmpfile" && mv "$_tmpfile" "$template_file"
719
757
 
720
758
  log_verbose "Prompt template created: $template_file"
721
759
  }