Migrate all repos into monorepo context folders

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.
This commit is contained in:
2026-06-30 20:39:52 +02:00
parent 2f2b295531
commit a5f8fb49ab
1717 changed files with 447332 additions and 0 deletions
@@ -0,0 +1,187 @@
import { describe, it, expect } from 'vitest';
import {
calculateTeamResult,
calculateRankings,
getCompletionStatus,
calculateAllResults,
} from '../aggregation.js';
import { Score, Team, TeamResult } from '../../shared/types.js';
function makeScore(jurorId: string, teamId: string, criterionId: string, value: number): Score {
return { jurorId, teamId, criterionId, value, updatedAt: new Date().toISOString() };
}
describe('calculateTeamResult', () => {
it('returns zero total and average when no scores exist', () => {
const result = calculateTeamResult('team-a', 'Team A', [], 5);
expect(result.total).toBe(0);
expect(result.average).toBe(0);
expect(result.jurorCount).toBe(0);
expect(result.complete).toBe(false);
});
it('calculates total as sum of all score values for the team', () => {
const scores: Score[] = [
makeScore('juror1', 'team-a', 'c1', 8),
makeScore('juror1', 'team-a', 'c2', 7),
makeScore('juror2', 'team-a', 'c1', 9),
makeScore('juror1', 'team-b', 'c1', 10), // different team, should be excluded
];
const result = calculateTeamResult('team-a', 'Team A', scores, 2);
expect(result.total).toBe(8 + 7 + 9);
});
it('counts only jurors who submitted all criteria', () => {
const scores: Score[] = [
makeScore('juror1', 'team-a', 'c1', 8),
makeScore('juror1', 'team-a', 'c2', 7),
makeScore('juror2', 'team-a', 'c1', 9), // only 1 of 2 criteria
];
const result = calculateTeamResult('team-a', 'Team A', scores, 2);
expect(result.jurorCount).toBe(1); // only juror1 has all 2 criteria
});
it('calculates average as total / jurorCount', () => {
const scores: Score[] = [
makeScore('juror1', 'team-a', 'c1', 8),
makeScore('juror1', 'team-a', 'c2', 6),
makeScore('juror2', 'team-a', 'c1', 10),
makeScore('juror2', 'team-a', 'c2', 4),
];
const result = calculateTeamResult('team-a', 'Team A', scores, 2);
// total = 28, jurorCount = 2, average = 14
expect(result.total).toBe(28);
expect(result.jurorCount).toBe(2);
expect(result.average).toBe(14);
});
it('marks complete when all jurors with scores have all criteria', () => {
const scores: Score[] = [
makeScore('juror1', 'team-a', 'c1', 8),
makeScore('juror1', 'team-a', 'c2', 7),
makeScore('juror2', 'team-a', 'c1', 9),
makeScore('juror2', 'team-a', 'c2', 6),
];
const result = calculateTeamResult('team-a', 'Team A', scores, 2);
expect(result.complete).toBe(true);
});
it('marks incomplete when some jurors have partial scores', () => {
const scores: Score[] = [
makeScore('juror1', 'team-a', 'c1', 8),
makeScore('juror1', 'team-a', 'c2', 7),
makeScore('juror2', 'team-a', 'c1', 9), // missing c2
];
const result = calculateTeamResult('team-a', 'Team A', scores, 2);
expect(result.complete).toBe(false);
});
});
describe('calculateRankings', () => {
it('sorts by total descending and assigns ranks', () => {
const results: TeamResult[] = [
{ teamId: 'a', teamName: 'A', total: 200, average: 0, stddev: 0, rank: 0, jurorCount: 0, complete: false },
{ teamId: 'b', teamName: 'B', total: 250, average: 0, stddev: 0, rank: 0, jurorCount: 0, complete: false },
{ teamId: 'c', teamName: 'C', total: 230, average: 0, stddev: 0, rank: 0, jurorCount: 0, complete: false },
];
const ranked = calculateRankings(results);
expect(ranked[0].teamId).toBe('b');
expect(ranked[0].rank).toBe(1);
expect(ranked[1].teamId).toBe('c');
expect(ranked[1].rank).toBe(2);
expect(ranked[2].teamId).toBe('a');
expect(ranked[2].rank).toBe(3);
});
it('assigns same rank to ties', () => {
const results: TeamResult[] = [
{ teamId: 'a', teamName: 'A', total: 250, average: 0, stddev: 0, rank: 0, jurorCount: 0, complete: false },
{ teamId: 'b', teamName: 'B', total: 240, average: 0, stddev: 0, rank: 0, jurorCount: 0, complete: false },
{ teamId: 'c', teamName: 'C', total: 240, average: 0, stddev: 0, rank: 0, jurorCount: 0, complete: false },
{ teamId: 'd', teamName: 'D', total: 230, average: 0, stddev: 0, rank: 0, jurorCount: 0, complete: false },
];
const ranked = calculateRankings(results);
expect(ranked[0].rank).toBe(1); // 250
expect(ranked[1].rank).toBe(2); // 240
expect(ranked[2].rank).toBe(2); // 240 (tie)
expect(ranked[3].rank).toBe(4); // 230
});
it('handles empty input', () => {
const ranked = calculateRankings([]);
expect(ranked).toEqual([]);
});
it('handles single team', () => {
const results: TeamResult[] = [
{ teamId: 'a', teamName: 'A', total: 100, average: 0, stddev: 0, rank: 0, jurorCount: 0, complete: false },
];
const ranked = calculateRankings(results);
expect(ranked[0].rank).toBe(1);
});
});
describe('getCompletionStatus', () => {
it('returns pending when no scores exist', () => {
expect(getCompletionStatus('juror1', 'team-a', [], 5)).toBe('pending');
});
it('returns pending when scores exist for other juror/team', () => {
const scores: Score[] = [
makeScore('juror2', 'team-a', 'c1', 8),
makeScore('juror1', 'team-b', 'c1', 7),
];
expect(getCompletionStatus('juror1', 'team-a', scores, 5)).toBe('pending');
});
it('returns partial when some but not all criteria scored', () => {
const scores: Score[] = [
makeScore('juror1', 'team-a', 'c1', 8),
makeScore('juror1', 'team-a', 'c2', 7),
];
expect(getCompletionStatus('juror1', 'team-a', scores, 5)).toBe('partial');
});
it('returns complete when all criteria scored', () => {
const scores: Score[] = [
makeScore('juror1', 'team-a', 'c1', 8),
makeScore('juror1', 'team-a', 'c2', 7),
makeScore('juror1', 'team-a', 'c3', 9),
makeScore('juror1', 'team-a', 'c4', 6),
makeScore('juror1', 'team-a', 'c5', 10),
];
expect(getCompletionStatus('juror1', 'team-a', scores, 5)).toBe('complete');
});
});
describe('calculateAllResults', () => {
it('calculates results for all teams with rankings', () => {
const teams: Team[] = [
{ id: 'team-a', number: 1, name: 'Team A', description: '' },
{ id: 'team-b', number: 2, name: 'Team B', description: '' },
];
const scores: Score[] = [
makeScore('juror1', 'team-a', 'c1', 8),
makeScore('juror1', 'team-a', 'c2', 7),
makeScore('juror1', 'team-b', 'c1', 10),
makeScore('juror1', 'team-b', 'c2', 9),
];
const results = calculateAllResults(scores, teams, 2);
expect(results[0].teamId).toBe('team-b'); // higher total (19)
expect(results[0].rank).toBe(1);
expect(results[0].total).toBe(19);
expect(results[1].teamId).toBe('team-a'); // lower total (15)
expect(results[1].rank).toBe(2);
expect(results[1].total).toBe(15);
});
it('handles empty scores', () => {
const teams: Team[] = [
{ id: 'team-a', number: 1, name: 'Team A', description: '' },
];
const results = calculateAllResults([], teams, 5);
expect(results).toHaveLength(1);
expect(results[0].total).toBe(0);
expect(results[0].rank).toBe(1);
});
});
@@ -0,0 +1,71 @@
import { describe, it, expect } from 'vitest';
import { shouldAcceptScore, resolveConflict } from '../conflict.js';
import { Score } from '../../shared/types.js';
function makeScore(overrides: Partial<Score> = {}): Score {
return {
jurorId: 'finke',
teamId: 'dual',
criterionId: 'gesamtidee',
value: 7,
updatedAt: '2026-05-19T18:00:00.000Z',
...overrides,
};
}
describe('shouldAcceptScore', () => {
it('accepts when no existing score', () => {
const incoming = makeScore();
const result = shouldAcceptScore(incoming, undefined);
expect(result).toEqual({ accept: true });
});
it('accepts when incoming timestamp is newer', () => {
const existing = makeScore({ updatedAt: '2026-05-19T18:00:00.000Z' });
const incoming = makeScore({ updatedAt: '2026-05-19T18:01:00.000Z', value: 9 });
const result = shouldAcceptScore(incoming, existing);
expect(result).toEqual({ accept: true });
});
it('rejects when incoming timestamp is older', () => {
const existing = makeScore({ updatedAt: '2026-05-19T18:01:00.000Z' });
const incoming = makeScore({ updatedAt: '2026-05-19T18:00:00.000Z', value: 3 });
const result = shouldAcceptScore(incoming, existing);
expect(result).toEqual({ accept: false, reason: 'Newer score already exists' });
});
it('rejects when timestamps are equal', () => {
const existing = makeScore({ updatedAt: '2026-05-19T18:00:00.000Z' });
const incoming = makeScore({ updatedAt: '2026-05-19T18:00:00.000Z', value: 5 });
const result = shouldAcceptScore(incoming, existing);
expect(result).toEqual({ accept: false, reason: 'Newer score already exists' });
});
});
describe('resolveConflict', () => {
it('accepts when force is true regardless of timestamps', () => {
const existing = makeScore({ updatedAt: '2026-05-19T18:01:00.000Z' });
const incoming = makeScore({ updatedAt: '2026-05-19T18:00:00.000Z', value: 2 });
const result = resolveConflict(incoming, existing, true);
expect(result).toEqual({ accept: true });
});
it('delegates to shouldAcceptScore when force is false', () => {
const existing = makeScore({ updatedAt: '2026-05-19T18:01:00.000Z' });
const incoming = makeScore({ updatedAt: '2026-05-19T18:00:00.000Z', value: 2 });
const result = resolveConflict(incoming, existing, false);
expect(result).toEqual({ accept: false, reason: 'Newer score already exists' });
});
it('delegates to shouldAcceptScore when force is undefined', () => {
const incoming = makeScore();
const result = resolveConflict(incoming, undefined);
expect(result).toEqual({ accept: true });
});
it('accepts with force even when no existing score', () => {
const incoming = makeScore();
const result = resolveConflict(incoming, undefined, true);
expect(result).toEqual({ accept: true });
});
});
@@ -0,0 +1,296 @@
import { describe, it, expect, beforeEach, afterAll } from 'vitest';
import express from 'express';
import fs from 'fs';
import path from 'path';
import { registerRoutes } from '../routes.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');
function createApp() {
const app = express();
app.use(express.json());
registerRoutes(app);
return app;
}
function resetStore() {
const seedData = fs.readFileSync(SEED_FILE, 'utf-8');
fs.writeFileSync(SESSION_FILE, seedData, 'utf-8');
if (fs.existsSync(BACKUP_FILE)) {
fs.unlinkSync(BACKUP_FILE);
}
}
// Helper to make requests without supertest (using native fetch with app.listen)
// We'll use a simple approach: call the route handlers directly via the express app
import { type Server } from 'http';
let server: Server;
let baseUrl: string;
beforeEach(() => {
resetStore();
});
afterAll(() => {
resetStore();
if (server) server.close();
});
describe('server/routes', () => {
const app = createApp();
// Start server on a random port
beforeEach(async () => {
await new Promise<void>((resolve) => {
if (server) server.close();
server = app.listen(0, () => {
const addr = server.address();
if (addr && typeof addr !== 'string') {
baseUrl = `http://localhost:${addr.port}`;
}
resolve();
});
});
});
afterAll(() => {
if (server) server.close();
});
// ─── Session Endpoints ───────────────────────────────────────────────
describe('GET /api/session', () => {
it('returns current session state', async () => {
const res = await fetch(`${baseUrl}/api/session`);
expect(res.status).toBe(200);
const data = await res.json();
expect(data.id).toBe('unikat-2026');
expect(data.status).toBe('setup');
});
});
describe('POST /api/session/start', () => {
it('starts the session', async () => {
const res = await fetch(`${baseUrl}/api/session/start`, { method: 'POST' });
expect(res.status).toBe(200);
const data = await res.json();
expect(data.status).toBe('active');
expect(data.startedAt).toBeTruthy();
});
it('rejects if already active', async () => {
await fetch(`${baseUrl}/api/session/start`, { method: 'POST' });
const res = await fetch(`${baseUrl}/api/session/start`, { method: 'POST' });
expect(res.status).toBe(400);
const data = await res.json();
expect(data.error).toContain('already active');
});
});
describe('POST /api/session/end', () => {
it('ends an active session', async () => {
await fetch(`${baseUrl}/api/session/start`, { method: 'POST' });
const res = await fetch(`${baseUrl}/api/session/end`, { method: 'POST' });
expect(res.status).toBe(200);
const data = await res.json();
expect(data.status).toBe('ended');
expect(data.endedAt).toBeTruthy();
});
it('rejects if not active', async () => {
const res = await fetch(`${baseUrl}/api/session/end`, { method: 'POST' });
expect(res.status).toBe(400);
const data = await res.json();
expect(data.error).toContain('not active');
});
});
// ─── Team and Juror Endpoints ────────────────────────────────────────
describe('GET /api/teams', () => {
it('returns all 11 teams', async () => {
const res = await fetch(`${baseUrl}/api/teams`);
expect(res.status).toBe(200);
const data = await res.json();
expect(data).toHaveLength(11);
expect(data[0]).toHaveProperty('id');
expect(data[0]).toHaveProperty('name');
});
});
describe('GET /api/jurors', () => {
it('returns all 6 jurors', async () => {
const res = await fetch(`${baseUrl}/api/jurors`);
expect(res.status).toBe(200);
const data = await res.json();
expect(data).toHaveLength(6);
expect(data[0]).toHaveProperty('id');
expect(data[0]).toHaveProperty('name');
});
});
// ─── Score Endpoints ─────────────────────────────────────────────────
describe('POST /api/scores', () => {
it('rejects when session is not active (403)', async () => {
const res = await fetch(`${baseUrl}/api/scores`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jurorId: 'finke', teamId: 'dual', criterionId: 'gesamtidee', value: 8 }),
});
expect(res.status).toBe(403);
});
it('accepts a valid score when session is active', async () => {
await fetch(`${baseUrl}/api/session/start`, { method: 'POST' });
const res = await fetch(`${baseUrl}/api/scores`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jurorId: 'finke', teamId: 'dual', criterionId: 'gesamtidee', value: 8 }),
});
expect(res.status).toBe(200);
const data = await res.json();
expect(data.jurorId).toBe('finke');
expect(data.value).toBe(8);
});
it('rejects value out of range (400)', async () => {
await fetch(`${baseUrl}/api/session/start`, { method: 'POST' });
const res = await fetch(`${baseUrl}/api/scores`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jurorId: 'finke', teamId: 'dual', criterionId: 'gesamtidee', value: 11 }),
});
expect(res.status).toBe(400);
});
it('rejects unknown jurorId (400)', async () => {
await fetch(`${baseUrl}/api/session/start`, { method: 'POST' });
const res = await fetch(`${baseUrl}/api/scores`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jurorId: 'unknown', teamId: 'dual', criterionId: 'gesamtidee', value: 5 }),
});
expect(res.status).toBe(400);
});
it('rejects unknown teamId (400)', async () => {
await fetch(`${baseUrl}/api/session/start`, { method: 'POST' });
const res = await fetch(`${baseUrl}/api/scores`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jurorId: 'finke', teamId: 'unknown', criterionId: 'gesamtidee', value: 5 }),
});
expect(res.status).toBe(400);
});
it('rejects unknown criterionId (400)', async () => {
await fetch(`${baseUrl}/api/session/start`, { method: 'POST' });
const res = await fetch(`${baseUrl}/api/scores`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jurorId: 'finke', teamId: 'dual', criterionId: 'unknown', value: 5 }),
});
expect(res.status).toBe(400);
});
});
describe('GET /api/scores', () => {
it('returns all scores', async () => {
const res = await fetch(`${baseUrl}/api/scores`);
expect(res.status).toBe(200);
const data = await res.json();
expect(Array.isArray(data)).toBe(true);
});
});
describe('GET /api/scores/:jurorId', () => {
it('returns scores for a specific juror', async () => {
// Add a score first
await fetch(`${baseUrl}/api/session/start`, { method: 'POST' });
await fetch(`${baseUrl}/api/scores`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jurorId: 'finke', teamId: 'dual', criterionId: 'gesamtidee', value: 7 }),
});
const res = await fetch(`${baseUrl}/api/scores/finke`);
expect(res.status).toBe(200);
const data = await res.json();
expect(data).toHaveLength(1);
expect(data[0].jurorId).toBe('finke');
});
});
// ─── Results and Progress Endpoints ──────────────────────────────────
describe('GET /api/results', () => {
it('returns aggregated results for all teams', async () => {
const res = await fetch(`${baseUrl}/api/results`);
expect(res.status).toBe(200);
const data = await res.json();
expect(data).toHaveLength(11);
expect(data[0]).toHaveProperty('teamId');
expect(data[0]).toHaveProperty('total');
expect(data[0]).toHaveProperty('average');
expect(data[0]).toHaveProperty('rank');
});
});
describe('GET /api/progress', () => {
it('returns juror completion matrix', async () => {
const res = await fetch(`${baseUrl}/api/progress`);
expect(res.status).toBe(200);
const data = await res.json();
expect(data).toHaveProperty('finke');
expect(data.finke).toHaveProperty('completed');
expect(data.finke).toHaveProperty('total');
expect(data.finke.total).toBe(11);
});
it('does not include individual score values', async () => {
// Add a score
await fetch(`${baseUrl}/api/session/start`, { method: 'POST' });
await fetch(`${baseUrl}/api/scores`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jurorId: 'finke', teamId: 'dual', criterionId: 'gesamtidee', value: 8 }),
});
const res = await fetch(`${baseUrl}/api/progress`);
const data = await res.json();
const json = JSON.stringify(data);
// Should not contain any score value or the word "value"
expect(json).not.toContain('"value"');
expect(json).not.toContain('"scores"');
});
});
describe('GET /api/export/csv', () => {
it('returns CSV with correct headers', async () => {
const res = await fetch(`${baseUrl}/api/export/csv`);
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toContain('text/csv');
expect(res.headers.get('content-disposition')).toContain('attachment');
const csv = await res.text();
const lines = csv.split('\n');
expect(lines[0]).toContain('Team');
expect(lines[0]).toContain('Total');
expect(lines[0]).toContain('Average');
expect(lines[0]).toContain('Rank');
});
it('has one row per team plus header', async () => {
const res = await fetch(`${baseUrl}/api/export/csv`);
const csv = await res.text();
const lines = csv.split('\n');
// 1 header + 11 team rows
expect(lines).toHaveLength(12);
});
});
});
@@ -0,0 +1,229 @@
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);
});
});