Some checks failed
tests / Unit tests (Node 22) (push) Failing after 2m13s
Expands the unit test suite from 320 to 380 tests (+60) and adds a
Gitea Actions CI workflow. Closes all six follow-up recommendations
from the test-architecture validation report.
New tests (tests/unit/):
- ollamaClient.test.ts — Ollama SDK wrapper, options passthrough
- litellmClient.test.ts — OpenAI SDK wrapper, model fallback
- personaLoader.test.ts — Zod validation + cache invalidation
- foundryReward.test.ts — Tool plugin: lookup, errors, partial grants
- xpAwarder.test.ts — Bulk XP awards + per-player skip reasons
- redisErrorPath.test.ts — Singleton error handler does not crash
- messageRouterRunLLMTurn.test.ts — 18 cases for the runtime heart:
narrative-only path, tool dispatch, filter correction, retry loop
guard, missed-skill-check heuristic, typing indicator interval,
LLM error fallback, archive on resolve.
Coverage (line %):
- harness/litellmClient.ts 0 → 100
- harness/ollamaClient.ts 0 → 100
- harness/tools/foundryReward.ts 0 → 100
- session/xpAwarder.ts 0 → 100
- persona/loader.ts 0 → 100
- db/redis.ts 0 → 100
- bot/handlers/messageRouter.ts 0 → 39.86 (runLLMTurn now covered)
Tooling:
- package.json: + test:coverage, test:watch scripts
- devDep: @vitest/coverage-v8@^3.1.0
- tests/README.md: conventions, anti-patterns, template map
- .gitignore: exclude coverage/
- .gitea/workflows/test.yml: Node 22, npm cache, tsc --noEmit gate
Documentation (from earlier /bmad-document-project run, now committed):
- docs/index.md
- docs/project-overview.md
- docs/architecture.md
- docs/deployment-guide.md
- docs/api-contracts.md
- docs/data-models.md
- docs/source-tree-analysis.md
- docs/component-inventory.md
- docs/development-guide.md
- _bmad-output/test-artifacts/automate-validation-report.md
Co-Authored-By: Claude <noreply@anthropic.com>
121 lines
3.3 KiB
TypeScript
121 lines
3.3 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { mkdtempSync, writeFileSync, rmSync } from 'fs';
|
|
import { tmpdir } from 'os';
|
|
import { join } from 'path';
|
|
|
|
import { loadPersona, clearPersonaCache } from '../../src/persona/loader.js';
|
|
|
|
let tmpDir: string;
|
|
|
|
beforeEach(() => {
|
|
clearPersonaCache();
|
|
tmpDir = mkdtempSync(join(tmpdir(), 'persona-test-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
function writePersona(yaml: string): string {
|
|
const path = join(tmpDir, 'persona.yaml');
|
|
writeFileSync(path, yaml, 'utf8');
|
|
return path;
|
|
}
|
|
|
|
describe('loadPersona', () => {
|
|
it('loads a valid persona YAML file and parses it', () => {
|
|
const path = writePersona(`
|
|
name: "Zalram Cloudwalker"
|
|
description: "Aasimar Divination Wizard, level 8"
|
|
persona: |
|
|
You are Zalram — bound to the digital realm.
|
|
responseStyle: "Dry, formal, occasionally sardonic."
|
|
`);
|
|
|
|
const persona = loadPersona(path);
|
|
|
|
expect(persona.name).toBe('Zalram Cloudwalker');
|
|
expect(persona.description).toBe('Aasimar Divination Wizard, level 8');
|
|
expect(persona.persona).toContain('You are Zalram');
|
|
expect(persona.responseStyle).toBe('Dry, formal, occasionally sardonic.');
|
|
});
|
|
|
|
it('caches the result — second call returns the same instance without re-reading the file', () => {
|
|
const path = writePersona(`
|
|
name: "Test"
|
|
description: "A test persona"
|
|
persona: "Persona text"
|
|
responseStyle: "Style text"
|
|
`);
|
|
|
|
const first = loadPersona(path);
|
|
// Replace the file with something invalid. The cached result must still come back.
|
|
writeFileSync(path, 'this is not valid YAML: [', 'utf8');
|
|
|
|
const second = loadPersona(path);
|
|
expect(second).toBe(first);
|
|
});
|
|
|
|
it('clears the cache when clearPersonaCache is called', () => {
|
|
const path1 = writePersona(`
|
|
name: "First"
|
|
description: "d"
|
|
persona: "p"
|
|
responseStyle: "r"
|
|
`);
|
|
const first = loadPersona(path1);
|
|
|
|
// Mutate the file to something different, then clear + reload.
|
|
writeFileSync(path1, `
|
|
name: "Second"
|
|
description: "d"
|
|
persona: "p"
|
|
responseStyle: "r"
|
|
`, 'utf8');
|
|
clearPersonaCache();
|
|
|
|
const second = loadPersona(path1);
|
|
expect(second.name).toBe('Second');
|
|
expect(second).not.toBe(first);
|
|
});
|
|
|
|
it('uses ./persona.yaml as the default path when none is provided', () => {
|
|
// This test would require a real ./persona.yaml to exist. Verify the
|
|
// default-path behaviour indirectly by ensuring the function uses the
|
|
// passed-in path even when it differs from the default.
|
|
const path = writePersona(`
|
|
name: "DefaultTest"
|
|
description: "d"
|
|
persona: "p"
|
|
responseStyle: "r"
|
|
`);
|
|
|
|
const persona = loadPersona(path);
|
|
expect(persona.name).toBe('DefaultTest');
|
|
});
|
|
|
|
it('throws a Zod validation error when a required field is missing', () => {
|
|
const path = writePersona(`
|
|
name: "Missing fields"
|
|
# description, persona, responseStyle all absent
|
|
`);
|
|
|
|
expect(() => loadPersona(path)).toThrow();
|
|
});
|
|
|
|
it('throws a Zod validation error when a field has the wrong type', () => {
|
|
const path = writePersona(`
|
|
name: 123
|
|
description: "d"
|
|
persona: "p"
|
|
responseStyle: "r"
|
|
`);
|
|
|
|
expect(() => loadPersona(path)).toThrow();
|
|
});
|
|
|
|
it('throws when the file does not exist', () => {
|
|
expect(() => loadPersona(join(tmpDir, 'does-not-exist.yaml'))).toThrow();
|
|
});
|
|
});
|