ai-code-battle/cmd/acb-indexer/src/generator.test.ts
jedarden 4bbc3f0515 Complete Phase 4: Add index builder container
- Add /api/data/export endpoint to worker-api for data export
- Create cmd/acb-indexer/ TypeScript container:
  - API client for fetching data from Worker API
  - Index generator for leaderboard, bot profiles, match index
  - File writer for outputting JSON files
  - Optional Cloudflare Pages deploy support
  - Unit tests (6 tests)
- Update PROGRESS.md to mark Phase 4 complete

Phase 4 is now complete. All exit criteria met:
- Matchmaker cron creates jobs in D1
- Workers claim and execute matches
- Replays land in R2
- Results flow into D1
- Ratings update via Glicko-2
- Leaderboard.json rebuilds automatically
- Stale job reaper recovers from worker disappearance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 08:39:47 -04:00

160 lines
4.5 KiB
TypeScript

// Index Generator Tests
import { describe, it, expect } from 'vitest';
import { IndexGenerator } from './generator.js';
import type { ExportData, ExportBot, ExportMatch } from './types.js';
function createMockData(): ExportData {
const bots: ExportBot[] = [
{
id: 'bot-1',
name: 'TestBot1',
owner_id: 'owner-1',
rating: 1500,
rating_deviation: 50,
rating_volatility: 0.06,
matches_played: 10,
matches_won: 7,
created_at: '2026-01-01T00:00:00Z',
updated_at: '2026-03-01T00:00:00Z',
health_status: 'healthy',
},
{
id: 'bot-2',
name: 'TestBot2',
owner_id: 'owner-2',
rating: 1450,
rating_deviation: 60,
rating_volatility: 0.07,
matches_played: 5,
matches_won: 2,
created_at: '2026-01-15T00:00:00Z',
updated_at: '2026-03-01T00:00:00Z',
health_status: 'healthy',
},
{
id: 'bot-3',
name: 'UnrankedBot',
owner_id: 'owner-3',
rating: 1200,
rating_deviation: 350,
rating_volatility: 0.06,
matches_played: 0,
matches_won: 0,
created_at: '2026-02-01T00:00:00Z',
updated_at: '2026-02-01T00:00:00Z',
health_status: 'unknown',
},
];
const matches: ExportMatch[] = [
{
id: 'match-1',
status: 'completed',
winner_id: 'bot-1',
turns: 50,
end_reason: 'domination',
map_id: 'map-1',
created_at: '2026-03-01T10:00:00Z',
completed_at: '2026-03-01T10:05:00Z',
participants: [
{
bot_id: 'bot-1',
player_index: 0,
score: 100,
rating_before: 1480,
rating_after: 1500,
},
{
bot_id: 'bot-2',
player_index: 1,
score: 50,
rating_before: 1470,
rating_after: 1450,
},
],
},
];
return {
bots,
matches,
rating_history: [
{
bot_id: 'bot-1',
rating: 1480,
rating_deviation: 55,
recorded_at: '2026-02-15T00:00:00Z',
},
{
bot_id: 'bot-1',
rating: 1500,
rating_deviation: 50,
recorded_at: '2026-03-01T00:00:00Z',
},
],
generated_at: '2026-03-24T08:00:00Z',
};
}
describe('IndexGenerator', () => {
it('generates leaderboard with correct rankings', () => {
const generator = new IndexGenerator(createMockData());
const leaderboard = generator.generateLeaderboard();
expect(leaderboard.updated_at).toBe('2026-03-24T08:00:00Z');
expect(leaderboard.entries).toHaveLength(2); // Only bots with matches
expect(leaderboard.entries[0].bot_id).toBe('bot-1');
expect(leaderboard.entries[0].rank).toBe(1);
expect(leaderboard.entries[0].rating).toBe(1500);
expect(leaderboard.entries[0].win_rate).toBe(70); // 7/10 * 100
});
it('generates bot directory', () => {
const generator = new IndexGenerator(createMockData());
const directory = generator.generateBotDirectory();
expect(directory.bots).toHaveLength(3);
expect(directory.bots[0].id).toBe('bot-1');
expect(directory.bots[0].name).toBe('TestBot1');
});
it('generates bot profile with rating history', () => {
const generator = new IndexGenerator(createMockData());
const profile = generator.generateBotProfile('bot-1');
expect(profile).not.toBeNull();
expect(profile!.id).toBe('bot-1');
expect(profile!.name).toBe('TestBot1');
expect(profile!.rating_history).toHaveLength(2);
expect(profile!.recent_matches).toHaveLength(1);
expect(profile!.recent_matches[0].participants[0].won).toBe(true);
});
it('returns null for non-existent bot profile', () => {
const generator = new IndexGenerator(createMockData());
const profile = generator.generateBotProfile('non-existent');
expect(profile).toBeNull();
});
it('generates match index', () => {
const generator = new IndexGenerator(createMockData());
const matchIndex = generator.generateMatchIndex();
expect(matchIndex.matches).toHaveLength(1);
expect(matchIndex.matches[0].id).toBe('match-1');
expect(matchIndex.matches[0].winner_id).toBe('bot-1');
expect(matchIndex.matches[0].participants).toHaveLength(2);
});
it('generates all indexes at once', () => {
const generator = new IndexGenerator(createMockData());
const all = generator.generateAll();
expect(all.leaderboard.entries).toHaveLength(2);
expect(all.botDirectory.bots).toHaveLength(3);
expect(all.botProfiles.size).toBe(3);
expect(all.matchIndex.matches).toHaveLength(1);
});
});