Shumiland/tests/helpers/router.test.js
Vadym Samoilenko 9b41fa447a
Some checks are pending
CI / Type Check (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Unit Tests (push) Waiting to run
Deploy / Build & Push Image (push) Waiting to run
Deploy / Deploy to VPS (push) Blocked by required conditions
feat: complete backend B1-B7 — Payload CMS, ezy payments, leads, deploy
- B1: Next.js 15 + Payload CMS 3.0 + Postgres 16, ESLint, Prettier, Husky, Vitest
- B2: 9 collections, 6 globals, 12 Page Builder blocks, access control, slugify/revalidate hooks
- B3: ezy.com.ua payments, Binotel HMAC webhook, leads API, Telegram bot, Resend email, rate limiting
- B4: Tariffs collection with ezy API sync (cron + manual), dynamic pricing source-of-truth
- B5: 13 test files covering unit libs and all API routes
- B6: Dockerfile multi-stage, docker-compose.prod.yml, nginx.conf SSL, GitHub Actions CI/CD, health endpoint
- B7: docs/admin-guide-ua.md (marketer guide), docs/deploy.md (VPS instructions), README quickstart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 19:14:54 +01:00

199 lines
8.4 KiB
JavaScript

/**
* Tests for .claude/helpers/router.js
*
* Run: node --test tests/helpers/router.test.js
*
* Coverage gaps addressed:
* - routeTask: each task pattern, default fallback, case insensitivity
* - Return shape: agent, confidence, reason fields always present
* - Edge cases: empty string, whitespace-only, numeric string
* - Pattern conflicts: first-match wins (no multi-match merging)
*/
'use strict'
const { describe, it } = require('node:test')
const assert = require('node:assert/strict')
const { routeTask, AGENT_CAPABILITIES, TASK_PATTERNS } = require('../../.claude/helpers/router.js')
describe('router.js — routeTask()', () => {
// ── Return shape ────────────────────────────────────────────────────────────
describe('return shape', () => {
it('always returns an object with agent, confidence, and reason', () => {
const r = routeTask('anything')
assert.ok(typeof r.agent === 'string', 'agent must be a string')
assert.ok(typeof r.confidence === 'number', 'confidence must be a number')
assert.ok(typeof r.reason === 'string', 'reason must be a string')
})
it('confidence is between 0 and 1', () => {
const tasks = ['build a feature', 'write tests', 'random unknown task']
for (const t of tasks) {
const r = routeTask(t)
assert.ok(r.confidence >= 0 && r.confidence <= 1, `confidence out of range for: ${t}`)
}
})
})
// ── Pattern matching ────────────────────────────────────────────────────────
describe('pattern → agent routing', () => {
// NOTE: router uses first-match-wins on TASK_PATTERNS key order.
// Keywords like 'build', 'create', 'implement' match the coder pattern BEFORE
// domain-specific patterns (backend-dev, frontend-dev). Tests below reflect
// actual routing behaviour, not intuitive intent — see the "known routing quirks"
// section below for cases where first-match hides the intended agent.
const cases = [
// coder pattern: 'implement|create|build|add|write code'
['implement a login page', 'coder'],
['create a REST endpoint', 'coder'], // 'create' → coder, not backend-dev
['build authentication', 'coder'],
['add a new field to the schema', 'coder'],
['write code for the parser', 'coder'],
// tester pattern: 'test|spec|coverage|unit test|integration'
['test the signup flow', 'tester'],
['write unit tests for utils', 'tester'],
['check coverage for auth module', 'tester'],
['integration tests for payment API', 'tester'],
['write a spec for the validator', 'tester'],
// reviewer pattern: 'review|audit|check|validate|security'
['review the PR for security issues', 'reviewer'],
['audit the codebase for vulnerabilities', 'reviewer'],
['check code quality', 'reviewer'],
['validate the configuration', 'reviewer'],
// researcher pattern: 'research|find|search|documentation|explore'
['research best JWT libraries', 'researcher'],
['find documentation for Redis', 'researcher'],
['search for OAuth examples', 'researcher'],
['explore alternative approaches', 'researcher'],
// architect pattern: 'design|architect|structure|plan'
['design the microservice architecture', 'architect'],
['plan the database structure', 'architect'],
// backend-dev pattern: 'api|endpoint|server|backend|database'
// Only works when coder/tester/reviewer/researcher/architect keywords are absent
['set up the database schema', 'backend-dev'],
['the server-side middleware', 'backend-dev'], // 'server' matches backend-dev
['REST api for payment gateway', 'backend-dev'], // 'api' without documentation/search
// frontend-dev pattern: 'ui|frontend|component|react|css|style'
// Only works when patterns 1-6 don't fire first
['style the header with CSS', 'frontend-dev'],
['the UI layout mockup', 'frontend-dev'], // no review/research/create
['react hooks and state', 'frontend-dev'], // 'react' not in earlier patterns
// devops pattern: 'deploy|docker|ci|cd|pipeline|infrastructure'
['deploy to production', 'devops'],
['set up Docker Compose', 'devops'], // 'set up' doesn't match coder
['configure the CI pipeline', 'devops'],
]
for (const [task, expectedAgent] of cases) {
it(`"${task}" → ${expectedAgent}`, () => {
const r = routeTask(task)
assert.equal(r.agent, expectedAgent, `Expected ${expectedAgent}, got ${r.agent}`)
assert.equal(r.confidence, 0.8)
})
}
})
// ── Default fallback ────────────────────────────────────────────────────────
describe('default fallback', () => {
it('routes to coder with confidence 0.5 when no pattern matches', () => {
const r = routeTask('do something completely unknown xyz123')
assert.equal(r.agent, 'coder')
assert.equal(r.confidence, 0.5)
})
it('handles empty string input without throwing', () => {
assert.doesNotThrow(() => routeTask(''))
const r = routeTask('')
assert.equal(r.agent, 'coder')
assert.equal(r.confidence, 0.5)
})
it('handles whitespace-only input', () => {
const r = routeTask(' ')
assert.equal(r.agent, 'coder')
})
it('handles numeric string input', () => {
const r = routeTask('12345')
assert.equal(r.agent, 'coder')
})
})
// ── Case insensitivity ──────────────────────────────────────────────────────
describe('case insensitivity', () => {
it('matches uppercase task keywords', () => {
const r = routeTask('IMPLEMENT a feature')
assert.equal(r.agent, 'coder')
})
it('matches mixed-case task keywords', () => {
const r = routeTask('Write Tests For The Module')
assert.equal(r.agent, 'tester')
})
})
// ── AGENT_CAPABILITIES export ───────────────────────────────────────────────
describe('AGENT_CAPABILITIES', () => {
it('exports capabilities for all core agents', () => {
const expected = [
'coder',
'tester',
'reviewer',
'researcher',
'architect',
'backend-dev',
'frontend-dev',
'devops',
]
for (const agent of expected) {
assert.ok(agent in AGENT_CAPABILITIES, `Missing capabilities for: ${agent}`)
assert.ok(Array.isArray(AGENT_CAPABILITIES[agent]))
}
})
})
// ── Known routing quirks (first-match-wins causes mis-routing) ───────────────
// These are NOT bugs in the tests — they document real mis-routing that should
// be fixed by reordering TASK_PATTERNS or using a scoring approach.
describe('known routing quirks — first-match wins over domain intent', () => {
it('"create a React component" routes to coder, not frontend-dev (create matches first)', () => {
assert.equal(routeTask('create a React component').agent, 'coder')
})
it('"build the UI dashboard" routes to coder, not frontend-dev (build matches first)', () => {
assert.equal(routeTask('build the UI dashboard').agent, 'coder')
})
it('"implement server-side authentication" routes to coder, not backend-dev', () => {
assert.equal(routeTask('implement server-side authentication').agent, 'coder')
})
it('"build the REST API" routes to coder, not backend-dev (build matches first)', () => {
assert.equal(routeTask('build the REST API').agent, 'coder')
})
})
// ── Integration: first-match-wins when task matches multiple patterns ────────
describe('first-match-wins on ambiguous tasks', () => {
it('resolves to the first matching pattern when task fits multiple', () => {
// "implement api" matches 'implement' (→ coder) AND 'api' (→ backend-dev)
const r = routeTask('implement api endpoint')
assert.equal(r.agent, 'coder') // coder wins because it is listed first
assert.equal(r.confidence, 0.8)
})
})
})