Type something to search...

Testing Astrolock changes locally

Testing

How to test Astrolock changes during development.

Adding the CLI to Your Path

# Create symlink to local bin directory
ln -s /path/to/astrolock-template/bin/astrolock ~/.local/bin/astrolock

# Ensure ~/.local/bin is in PATH
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc

Running Commands in USER_FOLDER

The CLI detects configuration based on current directory:

# Navigate to user site
cd ~/my-site

# Commands use local config automatically
astrolock write      # Uses ./config, ./content, ./public
ls content/    # Lists content from ./content
astrolock deploy aws      # Uses ./.astrolock/astrolock.yaml

The CLI sets environment variables based on current directory:

  • ASTROLOCK_CONFIG_DIR=./config
  • ASTROLOCK_CONTENT_DIR=./content
  • ASTROLOCK_PUBLIC_DIR=./public

Testing CLI Changes

# Navigate to Astrolock source
cd /path/to/astrolock

# Regenerate CLI
bashly generate

# Verify command exists
./bin/astrolock --help
./bin/astrolock {new-command} --help

# Test command
./bin/astrolock {new-command} {args}

Testing Site Changes

Uses default Astrolock content (documentation site):

cd /path/to/astrolock
yarn dev
# Visit http://localhost:4321

Testing a New Docker Image Locally

cd /path/to/astrolock

# Build image
docker build -f plugins/docker-runner/Dockerfile -t astrolock:local .

# Verify
docker images | grep astrolock
cd ~/my-site

# Run dev server
docker run --rm -it \
  -v $(pwd):/site \
  -p 4321:4321 \
  astrolock:local \
  astrolock write

# Run build
docker run --rm -it \
  -v $(pwd):/site \
  astrolock:local \
  astrolock build

# Interactive shell
docker run --rm -it \
  -v $(pwd):/site \
  astrolock:local \
  /bin/bash
docker run --rm -it \
  -v $(pwd):/site \
  astrolock:local \
  astrolock --help

docker run --rm -it \
  -v $(pwd):/site \
  astrolock:local \
  ls content/

Test Suite Overview

Astrolock has four types of tests:

TypeCommandLocationPurpose
Unit Testsyarn testsrc/**/*.test.tsTypeScript utilities
CLI Validationbashly validatecli/bashly.ymlCLI definition
Approval Testscd cli && ./test/approvecli/test/approvals/CLI output verification
Integration Testsmake test.integrationcli/test/integration/End-to-end CLI workflows

Running All Tests

# Run everything (unit + approval + integration)
make test.all
# TypeScript unit tests
yarn test

# CLI validation only
cd cli && bashly validate

# Approval tests only
cd cli && ./test/approve

# Integration tests only
make test.integration

# Specific integration test suite
cd cli && ./test/run-integration integration/init.bats
cd cli && ./test/run-integration integration/content.bats

BATS Integration Tests

Integration tests verify end-to-end CLI workflows using BATS (Bash Automated Testing System). These tests create real temporary sites, execute actual commands, and validate the complete behavior.

Test Coverage

57 integration tests across 5 core commands:

Test SuiteTestsCoverage
init.bats12Site initialization, directory structure, config creation
content.bats14Blog posts, media items, authors, slugification
collection-add.bats13Collection creation, media directories, configuration
build.bats10Production builds, environment variables, targets
deploy.bats8Platform detection, deploy configuration, flags

Running Integration Tests

# Via Makefile (recommended)
make test.integration

# Direct execution
cd cli && ./test/run-integration

Test Structure

Integration tests follow a consistent pattern:

#!/usr/bin/env bats
# Load test helpers
load 'helpers'

setup() {
  # Create isolated test environment
  export TEST_TEMP_DIR="$(mktemp -d)"
  export USER_ROOT="$TEST_TEMP_DIR"
  cd "$TEST_TEMP_DIR"

  # Initialize test site
  create_initialized_site
}

teardown() {
  # Clean up after test
  cleanup_test_dir
}

@test "command does expected thing" {
  # Arrange: Set up test conditions
  # Act: Run the command
  run_astrolock content blog "My Post"

  # Assert: Verify results
  assert_success
  assert_file_exist "content/blog/my-post.md"
  assert_valid_frontmatter "content/blog/my-post.md"
}
cli/test/integration/
├── helpers.bash              # Shared utilities (30+ functions)
├── fixtures/                 # Test data
│   ├── minimal-site.yaml    # Basic config
│   ├── full-site.yaml       # Complete config
│   ├── sample-blog-post.md
│   ├── sample-media-item.md
│   └── deploy-targets.yaml
├── init.bats                 # Site initialization tests
├── content.bats              # Content creation tests
├── collection-add.bats       # Collection management tests
├── build.bats                # Build process tests
└── deploy.bats               # Deployment tests

Helper Functions

The helpers.bash library provides 30+ utility functions for common test operations:

# Create minimal initialized site
init_minimal_site

# Create full site structure with content
create_initialized_site

# Get path to CLI binary
get_cli_path

# Get path to fixture file
get_fixture_path "minimal-site.yaml"
# Validate YAML file
assert_valid_yaml ".astrolock/astrolock.yaml"

# Get value from YAML
yaml_get "config.yaml" ".site.title"

# Assert YAML value equals expected
assert_yaml_equals "config.yaml" ".site.title" "My Site"

# Assert YAML path exists
assert_yaml_exists "config.yaml" ".collections.blog"

# Assert collection exists in config
assert_collection_exists "blog"

# Assert collection has content type
assert_collection_content_type "blog" "text"
# Validate frontmatter in markdown
assert_valid_frontmatter "content/blog/post.md"

# Get frontmatter value
get_frontmatter_value "content/blog/post.md" "title"

# Assert frontmatter field equals expected
assert_frontmatter_equals "content/blog/post.md" "title" "My Post"

# Assert frontmatter field exists
assert_frontmatter_exists "content/blog/post.md" "date"
# Run astrolock command
run_astrolock content blog "My Post"

# Run with input (for interactive commands)
run_astrolock_with_input "Site\nDescription" init --force

# Assert command succeeds
assert_astrolock_success content blog "Post"

# Assert command fails
assert_astrolock_fails content nonexistent "Post"
# Assert file exists (from bats-file)
assert_file_exist "content/blog/post.md"

# Assert directory exists
assert_dir_exist "content/blog"

# Assert directory structure
assert_directory_structure \
  "content/blog" \
  "content/pages" \
  "public/images"

# Assert file structure
assert_file_structure \
  "content/blog/-index.md" \
  "content/pages/about.md"
# Assert date is ISO 8601 format
assert_iso8601_date "2025-12-06T12:00:00Z"

# Assert string is URL-friendly slug
assert_is_slug "my-blog-post"

# Assert output contains text
assert_output_contains "Success"

# Assert output does not contain text
assert_output_not_contains "Error"
# Setup mock binary directory
setup_mock_bin

# Create mock command
mock_command "yarn" 0  # Exit code 0

# Mock with custom output
mock_command_with_output "aws" "Success" 0

# Assert mock was called
assert_mock_called "yarn"

# Assert mock was NOT called
assert_mock_not_called "netlify"

# Get mock call count
get_mock_call_count "yarn"

Writing New Integration Tests

1. Choose the Right Test Suite

Add tests to existing suite or create new one:

# Add to existing suite
vim cli/test/integration/content.bats

# Create new suite
vim cli/test/integration/my-feature.bats
chmod +x cli/test/integration/my-feature.bats

2. Write Test Cases

Follow the arrange-act-assert pattern:

@test "content creates blog post with valid frontmatter" {
  # Arrange: Setup test environment (done in setup())

  # Act: Execute command
  run_astrolock content blog "Test Post"

  # Assert: Verify results
  assert_success
  assert_file_exist "content/blog/test-post.md"
  assert_valid_frontmatter "content/blog/test-post.md"
  assert_frontmatter_equals "content/blog/test-post.md" "title" "Test Post"
  assert_frontmatter_equals "content/blog/test-post.md" "draft" "false"
}

3. Test Error Conditions

@test "content fails for unknown collection" {
  run_astrolock content nonexistent "Test"

  assert_failure
  assert_output_contains "Unknown collection"
}

@test "content prevents duplicate filenames" {
  # Create first post
  run_astrolock content blog "Duplicate"
  assert_success

  # Try to create duplicate
  run_astrolock content blog "Duplicate"
  assert_failure
  assert_output_contains "already exists"
}

4. Test Interactive Commands

For commands that read from stdin:

@test "collection add creates text collection" {
  # Input format: type\nname\ndisplay\nauthor\ncta
  run_astrolock_with_input "1\narticles\nArticles\nauthor\nRead" collection add

  assert_success
  assert_collection_exists "articles"
  assert_collection_content_type "articles" "text"
}

5. Mock External Dependencies

For commands that call external tools:

setup() {
  export TEST_TEMP_DIR="$(mktemp -d)"
  export USER_ROOT="$TEST_TEMP_DIR"
  cd "$TEST_TEMP_DIR"

  create_initialized_site

  # Mock yarn to avoid actual builds
  setup_mock_bin
  mock_command "yarn" 0
}

@test "build runs yarn install if needed" {
  run_astrolock build

  assert_success
  assert_mock_called "yarn"
}

Test Isolation

Each test runs in complete isolation:

  1. Unique temp directory - mktemp -d creates fresh directory per test
  2. Clean environment - No shared state between tests
  3. Automatic cleanup - teardown() removes all test artifacts
  4. Independent execution - Tests can run in any order
setup() {
  # Each test gets unique directory
  export TEST_TEMP_DIR="$(mktemp -d)"  # /tmp/tmp.XYZ123
  export USER_ROOT="$TEST_TEMP_DIR"
  cd "$TEST_TEMP_DIR"

  # Initialize fresh site
  create_initialized_site
}

teardown() {
  # Cleanup is guaranteed even if test fails
  cleanup_test_dir  # Removes $TEST_TEMP_DIR
}

This ensures:

  • Tests never interfere with each other
  • Tests never modify your actual filesystem
  • Failed tests don’t leave artifacts
  • Tests produce consistent results

Debugging Integration Tests

cd cli

# Run just one test file
./test/run-integration integration/init.bats

# BATS doesn't support running individual @test,
# but you can temporarily comment out other tests
cd cli

# See detailed output from commands
./test/run-integration --verbose integration/init.bats

# See BATS internal debugging
./test/run-integration --trace integration/init.bats
@test "debug by inspecting files" {
  run_astrolock init --force <<< "Test\nSite"

  # Print directory tree to BATS output (fd 3)
  debug_print_tree >&3

  # Print file contents
  debug_print_file ".astrolock/astrolock.yaml" >&3

  assert_success
}
# Temporarily disable cleanup to inspect files
teardown() {
  echo "Test dir: $TEST_TEMP_DIR" >&3
  # Comment out: cleanup_test_dir
}

# Run test, then inspect directory manually
cd $TEST_TEMP_DIR  # From test output
ls -la
cat .astrolock/astrolock.yaml

Installing BATS Dependencies

Integration tests require BATS and helper libraries:

# Install BATS
brew install bats-core

# Install helper libraries
brew tap kaos/shell
brew install bats-assert bats-support bats-file

Best Practices

# Good ✓
@test "init creates .astrolock/astrolock.yaml with valid structure"
@test "content fails when collection does not exist"

# Bad ✗
@test "test1"
@test "it works"
# Good ✓
@test "content creates file with correct filename" {
  run_astrolock content blog "My Post"
  assert_file_exist "content/blog/my-post.md"
}

@test "content creates file with valid frontmatter" {
  run_astrolock content blog "My Post"
  assert_valid_frontmatter "content/blog/my-post.md"
}

# Bad ✗
@test "content creates everything correctly" {
  # Tests too many things at once
  run_astrolock content blog "My Post"
  assert_file_exist "content/blog/my-post.md"
  assert_valid_frontmatter "content/blog/my-post.md"
  assert_frontmatter_equals "content/blog/my-post.md" "title" "My Post"
  assert_frontmatter_equals "content/blog/my-post.md" "draft" "false"
  assert_dir_exist "content/blog"
  # ... 20 more assertions
}
# Good ✓
assert_file_exist "content/blog/post.md"
assert_valid_yaml ".astrolock/astrolock.yaml"
assert_frontmatter_equals "post.md" "title" "My Post"

# Bad ✗
[ -f "content/blog/post.md" ] || fail "File missing"
yq eval '.' ".astrolock/astrolock.yaml" || fail "Invalid YAML"
grep "title: My Post" post.md || fail "Title wrong"
# Good ✓
setup() {
  # ...
  setup_mock_bin
  mock_command "yarn" 0      # Mock expensive builds
  mock_command "aws" 0       # Mock cloud CLI
  mock_command "netlify" 0   # Mock deployment
}

# Bad ✗
@test "deploy to AWS" {
  # Actually calls AWS CLI - slow, expensive, requires credentials
  run_astrolock deploy production --execute
}

Creating Test Sites

# Create temporary directory
mkdir ~/test-site && cd ~/test-site

# Initialize with Astrolock
astrolock init --defaults

# Test
astrolock write

Debugging Tips

export astrolock_debug=1
astrolock {command}
# See what config is being loaded
astrolock build --verbose
# Check compiled CLI
less bin/astrolock

# Check generated templates
less cli/lib/templates.sh

Common Issues

After bashly changes:

bashly generate  # Regenerate CLI