Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)
Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
230 lines
7.0 KiB
TypeScript
230 lines
7.0 KiB
TypeScript
import { describe, it, expect, beforeEach, afterAll } from 'vitest';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { Score } from '../../shared/types.js';
|
|
|
|
const DATA_DIR = path.join(process.cwd(), 'server', 'data');
|
|
const SESSION_FILE = path.join(DATA_DIR, 'session.json');
|
|
const SEED_FILE = path.join(DATA_DIR, 'seed.json');
|
|
const BACKUP_FILE = path.join(DATA_DIR, 'session.json.bak');
|
|
|
|
// Import the store module — initialization runs on import
|
|
import * as store from '../store.js';
|
|
|
|
describe('server/store', () => {
|
|
// Reset session.json from seed before each test for isolation
|
|
beforeEach(() => {
|
|
const seedData = fs.readFileSync(SEED_FILE, 'utf-8');
|
|
fs.writeFileSync(SESSION_FILE, seedData, 'utf-8');
|
|
// Remove backup if it exists
|
|
if (fs.existsSync(BACKUP_FILE)) {
|
|
fs.unlinkSync(BACKUP_FILE);
|
|
}
|
|
});
|
|
|
|
// Clean up after all tests
|
|
afterAll(() => {
|
|
// Restore session.json from seed
|
|
const seedData = fs.readFileSync(SEED_FILE, 'utf-8');
|
|
fs.writeFileSync(SESSION_FILE, seedData, 'utf-8');
|
|
if (fs.existsSync(BACKUP_FILE)) {
|
|
fs.unlinkSync(BACKUP_FILE);
|
|
}
|
|
});
|
|
|
|
it('getStore() returns valid StoreData', () => {
|
|
const data = store.getStore();
|
|
expect(data).toHaveProperty('session');
|
|
expect(data).toHaveProperty('teams');
|
|
expect(data).toHaveProperty('jurors');
|
|
expect(data).toHaveProperty('scores');
|
|
expect(data.session.id).toBe('unikat-2026');
|
|
});
|
|
|
|
it('getStore() returns seed data structure', () => {
|
|
const data = store.getStore();
|
|
expect(data.teams).toHaveLength(11);
|
|
expect(data.jurors).toHaveLength(6);
|
|
expect(data.scores).toEqual([]);
|
|
});
|
|
|
|
it('saveStore() creates a .bak backup', () => {
|
|
const data = store.getStore();
|
|
store.saveStore(data);
|
|
expect(fs.existsSync(BACKUP_FILE)).toBe(true);
|
|
});
|
|
|
|
it('saveStore() writes data atomically (persists changes)', () => {
|
|
const data = store.getStore();
|
|
data.session.status = 'active';
|
|
store.saveStore(data);
|
|
|
|
const reloaded = store.getStore();
|
|
expect(reloaded.session.status).toBe('active');
|
|
});
|
|
|
|
it('saveStore() backup contains previous data', () => {
|
|
const originalData = store.getStore();
|
|
const originalStatus = originalData.session.status;
|
|
|
|
originalData.session.status = 'active';
|
|
store.saveStore(originalData);
|
|
|
|
const backupContent = JSON.parse(fs.readFileSync(BACKUP_FILE, 'utf-8'));
|
|
expect(backupContent.session.status).toBe(originalStatus);
|
|
});
|
|
|
|
it('getSession() returns the session', () => {
|
|
const session = store.getSession();
|
|
expect(session.id).toBe('unikat-2026');
|
|
expect(session.status).toBe('setup');
|
|
expect(session.config.criteria).toHaveLength(5);
|
|
});
|
|
|
|
it('getTeams() returns all teams', () => {
|
|
const teams = store.getTeams();
|
|
expect(teams).toHaveLength(11);
|
|
expect(teams[0]).toHaveProperty('id');
|
|
expect(teams[0]).toHaveProperty('name');
|
|
expect(teams[0]).toHaveProperty('number');
|
|
});
|
|
|
|
it('getJurors() returns all jurors', () => {
|
|
const jurors = store.getJurors();
|
|
expect(jurors).toHaveLength(6);
|
|
expect(jurors[0]).toHaveProperty('id');
|
|
expect(jurors[0]).toHaveProperty('name');
|
|
expect(jurors[0]).toHaveProperty('active');
|
|
});
|
|
|
|
it('getScores() returns empty scores array initially', () => {
|
|
const scores = store.getScores();
|
|
expect(Array.isArray(scores)).toBe(true);
|
|
expect(scores).toHaveLength(0);
|
|
});
|
|
|
|
it('addScore() adds a new score', () => {
|
|
const score: Score = {
|
|
jurorId: 'finke',
|
|
teamId: 'dual',
|
|
criterionId: 'gesamtidee',
|
|
value: 8,
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
store.addScore(score);
|
|
|
|
const scores = store.getScores();
|
|
expect(scores).toHaveLength(1);
|
|
expect(scores[0].jurorId).toBe('finke');
|
|
expect(scores[0].teamId).toBe('dual');
|
|
expect(scores[0].criterionId).toBe('gesamtidee');
|
|
expect(scores[0].value).toBe(8);
|
|
});
|
|
|
|
it('addScore() updates existing score (match by jurorId+teamId+criterionId)', () => {
|
|
const score1: Score = {
|
|
jurorId: 'finke',
|
|
teamId: 'dual',
|
|
criterionId: 'gesamtidee',
|
|
value: 7,
|
|
updatedAt: '2026-05-19T10:00:00Z',
|
|
};
|
|
store.addScore(score1);
|
|
|
|
const score2: Score = {
|
|
jurorId: 'finke',
|
|
teamId: 'dual',
|
|
criterionId: 'gesamtidee',
|
|
value: 9,
|
|
updatedAt: '2026-05-19T10:01:00Z',
|
|
};
|
|
store.addScore(score2);
|
|
|
|
const scores = store.getScores();
|
|
const matching = scores.filter(
|
|
(s) =>
|
|
s.jurorId === 'finke' &&
|
|
s.teamId === 'dual' &&
|
|
s.criterionId === 'gesamtidee'
|
|
);
|
|
expect(matching).toHaveLength(1);
|
|
expect(matching[0].value).toBe(9);
|
|
expect(matching[0].updatedAt).toBe('2026-05-19T10:01:00Z');
|
|
});
|
|
|
|
it('addScore() does not affect scores for different criteria', () => {
|
|
const score1: Score = {
|
|
jurorId: 'finke',
|
|
teamId: 'dual',
|
|
criterionId: 'gesamtidee',
|
|
value: 7,
|
|
updatedAt: '2026-05-19T10:00:00Z',
|
|
};
|
|
const score2: Score = {
|
|
jurorId: 'finke',
|
|
teamId: 'dual',
|
|
criterionId: 'markt',
|
|
value: 9,
|
|
updatedAt: '2026-05-19T10:01:00Z',
|
|
};
|
|
store.addScore(score1);
|
|
store.addScore(score2);
|
|
|
|
const scores = store.getScores();
|
|
expect(scores).toHaveLength(2);
|
|
});
|
|
|
|
it('updateSession() merges partial updates', () => {
|
|
const updated = store.updateSession({
|
|
status: 'active',
|
|
startedAt: '2026-05-19T18:00:00Z',
|
|
});
|
|
expect(updated.status).toBe('active');
|
|
expect(updated.startedAt).toBe('2026-05-19T18:00:00Z');
|
|
expect(updated.id).toBe('unikat-2026');
|
|
expect(updated.config.criteria).toHaveLength(5);
|
|
});
|
|
|
|
it('updateSession() persists changes', () => {
|
|
store.updateSession({ status: 'active' });
|
|
const session = store.getSession();
|
|
expect(session.status).toBe('active');
|
|
});
|
|
|
|
it('updateJuror() updates a juror by ID', () => {
|
|
const updated = store.updateJuror('finke', { active: true });
|
|
expect(updated).not.toBeNull();
|
|
expect(updated!.active).toBe(true);
|
|
expect(updated!.name).toBe('Finke');
|
|
expect(updated!.id).toBe('finke');
|
|
});
|
|
|
|
it('updateJuror() persists changes', () => {
|
|
store.updateJuror('finke', { active: true });
|
|
const jurors = store.getJurors();
|
|
const finke = jurors.find((j) => j.id === 'finke');
|
|
expect(finke!.active).toBe(true);
|
|
});
|
|
|
|
it('updateJuror() returns null for unknown jurorId', () => {
|
|
const result = store.updateJuror('unknown-juror', { active: true });
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('initializes from seed.json if session.json does not exist', () => {
|
|
// Delete session.json
|
|
fs.unlinkSync(SESSION_FILE);
|
|
expect(fs.existsSync(SESSION_FILE)).toBe(false);
|
|
|
|
// Manually call the initialization logic by writing seed
|
|
const seedData = fs.readFileSync(SEED_FILE, 'utf-8');
|
|
fs.writeFileSync(SESSION_FILE, seedData, 'utf-8');
|
|
|
|
// Verify the store reads correctly
|
|
const data = store.getStore();
|
|
expect(data.session.id).toBe('unikat-2026');
|
|
expect(data.teams).toHaveLength(11);
|
|
expect(data.jurors).toHaveLength(6);
|
|
});
|
|
});
|