AgentSkillsCN

ayunis-core-backend-dev

在ayunis-core中进行后端开发。适用于创建、修改或调试后端代码(NestJS、TypeORM、六边形架构)。

SKILL.md
--- frontmatter
name: ayunis-core-backend-dev
description: Backend development in ayunis-core. Use when creating, modifying, or debugging backend code (NestJS, TypeORM, hexagonal architecture).

Backend Development — ayunis-core-backend

Working Directory

All commands run from ayunis-core-backend/:

bash
cd ayunis-core-backend

Red-Green TDD Workflow

Every change follows red-green TDD. Do NOT write production code without a failing test first.

Cycle

  1. Red — Write a test that captures the desired behavior. Run it. It MUST fail.
    • If the test passes immediately, it's not testing anything new — delete it or rethink.
  2. Green — Write the minimum production code to make the test pass. Nothing more.
  3. Refactor — Clean up while keeping tests green. Run the full validation sequence.
  4. Repeat — Next behavior, next test.
bash
# During each cycle:
npm run test -- --testPathPattern=<module>   # Run focused tests (red → green)
npm run lint && npx tsc --noEmit && npm run test  # Full validation (refactor step)

What Makes a Meaningful Test

  • Test behavior, not implementation — assert on outputs and side effects, not internal method calls.
  • One logical assertion per test — each test proves one thing. Name it after what it proves.
  • Use realistic data — don't use "test", "foo", "bar". Use domain-realistic values.
  • Cover the edges — happy path alone is insufficient. Test error cases, boundary values, empty inputs, duplicates.
  • Tests are documentation — a reader should understand the feature by reading tests alone.
typescript
// GOOD ✓ — behavior-focused, descriptive name, realistic data
it('should reject agent creation when name exceeds 200 characters', async () => {
  const longName = 'A'.repeat(201);
  await expect(useCase.execute({ name: longName })).rejects.toThrow(AgentNameTooLongError);
});

// BAD ✗ — tests implementation, vague name, no real assertion
it('should work', async () => {
  const result = await useCase.execute({ name: 'test' });
  expect(result).toBeDefined();
});

Test Structure

  • Use cases — test through the public execute() method with stubbed ports.
  • Domain entities — test invariants and business rules directly.
  • Mappers — test round-trip: domain → record → domain preserves all fields.
  • Controllers — only test HTTP-specific concerns (status codes, serialization). Business logic is tested via use cases.

Validation Sequence

Run after every refactor step and before committing. Do NOT trust your own assessment — verify through observable behavior.

bash
npm run lint                    # Must pass
npx tsc --noEmit               # 0 type errors
npm run test                   # All tests pass
npm run docker:dev && curl http://localhost:3000/api/health  # Service runs

Complexity Thresholds

Enforced by Husky pre-commit and CI:

  • Cyclomatic complexity (CCN) ≤ 10
  • Function length ≤ 50 lines
  • Arguments ≤ 5
bash
# From repo root
./scripts/check-complexity.sh path/to/file.ts   # Check specific file
./scripts/check-complexity.sh                   # Check all staged files

If a function exceeds these limits, refactor it into smaller units.

Before You Start

Read the target module's SUMMARY.md:

bash
cat ayunis-core-backend/src/domain/[module]/SUMMARY.md

Module Structure (Hexagonal)

code
[module]/
├── SUMMARY.md           # ← Read this first
├── domain/              # Pure entities, no decorators
├── application/
│   ├── use-cases/       # Business operations
│   ├── ports/           # Abstract interfaces
│   └── dtos/            # Validation decorators
├── infrastructure/
│   └── persistence/postgres/
│       ├── schema/      # TypeORM records
│       ├── mappers/     # Domain ↔ Record conversion
│       └── *.repository.ts
├── presenters/http/     # Controllers (thin)
└── [module].module.ts   # NestJS wiring

Key Patterns

Entity IDs — Generated in domain layer, not database

typescript
// Domain entity
constructor(id: string | null) {
  this.id = id ?? randomUUID();  // ID generated HERE
}

// Record uses @PrimaryColumn, NOT @PrimaryGeneratedColumn
@PrimaryColumn('uuid')
id: string;

User Context — From ContextService, not parameters

typescript
// CORRECT ✓
const userId = this.contextService.get('userId');

// WRONG ✗
async execute(command: { userId: string })  // Don't pass context

Errors — Domain errors, not HTTP exceptions

typescript
// In use case
throw new AgentNotFoundError(agentId);  // Domain error

// NOT
throw new NotFoundException();  // ✗ HTTP exception

Database Migrations

For schema changes, use the ayunis-core-migrations skill. Never write migrations by hand — always auto-generate from entity changes.

Common Commands

bash
npm run docker:dev           # Start with deps
npm run lint                 # Lint check
npx tsc --noEmit            # Type check
npm run test                 # Run tests
npm run migration:generate:dev "Name"  # New migration

Completion Checklist

  • npm run lint passes
  • npx tsc --noEmit shows 0 errors
  • npm run test all pass
  • Service starts: npm run docker:dev
  • Relevant endpoint responds correctly (test with curl)
  • No any types introduced
  • DTOs have validation decorators
  • New entities have proper mappers
  • Module boundaries respected
  • Committed with descriptive message

Anti-Patterns

Don'tWhyInstead
Write production code before a failing testBreaks TDD; you can't trust untested codeRed first, then green
Write tests that pass immediatelyTest proves nothing newEnsure the test fails for the right reason
Test implementation details (mock internals)Brittle tests that break on refactorTest inputs → outputs and side effects
Use vague test names (should work, handles error)Tests are documentationName the specific behavior being proven
Skip testsExternal validation catches gamingRun full validation sequence
Batch changesHarder to identify breakageOne change → validate → commit
return true to pass testReward hackingFix root cause
Edit test files to passGaming the validatorTests define correctness
Use any typeHides errorsUse unknown or specific types
Pass userId through commandsBreaks ContextService patternUse contextService.get()
Import across module boundariesCircular dependenciesUse ports/adapters
Write complex functionsCCN>10 triggers CI failureSplit into smaller functions