diff --git a/.github/workflows/quality.yaml b/.github/workflows/quality.yaml index 4464838b0..de65c1817 100644 --- a/.github/workflows/quality.yaml +++ b/.github/workflows/quality.yaml @@ -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 diff --git a/.npmignore b/.npmignore index 452bb4ba4..c7fad9e92 100644 --- a/.npmignore +++ b/.npmignore @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 459195916..362d638e3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/package.json b/package.json index 4340a2fe0..e76f9d0dc 100644 --- a/package.json +++ b/package.json @@ -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}": [ diff --git a/src/core/tasks/bmad-create-prd/SKILL.md b/src/bmm/workflows/2-plan-workflows/bmad-create-prd/SKILL.md similarity index 100% rename from src/core/tasks/bmad-create-prd/SKILL.md rename to src/bmm/workflows/2-plan-workflows/bmad-create-prd/SKILL.md diff --git a/src/core/tasks/bmad-create-prd/bmad-skill-manifest.yaml b/src/bmm/workflows/2-plan-workflows/bmad-create-prd/bmad-skill-manifest.yaml similarity index 100% rename from src/core/tasks/bmad-create-prd/bmad-skill-manifest.yaml rename to src/bmm/workflows/2-plan-workflows/bmad-create-prd/bmad-skill-manifest.yaml diff --git a/src/core/tasks/bmad-create-prd/data/domain-complexity.csv b/src/bmm/workflows/2-plan-workflows/bmad-create-prd/data/domain-complexity.csv similarity index 100% rename from src/core/tasks/bmad-create-prd/data/domain-complexity.csv rename to src/bmm/workflows/2-plan-workflows/bmad-create-prd/data/domain-complexity.csv diff --git a/src/core/tasks/bmad-create-prd/data/prd-purpose.md b/src/bmm/workflows/2-plan-workflows/bmad-create-prd/data/prd-purpose.md similarity index 100% rename from src/core/tasks/bmad-create-prd/data/prd-purpose.md rename to src/bmm/workflows/2-plan-workflows/bmad-create-prd/data/prd-purpose.md diff --git a/src/core/tasks/bmad-create-prd/data/project-types.csv b/src/bmm/workflows/2-plan-workflows/bmad-create-prd/data/project-types.csv similarity index 100% rename from src/core/tasks/bmad-create-prd/data/project-types.csv rename to src/bmm/workflows/2-plan-workflows/bmad-create-prd/data/project-types.csv diff --git a/src/core/tasks/bmad-create-prd/steps-c/step-01-init.md b/src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-01-init.md similarity index 100% rename from src/core/tasks/bmad-create-prd/steps-c/step-01-init.md rename to src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-01-init.md diff --git a/src/core/tasks/bmad-create-prd/steps-c/step-01b-continue.md b/src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-01b-continue.md similarity index 100% rename from src/core/tasks/bmad-create-prd/steps-c/step-01b-continue.md rename to src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-01b-continue.md diff --git a/src/core/tasks/bmad-create-prd/steps-c/step-02-discovery.md b/src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-02-discovery.md similarity index 100% rename from src/core/tasks/bmad-create-prd/steps-c/step-02-discovery.md rename to src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-02-discovery.md diff --git a/src/core/tasks/bmad-create-prd/steps-c/step-02b-vision.md b/src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-02b-vision.md similarity index 100% rename from src/core/tasks/bmad-create-prd/steps-c/step-02b-vision.md rename to src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-02b-vision.md diff --git a/src/core/tasks/bmad-create-prd/steps-c/step-02c-executive-summary.md b/src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-02c-executive-summary.md similarity index 100% rename from src/core/tasks/bmad-create-prd/steps-c/step-02c-executive-summary.md rename to src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-02c-executive-summary.md diff --git a/src/core/tasks/bmad-create-prd/steps-c/step-03-success.md b/src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-03-success.md similarity index 100% rename from src/core/tasks/bmad-create-prd/steps-c/step-03-success.md rename to src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-03-success.md diff --git a/src/core/tasks/bmad-create-prd/steps-c/step-04-journeys.md b/src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-04-journeys.md similarity index 100% rename from src/core/tasks/bmad-create-prd/steps-c/step-04-journeys.md rename to src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-04-journeys.md diff --git a/src/core/tasks/bmad-create-prd/steps-c/step-05-domain.md b/src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-05-domain.md similarity index 100% rename from src/core/tasks/bmad-create-prd/steps-c/step-05-domain.md rename to src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-05-domain.md diff --git a/src/core/tasks/bmad-create-prd/steps-c/step-06-innovation.md b/src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-06-innovation.md similarity index 100% rename from src/core/tasks/bmad-create-prd/steps-c/step-06-innovation.md rename to src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-06-innovation.md diff --git a/src/core/tasks/bmad-create-prd/steps-c/step-07-project-type.md b/src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-07-project-type.md similarity index 100% rename from src/core/tasks/bmad-create-prd/steps-c/step-07-project-type.md rename to src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-07-project-type.md diff --git a/src/core/tasks/bmad-create-prd/steps-c/step-08-scoping.md b/src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-08-scoping.md similarity index 100% rename from src/core/tasks/bmad-create-prd/steps-c/step-08-scoping.md rename to src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-08-scoping.md diff --git a/src/core/tasks/bmad-create-prd/steps-c/step-09-functional.md b/src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-09-functional.md similarity index 100% rename from src/core/tasks/bmad-create-prd/steps-c/step-09-functional.md rename to src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-09-functional.md diff --git a/src/core/tasks/bmad-create-prd/steps-c/step-10-nonfunctional.md b/src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-10-nonfunctional.md similarity index 100% rename from src/core/tasks/bmad-create-prd/steps-c/step-10-nonfunctional.md rename to src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-10-nonfunctional.md diff --git a/src/core/tasks/bmad-create-prd/steps-c/step-11-polish.md b/src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-11-polish.md similarity index 100% rename from src/core/tasks/bmad-create-prd/steps-c/step-11-polish.md rename to src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-11-polish.md diff --git a/src/core/tasks/bmad-create-prd/steps-c/step-12-complete.md b/src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-12-complete.md similarity index 100% rename from src/core/tasks/bmad-create-prd/steps-c/step-12-complete.md rename to src/bmm/workflows/2-plan-workflows/bmad-create-prd/steps-c/step-12-complete.md diff --git a/src/core/tasks/bmad-create-prd/templates/prd-template.md b/src/bmm/workflows/2-plan-workflows/bmad-create-prd/templates/prd-template.md similarity index 100% rename from src/core/tasks/bmad-create-prd/templates/prd-template.md rename to src/bmm/workflows/2-plan-workflows/bmad-create-prd/templates/prd-template.md diff --git a/src/core/tasks/bmad-create-prd/workflow.md b/src/bmm/workflows/2-plan-workflows/bmad-create-prd/workflow.md similarity index 100% rename from src/core/tasks/bmad-create-prd/workflow.md rename to src/bmm/workflows/2-plan-workflows/bmad-create-prd/workflow.md diff --git a/test/README.md b/test/README.md index bc8ac48fc..ff60267fb 100644 --- a/test/README.md +++ b/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` diff --git a/test/fixtures/agent-schema/invalid/critical-actions/actions-as-string.agent.yaml b/test/fixtures/agent-schema/invalid/critical-actions/actions-as-string.agent.yaml deleted file mode 100644 index 46396e0f4..000000000 --- a/test/fixtures/agent-schema/invalid/critical-actions/actions-as-string.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/invalid/critical-actions/empty-string-in-actions.agent.yaml b/test/fixtures/agent-schema/invalid/critical-actions/empty-string-in-actions.agent.yaml deleted file mode 100644 index 3a87232c2..000000000 --- a/test/fixtures/agent-schema/invalid/critical-actions/empty-string-in-actions.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/invalid/menu-commands/empty-command-target.agent.yaml b/test/fixtures/agent-schema/invalid/menu-commands/empty-command-target.agent.yaml deleted file mode 100644 index 0194c4026..000000000 --- a/test/fixtures/agent-schema/invalid/menu-commands/empty-command-target.agent.yaml +++ /dev/null @@ -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: " " diff --git a/test/fixtures/agent-schema/invalid/menu-commands/no-command-target.agent.yaml b/test/fixtures/agent-schema/invalid/menu-commands/no-command-target.agent.yaml deleted file mode 100644 index 888e2d36b..000000000 --- a/test/fixtures/agent-schema/invalid/menu-commands/no-command-target.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/invalid/menu-triggers/camel-case.agent.yaml b/test/fixtures/agent-schema/invalid/menu-triggers/camel-case.agent.yaml deleted file mode 100644 index 62fbb3136..000000000 --- a/test/fixtures/agent-schema/invalid/menu-triggers/camel-case.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/invalid/menu-triggers/compound-invalid-format.agent.yaml b/test/fixtures/agent-schema/invalid/menu-triggers/compound-invalid-format.agent.yaml deleted file mode 100644 index 07a550f46..000000000 --- a/test/fixtures/agent-schema/invalid/menu-triggers/compound-invalid-format.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/invalid/menu-triggers/compound-mismatched-kebab.agent.yaml b/test/fixtures/agent-schema/invalid/menu-triggers/compound-mismatched-kebab.agent.yaml deleted file mode 100644 index 46febb326..000000000 --- a/test/fixtures/agent-schema/invalid/menu-triggers/compound-mismatched-kebab.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/invalid/menu-triggers/duplicate-triggers.agent.yaml b/test/fixtures/agent-schema/invalid/menu-triggers/duplicate-triggers.agent.yaml deleted file mode 100644 index 8b5cf7c8c..000000000 --- a/test/fixtures/agent-schema/invalid/menu-triggers/duplicate-triggers.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/invalid/menu-triggers/empty-trigger.agent.yaml b/test/fixtures/agent-schema/invalid/menu-triggers/empty-trigger.agent.yaml deleted file mode 100644 index c6d9fbfa9..000000000 --- a/test/fixtures/agent-schema/invalid/menu-triggers/empty-trigger.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/invalid/menu-triggers/leading-asterisk.agent.yaml b/test/fixtures/agent-schema/invalid/menu-triggers/leading-asterisk.agent.yaml deleted file mode 100644 index 5e9585960..000000000 --- a/test/fixtures/agent-schema/invalid/menu-triggers/leading-asterisk.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/invalid/menu-triggers/snake-case.agent.yaml b/test/fixtures/agent-schema/invalid/menu-triggers/snake-case.agent.yaml deleted file mode 100644 index 7dc177935..000000000 --- a/test/fixtures/agent-schema/invalid/menu-triggers/snake-case.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/invalid/menu-triggers/trigger-with-spaces.agent.yaml b/test/fixtures/agent-schema/invalid/menu-triggers/trigger-with-spaces.agent.yaml deleted file mode 100644 index b64a406d8..000000000 --- a/test/fixtures/agent-schema/invalid/menu-triggers/trigger-with-spaces.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/invalid/menu/empty-menu.agent.yaml b/test/fixtures/agent-schema/invalid/menu/empty-menu.agent.yaml deleted file mode 100644 index b5be54ef7..000000000 --- a/test/fixtures/agent-schema/invalid/menu/empty-menu.agent.yaml +++ /dev/null @@ -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: [] diff --git a/test/fixtures/agent-schema/invalid/menu/missing-menu.agent.yaml b/test/fixtures/agent-schema/invalid/menu/missing-menu.agent.yaml deleted file mode 100644 index 55e7789a6..000000000 --- a/test/fixtures/agent-schema/invalid/menu/missing-menu.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/invalid/metadata/empty-module-string.agent.yaml b/test/fixtures/agent-schema/invalid/metadata/empty-module-string.agent.yaml deleted file mode 100644 index bb68d2de0..000000000 --- a/test/fixtures/agent-schema/invalid/metadata/empty-module-string.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/invalid/metadata/empty-name.agent.yaml b/test/fixtures/agent-schema/invalid/metadata/empty-name.agent.yaml deleted file mode 100644 index d5dbfdd09..000000000 --- a/test/fixtures/agent-schema/invalid/metadata/empty-name.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/invalid/metadata/extra-metadata-fields.agent.yaml b/test/fixtures/agent-schema/invalid/metadata/extra-metadata-fields.agent.yaml deleted file mode 100644 index 10f283d51..000000000 --- a/test/fixtures/agent-schema/invalid/metadata/extra-metadata-fields.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/invalid/metadata/missing-id.agent.yaml b/test/fixtures/agent-schema/invalid/metadata/missing-id.agent.yaml deleted file mode 100644 index 0b24082af..000000000 --- a/test/fixtures/agent-schema/invalid/metadata/missing-id.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/invalid/persona/empty-principles-array.agent.yaml b/test/fixtures/agent-schema/invalid/persona/empty-principles-array.agent.yaml deleted file mode 100644 index 4033e6908..000000000 --- a/test/fixtures/agent-schema/invalid/persona/empty-principles-array.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/invalid/persona/empty-string-in-principles.agent.yaml b/test/fixtures/agent-schema/invalid/persona/empty-string-in-principles.agent.yaml deleted file mode 100644 index 9bba71bb4..000000000 --- a/test/fixtures/agent-schema/invalid/persona/empty-string-in-principles.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/invalid/persona/extra-persona-fields.agent.yaml b/test/fixtures/agent-schema/invalid/persona/extra-persona-fields.agent.yaml deleted file mode 100644 index 73365a5e3..000000000 --- a/test/fixtures/agent-schema/invalid/persona/extra-persona-fields.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/invalid/persona/missing-role.agent.yaml b/test/fixtures/agent-schema/invalid/persona/missing-role.agent.yaml deleted file mode 100644 index 3dbd6c457..000000000 --- a/test/fixtures/agent-schema/invalid/persona/missing-role.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/invalid/prompts/empty-content.agent.yaml b/test/fixtures/agent-schema/invalid/prompts/empty-content.agent.yaml deleted file mode 100644 index 3248edca3..000000000 --- a/test/fixtures/agent-schema/invalid/prompts/empty-content.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/invalid/prompts/extra-prompt-fields.agent.yaml b/test/fixtures/agent-schema/invalid/prompts/extra-prompt-fields.agent.yaml deleted file mode 100644 index aeccee29e..000000000 --- a/test/fixtures/agent-schema/invalid/prompts/extra-prompt-fields.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/invalid/prompts/missing-content.agent.yaml b/test/fixtures/agent-schema/invalid/prompts/missing-content.agent.yaml deleted file mode 100644 index 7f31723b7..000000000 --- a/test/fixtures/agent-schema/invalid/prompts/missing-content.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/invalid/prompts/missing-id.agent.yaml b/test/fixtures/agent-schema/invalid/prompts/missing-id.agent.yaml deleted file mode 100644 index f05f054a2..000000000 --- a/test/fixtures/agent-schema/invalid/prompts/missing-id.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/invalid/top-level/empty-file.agent.yaml b/test/fixtures/agent-schema/invalid/top-level/empty-file.agent.yaml deleted file mode 100644 index bdc8a1e1b..000000000 --- a/test/fixtures/agent-schema/invalid/top-level/empty-file.agent.yaml +++ /dev/null @@ -1,5 +0,0 @@ -# Test: Empty YAML file -# Expected: FAIL -# Error code: invalid_type -# Error path: -# Error expected: object diff --git a/test/fixtures/agent-schema/invalid/top-level/extra-top-level-keys.agent.yaml b/test/fixtures/agent-schema/invalid/top-level/extra-top-level-keys.agent.yaml deleted file mode 100644 index cc888a51b..000000000 --- a/test/fixtures/agent-schema/invalid/top-level/extra-top-level-keys.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/invalid/top-level/missing-agent-key.agent.yaml b/test/fixtures/agent-schema/invalid/top-level/missing-agent-key.agent.yaml deleted file mode 100644 index aa8814190..000000000 --- a/test/fixtures/agent-schema/invalid/top-level/missing-agent-key.agent.yaml +++ /dev/null @@ -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: ❌ diff --git a/test/fixtures/agent-schema/invalid/yaml-errors/invalid-indentation.agent.yaml b/test/fixtures/agent-schema/invalid/yaml-errors/invalid-indentation.agent.yaml deleted file mode 100644 index 599edbb04..000000000 --- a/test/fixtures/agent-schema/invalid/yaml-errors/invalid-indentation.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/invalid/yaml-errors/malformed-yaml.agent.yaml b/test/fixtures/agent-schema/invalid/yaml-errors/malformed-yaml.agent.yaml deleted file mode 100644 index 97c66a3b6..000000000 --- a/test/fixtures/agent-schema/invalid/yaml-errors/malformed-yaml.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/valid/critical-actions/empty-critical-actions.agent.yaml b/test/fixtures/agent-schema/valid/critical-actions/empty-critical-actions.agent.yaml deleted file mode 100644 index dc73477f1..000000000 --- a/test/fixtures/agent-schema/valid/critical-actions/empty-critical-actions.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/valid/critical-actions/no-critical-actions.agent.yaml b/test/fixtures/agent-schema/valid/critical-actions/no-critical-actions.agent.yaml deleted file mode 100644 index 2df52f7f9..000000000 --- a/test/fixtures/agent-schema/valid/critical-actions/no-critical-actions.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/valid/critical-actions/valid-critical-actions.agent.yaml b/test/fixtures/agent-schema/valid/critical-actions/valid-critical-actions.agent.yaml deleted file mode 100644 index 198bc835e..000000000 --- a/test/fixtures/agent-schema/valid/critical-actions/valid-critical-actions.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/valid/menu-commands/all-command-types.agent.yaml b/test/fixtures/agent-schema/valid/menu-commands/all-command-types.agent.yaml deleted file mode 100644 index 22ae9886d..000000000 --- a/test/fixtures/agent-schema/valid/menu-commands/all-command-types.agent.yaml +++ /dev/null @@ -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 - \ No newline at end of file diff --git a/test/fixtures/agent-schema/valid/menu-commands/multiple-commands.agent.yaml b/test/fixtures/agent-schema/valid/menu-commands/multiple-commands.agent.yaml deleted file mode 100644 index 9133b02de..000000000 --- a/test/fixtures/agent-schema/valid/menu-commands/multiple-commands.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/valid/menu-triggers/compound-triggers.agent.yaml b/test/fixtures/agent-schema/valid/menu-triggers/compound-triggers.agent.yaml deleted file mode 100644 index 7a9fdec0b..000000000 --- a/test/fixtures/agent-schema/valid/menu-triggers/compound-triggers.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/valid/menu-triggers/kebab-case-triggers.agent.yaml b/test/fixtures/agent-schema/valid/menu-triggers/kebab-case-triggers.agent.yaml deleted file mode 100644 index cfae4fdea..000000000 --- a/test/fixtures/agent-schema/valid/menu-triggers/kebab-case-triggers.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/valid/menu/multiple-menu-items.agent.yaml b/test/fixtures/agent-schema/valid/menu/multiple-menu-items.agent.yaml deleted file mode 100644 index c95025025..000000000 --- a/test/fixtures/agent-schema/valid/menu/multiple-menu-items.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/valid/menu/single-menu-item.agent.yaml b/test/fixtures/agent-schema/valid/menu/single-menu-item.agent.yaml deleted file mode 100644 index 00c361d00..000000000 --- a/test/fixtures/agent-schema/valid/menu/single-menu-item.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/valid/metadata/core-agent-with-module.agent.yaml b/test/fixtures/agent-schema/valid/metadata/core-agent-with-module.agent.yaml deleted file mode 100644 index e8ad0497a..000000000 --- a/test/fixtures/agent-schema/valid/metadata/core-agent-with-module.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/valid/metadata/empty-module-name-in-path.agent.yaml b/test/fixtures/agent-schema/valid/metadata/empty-module-name-in-path.agent.yaml deleted file mode 100644 index 10a54cb82..000000000 --- a/test/fixtures/agent-schema/valid/metadata/empty-module-name-in-path.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/valid/metadata/malformed-path-treated-as-core.agent.yaml b/test/fixtures/agent-schema/valid/metadata/malformed-path-treated-as-core.agent.yaml deleted file mode 100644 index f7f752b17..000000000 --- a/test/fixtures/agent-schema/valid/metadata/malformed-path-treated-as-core.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/valid/metadata/module-agent-correct.agent.yaml b/test/fixtures/agent-schema/valid/metadata/module-agent-correct.agent.yaml deleted file mode 100644 index 6b5683f83..000000000 --- a/test/fixtures/agent-schema/valid/metadata/module-agent-correct.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/valid/metadata/module-agent-missing-module.agent.yaml b/test/fixtures/agent-schema/valid/metadata/module-agent-missing-module.agent.yaml deleted file mode 100644 index 6919c6141..000000000 --- a/test/fixtures/agent-schema/valid/metadata/module-agent-missing-module.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/valid/metadata/wrong-module-value.agent.yaml b/test/fixtures/agent-schema/valid/metadata/wrong-module-value.agent.yaml deleted file mode 100644 index 9f6c9d218..000000000 --- a/test/fixtures/agent-schema/valid/metadata/wrong-module-value.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/valid/persona/complete-persona.agent.yaml b/test/fixtures/agent-schema/valid/persona/complete-persona.agent.yaml deleted file mode 100644 index bee421b21..000000000 --- a/test/fixtures/agent-schema/valid/persona/complete-persona.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/valid/prompts/empty-prompts.agent.yaml b/test/fixtures/agent-schema/valid/prompts/empty-prompts.agent.yaml deleted file mode 100644 index da32f70e1..000000000 --- a/test/fixtures/agent-schema/valid/prompts/empty-prompts.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/valid/prompts/no-prompts.agent.yaml b/test/fixtures/agent-schema/valid/prompts/no-prompts.agent.yaml deleted file mode 100644 index 46c50f11f..000000000 --- a/test/fixtures/agent-schema/valid/prompts/no-prompts.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/valid/prompts/valid-prompts-minimal.agent.yaml b/test/fixtures/agent-schema/valid/prompts/valid-prompts-minimal.agent.yaml deleted file mode 100644 index 2a2d7d982..000000000 --- a/test/fixtures/agent-schema/valid/prompts/valid-prompts-minimal.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/valid/prompts/valid-prompts-with-description.agent.yaml b/test/fixtures/agent-schema/valid/prompts/valid-prompts-with-description.agent.yaml deleted file mode 100644 index 5585415e0..000000000 --- a/test/fixtures/agent-schema/valid/prompts/valid-prompts-with-description.agent.yaml +++ /dev/null @@ -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 diff --git a/test/fixtures/agent-schema/valid/top-level/minimal-core-agent.agent.yaml b/test/fixtures/agent-schema/valid/top-level/minimal-core-agent.agent.yaml deleted file mode 100644 index f3bf0b9ed..000000000 --- a/test/fixtures/agent-schema/valid/top-level/minimal-core-agent.agent.yaml +++ /dev/null @@ -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 diff --git a/test/test-agent-schema.js b/test/test-agent-schema.js deleted file mode 100644 index 8f3318fd7..000000000 --- a/test/test-agent-schema.js +++ /dev/null @@ -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); -}); diff --git a/test/test-cli-integration.sh b/test/test-cli-integration.sh deleted file mode 100755 index cab4212d3..000000000 --- a/test/test-cli-integration.sh +++ /dev/null @@ -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 diff --git a/test/unit-test-schema.js b/test/unit-test-schema.js deleted file mode 100644 index e70d2ae8b..000000000 --- a/test/unit-test-schema.js +++ /dev/null @@ -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); -} diff --git a/tools/schema/agent.js b/tools/schema/agent.js deleted file mode 100644 index dfec1322f..000000000 --- a/tools/schema/agent.js +++ /dev/null @@ -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: " or fuzzy match on " - * @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} 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//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`, - }); -} diff --git a/tools/validate-agent-schema.js b/tools/validate-agent-schema.js deleted file mode 100644 index 3cd85823f..000000000 --- a/tools/validate-agent-schema.js +++ /dev/null @@ -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); -});