chore: remove dead agent schema validation infrastructure
The *.agent.yaml format was replaced by SKILL.md-based agents. Zero agent YAML files remain in src/, so remove the Zod schema, validator CLI, fixture-based test suite (52 fixtures), unit tests, CLI integration tests, and the CI steps that invoked them. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f3f606a9ce
commit
f0c7cf41c7
|
|
@ -4,8 +4,6 @@ name: Quality & Validation
|
|||
# - Prettier (formatting)
|
||||
# - ESLint (linting)
|
||||
# - markdownlint (markdown quality)
|
||||
# - Schema validation (YAML structure)
|
||||
# - Agent schema tests (fixture-based validation)
|
||||
# - Installation component tests (compilation)
|
||||
# Keep this workflow aligned with `npm run quality` in `package.json`.
|
||||
|
||||
|
|
@ -105,12 +103,6 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Validate YAML schemas
|
||||
run: npm run validate:schemas
|
||||
|
||||
- name: Run agent schema validation tests
|
||||
run: npm run test:schemas
|
||||
|
||||
- name: Test agent compilation components
|
||||
run: npm run test:install
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ tools/build-docs.mjs
|
|||
tools/fix-doc-links.js
|
||||
tools/validate-doc-links.js
|
||||
tools/validate-file-refs.js
|
||||
tools/validate-agent-schema.js
|
||||
|
||||
# Images (branding/marketing only)
|
||||
banner-bmad-method.png
|
||||
|
|
|
|||
|
|
@ -146,7 +146,6 @@ Keep messages under 72 characters. Each commit = one logical change.
|
|||
- Web/planning agents can be larger with complex tasks
|
||||
- Everything is natural language (markdown) — no code in core framework
|
||||
- Use BMad modules for domain-specific features
|
||||
- Validate YAML schemas: `npm run validate:schemas`
|
||||
- Validate file references: `npm run validate:refs`
|
||||
|
||||
### File-Pattern-to-Validator Mapping
|
||||
|
|
|
|||
|
|
@ -39,15 +39,12 @@
|
|||
"lint:fix": "eslint . --ext .js,.cjs,.mjs,.yaml --fix",
|
||||
"lint:md": "markdownlint-cli2 \"**/*.md\"",
|
||||
"prepare": "command -v husky >/dev/null 2>&1 && husky || exit 0",
|
||||
"quality": "npm run format:check && npm run lint && npm run lint:md && npm run docs:build && npm run validate:schemas && npm run test:schemas && npm run test:install && npm run validate:refs",
|
||||
"quality": "npm run format:check && npm run lint && npm run lint:md && npm run docs:build && npm run test:install && npm run validate:refs",
|
||||
"rebundle": "node tools/cli/bundlers/bundle-web.js rebundle",
|
||||
"test": "npm run test:schemas && npm run test:refs && npm run test:install && npm run validate:schemas && npm run lint && npm run lint:md && npm run format:check",
|
||||
"test:coverage": "c8 --reporter=text --reporter=html npm run test:schemas",
|
||||
"test": "npm run test:refs && npm run test:install && npm run lint && npm run lint:md && npm run format:check",
|
||||
"test:install": "node test/test-installation-components.js",
|
||||
"test:refs": "node test/test-file-refs-csv.js",
|
||||
"test:schemas": "node test/test-agent-schema.js",
|
||||
"validate:refs": "node tools/validate-file-refs.js --strict",
|
||||
"validate:schemas": "node tools/validate-agent-schema.js"
|
||||
"validate:refs": "node tools/validate-file-refs.js --strict"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,cjs,mjs}": [
|
||||
|
|
|
|||
301
test/README.md
301
test/README.md
|
|
@ -1,295 +1,38 @@
|
|||
# Agent Schema Validation Test Suite
|
||||
# Test Suite
|
||||
|
||||
Comprehensive test coverage for the BMAD agent schema validation system.
|
||||
|
||||
## Overview
|
||||
|
||||
This test suite validates the Zod-based schema validator (`tools/schema/agent.js`) that ensures all `*.agent.yaml` files conform to the BMAD agent specification.
|
||||
|
||||
## Test Statistics
|
||||
|
||||
- **Total Test Fixtures**: 50
|
||||
- **Valid Test Cases**: 18
|
||||
- **Invalid Test Cases**: 32
|
||||
- **Code Coverage**: 100% all metrics (statements, branches, functions, lines)
|
||||
- **Exit Code Tests**: 4 CLI integration tests
|
||||
Tests for the BMAD-METHOD tooling infrastructure.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
npm test
|
||||
# Run all quality checks
|
||||
npm run quality
|
||||
|
||||
# Run with coverage report
|
||||
npm run test:coverage
|
||||
|
||||
# Run CLI integration tests
|
||||
./test/test-cli-integration.sh
|
||||
|
||||
# Validate actual agent files
|
||||
npm run validate:schemas
|
||||
# Run individual test suites
|
||||
npm run test:install # Installation component tests
|
||||
npm run test:refs # File reference CSV tests
|
||||
npm run validate:refs # File reference validation (strict)
|
||||
```
|
||||
|
||||
## Test Organization
|
||||
|
||||
### Test Fixtures
|
||||
|
||||
Located in `test/fixtures/agent-schema/`, organized by category:
|
||||
|
||||
```
|
||||
test/fixtures/agent-schema/
|
||||
├── valid/ # 15 fixtures that should pass
|
||||
│ ├── top-level/ # Basic structure tests
|
||||
│ ├── metadata/ # Metadata field tests
|
||||
│ ├── persona/ # Persona field tests
|
||||
│ ├── critical-actions/ # Critical actions tests
|
||||
│ ├── menu/ # Menu structure tests
|
||||
│ ├── menu-commands/ # Command target tests
|
||||
│ ├── menu-triggers/ # Trigger format tests
|
||||
│ └── prompts/ # Prompts field tests
|
||||
└── invalid/ # 32 fixtures that should fail
|
||||
├── top-level/ # Structure errors
|
||||
├── metadata/ # Metadata validation errors
|
||||
├── persona/ # Persona validation errors
|
||||
├── critical-actions/ # Critical actions errors
|
||||
├── menu/ # Menu errors
|
||||
├── menu-commands/ # Command target errors
|
||||
├── menu-triggers/ # Trigger format errors
|
||||
├── prompts/ # Prompts errors
|
||||
└── yaml-errors/ # YAML parsing errors
|
||||
```
|
||||
|
||||
## Test Categories
|
||||
|
||||
### 1. Top-Level Structure Tests (4 fixtures)
|
||||
|
||||
Tests the root-level agent structure:
|
||||
|
||||
- ✅ Valid: Minimal core agent with required fields
|
||||
- ❌ Invalid: Empty YAML file
|
||||
- ❌ Invalid: Missing `agent` key
|
||||
- ❌ Invalid: Extra top-level keys (strict mode)
|
||||
|
||||
### 2. Metadata Field Tests (7 fixtures)
|
||||
|
||||
Tests agent metadata validation:
|
||||
|
||||
- ✅ Valid: Module agent with correct `module` field
|
||||
- ❌ Invalid: Missing required fields (`id`, `name`, `title`, `icon`)
|
||||
- ❌ Invalid: Empty strings in metadata
|
||||
- ❌ Invalid: Module agent missing `module` field
|
||||
- ❌ Invalid: Core agent with unexpected `module` field
|
||||
- ❌ Invalid: Wrong `module` value (doesn't match path)
|
||||
- ❌ Invalid: Extra unknown metadata fields
|
||||
|
||||
### 3. Persona Field Tests (6 fixtures)
|
||||
|
||||
Tests persona structure and validation:
|
||||
|
||||
- ✅ Valid: Complete persona with all fields
|
||||
- ❌ Invalid: Missing required fields (`role`, `identity`, etc.)
|
||||
- ❌ Invalid: `principles` as string instead of array
|
||||
- ❌ Invalid: Empty `principles` array
|
||||
- ❌ Invalid: Empty strings in `principles` array
|
||||
- ❌ Invalid: Extra unknown persona fields
|
||||
|
||||
### 4. Critical Actions Tests (5 fixtures)
|
||||
|
||||
Tests optional `critical_actions` field:
|
||||
|
||||
- ✅ Valid: No `critical_actions` field (optional)
|
||||
- ✅ Valid: Empty `critical_actions` array
|
||||
- ✅ Valid: Valid action strings
|
||||
- ❌ Invalid: Empty strings in actions
|
||||
- ❌ Invalid: Actions as non-array type
|
||||
|
||||
### 5. Menu Field Tests (4 fixtures)
|
||||
|
||||
Tests required menu structure:
|
||||
|
||||
- ✅ Valid: Single menu item
|
||||
- ✅ Valid: Multiple menu items with different commands
|
||||
- ❌ Invalid: Missing `menu` field
|
||||
- ❌ Invalid: Empty `menu` array
|
||||
|
||||
### 6. Menu Command Target Tests (4 fixtures)
|
||||
|
||||
Tests menu item command targets:
|
||||
|
||||
- ✅ Valid: All 6 command types (`workflow`, `validate-workflow`, `exec`, `action`, `tmpl`, `data`)
|
||||
- ✅ Valid: Multiple command targets in one menu item
|
||||
- ❌ Invalid: No command target fields
|
||||
- ❌ Invalid: Empty string command targets
|
||||
|
||||
### 7. Menu Trigger Validation Tests (7 fixtures)
|
||||
|
||||
Tests trigger format enforcement:
|
||||
|
||||
- ✅ Valid: Kebab-case triggers (`help`, `list-tasks`, `multi-word-trigger`)
|
||||
- ❌ Invalid: Leading asterisk (`*help`)
|
||||
- ❌ Invalid: CamelCase (`listTasks`)
|
||||
- ❌ Invalid: Snake_case (`list_tasks`)
|
||||
- ❌ Invalid: Spaces (`list tasks`)
|
||||
- ❌ Invalid: Duplicate triggers within agent
|
||||
- ❌ Invalid: Empty trigger string
|
||||
|
||||
### 8. Prompts Field Tests (8 fixtures)
|
||||
|
||||
Tests optional `prompts` field:
|
||||
|
||||
- ✅ Valid: No `prompts` field (optional)
|
||||
- ✅ Valid: Empty `prompts` array
|
||||
- ✅ Valid: Prompts with required `id` and `content`
|
||||
- ✅ Valid: Prompts with optional `description`
|
||||
- ❌ Invalid: Missing `id`
|
||||
- ❌ Invalid: Missing `content`
|
||||
- ❌ Invalid: Empty `content` string
|
||||
- ❌ Invalid: Extra unknown prompt fields
|
||||
|
||||
### 9. YAML Parsing Tests (2 fixtures)
|
||||
|
||||
Tests YAML parsing error handling:
|
||||
|
||||
- ❌ Invalid: Malformed YAML syntax
|
||||
- ❌ Invalid: Invalid indentation
|
||||
|
||||
## Test Scripts
|
||||
|
||||
### Main Test Runner
|
||||
### Installation Component Tests
|
||||
|
||||
**File**: `test/test-agent-schema.js`
|
||||
**File**: `test/test-installation-components.js`
|
||||
|
||||
Automated test runner that:
|
||||
Validates that the installer compiles and assembles agents correctly.
|
||||
|
||||
- Loads all fixtures from `test/fixtures/agent-schema/`
|
||||
- Validates each against the schema
|
||||
- Compares results with expected outcomes (parsed from YAML comments)
|
||||
- Reports detailed results by category
|
||||
- Exits with code 0 (pass) or 1 (fail)
|
||||
### File Reference Tests
|
||||
|
||||
**Usage**:
|
||||
**File**: `test/test-file-refs-csv.js`
|
||||
|
||||
```bash
|
||||
npm test
|
||||
# or
|
||||
node test/test-agent-schema.js
|
||||
Tests the CSV-based file reference validation logic.
|
||||
|
||||
## Test Fixtures
|
||||
|
||||
Located in `test/fixtures/`:
|
||||
|
||||
```text
|
||||
test/fixtures/
|
||||
└── file-refs-csv/ # Fixtures for file reference CSV tests
|
||||
```
|
||||
|
||||
### Coverage Report
|
||||
|
||||
**Command**: `npm run test:coverage`
|
||||
|
||||
Generates code coverage report using c8:
|
||||
|
||||
- Text output to console
|
||||
- HTML report in `coverage/` directory
|
||||
- Tracks statement, branch, function, and line coverage
|
||||
|
||||
**Current Coverage**:
|
||||
|
||||
- Statements: 100%
|
||||
- Branches: 100%
|
||||
- Functions: 100%
|
||||
- Lines: 100%
|
||||
|
||||
### CLI Integration Tests
|
||||
|
||||
**File**: `test/test-cli-integration.sh`
|
||||
|
||||
Bash script that tests CLI behavior:
|
||||
|
||||
1. Validates existing agent files
|
||||
2. Verifies test fixture validation
|
||||
3. Checks exit code 0 for valid files
|
||||
4. Verifies test runner output format
|
||||
|
||||
**Usage**:
|
||||
|
||||
```bash
|
||||
./test/test-cli-integration.sh
|
||||
```
|
||||
|
||||
## Manual Testing
|
||||
|
||||
See **[MANUAL-TESTING.md](./MANUAL-TESTING.md)** for detailed manual testing procedures, including:
|
||||
|
||||
- Testing with invalid files
|
||||
- GitHub Actions workflow verification
|
||||
- Troubleshooting guide
|
||||
- PR merge blocking tests
|
||||
|
||||
## Coverage Achievement
|
||||
|
||||
**100% code coverage achieved!** All branches, statements, functions, and lines in the validation logic are tested.
|
||||
|
||||
Edge cases covered include:
|
||||
|
||||
- Malformed module paths (e.g., `src/bmm` without `/agents/`)
|
||||
- Empty module names in paths (e.g., `src/modules//agents/`)
|
||||
- Whitespace-only module field values
|
||||
- All validation error paths
|
||||
- All success paths for valid configurations
|
||||
|
||||
## Adding New Tests
|
||||
|
||||
To add new test cases:
|
||||
|
||||
1. Create a new `.agent.yaml` file in the appropriate `valid/` or `invalid/` subdirectory
|
||||
2. Add comment metadata at the top:
|
||||
|
||||
```yaml
|
||||
# Test: Description of what this tests
|
||||
# Expected: PASS (or FAIL - error description)
|
||||
# Path context: src/bmm/agents/test.agent.yaml (if needed)
|
||||
```
|
||||
|
||||
3. Run the test suite to verify: `npm test`
|
||||
|
||||
## Integration with CI/CD
|
||||
|
||||
The validation is integrated into the GitHub Actions workflow:
|
||||
|
||||
**File**: `.github/workflows/lint.yaml`
|
||||
|
||||
**Job**: `agent-schema`
|
||||
|
||||
**Runs on**: All pull requests
|
||||
|
||||
**Blocks merge if**: Validation fails
|
||||
|
||||
## Files
|
||||
|
||||
- `test/test-agent-schema.js` - Main test runner
|
||||
- `test/test-cli-integration.sh` - CLI integration tests
|
||||
- `test/MANUAL-TESTING.md` - Manual testing guide
|
||||
- `test/fixtures/agent-schema/` - Test fixtures (47 files)
|
||||
- `tools/schema/agent.js` - Validation logic (under test)
|
||||
- `tools/validate-agent-schema.js` - CLI wrapper
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **zod**: Schema validation library
|
||||
- **yaml**: YAML parsing
|
||||
- **glob**: File pattern matching
|
||||
- **c8**: Code coverage reporting
|
||||
|
||||
## Success Criteria
|
||||
|
||||
All success criteria from the original task have been exceeded:
|
||||
|
||||
- ✅ 50 test fixtures covering all validation rules (target: 47+)
|
||||
- ✅ Automated test runner with detailed reporting
|
||||
- ✅ CLI integration tests verifying exit codes and output
|
||||
- ✅ Manual testing documentation
|
||||
- ✅ **100% code coverage achieved** (target: 99%+)
|
||||
- ✅ Both positive and negative test cases
|
||||
- ✅ Clear and actionable error messages
|
||||
- ✅ GitHub Actions integration verified
|
||||
- ✅ Aggressive defensive assertions implemented
|
||||
|
||||
## Resources
|
||||
|
||||
- **Schema Documentation**: `schema-classification.md`
|
||||
- **Validator Implementation**: `tools/schema/agent.js`
|
||||
- **CLI Tool**: `tools/validate-agent-schema.js`
|
||||
- **Project Guidelines**: `CLAUDE.md`
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
# Test: critical_actions as non-array
|
||||
# Expected: FAIL
|
||||
# Error code: invalid_type
|
||||
# Error path: agent.critical_actions
|
||||
# Error expected: array
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: actions-string
|
||||
name: Actions String
|
||||
title: Actions String
|
||||
icon: ❌
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
|
||||
critical_actions: This should be an array
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
# Test: critical_actions with empty strings
|
||||
# Expected: FAIL
|
||||
# Error code: custom
|
||||
# Error path: agent.critical_actions[1]
|
||||
# Error message: agent.critical_actions[] must be a non-empty string
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: empty-action-string
|
||||
name: Empty Action String
|
||||
title: Empty Action String
|
||||
icon: ❌
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
|
||||
critical_actions:
|
||||
- Valid action
|
||||
- " "
|
||||
- Another valid action
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
# Test: Menu item with empty string command target
|
||||
# Expected: FAIL
|
||||
# Error code: custom
|
||||
# Error path: agent.menu[0].action
|
||||
# Error message: agent.menu[].action must be a non-empty string
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: empty-command
|
||||
name: Empty Command Target
|
||||
title: Empty Command
|
||||
icon: ❌
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: " "
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Test: Menu item with no command target fields
|
||||
# Expected: FAIL
|
||||
# Error code: custom
|
||||
# Error path: agent.menu[0]
|
||||
# Error message: agent.menu[] entries must include at least one command target field
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: no-command
|
||||
name: No Command Target
|
||||
title: No Command
|
||||
icon: ❌
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help but no command target
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
# Test: CamelCase trigger
|
||||
# Expected: FAIL
|
||||
# Error code: custom
|
||||
# Error path: agent.menu[0].trigger
|
||||
# Error message: agent.menu[].trigger must be kebab-case (lowercase words separated by hyphen)
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: camel-case-trigger
|
||||
name: CamelCase Trigger
|
||||
title: CamelCase
|
||||
icon: ❌
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
|
||||
menu:
|
||||
- trigger: listTasks
|
||||
description: Invalid CamelCase trigger
|
||||
action: list_tasks
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
# Test: Compound trigger with invalid format
|
||||
# Expected: FAIL
|
||||
# Error code: custom
|
||||
# Error path: agent.menu[0].trigger
|
||||
# Error message: agent.menu[].trigger compound format error: invalid compound trigger format
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: compound-invalid-format
|
||||
name: Invalid Format
|
||||
title: Invalid Format Test
|
||||
icon: 🧪
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
|
||||
menu:
|
||||
- trigger: TS or tech-spec
|
||||
description: Missing fuzzy match clause
|
||||
action: test
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
# Test: Compound trigger with old format (no longer supported)
|
||||
# Expected: FAIL
|
||||
# Error code: custom
|
||||
# Error path: agent.menu[0].trigger
|
||||
# Error message: agent.menu[].trigger compound format error: invalid compound trigger format
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: compound-mismatched-kebab
|
||||
name: Old Format
|
||||
title: Old Format Test
|
||||
icon: 🧪
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
|
||||
menu:
|
||||
- trigger: TS or tech-spec or fuzzy match on tech-spec
|
||||
description: Old format with middle kebab-case (no longer supported)
|
||||
action: test
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
# Test: Duplicate triggers within same agent
|
||||
# Expected: FAIL
|
||||
# Error code: custom
|
||||
# Error path: agent.menu[2].trigger
|
||||
# Error message: agent.menu[].trigger duplicates "help" within the same agent
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: duplicate-triggers
|
||||
name: Duplicate Triggers
|
||||
title: Duplicate
|
||||
icon: ❌
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: First help command
|
||||
action: display_help
|
||||
- trigger: list-tasks
|
||||
description: List tasks
|
||||
action: list_tasks
|
||||
- trigger: help
|
||||
description: Duplicate help command
|
||||
action: show_help
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
# Test: Empty trigger string
|
||||
# Expected: FAIL
|
||||
# Error code: custom
|
||||
# Error path: agent.menu[0].trigger
|
||||
# Error message: agent.menu[].trigger must be a non-empty string
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: empty-trigger
|
||||
name: Empty Trigger
|
||||
title: Empty
|
||||
icon: ❌
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
|
||||
menu:
|
||||
- trigger: " "
|
||||
description: Empty trigger
|
||||
action: display_help
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
# Test: Trigger with leading asterisk
|
||||
# Expected: FAIL
|
||||
# Error code: custom
|
||||
# Error path: agent.menu[0].trigger
|
||||
# Error message: agent.menu[].trigger must be kebab-case (lowercase words separated by hyphen)
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: asterisk-trigger
|
||||
name: Asterisk Trigger
|
||||
title: Asterisk
|
||||
icon: ❌
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
|
||||
menu:
|
||||
- trigger: "*help"
|
||||
description: Invalid trigger with asterisk
|
||||
action: display_help
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
# Test: Snake_case trigger
|
||||
# Expected: FAIL
|
||||
# Error code: custom
|
||||
# Error path: agent.menu[0].trigger
|
||||
# Error message: agent.menu[].trigger must be kebab-case (lowercase words separated by hyphen)
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: snake-case-trigger
|
||||
name: Snake Case Trigger
|
||||
title: Snake Case
|
||||
icon: ❌
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
|
||||
menu:
|
||||
- trigger: list_tasks
|
||||
description: Invalid snake_case trigger
|
||||
action: list_tasks
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
# Test: Trigger with spaces
|
||||
# Expected: FAIL
|
||||
# Error code: custom
|
||||
# Error path: agent.menu[0].trigger
|
||||
# Error message: agent.menu[].trigger must be kebab-case (lowercase words separated by hyphen)
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: spaces-trigger
|
||||
name: Spaces Trigger
|
||||
title: Spaces
|
||||
icon: ❌
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
|
||||
menu:
|
||||
- trigger: list tasks
|
||||
description: Invalid trigger with spaces
|
||||
action: list_tasks
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
# Test: Empty menu array
|
||||
# Expected: FAIL
|
||||
# Error code: too_small
|
||||
# Error path: agent.menu
|
||||
# Error minimum: 1
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: empty-menu
|
||||
name: Empty Menu
|
||||
title: Empty Menu
|
||||
icon: ❌
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
|
||||
menu: []
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
# Test: Missing menu field
|
||||
# Expected: FAIL
|
||||
# Error code: invalid_type
|
||||
# Error path: agent.menu
|
||||
# Error expected: array
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: missing-menu
|
||||
name: Missing Menu
|
||||
title: Missing Menu
|
||||
icon: ❌
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
# Test: Module field with whitespace only
|
||||
# Expected: FAIL
|
||||
# Error code: custom
|
||||
# Error path: agent.metadata.module
|
||||
# Error message: agent.metadata.module must be a non-empty string
|
||||
# Path context: src/bmm/agents/empty-module-string.agent.yaml
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: empty-module
|
||||
name: Empty Module String
|
||||
title: Empty Module
|
||||
icon: ❌
|
||||
module: " "
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Test: Empty string in metadata.name field
|
||||
# Expected: FAIL
|
||||
# Error code: custom
|
||||
# Error path: agent.metadata.name
|
||||
# Error message: agent.metadata.name must be a non-empty string
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: empty-name-test
|
||||
name: " "
|
||||
title: Empty Name Test
|
||||
icon: ❌
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
# Test: Extra unknown fields in metadata
|
||||
# Expected: FAIL
|
||||
# Error code: unrecognized_keys
|
||||
# Error path: agent.metadata
|
||||
# Error keys: ["unknown_field", "another_extra"]
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: extra-fields
|
||||
name: Extra Fields
|
||||
title: Extra Fields
|
||||
icon: ❌
|
||||
hasSidecar: false
|
||||
unknown_field: This is not allowed
|
||||
another_extra: Also invalid
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# Test: Missing required metadata.id field
|
||||
# Expected: FAIL
|
||||
# Error code: invalid_type
|
||||
# Error path: agent.metadata.id
|
||||
# Error expected: string
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
name: Missing ID Agent
|
||||
title: Missing ID
|
||||
icon: ❌
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Test: Empty principles array
|
||||
# Expected: FAIL
|
||||
# Error code: too_small
|
||||
# Error path: agent.persona.principles
|
||||
# Error minimum: 1
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: empty-principles
|
||||
name: Empty Principles
|
||||
title: Empty Principles
|
||||
icon: ❌
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles: []
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
# Test: Empty string in principles array
|
||||
# Expected: FAIL
|
||||
# Error code: custom
|
||||
# Error path: agent.persona.principles[1]
|
||||
# Error message: agent.persona.principles[] must be a non-empty string
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: empty-principle-string
|
||||
name: Empty Principle String
|
||||
title: Empty Principle
|
||||
icon: ❌
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Valid principle
|
||||
- " "
|
||||
- Another valid principle
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
# Test: Extra unknown fields in persona
|
||||
# Expected: FAIL
|
||||
# Error code: unrecognized_keys
|
||||
# Error path: agent.persona
|
||||
# Error keys: ["extra_field", "another_extra"]
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: extra-persona-fields
|
||||
name: Extra Persona Fields
|
||||
title: Extra Persona
|
||||
icon: ❌
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
extra_field: Not allowed
|
||||
another_extra: Also invalid
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Test: Missing required persona.role field
|
||||
# Expected: FAIL
|
||||
# Error code: invalid_type
|
||||
# Error path: agent.persona.role
|
||||
# Error expected: string
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: missing-role
|
||||
name: Missing Role
|
||||
title: Missing Role
|
||||
icon: ❌
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
# Test: Prompt with empty content string
|
||||
# Expected: FAIL
|
||||
# Error code: custom
|
||||
# Error path: agent.prompts[0].content
|
||||
# Error message: agent.prompts[].content must be a non-empty string
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: empty-content
|
||||
name: Empty Content
|
||||
title: Empty Content
|
||||
icon: ❌
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
|
||||
prompts:
|
||||
- id: prompt1
|
||||
content: " "
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
# Test: Extra unknown fields in prompts
|
||||
# Expected: FAIL
|
||||
# Error code: unrecognized_keys
|
||||
# Error path: agent.prompts[0]
|
||||
# Error keys: ["extra_field"]
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: extra-prompt-fields
|
||||
name: Extra Prompt Fields
|
||||
title: Extra Fields
|
||||
icon: ❌
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
|
||||
prompts:
|
||||
- id: prompt1
|
||||
content: Valid content
|
||||
description: Valid description
|
||||
extra_field: Not allowed
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
# Test: Prompt missing required content field
|
||||
# Expected: FAIL
|
||||
# Error code: invalid_type
|
||||
# Error path: agent.prompts[0].content
|
||||
# Error expected: string
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: prompt-missing-content
|
||||
name: Prompt Missing Content
|
||||
title: Missing Content
|
||||
icon: ❌
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
|
||||
prompts:
|
||||
- id: prompt1
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
# Test: Prompt missing required id field
|
||||
# Expected: FAIL
|
||||
# Error code: invalid_type
|
||||
# Error path: agent.prompts[0].id
|
||||
# Error expected: string
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: prompt-missing-id
|
||||
name: Prompt Missing ID
|
||||
title: Missing ID
|
||||
icon: ❌
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
|
||||
prompts:
|
||||
- content: Prompt without ID
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# Test: Empty YAML file
|
||||
# Expected: FAIL
|
||||
# Error code: invalid_type
|
||||
# Error path:
|
||||
# Error expected: object
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
# Test: Extra top-level keys beyond 'agent'
|
||||
# Expected: FAIL
|
||||
# Error code: unrecognized_keys
|
||||
# Error path:
|
||||
# Error keys: ["extra_key", "another_extra"]
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: extra-test
|
||||
name: Extra Test Agent
|
||||
title: Extra Test
|
||||
icon: 🧪
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
||||
extra_key: This should not be allowed
|
||||
another_extra: Also invalid
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
# Test: Missing required 'agent' top-level key
|
||||
# Expected: FAIL
|
||||
# Error code: invalid_type
|
||||
# Error path: agent
|
||||
# Error expected: object
|
||||
|
||||
metadata:
|
||||
id: bad-test
|
||||
name: Bad Test Agent
|
||||
title: Bad Test
|
||||
icon: ❌
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
# Test: Invalid YAML structure with inconsistent indentation
|
||||
# Expected: FAIL - YAML parse error
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: invalid-indent
|
||||
name: Invalid Indentation
|
||||
title: Invalid
|
||||
icon: ❌
|
||||
persona:
|
||||
role: Test
|
||||
identity: Test
|
||||
communication_style: Test
|
||||
principles:
|
||||
- Test
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Help
|
||||
action: help
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
# Test: Malformed YAML with syntax errors
|
||||
# Expected: FAIL - YAML parse error
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: malformed
|
||||
name: Malformed YAML
|
||||
title: [Malformed
|
||||
icon: 🧪
|
||||
persona:
|
||||
role: Test
|
||||
identity: Test
|
||||
communication_style: Test
|
||||
principles:
|
||||
- Test
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Help
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Test: Empty critical_actions array
|
||||
# Expected: PASS - empty array is valid for optional field
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: empty-critical-actions
|
||||
name: Empty Critical Actions
|
||||
title: Empty Critical Actions
|
||||
icon: 🧪
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent with empty critical actions
|
||||
identity: I am a test agent with empty critical actions array.
|
||||
communication_style: Clear
|
||||
principles:
|
||||
- Test empty arrays
|
||||
|
||||
critical_actions: []
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
# Test: No critical_actions field (optional)
|
||||
# Expected: PASS
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: no-critical-actions
|
||||
name: No Critical Actions
|
||||
title: No Critical Actions
|
||||
icon: 🧪
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent without critical actions
|
||||
identity: I am a test agent without critical actions.
|
||||
communication_style: Clear
|
||||
principles:
|
||||
- Test optional fields
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
# Test: critical_actions with valid strings
|
||||
# Expected: PASS
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: valid-critical-actions
|
||||
name: Valid Critical Actions
|
||||
title: Valid Critical Actions
|
||||
icon: 🧪
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent with critical actions
|
||||
identity: I am a test agent with valid critical actions.
|
||||
communication_style: Clear
|
||||
principles:
|
||||
- Test valid arrays
|
||||
|
||||
critical_actions:
|
||||
- Load configuration from disk
|
||||
- Initialize user context
|
||||
- Set communication preferences
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
# Test: Menu items with all valid command target types
|
||||
# Expected: PASS
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: all-commands
|
||||
name: All Command Types
|
||||
title: All Commands
|
||||
icon: 🧪
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent with all command types
|
||||
identity: I test all available command target types.
|
||||
communication_style: Clear
|
||||
principles:
|
||||
- Test all command types
|
||||
|
||||
menu:
|
||||
- trigger: workflow-test
|
||||
description: Test workflow command
|
||||
exec: path/to/workflow
|
||||
- trigger: validate-test
|
||||
description: Test validate-workflow command
|
||||
validate-workflow: path/to/validation
|
||||
- trigger: exec-test
|
||||
description: Test exec command
|
||||
exec: npm test
|
||||
- trigger: action-test
|
||||
description: Test action command
|
||||
action: perform_action
|
||||
- trigger: tmpl-test
|
||||
description: Test tmpl command
|
||||
tmpl: path/to/template
|
||||
- trigger: data-test
|
||||
description: Test data command
|
||||
data: path/to/data
|
||||
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# Test: Menu item with multiple command targets
|
||||
# Expected: PASS - multiple targets are allowed
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: multiple-commands
|
||||
name: Multiple Commands
|
||||
title: Multiple Commands
|
||||
icon: 🧪
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent with multiple command targets
|
||||
identity: I test multiple command targets per menu item.
|
||||
communication_style: Clear
|
||||
principles:
|
||||
- Test multiple targets
|
||||
|
||||
menu:
|
||||
- trigger: multi-command
|
||||
description: Menu item with multiple command targets
|
||||
exec: path/to/workflow
|
||||
action: perform_action
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
# Test: Valid compound triggers
|
||||
# Expected: PASS
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: compound-triggers
|
||||
name: Compound Triggers
|
||||
title: Compound Triggers Test
|
||||
icon: 🧪
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent with compound triggers
|
||||
identity: I test compound trigger validation.
|
||||
communication_style: Clear
|
||||
principles:
|
||||
- Test compound format
|
||||
|
||||
menu:
|
||||
- trigger: TS or fuzzy match on tech-spec
|
||||
description: "[TS] Two-word compound trigger"
|
||||
action: tech_spec
|
||||
- trigger: DS or fuzzy match on dev-story
|
||||
description: "[DS] Another two-word compound trigger"
|
||||
action: dev_story
|
||||
- trigger: WI or fuzzy match on three-name-thing
|
||||
description: "[WI] Three-word compound trigger (uses first 2 words for shortcut)"
|
||||
action: three_name_thing
|
||||
- trigger: H or fuzzy match on help
|
||||
description: "[H] Single-word compound trigger (1-letter shortcut)"
|
||||
action: help
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
# Test: Valid kebab-case triggers
|
||||
# Expected: PASS
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: kebab-triggers
|
||||
name: Kebab Case Triggers
|
||||
title: Kebab Triggers
|
||||
icon: 🧪
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent with kebab-case triggers
|
||||
identity: I test kebab-case trigger validation.
|
||||
communication_style: Clear
|
||||
principles:
|
||||
- Test kebab-case format
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Single word trigger
|
||||
action: display_help
|
||||
- trigger: list-tasks
|
||||
description: Two word trigger
|
||||
action: list_tasks
|
||||
- trigger: three-word-process
|
||||
description: Three word trigger
|
||||
action: init_workflow
|
||||
- trigger: test123
|
||||
description: Trigger with numbers
|
||||
action: test
|
||||
- trigger: multi-word-kebab-case-trigger
|
||||
description: Long kebab-case trigger
|
||||
action: long_action
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
# Test: Menu with multiple valid items using different command types
|
||||
# Expected: PASS
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: multiple-menu
|
||||
name: Multiple Menu Items
|
||||
title: Multiple Menu
|
||||
icon: 🧪
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent with multiple menu items
|
||||
identity: I am a test agent with diverse menu commands.
|
||||
communication_style: Clear
|
||||
principles:
|
||||
- Test multiple menu items
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
- trigger: start-workflow
|
||||
description: Start a workflow
|
||||
exec: path/to/workflow
|
||||
- trigger: execute
|
||||
description: Execute command
|
||||
exec: npm test
|
||||
- trigger: use-template
|
||||
description: Use template
|
||||
tmpl: path/to/template
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
# Test: Menu with single valid item
|
||||
# Expected: PASS
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: single-menu
|
||||
name: Single Menu Item
|
||||
title: Single Menu
|
||||
icon: 🧪
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent with single menu item
|
||||
identity: I am a test agent.
|
||||
communication_style: Clear
|
||||
principles:
|
||||
- Test minimal menu
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help information
|
||||
action: display_help
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Test: Core agent can have module field
|
||||
# Expected: PASS
|
||||
# Note: Core agents can now include module field if needed
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: core-with-module
|
||||
name: Core With Module
|
||||
title: Core Agent
|
||||
icon: ✅
|
||||
module: bmm
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Test: Empty module name in path (src/modules//agents/)
|
||||
# Expected: PASS - treated as core agent (empty module normalizes to null)
|
||||
# Path context: src/modules//agents/test.agent.yaml
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: empty-module-path
|
||||
name: Empty Module in Path
|
||||
title: Empty Module Path
|
||||
icon: 🧪
|
||||
hasSidecar: false
|
||||
# No module field - path has empty module name, treated as core
|
||||
|
||||
persona:
|
||||
role: Test agent for empty module name in path
|
||||
identity: I test the edge case where module name in path is empty.
|
||||
communication_style: Clear
|
||||
principles:
|
||||
- Test path parsing edge cases
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Test: Malformed module path (no slash after module name) treated as core
|
||||
# Expected: PASS - malformed path returns null, treated as core agent
|
||||
# Path context: src/bmm
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: malformed-path
|
||||
name: Malformed Path Test
|
||||
title: Malformed Path
|
||||
icon: 🧪
|
||||
hasSidecar: false
|
||||
# No module field - will be treated as core since path parsing returns null
|
||||
|
||||
persona:
|
||||
role: Test agent for malformed path edge case
|
||||
identity: I test edge cases in path parsing.
|
||||
communication_style: Clear
|
||||
principles:
|
||||
- Test edge case handling
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Test: Valid module agent with correct module field
|
||||
# Expected: PASS
|
||||
# Path context: src/bmm/agents/module-agent-correct.agent.yaml
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: bmm-test
|
||||
name: BMM Test Agent
|
||||
title: BMM Test
|
||||
icon: 🧪
|
||||
module: bmm
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test module agent
|
||||
identity: I am a module-scoped test agent.
|
||||
communication_style: Professional
|
||||
principles:
|
||||
- Test module validation
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# Test: Module agent can omit module field
|
||||
# Expected: PASS
|
||||
# Note: Module field is optional
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: bmm-missing-module
|
||||
name: No Module
|
||||
title: Optional Module
|
||||
icon: ✅
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Test: Module agent can have any module value
|
||||
# Expected: PASS
|
||||
# Note: Module validation removed - agents can declare any module
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: wrong-module
|
||||
name: Any Module
|
||||
title: Any Module Value
|
||||
icon: ✅
|
||||
module: cis
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent
|
||||
identity: Test identity
|
||||
communication_style: Test style
|
||||
principles:
|
||||
- Test principle
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Test: All persona fields properly filled
|
||||
# Expected: PASS
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: complete-persona
|
||||
name: Complete Persona Agent
|
||||
title: Complete Persona
|
||||
icon: 🧪
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Comprehensive test agent with all persona fields
|
||||
identity: I am a test agent designed to validate complete persona structure with multiple characteristics and attributes.
|
||||
communication_style: Professional, clear, and thorough with attention to detail
|
||||
principles:
|
||||
- Validate all persona fields are present
|
||||
- Ensure array fields work correctly
|
||||
- Test comprehensive documentation
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Test: Empty prompts array
|
||||
# Expected: PASS - empty array valid for optional field
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: empty-prompts
|
||||
name: Empty Prompts
|
||||
title: Empty Prompts
|
||||
icon: 🧪
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent with empty prompts
|
||||
identity: I am a test agent with empty prompts array.
|
||||
communication_style: Clear
|
||||
principles:
|
||||
- Test empty arrays
|
||||
|
||||
prompts: []
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
# Test: No prompts field (optional)
|
||||
# Expected: PASS
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: no-prompts
|
||||
name: No Prompts
|
||||
title: No Prompts
|
||||
icon: 🧪
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent without prompts
|
||||
identity: I am a test agent without prompts field.
|
||||
communication_style: Clear
|
||||
principles:
|
||||
- Test optional fields
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
# Test: Prompts with required id and content only
|
||||
# Expected: PASS
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: valid-prompts-minimal
|
||||
name: Valid Prompts Minimal
|
||||
title: Valid Prompts
|
||||
icon: 🧪
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent with minimal prompts
|
||||
identity: I am a test agent with minimal prompt structure.
|
||||
communication_style: Clear
|
||||
principles:
|
||||
- Test minimal prompts
|
||||
|
||||
prompts:
|
||||
- id: prompt1
|
||||
content: This is a valid prompt content
|
||||
- id: prompt2
|
||||
content: Another valid prompt
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
# Test: Prompts with optional description field
|
||||
# Expected: PASS
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: valid-prompts-description
|
||||
name: Valid Prompts With Description
|
||||
title: Valid Prompts Desc
|
||||
icon: 🧪
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent with prompts including descriptions
|
||||
identity: I am a test agent with complete prompt structure.
|
||||
communication_style: Clear
|
||||
principles:
|
||||
- Test complete prompts
|
||||
|
||||
prompts:
|
||||
- id: prompt1
|
||||
content: This is a valid prompt content
|
||||
description: This prompt does something useful
|
||||
- id: prompt2
|
||||
content: Another valid prompt
|
||||
description: This prompt does something else
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Test: Valid core agent with only required fields
|
||||
# Expected: PASS
|
||||
# Path context: src/core/agents/minimal-core-agent.agent.yaml
|
||||
|
||||
agent:
|
||||
metadata:
|
||||
id: minimal-test
|
||||
name: Minimal Test Agent
|
||||
title: Minimal Test
|
||||
icon: 🧪
|
||||
hasSidecar: false
|
||||
|
||||
persona:
|
||||
role: Test agent with minimal configuration
|
||||
identity: I am a minimal test agent used for schema validation testing.
|
||||
communication_style: Clear and concise
|
||||
principles:
|
||||
- Validate schema requirements
|
||||
- Demonstrate minimal valid structure
|
||||
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Show help
|
||||
action: display_help
|
||||
|
|
@ -1,387 +0,0 @@
|
|||
/**
|
||||
* Agent Schema Validation Test Runner
|
||||
*
|
||||
* Runs all test fixtures and verifies expected outcomes.
|
||||
* Reports pass/fail for each test and overall coverage statistics.
|
||||
*
|
||||
* Usage: node test/test-agent-schema.js
|
||||
* Exit codes: 0 = all tests pass, 1 = test failures
|
||||
*/
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const yaml = require('yaml');
|
||||
const { validateAgentFile } = require('../tools/schema/agent.js');
|
||||
const { glob } = require('glob');
|
||||
|
||||
// ANSI color codes
|
||||
const colors = {
|
||||
reset: '\u001B[0m',
|
||||
green: '\u001B[32m',
|
||||
red: '\u001B[31m',
|
||||
yellow: '\u001B[33m',
|
||||
blue: '\u001B[34m',
|
||||
cyan: '\u001B[36m',
|
||||
dim: '\u001B[2m',
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse test metadata from YAML comments
|
||||
* @param {string} filePath
|
||||
* @returns {{shouldPass: boolean, errorExpectation?: object, pathContext?: string}}
|
||||
*/
|
||||
function parseTestMetadata(filePath) {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
let shouldPass = true;
|
||||
let pathContext = null;
|
||||
const errorExpectation = {};
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes('Expected: PASS')) {
|
||||
shouldPass = true;
|
||||
} else if (line.includes('Expected: FAIL')) {
|
||||
shouldPass = false;
|
||||
}
|
||||
|
||||
// Parse error metadata
|
||||
const codeMatch = line.match(/^# Error code: (.+)$/);
|
||||
if (codeMatch) {
|
||||
errorExpectation.code = codeMatch[1].trim();
|
||||
}
|
||||
|
||||
const pathMatch = line.match(/^# Error path: (.+)$/);
|
||||
if (pathMatch) {
|
||||
errorExpectation.path = pathMatch[1].trim();
|
||||
}
|
||||
|
||||
const messageMatch = line.match(/^# Error message: (.+)$/);
|
||||
if (messageMatch) {
|
||||
errorExpectation.message = messageMatch[1].trim();
|
||||
}
|
||||
|
||||
const minimumMatch = line.match(/^# Error minimum: (\d+)$/);
|
||||
if (minimumMatch) {
|
||||
errorExpectation.minimum = parseInt(minimumMatch[1], 10);
|
||||
}
|
||||
|
||||
const expectedMatch = line.match(/^# Error expected: (.+)$/);
|
||||
if (expectedMatch) {
|
||||
errorExpectation.expected = expectedMatch[1].trim();
|
||||
}
|
||||
|
||||
const receivedMatch = line.match(/^# Error received: (.+)$/);
|
||||
if (receivedMatch) {
|
||||
errorExpectation.received = receivedMatch[1].trim();
|
||||
}
|
||||
|
||||
const keysMatch = line.match(/^# Error keys: \[(.+)\]$/);
|
||||
if (keysMatch) {
|
||||
errorExpectation.keys = keysMatch[1].split(',').map((k) => k.trim().replaceAll(/['"]/g, ''));
|
||||
}
|
||||
|
||||
const contextMatch = line.match(/^# Path context: (.+)$/);
|
||||
if (contextMatch) {
|
||||
pathContext = contextMatch[1].trim();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
shouldPass,
|
||||
errorExpectation: Object.keys(errorExpectation).length > 0 ? errorExpectation : null,
|
||||
pathContext,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert dot-notation path string to array (handles array indices)
|
||||
* e.g., "agent.menu[0].trigger" => ["agent", "menu", 0, "trigger"]
|
||||
*/
|
||||
function parsePathString(pathString) {
|
||||
return pathString
|
||||
.replaceAll(/\[(\d+)\]/g, '.$1') // Convert [0] to .0
|
||||
.split('.')
|
||||
.map((part) => {
|
||||
const num = parseInt(part, 10);
|
||||
return isNaN(num) ? part : num;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate error against expectations
|
||||
* @param {object} error - Zod error issue
|
||||
* @param {object} expectation - Expected error structure
|
||||
* @returns {{valid: boolean, reason?: string}}
|
||||
*/
|
||||
function validateError(error, expectation) {
|
||||
// Check error code
|
||||
if (expectation.code && error.code !== expectation.code) {
|
||||
return { valid: false, reason: `Expected code "${expectation.code}", got "${error.code}"` };
|
||||
}
|
||||
|
||||
// Check error path
|
||||
if (expectation.path) {
|
||||
const expectedPath = parsePathString(expectation.path);
|
||||
const actualPath = error.path;
|
||||
|
||||
if (JSON.stringify(expectedPath) !== JSON.stringify(actualPath)) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: `Expected path ${JSON.stringify(expectedPath)}, got ${JSON.stringify(actualPath)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// For custom errors, strictly check message
|
||||
if (expectation.code === 'custom' && expectation.message && error.message !== expectation.message) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: `Expected message "${expectation.message}", got "${error.message}"`,
|
||||
};
|
||||
}
|
||||
|
||||
// For Zod errors, check type-specific fields
|
||||
if (expectation.minimum !== undefined && error.minimum !== expectation.minimum) {
|
||||
return { valid: false, reason: `Expected minimum ${expectation.minimum}, got ${error.minimum}` };
|
||||
}
|
||||
|
||||
if (expectation.expected && error.expected !== expectation.expected) {
|
||||
return { valid: false, reason: `Expected type "${expectation.expected}", got "${error.expected}"` };
|
||||
}
|
||||
|
||||
if (expectation.received && error.received !== expectation.received) {
|
||||
return { valid: false, reason: `Expected received "${expectation.received}", got "${error.received}"` };
|
||||
}
|
||||
|
||||
if (expectation.keys) {
|
||||
const expectedKeys = expectation.keys.sort();
|
||||
const actualKeys = (error.keys || []).sort();
|
||||
if (JSON.stringify(expectedKeys) !== JSON.stringify(actualKeys)) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: `Expected keys ${JSON.stringify(expectedKeys)}, got ${JSON.stringify(actualKeys)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a single test case
|
||||
* @param {string} filePath
|
||||
* @returns {{passed: boolean, message: string}}
|
||||
*/
|
||||
function runTest(filePath) {
|
||||
const metadata = parseTestMetadata(filePath);
|
||||
const { shouldPass, errorExpectation, pathContext } = metadata;
|
||||
|
||||
try {
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
let agentData;
|
||||
|
||||
try {
|
||||
agentData = yaml.parse(fileContent);
|
||||
} catch (parseError) {
|
||||
// YAML parse error
|
||||
if (shouldPass) {
|
||||
return {
|
||||
passed: false,
|
||||
message: `Expected PASS but got YAML parse error: ${parseError.message}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
passed: true,
|
||||
message: 'Got expected YAML parse error',
|
||||
};
|
||||
}
|
||||
|
||||
// Determine validation path
|
||||
// If pathContext is specified in comments, use it; otherwise derive from fixture location
|
||||
let validationPath = pathContext;
|
||||
if (!validationPath) {
|
||||
// Map fixture location to simulated src/ path
|
||||
const relativePath = path.relative(path.join(__dirname, 'fixtures/agent-schema'), filePath);
|
||||
const parts = relativePath.split(path.sep);
|
||||
|
||||
if (parts.includes('metadata') && parts[0] === 'valid') {
|
||||
// Valid metadata tests: check if filename suggests module or core
|
||||
const filename = path.basename(filePath);
|
||||
if (filename.includes('module')) {
|
||||
validationPath = 'src/bmm/agents/test.agent.yaml';
|
||||
} else {
|
||||
validationPath = 'src/core/agents/test.agent.yaml';
|
||||
}
|
||||
} else if (parts.includes('metadata') && parts[0] === 'invalid') {
|
||||
// Invalid metadata tests: derive from filename
|
||||
const filename = path.basename(filePath);
|
||||
if (filename.includes('module') || filename.includes('wrong-module')) {
|
||||
validationPath = 'src/bmm/agents/test.agent.yaml';
|
||||
} else if (filename.includes('core')) {
|
||||
validationPath = 'src/core/agents/test.agent.yaml';
|
||||
} else {
|
||||
validationPath = 'src/core/agents/test.agent.yaml';
|
||||
}
|
||||
} else {
|
||||
// Default to core agent path
|
||||
validationPath = 'src/core/agents/test.agent.yaml';
|
||||
}
|
||||
}
|
||||
|
||||
const result = validateAgentFile(validationPath, agentData);
|
||||
|
||||
if (result.success && shouldPass) {
|
||||
return {
|
||||
passed: true,
|
||||
message: 'Validation passed as expected',
|
||||
};
|
||||
}
|
||||
|
||||
if (!result.success && !shouldPass) {
|
||||
const actualError = result.error.issues[0];
|
||||
|
||||
// If we have error expectations, validate strictly
|
||||
if (errorExpectation) {
|
||||
const validation = validateError(actualError, errorExpectation);
|
||||
|
||||
if (!validation.valid) {
|
||||
return {
|
||||
passed: false,
|
||||
message: `Error validation failed: ${validation.reason}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
passed: true,
|
||||
message: `Got expected error (${errorExpectation.code}): ${actualError.message}`,
|
||||
};
|
||||
}
|
||||
|
||||
// No specific expectations - just check that it failed
|
||||
return {
|
||||
passed: true,
|
||||
message: `Got expected validation error: ${actualError?.message}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (result.success && !shouldPass) {
|
||||
return {
|
||||
passed: false,
|
||||
message: 'Expected validation to FAIL but it PASSED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!result.success && shouldPass) {
|
||||
return {
|
||||
passed: false,
|
||||
message: `Expected validation to PASS but it FAILED: ${result.error.issues[0]?.message}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
passed: false,
|
||||
message: 'Unexpected test state',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
passed: false,
|
||||
message: `Test execution error: ${error.message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main test runner
|
||||
*/
|
||||
async function main() {
|
||||
console.log(`${colors.cyan}╔═══════════════════════════════════════════════════════════╗${colors.reset}`);
|
||||
console.log(`${colors.cyan}║ Agent Schema Validation Test Suite ║${colors.reset}`);
|
||||
console.log(`${colors.cyan}╚═══════════════════════════════════════════════════════════╝${colors.reset}\n`);
|
||||
|
||||
// Find all test fixtures
|
||||
const testFiles = await glob('test/fixtures/agent-schema/**/*.agent.yaml', {
|
||||
cwd: path.join(__dirname, '..'),
|
||||
absolute: true,
|
||||
});
|
||||
|
||||
if (testFiles.length === 0) {
|
||||
console.log(`${colors.yellow}⚠️ No test fixtures found${colors.reset}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(`Found ${colors.cyan}${testFiles.length}${colors.reset} test fixture(s)\n`);
|
||||
|
||||
// Group tests by category
|
||||
const categories = {};
|
||||
for (const testFile of testFiles) {
|
||||
const relativePath = path.relative(path.join(__dirname, 'fixtures/agent-schema'), testFile);
|
||||
const parts = relativePath.split(path.sep);
|
||||
const validInvalid = parts[0]; // 'valid' or 'invalid'
|
||||
const category = parts[1]; // 'top-level', 'metadata', etc.
|
||||
|
||||
const categoryKey = `${validInvalid}/${category}`;
|
||||
if (!categories[categoryKey]) {
|
||||
categories[categoryKey] = [];
|
||||
}
|
||||
categories[categoryKey].push(testFile);
|
||||
}
|
||||
|
||||
// Run tests by category
|
||||
let totalTests = 0;
|
||||
let passedTests = 0;
|
||||
const failures = [];
|
||||
|
||||
for (const [categoryKey, files] of Object.entries(categories).sort()) {
|
||||
const [validInvalid, category] = categoryKey.split('/');
|
||||
const categoryLabel = category.replaceAll('-', ' ').toUpperCase();
|
||||
const validLabel = validInvalid === 'valid' ? '✅' : '❌';
|
||||
|
||||
console.log(`${colors.blue}${validLabel} ${categoryLabel} (${validInvalid})${colors.reset}`);
|
||||
|
||||
for (const testFile of files) {
|
||||
totalTests++;
|
||||
const testName = path.basename(testFile, '.agent.yaml');
|
||||
const result = runTest(testFile);
|
||||
|
||||
if (result.passed) {
|
||||
passedTests++;
|
||||
console.log(` ${colors.green}✓${colors.reset} ${testName} ${colors.dim}${result.message}${colors.reset}`);
|
||||
} else {
|
||||
console.log(` ${colors.red}✗${colors.reset} ${testName} ${colors.red}${result.message}${colors.reset}`);
|
||||
failures.push({
|
||||
file: path.relative(process.cwd(), testFile),
|
||||
message: result.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log(`${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}`);
|
||||
console.log(`${colors.cyan}Test Results:${colors.reset}`);
|
||||
console.log(` Total: ${totalTests}`);
|
||||
console.log(` Passed: ${colors.green}${passedTests}${colors.reset}`);
|
||||
console.log(` Failed: ${passedTests === totalTests ? colors.green : colors.red}${totalTests - passedTests}${colors.reset}`);
|
||||
console.log(`${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}\n`);
|
||||
|
||||
// Report failures
|
||||
if (failures.length > 0) {
|
||||
console.log(`${colors.red}❌ FAILED TESTS:${colors.reset}\n`);
|
||||
for (const failure of failures) {
|
||||
console.log(`${colors.red}✗${colors.reset} ${failure.file}`);
|
||||
console.log(` ${failure.message}\n`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`${colors.green}✨ All tests passed!${colors.reset}\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Run
|
||||
main().catch((error) => {
|
||||
console.error(`${colors.red}Fatal error:${colors.reset}`, error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
#!/bin/bash
|
||||
# CLI Integration Tests for Agent Schema Validator
|
||||
# Tests the CLI wrapper (tools/validate-agent-schema.js) behavior and error handling
|
||||
# NOTE: Tests CLI functionality using temporary test fixtures
|
||||
|
||||
echo "========================================"
|
||||
echo "CLI Integration Tests"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
PASSED=0
|
||||
FAILED=0
|
||||
|
||||
# Get the repo root (assuming script is in test/ directory)
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
# Create temp directory for test fixtures
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
cleanup() {
|
||||
rm -rf "$TEMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Test 1: CLI fails when no files found (exit 1)
|
||||
echo "Test 1: CLI fails when no agent files found (should exit 1)"
|
||||
mkdir -p "$TEMP_DIR/empty/src/core/agents"
|
||||
OUTPUT=$(node "$REPO_ROOT/tools/validate-agent-schema.js" "$TEMP_DIR/empty" 2>&1)
|
||||
EXIT_CODE=$?
|
||||
if [ $EXIT_CODE -eq 1 ] && echo "$OUTPUT" | grep -q "No agent files found"; then
|
||||
echo -e "${GREEN}✓${NC} CLI fails correctly when no files found (exit 1)"
|
||||
PASSED=$((PASSED + 1))
|
||||
else
|
||||
echo -e "${RED}✗${NC} CLI failed to handle no files properly (exit code: $EXIT_CODE)"
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 2: CLI reports validation errors with exit code 1
|
||||
echo "Test 2: CLI reports validation errors (should exit 1)"
|
||||
mkdir -p "$TEMP_DIR/invalid/src/core/agents"
|
||||
cat > "$TEMP_DIR/invalid/src/core/agents/bad.agent.yaml" << 'EOF'
|
||||
agent:
|
||||
metadata:
|
||||
id: bad
|
||||
name: Bad
|
||||
title: Bad
|
||||
icon: 🧪
|
||||
persona:
|
||||
role: Test
|
||||
identity: Test
|
||||
communication_style: Test
|
||||
principles: []
|
||||
menu: []
|
||||
EOF
|
||||
OUTPUT=$(node "$REPO_ROOT/tools/validate-agent-schema.js" "$TEMP_DIR/invalid" 2>&1)
|
||||
EXIT_CODE=$?
|
||||
if [ $EXIT_CODE -eq 1 ] && echo "$OUTPUT" | grep -q "failed validation"; then
|
||||
echo -e "${GREEN}✓${NC} CLI reports errors correctly (exit 1)"
|
||||
PASSED=$((PASSED + 1))
|
||||
else
|
||||
echo -e "${RED}✗${NC} CLI failed to report errors (exit code: $EXIT_CODE)"
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 3: CLI discovers and counts agent files correctly
|
||||
echo "Test 3: CLI discovers and counts agent files"
|
||||
mkdir -p "$TEMP_DIR/valid/src/core/agents"
|
||||
cat > "$TEMP_DIR/valid/src/core/agents/test1.agent.yaml" << 'EOF'
|
||||
agent:
|
||||
metadata:
|
||||
id: test1
|
||||
name: Test1
|
||||
title: Test1
|
||||
icon: 🧪
|
||||
persona:
|
||||
role: Test
|
||||
identity: Test
|
||||
communication_style: Test
|
||||
principles: [Test]
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Help
|
||||
action: help
|
||||
EOF
|
||||
cat > "$TEMP_DIR/valid/src/core/agents/test2.agent.yaml" << 'EOF'
|
||||
agent:
|
||||
metadata:
|
||||
id: test2
|
||||
name: Test2
|
||||
title: Test2
|
||||
icon: 🧪
|
||||
persona:
|
||||
role: Test
|
||||
identity: Test
|
||||
communication_style: Test
|
||||
principles: [Test]
|
||||
menu:
|
||||
- trigger: help
|
||||
description: Help
|
||||
action: help
|
||||
EOF
|
||||
OUTPUT=$(node "$REPO_ROOT/tools/validate-agent-schema.js" "$TEMP_DIR/valid" 2>&1)
|
||||
EXIT_CODE=$?
|
||||
if [ $EXIT_CODE -eq 0 ] && echo "$OUTPUT" | grep -q "Found 2 agent file"; then
|
||||
echo -e "${GREEN}✓${NC} CLI discovers and counts files correctly"
|
||||
PASSED=$((PASSED + 1))
|
||||
else
|
||||
echo -e "${RED}✗${NC} CLI file discovery failed"
|
||||
echo "Output: $OUTPUT"
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 4: CLI provides detailed error messages
|
||||
echo "Test 4: CLI provides detailed error messages"
|
||||
OUTPUT=$(node "$REPO_ROOT/tools/validate-agent-schema.js" "$TEMP_DIR/invalid" 2>&1)
|
||||
if echo "$OUTPUT" | grep -q "Path:" && echo "$OUTPUT" | grep -q "Error:"; then
|
||||
echo -e "${GREEN}✓${NC} CLI provides error details (Path and Error)"
|
||||
PASSED=$((PASSED + 1))
|
||||
else
|
||||
echo -e "${RED}✗${NC} CLI error details missing"
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 5: CLI validates real BMAD agents (smoke test)
|
||||
echo "Test 5: CLI validates actual BMAD agents (smoke test)"
|
||||
OUTPUT=$(node "$REPO_ROOT/tools/validate-agent-schema.js" 2>&1)
|
||||
EXIT_CODE=$?
|
||||
if [ $EXIT_CODE -eq 0 ] && echo "$OUTPUT" | grep -qE "Found [0-9]+ agent file"; then
|
||||
echo -e "${GREEN}✓${NC} CLI validates real BMAD agents successfully"
|
||||
PASSED=$((PASSED + 1))
|
||||
else
|
||||
echo -e "${RED}✗${NC} CLI failed on real BMAD agents (exit code: $EXIT_CODE)"
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Summary
|
||||
echo "========================================"
|
||||
echo "Test Results:"
|
||||
echo " Passed: ${GREEN}$PASSED${NC}"
|
||||
echo " Failed: ${RED}$FAILED${NC}"
|
||||
echo "========================================"
|
||||
|
||||
if [ $FAILED -eq 0 ]; then
|
||||
echo -e "\n${GREEN}✨ All CLI integration tests passed!${NC}\n"
|
||||
exit 0
|
||||
else
|
||||
echo -e "\n${RED}❌ Some CLI integration tests failed${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
/**
|
||||
* Unit Tests for Agent Schema Edge Cases
|
||||
*
|
||||
* Tests internal functions to achieve 100% branch coverage
|
||||
*/
|
||||
|
||||
const { validateAgentFile } = require('../tools/schema/agent.js');
|
||||
|
||||
console.log('Running edge case unit tests...\n');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
// Test 1: Path with malformed module structure (no slash after module name)
|
||||
// This tests line 213: slashIndex === -1
|
||||
console.log('Test 1: Malformed module path (no slash after module name)');
|
||||
try {
|
||||
const result = validateAgentFile('src/bmm', {
|
||||
agent: {
|
||||
metadata: {
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
title: 'Test',
|
||||
icon: '🧪',
|
||||
},
|
||||
persona: {
|
||||
role: 'Test',
|
||||
identity: 'Test',
|
||||
communication_style: 'Test',
|
||||
principles: ['Test'],
|
||||
},
|
||||
menu: [{ trigger: 'help', description: 'Help', action: 'help' }],
|
||||
},
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log('✗ Should have failed - missing module field');
|
||||
failed++;
|
||||
} else {
|
||||
console.log('✓ Correctly handled malformed path (treated as core agent)');
|
||||
passed++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('✗ Unexpected error:', error.message);
|
||||
failed++;
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Test 2: Module option with empty string
|
||||
// This tests line 222: trimmed.length > 0
|
||||
console.log('Test 2: Module agent with empty string in module field');
|
||||
try {
|
||||
const result = validateAgentFile('src/bmm/agents/test.agent.yaml', {
|
||||
agent: {
|
||||
metadata: {
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
title: 'Test',
|
||||
icon: '🧪',
|
||||
module: ' ', // Empty after trimming
|
||||
},
|
||||
persona: {
|
||||
role: 'Test',
|
||||
identity: 'Test',
|
||||
communication_style: 'Test',
|
||||
principles: ['Test'],
|
||||
},
|
||||
menu: [{ trigger: 'help', description: 'Help', action: 'help' }],
|
||||
},
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log('✗ Should have failed - empty module string');
|
||||
failed++;
|
||||
} else {
|
||||
console.log('✓ Correctly rejected empty module string');
|
||||
passed++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('✗ Unexpected error:', error.message);
|
||||
failed++;
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Test 3: Core agent path (src/core/agents/...) - tests the !filePath.startsWith(marker) branch
|
||||
console.log('Test 3: Core agent path returns null for module');
|
||||
try {
|
||||
const result = validateAgentFile('src/core/agents/test.agent.yaml', {
|
||||
agent: {
|
||||
metadata: {
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
title: 'Test',
|
||||
icon: '🧪',
|
||||
// No module field - correct for core agent
|
||||
},
|
||||
persona: {
|
||||
role: 'Test',
|
||||
identity: 'Test',
|
||||
communication_style: 'Test',
|
||||
principles: ['Test'],
|
||||
},
|
||||
menu: [{ trigger: 'help', description: 'Help', action: 'help' }],
|
||||
},
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log('✓ Core agent validated correctly (no module required)');
|
||||
passed++;
|
||||
} else {
|
||||
console.log('✗ Core agent should pass without module field');
|
||||
failed++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('✗ Unexpected error:', error.message);
|
||||
failed++;
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Summary
|
||||
console.log('═══════════════════════════════════════');
|
||||
console.log('Edge Case Unit Test Results:');
|
||||
console.log(` Passed: ${passed}`);
|
||||
console.log(` Failed: ${failed}`);
|
||||
console.log('═══════════════════════════════════════\n');
|
||||
|
||||
if (failed === 0) {
|
||||
console.log('✨ All edge case tests passed!\n');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log('❌ Some edge case tests failed\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
@ -1,489 +0,0 @@
|
|||
// Zod schema definition for *.agent.yaml files
|
||||
const assert = require('node:assert');
|
||||
const { z } = require('zod');
|
||||
|
||||
const COMMAND_TARGET_KEYS = ['validate-workflow', 'exec', 'action', 'tmpl', 'data'];
|
||||
const TRIGGER_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
||||
const COMPOUND_TRIGGER_PATTERN = /^([A-Z]{1,3}) or fuzzy match on ([a-z0-9]+(?:-[a-z0-9]+)*)$/;
|
||||
|
||||
/**
|
||||
* Derive the expected shortcut from a kebab-case trigger.
|
||||
* - Single word: first letter (e.g., "help" → "H")
|
||||
* - Multi-word: first letter of first two words (e.g., "tech-spec" → "TS")
|
||||
* @param {string} kebabTrigger The kebab-case trigger name.
|
||||
* @returns {string} The expected uppercase shortcut.
|
||||
*/
|
||||
function deriveShortcutFromKebab(kebabTrigger) {
|
||||
const words = kebabTrigger.split('-');
|
||||
if (words.length === 1) {
|
||||
return words[0][0].toUpperCase();
|
||||
}
|
||||
return (words[0][0] + words[1][0]).toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and validate a compound trigger string.
|
||||
* Format: "<SHORTCUT> or fuzzy match on <kebab-case>"
|
||||
* @param {string} triggerValue The trigger string to parse.
|
||||
* @returns {{ valid: boolean, shortcut?: string, kebabTrigger?: string, error?: string }}
|
||||
*/
|
||||
function parseCompoundTrigger(triggerValue) {
|
||||
const match = COMPOUND_TRIGGER_PATTERN.exec(triggerValue);
|
||||
if (!match) {
|
||||
return { valid: false, error: 'invalid compound trigger format' };
|
||||
}
|
||||
|
||||
const [, shortcut, kebabTrigger] = match;
|
||||
|
||||
return { valid: true, shortcut, kebabTrigger };
|
||||
}
|
||||
|
||||
// Public API ---------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Validate an agent YAML payload against the schema derived from its file location.
|
||||
* Exposed as the single public entry point, so callers do not reach into schema internals.
|
||||
*
|
||||
* @param {string} filePath Path to the agent file (used to infer module scope).
|
||||
* @param {unknown} agentYaml Parsed YAML content.
|
||||
* @returns {import('zod').SafeParseReturnType<unknown, unknown>} SafeParse result.
|
||||
*/
|
||||
function validateAgentFile(filePath, agentYaml) {
|
||||
const expectedModule = deriveModuleFromPath(filePath);
|
||||
const schema = agentSchema({ module: expectedModule });
|
||||
return schema.safeParse(agentYaml);
|
||||
}
|
||||
|
||||
module.exports = { validateAgentFile };
|
||||
|
||||
// Internal helpers ---------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Build a Zod schema for validating a single agent definition.
|
||||
* The schema is generated per call so module-scoped agents can pass their expected
|
||||
* module slug while core agents leave it undefined.
|
||||
*
|
||||
* @param {Object} [options]
|
||||
* @param {string|null|undefined} [options.module] Module slug for module agents; omit or null for core agents.
|
||||
* @returns {import('zod').ZodSchema} Configured Zod schema instance.
|
||||
*/
|
||||
function agentSchema(options = {}) {
|
||||
const expectedModule = normalizeModuleOption(options.module);
|
||||
|
||||
return (
|
||||
z
|
||||
.object({
|
||||
agent: buildAgentSchema(expectedModule),
|
||||
})
|
||||
.strict()
|
||||
// Refinement: enforce trigger format and uniqueness rules after structural checks.
|
||||
.superRefine((value, ctx) => {
|
||||
const seenTriggers = new Set();
|
||||
|
||||
let index = 0;
|
||||
for (const item of value.agent.menu) {
|
||||
// Handle legacy format with trigger field
|
||||
if (item.trigger) {
|
||||
const triggerValue = item.trigger;
|
||||
let canonicalTrigger = triggerValue;
|
||||
|
||||
// Check if it's a compound trigger (contains " or ")
|
||||
if (triggerValue.includes(' or ')) {
|
||||
const result = parseCompoundTrigger(triggerValue);
|
||||
if (!result.valid) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['agent', 'menu', index, 'trigger'],
|
||||
message: `agent.menu[].trigger compound format error: ${result.error}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate that shortcut matches description brackets
|
||||
const descriptionMatch = item.description?.match(/^\[([A-Z]{1,3})\]/);
|
||||
if (!descriptionMatch) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['agent', 'menu', index, 'description'],
|
||||
message: `agent.menu[].description must start with [SHORTCUT] where SHORTCUT matches the trigger shortcut "${result.shortcut}"`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const descriptionShortcut = descriptionMatch[1];
|
||||
if (descriptionShortcut !== result.shortcut) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['agent', 'menu', index, 'description'],
|
||||
message: `agent.menu[].description shortcut "[${descriptionShortcut}]" must match trigger shortcut "${result.shortcut}"`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
canonicalTrigger = result.kebabTrigger;
|
||||
} else if (!TRIGGER_PATTERN.test(triggerValue)) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['agent', 'menu', index, 'trigger'],
|
||||
message: 'agent.menu[].trigger must be kebab-case (lowercase words separated by hyphen)',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (seenTriggers.has(canonicalTrigger)) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['agent', 'menu', index, 'trigger'],
|
||||
message: `agent.menu[].trigger duplicates "${canonicalTrigger}" within the same agent`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
seenTriggers.add(canonicalTrigger);
|
||||
}
|
||||
// Handle multi format with triggers array (new format)
|
||||
else if (item.triggers && Array.isArray(item.triggers)) {
|
||||
for (const [triggerIndex, triggerItem] of item.triggers.entries()) {
|
||||
let triggerName = null;
|
||||
|
||||
// Extract trigger name from all three formats
|
||||
if (triggerItem.trigger) {
|
||||
// Format 1: Simple flat format with trigger field
|
||||
triggerName = triggerItem.trigger;
|
||||
} else {
|
||||
// Format 2a or 2b: Object-key format
|
||||
const keys = Object.keys(triggerItem);
|
||||
if (keys.length === 1 && keys[0] !== 'trigger') {
|
||||
triggerName = keys[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (triggerName) {
|
||||
if (!TRIGGER_PATTERN.test(triggerName)) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['agent', 'menu', index, 'triggers', triggerIndex],
|
||||
message: `agent.menu[].triggers[] must be kebab-case (lowercase words separated by hyphen) - got "${triggerName}"`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (seenTriggers.has(triggerName)) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['agent', 'menu', index, 'triggers', triggerIndex],
|
||||
message: `agent.menu[].triggers[] duplicates "${triggerName}" within the same agent`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
seenTriggers.add(triggerName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
index += 1;
|
||||
}
|
||||
})
|
||||
// Refinement: suggest conversational_knowledge when discussion is true
|
||||
.superRefine((value, ctx) => {
|
||||
if (value.agent.discussion === true && !value.agent.conversational_knowledge) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['agent', 'conversational_knowledge'],
|
||||
message: 'It is recommended to include conversational_knowledge when discussion is true',
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble the full agent schema using the module expectation provided by the caller.
|
||||
* @param {string|null} expectedModule Trimmed module slug or null for core agents.
|
||||
*/
|
||||
function buildAgentSchema(expectedModule) {
|
||||
return z
|
||||
.object({
|
||||
metadata: buildMetadataSchema(expectedModule),
|
||||
persona: buildPersonaSchema(),
|
||||
critical_actions: z.array(createNonEmptyString('agent.critical_actions[]')).optional(),
|
||||
menu: z.array(buildMenuItemSchema()).min(1, { message: 'agent.menu must include at least one entry' }),
|
||||
prompts: z.array(buildPromptSchema()).optional(),
|
||||
discussion: z.boolean().optional(),
|
||||
conversational_knowledge: z.array(z.object({}).passthrough()).min(1).optional(),
|
||||
})
|
||||
.strict();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate metadata shape.
|
||||
* @param {string|null} expectedModule Trimmed module slug or null when core agent metadata is expected.
|
||||
* Note: Module field is optional and can be any value - no validation against path.
|
||||
*/
|
||||
function buildMetadataSchema(expectedModule) {
|
||||
const schemaShape = {
|
||||
id: createNonEmptyString('agent.metadata.id'),
|
||||
name: createNonEmptyString('agent.metadata.name'),
|
||||
title: createNonEmptyString('agent.metadata.title'),
|
||||
icon: createNonEmptyString('agent.metadata.icon'),
|
||||
module: createNonEmptyString('agent.metadata.module').optional(),
|
||||
capabilities: createNonEmptyString('agent.metadata.capabilities').optional(),
|
||||
hasSidecar: z.boolean(),
|
||||
};
|
||||
|
||||
return z.object(schemaShape).strict();
|
||||
}
|
||||
|
||||
function buildPersonaSchema() {
|
||||
return z
|
||||
.object({
|
||||
role: createNonEmptyString('agent.persona.role'),
|
||||
identity: createNonEmptyString('agent.persona.identity'),
|
||||
communication_style: createNonEmptyString('agent.persona.communication_style'),
|
||||
principles: z.union([
|
||||
createNonEmptyString('agent.persona.principles'),
|
||||
z
|
||||
.array(createNonEmptyString('agent.persona.principles[]'))
|
||||
.min(1, { message: 'agent.persona.principles must include at least one entry' }),
|
||||
]),
|
||||
})
|
||||
.strict();
|
||||
}
|
||||
|
||||
function buildPromptSchema() {
|
||||
return z
|
||||
.object({
|
||||
id: createNonEmptyString('agent.prompts[].id'),
|
||||
content: z.string().refine((value) => value.trim().length > 0, {
|
||||
message: 'agent.prompts[].content must be a non-empty string',
|
||||
}),
|
||||
description: createNonEmptyString('agent.prompts[].description').optional(),
|
||||
})
|
||||
.strict();
|
||||
}
|
||||
|
||||
/**
|
||||
* Schema for individual menu entries ensuring they are actionable.
|
||||
* Supports both legacy format and new multi format.
|
||||
*/
|
||||
function buildMenuItemSchema() {
|
||||
// Legacy menu item format
|
||||
const legacyMenuItemSchema = z
|
||||
.object({
|
||||
trigger: createNonEmptyString('agent.menu[].trigger'),
|
||||
description: createNonEmptyString('agent.menu[].description'),
|
||||
'validate-workflow': createNonEmptyString('agent.menu[].validate-workflow').optional(),
|
||||
exec: createNonEmptyString('agent.menu[].exec').optional(),
|
||||
action: createNonEmptyString('agent.menu[].action').optional(),
|
||||
tmpl: createNonEmptyString('agent.menu[].tmpl').optional(),
|
||||
data: z.string().optional(),
|
||||
checklist: createNonEmptyString('agent.menu[].checklist').optional(),
|
||||
document: createNonEmptyString('agent.menu[].document').optional(),
|
||||
'ide-only': z.boolean().optional(),
|
||||
'web-only': z.boolean().optional(),
|
||||
discussion: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.superRefine((value, ctx) => {
|
||||
const hasCommandTarget = COMMAND_TARGET_KEYS.some((key) => {
|
||||
const commandValue = value[key];
|
||||
return typeof commandValue === 'string' && commandValue.trim().length > 0;
|
||||
});
|
||||
|
||||
if (!hasCommandTarget) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: 'agent.menu[] entries must include at least one command target field',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Multi menu item format
|
||||
const multiMenuItemSchema = z
|
||||
.object({
|
||||
multi: createNonEmptyString('agent.menu[].multi'),
|
||||
triggers: z
|
||||
.array(
|
||||
z.union([
|
||||
// Format 1: Simple flat format (has trigger field)
|
||||
z
|
||||
.object({
|
||||
trigger: z.string(),
|
||||
input: createNonEmptyString('agent.menu[].triggers[].input'),
|
||||
route: createNonEmptyString('agent.menu[].triggers[].route').optional(),
|
||||
action: createNonEmptyString('agent.menu[].triggers[].action').optional(),
|
||||
data: z.string().optional(),
|
||||
type: z.enum(['exec', 'action', 'workflow']).optional(),
|
||||
})
|
||||
.strict()
|
||||
.refine((data) => data.trigger, { message: 'Must have trigger field' })
|
||||
.superRefine((value, ctx) => {
|
||||
// Must have either route or action (or both)
|
||||
if (!value.route && !value.action) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: 'agent.menu[].triggers[] must have either route or action (or both)',
|
||||
});
|
||||
}
|
||||
}),
|
||||
// Format 2a: Object with array format (like bmad-builder.agent.yaml)
|
||||
z
|
||||
.object({})
|
||||
.passthrough()
|
||||
.refine(
|
||||
(value) => {
|
||||
const keys = Object.keys(value);
|
||||
if (keys.length !== 1) return false;
|
||||
const triggerItems = value[keys[0]];
|
||||
return Array.isArray(triggerItems);
|
||||
},
|
||||
{ message: 'Must be object with single key pointing to array' },
|
||||
)
|
||||
.superRefine((value, ctx) => {
|
||||
const triggerName = Object.keys(value)[0];
|
||||
const triggerItems = value[triggerName];
|
||||
|
||||
if (!Array.isArray(triggerItems)) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: `Trigger "${triggerName}" must be an array of items`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check required fields in the array
|
||||
const hasInput = triggerItems.some((item) => 'input' in item);
|
||||
const hasRouteOrAction = triggerItems.some((item) => 'route' in item || 'action' in item);
|
||||
|
||||
if (!hasInput) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: `Trigger "${triggerName}" must have an input field`,
|
||||
});
|
||||
}
|
||||
|
||||
if (!hasRouteOrAction) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: `Trigger "${triggerName}" must have a route or action field`,
|
||||
});
|
||||
}
|
||||
}),
|
||||
// Format 2b: Object with direct fields (like analyst.agent.yaml)
|
||||
z
|
||||
.object({})
|
||||
.passthrough()
|
||||
.refine(
|
||||
(value) => {
|
||||
const keys = Object.keys(value);
|
||||
if (keys.length !== 1) return false;
|
||||
const triggerFields = value[keys[0]];
|
||||
return !Array.isArray(triggerFields) && typeof triggerFields === 'object';
|
||||
},
|
||||
{ message: 'Must be object with single key pointing to object' },
|
||||
)
|
||||
.superRefine((value, ctx) => {
|
||||
const triggerName = Object.keys(value)[0];
|
||||
const triggerFields = value[triggerName];
|
||||
|
||||
// Check required fields
|
||||
if (!triggerFields.input || typeof triggerFields.input !== 'string') {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: `Trigger "${triggerName}" must have an input field`,
|
||||
});
|
||||
}
|
||||
|
||||
if (!triggerFields.route && !triggerFields.action) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: `Trigger "${triggerName}" must have a route or action field`,
|
||||
});
|
||||
}
|
||||
}),
|
||||
]),
|
||||
)
|
||||
.min(1, { message: 'agent.menu[].triggers must have at least one trigger' }),
|
||||
discussion: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.superRefine((value, ctx) => {
|
||||
// Check for duplicate trigger names
|
||||
const seenTriggers = new Set();
|
||||
for (const [index, triggerItem] of value.triggers.entries()) {
|
||||
let triggerName = null;
|
||||
|
||||
// Extract trigger name from either format
|
||||
if (triggerItem.trigger) {
|
||||
// Format 1
|
||||
triggerName = triggerItem.trigger;
|
||||
} else {
|
||||
// Format 2
|
||||
const keys = Object.keys(triggerItem);
|
||||
if (keys.length === 1) {
|
||||
triggerName = keys[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (triggerName) {
|
||||
if (seenTriggers.has(triggerName)) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['agent', 'menu', 'triggers', index],
|
||||
message: `Trigger name "${triggerName}" is duplicated`,
|
||||
});
|
||||
}
|
||||
seenTriggers.add(triggerName);
|
||||
|
||||
// Validate trigger name format
|
||||
if (!TRIGGER_PATTERN.test(triggerName)) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['agent', 'menu', 'triggers', index],
|
||||
message: `Trigger name "${triggerName}" must be kebab-case (lowercase words separated by hyphen)`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return z.union([legacyMenuItemSchema, multiMenuItemSchema]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the expected module slug from a file path residing under src/<module>/agents/.
|
||||
* @param {string} filePath Absolute or relative agent path.
|
||||
* @returns {string|null} Module slug if identifiable, otherwise null.
|
||||
*/
|
||||
function deriveModuleFromPath(filePath) {
|
||||
assert(filePath, 'validateAgentFile expects filePath to be provided');
|
||||
assert(typeof filePath === 'string', 'validateAgentFile expects filePath to be a string');
|
||||
assert(filePath.startsWith('src/'), 'validateAgentFile expects filePath to start with "src/"');
|
||||
|
||||
const marker = 'src/';
|
||||
if (!filePath.startsWith(marker)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const remainder = filePath.slice(marker.length);
|
||||
const slashIndex = remainder.indexOf('/');
|
||||
return slashIndex === -1 ? null : remainder.slice(0, slashIndex);
|
||||
}
|
||||
|
||||
function normalizeModuleOption(moduleOption) {
|
||||
if (typeof moduleOption !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = moduleOption.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
|
||||
// Primitive validators -----------------------------------------------------
|
||||
|
||||
function createNonEmptyString(label) {
|
||||
return z.string().refine((value) => value.trim().length > 0, {
|
||||
message: `${label} must be a non-empty string`,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
/**
|
||||
* Agent Schema Validator CLI
|
||||
*
|
||||
* Scans all *.agent.yaml files in src/{core,modules/*}/agents/
|
||||
* and validates them against the Zod schema.
|
||||
*
|
||||
* Usage: node tools/validate-agent-schema.js [project_root]
|
||||
* Exit codes: 0 = success, 1 = validation failures
|
||||
*
|
||||
* Optional argument:
|
||||
* project_root - Directory to scan (defaults to BMAD repo root)
|
||||
*/
|
||||
|
||||
const { glob } = require('glob');
|
||||
const yaml = require('yaml');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { validateAgentFile } = require('./schema/agent.js');
|
||||
|
||||
/**
|
||||
* Main validation routine
|
||||
* @param {string} [customProjectRoot] - Optional project root to scan (for testing)
|
||||
*/
|
||||
async function main(customProjectRoot) {
|
||||
console.log('🔍 Scanning for agent files...\n');
|
||||
|
||||
// Determine project root: use custom path if provided, otherwise default to repo root
|
||||
const project_root = customProjectRoot || path.join(__dirname, '..');
|
||||
|
||||
// Find all agent files
|
||||
const agentFiles = await glob('src/{core,bmm}/agents/**/*.agent.yaml', {
|
||||
cwd: project_root,
|
||||
absolute: true,
|
||||
});
|
||||
|
||||
if (agentFiles.length === 0) {
|
||||
console.log('ℹ️ No *.agent.yaml files found — agents may use the new SKILL.md format.');
|
||||
console.log(' Skipping legacy agent schema validation.\n');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(`Found ${agentFiles.length} agent file(s)\n`);
|
||||
|
||||
const errors = [];
|
||||
|
||||
// Validate each file
|
||||
for (const filePath of agentFiles) {
|
||||
const relativePath = path.relative(process.cwd(), filePath);
|
||||
|
||||
try {
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
const agentData = yaml.parse(fileContent);
|
||||
|
||||
// Convert absolute path to relative src/ path for module detection
|
||||
const srcRelativePath = relativePath.startsWith('src/') ? relativePath : path.relative(project_root, filePath).replaceAll('\\', '/');
|
||||
|
||||
const result = validateAgentFile(srcRelativePath, agentData);
|
||||
|
||||
if (result.success) {
|
||||
console.log(`✅ ${relativePath}`);
|
||||
} else {
|
||||
errors.push({
|
||||
file: relativePath,
|
||||
issues: result.error.issues,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push({
|
||||
file: relativePath,
|
||||
issues: [
|
||||
{
|
||||
code: 'parse_error',
|
||||
message: `Failed to parse YAML: ${error.message}`,
|
||||
path: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Report errors
|
||||
if (errors.length > 0) {
|
||||
console.log('\n❌ Validation failed for the following files:\n');
|
||||
|
||||
for (const { file, issues } of errors) {
|
||||
console.log(`\n📄 ${file}`);
|
||||
for (const issue of issues) {
|
||||
const pathString = issue.path.length > 0 ? issue.path.join('.') : '(root)';
|
||||
console.log(` Path: ${pathString}`);
|
||||
console.log(` Error: ${issue.message}`);
|
||||
if (issue.code) {
|
||||
console.log(` Code: ${issue.code}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n\n💥 ${errors.length} file(s) failed validation`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\n✨ All ${agentFiles.length} agent file(s) passed validation!\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Run with optional command-line argument for project root
|
||||
const customProjectRoot = process.argv[2];
|
||||
main(customProjectRoot).catch((error) => {
|
||||
console.error('Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
Loading…
Reference in New Issue