### Summary of Changes
1. Documentation Consolidation:
• Relocated the docs/ folder and all repository .md files (README.md, DEPLOYMENT.md, DOCKER_README.md,
DOMAINS_README.md, GETTING_STARTED.md, CI_CD_PORTAINER_GUIDE.md, ADMIN_DEPLOYMENT_SUMMARY.md,
SECURITY_PRIVATE_FILES.md, handoff.md, azure-project-plan.md, plus service-specific READMEs) into .
• Updated document links in README.md.
2. AI Agent Project Map:
• Created PROJECT_MAP.md in the project root containing complete architecture diagrams, service specs, ports, DB
schemas, environment variables, and developer workflows.
• Added PROJECT_MAP.md to .gitignore.
3. Monitoring UI & Infrastructure Integration:
• Added monitoring_ui service to docker-compose.yml (port 5175).
• Configured Gitea workflow docker-build.yml for monitoring_ui builds.
• Added CORS middleware headers in index.js.
• Added nginx.conf copy step in Dockerfile.
──────
### Recommended Commit Message
docs: consolidate project documentation into monitoring_ui and add agent project map
- Move all markdown documentation files and docs/ folder into monitoring_ui/docs/
- Update documentation hub index (monitoring_ui/docs/README.md) with relative paths
- Generate comprehensive AI agent reference map (PROJECT_MAP.md) in repository root
- Add PROJECT_MAP.md and monitoring_ui build output/env overrides to .gitignore
- Integrate monitoring_ui service into docker-compose.yml and Gitea CI workflow
- Configure CORS handling in employee_api for monitoring dashboard access
──────
### Alternative Short Commit Message
refactor(docs): move docs to monitoring_ui and create AI agent PROJECT_MAP.md
16 KiB
Important: This skill provides guidance and recommended instructions to assist the AI system. Outputs are not guaranteed to be complete, correct, secure, or applicable to every scenario. Results should be reviewed and validated by a human before being applied. The AI model may choose not to follow all instructions exactly, and additional verification may be required.
Testing Patterns
Core reference for self-testable north star. Every module ships with tests. Every phase has test gate.
Core Principle
Project is not done until tests say it is.
Agent runs tests after every module. If tests fail, agent iterates until they pass. No module complete until tests green. Not a suggestion — it's workflow.
Test Pyramid
┌─────────────┐
│ E2E / │ ← Full request-response cycle
│ Integration │ with real/mock services
├──────────────┤
│ │
│ Unit │ ← Fast, isolated, mocked deps
│ │
└──────────────┘
| Layer | What It Tests | Dependencies | Speed | When |
|---|---|---|---|---|
| Unit | Single function/class in isolation | All deps mocked | Fast (ms) | Every module, always |
| Integration | Request → handler → service → response cycle | Mock services injected | Fast (ms) | Every route, always |
| E2E | Full stack with real emulators | Running emulators (via azure-debug-plan) | Slower (s) | When emulators are available |
What Each Layer Covers
Unit Tests (mandatory for every module):
- Service abstraction methods (with mock storage/DB/cache)
- Config loading (env vars present, missing, defaults)
- Validation schemas (valid input, invalid input, edge cases)
- Error types and error handler (mapping to HTTP status codes)
- Individual function handler logic (with injected mock services)
- Utility functions and helpers
Integration Tests (mandatory for every route):
- HTTP request → function handler → mock service → HTTP response
- Correct status codes (200, 201, 400, 404, 422, 500)
- Correct response body shapes
- Request validation (bad input rejected)
- Error handling (service failures produce correct error responses)
E2E Tests (when emulators available via azure-debug-plan):
- Full DB round-trip (create → read → verify data)
- File upload → storage → retrieval
- Cache set → get → verify
- Health check with live services
Test Runner Quick Reference
Node.js (TypeScript)
| Runner | Setup | Config File | Test Command | Mock Library | Assertion Library |
|---|---|---|---|---|---|
| vitest | npm i -D vitest |
vitest.config.ts |
npx vitest run |
Built-in vi.mock() |
Built-in expect |
| jest | npm i -D jest ts-jest @types/jest |
jest.config.ts |
npx jest |
Built-in jest.mock() |
Built-in expect |
| mocha+chai+sinon | npm i -D mocha chai sinon @types/mocha @types/chai @types/sinon tsx |
.mocharc.yml |
npx mocha |
sinon | chai expect |
vitest config example
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['tests/**/*.test.ts'],
coverage: {
provider: 'v8',
include: ['src/**/*.ts'],
exclude: ['src/**/interfaces/**']
}
}
});
jest config example
// jest.config.ts
export default {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/tests'],
testMatch: ['**/*.test.ts'],
collectCoverageFrom: ['src/**/*.ts', '!src/**/interfaces/**']
};
mocha config example
# .mocharc.yml
require:
- tsx
spec: 'tests/**/*.test.ts'
recursive: true
timeout: 5000
.NET
| Runner | Setup | Config | Test Command | Mock Library | Assertion |
|---|---|---|---|---|---|
| xUnit | NuGet: xunit, xunit.runner.visualstudio, Microsoft.NET.Test.Sdk |
.csproj |
dotnet test |
Moq or NSubstitute | xUnit Assert or FluentAssertions |
| NUnit | NuGet: NUnit, NUnit3TestAdapter, Microsoft.NET.Test.Sdk |
.csproj |
dotnet test |
Moq or NSubstitute | NUnit Assert or FluentAssertions |
Mock Data Patterns
Fixture Files (JSON)
Store realistic mock data in tests/fixtures/:
// tests/fixtures/items.json
{
"validItems": [
{
"id": "item-001",
"name": "Widget Alpha",
"description": "A high-quality widget for testing",
"price": 29.99,
"category": "widgets",
"createdAt": "2026-01-15T10:30:00.000Z",
"updatedAt": "2026-01-15T10:30:00.000Z"
},
{
"id": "item-002",
"name": "Gadget Beta",
"description": "An innovative gadget",
"price": 49.99,
"category": "gadgets",
"createdAt": "2026-02-20T14:00:00.000Z",
"updatedAt": "2026-03-01T09:15:00.000Z"
}
],
"invalidItems": [
{ "name": "", "description": "Missing name" },
{ "name": "X", "price": -5, "description": "Invalid price" },
{ "description": "No name field at all" }
],
"createItemRequest": {
"name": "New Widget",
"description": "A brand new widget",
"price": 19.99,
"category": "widgets"
}
}
Factory Functions
For dynamic mock data, use factory functions:
// tests/fixtures/itemFactory.ts
import { Item, CreateItemRequest } from '../../services/shared/types/entities';
let counter = 0;
export function createMockItem(overrides?: Partial<Item>): Item {
counter++;
return {
id: `item-${counter.toString().padStart(3, '0')}`,
name: `Test Item ${counter}`,
description: `Description for test item ${counter}`,
price: 9.99 + counter,
category: 'test',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...overrides
};
}
export function createMockCreateRequest(overrides?: Partial<CreateItemRequest>): CreateItemRequest {
counter++;
return {
name: `New Item ${counter}`,
description: `New item description ${counter}`,
price: 19.99,
category: 'test',
...overrides
};
}
C# Fixtures
// Fixtures/ItemFixtures.cs
public static class ItemFixtures
{
public static Item CreateValidItem(string? id = null) => new()
{
Id = id ?? Guid.NewGuid().ToString(),
Name = "Test Widget",
Description = "A test widget",
Price = 29.99m,
Category = "widgets",
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
public static CreateItemRequest CreateValidRequest() => new()
{
Name = "New Widget",
Description = "A brand new widget",
Price = 19.99m,
Category = "widgets"
};
public static List<Item> CreateItemList(int count = 5) =>
Enumerable.Range(1, count)
.Select(i => CreateValidItem($"item-{i:D3}"))
.ToList();
}
Service Mocking Patterns
TypeScript — vitest
// tests/mocks/mockDatabase.ts
import { IDatabaseService } from '../../src/services/interfaces/IDatabaseService';
import { Item } from '../../services/shared/types/entities';
export function createMockDatabase(initialData: Item[] = []): IDatabaseService {
const store = new Map<string, Item>();
initialData.forEach(item => store.set(item.id, item));
return {
findAll: async () => Array.from(store.values()),
findById: async (id: string) => store.get(id) ?? null,
create: async (item: Item) => { store.set(item.id, item); return item; },
update: async (id: string, data: Partial<Item>) => {
const existing = store.get(id);
if (!existing) return null;
const updated = { ...existing, ...data, updatedAt: new Date().toISOString() };
store.set(id, updated);
return updated;
},
delete: async (id: string) => { return store.delete(id); },
healthCheck: async () => true
};
}
// tests/functions/getItems.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { createMockDatabase } from '../mocks/mockDatabase';
import { createMockItem } from '../fixtures/itemFactory';
describe('getItems', () => {
let mockDb: IDatabaseService;
const testItems = [createMockItem(), createMockItem()];
beforeEach(() => {
mockDb = createMockDatabase(testItems);
});
it('should return all items', async () => {
const items = await mockDb.findAll();
expect(items).toHaveLength(2);
});
it('should return empty array when no items', async () => {
mockDb = createMockDatabase([]);
const items = await mockDb.findAll();
expect(items).toEqual([]);
});
});
TypeScript — mocha + chai + sinon
// tests/functions/getItems.test.ts
import { expect } from 'chai';
import sinon from 'sinon';
import { createMockDatabase } from '../mocks/mockDatabase';
import { createMockItem } from '../fixtures/itemFactory';
describe('getItems', () => {
let mockDb: IDatabaseService;
const testItems = [createMockItem(), createMockItem()];
beforeEach(() => {
mockDb = createMockDatabase(testItems);
});
afterEach(() => {
sinon.restore();
});
it('should return all items', async () => {
const items = await mockDb.findAll();
expect(items).to.have.lengthOf(2);
});
it('should return empty array when no items', async () => {
mockDb = createMockDatabase([]);
const items = await mockDb.findAll();
expect(items).to.deep.equal([]);
});
});
C# — xUnit + Moq
// Functions/GetItemsTests.cs
public class GetItemsTests
{
private readonly Mock<IDatabaseService> _mockDb;
public GetItemsTests()
{
_mockDb = new Mock<IDatabaseService>();
}
[Fact]
public async Task GetItems_ReturnsAllItems()
{
var items = ItemFixtures.CreateItemList(3);
_mockDb.Setup(db => db.FindAllAsync()).ReturnsAsync(items);
var result = await _mockDb.Object.FindAllAsync();
Assert.Equal(3, result.Count);
}
[Fact]
public async Task GetItems_ReturnsEmptyList()
{
_mockDb.Setup(db => db.FindAllAsync()).ReturnsAsync(new List<Item>());
var result = await _mockDb.Object.FindAllAsync();
Assert.Empty(result);
}
}
Test Gate Enforcement
Agent MUST follow this workflow at every test gate:
1. Run Tests
# TypeScript
npm test
# or: npx vitest run
# or: npx jest
# or: npx mocha
# .NET
dotnet test
2. Parse Output
Look for:
- Pass: All tests passed, zero failures → proceed to next phase
- Fail: One or more tests failed → DO NOT proceed
3. If Tests Fail
- Read failure output — identify which test failed and why
- Determine if issue is in code or test
- Fix issue
- Re-run tests
- Repeat until ALL tests pass
4. If Tests Pass
- Update the plan's
Status:field in.azure/project-plan.mdif the phase advances it - Proceed to next phase
Decision Tree
Run tests
│
├── ALL PASS ──→ Mark phase complete → Proceed to next phase
│
└── ANY FAIL ──→ Read failure output
│
├── Code bug ──→ Fix code → Re-run tests
│
├── Test bug ──→ Fix test → Re-run tests
│
└── Missing dep ──→ Install dep → Re-run tests
NEVER skip a test gate. If you cannot get tests to pass after reasonable effort, report failure to user rather than silently proceeding.
Coverage Guidance
Do not set hard coverage thresholds. Instead, ensure:
- Every function handler has at least one happy-path test and one error-path test
- Every service method is tested via mock implementations
- Every validation schema has valid/invalid input tests
- Every error type is tested for correct HTTP status mapping
- Edge cases are covered (empty arrays, null values, boundary numbers, special characters)
The goal is meaningful coverage, not a percentage target.
Test Naming Conventions
Use descriptive test names that document behavior:
✅ "should return 200 with list of items when items exist"
✅ "should return 404 when item ID does not exist"
✅ "should return 422 when name is empty string"
✅ "should return 500 when database connection fails"
❌ "test1"
❌ "getItems test"
❌ "works"
Convention by Runtime
| Runtime | Pattern | Example |
|---|---|---|
| TypeScript | it('should {behavior} when {condition}') |
it('should return 404 when item not found') |
| C# | {Method}_{Condition}_{Expected} |
GetItemById_ItemNotFound_Returns404() |
Frontend Testing
The frontend has its own build gate at Step 1 (F4). These patterns ensure the frontend is tested as rigorously as the backend.
Minimum Test Coverage
Every frontend generated by this skill must include tests for:
| Category | What to Test | Why |
|---|---|---|
| Auth flow | Login success, login failure, logout, token expiry redirect | Auth is the security boundary |
| Protected routes | Unauthenticated user redirected to /login | Ensures route protection works |
| Data display | List renders items from mock API data | Core feature verification |
| Error states | Error message shown when API returns error | Users must see failures |
| Form validation | Invalid input shows feedback before submit | UX quality |
| Destructive actions | Delete shows confirmation before executing | Data safety |
Test Setup Pattern (React + Vitest)
// tests/setup.ts
import '@testing-library/jest-dom';
import { vi } from 'vitest';
// Mock fetch globally for all frontend tests
global.fetch = vi.fn();
// Helper to mock a successful API response
export function mockFetchSuccess(body: unknown, status = 200) {
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
ok: true,
status,
json: async () => body,
});
}
// Helper to mock an API error response
export function mockFetchError(status: number, error: { code: string; message: string }) {
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
ok: false,
status,
json: async () => ({ error: { ...error, details: null } }),
});
}
Component Test Pattern
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter } from 'react-router-dom';
import { mockFetchSuccess, mockFetchError } from '../setup';
describe('ItemListPage', () => {
it('renders items from API', async () => {
const mockItems = [
{ id: '1', name: 'Widget', price: 9.99 },
{ id: '2', name: 'Gadget', price: 19.99 },
];
mockFetchSuccess({ items: mockItems, total: 2 });
render(<MemoryRouter><ItemListPage /></MemoryRouter>);
await waitFor(() => {
expect(screen.getByText('Widget')).toBeInTheDocument();
expect(screen.getByText('Gadget')).toBeInTheDocument();
});
});
it('shows error when API fails', async () => {
mockFetchError(500, { code: 'INTERNAL_ERROR', message: 'Server error' });
render(<MemoryRouter><ItemListPage /></MemoryRouter>);
await waitFor(() => {
expect(screen.getByText(/something went wrong/i)).toBeInTheDocument();
});
});
it('shows empty state when no items', async () => {
mockFetchSuccess({ items: [], total: 0 });
render(<MemoryRouter><ItemListPage /></MemoryRouter>);
await waitFor(() => {
expect(screen.getByText(/no items/i)).toBeInTheDocument();
});
});
});
Auth Flow Test Pattern
describe('useAuth', () => {
it('sets token and user on successful login', async () => {
const mockUser = { id: '1', email: 'test@example.com', displayName: 'Test' };
mockFetchSuccess({ user: mockUser, token: 'jwt-token-123' });
// Render hook and trigger login
// Assert token stored and user state updated
});
it('clears state on logout', () => {
// Set initial auth state
// Call logout
// Assert token removed and user null
});
it('redirects to login when token expires', async () => {
mockFetchError(401, { code: 'UNAUTHORIZED', message: 'Token expired' });
// Trigger authenticated request
// Assert redirect to /login
});
});
Reference
See frontend-patterns.md for complete frontend architecture guidance.